4. 按键输入:按键消抖原理、轮询方式读取按键、中断方式读取按键
大家好,我是你们的嵌入式讲师。今天我们来聊聊按键输入。按键这东西,看着简单,但坑不少。我刚开始做项目时,就因为在按键处理上偷懒,结果产品在客户手里频繁误触发,被骂得狗血淋头。所以,这一节咱们把按键的底裤都扒干净。
4.1 按键消抖原理:为什么按键会“抖”?
你按下一个机械按键,是不是觉得“咔哒”一下就到位了?其实不是。在微观层面,金属触点接触时会有弹性碰撞,产生一连串的“毛刺”信号。这个过程通常持续5-20ms。如果你直接读这个信号,MCU会以为你按了十几次。
核心结论:按键抖动是物理现象,不是BUG。我们的任务就是把这5-20ms的抖动期“过滤掉”。
消抖方法主要有两种:硬件消抖和软件消抖。硬件消抖加电容、加施密特触发器,成本高。我们做嵌入式,能用软件解决的,绝不加硬件。所以,重点讲软件消抖。
软件消抖的原理很简单:延时 + 多次采样。检测到电平变化后,等10-20ms再读一次。如果电平稳定,才认为是有效按键。
我的经验:我个人习惯用10ms延时。为什么?因为大部分按键的抖动时间在5-10ms,10ms刚好能覆盖。太短了消不干净,太长了影响响应速度。
4.2 轮询方式读取按键:简单粗暴,但够用
轮询,说白了就是主循环里不停地读IO口。适合对实时性要求不高的场景,比如设置参数、切换模式。
下面是一个标准的轮询消抖代码,我用STM32 HAL库写的:
// 按键结构体
typedef struct {
uint8_t current_state; // 当前状态
uint8_t last_state; // 上一次状态
uint8_t stable_state; // 稳定后的状态
uint32_t last_tick; // 上次变化的时间戳
} Button_t;
Button_t btn;
// 初始化
void Button_Init(void) {
btn.current_state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
btn.last_state = btn.current_state;
btn.stable_state = btn.current_state;
btn.last_tick = HAL_GetTick();
}
// 轮询处理,放在主循环
void Button_Process(void) {
btn.current_state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
// 检测到电平变化
if (btn.current_state != btn.last_state) {
btn.last_tick = HAL_GetTick(); // 记录变化时间
}
// 如果电平稳定超过10ms,认为是有效按键
if ((HAL_GetTick() - btn.last_tick) > 10) {
if (btn.current_state != btn.stable_state) {
btn.stable_state = btn.current_state;
// 检测到按下(假设低电平有效)
if (btn.stable_state == GPIO_PIN_RESET) {
// 执行按键动作
Key_Action();
}
}
}
btn.last_state = btn.current_state;
}
注意:轮询方式有个致命缺点——如果主循环里有耗时操作(比如刷屏、写Flash),按键响应会变慢。我曾经在一个项目里,主循环跑了50ms,按键按下去要等半秒才有反应,用户体验极差。
4.3 中断方式读取按键:实时响应,但要注意“坑”
如果你需要按键秒响应,比如紧急停止、唤醒系统,那就得上中断了。中断方式的好处是:按键一按,CPU立刻停下当前工作来处理。
但中断里不能做延时!为什么?因为中断服务函数(ISR)要快进快出。你在中断里delay(10ms),整个系统就卡死了。
正确的做法是:中断里只标记事件,在主循环里处理消抖和动作。这叫“中断标记 + 主循环处理”模式。
看代码:
// 全局标志
volatile uint8_t key_pressed_flag = 0;
uint32_t key_interrupt_tick = 0;
// GPIO中断回调(HAL库自动调用)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == KEY_Pin) {
key_pressed_flag = 1; // 标记有按键事件
key_interrupt_tick = HAL_GetTick(); // 记录中断时间
}
}
// 主循环中处理
void Key_Interrupt_Handler(void) {
if (key_pressed_flag) {
key_pressed_flag = 0; // 清除标志
// 延时10ms消抖(在主循环里做)
while ((HAL_GetTick() - key_interrupt_tick) < 10);
// 再次读取确认
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
// 确认按键按下,执行动作
Key_Action();
}
}
}
关键点:中断里只做两件事——置标志位、记录时间戳。消抖和动作执行全部放到主循环。这样既保证了实时性,又不会阻塞系统。
4.4 轮询 vs 中断:怎么选?
我给大家一个选择标准:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 菜单设置、模式切换 | 轮询 | 实时性要求低,代码简单 |
| 紧急停止、唤醒 | 中断 | 需要毫秒级响应 |
| 主循环有大量耗时任务 | 中断 | 避免按键响应延迟 |
| 电池供电的低功耗设备 | 中断 | 可以唤醒MCU,轮询会浪费功耗 |
避坑指南:我曾经在一个产品里用了中断方式,但忘了配置GPIO的上拉电阻。结果按键没按时,引脚悬空,中断疯狂触发。嗯,从那以后,我每次配置GPIO都会检查上拉/下拉设置。
4.5 本章知识体系
下面这张图,是我画的知识结构。你看一眼,就能把按键输入的全貌记住:
你看,整个按键输入就三个核心:消抖原理、轮询方式、中断方式。消抖是基础,轮询和中断是两种实现手段。掌握了这些,市面上99%的按键需求你都能搞定。
好了,这一节就到这里。记住:按键虽小,但处理不好会出大问题。下次你写按键代码时,想想我今天说的“10ms延时”和“中断里只标记”,能帮你省不少调试时间。
公众号:蓝海资料掘金营,微信deep3321