在 8051 单片机实验里,矩阵键盘和 LED 显示往往是最先碰到的一组输入输出组合,但不少资料只给出代码,没把扫描过程和端口分工讲清楚。本文按“硬件端口如何分配、程序如何逐行扫描、键值又是怎样送到 LED”这条主线重写这个案例,帮助你判断这套方案为什么能工作,以及后续该从哪几处入手做调试。
8255 与键盘、LED 的端口分配怎么定
这个案例的目标很明确:使用 8051 微控制器配合 8255 并行接口芯片,完成一个 4x4 矩阵键盘的扫描,并把识别到的键值实时显示到 LED 显示器上。

程序首先定义了 8255 的几个关键寄存器地址,其中控制寄存器位于 0x8006,A、B、C 三个端口分别位于 0x8000、0x8002、0x8004。这部分地址映射决定了后面每一次读写究竟落到哪个外设端口上。
#include
#include
xdata unsigned char COM8255 _at_ 0x8006;
xdata unsigned char PA8255 _at_ 0x8000;
xdata unsigned char PB8255 _at_ 0x8002;
xdata unsigned char PC8255 _at_ 0x8004;
unsigned char tab[16]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned char dat,col,rol;
其中,tab[16] 是数码管显示编码表,保存了 16 个按键值对应的段码。也就是说,矩阵键盘扫描得到的结果不是直接显示,而是先作为索引,再去查这张表,最后输出到显示端口。
初始化阶段最关键的一句是:
COM8255=0x81; //8255 PA、PB输出,PC7--PC4输出,PC3--PC0输入
这条配置决定了整套方案的工作分工:
- PA 作为输出口,用来送数码管段码;
- PB 作为输出口,用来配合 LED 显示控制;
- PC 高四位作为输出,用于矩阵键盘逐行扫描;
- PC 低四位作为输入,用于读取列线状态。
这也是矩阵键盘最典型的接法之一:一侧负责“轮流拉低”行线,另一侧负责“读取变化”列线。只要有按键按下,当前被扫描到的那一行就会和某一列形成导通,从而把列输入拉成低电平。
延时函数和逐行扫描为什么缺一不可
矩阵键盘扫描不是一次读端口就能稳定完成的。原因有两个:一是机械按键本身存在抖动,二是行线切换之后需要一点点时间让电平稳定下来。所以程序单独准备了两个延时函数,分别承担不同任务。

void delay20ms(unsigned char t)
{
unsigned char a,b;
a=0xff;
b=t;
while(b)
{ while(a--);
b--;
}
}
void delay(unsigned char m)
{
unsigned char n;
while(m)
{
for(n=110;n>0;n--);
m--;
}
}
delay20ms 用于较长时间保持显示,delay 用于较短的扫描等待和简单去抖。它们本质上都是循环延时,真实时长会受到单片机时钟频率影响,因此在不同实验板上,显示稳定性和响应速度可能还需要重新校准。
真正的核心在 keyscan 函数。它采用的是逐行扫描法:先让 PC 口高四位中的某一位输出低电平,其余保持高电平,再读取低四位列线状态;之后把这个“低电平窗口”左移,继续扫描下一行,直到四行全部检查完毕。
unsigned char keyscan(void)
{
unsigned char i,t,k,m;
m=0xef; //扫描初值。 PC4输出0,即扫描PC4线
for(i=0;i<4;i++)
{
PC8255=m; //输出行线(高四位)
delay(1);
t=PC8255; //读取列线(低四位)
t|=0xf0;
switch(t)
{
case 0xfe: k=4*i;
break;
case 0xfd: k=4*i+1;
break;
case 0xfb: k=4*i+2;
break;
case 0xf7: k=4*i+3;
break;
}
m=m<<1; //“0”向左移动,扫描下一行
m|=0x01;
}
return(k);
}
这里有几个细节值得特别注意:
扫描初值为什么是 0xef
0xef 的二进制形式是 1110 1111,也就是 PC4 为 0,其余位为 1。由于 PC 高四位承担行扫描任务,这就意味着第一次先扫描第一行。
为什么读取后还要执行 t|=0xf0
因为程序只关心低四位列输入,高四位原本是扫描输出位。执行 t|=0xf0 后,高四位被统一置 1,这样后面的 switch 就只需要针对列线低电平组合做判断,匹配会更直接。

