ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

51单片机汇编实战:4×4矩阵键盘与8位数码管动态显示

51单片机汇编实战:4×4矩阵键盘与8位数码管动态显示

简介:这份资源面向单片机初学者与电子爱好者,聚焦51单片机汇编语言下的矩阵键盘扫描实践,帮助读者理解行线列线交叉检测按键的硬件原理,并掌握汇编程序控制输入输出与数码管显示的完整流程。压缩包共1个文件,为1份doc文档,约283KB,内含完整程序源代码与关键代码块说明,涵盖变量定义、主函数初始化、按键判断与键值计算、数码管显示与移位、清零及延时子程序、共阴极段码表等模块,便于对照阅读与调试。已有569人学习下载,适合作为汇编入门与嵌入式底层操作训练的参考材料,读者可从中获得可运行的代码框架、去抖与扫描思路,以及数码管动态显示的实现细节,为后续项目开发打下基础。

1. 一块 4×4 键盘加 8 位数码管,为什么值得用汇编写一遍

去年帮一个做实验箱的朋友调程序,现象很怪:按键按下去数码管偶尔跳两个数,松手又正常。示波器一抓,P1 口在扫描间隙被拉成中间电平,读回来的行线全是毛刺。问题不在硬件,在于扫描和显示挤在同一个循环里,谁也没让谁。这类毛病用 C 写一样会犯,但用汇编写一遍,你会被逼着把每一个机器周期、每一次端口读写都算清楚,之后再回头看 C 的位操作,心里就有底了。

这份资源就是围绕这个场景展开的:51 单片机、4×4 矩阵键盘、8 位共阴极数码管,全部用汇编实现。功能不复杂——按键按下,键值进显示缓冲,数码管左移一位腾出位置;按退格键删一位,按清零键全清。适合刚学完指令系统、想找一个能跑通的完整工程练手的人,也适合带实验课的开发者拿去当参考实现。矩阵键盘扫描这个题目网上代码很多,但能把去抖、键值计算、移位显示、退格清零串成一个闭环的完整汇编工程并不多,这份代码的价值就在这里。

2. 工程结构与硬件接线:先搞清楚 P0、P1、P2 各管什么

2.1 内存布局与变量定义

汇编工程没有目录树,结构体现在两处:一是伪指令划出的代码段和数据段,二是 EQU 定义的变量区。这份代码把 8 个数码管缓冲放在 30H 到 37H,另外留了 38H 给移位时的临时位。

S1 EQU 30H ; 第 1 位数码管段码缓冲 S2 EQU 31H ; 第 2 位 S3 EQU 32H S4 EQU 33H S5 EQU 34H S6 EQU 35H S7 EQU 36H S8 EQU 37H S9 EQU 38H ; 左移时 S8 的旧值暂存到这里 ORG 0000H LJMP START ; 复位向量,跳过中断向量区 ORG 0030H ; 主程序从 0030H 开始,避开向量表

30H 到 37H 属于 51 内部 RAM 的直接寻址区,用MOV S1, A一条指令就能写,比走寄存器间接寻址快,这是选这段地址的原因。S9 单独放在 38H,是因为左移时MOV S9, S8之后 S8 马上要被 S7 覆盖,必须有个地方接住最右边那位。位标志用了两个:40H 和 7AH。40H 在可位寻址区,7AH 也在位寻址区,分别标记「需要左移」和「需要右移」,后面显示子程序靠查这两个位决定走哪条分支。

提示:EQU 只是编译期替换,不占空间。真正占空间的是 30H 到 38H 这 9 个字节,写代码时别把堆栈指针往这段压,否则显示缓冲会被栈数据冲掉。

2.2 端口分配与硬件连接

三个端口分工明确,接线错了后面全白搭:

端口用途方向说明
P1.4~P1.7矩阵键盘行线输入读之前先写 1
P1.0~P1.3矩阵键盘列线输出逐列拉低扫描
P0数码管段码输出共阴极,高电平点亮
P2数码管位选输出经 74HC138 译码,00H/04H/08H…1CH

P1 口一行四列、四行四列,正好 16 个键。列线做输出、行线做输入,这是矩阵键盘最常见的接法:扫描时给某一列低电平,其余列高电平,然后读四根行线,哪根变低就说明这一列这一行的交叉点被按下。P2 送的是 138 译码器的输入,所以位选值是 00H、04H、08H、0CH 这样每次加 4,而不是 01H、02H、03H。这一点如果照抄别人的代码很容易翻车,因为有些板子是直接驱动三极管,位选就是 01H 起步。

2.3 主循环:显示和扫描的调度关系

