矩阵键盘扫描:用 M+N 根线读 M×N 个按键
更新时间:2026-09-24。本文是「集成电路大类 · 芯片设计」**入门层(beginner)**数字逻辑基础的一篇。门锁上的 12 位按键、电话机的拨号盘、微波炉面板上那一片按键,打开外壳看 PCB,几乎都不是一键接一根线,而是按键排在横竖两组线的交叉点上。给一堆按键,最省管脚的接法就是排成矩阵。它和 LED 点阵动态扫描是同一套「分时复用」思想,只是把输出显示反过来做输入检测。
本文要回答的问题
- 4×4=16 个键凭什么只要 8 根线?行线、列线各配成输入还是输出?
- 扫描一拍里具体发生什么:谁输出、谁输入、读到什么?
- 多键同时按时为什么会冒出「鬼影键(ghost)」?二极管为什么能防?
- 机械触点的按下抖动、长按、自动重复在扫描状态机里怎么处理?
- 扫描频率、响应延迟和 CPU 占用之间怎么平衡?
一、先算一笔管脚账:一键一线贵在哪
最朴素的接法是一个按键占一根 GPIO,另一端接地,IO 配上拉读电平。16 个键就要 16 根线。在 MCU 管脚紧张、或者按键要挂在一颗专用扫描芯片上时,这个开销很不划算:48 脚的单片机去掉电源、晶振、下载口,剩下能自由使用的 IO 往往不到 30 根,面板上再来 20 个键,别的外设就别接了。
矩阵接法做了一个交换:用时间换管脚。把 M 条行线和 N 条列线垂直交叉,每个交叉点放一个按键,键按下时把对应的一条行线和一条列线短接,于是 M×N 个开关只挂在 M+N 根线上。4×4 的 16 键只用 8 根 IO,4×5 的 20 键用 9 根,8×8 的 64 键也只要 16 根——键越多,省得越狠。代价是主机必须不停轮转扫描,不能像独立按键那样随时直接读电平。
| 键盘规格 | 按键数 | 一键一线 | 矩阵 M+N | 省下的 IO |
|---|---|---|---|---|
| 3×4(电话键) | 12 | 12 | 7 | 5 |
| 4×4(常见小键盘) | 16 | 16 | 8 | 8 |
| 4×5(20 键面板) | 20 | 20 | 9 | 11 |
| 8×8(全键盘雏形) | 64 | 64 | 16 | 48 |
二、行列交叉:按键就是一个跨接开关
矩阵键盘扫描的硬件约定很固定:M 条行线配成输出,N 条列线配成输入,每条列线用内部或外部电阻上拉到高电平。扫描时,每次只把一条行线输出低电平,其他行线输出高(或切换为高阻),然后读 N 条列线。没有键按下时,列线被上拉电阻稳稳拉在高电平;一旦当前这一行上有键按下,这条行线就通过闭合的按键把对应列线拽低,于是读到「某一列为低」。键位坐标就此确定:当前拉低的是第几行,读到低的是第几列,交叉点就是那个键。
这张图定格在扫描的一拍:计数器给出独热(one-hot)行码,四条行线里只有 R2 输出低电平;按下的键把 R2 和 C1 短接,C1 被拉低,其余三列仍被上拉为高。下一拍计数器轮转,R3 拉低、R2 恢复高,如此循环。4 拍扫完 4 行,任意时刻按下的键最多再过 4 拍就会被扫到。
把连续 4 拍列出来,就是一张完整的扫描轨迹表。假设只有数字键「5」(R2,C1)被按住,每个节拍 10ms:
| 节拍 | 时刻 | 拉低的行 | C0 | C1 | C2 | C3 | 本拍结论 |
|---|---|---|---|---|---|---|---|
| 1 | 0ms | R0 | 1 | 1 | 1 | 1 | 无键 |
| 2 | 10ms | R1 | 1 | 1 | 1 | 1 | 无键 |
| 3 | 20ms | R2 | 1 | 0 | 1 | 1 | (R2,C1) 闭合 |
| 4 | 30ms | R3 | 1 | 1 | 1 | 1 | 无键 |
| 5 | 40ms | R0 | 1 | 1 | 1 | 1 | 无键(进入第二遍) |
注意第 4 拍:虽然键还按着,但此刻拉低的是 R3,被按下键所在的 R2 输出高,C1 被上拉拉回高电平,所以读不到。一个按住的键在每遍扫描里只有一拍能被看到,这正是「分时」二字的含义,也是去抖计数必须按「扫描遍」而不是按「节拍」归并的原因——同一个键每 4 拍才轮到一次有效观察。
列线必须上拉,原因和普通输入口一样:键没按下时列线不接任何驱动源,悬空会飘,电平在 0 和 1 之间随机晃,误报必然出现。上拉电阻保证悬空时读到确定的高,只有按键闭合才被行线主动拉低。想再省管脚,可以用一个 3-8 译码器把 3 根扫描线扩成 8 条行线,列侧再用一个编码器把 8 位列码压成 3 位,64 键理论上 6 根线加两片逻辑就能覆盖。
顺便回答一个常见疑问:为什么固定「行输出、列输入」,能不能反过来?可以。行列的角色完全是软件配置出来的,把列配输出、行配上拉输入,逐列拉低、读行,结论一样成立。实际怎么选看 PCB 布线和 MCU 哪一侧带可编程上拉:多数 MCU 只有部分引脚支持内部上拉,就让这一侧做列输入,省掉外部电阻;两侧布线长度差异大时,通常把更容易耦合干扰的一侧配成输出(输出不怕干扰),安静的一侧做输入。极性也可以整体反过来——逐行输出高、列用下拉输入,按下读到高,只是历史上 TTL 输入低电平抗噪更好,「拉低读低」成了惯例。
概念上的核心 Verilog 只有几行:
// 行扫描码独热轮转;列线上拉输入;低有效
logic [M-1:0] row_out;
always_ff @(posedge clk)
if (scan_ce) row_out <= {row_out[0], row_out[M-1:1]}; // 循环右移
wire any_press = ~(&col_in); // 任一列被拉低 = 当前行有键三、重键与鬼影:矩形三个角按下,第四个角「凭空出现」
只支持单键时,上面的逻辑无懈可击。麻烦从多键同时按开始。假设 (R1,C1)、(R1,C2)、(R2,C1) 三个键同时按下——它们正好占了一个矩形的三个角。扫描轮到 R2 时,R2 本来要通过 (R2,C1) 拉低 C1;但 C1 又通过闭合的 (R1,C1) 接到 R1,R1 此刻虽输出高,却挡不住电流继续经由 (R1,C2) 流到 C2。于是扫描 R2 这一拍,C2 也被读到低,系统报告「(R2,C2) 键按下」——这个键根本没有人碰,它就是鬼影键(ghost key)。三个真实开关在矩阵里搭出了一条绕到第四个角的导电通路。
把这条绕行通路按电流方向走一遍:R2 输出低,(R2,C1) 闭合把 C1 拉低;C1 又经闭合的 (R1,C1) 接到 R1;此刻轮到 R2 扫描,R1 虽是输出高,但高电平的驱动能力被两条低阻通路「架」住,电流从其他行的高电平经 (R1,C2) 这个闭合键灌到 C2,把 C2 也拖到逻辑低附近。扫描逻辑并不知道这条绕路,它只认「C2 为低」这个事实,于是 (R2,C2) 这个从未按下的键被坐实。值得强调的是,触发条件不是「按了三个键」这么笼统,而是恰好占住某个矩形的三个角;三个键排在同一条行或同一条列上并不会产生鬼影,这也是判断现场问题时区分「抖动误报」和「鬼影误报」的依据——鬼影键永远和真实键凑成矩形。
两种工程对策:
- 每只键串联一只二极管:电流只能从列流向行(或反之),上面那条绕行路径在某一段必然反偏截止,鬼影通路被切断,任意多键同按都能逐个识别,称为 N-key rollover,游戏键盘和高档矩阵键盘都这么做,代价是每个键多一只器件;
- 软件整帧忽略:一拍里若读到同一行有多列为低、或多行同时出现闭合点,说明存在多键组合,本帧结果全部丢弃,只在「恰好一个闭合点」时报键。廉价薄膜键盘、家电面板普遍采用,单键操作完全可靠,代价是多键同按会失灵。
还有一种介于两者之间的做法:对 Shift、Fn 这类修饰键所在的行列做特殊处理,保证「修饰键 + 普通键」的组合能用,其余多键仍然忽略,成本最低、体验够用。
四、扫描状态机:去抖、按下事件、长按与自动重复
机械按键的金属触点在合拢瞬间不会立刻贴死,而是弹开、贴合、再弹开,在 5~15ms 内抖上好几轮才稳定。直接拿原始电平当结果,按一次键会被识别成一串乱码。因此矩阵键盘扫描一定挂在一个低频定时节拍下(典型 5~10ms 一拍,比防抖窗口的要求略快),由状态机消化,而不是让 CPU 死等电平稳定。
图中状态机每个扫描节拍跑一遍,按键的一生被切成五段:空闲时什么都不做;一旦某键位读到低,进入「候选按下」并开始计数,计数期间电平又跳回高就判定为抖动、原路退回;只有同一键位连续 N 拍(如 2~3 拍,约 10~30ms)读到相同低电平,才确认按下,发出一个单周期的 press 脉冲;之后进入保持态,长按就是保持时间超过阈值(如 1 秒),自动重复就是保持期间每隔固定间隔(如 100ms)重发一次 press;松开时同样要连续 N 拍读到高才发 release,防止触点回弹造成假松开。
用前面的时序算一笔具体账:节拍 10ms,4 行键盘扫一遍 40ms。「连续 2 拍读到同一键」的去抖判据必须按扫描遍理解,实际就是连续 2 遍、约 80ms 内该键位两次轮到时都为低,才确认按下;加上最坏情况下第一拍就要等近 40ms 才扫到,一次按键从上电到报出 press 的典型延迟落在 40~120ms——和机械键盘宣传的「毫秒级响应」听起来差很远,但人手本身的动作在百毫秒量级,这个延迟日常使用完全无感,真正需要更快的是游戏场景,那边靠二极管加多键扫描硬件把延迟压到个位数毫秒。
在纯软件实现里,这套状态机通常就是定时中断里的一张二维表:表中每个键位存上一次电平和去抖计数,中断服务程序逐行输出独热码、读列、逐键更新表格,发现确认的状态变化就往一个环形缓冲里塞键值。主程序只管从缓冲取键,完全不碰 GPIO。硬件扫描 IP 则把这张表做成寄存器阵列,发现新键后通过 valid-ready/FIFO 上报,或者直接拉中断线通知 中断/DMA 取走。
五、扫描频率、延迟与功耗的取舍
矩阵键盘扫描有三个互相牵扯的参数。扫描一遍需要 M 拍,每行一拍,所以 8 行键盘最坏情况下要等 8 个节拍才能发现刚按下的键。节拍取 10ms,最坏延迟就是 80ms——人对按键的感知极限大约在 100ms 上下,8 行以内用 5~10ms 节拍都算跟手。
节拍是不是越快越好?不是。去抖要求的是「连续观察 10~30ms」,这个窗口由机械触点的物理特性决定,节拍再快也压缩不了;节拍超过 1ms 之后,提升的只是最坏延迟,付出的却是中断更频繁、CPU 占用更高。反过来,把节拍放慢到 20ms,8 行键盘最坏 160ms 才有反应,敲起来会觉得「肉」。实际工程的取值几乎都集中在 5~20ms。
功耗敏感的电池产品还有两招:一是大幅降低扫描频率(如每 50ms 扫一遍),代价是响应变慢,门锁、遥控器可以接受;二是平时彻底停掉扫描时钟(见低功耗入门),把列线或全部 IO 配成按键中断唤醒源,任何按键合拢都触发 EXTI 把芯片叫醒,醒来后再启动周期扫描精确定位键位。这样待机时扫描逻辑零耗电,是遥控器和便携仪表的标准做法。
六、典型硬件参数与键值映射
落到画板和代码层面,矩阵键盘还有一批反复出现的工程细节:
- 常见规格:3×4 电话键(7 根 GPIO)、4×4 十六键(8 根)、4×5 二十键面板(9 根);行侧加一片 3-8 译码器,3 根线就能扫 8 行;
- 上拉阻值:内部上拉一般几十 kΩ,弱上拉省电但抗干扰差;工业面板常用 4.7~10kΩ 外部上拉,配合 RC 滤波抑制线缆耦合的尖峰;
- 非活动行的处理:没有轮到的行线输出高或切成高阻,避免它与按下的键形成额外电流路径,尤其在多键场景下,输出高的行正是鬼影电流的源头;
- 键值映射:扫描器报出的是行列坐标(如 8 位键码 = 行号拼列号),固件里再放一张映射表把坐标翻译成 ASCII、扫描码(scan code)或自定义命令,换面板布局只改表、不改扫描逻辑;
- 长按与双击的软件扩展:状态机之外可以再加一个时间窗判断,按住超过阈值发一个「长按」键值(门锁里常用来触发反锁),短时间内两次确认按下则发「双击」,这些都建立在去抖之后的干净事件上,直接处理原始电平会把双击抖成四五次。
七、和 LED 点阵扫描的同构关系
矩阵键盘与 LED 点阵/数码管动态扫描在硬件拓扑上完全对称,放在一起看最容易理解:
| LED 点阵扫描 | 矩阵键盘扫描 | |
|---|---|---|
| 行的角色 | 输出显示数据 | 输出独热扫描码 |
| 列的角色 | 逐列选通 | 输入读电平(上拉) |
| 复用靠什么 | 视觉暂留(余晖) | 机械去抖(毫秒级窗口) |
| 串二极管的理由 | 防反向导通、防鬼影 | 阻断寄生回流、防 ghost |
| 核心时序部件 | 扫描计数器 + 译码器 | 扫描计数器 + 译码器 |
两者都用一个独热/译码的扫描计数器轮流转,把「M×N 个元件」压缩成「M+N 根线加时间」;区别仅在驱动方向——那边是输出点灯,这边是输入读开关;判据也不同——那边骗人眼,这边骗不过机械抖动。连「串二极管防鬼影」都是同一条原理:阻断矩阵里不该存在的电流绕行路径。理解了其中一个,另一个可以直接平移。
这一篇在知识体系里的位置
本篇属于芯片设计入门层的数字接口与 GPIO 主题,回答「一堆开关怎么最省管脚地接进数字系统」。向硬件基础读,先有 GPIO 与上下拉解释列线为什么必须上拉,脉冲整形与防抖解释去抖的时序原理;向平行技巧读,LED 矩阵扫描、独热轮转和译码器/编码器是同一套分时复用家族;向系统接口读,键码最终通过 valid-ready/FIFO和中断/DMA交给 CPU。
总结
矩阵键盘扫描就是把 M×N 个按键排在 M 条行线与 N 条列线的交叉点、按下即短接行列,用 M+N 根 IO 替代一键一线:行线配输出用独热码逐行拉低,列线配上拉输入读回,某列为低时「当前行号加该列号」唯一确定键位;多键构成矩形三角时会借闭合开关在第四个角产生鬼影键,串二极管可支持任意多键,廉价方案则在检测到多个闭合点时整帧忽略;扫描在 5~20ms 定时节拍下由状态机完成,同一键连续 2~3 拍读数一致才确认,再产生 press/release 单周期事件并派生长按与自动重复,平时可时钟门控、靠按键中断唤醒——它与 LED 点阵扫描是同一套行列分时复用拓扑的输入与输出两面。