按键扫描还放 while 里?难怪你的 STM32 项目越写越卡!
你是不是也遇到过这种情况?
刚开始学 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 项目“越写越卡”,不一定是芯片慢,也不一定是库函数慢,而是早期图省事,把按键扫描写成了阻塞式。
学习阶段可以简单写。
项目阶段一定要改思路。
最后记住这几个原则:
- 不要在按键扫描里长时间
delay - 不要用
while死等按键松开 - 不要把菜单业务塞进按键底层
- 按键扫描只负责识别动作
- 业务逻辑交给主循环处理
- 复杂按键功能,优先考虑定时器扫描加状态机
你以为按键只是几行 GPIO 判断。
但在真实项目里,它可能就是程序变慢、响应卡顿、功能互相影响的根源。
所以下次你的 STM32 项目出现“按键一多就卡”的问题,别急着怀疑芯片,也别急着换库。
先回头看看你的 Key_Scan()。
问题很可能就在这里。
关注我,后面继续聊更多单片机项目里“看着简单、实际很坑”的问题。
更多推荐



所有评论(0)