实验一 流水灯—GPIO输出

一.实验目标

  1. 编程控制D4-D7四盏LED灯间隔1s左右自右向左顺次点亮,实现流水灯效果;

  2. 通过本实验掌握Keil
    5新建工程、使用STM32库函数编写应用程序、将程序下载至实验板的步骤、方法;

  3. 通过本实验掌握STM32的GPIO外设的基本概念,以及对其输出进行简单操作的基本方法。

二.知识储备及设计思路

在配置GPIO口时,我们有必要了解GPIO的功能和特性。

STM32F407通用GPIO简介

每个通用 I/O 端口包括 4 个 32位配置寄存器(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR)、2 个 32位数据寄存器(GPIOx_IDR 和 GPIOx_ODR)、1 个 32 位置位/复位寄存器(GPIOx_BSRR)、1 个 32 位锁定寄存器 (GPIOx_LCKR) 和 2 个 32位复用功能选择寄存器(GPIOx_AFRH 和GPIOx_AFRL)。

GPIO 主要特性

● 受控 I/O 多达 16 个

● 输出状态:推挽或开漏 + 上拉/下拉

● 从输出数据寄存器 (GPIOx_ODR) 或外设(复用功能输出)输出数据

● 可为每个 I/O 选择不同的速度

● 输入状态:浮空、上拉/下拉、模拟

● 将数据输入到输入数据寄存器 (GPIOx_IDR) 或外设(复用功能输入)

● 置位和复位寄存器 (GPIOx_BSRR),对 GPIOx_ODR 具有按位写权限

● 锁定机制 (GPIOx_LCKR),可冻结 I/O 配置

● 模拟功能

● 复用功能输入/输出选择寄存器(一个 I/O 最多可具有 16 个复用功能)

● 快速翻转,每次翻转最快只需要两个时钟周期

● 引脚复用非常灵活,允许将 I/O 引脚用作 GPIO 或多种外设功能中的一种

GPIO 功能描述

根据数据手册中列出的每个 I/O 端口的特性,可通过软件将通用 I/O (GPIO)
端口的各个端口 位分别配置为多种模式:

● 输入浮空

● 输入上拉

● 输入下拉

● 模拟功能

● 具有上拉或下拉功能的开漏输出

● 具有上拉或下拉功能的推挽输出

● 具有上拉或下拉功能的复用功能推挽

● 具有上拉或下拉功能的复用功能开漏

5V容忍I/O端口基本结构:

每个 I/O 端口位均可自由编程,但 I/O 端口寄存器必须按 32
位字、半字或字节进行访问。 GPIOx_BSRR 寄存器旨在实现对 GPIO ODR
寄存器进行原子读取/修改访问。这样便可确保
在读取和修改访问之间发生中断请求也不会有问题。

接下来我们详细介绍IO配置常用的8个寄存器:
MODER、OTYPER、OSPEEDR、PUPDR、ODR、IDR
、AFRH和AFRL。同时讲解对应的HAL库配置方法。

首先看MODER寄存器,该寄存器是GPIO端口模式控制寄存器,用于控制GPIOx(STM32F4最多有9组IO,分别用大写字母表示,即x=A/B/C/D/E/F/G/H/I,下同)的工作模式,该寄存器各位描述如表所示:

该寄存器各位在复位后,一般都是0(个别不是0,比如JTAG占用的几个IO口),也就是默认条件下一般是输入状态的。每组IO下有16个IO口,该寄存器共32位,每2个位控制1个IO。

OTYPER寄存器,用于控制GPIOx的输出类型,该寄存器各位描述如表所示:

该寄存器仅用于输出模式,在输入模式(MODER[1:0]=00/11时)下不起作用。该寄存器低16位有效,每一个位控制一个IO口。设置为0是推挽输出,设置为1是开漏输出。复位后,该寄存器值均为0,也就是在输出模式下IO口默认为推挽输出。

OSPEEDR寄存器,该寄存器用于控制GPIOx的输出速度,该寄存器各位描述见表:

该寄存器也仅用于输出模式,在输入模式(MODER[1:0]=00/11时)下不起作用。该寄存器每2个位控制一个IO口,复位后,该寄存器值一般为0。

PUPDR寄存器,该寄存器用于控制GPIOx的上拉/下拉,该寄存器各位描述见表:

