☰
51单片机矩阵键盘扫描:从电平到键值的汇编实现与避坑指南
2026/10/28 11:56:38 网站建设 项目流程

简介:这份资源面向单片机初学者与嵌入式入门学习者,聚焦51单片机汇编语言实现矩阵键盘扫描这一经典练习,帮助读者理解行线列线交叉检测按键的硬件原理与汇编编程方法。压缩包共1个文件,为1个doc文档,约283KB,内含完整程序源代码与代码块说明,涵盖变量定义、主函数初始化、按键判断与键值计算、数码管显示与移位、清零及延时子程序、共阴极段码表等模块,并附有逐段注释便于对照阅读。已有569人学习下载,适合作为汇编入门实操参考,读者可从中掌握扫描去抖流程、键值计算与数码管动态显示的组织方式,为后续嵌入式开发打基础。

1. 矩阵键盘扫描:为什么四个 IO 能读出十六个键

十六个按键,只用了八个 IO,其中四个还在同一时刻只输出一个低电平——这件事第一次看原理图的人多半会愣一下:明明只驱动了一根线,凭什么知道是哪一行哪一列交叉处的键被按下?更反直觉的是,扫描程序里没有任何"读取按键编号"的指令,它读到的永远只是 P1 口某几位的 0 和 1,键值 01H、05H、09H、13 这些数字全是算出来的。

矩阵键盘扫描是嵌入式入门里绕不开的一道坎,也是很多人第一次真正面对"硬件电平如何变成软件语义"的地方。搞懂它,你收获的不只是一个键盘驱动,而是扫描、去抖、键值编码、显示复用这一整套在资源受限环境下的经典套路。下面从电平现象一路拆到汇编指令级实现。

2. 行列扫描的电平逻辑:一根低电平线如何定位一个键

2.1 硬件拓扑决定了"扫描"这个动作的存在

矩阵键盘的物理结构是 4 行 4 列共 8 根线,行线与列线交叉,每个交叉点串一个机械按键。按键没按下时行列之间断路,按下时该行与该列短接。如果给所有行线输出高电平、所有列线输出低电平,那么任何一次按下都会把某根行线拉低——这就是"有没有键按下"的检测原理,对应源码里的 KS 子程序:

KS: ; 检测有无按键按下 MOV P1, #0F0H ; 高4位(行线)全1,低4位(列线)全0 MOV A, P1 ; 读回P1口实际电平 XRL A, #0F0H ; 与0F0H异或:行线被拉低的位会变成1 RET ; A非零表示有键按下

这里有个容易被忽略的细节:MOV P1, #0F0H之后立刻MOV A, P1,读的不是刚才写进去的值,而是引脚上的真实电平。51 单片机的 IO 口是准双向结构,写 1 之后引脚处于弱上拉状态,可以被外部拉低;写 0 则是强下拉。所以"先写 1 再读"是读引脚的标准姿势,写 0 的位读回来永远是 0。XRL 那一步是把"本该为 1 的行线"和"实际电平"做异或,哪一位被按键拉低了,异或结果对应位就是 1,A 非零即代表有按下。

但 KS 只能回答"有没有",回答不了"是哪一个"。要定位,必须让行和列的电平组合具有唯一性——这就是逐列扫描的动机。

2.2 逐列拉低:把二维坐标压成一维键值

源码里 K2 到 K3 这一段是扫描的核心。它维护一个列扫描值 R2,初值11110111B,也就是 P1.3 为 0、其余为 1。注意 P1 的高四位是行线、低四位是列线,所以这个值的意思是"只把第一列拉低,四行全部释放为高"。

K2: MOV R2, #11110111B ; 第一列(P1.3)拉低,其余为1 MOV R0, #00H ; R0存列偏移,首列为0 K3: MOV P1, R2 ; 列扫描值送P1口 L0: JB P1.7, L1 ; 第0行仍为高?是则无键,查下一行 MOV A, #01H ; 第0行有键,行首键号01H AJMP LK L1: JB P1.6, L2 ; 第1行 MOV A, #05H ; 行首键号05H AJMP LK L2: JB P1.5, L3 ; 第2行 MOV A, #09H AJMP LK L3: JB P1.4, NEXT ; 第3行 MOV A, #13 LK: ADD A, R0 ; 键值 = 行首键号 + 列偏移 MOV R1, A SETB 40H SETB 7AH ; 置位"数字前移"标志 AJMP K4 NEXT: INC R0 ; 列偏移加一 MOV A, R2 JNB Acc.0, K4 ; 扫描值最低位已为0,四列扫完 RR A ; 循环右移,把0挪到下一列 MOV R2, A AJMP K3

