news 2026/10/15 6:12:15

汇编实现矩阵键盘扫描:小资源下的确定性方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
汇编实现矩阵键盘扫描:小资源下的确定性方案

简介:这份资源面向单片机初学者与嵌入式爱好者,聚焦51单片机汇编语言实现矩阵键盘扫描这一经典入门项目,帮助读者理解行线列线交叉检测按键的硬件原理与汇编编程方法。压缩包内共1个doc文件,约283KB,完整收录了从变量定义、主函数、键盘扫描子程序到数码管显示、移位操作、延时子程序及共阴极段码表的源代码与说明,代码结构清晰,便于对照学习。目前已有569人学习下载,适合作为汇编语言与单片机输入输出操作的实践参考。通过这份资料,读者可以掌握矩阵键盘扫描的完整实现流程,理解键值计算、去抖动处理、数码管动态显示与移位控制等关键环节,并借助源码注释快速上手调试,为后续嵌入式开发打下底层硬件操作基础。

1. 矩阵键盘扫描为什么在汇编里反而更稳

很多人第一次接触矩阵键盘,是在某款入门开发板上:4 行 4 列,8 根 IO 口,能读出 16 个按键。用 C 语言写,一个双重循环加 10 毫秒消抖就完事。可一旦换成汇编,尤其是资源只有 1K 到 4K 字节的单片机,事情就变得微妙起来——代码空间要抠着用,寄存器要反复复用,扫描周期还得卡准,否则按键要么连击、要么迟钝。

我真正被矩阵键盘“教育”是在一个模拟项目里:某工业面板需要 12 个按键,主控是一颗 8 位单片机,程序空间只剩不到 600 字节。C 编译器生成的扫描代码体积超标,中断响应也偶尔超时。最后用汇编写了一版行列扫描,代码压到 200 字节出头,扫描周期稳定在 2 毫秒以内。从那以后我认定:矩阵键盘扫描这件事,汇编不是炫技,而是在小资源场景下把确定性握在自己手里。

这篇文章面向的是已经能看懂单片机数据手册、知道 IO 口方向寄存器怎么配的从业者。如果你正在用汇编做按键扫描,或者被 C 代码的体积和时序逼到墙角,下面的内容可以直接抄作业。我会从行列扫描原理讲起,一步步给出可复现的汇编代码框架、参数设置和排错方法,最后落到一个能稳定跑的完整扫描流程。

2. 行列扫描的电气本质与汇编实现选型

2.1 为什么是“行输出、列输入”而不是反过来

矩阵键盘的物理结构就是行线和列线的交叉点放按键。扫描的本质是:让行线逐行输出低电平,其余行保持高电平,然后读列线。如果某列读到低电平,说明该列与当前低电平行交叉处的按键被按下。

反过来行输入、列输出行不行?电气上可以,但工程上不划算。原因有两个:第一,行线作为输出时,同一时刻只有一行被拉低,驱动电流集中在一根 IO 上,对端口的灌电流能力要求明确;第二,列线作为输入时,通常要开内部上拉,读到的低电平直接对应按键,逻辑清晰。如果反过来,列线输出低电平,行线输入,那么同一时刻多行可能同时被按下,读回来的行状态会互相干扰,消抖和键值映射都更麻烦。

常见做法是行线接单片机输出口,列线接输入口并开启内部上拉。以 4×4 键盘为例,8 根 IO 分两组:低 4 位做行输出,高 4 位做列输入。这个分配不是固定的,但一旦确定,键值表就要跟着走。

2.2 汇编里做扫描的三种结构:轮询、定时中断、状态机

在汇编里实现扫描,结构选择直接决定代码复杂度和实时性。

轮询方式最简单:主循环里调用扫描子程序,扫完一轮延时消抖,再扫下一轮。优点是代码直白,缺点是主循环被拉长,如果还有其他任务,响应会变慢。适合功能单一的小项目。

定时中断方式把扫描放在定时器中断里,比如每 2 毫秒进一次中断,扫一行,4 行轮完是 8 毫秒,再配合状态计数做消抖。优点是扫描周期固定,主循环不受影响;缺点是中断服务程序要保存和恢复现场,寄存器占用要精打细算。

状态机方式是把按键过程拆成“空闲、消抖、按下、连发、释放”几个状态,每个状态只做一件事。代码量比前两种大,但逻辑最清晰,适合按键数量多、需要长按连发的场景。