该寄存器每2个位控制一个IO口,用于设置上下拉,这里提醒大家,STM32F1是通过ODR寄存器控制上下拉的,而STM32F4则由单独的寄存器PUPDR控制上下拉,使用起来更加灵活。复位后,该寄存器值一般为0。

ODR寄存器,该寄存器用于控制GPIOx的输出电平,该寄存器各位描述见表:

接下来我们看看另一个非常重要的寄存器BSRR,它叫置位/复位寄存器。该寄存器和ODR寄存器具有类似的作用,都可以用来设置GPIO端口的输出位是1还是0。该寄存器各位描述见表:

对于低16位(0-15),我们往相应的位写1,那么对应的IO口会输出高电平,往相应的位写0,对IO口没有任何影响。高16位(16-31)作用刚好相反,对相应的位写1会输出低电平,写0没有任何影响。也就是说,对于BSRR寄存器,你写0的话,对IO口电平是没有任何影响的。我们要设置某个IO口电平,只需要相关位设置为1即可。而ODR寄存器,我们要设置某个IO口电平,我们首先需要读出来ODR寄存器的值,然后对整个ODR寄存器重新赋值来达到设置某个或者某些IO口的目的,而BSRR寄存器,我们就不需要先读,而是直接设置即可,这在多任务实时操作系统中作用很大。

我们通过提供的操作函数HAL_GPIO_WritePin能够配置IO.

void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

IDR寄存器,该寄存器用于读取GPIOx的输入数据,该寄存器各位描述见表:

该寄存器用于读取某个IO的电平,如果对应的位为0(IDRy=0),则说明该IO输入的是低电平,如果是1(IDRy=1),则表示输入的是高电平。HAL库操作该寄存器读取IO输入数据相关函数:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

复用功能选择寄存器(AFRH
和AFRL),这两个寄存器是用来设置IO口的复用功能的。实际上,在我们调用函数HAL_GPIO_Init的时候,如果我们设置了初始化结构体成员变量Mode为复用模式,同时设置了Alternate的值,那么会在该函数内部自动设置这两个寄存器的值,达到设置端口复用映射的目的。

GPIO相关的函数和定义分布在HAL库文件stm32f4xx_hal_gpio.c和头文件stm32f4xx_hal_gpio.h文件中。

在这里用到了HAL_GPIO_Init函数,用于配置GPIO口,为了用户使用更加清晰,我们这里分析。

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

该函数有两个参数,第一个参数是用来指定需要初始化的GPIO对应的GPIO组,取值范围为GPIOA~GPIOK。第二个参数为初始化参数结构体指针,结构体类型为GPIO_InitTypeDef。下面我们分别看看这两个结构体的定义。

在这个地方用到GPIO_TypeDef结构体,用于定义GPIO的寄存器,我们前面进行初始话设定的值,实际通过函数HAL_GPIO_Init写入这些寄存器中。

typedef struct { __IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */ __IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */ __IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */ __IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */ __IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */ __IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */ __IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */ __IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */ __IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */ } GPIO_TypeDef;

在这个地方用到GPIO_InitTypeDef结构体,用于定义GPIO功能,我们前面进行初始话设定的值,实际通过函数HAL_GPIO_Init写入这些寄存器中。

typedef struct { uint32_t Pin; /*!< Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ uint32_t Mode; /*!< Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIO_mode_define */ uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.This parameter can be a value of @ref GPIO_pull_define */ uint32_t Speed; /*!< Specifies the speed for the selected pins. This parameter can be a value of @ref GPIO_speed_define */ uint32_t Alternate; /*!< Peripheral to be connected to the selected pins. This parameter can be a value of @ref GPIO_Alternate_function_selection */ }GPIO_InitTypeDef;

三.引脚说明与硬件连接


3.1.1 LED灯硬件连接图

图3.1.1为LED灯硬件连接图。其中LED灯D4-D5分别连接至GPIOF的6、7引脚,D6-D7连接至GPIOI的10、11引脚。通过配置这4个引脚并输出高低电平,来驱动对应的LED灯并控制其亮灭。

LED灯为高电平驱动点亮,通过控制相对应的GPIO口分别输出高(输出“1”)、低(输出“0”)电平便可控制相对应的LED灯实现点亮、熄灭,因此,流水灯的实现便是数据“1”的“流动”。

