你是不是也遇到过这种情况?

明明只按了一下按键,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 的朋友。也欢迎留言说说,你项目里遇到过哪些离谱的按键问题。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