3. GPIO 输入检测:按键消抖与 LED 控制实战
各位同学,欢迎来到第三章。上一章我们点亮了 LED,那是输出。这一章我们反过来,让单片机“听”按键的动作——也就是 GPIO 输入检测。
按键看似简单,但实际项目中坑不少。我记得刚入行时,做一个手持设备,按键一按就触发好几次,用户投诉说“这机器是不是疯了”。后来才发现,是没做消抖处理。好,咱们今天就把这个坑填上。
3.1 按键为什么会抖动?
你按下按键,金属触点接触时并不是“啪”一下就稳定了。实际上,它会弹跳几次,持续几毫秒到十几毫秒。单片机跑得飞快,几微秒就能读一次,所以它会读到一连串的 0 和 1 交替变化。
核心概念:按键抖动是机械触点固有的物理现象,不是电路故障。消抖的目的就是滤掉这些毛刺,只认一次稳定的电平变化。
抖动的典型波形是这样的:
3.2 消抖的两种主流方法
消抖说白了就两个思路:硬件消抖和软件消抖。我个人习惯优先用软件消抖,因为不增加成本,改起来也方便。
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 硬件消抖 | RC 低通滤波 + 施密特触发器 | 不占 CPU,响应快 | 增加元件,PCB 面积变大 |
| 软件消抖 | 延时采样或连续多次采样 | 零成本,灵活可调 | 占用 CPU 时间(可用定时器优化) |
我的建议:量产产品中,如果按键数量少(1~4 个),用软件消抖完全够用。如果按键矩阵很大,或者对响应速度要求极高,可以考虑硬件消抖。
3.3 软件消抖的经典实现
最常用的方法是“延时 10~20ms 后再次读取”。为什么是 10~20ms?因为绝大多数按键的抖动不会超过 20ms。我曾经遇到过一种劣质按键,抖了 30ms,那次我直接把延时改到 40ms 才搞定。
代码逻辑很简单:
- 检测到按键电平变化(比如从高变低)
- 延时 20ms
- 再次读取同一引脚
- 如果电平仍然为低,确认按键按下
- 否则视为干扰,忽略
用 HAL 库写出来就是这样:
// 按键消抖检测函数
// 返回值:1 表示按键确实按下,0 表示抖动或未按下
uint8_t Key_Read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 第一次检测到低电平
{
HAL_Delay(20); // 延时 20ms 避开抖动
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 再次确认
{
return 1; // 按键确实按下
}
}
return 0; // 抖动或未按下
}
注意:HAL_Delay(20) 会阻塞 CPU 20ms。在简单系统中没问题,但在多任务或实时性要求高的系统中,建议改用定时器轮询或状态机方式。
3.4 HAL_GPIO_ReadPin 函数详解
这个函数是 HAL 库读取 GPIO 电平的标准接口。咱们拆开看看:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
| 参数/返回值 | 说明 |
|---|---|
| GPIOx | GPIO 端口基地址,如 GPIOA、GPIOB、GPIOC |
| GPIO_Pin | 引脚编号,如 GPIO_PIN_0、GPIO_PIN_1,可用 | 组合多个引脚 |
| 返回值 | GPIO_PIN_RESET (0) 或 GPIO_PIN_SET (1) |
底层原理其实很简单:它读取 GPIOx->IDR 寄存器的对应位。IDR 是输入数据寄存器,每一位对应一个引脚的电平状态。
避坑指南:我曾经在项目里直接用 (GPIOA->IDR & GPIO_PIN_0) 来读引脚,结果发现某些型号的 STM32 在低功耗模式下 IDR 会失效。用 HAL_GPIO_ReadPin 就安全得多,因为它内部做了时钟和模式检查。
3.5 CubeMX 配置 GPIO 输入
咱们以 STM32F103C8T6 为例,配置一个按键输入引脚:
- 打开 CubeMX,选择芯片型号
- 在 Pinout 视图中,点击 PC13 引脚(假设接按键)
- 选择 GPIO_Input 模式
- 在 GPIO Settings 标签页中配置:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| GPIO mode | Input mode | 输入模式 |
| Pull-up/Pull-down | Pull-up | 外部按键接 GND,内部上拉保证空闲时高电平 |
| Maximum output speed | Low | 输入模式不关心速度,保持默认即可 |
小技巧:如果按键一端接 GND,另一端接 GPIO,就选内部上拉。如果按键一端接 VCC,另一端接 GPIO,就选内部下拉。这样外部只需要一个按键,省掉电阻。
3.6 实战:按键控制 LED 亮灭
好,理论讲完了,咱们直接上手。硬件连接如下:
- 按键:一端接 PC13,另一端接 GND
- LED:正极接 PB0(通过 220Ω 电阻),负极接 GND
完整代码:
// main.c 主循环部分
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init(); // CubeMX 生成的 GPIO 初始化
uint8_t key_pressed = 0; // 按键按下标志
while (1)
{
if (Key_Read(GPIOC, GPIO_PIN_13)) // 检测按键
{
// 防止一次按下多次触发
if (!key_pressed)
{
key_pressed = 1;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 翻转 LED
}
}
else
{
key_pressed = 0; // 按键释放后清除标志
}
}
}
// 按键消抖函数(前面已定义)
uint8_t Key_Read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
{
return 1;
}
}
return 0;
}
关键点:为什么加 key_pressed 标志?因为按键按下期间,Key_Read 会一直返回 1,如果不加标志,LED 会在一次按下中反复翻转。这个坑我踩过,当时调试了半天,以为是消抖没做好。
3.7 本章知识体系总览
下面这张图把 GPIO 输入检测的完整流程串起来了:
嗯,这一章的内容就到这里。按键消抖看起来是个小问题,但处理不好会让整个产品体验大打折扣。记住:延时 20ms 再读一次,加个防抖标志,基本就能应对 90% 的场景了。