四.程序设计

要实现四盏LED灯自右向左的流水灯效果,则需将LED灯按照“0001”这样一组数据,每经过一段延时,整体进行一次循环左移,并通过相对应的GPIO口输出LED灯的引脚状态,从而显示以实现流水灯效果。

在主函数main的while循环中,我们只进行IO高低电平切换。

while (1) { HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_SET);//PF6设置为高电平 HAL_GPIO_WritePin(GPIOF, GPIO_PIN_7, GPIO_PIN_RESET);//PF7设置为低电平 HAL_GPIO_WritePin(GPIOI, GPIO_PIN_10, GPIO_PIN_RESET);//PI10设置为低电平 HAL_GPIO_WritePin(GPIOI, GPIO_PIN_11, GPIO_PIN_RESET);//PI11设置为低电平 HAL_Delay(1000); //HAL库自带延时函数,延时ms HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_7, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_10, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_11, GPIO_PIN_RESET); HAL_Delay(1000); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_7, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_10, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_11, GPIO_PIN_RESET); HAL_Delay(1000); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_7, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_10, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_11, GPIO_PIN_SET); HAL_Delay(1000); }

main函数代码中while(1)循环中代码。其中,代码执行顺序:

控制PF6管脚输出高电平,控制其亮;

控制PF7/PI10/PI11管脚输出低电平,控制其灭;

经过HAL_Delay延时函数1s的延时,然后进入下一状态,继续循环,从而实现流水灯效果。

接下来,将依次介绍main函数中几个主要子函数的功能与内容。

子函数MX_GPIO_Init代码,是stm32cube自动生成的代码,其功能为对LED灯对应的GPIO引脚进行初始化配置。其中,代码分别为:

使能对应GPIO的时钟。

拉低对应的各个引脚使LED灯全部熄灭,准备新的引脚状态的写入,点亮流水灯。

配置对应引脚为输出模式,输出状态为推挽输出,输出速度为低速,无上拉或下拉,并通过函数HAL_GPIO_Init完成配置。

void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOI_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOH_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOI, GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOF, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET); /*Configure GPIO pins : PI10 PI11 */ GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOI, &GPIO_InitStruct); /*Configure GPIO pins : PF6 PF7 */ GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); }

函数HAL_Delay延时是通过SysTick—系统定时器实现的. SysTick属于CM4
内核中的一个外设,内嵌在NVIC 中。系统定时器是一个24bit
的向下递减的计数器,计数器每计数一次的时间为1/SYSCLK,一般我们设置系统时钟SYSCLK
等于168M。当重装载数值寄存器的值递减到0
的时候,系统定时器就产生一次中断,以此循环往复。

因为SysTick 是属于CM4 内核的外设,所以所有基于CM4 内核的单片机都具有这个

系统定时器,使得软件在CM4 单片机中可以很容易的移植。系统定时器一般用于操作系统,

用于产生时基,维持操作系统的心跳。

HAL_Delay函数输入延时时间,然后通过HAL_GetTick()记录当前tickstart时间,就一致停在while循环,直到(HAL_GetTick()
- tickstart) < wait 达到延时时间,退出while循环。

__weak void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); uint32_t wait = Delay; /* Add a freq to guarantee minimum wait */ if (wait < HAL_MAX_DELAY) { wait += (uint32_t)(uwTickFreq); } while((HAL_GetTick() - tickstart) < wait) { } }

SysTick—系统定时器自带的HAL_Delay不能实现us的延时,需要修改代码,放在后面单独章节详细介绍,这里就不再赘述,大家知道HAL_Delay能够实现ms的延时。

HAL库的延时函数有一个局限性,在中断服务函数中使用HAL_Delay会引起混乱,因为它是通过中断方式实现,而Systick的中断优先级是最低的,所以在中断中运行HAL_Delay会导致延时出现严重误差。

五.实验结果

3.1.5 流水灯实验结果

将编译好的代码下载到实验板中,按下复位键,LED灯D4-D7自右向左每隔1s顺次点亮,实现流水灯效果。图3.1.5为移位显示过程的截图。

六.思考与拓展

  1. 思考GPIO为何要配置成“推挽输出”?

  2. 自己动手编程实现渐快、渐慢的流水灯效果。

七.STM32CubeMx配置GPIO