主程序本身只有四行,但调度顺序决定了整个系统的响应手感:

START: MOV R1, #00H ; 键值寄存器,同时兼作段码表偏移 MOV R7, #08H ; 扫描计数,8 次后强制进显示 MOV DPTR, #TABLE ; 段码表首地址 MAIN: LCALL DISPLAY1 ; 先刷新一遍显示 LCALL KEY_INT ; 再扫键盘 LJMP MAIN ; 无限循环

先显示后扫描,是为了让数码管在两次扫描之间保持点亮,视觉上不闪。R7 初值 8 是个细节:KEY_INT 内部每扫完一轮列会DJNZ R7,减到 0 就跳出去调一次 DISPLAY1 再返回,相当于给扫描加了个上限,防止某一轮卡在键盘检测里太久导致显示断流。R1 一个寄存器干两件事——平时存键值,显示时当MOVC A, @A+DPTR的偏移量,因为键值 0 到 9 正好对应段码表前 10 项,这个复用省掉了一次数据搬运。

3. 键盘扫描核心链路:从检测按下到算出键值

3.1 两级去抖:KS 检测加 DELAY1 复检

去抖这段是整份代码里最容易被简化掉、也最容易出问题的地方。代码用了「检测—延时—再检测」的标准结构:

KEY_INT: LCALL KS ; 第一次检测,A 非零说明可能有键 JNZ K1 LCALL DELAY1 ; 没键也延时一下,避免空转太猛 AJMP KEY_INT K1: LCALL DELAY1 ; 有键,先等 10ms 左右 LCALL KS ; 再检测一次 JNZ K2 ; 两次都非零才确认 AJMP KEY_INT

KS 子程序干的事很直接:P1 高四位写 1、低四位写 0,读回 P1 后和 0F0H 异或。没有键按下时行线被列线拉低,读回高四位是 0,异或结果是 0;有键按下时某根行线被按键短接到低电平的列线,读回高四位出现 1,异或结果非零。用异或而不是直接判断,是为了把「高四位有任意一位为 1」压缩成一个零/非零判断,JNZ一条指令就够。

DELAY1 的延时量是10 × DISPLAY1 耗时,不是死循环空转:

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

这个写法值得学。普通延时子程序用DJNZ空转,数码管在延时期间是灭的,10ms 去抖就会看到明显闪烁。这里把 DISPLAY1 塞进延时循环,去抖的同时显示照常刷新,一举两得。代价是延时时间不精确,取决于 DISPLAY1 里每个 DELAY2 的长度,但对按键去抖来说 8 到 15ms 都算合格区间,不需要精确。

3.2 列扫描与行判断:R2 循环右移的巧劲

确认有键之后,进入逐列扫描。R2 初值11110111B,最低位那根列线拉低,其余列高:

K2: MOV R2, #11110111B ; 第一列(P1.3)拉低 MOV R0, #00H ; 列号计数,每换一列加 1 K3: MOV P1, R2 ; 输出列扫描值 L0: JB P1.7, L1 ; 第 0 行没按下,看下一行 MOV A, #01H ; 第 0 行首键号 AJMP LK L1: JB P1.6, L2 MOV A, #05H ; 第 1 行首键号 AJMP LK L2: JB P1.5, L3 MOV A, #09H ; 第 2 行首键号 AJMP LK L3: JB P1.4, NEXT MOV A, #13 ; 第 3 行首键号 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 ; 循环右移,拉低下一列 MOV R2, A AJMP K3

首键号 01H、05H、09H、13 是十进制 13,也就是 0DH,不是十六进制 0D 的笔误——这里用的是十进制写法,因为键值最终要当段码表偏移,0 到 15 连续排布最方便。行首键号加列号得到 0 到 15 的连续键值,这个设计让段码表可以直接按 0 到 9 顺序排,不用做映射表。

RR A循环右移是这段的巧劲。R2 从11110111B开始,右移一次变11111011B,再移变11111101B、11111110B,正好依次拉低 P1.3、P1.2、P1.1、P1.0 四列。JNB Acc.0, K4判断的是移位后最低位是否为 0——当11111110B再右移会变成01111111B,最低位变 1,但代码在移位前就判断,所以实际是在第四列扫完后退出。这个边界条件建议自己拿纸推一遍,推通了矩阵键盘的扫描逻辑就彻底懂了。

3.3 键值后处理:退格、清零、移位三个分支

键值算出来后不是直接显示,DISPLAY1 开头先做三次判断:

DISPLAY1: MOV A, R1 XRL A, #0BH ; 键值 11 是退格键? JZ DEL1 MOV A, R1 XRL A, #0CH ; 键值 12 是清零键? JZ ZERO JB 7AH, SWAP_SHU1 ; 有左移标志,先移位再显示 ; ……正常显示流程

用XRL加JZ而不是CJNE,是因为CJNE会改进位标志,而这段代码后面还要用位标志判断移位,少动一个标志位少一分风险。0BH 和 0CH 这两个键值对应键盘上第 12、13 个键,具体是哪两个物理按键取决于键盘丝印,改键位只要改这两个立即数。

退格分支 DEL1 在源码里被注释掉了,只留了框架:

; DEL1: ; MOV S9, #00H ; LCALL SWAP_SHU2 ; DEL2: ; SETB 40H ; CLR 7AH ; LJMP DISPLAY1

注释掉的原因大概率是退格逻辑没调通——右移之后最左边会残留旧值,需要额外清零。要启用的话,DEL1 里应该先把 S1 清 0 再右移,或者右移后补一句MOV S1, #00H。这是这份代码留给二次开发的一个口子,后面第 5 章会展开。

4. 数码管动态显示与移位:8 位缓冲怎么滚起来

4.1 段码表与共阴极编码

段码表放在程序末尾,10 个数字加一个 3FH:

TABLE: DB 80H, 06H, 5BH, 4FH, 66H ; 0~4 DB 6DH, 7DH, 07H, 7FH, 6FH ; 5~9 DB 3FH ; 第 11 项,对应键值 10

80H 对应数字 0,二进制10000000B,最高位是 dp 段,低七位全 0——等等,共阴极 0 应该是 a、b、c、d、e、f 六段亮,编码 3FH 才对。这里 80H 排第一,说明这张表不是从 0 开始的,而是把 0 放在了第二位。对照后面MOVC A, @A+DPTR用 R1 做偏移,R1 是键值 0 到 15,那键值 0 取到的是 80H,显示的是一个点或者全灭。这大概率是原作者的笔误,或者键盘 0 键的键值实际是 1。改的时候把 TABLE 第一项改成 3FH、最后那个 3FH 删掉,或者把键值整体加 1,两种改法都行,但只能改一处,改两处就错位。

注意:段码表错位是这类工程最常见的翻车点,现象是「按 1 显示 0、按 0 显示乱码」。调的时候先单独写个测试循环,把 0 到 9 依次送 P0,确认硬件编码,再回头对表。

4.2 动态扫描:位选加段码,靠 DELAY2 撑住亮度

8 位数码管共用 P0 段码线,靠 P2 位选轮流点亮,一次只亮一位:

MOV A, R1 MOVC A, @A+DPTR ; 查表取段码 MOV S1, A ; 存进第 1 位缓冲 MOV P2, #00H ; 选通第 1 位 MOV P0, S1 ; 送段码 LCALL DELAY2 MOV P2, #04H ; 选通第 2 位 MOV P0, S2 LCALL DELAY2 ; ……依次到第 8 位,P2 送 1CH

每位之间插一个 DELAY2,延时量是43 × 12次空转:

DELAY2: MOV R4, #43 D1: MOV R5, #12 D2: DJNZ R5, D2 DJNZ R4, D1 RET

43 × 12 在 12MHz 晶振下大约 1ms 出头,8 位扫一遍 8ms 左右,刷新率 125Hz,肉眼看不到闪烁。这个值不能随便改:改小了亮度不够,改大了单次扫描时间变长,按键响应会变迟钝。如果换 11.0592MHz 晶振,延时略短,问题不大;换 6MHz 就要把 43 调大,否则亮度掉得厉害。

4.3 左移与右移:缓冲区的整体搬迁

左移发生在有新键值、且 7AH 标志置位时:

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 还是旧值

注意最后没有给 S1 赋新值,而是跳回 DISPLAY1 开头重新走一遍——第二次进来时 7AH 已清,会走正常显示流程,把 R1 里的新键值查表写进 S1。这个「移完再回来填」的写法省了一次查表,但要求 R1 在移位过程中不能被改,所以移位期间不能响应新按键。实际使用中 8 次扫描才进一次显示,这个窗口很短,感觉不到丢键。

右移 SWAP_SHU2 是反方向,S1 到 S8 依次被后一位覆盖,S9 的值补进 S8。这段代码在退格分支里被调用,但 DEL1 被注释了,所以右移目前是死代码。要激活退格,除了取消注释,还得处理 S1 的残留——右移后 S1 还是旧值,需要手动清 0,否则退格退不干净。

5. 避坑与排查:五个真实会卡住你的点

5.1 按键连击,按一下出好几个数

