26.3.14 ZD-EP49 [GPIO寄存器] GPIO寄存器介绍及其配置流程
关于GPIO的寄存器,主要就是了解相关寄存器种类及其作用。先看看寄存器的定义:寄存器(Register),是中央处理器内部用于暂存指令、数据和地址的高速存储部件,通常由触发器或锁存器构成。运用在GPIO(通用输入输出端口)主要用于确定GPIO的工作模式和储存输入输出信号。下面以F407xx系列为例介绍GPIO相关的寄存器。
1 GPIO寄存器介绍
STM32F4每组(即A~I)通用GPIO口各有10个32位寄存器,包括:
4 个 32 位配置寄存器(MODER、OTYPER、OSPEEDR 和 PUPDR)
2 个 32 位数据寄存器(IDR 和 ODR)
1 个 32 位置位/复位寄存器 (BSRR)
1 个 32 位锁定寄存器 (LCKR)
2 个 32 位复用功能选择寄存器(AFRH 和 AFRL)
其中本章将介绍的为前七种,LCKR主要用于锁定寄存器储存的数据,用处不大,而AFRH和AFRL用于复用功能,这里暂时不做介绍。
1.1 GPIO 端口模式寄存器 (GPIOx_MODER) (x =A..I)
顾名思义,即用于设置GPIO的工作模式,其描述如下:

每组GPIO下有16个IO口,对应到该寄存器,即每两个位控制一个IO口,因此通过00,01,10,11四种状态来对应不同的模式,需要注意的是,00对应的输入模式还要通过待会讲到的上下拉寄存器来控制不同的输入模式,01对应的输出模式还要通过输出类型寄存器来控制开漏或推挽输出。
这里的[1:0]指的是最高位为1,最低位为0,共两个位,这里的位域是相对的,属于[31:0]位域下
用复位值举例说明一下这样的配置值代表什么意思。比如 GPIOA 的复位值是 0xABFF FFFF,低 16 位都是 1,也就是 PA0~PA7 默认都是模拟模式。高 16 位的值是 0xABFF,也就是 PA8~PA12 默认是模拟模式,PA13\PA14\PA15 则默认是复用功能模式。而 GPIOB 的复位值是 0xFFFF FEBF,只有 PB3 默认是复用功能模式,其他默认都是模拟模式。这四个默认是复用功能模式的 IO 口都是 JTAG 功能对应的 IO 口。
一般用十六进制对位值进行描述,每个十六进制数字对应四个二进制位,如0xABFF FFFF的后四位十六进制数字对应的是十六位二进制位,切均为1,即后十六位为1111 1111 1111 1111,因此PA0~PA7(即MODER0~MODER7)对应的模式就位模拟模式,而十六进制的前四位对应的前十六位二进制位为1010 1011 1111 1111,因此PA8~PA12均为默认模式,而PA13/14/15均为复用功能模式。
1.2 GPIO 端口输出类型寄存器 (GPIOx_OTYPER) (x = A..I)
如图:

OTYPER,即Output Type Register,用于控制输出模式,在MODER[1:0]=00/11的输入模式时不起作用,该寄存器默认复位值均为0,即默认均为推挽输出。
1.3 GPIO 端口输出速度寄存器 (GPIOx_OSPEEDR) (x = A..I)
如图:

OSPEEDR,即Output Speed Register,用于控制输出速度,在MODER[1:0]=00/11的输入模式时不起作用。
1.4 GPIO 端口上拉/下拉寄存器 (GPIOx_PUPDR) (x = A..I)
如图:

PUPDR,即Pull-Up/Down Register,用于设置上下拉电阻,复位值一般为0,即浮空状态。
以上四个寄存器用于配置GPIO的模式和状态,通关不同的组合得到:

1.5 端口输入数据寄存器(IDR)
如图:

IDR,即Input Date Register,为只读寄存器,器用于获取 GPIOx 的输入高低电平,低16位有效,高16位保留,并且只能以 16 位的形式读出。读出的值为对应 IO 口的状态。
1.6 端口输出数据寄存器(ODR)
如图:

ODR,即Output Date Register,该寄存器用于控制 GPIOx 的输出高电平或者低电平,低16位有效,高16位保留,当 CPU 写访问该寄存器,如果对应的某位写 0(ODRy=0),则表示设置该 IO 口输出的是低电平,如果写 1(ODRy=1),则表示设置该 IO 口输出的是高电平,y=0~15。
1.7 端口置位/复位寄存器(BSRR)