我一般会这样选:按键少于 8 个、主循环不忙,用轮询;按键多、有显示刷新或通信任务,用定时中断;需要长按和双击,上状态机。下面给出的代码框架以定时中断扫描为主,因为它最能体现汇编对时序的控制力,也最容易暴露资源冲突的坑。

2.3 扫描周期与消抖时间的参数计算

消抖时间不是随便定的。机械按键的抖动时间通常在 5 到 20 毫秒之间,所以软件消抖的采样间隔要大于抖动时间。常见做法是每 10 毫秒采样一次,连续两次读到相同键值才确认。

如果放在定时中断里,扫描一行的时间 = 中断周期 × 行数。比如中断周期 2 毫秒,4 行扫完是 8 毫秒。要凑够 10 毫秒消抖,可以每扫完一轮计数一次,计到 2 次就是 16 毫秒,计到 1 次是 8 毫秒。我通常取 2 次,牺牲一点响应速度换稳定。

这里有个容易翻车的地方:中断周期不能太短。如果设成 100 微秒,4 行扫完 400 微秒,中断频率太高,主程序几乎跑不动。一般 1 到 5 毫秒是合理区间。具体值要看单片机主频和中断服务程序的指令数。假设主频 8MHz,一条指令 1 微秒,中断服务程序 50 条指令,那中断本身就要 50 微秒,周期设 1 毫秒时 CPU 占用 5%,可以接受。

2.4 键值映射表在汇编里的存放方式

键值映射就是把“第几行第几列”翻译成具体的键码。在汇编里,最省空间的做法是用查表指令,比如MOVC A, @A+DPTR。把 16 个键码按行优先顺序放在程序存储器里,扫描到行列后算出偏移量,一次查表就得到键值。

偏移量的算法:行号 × 4 + 列号。行号和列号都是从 0 开始。比如第 2 行第 3 列,偏移量是 2×4+3=11,查表第 11 个字节就是键码。

如果键盘不是 4×4,而是 4×3 或 3×4,表的大小和偏移算法要跟着改。常见错误是行列顺序搞反,导致键值错位。我的习惯是:在纸上画出键盘布局,标好行列编号,再按同样顺序写表,最后用万用表逐个按键验证。

3. 用汇编写一个可复现的 4×4 扫描程序

3.1 端口定义与寄存器分配

先确定硬件连接。假设使用某款 8 位单片机,P1 口低 4 位接行线,高 4 位接列线。行线输出低电平有效,列线输入带上拉。定义如下:

; 端口定义 PORT_KEY EQU P1 ; 键盘连接口 ROW_MASK EQU 0x0F ; 低4位为行 COL_MASK EQU 0xF0 ; 高4位为列 ; 寄存器分配 ROW_IDX EQU R0 ; 当前扫描行号 0-3 COL_VAL EQU R1 ; 读回的列值 KEY_CODE EQU R2 ; 最终键值 TMP_REG EQU R3 ; 临时寄存器 SCAN_CNT EQU R4 ; 消抖计数 LAST_KEY EQU R5 ; 上一次键值

这段定义里,ROW_MASK和COL_MASK用来分离行和列。ROW_IDX在扫描过程中从 0 到 3 循环,COL_VAL保存读回的列状态,KEY_CODE是查表后的结果。SCAN_CNT和LAST_KEY用于消抖比较。

寄存器分配的原则是:中断服务程序里用到的寄存器,要么在主程序里保护,要么用专用的寄存器组。如果单片机支持多寄存器组,把中断用的寄存器切到另一组,省去压栈弹栈的时间。这一点在 2 毫秒中断周期下很关键。

3.2 行扫描与列读取的核心代码

下面是扫描一行的核心代码。思路是:先把当前行对应的位拉低,其余行拉高,然后读列线,判断是否有低电平。

SCAN_ONE_ROW: ; 输入:ROW_IDX 当前行号 0-3 ; 输出:COL_VAL 列状态,低4位有效,0表示有按键 MOV A, ROW_IDX MOV DPTR, #ROW_TABLE ; 行输出表 MOVC A, @A+DPTR ; 取行输出值 MOV PORT_KEY, A ; 输出到端口 NOP ; 等待电平稳定 NOP MOV A, PORT_KEY ; 读回端口 ANL A, #COL_MASK ; 只保留列线 SWAP A ; 高4位移到低4位 MOV COL_VAL, A ; 保存列状态 RET ROW_TABLE: DB 0xFE, 0xFD, 0xFB, 0xF7 ; 第0行拉低,第1行拉低...

