在 8051 的入门实验里,七段数码管几乎是最常见的显示外设之一,但真正写代码时,很多人会卡在“段码怎么配、位选怎么扫、数据又是怎么滚动”的细节上。本文把一段基于 8255 的示例程序重新拆开说明,目标很明确:让 8 个共阳极七段数码管循环显示数字序列,并看清每个函数在整套显示流程里的职责,这样你后续改成别的数字或字符时也更容易下手。
先明确任务与硬件映射
这段程序要实现的核心效果,是在八个七段数码管上循环显示数字 12345678。这里使用的是共阳极数码管,控制上又引入了 8255 作为 I/O 扩展芯片,因此程序一开始要先把和硬件地址直接对应的端口定义出来。

代码里通过 xdata 关键字,把 8255 的控制口和三个数据口映射到固定地址。这样后面给这些变量赋值,本质上就是在直接操作外设寄存器。
#include <reg52.h>
#include <stdio.h>
xdata unsigned char COM8255 _at_ 0x8006;
xdata unsigned char PA8255 _at_ 0x8000;
xdata unsigned char PB8255 _at_ 0x8002;
xdata unsigned char PC8255 _at_ 0x8004;
从这几个定义可以直接看出程序后面的分工:
COM8255用来设置 8255 的工作方式。PA8255负责输出段码,也就是控制数码管 a、b、c、d、e、f、g 等段是否点亮。PB8255负责位选,决定当前点亮的是 8 个数码管中的哪一个。PC8255这段示例里虽然定义了,但后续并没有实际参与显示流程。
理解这一步很重要,因为后面的显示函数其实就是围绕“段码输出 + 位选切换”两个动作展开的。
段码表和显示缓冲区分别做什么
要让数码管显示数字,程序不能直接把 1、2、3 这样的值送到端口,而是要先查表,找到对应的七段编码。为此,代码定义了 tab[] 和 buff[] 两个数组。
unsigned char tab[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8}; //0-f
unsigned char buff[8]={1,2,3,4,5,6,7,8} ;
tab[] 的作用,是保存数字在七段数码管上的段码。这里给出的内容实际覆盖了 0 到 7 的编码值,注释里虽然写了 //0-f,但从数组长度看,当前示例并没有把 8 到 F 全部列出来。对于这篇教程的目标来说,这并不影响理解:程序就是通过 tab[buff[d]] 这种方式,把要显示的数字转换成对应段码。
buff[8] 则是显示缓冲区,可以把它理解为“当前 8 位数码管屏幕上应当出现的内容”。初始化后,它存放的就是 1,2,3,4,5,6,7,8,也就是程序启动时的初始显示序列。
这种写法的好处是,显示逻辑不需要关心“下一个要亮什么”,它只负责读取 buff[];而数据变化逻辑则只需要修改 buff[] 的内容。显示和数据更新因此被拆成了两个相对独立的部分。
循环显示为什么要靠延时、扫描和移位配合
数码管的“循环显示”不是一次性把 8 个数字同时写进去就结束了,它通常依赖动态扫描。也就是说,程序会快速轮流点亮每一位,只要刷新得足够快,人眼就会感觉 8 位数码管一直稳定亮着。

delay():决定单次显示停留时间
先看延时函数:

void delay(unsigned int z)
{
unsigned int x,y;
for(x=z;x>0;x--)
for(y=255;y>0;y--);
}
这个函数使用双重循环制造软件延时,参数 z 用来控制总延时长短。它的意义并不复杂:如果没有这个停顿,位选切换太快,某一位的点亮时间过短,显示效果就不稳定;如果延时过长,又会让扫描频率变低,出现肉眼可见的闪烁。
dataFlow():把显示内容向前滚动一位
接着是数据流动函数:
void dataFlow(unsigned char c)
{
unsigned char h,s;
h=buff[0];
for(s=0;s<7;s++)
{
buff[s]=buff[s+1];
}
buff[7]=c;
}
它做的事情很直接:把 buff[] 里原有的 8 个数整体向前移动一格,然后把新传入的字符 c 放到最后一位。这样每调用一次,显示序列都会更新一次。
例如初始内容是 1 2 3 4 5 6 7 8,调用一次 dataFlow(0) 之后,就会变成 2 3 4 5 6 7 8 0。如果再继续传入后续数字,就能形成连续滚动的视觉效果。
这里的变量 h 虽然保存了 buff[0],但在当前代码中没有继续使用,因此它对最终功能没有实际影响。
display():段码输出和位选扫描一起完成
真正把数据显示到数码管上的,是 display() 函数:
void display()
{
unsigned char site=0x01,d;
for(d=0;d<8;d++)
{
PA8255=tab[buff[d]];
PB8255=site;
delay(300);
site<<=1;
}
}
这个函数每执行一轮,就会从第 1 位扫到第 8 位,流程可以拆成 4 步:
- 从
buff[d]取出当前位要显示的数字。 - 用这个数字去查
tab[],得到段码后送到PA8255。 - 把当前位选值
site送到PB8255,选中某一位数码管。 - 调用
delay(300)让这一位保持一小段时间,再通过site<<=1切到下一位。
其中 site 初始值是 0x01,每次左移一位,正好对应 8 位数码管依次选通的过程。这就是典型的动态扫描显示思路。
主函数怎样把 12345678 变成持续循环的效果
把前面的几个函数连起来之后,主程序逻辑就比较清楚了:
void main(void)
{
unsigned char j;
COM8255=0x80;
while(1)
{
for(i=0;i<10;i++)
{
for(j=0;j<8;j++)
display();
dataFlow(i);
}
}
}
程序先通过 COM8255=0x80 初始化 8255 工作模式,然后进入无限循环,不断刷新和更新显示内容。
这里可以把两层 for 循环分开看:
- 外层
for(i=0;i<10;i++)决定要依次送入显示缓冲区的新数字,范围是0到9。 - 内层
for(j=0;j<8;j++) display();并不改变缓冲区内容,它只是重复扫描显示,让当前 8 位内容维持一段可见时间。 - 等当前画面维持够了,再调用
dataFlow(i),把新数字压入缓冲区末尾,形成下一帧显示内容。
于是,程序的运行节奏就变成了:
先稳定显示当前 8 位内容 -> 再把新数字塞到末尾并整体左移 -> 再显示新的 8 位内容
按照这个逻辑,最初的 12345678 会逐步演变成新的序列,形成持续循环滚动的效果。
需要注意的是,这段代码按教程意图说明没有问题,但如果你准备直接编译,还要留意一个细节:代码中的 for(i=0;i<10;i++) 使用了变量 i,而当前展示的 main() 里只显式定义了 j,因此实际工程里还需要补上 i 的定义。
这段程序适合理解哪些关键点
如果你是拿它做 8051 数码管实验,这段示例最值得掌握的不是把代码背下来,而是理清 3 个层次:
- 硬件层:
PA8255管段码,PB8255管位选,COM8255负责模式设置。 - 数据层:
tab[]决定“数字怎么变成段码”,buff[]决定“当前 8 位显示什么”。 - 时序层:
display()负责动态扫描,dataFlow()负责序列滚动,delay()负责显示停留时间。
把这三层关系看明白之后,你就可以继续往下扩展,比如把初始序列从 12345678 改成别的内容,补全 tab[] 的更多编码,或者进一步加入字母、符号和更复杂的滚动方式。