现象:按一次键,数码管连移两三位。原因:去抖只做了「延时后复检」,但没等按键释放就返回了,主循环下一轮又检测到同一个按键还按着。解决:在 K4 之后加一段等释放的循环,LCALL KS直到 A 为零再 RET;或者用「松手检测」标志位,检测到释放前不再接受新键值。这份代码没做释放检测,是它最明显的短板,二次开发时优先补。

5.2 数码管某一位常亮或全灭

现象:8 位里固定某一位不亮,或者一直显示同一个数字。原因:P2 位选值算错。138 译码器输入是 A、B、C 三根线,对应值 0 到 7,但代码里送的是 00H、04H、08H……1CH,这是把译码输入当成了字节地址。如果你的板子 P2 直接接三极管基极,位选应该是 01H、02H、04H、08H、10H、20H、40H、80H。解决:先量 P2 口实际电平,确认是译码还是直驱,再改位选序列。

5.3 段码表错位,数字整体偏移

现象:按 1 显示 0,按 0 显示一个点。原因:TABLE 第一项是 80H 而不是 3FH,键值 0 取到了 dp 段编码。解决:把 TABLE 改成DB 3FH, 06H, 5BH, ...从 0 开始排,或者把 R1 在查表前加 1。改完用单步调试看MOVC之后的 A 值,和预期段码对一遍。

5.4 移位后最左位残留旧数字

现象:左移几次后,最左边一直显示第一个按下的数字。原因:SWAP_SHU1 移完跳回 DISPLAY1,第二次进来时如果 R1 没变(比如按键已释放但键值没更新),S1 不会被覆盖。解决:移位后直接给 S1 赋新键值的段码,不要依赖回跳;或者在 MAIN 里每次扫描前把 R1 清 0,只有检测到新键才写入。

5.5 延时改了之后显示变暗或按键变迟钝

现象:把 DELAY2 的 43 改小,数码管变暗;改大,按键要按很久才有反应。原因:DELAY2 同时决定单次点亮时长和整轮扫描周期,两个指标互相拉扯。解决:亮度靠减小 DELAY2、提高刷新率来补,但刷新率超过 200Hz 后亮度提升有限,反而增加 CPU 占用。经验值是 8 位扫一遍控制在 5 到 10ms,对应 DELAY2 在 30 到 60 之间调,用示波器看 P2 任一位的波形周期最准。

6. 二次开发:把退格补全,再谈键值映射

这份代码最值得动手改的地方是退格分支。DEL1 被注释掉不是偶然,右移之后 S1 残留旧值是绕不过去的坎。我的改法是放弃「移完回跳」的写法,在 SWAP_SHU2 末尾直接补一句清零:

SWAP_SHU2: MOV S1, S2 MOV S2, S3 MOV S3, S4 MOV S4, S5 MOV S5, S6 MOV S6, S7 MOV S7, S8 MOV S8, S9 MOV S9, #00H ; 暂存位清空,防止下次左移带出脏数据 MOV S1, #00H ; 最左位补零,退格才退得干净 CLR 40H CLR 70H RET ; 不再 LJMP DISPLAY1,交给调用者

改完把 DEL1 的注释去掉,内容换成LCALL SWAP_SHU2加LJMP DISPLAY1。验证方法是连按三个数字再连按三次退格,看是否逐位清空、第四次退格是否无效果。这里有个细节:S9 清 0 必须在右移之后,否则左移时会把 0 带进显示区。

第二个改造点是键值映射。现在 0BH 是退格、0CH 是清零,硬编码在 DISPLAY1 开头。如果键盘丝印不同,或者想加确认键、小数点键,改立即数就行,但更稳的做法是定义符号:

KEY_BACK EQU 0BH KEY_CLR EQU 0CH ; 判断处改成 MOV A, R1 XRL A, #KEY_BACK JZ DEL1

这样换键盘只改两行 EQU。再进一步,可以把 16 个键的功能做成一张跳转表,用MOVC取偏移、JMP @A+DPTR分发,加功能不用动主流程。代价是多一层间接跳转,对 51 来说开销可接受。

最后一个技巧关于调试:汇编没有 printf,排查键值最土也最有效的办法是临时把 R1 直接送 P0 显示——按哪个键,数码管就显示那个键的原始键值,不查表。确认键值序列 0 到 15 连续无误,再接回段码表。我当年调第一块矩阵键盘时,就是靠这个笨办法发现列扫描少扫了一列,键值从 3 直接跳到 5。从那以后每次改扫描逻辑,我都先把原始键值打出来对一遍,再谈显示。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进