矩阵键盘扫描必须用非阻塞状态机实现:每轮主循环仅执行一行扫描、读取及消抖计数,行列IO需动态切换方向并加位掩码校验,否则易丢键、误判或触发鬼影键。

矩阵键盘扫描必须用非阻塞方式,否则会丢键
单片机跑C++(比如ARM Cortex-M + CMSIS或Arduino Core)时,delay() 或 while(!key_pressed) 这类阻塞写法在矩阵扫描中极危险。主循环一旦卡住,下一行扫描就延迟,快速连按直接漏掉;更糟的是,如果用SysTick做周期扫描但消抖用了阻塞延时,整个状态机就瘫痪了。
真正可行的路径只有一条:把扫描、消抖、事件生成全部拆成「每轮主循环只做一步」的状态迁移。比如用一个 scan_step 变量控制当前扫到第几行,再用 debounce_counter[ROW][COL] 记录每个键的连续稳定低电平次数。
- 每次主循环只拉低一行、读一列、更新对应8个键的计数器
- 计数器满阈值(如3次)才置
key_down[ROW][COL] = true - 松开检测同理:连续读到高电平满阈值后才置
key_up[ROW][COL] = true - 所有操作都在
void keyboard_task()里完成,绝不调用任何阻塞函数
行列IO方向不能固定,必须动态切换
很多初学者把行口硬设为输出、列口硬设为输入,结果发现某些键按下去没反应——问题出在电气特性上。当某行被拉低、某列被读取时,如果该列没接上拉电阻,浮空电平不可靠;而如果列口始终设为输入,又没法在反转法里把它当输出用。
正确做法是:扫描前统一把所有行列口设为推挽输出并拉高,再按需切换单个方向。例如逐行扫描时:
- 先设所有列口为输入+上拉(
GPIO_MODE_INPUT_PULLUP) - 再把当前行口设为推挽输出,写
0;其余行口设为高阻或开漏(避免灌电流冲突) - 延时1–2μs等电平稳定,再读列口
- 扫完一行后,立刻把该行口恢复为高阻或输入,防止干扰下一轮
不这么做,可能触发「鬼影键」:比如按下 (0,0) 和 (1,1),程序误判出 (0,1) 和 (1,0) 都被按下。
read_column_state() 返回值必须做位掩码校验
直接用 GPIO_ReadInputData(GPIOx) 或 HAL_GPIO_ReadPin() 读整组IO,返回的是寄存器原始值,不是你想当然的「0/1数组」。比如4列接在GPIOB的pin8~11,读回来的 GPIOB->IDR 是32位整数,你要提取bit8~11还得手动掩码。
常见错误是写成 if(col_val & 0x01) 却忘了对齐——实际列线可能对应 bit9,结果永远读不对。稳妥写法是预定义掩码:
== 0
- 调用
scan_one_row()推进扫描步进 - 对每个已确认按下的键,递增
hold_time[ROW][COL]
然后在主循环里检查:if (hold_time[r][c] > 50) 表示已长按500ms,可发长按事件;if (hold_time[r][c] % 10 == 0 && hold_time[r][c] > 50) 就能实现100ms间隔的连发。这样连发节奏完全由定时器精度决定,不受主循环负载影响。
最容易被忽略的一点:松开按键后,hold_time 必须清零,否则下次按下会继承上次的计时,导致首次触发就误判为长按。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