逐行看这段代码,能读出三层设计。第一层是"行首键号"的选取:01H、05H、09H、13(十进制 13,即 0DH)。为什么不是 0、1、2、3?因为列偏移 R0 每次加 1,如果行首键号取 0、1、2、3,那么第 0 行第 1 列和第 1 行第 0 列会算出同一个值,坐标就撞车了。取 01H、05H、09H、0DH 这种间隔为 4 的基数,加上 0 到 3 的列偏移,正好铺满 1 到 16 且互不重叠——这是把二维坐标映射成一维编号最省事的做法,本质上是"行号乘 4 加列号"的变体,只不过乘 4 被提前算进了行首键号里。

第二层是 RR A 的循环右移。R2 初值最低位是 1,每扫完一列右移一次,0 的位置就从 P1.3 挪到 P1.2、P1.1、P1.0。JNB Acc.0, K4判断的是移位后最低位是否已经变成 0——当 0 被移到最低位,说明四列都扫过了,退出。用循环移位而不是查表或四次独立赋值,省掉了列号到掩码的转换,代价是可读性差一些,这是汇编里典型的空间换清晰度。

第三层是SETB 7AH。这个位标志在扫描命中后被置位,后面显示子程序靠它决定要不要做一次整体左移。也就是说,键值计算和显示刷新是通过一个位变量解耦的,扫描只管置标志,显示自己决定怎么消费。

2.3 键值编码与段码表的错位

算出来的键值 R1 落在 1 到 16 之间,但段码表 TABLE 只有 11 项:0 到 9 加一个 3FH(对应数字 0 的重复编码)。这里就出现了第一个需要留意的错位:键值 1 到 10 对应数字 1 到 9 和 0,键值 11(0BH)和 12(0CH)被留作功能键——退格和清零。源码里 DISPLAY1 开头正是这么判断的:

DISPLAY1: MOV A, R1 XRL A, #0BH ; 键值等于0BH? JZ DEL1 ; 是,走退格分支 MOV A, R1 XRL A, #0CH ; 键值等于0CH? JZ ZERO ; 是,走清零分支 JB 7AH, SWAP_SHU1 ; 否则看是否需要左移

用 XRL 加 JZ 判断相等,是 51 汇编里比 CJNE 更省字节的写法,因为 XRL 直接改 A 并置标志位,不需要额外比较操作数。代价是判断完 A 已经被破坏,所以每次判断前都要重新MOV A, R1。这种"破坏性比较"在资源紧张的汇编里很常见,读代码时如果没注意到 A 被改,很容易误判后续逻辑。

段码表本身是共阴极编码:80H 对应数字 0(段 a 到 g 中只有 g 不亮?实际 80H 是 dp 位,这里表首项 80H 更像占位或特殊符号),06H 是数字 1,5BH 是 2,一直到 6FH 是 9,3FH 是 0。表项顺序和键值顺序必须严格对齐,否则按 1 显示 2 这种玄学问题就会冒出来。这也是为什么键值从 1 开始而不是 0——表首留了一个位置给 80H,键值 1 正好索引到 06H。

3. 去抖与显示复用:一个主循环里塞进两套时序

3.1 两次 KS 加一次延时,去抖为什么必须做两遍

机械按键按下瞬间触点会弹跳,电平在几毫秒内反复跳变。如果 KS 第一次检测到非零就直接算键值,很可能读到的是抖动过程中的假电平,算出一个错误的行列组合。源码的处理是:

KEY_INT: LCALL KS ; 第一次检测 JNZ K1 ; 非零,可能真有键 LCALL DELAY1 ; 为零,延时后再查(这里逻辑上略绕) AJMP KEY_INT K1: LCALL DELAY1 ; 确认有键后先延时 LCALL KS ; 第二次检测 JNZ K2 ; 仍非零,确认按下 AJMP KEY_INT

