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

刚开始学 STM32,按键扫描写在 while(1) 里,按一下 LED 翻转,效果很好。心里还挺踏实:这不挺简单的吗?

结果项目一复杂,问题就来了。

加了 OLED 显示,屏幕刷新开始卡。加了串口通信,数据偶尔接收不及时。再加几个菜单按键,程序响应越来越慢。最尴尬的是,你明明只是在按键函数里多写了几行逻辑,整个系统却像“变笨”了一样。

很多初学者第一反应是:

是不是 STM32 主频不够?
是不是 HAL 库太慢?
是不是代码写多了?

其实,大概率不是芯片不行,而是你的按键扫描方式出了问题


一、最常见的写法,看起来没问题

很多人最开始都会这么写:

while (1)
{
    Key_Scan();      // 扫描按键
    OLED_Show();     // 刷新屏幕
    UART_Task();     // 处理串口
    ADC_Task();      // 采集传感器
}

看起来很合理,对吧?

主循环里依次执行任务。按键、显示、串口、ADC,一个都没落下。

但真正的问题,往往不在 while(1),而是藏在 Key_Scan() 里面。


二、真正拖慢程序的,是阻塞式按键扫描

很多初学者为了按键消抖,会写成这样:

void Key_Scan(void)
{
    if (KEY1 == 0)              // 检测到按键按下
    {
        delay_ms(20);           // 延时 20ms,用来消抖

        if (KEY1 == 0)          // 再次确认按键仍然按下
        {
            LED_Toggle();       // 执行按键功能,比如翻转 LED

            while (KEY1 == 0);  // 等待按键松开
        }
    }
}

这段代码在学习阶段没毛病。

点个灯、控制蜂鸣器、做个小实验,都能正常跑。

但放到真实项目里,它就是一个隐藏炸弹。

为什么?

因为:

delay_ms(20) 会卡住 CPU。
while (KEY1 == 0); 会一直等用户松手。

也就是说,只要程序执行到这里,CPU 就被按键函数“扣住”了。

这时候 OLED 还刷不刷新?
串口还接不接收?
ADC 还采不采样?
电机控制还更不更新?

答案是:都得等。

这就是为什么你一按按键,系统其他功能就开始变慢。

不是程序真的跑不动,而是 CPU 被按键函数堵住了。


三、菜单项目里,这个坑尤其明显

比如你做一个 STM32 参数设置界面:

  • KEY1:切换菜单
  • KEY2:参数加
  • KEY3:参数减
  • KEY4:确认保存

刚开始每个按键写一个 if,还能接受。

后来你又想加:

  • 短按切换
  • 长按连加
  • 双击返回
  • 组合键进入设置模式

于是按键扫描函数越来越长,里面塞满了各种业务逻辑。

最后代码很容易变成这样:

void Key_Scan(void)
{
    if (KEY1 == 0)
    {
        delay_ms(20);              // 消抖
        if (KEY1 == 0)
        {
            menu_index++;          // 切换菜单
            OLED_Refresh();        // 顺手刷新屏幕

            while (KEY1 == 0);     // 等待松手
        }
    }

    if (KEY2 == 0)
    {
        delay_ms(20);              // 消抖
        if (KEY2 == 0)
        {
            temp_set++;            // 修改温度设定值
            Save_Flag = 1;         // 设置保存标志

            while (KEY2 == 0);     // 等待松手
        }
    }
}

看起来只是多写了几个按键。

实际上,按键扫描函数已经变成了“堵路大哥”。

它不仅负责检测按键,还负责菜单、显示、参数处理。

项目越写越乱,响应自然越来越慢。


四、正确思路:按键只上报事件,不要霸占主循环

项目里更推荐的思路是:

按键扫描不要阻塞主循环。
按键底层只负责识别动作。
真正的功能处理,交给主循环。

也就是说,按键函数不要直接做一大堆事情。

它只需要告诉系统:

  • KEY1 短按了一次
  • KEY2 长按触发了
  • KEY3 松开了
  • KEY4 双击了