ROW_TABLE里存放的是行输出值。0xFE表示最低位为 0,其余为 1,对应第 0 行拉低。0xFD对应第 1 行,依此类推。MOVC A, @A+DPTR用ROW_IDX做偏移,取出对应的行输出值。

读回端口后,用ANL A, #COL_MASK屏蔽掉行线,只留列线。SWAP A把高 4 位移到低 4 位,方便后续判断。如果某列被按下,对应的位会是 0。比如读回0xE0,交换后是0x0E,低 4 位是1110,说明第 0 列被按下。

这里有两个NOP,是为了等待端口电平稳定。如果单片机主频较高,或者端口驱动能力弱,可以适当增加 NOP 数量。但也不能太多,否则扫描周期被拉长。我一般会在示波器上看行线波形,确认电平建立时间后再定。

3.3 消抖计数与键值查表

扫描到列状态后,不能立刻认定按键有效,要经过消抖。消抖的逻辑是:如果本次读到的列状态与上一次相同,计数器加一;如果不同,计数器清零,并更新上一次状态。当计数器达到阈值,才确认按键。

DEBOUNCE: ; 输入:COL_VAL 当前列状态 ; 输出:KEY_CODE 有效键值,0xFF表示无键 MOV A, COL_VAL JZ NO_KEY ; 列状态为0,说明无按键 CJNE A, LAST_KEY, NEW_KEY INC SCAN_CNT MOV A, SCAN_CNT CJNE A, #2, NO_KEY ; 连续2次相同才确认 ; 确认按键,查表 MOV A, ROW_IDX RL A ; 行号×2 RL A ; 行号×4 ADD A, COL_VAL ; 加上列号 MOV DPTR, #KEY_TABLE MOVC A, @A+DPTR MOV KEY_CODE, A RET NEW_KEY: MOV LAST_KEY, COL_VAL MOV SCAN_CNT, #0 NO_KEY: MOV KEY_CODE, #0xFF RET KEY_TABLE: DB '1','2','3','A' DB '4','5','6','B' DB '7','8','9','C' DB '*','0','#','D'

CJNE A, LAST_KEY, NEW_KEY比较当前列状态和上一次状态。如果不同,跳到NEW_KEY更新状态并清零计数。如果相同,计数器加一。CJNE A, #2, NO_KEY判断是否达到 2 次。达到后,用行号和列号算偏移,查KEY_TABLE得到键码。

KEY_TABLE按行优先排列,和键盘布局一一对应。这里用的是 ASCII 码,实际项目里可以换成自定义键码。注意COL_VAL是交换后的低 4 位,某位为 0 表示按下。但查表时用的是列号,不是列状态。所以COL_VAL需要转换成列号:如果COL_VAL是1110,列号是 0;1101列号是 1;1011列号是 2;0111列号是 3。这个转换可以用一个小的查表或者位扫描实现。上面的代码为了简洁,假设COL_VAL已经转换成列号,实际使用时需要加一段转换。

3.4 主循环与中断服务程序的配合

如果扫描放在定时中断里,主循环只负责处理键值。中断服务程序每 2 毫秒执行一次,每次扫一行,4 次扫完一轮。主循环检查KEY_CODE,如果有有效键值就执行对应动作。

TIMER_ISR: PUSH ACC PUSH PSW ; 重装定时器 MOV TH0, #HIGH(65536-2000) MOV TL0, #LOW(65536-2000) ; 扫描一行 LCALL SCAN_ONE_ROW LCALL DEBOUNCE ; 行号递增 INC ROW_IDX CJNE ROW_IDX, #4, ISR_END MOV ROW_IDX, #0 ISR_END: POP PSW POP ACC RETI

中断服务程序里先保存 ACC 和 PSW,因为扫描和消抖会用到累加器和标志位。定时器重装值根据主频计算,假设 12MHz 晶振,机器周期 1 微秒,2 毫秒需要 2000 个机器周期,所以重装值是 65536-2000。

