按键消抖还在 delay?难怪你按一下,STM32 像抽风一样连触发
你是不是也遇到过这种情况?
明明只按了一下按键,LED 却闪了好几次。
菜单本来只想往下走一格,结果一下跳了两三格。

更烦的是,项目里想做“短按切换模式,长按进入设置”,结果短按也触发,长按也触发,程序像抽风一样。
很多初学者第一反应就是:
那我 delay 一下不就好了?
比如这样写:
if (KEY == 0)
{
delay_ms(20);
if (KEY == 0)
{
LED = !LED;
}
}
这段代码看起来没问题。
点灯实验里也确实能用。
但我想说一句:按键消抖不是简单 delay 一下。
01 按键为什么会抖?
机械按键不是理想开关。
你手指按下去的一瞬间,触点不是立刻稳定接通,而是会在很短时间内来回抖几下。
也就是说,单片机看到的电平可能是这样的:
1 1 0 1 0 0 1 0 0 0
你以为自己只按了一次。
但单片机可能以为你按了好几次。
尤其是 STM32 这种主频很高的芯片,几毫秒时间里,它已经循环检测很多次 GPIO 了。
所以,按键乱触发,不一定是板子坏了。
很多时候,是代码把“电平抖动”当成了“有效按键”。
02 delay 消抖最大的问题是什么?
delay 消抖不是完全不能用。
如果你只是写个点灯实验,问题不大。
但一到真实项目里,它就容易出事。
比如你的程序还要做这些事情:
串口接收数据
OLED 刷新显示
ADC 采样
PWM 输出
FreeRTOS 任务调度
通信协议解析
这时候你按一下按键,程序直接卡在 delay_ms(20)。
如果用户连续按几次,系统就会一卡一卡。
串口数据可能丢。
屏幕刷新可能变慢。
通信响应可能超时。
所以 delay 消抖的问题不是“不准”,而是太粗暴。
项目里不怕代码简单,就怕代码简单到把系统堵住。
03 更靠谱的办法:定时扫描
比较推荐的做法是:
每隔固定时间扫描一次按键,比如 10ms 扫描一次。
不是一读到按下就马上处理。
而是连续几次都读到按下,才确认按键真的按下了。
比如:
#define KEY_DOWN_LEVEL 0
#define KEY_SCAN_TIME 10
#define KEY_FILTER_CNT 3
uint8_t key_cnt = 0;
uint8_t key_state = 1;
uint8_t key_event = 0;
void Key_Scan(void)
{
if (KEY_READ() == KEY_DOWN_LEVEL)
{
if (key_cnt < KEY_FILTER_CNT)
{
key_cnt++;
}
if (key_cnt >= KEY_FILTER_CNT && key_state == 1)
{
key_state = 0;
key_event = 1; // 按键按下事件
}
}
else
{
key_cnt = 0;
key_state = 1;
}
}
这段代码的意思很简单。
如果连续 3 次都检测到按下,才认为按键真的按下。
如果 10ms 扫描一次,连续 3 次就是大约 30ms。
这样大部分机械抖动就被过滤掉了。
关键是:这个过程不需要 delay 卡住程序。
你可以把 Key_Scan() 放到定时器中断里,也可以放到 10ms 周期任务里。
主循环只处理事件:
if (key_event)
{
key_event = 0;
LED = !LED;
}
这样写,程序就清爽很多。
04 短按和长按,别写乱了
很多人短按、长按分不清,是因为逻辑写错了。
最常见的错误是:
按下瞬间就触发短按,按久了再触发长按。
这样当然会冲突。
因为刚按下的时候,你根本不知道用户是想短按,还是想长按。
正确思路应该是:
按下后开始计时
超过一定时间,触发长按
如果没超过长按时间就松开,触发短按
比如:
#define LONG_PRESS_TIME 80 // 80 * 10ms = 800ms
uint16_t press_time = 0;
uint8_t short_event = 0;
uint8_t long_event = 0;
uint8_t long_flag = 0;
void Key_Process(void)
{
if (key_state == 0) // 已确认按下
{
if (press_time < LONG_PRESS_TIME)
{
press_time++;
}
else if (long_flag == 0)
{
long_flag = 1;
long_event = 1; // 长按事件
}
}
else
{
if (press_time > 0 && press_time < LONG_PRESS_TIME)
{
short_event = 1; // 松开时判断短按
}
press_time = 0;
long_flag = 0;
}
}
这里有个重点:
短按一般在松开时判断,不是在按下时判断。
这句话很重要。
很多项目里按键逻辑乱,就是因为短按触发太早。
05 项目里建议这样分层
不要在业务代码里到处读 GPIO。
比如菜单程序里,不要这样写:
if (KEY1 == 0)
{
menu++;
}
这样后面一旦要加消抖、长按、组合键,代码会越来越乱。
更好的方式是:
底层:负责读取 GPIO
按键模块:负责消抖、短按、长按
业务逻辑:只处理按键事件
比如主程序只关心:
if (short_event)
{
short_event = 0;
menu++;
}
if (long_event)
{
long_event = 0;
enter_setting_mode();
}
这样代码就舒服多了。
菜单不用关心按键有没有抖。
业务不用关心低电平有效还是高电平有效。
以后换按键、改消抖时间、加长按重复触发,也不会把整个工程改乱。
06 最后记住一句话
按键消抖看起来是小问题。
但它考验的是嵌入式编程思维。
GPIO 读到的是电平。
用户操作才是事件。
不要一看到低电平就执行。
不要动不动就 delay 堵住程序。
真正适合项目的按键写法,应该是:
定时扫描 + 消抖计数 + 状态判断 + 事件输出
下次你的 STM32 按键又出现“按一次触发多次”“短按长按分不清”的问题,别急着怀疑硬件。
先看看代码是不是还停留在:
按下 -> delay -> 执行
很多按键翻车现场,问题就出在这里。
如果这篇文章对你有帮助,建议收藏起来,转发给正在学 STM32 的朋友。也欢迎留言说说,你项目里遇到过哪些离谱的按键问题。
更多推荐


所有评论(0)