BSRR,即Bit Set/Reset Register,该寄存器也用于控制GPIOx的输出是高电平还是低电平。
为什么有了 ODR 寄存器,还要这个 BDRR 寄存器呢?我们先看看 BSRR 的寄存器描述,首先 BSRR 是只写权限,而 ODR 是可读可写权限。BSRR 寄存器 32 位有效,对于低 16 位(0-15),我们往相应的位写 1(BSy=1),那么对应的 IO 口会输出高电平,往相应的位写 0(BSy=0),对 IO 口没有任何影响,高 16 位(16-31)作用刚好相反,对相应的位写 1(BRy=1)会输出低电平,写 0(BRy=0)没有任何影响,y=0~15。
也就是说,对于 BSRR 寄存器,你写 0 的话,对 IO 口电平是没有任何影响的。我们要设置某个 IO 口电平,只需要相关位设置为 1 即可。而 ODR 寄存器,我们要设置某个 IO 口电平,我们首先需要读出来 ODR 寄存器的值,然后对整个 ODR 寄存器重新赋值来达到设置某个或者某些 IO 口的目的,而 BSRR 寄存器,我们就不需要先读,而是直接设置即可,这在多任务实时操作系统中作用很大。BSRR 寄存器还有一个好处,就是 BSRR 寄存器改变引脚状态的时候,不会被中断打断,而 ODR 寄存器有被中断打断的风险。
简单来说,ODR要经历 读->改->写的过程,若程序中断,该过程有可能被打断从而使程序发生错误,而BSRR只经历写的过程,不会被程序中断而打断,因此往往更推荐通过BSRR来进行输出。
2 GPIO寄存器配置函数
我们先来看通用外设的驱动模型:

在这里GPIO用到的有初始化,读和写函数三个步骤,那么我们从初始化开始:
2.1 HAL_GPIO_Init()函数
该函数原型如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);HAL_GPIO_Init()函数声明
其中涉及到两个结构体GPIO_TypeDef和GPIO_InitTypeDef
TypeDef下定义的主要是那10个寄存器,如下:
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_TypeDef定义
重点不在于该结构体的内容,而是形参GPIOx,这里的x就是对应芯片的GPIO组号,如GPIOA。这个形参并不是可以随便定义的,它必须按照GPIOx(x=A~G)的形式来命名,这是因为每个GPIOx都对应定义了一个宏,如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)GPIOx宏定义
其中GPIOx_BASE对应的是一个地址,因此我们称此处的宏为GPIO寄存器基地址,只有形参输入的是对应的GPIOx,才能指向对应的GPIOx的地址并进行相应的操作。
虽然这里标到了K,但实际上F407系列只有7组IO口,即只能到G。
InitTypeDef下定义的是GPIO的模式和状态,如下:
typedef struct
{
uint32_t Pin; /* 引脚号 */
uint32_t Mode; /* 模式设置 */
uint32_t Pull; /* 上拉下拉设置 */
uint32_t Speed; /* 速度设置 */
uint32_t Alternate; /* 复用功能 */
} GPIO_InitTypeDef;GPIO_InitTypeDef定义
- Pin表示引脚号,从GPIO_PIN_0到GPIO_PIN_15,另有GPIO_PIN_ALL(全选所有针脚)和GPIO_PIN_MASK(与ALL等价)。
- Mode表示GPIO模式, 有以下选择:
#define GPIO_MODE_INPUT (0x00000000U) /* 输入模式 */
#define GPIO_MODE_OUTPUT_PP (0x00000001U) /* 推挽输出 */
#define GPIO_MODE_OUTPUT_OD (0x00000011U) /* 开漏输出 */
#define GPIO_MODE_AF_PP (0x00000002U) /* 推挽式复用 */
#define GPIO_MODE_AF_OD (0x00000012U) /* 开漏式复用 */
#define GPIO_MODE_AF_INPUT GPIO_MODE_INPUT
#define GPIO_MODE_ANALOG (0x00000003U) /* 模拟模式 */
#define GPIO_MODE_IT_RISING (0x11110000U) /* 外部中断,上升沿触发检测 */
#define GPIO_MODE_IT_FALLING (0x11210000U) /* 外部中断,下降沿触发检测 */
/* 外部中断,上升和下降双沿触发检测 */
#define GPIO_MODE_IT_RISING_FALLING (0x11310000U)
#define GPIO_MODE_EVT_RISING (0x11120000U) /* 外部事件,上升沿触发检测 */
#define GPIO_MODE_EVT_FALLING (0x11220000U) /* 外部事件,下降沿触发检测 */
/* 外部事件,上升和下降双沿触发检测 */
#define GPIO_MODE_EVT_RISING_FALLING (0x11320000U)GPIO模式定义宏
- Pull表示上下拉电阻,有以下选择:
#define GPIO_NOPULL (0x00000000U) /* 无上下拉 */
#define GPIO_PULLUP (0x00000001U) /* 上拉 */
#define GPIO_PULLDOWN (0x00000002U) /* 下拉 */GPIO上下拉电阻定义宏
- Speed表示输出速度,有以下选择:
#define GPIO_SPEED_FREQ_LOW (0x00000002U) /* 低速 */
#define GPIO_SPEED_FREQ_MEDIUM (0x00000001U) /* 中速 */
#define GPIO_SPEED_FREQ_HIGH (0x00000003U) /* 高速 */GPIO输出速度定义宏
- Alternal表示复用功能,不同的 GPIO 口可以复用的功能不同,具体可参考数据手册《STM32F407ZGT6.pdf》。复用功能的选择在 stm32f4xx_hal_gpio_ex.h 文件里进行了定义,后面具体用到了,我们在进行讲解。
2.2 HAL_GPIO_WritePin()函数
该函数用于向引脚写入信号,原型如下:
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx,
uint16_t GPIO_Pin,
GPIO_PinState PinState);HAL_GPIO_WritePin()函数声明
共有三个形参,GPIOx表示端口号,范围A~G;GPIO_Pin表示引脚号,范围0~15;PinState表示输出状态,有GPIO_PIN_SET表示高电平,GPIO_PIN_RESET表示低电平。
2.3 HAL_GPIO_TogglePin()函数
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);HAL_GPIO_TogglePin()函数
共有两个形参,GPIOx表示端口号,GPIO_Pin表示引脚号。
3 GPIO配置步骤(时钟使能函数)
如图:

2,3的三个函数我们已经介绍完了,下面回到最开始,介绍GPIO使能时钟函数__HAL_RCC_GPIOx_CLK_ENABEL()。
我们先来看这个函数是怎么定义的:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)__HAL_RCC_GPIOA_CLK_ENABLE()函数定义
我们来逐步分析:
因函数为宏定义,为使函数能在任意场景下都能被正常调用执行,通过do{}while()语句包裹来执行,其中while(0U)表明该函数只执行一次。
通过__IO(实际宏定义为volatile,易变变量类型,表明该变量可能会被程序控制流以外的环境改变,防止该变量被编译器优化)定义了一个32位的临时变量tmpreg用于储存后续读取到的寄存器状态。
调用SIT_BIT函数,该函数定义如下:
#define SET_BIT(REG, BIT) ((REG) |= (BIT))SIT_BIT函数定义
通过按位或且赋值给REG赋值,这里我们回到原函数来进行讲解。
SET_BIT(RCC->AHB1ENR,RCC_AHB1ENR_GPIOAEN),整个函数操作翻译过来就是 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN,其中RCC->AHB1ENR表示的是RCC下的AHB1外设时钟使能寄存器,GPIOANE表示GPIOA时钟使能
首先通过函数跳转我们能够得到以下关于RCC_AHB1ENR_GPIOAEN的定义:
#define RCC_AHB1ENR_GPIOAEN_Pos (0U)
#define RCC_AHB1ENR_GPIOAEN_Msk (0x1UL << RCC_AHB1ENR_GPIOAEN_Pos) /*!< 0x00000001 */
#define RCC_AHB1ENR_GPIOAEN RCC_AHB1ENR_GPIOAEN_MskRCC_AHB1ENR_GPIOAEN定义
可知RCC_AHB1ENR_GPIOAEN表示的就是 1 << 0,即给第0位赋值为1,那么我们再看到RCC->AHB1ENR,根据参考手册我们能够得到以下内容:

可知使该位为1才能使能GPIOA的时钟,于是发生了一下运算:RCC->AHB1ENR |= 1 << 0,我们假设 RCC->AHB1ENR 的值是0xF0,换算为二进制即1111 0000,再经按位或且赋值逻辑即 1111 0000的第1位与1进行逻辑或运算,有0 | 1 = 1,则该寄存器的0位就被赋值了1,GPIOA的时钟被使能。
关于为什么要使用<<左移运算符,我们假设RCC_AHB1ENR_GPIOAEN操作的是第8位,使用<<运算符我们可以得到 1 << 7 = 1000 0000,此时再对RCC->AHB1ENR进行按位或且赋值运算就有 1111 0000 的后四位与1000进行或运算,得到 1111 0000 | 1000 0000 = 1111 0000,可以发现原有位为1的位经运算后仍为1,为0的位仍为0,若使用其他赋值方式则其他位很有可能被篡改,因此使用 |= x << y的赋值方式能够消除对其他位操作的影响。
接下来tmpreg变量被赋值成了另一个函数值READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN),该函数定义如下:
#define READ_BIT(REG, BIT) ((REG) & (BIT))READ_BIT()函数定义
将函数值翻译出来可以得到 RCC->AHB1ENR & RCC_AHB1ENR_GPIOAEN ,这个值本身没有任何含义,包括变量tmpreg也是,他们被定义的原因在于一段不起眼的注释:/* Delay after an RCC peripheral clock enabling */ 在使能外设时钟后,需要一个短暂延迟,确保时钟稳定生效,避免后续操作寄存器时出现异常。因此tmpreg的定义只是为了能调用READ_BIT()函数进行一次读操作来保证时钟的生效,本身没有任何含义,不直接使用READ_BIT()函数的原因就是防止这段无意义函数被编译器优化掉,包括之后的UNSUED()函数,他的定义如下:
#define UNUSED(X) (void)X /* To avoid gcc/g++ warnings */UNUSED()函数定义
它本身也是没有任何实际应用,仅仅将输入的形参X强制转化为一个void空类型,目的是让编译器认为输入的这个X变量它被使用过了,不是无意义的,因此整个围绕tmpreg变量的过程目的就是延时操作使时钟生效和保护它本身。
整个函数实际的操作就是将相关使能寄存器下的使能位赋值为1,并确保其稳定实现。
4 结语
没啥说的。