ROW_IDX在中断里递增,从 0 到 3 循环。每进一次中断扫一行,4 次中断扫完一轮。消抖计数在DEBOUNCE里处理,连续两次读到相同列状态才确认。这样一轮扫描 8 毫秒,两次确认 16 毫秒,满足消抖要求。

主循环里读KEY_CODE,如果非 0xFF 就处理。处理完后要把KEY_CODE清回 0xFF,避免重复执行。如果按键需要释放检测,还要在中断里判断列状态是否恢复为全 1。

4. 扫描程序里最容易翻车的五个地方

4.1 现象:按键反应迟钝,要按很久才识别

原因:消抖计数阈值设得太大,或者扫描周期太长。比如中断周期 10 毫秒,4 行扫完 40 毫秒,再计 2 次就是 80 毫秒,人感觉明显延迟。

解决:把中断周期降到 2 到 5 毫秒,消抖计数保持 2 次。这样一轮扫描 8 到 20 毫秒,确认时间 16 到 40 毫秒,手感正常。如果还慢,检查是不是在中断里做了太多事情,比如查表、运算、串口发送,把中断服务程序精简到只做扫描和计数。

4.2 现象:多个按键同时按下时,键值乱跳

原因:行列扫描时,如果两个按键在同一列不同行,或者同一行不同列,读回的列状态可能同时有多位为 0。代码里如果只取最低位或者不做多键判断,就会随机返回其中一个键值。

解决:在DEBOUNCE里判断COL_VAL是否只有一位为 0。如果有多位为 0,说明多键按下,可以忽略或者返回一个特殊键值。判断方法是:COL_VAL & (COL_VAL-1),如果结果非零,说明有多位为 1(注意列状态是低有效,取反后判断)。实际汇编里可以用循环移位统计 0 的个数。

4.3 现象:按键松开后,键值一直不消失

原因:消抖逻辑只处理了按下,没处理释放。LAST_KEY和SCAN_CNT在松开后没有及时清零,导致KEY_CODE保持上一次的值。

解决:在DEBOUNCE里增加释放判断。如果COL_VAL为 0(无按键),把LAST_KEY和SCAN_CNT清零,KEY_CODE设为 0xFF。这样松开后键值立即失效。如果主循环处理键值后没有清KEY_CODE,也会导致重复执行,记得在处理完后手动清零。

4.4 现象:中断里读端口,偶尔读到错误值

原因:端口电平在切换行线后没有稳定,读回的是过渡态。或者中断优先级被其他中断打断,导致读端口时行线状态已经变了。

解决:在输出行线后加 2 到 4 个 NOP,等电平稳定再读。如果还有其他中断,把键盘扫描中断设为高优先级,或者在读端口前关中断,读完再开。关中断的时间要短,几个指令周期内完成,不影响其他中断响应。

4.5 现象:代码烧进去后,按键完全没反应

原因:端口方向寄存器没配置。行线要设为输出,列线要设为输入并开上拉。很多单片机复位后端口默认是输入,行线没有输出低电平,列线读回全是高,自然读不到按键。

解决:在初始化代码里明确设置端口方向。比如行线对应的位设为 0(输出),列线对应的位设为 1(输入)。如果单片机有上拉寄存器,把列线对应的位上拉使能。初始化完成后,先读一次端口确认列线为高,再启动定时中断。

5. 从能跑到好用:扫描程序的验证与优化技巧

5.1 用 IO 口翻转做扫描周期的“示波器”

没有示波器的时候,可以用一个空闲 IO 口做指示。在中断服务程序开头把 IO 拉高,结尾拉低,然后用万用表测电压,或者接一个 LED 看亮度。如果 LED 明显闪烁,说明中断周期太长;如果亮度稳定,说明周期合适。更精确的做法是用逻辑分析仪抓这个 IO 的波形,直接读出中断周期和扫描一轮的时间。

我习惯在扫描程序里留一个调试 IO,正式发布时再关掉。这个 IO 翻转的代码只有两条指令,对时序影响可以忽略,但排查问题时能省很多时间。

5.2 键值查表法的边界检查

查表之前一定要确认偏移量在表范围内。行号 0 到 3,列号 0 到 3,偏移量 0 到 15。如果列状态转换出错,列号可能变成 4 到 7,查表就会越界,读到程序存储器里的其他数据,表现为随机键值。