至于这个事件拿来切换菜单,还是修改参数,应该由主循环里的业务逻辑决定。

这样代码才不会越写越乱。


五、推荐做法:定时器周期扫描按键

比如用定时器每 10ms 扫描一次按键。

到了时间,就读一次 GPIO。
没到时间,就不要管它。

注意:

定时器中断里不要写复杂业务。
不要在中断里刷新 OLED。
不要在中断里保存 Flash。
不要在中断里处理一堆菜单逻辑。

中断里只做轻量处理,最多设置一个按键事件标志。

示例代码:

volatile uint8_t key1_event = 0;   // KEY1 短按事件标志

// 定时器中断函数,假设每 10ms 进入一次
void TIMx_IRQHandler(void)
{
    static uint8_t key_state = 1;  // 记录按键状态,1 表示松开,0 表示按下
    static uint8_t cnt = 0;        // 消抖计数器

    if (TIM_GetITStatus(TIMx, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIMx, TIM_IT_Update);

        if (KEY1 == 0)            // 当前检测到按键按下
        {
            if (cnt < 3)
            {
                cnt++;            // 连续检测 3 次,约 30ms,用于消抖
            }
            else if (key_state == 1)
            {
                key_state = 0;    // 状态切换为“已经按下”
                key1_event = 1;   // 上报一次 KEY1 短按事件
            }
        }
        else                      // 当前检测到按键松开
        {
            cnt = 0;              // 清空消抖计数
            key_state = 1;        // 状态恢复为“松开”
        }
    }
}

然后主循环里处理事件:

while (1)
{
    if (key1_event)
    {
        key1_event = 0;          // 先清除事件,避免重复执行

        menu_index++;            // 在主循环里处理菜单切换
        OLED_Update_Flag = 1;    // 设置屏幕刷新标志
    }

    OLED_Task();                 // OLED 显示任务继续执行
    UART_Task();                 // 串口任务继续执行
    ADC_Task();                  // ADC 采样任务继续执行
}

这样一改,程序运行状态完全不一样。

按键扫描不会再死等。
主循环不会被按键卡住。
OLED、串口、ADC 都能正常跑。
后面加功能,也不会互相拖累。


六、如果要支持长按,就用状态机

很多人写长按,会这样写:

while (KEY2 == 0)
{
    temp_set++;       // 长按时参数不断增加
    delay_ms(100);    // 每 100ms 增加一次
}

这个写法看着很直观,但项目里非常危险。

用户一直按着,CPU 就一直陪它耗着。
其他任务自然会被影响。

更好的方式是:定时器里计数,到了时间就上报长按事件。

if (key_press_time > 100)     // 10ms 扫描一次,100 次约等于 1 秒
{
    key_long_event = 1;       // 上报长按事件
}

主循环拿到 key_long_event 后,再去执行参数连加、菜单滚动等操作。

这样做的核心还是那句话:

不要让按键函数等结果。
要让按键函数产生事件。


七、项目经验总结

按键不是小功能。

它是人和设备交互的入口。

入口写乱了,后面的显示、通信、采样、控制都会被影响。

很多 STM32 项目“越写越卡”,不一定是芯片慢,也不一定是库函数慢,而是早期图省事,把按键扫描写成了阻塞式。

学习阶段可以简单写。
项目阶段一定要改思路。

最后记住这几个原则:

  1. 不要在按键扫描里长时间 delay
  2. 不要用 while 死等按键松开
  3. 不要把菜单业务塞进按键底层
  4. 按键扫描只负责识别动作
  5. 业务逻辑交给主循环处理
  6. 复杂按键功能,优先考虑定时器扫描加状态机

你以为按键只是几行 GPIO 判断。

但在真实项目里,它可能就是程序变慢、响应卡顿、功能互相影响的根源。

所以下次你的 STM32 项目出现“按键一多就卡”的问题,别急着怀疑芯片,也别急着换库。

先回头看看你的 Key_Scan()

问题很可能就在这里。


关注我,后面继续聊更多单片机项目里“看着简单、实际很坑”的问题。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