4. GPIO 驱动开发:从模式配置到按键中断实战
GPIO 是 STM32 最基础、最常用的外设。说白了,它就是芯片和外界打交道的「手脚」。我刚开始学 STM32 时,觉得 GPIO 不就是点个灯、读个按键嘛,太简单了。直到后来做项目,因为中断没处理好,产品在客户现场频繁误触发……嗯,从那以后我再也不敢小看 GPIO 了。
这一章,我们就把 GPIO 的几种模式、HAL 库的 API 用法、以及按键中断与消抖这些实战问题,一次性讲透。
4.1 GPIO 的四种工作模式
STM32 的 GPIO 引脚,可以配置成四种基本模式。理解这四种模式,是驱动开发的第一步。
| 模式 | 典型用途 | 内部结构特点 |
|---|---|---|
| 输入模式 | 读取按键、传感器电平 | 施密特触发器开启,上下拉可选 |
| 输出模式 | 驱动 LED、蜂鸣器、继电器 | 推挽或开漏输出,速度可配 |
| 复用功能模式 | USART、SPI、I2C 等外设引脚 | 引脚由片上外设控制,非 GPIO 寄存器 |
| 模拟模式 | ADC 采样、DAC 输出 | 施密特触发器关闭,功耗最低 |
个人经验: 我习惯在 CubeMX 里先把所有用到的引脚模式配好,生成代码后再根据实际需求微调。尤其是复用功能,选错 AF 编号会导致外设不工作,排查起来很头疼。
4.2 HAL_GPIO_Init 详解
这个函数是 GPIO 配置的核心。它的原型长这样:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
第一个参数指定是哪一组 GPIO(GPIOA、GPIOB 等),第二个参数是一个结构体指针,里面包含了所有配置项。
GPIO_InitTypeDef 结构体关键成员:
- Pin:引脚编号,可以是 GPIO_PIN_0 ~ GPIO_PIN_15,也可以用或运算组合多个引脚。
- Mode:模式选择,如 GPIO_MODE_INPUT、GPIO_MODE_OUTPUT_PP、GPIO_MODE_AF_PP、GPIO_MODE_ANALOG。
- Pull:上下拉配置,GPIO_NOPULL、GPIO_PULLUP、GPIO_PULLDOWN。
- Speed:输出速度,GPIO_SPEED_FREQ_LOW/MEDIUM/HIGH/VERY_HIGH。
- Alternate:复用功能编号,仅在复用模式下有效。
避坑指南: 我曾经在配置 USART 时忘了设置 Alternate 参数,结果串口怎么都发不出数据。查了半天才发现 AF 没配。记住:复用模式下,Alternate 必须和 CubeMX 里看到的 AF 编号一致。
4.3 读写引脚:HAL_GPIO_WritePin / ReadPin / TogglePin
这三个 API 是日常开发中使用频率最高的。用法非常直接:
// 写引脚:设置 PA5 输出高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
// 读引脚:读取 PB3 的电平状态
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3);
// 翻转引脚:将 PC1 的电平取反
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_1);
你想想看,TogglePin 这个函数多方便。以前用标准库时,翻转引脚得先读再写,现在一行搞定。
小技巧: 我习惯把 LED 引脚封装成宏,比如
#define LED_TOGGLE() HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin),这样代码更简洁,移植也方便。
4.4 按键中断与消抖实战
按键处理是嵌入式开发中的经典场景。直接读引脚电平?不行,机械按键按下和释放时会产生抖动,不处理的话一次按下可能被误判成多次。
硬件消抖 vs 软件消抖:
- 硬件消抖:在按键引脚上加 RC 滤波电路或施密特触发器。适合对成本不敏感、对实时性要求高的场合。
- 软件消抖:检测到电平变化后,延时 10~20ms 再读一次。成本低,灵活,是大多数项目的首选。
我个人更推荐软件消抖,因为不需要额外元件,而且可以通过调整延时时间适应不同按键。
4.4.1 中断方式实现按键检测
轮询方式太浪费 CPU 了。用外部中断(EXTI)才是正经做法。配置步骤:
- 在 CubeMX 中将按键引脚配置为 GPIO_EXTI 模式,并使能中断。
- 设置触发边沿:上升沿、下降沿或双边沿。
- 在中断回调函数
HAL_GPIO_EXTI_Callback中处理按键逻辑。
代码示例:
// 中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY_Pin)
{
// 延时消抖
HAL_Delay(20);
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
// 按键确实被按下,执行动作
LED_TOGGLE();
}
}
}
注意: 中断回调函数里不要做耗时操作,比如 HAL_Delay(500)。中断服务程序应该短小精悍,否则会影响系统实时性。如果按键动作复杂,建议在回调中只设置一个标志位,在主循环中处理。
4.4.2 消抖的几种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 延时消抖 | 实现简单,代码量少 | 阻塞 CPU,不适合多任务 | 简单按键,非实时系统 |
| 状态机消抖 | 非阻塞,适合 RTOS | 代码稍复杂 | 多按键,实时性要求高 |
| 硬件 RC 消抖 | 不占 CPU,可靠性高 | 增加 BOM 成本 | 工业控制,恶劣环境 |
我在一个智能家居项目中用过状态机消抖,同时处理 8 个按键,效果很好。如果你用的是 FreeRTOS,可以创建一个按键任务,用消息队列传递按键事件,这样更优雅。
4.5 GPIO 驱动开发知识体系
下面这张图总结了本章的核心内容,帮你建立整体认知:
核心要点回顾:
- GPIO 四种模式:输入、输出、复用、模拟,各有各的用途。
- HAL_GPIO_Init 是配置入口,结构体成员一个都不能漏。
- WritePin / ReadPin / TogglePin 是日常三件套,熟记于心。
- 按键中断一定要做消抖,延时 20ms 是经验值,但别在中断里做耗时操作。