ok,我们今天先通过STM32U5A5这块开发板点个灯,并通过按键去控制led的亮灭。首先,我们先看一下这块开发板的原理图。
绿色led
红色与蓝色led
按键buttonok,我们现在打开cubemx去进行一下GPIO的配置
我们将各个GPIO口添加上Labels,
STM32U5A5 开发板 LED 与用户按键引脚配置表
| 外设 | 器件编号 | GPIO 引脚 | GPIO 模式 | 有效电平 | User Label |
|---|---|---|---|---|---|
| 绿色 LED | LD1 | PC7(默认) | GPIO_Output | 高电平点亮 | LED_GREEN |
| 蓝色 LED | LD2 | PB7 | GPIO_Output | 高电平点亮 | LED_BLUE |
| 红色 LED | LD3 | PG2 | GPIO_Output | 高电平点亮 | LED_RED |
| 用户按键 | B1 | PC13(默认) | GPIO_Input | 高电平表示按下 | KEY1 |
小灯亮起
假如我们现在要点亮红色小灯
while(1){HAL_GPIO_WritePin(LED_RED_GPIO_Port,LED_RED_Pin,GPIO_PIN_SET);}关于HAL_GPIO_WritePin这个函数,是HAL库提供的GPIO电平控制函数,具体请参考我上一篇博客,这里简单带过,从寄存器层面理解,这句话就是向GPIOG端口的BSRR第2位写入1,使PG2 输出高电平。
如图。小灯亮起
接下来,我们让红色 LED 以 500 ms 为间隔,交替执行点亮和熄灭操作。
while(1){// 点亮红色 LEDHAL_GPIO_WritePin(LED_RED_GPIO_Port,LED_RED_Pin,GPIO_PIN_SET);HAL_Delay(500);// 熄灭红色 LEDHAL_GPIO_WritePin(LED_RED_GPIO_Port,LED_RED_Pin,GPIO_PIN_RESET);HAL_Delay(500);}HAL_Delay(500):延时 500 ms,即 0.5 s。(关于这个函数我们不细讲,后续我专门写一篇博客去讲这些常见的函数)
另外,还有一种实现方式
while(1){HAL_GPIO_TogglePin(LED_RED_GPIO_Port,LED_RED_Pin);HAL_Delay(500);}HAL_GPIO_TogglePin函数
HAL_GPIO_TogglePin这个函数,我们展开看一下
voidHAL_GPIO_TogglePin(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin){uint32_todr;/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));/* get current Output Data Register value */odr=GPIOx->ODR;/* Set selected pins that were at low level, and reset ones that were high */GPIOx->BSRR=((odr&GPIO_Pin)<<GPIO_NUMBER)|(~odr&GPIO_Pin);}ODR寄存器
ODR是什么?Output Data Register,输出数据寄存器,作用是用来保存一个GPIO端口各个引脚输出逻辑状态的寄存器。
对于 STM32U5A5,ODR 虽然是一个 32 位寄存器,但只有低 16 位(Bit 0~15)用于 GPIO 输出控制,高 16 位(Bit 16~31)是保留位(Reserved)。
| 位范围 | 功能 | 说明 |
|---|---|---|
| Bit 0~15 | 输出数据位 | 分别对应 GPIOx 的 0~15 号引脚 |
| Bit 16~31 | 保留位 | 不用于 GPIO 输出控制 |
保留位不是额外的 16 个 GPIO 引脚,也不能拿来控制其他 GPIO。编写寄存器代码时应遵循参考手册对保留位的规定。
为什么 ODR 要设计成 32 位?
因为 STM32 是 32 位微控制器,其外设寄存器通常采用 32 位访问形式。
| 位范围 | ODR | BSRR |
|---|---|---|
| Bit 0~15 | 保存 GPIO 输出状态 | 写 1,设置对应引脚为高电平 |
| Bit 16~31 | 保留位 | 写 1,复位对应引脚为低电平 |
HAL_GPIO_TogglePin`函数实现
首先,定义一个uint32_t类型的局部变量odr,用于保存 GPIO 输出数据寄存器(ODR)的当前值,以便进行后续的位运算。
uint32_todr;odr=GPIOx->ODR;其中,GPIOx是指向 GPIO 外设寄存器结构体的指针,GPIOx->ODR表示该 GPIO 端口的输出数据寄存器(Output Data Register)。
因此,odr = GPIOx->ODR;的作用就是读取当前 GPIO 端口 ODR 寄存器的数值,并将其保存到局部变量odr中。
接下来,我们的目标是实现指定 GPIO 引脚的输出电平翻转,即原来为高电平(1)的引脚变为低电平(0),原来为低电平(0)的引脚变为高电平(1)。我们的思路就是,我们保存这个GPIO端口输出逻辑的状态,将这个状态取反给BSRR寄存器进行改写。
由于 BSRR 寄存器的低 16 位用于置位,高 16 位用于复位,因此不能直接将 ODR 的数值取反后写入 BSRR。
我们需要根据odr保存的输出状态,通过按位与、按位取反、左移和按位或运算,生成对应的 BSRR 控制数据。By the way,BSRR寄存器的结构(写0的位置不产生对应操作),[31:16]是复位区域,写1->输出0。[15:0]是置位区域,写1->输出1。
eg,我们的代码是控制的是PG2这个引脚,想让 PG2 输出高电平,就向 BSRR 的Bit[2] 写入 1,想让 PG2 输出低电平,就向 BSRR 的Bit [18] 写入 1。
假设PG2最初是处于高电平状态,那我的GPIOG的ODR寄存器保存着整个GPIOG端口的输出状态,ODR[2]=1;那我想让它状态翻转,直接取反赋值给BSRR不就好了吗?但是一个ODR寄存器里面保存着所有GPIOG区域内的端口的状态,直接取反,肯定会影响其他端口的状态,so,我们按位与,把想要的PIN脚状态取出,再左移16位,赋值给BSRR,即可将其状态反转。
这里有一个小插曲,当BSRR寄存器的某位处于置位状态时,由于BSRR的硬件规则,此时的ODR寄存器对应的位置会被清0,BSRR[18] 写 1 → 清除 ODR[2] → PG2 输出低电平。
那如果PG2最初是低电平状态,我的GPIOG的ODR寄存器ODR[2]=0,我想要的结果是什么?是ODR[2]=0 ->ODR[2]=1,通过按位取反和按位与生成 BSRR 的置位命令,即可将其状态反转。
GPIOx->BSRR=((odr&GPIO_Pin)<<16)|(~odr&GPIO_Pin);最终,通过向 BSRR 写入计算结果,实现指定 GPIO 引脚输出电平的翻转。
在这里思考的有些多了,每次总是忍不住往底层走,照这样学多久才能到头啊。。
HAL_GPIO_ReadPin
我们现在让按键去控制小灯的亮灭情况
while(1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_SET){HAL_GPIO_WritePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin,GPIO_PIN_SET);}else{HAL_GPIO_WritePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin,GPIO_PIN_RESET);}}HAL_GPIO_ReadPin我盲猜啊,就是去读IDR(Input Data Register输入数据寄存器)寄存器的值,判断一下我需要的引脚的状态
我们根据底层逻辑去客观验证一下我的猜想
GPIO_PinStateHAL_GPIO_ReadPin(constGPIO_TypeDef*GPIOx,uint16_tGPIO_Pin){GPIO_PinState bitstatus;/* Check the parameters */assert_param(IS_GPIO_PIN(GPIO_Pin));if((GPIOx->IDR&GPIO_Pin)!=0U){bitstatus=GPIO_PIN_SET;}else{bitstatus=GPIO_PIN_RESET;}returnbitstatus;}代码逻辑很简单,和我想得一模一样,if我们的IDR寄存器对应的我们需要的PIN位的那一位,是不是说的有点绕,PG2(仅展示后八位)为00000100,和IDR[2]按位与一下,就可以得到PG2的状态。
顺便插一嘴,const GPIO_TypeDef *GPIOx,这里为什么要用const,现在看的是读取引脚的函数。它只需要查看电平,所以用const表达“只读”的意思,对应地:
HAL_GPIO_ReadPin():读电平,参数带const。HAL_GPIO_WritePin():需要写寄存器,参数不带这个const。
我们按着按键,绿灯亮起。