解决办法是在查表前加边界判断:如果行号大于 3 或列号大于 3,直接返回 0xFF。汇编里可以用CJNE比较,也可以用掩码操作。比如列号只保留低 2 位,ANL A, #0x03,这样即使出错也不会越界。行号在中断里递增,本身不会超过 3,但保险起见还是加一道判断。

5.3 长按连发与双击的实现思路

长按连发需要记录按键按下的持续时间。可以在消抖确认后,启动一个计数器,每 100 毫秒加一。当计数器达到阈值(比如 5,即 500 毫秒),触发连发,之后每 100 毫秒重复一次键值。双击则需要记录两次按下的时间间隔,如果小于 300 毫秒,判定为双击。

在汇编里实现这些,状态机会更清晰。定义几个状态:空闲、消抖、按下、连发、等待释放。每个状态只做一件事,用CJNE跳转。代码量会增加,但逻辑不会乱。如果资源实在紧张,可以只做长按,双击交给上位机处理。

5.4 用表格对比不同扫描周期的实际手感

下面这张表是我在 12MHz 单片机上实测的扫描周期与手感关系。测试条件:4×4 键盘,消抖计数 2 次,主循环空转。

中断周期一轮扫描确认时间手感评价
1 ms4 ms8 ms反应快,偶有连击
2 ms8 ms16 ms稳定,推荐
5 ms20 ms40 ms稍慢,可接受
10 ms40 ms80 ms明显延迟

从表里可以看出,2 毫秒是一个平衡点。1 毫秒虽然更快,但中断频率高,CPU 占用大,而且机械按键的抖动还没结束就采样,容易误判。5 毫秒以上手感变差,除非主频很低或者中断服务程序很长,否则不建议。

5.5 一个我常犯的错误:忘了在中断里保护寄存器

早期写汇编时,我在中断服务程序里直接用了 R0 到 R7,没有保存。主程序里也在用这些寄存器,结果中断返回后主程序的数据被改得面目全非,表现是按键偶尔失灵、显示乱码。后来养成习惯:中断里用到的寄存器,要么用另一组寄存器,要么在入口 PUSH、出口 POP。多花几条指令,换稳定。

还有一个教训:中断服务程序里不要调用太深的子程序。LCALL 会压栈,如果中断频繁,栈空间可能溢出。我一般把扫描和消抖写成宏或者内联代码,减少调用层次。如果必须调用,确保栈深度不超过预留空间。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 6:11:33

专科生毕业论文AI工具测评:从选题到降重的实战指南

又到了一年一度被毕业论文按在地上摩擦的季节。作为一个专升本过来人,我太清楚专科生写论文的痛苦了——选题像开盲盒,文献综述憋三天只写出两行,查重率居高不下,导师留言永远只有一句"再改改"。今年我干脆把市面上叫得…

作者头像 李华
网站建设 2026/10/15 6:11:02

TensorFlow工程落地实战:从选型部署到全链路优化

TensorFlow这个框架,在深度学习圈子里属于那种“谁都能说上两句,但真正把它用到生产环境的人并不算多”的工具。我最早接触它是在2018年前后,那时候还在用1.x的静态图模式,写一个简单的模型都要先搭计算图,再开会话&am…

作者头像 李华
网站建设 2026/10/15 6:09:32

低代码与手写代码的边界:哪些场景该用低代码?

1. “手写代码已老”这句话,到底在哪条赛道上才成立我们上一家公司有个内部审批系统,开发团队花了三周做出来,上线两周后又因为审批流程调整要改代码,改完还要发版、走发布窗口,业务方在群里催了三次,研发负…

作者头像 李华
网站建设 2026/10/15 6:09:28

TensorFlow实战指南:从张量概念到部署落地

聊到机器学习框架,TensorFlow 几乎是绕不开的一个名字。不管你是刚接触深度学习,还是已经用别的框架写过模型,最终大概率都要跟它打照面。原因倒不是什么技术上的绝对碾压,而是它把"从研究原型到生产部署"这条链路铺得太…

作者头像 李华
网站建设 2026/10/15 6:09:15

数控电源电路原理详解:从DAC反馈到ADC采样与保护设计

简介:这份文档面向电子工程、自动化控制方向的学生与硬件爱好者,系统讲解数控稳压电源的电路原理与设计实现,适合课程设计、毕业设计或电源类项目参考。资源为单个doc文档,压缩包约487KB,内容围绕STC89C51单片机主控方…

作者头像 李华