键值是怎么计算出来的
当读到 0xfe、0xfd、0xfb、0xf7 时,分别表示当前扫描行上的第 1、2、3、4 列被按下。程序因此按 4*i 到 4*i+3 的方式给出键值索引。这样一来,4x4 键盘的 16 个按键就被映射成了 0 到 15。
从实现思路上看,这段代码完成的是“行列定位”而不是字符解释。它先得到一个键位编号,再交给显示部分去处理,因此后续无论你想显示数字、十六进制字符,还是改成控制命令,入口都比较清晰。
键值扫描出来后,LED 是怎样显示的
得到键值以后,程序会进入 display 函数,把这个编号转换成实际的数码管段码输出。
void display(unsigned char ch)
{
PB8255=0x1;
PA8255=tab[ch];
delay20ms(100);
}
这段逻辑不复杂,但职责很明确:
- 先通过
PB8255=0x1;使能或选通 LED 显示; - 再把
tab[ch]对应的段码送到 PA 口; - 最后通过
delay20ms(100)保持一段显示时间。
因为 tab 表里已经预先放好了 16 个编码,所以只要扫描结果 ch 落在 0 到 15 之间,数码管就能显示出对应内容。这也是为什么前面扫描函数必须返回一个稳定、连续的键值索引,而不是零散的端口状态。
如果你在仿真时发现按键已经识别,但 LED 显示不对,通常就要优先检查两件事:一是 tab 编码表是否与实际数码管类型匹配,二是 PA、PB 端口在电路中的连接方向是否与程序假设一致。
主程序如何把扫描和显示连成实时交互
主函数部分实际上就是把前面的几块逻辑串起来:先完成 8255 初始化,再不断轮询键盘,最后把结果显示出来。
void main(void)
{
COM8255=0x81; //8255 PA、PB输出,PC7--PC4输出,PC3--PC0输入
// Write your code here
while (1)
{
dat=keyscan();
display(dat);
}
}
这里的执行流程可以概括为三步:
- 写入
COM8255=0x81,确定 8255 各端口输入输出方向; - 在死循环里不断调用
keyscan(),轮询矩阵键盘; - 把返回的
dat交给display(dat),实时刷新 LED。
这种写法的优点是结构简单,适合实验、课程设计和基础功能验证。它没有中断机制,也没有更复杂的状态管理,因此理解门槛低,调试时可以直接从“端口配置是否正确、扫描值是否稳定、显示编码是否匹配”三条线逐项排查。
当然,这份程序也保留了很明显的教学式特征。比如,按键去抖处理比较基础,没有看到无按键状态的明确返回分支;如果多个按键同时按下,现有 switch 结构也没有专门处理冲突输入。但对于“8051 + 8255 + 4x4 矩阵键盘 + LED 显示”这个入门场景来说,它已经完整展示了从硬件扫描到显示输出的基本链路。
这个案例适合拿来理解哪些关键点
把整个程序串起来看,这个案例最适合用来理解三件事:第一,8255 如何扩展 8051 的并行 I/O;第二,矩阵键盘怎样通过逐行拉低、逐列读取完成定位;第三,扫描结果如何通过查表方式转换成 LED 可显示的数据。
如果你接下来要继续扩展这个实验,可以优先考虑优化去抖、增加无键按下判断,或者把显示从单个键值拓展到更多位数码管。等这几步都跑通之后,再回头看这份基础程序,就更容易分清哪些是核心逻辑,哪些只是为了演示而做的简化处理。







