
简介这份资源面向单片机初学者与嵌入式入门学习者聚焦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 的行线和实际电平做异或哪一位被按键拉低了异或结果对应位就是 1A 非零即代表有按下。但 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键值 110BH和 120CH被留作功能键——退格和清零。源码里 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 是数字 15BH 是 2一直到 6FH 是 93FH 是 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, ADPTR用了一个超出表长度的偏移读到程序区里的指令字节当成段码输出。解决在 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_INTR7 初值 08H意味着一次调用里最多扫八轮扫完就返回主循环立刻又进来。只要键还按着每一轮主循环都会重新算一次键值并置移位标志。解决在确认键值后等待按键释放循环调 KS 直到返回零或者引入上次键值比较只有键值变化才触发移位。4.4 移位后最左边一位残留旧数字现象连续按键时第八位最左显示的数字不消失或者移位后出现重复。原因是 S9 只作为暂存DISPLAY1 只显示 S1 到 S8S9 的内容永远不会被刷新到数码管上但它会在下一次左移时被 S8 覆盖。如果某次移位路径没走全比如从 SWAP_SHU2 返回后直接进 DISPLAY1 而没清标志S9 的旧值可能通过右移重新进入显示区。解决每次移位操作后显式把 S9 清零或者在显示子程序里对超出窗口的位做屏蔽。4.5 延时参数改了之后显示闪烁现象调整 DELAY2 的循环次数后数码管出现可见闪烁或亮度骤降。原因是动态扫描的刷新率与延时长度直接绑定DELAY2 太长则单个数位点亮过久、整体刷新率下降低于约 50Hz 人眼就能察觉闪烁太短则每个数位导通时间不足亮度不够。解决把 DELAY2 的循环次数当作刷新率参数来调先用示波器或逻辑分析仪测 P2 口选通信号的周期确认整屏刷新率在 100Hz 以上再微调亮度。5. 从这份源码看资源受限下的设计取舍把整份程序摊开看它做的每一个选择几乎都是寄存器不够、时序不能等逼出来的。键值用行首基数加列偏移算而不是查二维表省的是程序空间和查表指令周期显示缓冲区用九个独立字节加八条 MOV 移位而不是环形缓冲加指针省的是间接寻址要占用的 R0/R1延时循环里嵌显示刷新省的是一个独立定时器中断——51 只有两个定时器留给更关键的任务更划算。这些取舍单看每一条都像是土办法合在一起却构成了一套在 8 位机上跑得稳的完整调度。值得多想一层的是耦合的代价。显示刷新频率、按键扫描频率、去抖延时三者通过 DELAY1 和 DELAY2 缠在一起改一个动全身。这在教学项目里无所谓但在真实产品里就是维护噩梦。常见的改进思路是把显示刷新挪进定时器中断主循环只负责扫描和键值处理用中断把时序从主循环里剥离出来——代价是要多占一个定时器和一套中断现场保护在资源极紧的芯片上未必划算。所以这份源码的价值不在于它是最优解而在于它把没有中断时怎么用纯轮询撑起两套时序这件事完整地演示了一遍读懂了它再去看带中断的版本才知道中断到底替你省掉了什么。我自己的习惯是每次拿到这类裸机轮询代码先不急着改逻辑而是把每个 DELAY 子程序的循环次数和它被调用的位置画成一张调用频次表确认哪段时序是被复用的、哪段是独占的。有一次就是靠这张表发现某个延时被显示和扫描同时依赖改了一处导致另一处按键失灵翻车翻得很典型。从那以后我每次动延时参数前都强制走一遍这张表确认没有隐藏的共享路径。希望帮到你。本文还有配套的精品资源点击获取