3. GPIO输入检测:按键抖动现象与消抖原理
按键检测,听起来很简单对吧?不就是读个电平高低嘛。但实际做产品时,这个看似简单的操作,坑可不少。我最早做的一个小项目,按键控制LED灯,按下就亮,松开就灭。结果调试时发现,按一次灯闪了好几下。当时我还以为是硬件坏了,查了半天……后来才知道,这就是经典的按键抖动问题。
3.1 按键抖动现象
机械按键的内部结构,说白了就是两个金属簧片。按下时它们接触,松开时它们分离。但问题在于,金属接触的瞬间不是干净利落的——它会弹跳几次才能稳定下来。
抖动时间:通常持续 5~20ms,具体取决于按键的机械品质。
你想想看,CPU 的时钟是 MHz 级别的,1ms 能执行几千条指令。在这 5~20ms 的抖动期内,如果你连续读取 GPIO 电平,就会读到一串 0 和 1 交替的信号。这就是为什么按一次键,程序却认为你按了好几次。
| 阶段 | 电平状态 | 持续时间 |
|---|---|---|
| 按下前 | 高电平(上拉) | 稳定 |
| 按下抖动 | 高低电平交替 | 5~20ms |
| 按下稳定 | 低电平 | 持续 |
| 松开抖动 | 高低电平交替 | 5~20ms |
| 松开后 | 高电平(上拉) | 稳定 |
3.2 消抖原理
消抖的核心思路就一个:避开抖动期,在稳定期采样。具体做法分两种:硬件消抖和软件消抖。
硬件消抖——在按键引脚上加一个 RC 低通滤波器。电容充放电会平滑掉电平的跳变,让抖动被滤除。优点是 CPU 不用操心,缺点是增加了 BOM 成本和 PCB 面积。
软件消抖——这是嵌入式开发中最常用的方式。我个人的习惯是:检测到电平变化后,延时 10~20ms 再读一次。如果两次电平一致,就认为是有效按键事件。
我的经验:延时 10ms 通常就够了。如果按键质量特别差,可以延长到 20ms。但别超过 30ms,否则用户会觉得按键反应迟钝。
软件消抖的流程,我画了一张图,你可以对照着理解:
3.3 CubeMX 配置 GPIO 输入
接下来我们实际操作一下。打开 CubeMX,新建一个 STM32F103C8T6 的工程。我以这个型号为例,其他型号操作类似。
- 选择引脚:在 Pinout 视图中,找到 PA0 引脚。点击它,选择
GPIO_Input模式。 - 配置上拉电阻:在 GPIO 配置界面,将
GPIO Pull-up/Pull-down设置为Pull-up。这样按键未按下时,引脚为高电平;按下后接地,引脚为低电平。 - 配置标签:在
User Label一栏,输入KEY。这样生成的代码中,引脚宏定义就是KEY_Pin和KEY_GPIO_Port,可读性更好。
注意:如果你用的是开发板,先查一下原理图,确认按键接在哪个引脚上,以及按下后是低电平还是高电平。我曾经因为没看原理图,把上拉和下拉搞反了,折腾了半小时才发现。
3.4 HAL 库 GPIO 读函数
HAL 库提供了两个读取 GPIO 电平的函数:
| 函数 | 原型 | 说明 |
|---|---|---|
HAL_GPIO_ReadPin |
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) |
读取指定引脚的电平,返回 GPIO_PIN_SET(高)或 GPIO_PIN_RESET(低) |
HAL_GPIO_WritePin |
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) |
设置指定引脚的电平 |
用法很简单:
// 读取按键状态
GPIO_PinState keyState = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
if (keyState == GPIO_PIN_RESET) {
// 按键按下(低电平有效)
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 点亮LED
} else {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // 熄灭LED
}
3.5 实战:按键控制 LED 灯
好了,理论讲完了,我们直接上手写代码。这个实战的目标是:按一下按键,LED 状态翻转一次。注意,是翻转,不是按下亮松开灭。这样更能体现消抖的效果。
完整的代码框架如下:
/* main.c 中的 while 循环 */
while (1)
{
/* 读取按键状态 */
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
/* 延时消抖 */
HAL_Delay(10);
/* 再次确认 */
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
/* 等待按键松开,避免一次按下触发多次翻转 */
while (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET);
/* 翻转 LED 状态 */
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
}
关键点:
- 两次读取:第一次检测到按下后,延时 10ms 再读一次。两次一致才认为是有效按下。
- 等待松开:用
while循环等待按键释放。如果不加这一步,按住按键不放时,LED 会一直翻转。 - 使用 TogglePin:
HAL_GPIO_TogglePin是 HAL 库提供的翻转函数,比先读再写方便多了。
我的建议:实际项目中,我一般不会在 while 循环里用 HAL_Delay 做消抖,因为它会阻塞整个程序。更好的做法是用定时器或状态机来做非阻塞消抖。不过对于初学者,先理解这个基础版本就够了。
3.6 常见问题与避坑
- 按键没反应:先检查 GPIO 配置是否正确。用万用表量一下引脚电平,按下时是否变化。如果没变化,八成是引脚选错了。
- 按一次触发多次:消抖延时太短,或者根本没做消抖。把延时加到 15~20ms 试试。
- LED 亮度异常:检查 LED 限流电阻。STM32 GPIO 输出电流有限,直接驱动 LED 要加 330Ω~1kΩ 的电阻。
我曾经踩过的坑:有一次我把按键接在 PB3 引脚上,怎么配置都不对。后来查手册才发现,PB3 是 JTDO 功能引脚,默认被调试器占用了。在 CubeMX 里需要先把调试接口配置为 Serial Wire,才能正常使用这个引脚做 GPIO。
嗯,这一节的内容就到这里。按键消抖虽然基础,但它是嵌入式开发中第一个真正需要你思考「硬件特性 vs 软件处理」的地方。把这个搞明白了,后面学中断、定时器、状态机都会轻松很多。