4、STM32 GPIO编程:寄存器操作与HAL库对比、点亮LED、按键输入检测、外部中断
好,咱们进入第四章。这一章可以说是嵌入式开发的「基本功」。不管你是做智能家居、工业控制,还是做IoT设备,GPIO都是你打交道最多的外设。
我个人习惯,学任何一款MCU,第一件事就是点亮LED。为什么?因为这是验证硬件、开发环境、下载链路是否正常的「最小系统测试」。灯亮了,后面的事才有意义。
4.1 寄存器操作 vs HAL库:两种思维模式
先说说两种编程方式。很多新手会纠结「到底学寄存器还是学HAL库?」我的答案是:都要会,但分场景。
寄存器操作,说白了就是直接操作MCU的物理地址。你往某个地址写个0或1,硬件就动一下。这种方式快、直接、代码量小,但可读性差,移植性也差。
HAL库,是ST官方封装好的函数库。你调用HAL_GPIO_WritePin(),库函数帮你算好地址、配置好寄存器。优点是开发快、代码好读、移植方便。缺点是多了几层函数调用,效率略低。
我在项目中遇到过这样的情况:一个对时序要求极严的传感器驱动,用HAL库死活读不到正确数据,换成寄存器操作,一调就好。但反过来,做产品原型时,HAL库能让你一周干完一个月的活。
核心结论:寄存器操作是「内功」,HAL库是「招式」。内功扎实的人,用招式更灵活。
4.2 点亮LED:从寄存器到HAL库
咱们先拿一个最简单的例子——点亮LED。假设LED接在PA0引脚,低电平点亮。
4.2.1 寄存器方式
STM32的GPIO寄存器主要有几个:
- GPIOx_MODER:模式寄存器,配置输入/输出/复用/模拟
- GPIOx_OTYPER:输出类型寄存器,推挽/开漏
- GPIOx_OSPEEDR:输出速度寄存器
- GPIOx_PUPDR:上拉/下拉寄存器
- GPIOx_ODR:输出数据寄存器,写1或0控制引脚电平
- GPIOx_IDR:输入数据寄存器,读取引脚电平
点亮LED的寄存器代码:
// 开启GPIOA时钟(RCC寄存器)
RCC->AHB1ENR |= (1 << 0); // 使能GPIOA时钟
// 配置PA0为输出模式
GPIOA->MODER &= ~(0x3 << (0 * 2)); // 先清零
GPIOA->MODER |= (0x1 << (0 * 2)); // 设置为输出模式(01)
// 设置输出类型为推挽
GPIOA->OTYPER &= ~(1 << 0);
// 设置输出速度为低速
GPIOA->OSPEEDR &= ~(0x3 << (0 * 2));
// 设置无上拉无下拉
GPIOA->PUPDR &= ~(0x3 << (0 * 2));
// 点亮LED(低电平)
GPIOA->ODR &= ~(1 << 0); // 写0,灯亮
嗯,这里要注意:MODER寄存器每个引脚占2位,所以操作时要用(引脚号 * 2)来移位。我第一次写的时候忘了这个,结果配置错了,灯死活不亮。排查了半天才发现是模式没配对。
4.2.2 HAL库方式
同样的功能,用HAL库写起来就清爽多了:
// 定义GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
你看,HAL库把那些位运算都封装好了。你只需要填结构体,调函数就行。但如果你不理解寄存器,出了问题你连调试的方向都没有。
我的建议:学习阶段,先用寄存器写一遍,再用HAL库写一遍。对比着看,你就能理解HAL库背后到底干了什么。
4.3 按键输入检测:轮询方式
LED是输出,按键就是输入了。假设按键接在PA1引脚,按下为低电平。
寄存器方式配置输入:
// 配置PA1为输入模式
GPIOA->MODER &= ~(0x3 << (1 * 2)); // 清零,默认就是输入(00)
// 使能内部上拉(防止浮空)
GPIOA->PUPDR |= (0x1 << (1 * 2)); // 上拉
// 读取按键状态
uint8_t key_state = (GPIOA->IDR >> 1) & 0x01;
if(key_state == 0) {
// 按键按下
}
HAL库方式:
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 读取
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) {
// 按键按下
}
这里有个坑——按键抖动。机械按键按下和松开时,电平会跳变几次,持续几毫秒到十几毫秒。如果不做消抖,一次按下可能被误判成多次。
我曾经在一个产品里没做消抖,结果用户按一下,设备切换了三个模式。被客户骂惨了。
最简单的消抖方法:检测到按下后,延时20ms再读一次。如果还是按下状态,才确认是有效按下。
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) {
HAL_Delay(20); // 延时消抖
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) {
// 确认按键按下
}
}
4.4 外部中断:让CPU不再空等
轮询方式有个问题:CPU得一直循环检查按键状态,干不了别的活。对于IoT设备来说,这太浪费了。更好的方式是——外部中断。
外部中断的原理:当引脚电平发生变化(上升沿、下降沿或双边沿)时,硬件自动触发中断,CPU暂停当前任务,跳转到中断服务函数执行。执行完再回来。
4.4.1 寄存器配置外部中断
配置外部中断的步骤:
- 配置GPIO为输入模式
- 配置SYSCFG,将GPIO引脚连接到EXTI(外部中断控制器)
- 配置EXTI:选择触发边沿、使能中断
- 配置NVIC(嵌套向量中断控制器):使能中断通道、设置优先级
- 编写中断服务函数
寄存器代码示例(PA1下降沿触发):
// 1. 配置GPIO
GPIOA->MODER &= ~(0x3 << (1 * 2));
GPIOA->PUPDR |= (0x1 << (1 * 2));
// 2. 连接EXTI(SYSCFG寄存器)
SYSCFG->EXTICR[0] &= ~(0xF << (1 * 4));
SYSCFG->EXTICR[0] |= (0x0 << (1 * 4)); // PA口对应EXTI0-EXTI15
// 3. 配置EXTI
EXTI->IMR |= (1 << 1); // 使能中断线1
EXTI->FTSR |= (1 << 1); // 下降沿触发
// 4. 配置NVIC
NVIC->ISER[0] |= (1 << (EXTI1_IRQn & 0x1F));
// 5. 中断服务函数
void EXTI1_IRQHandler(void) {
if(EXTI->PR & (1 << 1)) {
// 处理按键事件
// ...
EXTI->PR |= (1 << 1); // 清除中断标志位
}
}
重要提醒:中断服务函数里一定要清除中断标志位!否则会一直触发中断,系统卡死。我见过有人调试时忘了这步,结果程序一跑就进HardFault,查了两天才发现是标志位没清。
4.4.2 HAL库配置外部中断
HAL库把上面那些复杂的寄存器操作都封装好了:
// 配置GPIO
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发中断
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置NVIC(HAL库自动处理)
HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
// 中断服务函数
void EXTI1_IRQHandler(void) {
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);
}
// HAL库的回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == GPIO_PIN_1) {
// 处理按键事件
}
}
你看,HAL库把中断处理分成了两层:EXTI1_IRQHandler是底层中断入口,它调用HAL_GPIO_EXTI_IRQHandler,后者再调用你写的回调函数。这样你只需要关心业务逻辑,不用管底层细节。
4.5 寄存器 vs HAL库:怎么选?
我整理了一个对比表,方便你参考:
| 对比项 | 寄存器操作 | HAL库 |
|---|---|---|
| 代码量 | 少,但位运算多 | 多,但结构清晰 |
| 执行效率 | 高,直接操作硬件 | 略低,有函数调用开销 |
| 可读性 | 差,需要查手册 | 好,函数名自解释 |
| 移植性 | 差,换MCU要重写 | 好,换同系列MCU改动小 |
| 调试难度 | 高,出错难排查 | 低,有断言和错误处理 |
| 适合场景 | 时序敏感、资源受限 | 快速开发、产品原型 |
我的建议是:学习用寄存器,干活用HAL库。但不管用哪种,一定要理解背后的原理。否则出了问题,你连问题出在哪都不知道。
本章小结:
- GPIO编程有两种方式:寄存器操作(底层、高效)和HAL库(封装、易用)
- 点亮LED是验证硬件和开发环境的「最小系统测试」
- 按键输入要注意消抖,否则会有误触发
- 外部中断让CPU从轮询中解放出来,适合IoT设备
- 中断服务函数里一定要清除标志位
下一章,我们会讲STM32的定时器。定时器这东西,在IoT里太常用了——做延时、做PWM、做脉冲计数、做RTC...嗯,到时候再细聊。