标准做法是"检测到按下 → 延时 → 再检测",两次都非零才认定有效。源码在无键分支里也调了一次 DELAY1,看起来多余,实际是把"等待按键"变成了带延时的轮询,避免空转占满总线。DELAY1 的实现值得单独看:

DELAY1: ; 延时一 MOV R3, #10 D4: LCALL DISPLAY1 ; 延时期间仍在刷新显示 DJNZ R3, D4 RET

延时循环里调用 DISPLAY1,这是整个程序最巧妙也最容易翻车的地方。数码管是动态扫描显示,靠 P2 口轮流选通八个数位、P0 口送段码,每个数位只亮一小段时间,靠人眼视觉暂留合成稳定图像。如果延时期间不刷新显示,数码管就会闪烁甚至熄灭。把显示刷新嵌进延时循环,等于让"等待"这段时间也被利用起来,这是单线程裸机程序里做时间复用的经典手法。

但这也带来一个隐患:DISPLAY1 内部又调用了 DELAY2,而 DELAY2 是纯空转延时,不刷新显示。如果 DELAY2 太长,单个数位点亮时间过久,会出现亮度不均;如果太短,亮度不够。源码里 DELAY2 是MOV R4, #43套MOV R5, #12的双重循环,按 12MHz 晶振估算大约几毫秒量级,正好卡在视觉暂留的边缘。这个参数没有理论最优值,只能实测调,属于典型的血泪经验参数。

3.2 八位数码管的移位显示:S1 到 S9 的滑动窗口

显示缓冲区用了 S1 到 S9 九个字节,但只显示八位。多出来的 S9 是移位操作的暂存位。左移子程序 SWAP_SHU1 做的事情很直白:

SWAP_SHU1: MOV S9, S8 ; 整体左移一位 MOV S8, S7 MOV S7, S6 MOV S6, S5 MOV S5, S4 MOV S4, S3 MOV S3, S2 MOV S2, S1 CLR 7AH ; 清移位标志 LJMP DISPLAY1

新按下的键值存在 S1,每次左移就把整个缓冲区往高位推一格,最老的数字从 S8 挤到 S9 后被丢弃。这等价于一个长度为 8 的滑动窗口,新数字从右侧进入、旧数字从左侧溢出。用九个变量加八条 MOV 实现,而不是用指针加循环,是因为 51 的间接寻址要占用 R0/R1,而这两个寄存器在扫描和键值计算里已经被征用了,直接 MOV 反而更省寄存器。

右移子程序 SWAP_SHU2 是反方向,配合退格键使用:删掉最右边一个数字,其余整体右移补位。注意它最后清了 40H 和 70H 两个位,40H 在扫描命中时被 SETB 过,这里清掉是为了防止退格后残留的移位请求再次触发。

3.3 主循环的调度顺序

MAIN 只有三行:调显示、调扫描、跳回。这个顺序不是随便排的。先显示保证上电后数码管立刻有内容,不会黑屏;再扫描保证按键响应;LJMP 回 MAIN 形成死循环。整个系统没有中断,所有时序靠子程序调用链自然形成——显示刷新频率由 DELAY2 决定,按键扫描频率由 DELAY1 里的 DISPLAY1 调用次数决定,两者互相耦合。这种耦合在裸机程序里是常态,但也意味着改任何一个延时参数都会同时影响显示亮度和按键灵敏度,调参时必须一起看。

4. 避坑与排查:汇编键盘程序最容易翻车的几处

4.1 数码管显示乱码或全亮

现象:上电后八个数码管显示无规律字符,或全部段同时点亮。原因多半是段码表索引错位——键值 R1 没有先清零或越界,MOVC A, @A+DPTR用了一个超出表长度的偏移,读到程序区里的指令字节当成段码输出。解决:在 DISPLAY1 取段码前加边界判断,确认 R1 落在 0 到 10 之间;同时检查 TABLE 表项数和键值上限是否匹配,键值 13、14 这类功能键必须在进表之前被拦截。

4.2 按键无反应但 KS 能测到按下

现象:按下键后 KS 返回非零,但键值始终不变或显示不更新。原因通常是列扫描值 R2 被意外改写。K3 到 NEXT 之间 R2 参与 RR A 移位,如果中途有子程序(比如 DELAY1 里的 DISPLAY1)改动了累加器 A 或 R2,扫描值就乱了。源码里 DISPLAY1 大量使用 A,虽然它没有显式写 R2,但调用约定上 A 是被破坏的。解决:在 K3 之前重新从固定值装载 R2,或者把列扫描值存到不被显示子程序触碰的寄存器/内存单元。

4.3 长按一次出多个数字

现象:按住一个键不放,数码管连续涌入同一个数字。原因是没有做"释放检测"。源码的 KEY_INT 在 K4 处DJNZ R7, KEY_INT,R7 初值 08H,意味着一次调用里最多扫八轮,扫完就返回,主循环立刻又进来。只要键还按着,每一轮主循环都会重新算一次键值并置移位标志。解决:在确认键值后等待按键释放(循环调 KS 直到返回零),或者引入"上次键值"比较,只有键值变化才触发移位。

4.4 移位后最左边一位残留旧数字

现象:连续按键时,第八位(最左)显示的数字不消失,或者移位后出现重复。原因是 S9 只作为暂存,DISPLAY1 只显示 S1 到 S8,S9 的内容永远不会被刷新到数码管上,但它会在下一次左移时被 S8 覆盖。如果某次移位路径没走全(比如从 SWAP_SHU2 返回后直接进 DISPLAY1 而没清标志),S9 的旧值可能通过右移重新进入显示区。解决:每次移位操作后显式把 S9 清零,或者在显示子程序里对超出窗口的位做屏蔽。

4.5 延时参数改了之后显示闪烁

现象:调整 DELAY2 的循环次数后,数码管出现可见闪烁或亮度骤降。原因是动态扫描的刷新率与延时长度直接绑定,DELAY2 太长则单个数位点亮过久、整体刷新率下降,低于约 50Hz 人眼就能察觉闪烁;太短则每个数位导通时间不足,亮度不够。解决:把 DELAY2 的循环次数当作刷新率参数来调,先用示波器或逻辑分析仪测 P2 口选通信号的周期,确认整屏刷新率在 100Hz 以上,再微调亮度。

5. 从这份源码看资源受限下的设计取舍

把整份程序摊开看,它做的每一个选择几乎都是"寄存器不够、时序不能等"逼出来的。键值用行首基数加列偏移算,而不是查二维表,省的是程序空间和查表指令周期;显示缓冲区用九个独立字节加八条 MOV 移位,而不是环形缓冲加指针,省的是间接寻址要占用的 R0/R1;延时循环里嵌显示刷新,省的是一个独立定时器中断——51 只有两个定时器,留给更关键的任务更划算。这些取舍单看每一条都像是"土办法",合在一起却构成了一套在 8 位机上跑得稳的完整调度。

值得多想一层的是耦合的代价。显示刷新频率、按键扫描频率、去抖延时三者通过 DELAY1 和 DELAY2 缠在一起,改一个动全身。这在教学项目里无所谓,但在真实产品里就是维护噩梦。常见的改进思路是把显示刷新挪进定时器中断,主循环只负责扫描和键值处理,用中断把时序从主循环里剥离出来——代价是要多占一个定时器和一套中断现场保护,在资源极紧的芯片上未必划算。所以这份源码的价值不在于它是最优解,而在于它把"没有中断时怎么用纯轮询撑起两套时序"这件事完整地演示了一遍,读懂了它,再去看带中断的版本,才知道中断到底替你省掉了什么。

我自己的习惯是,每次拿到这类裸机轮询代码,先不急着改逻辑,而是把每个 DELAY 子程序的循环次数和它被调用的位置画成一张调用频次表,确认哪段时序是被复用的、哪段是独占的。有一次就是靠这张表发现某个延时被显示和扫描同时依赖,改了一处导致另一处按键失灵,翻车翻得很典型。从那以后我每次动延时参数前都强制走一遍这张表,确认没有隐藏的共享路径。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询