蓝桥杯单片机入门培训(三):数码管显示技术
一、数码管:单片机的"数字嘴巴"
数码管是单片机系统中最常用的显示设备之一,它可以看作是多个LED灯按照特定排列组成的数字显示器。想象一下电子手表上的数字显示,那就是数码管在起作用!
数码管的结构组成
基本构成:一个数码管由8个LED灯组成(7段笔划 + 1个小数点),正好形成一个"8"字形:
A
---
F | | B
--- G
E | | C
---
D DP(小数点)
生动比喻:
把数码管想象成一个"数字工厂":
-
段选(控制显示什么数字):就像工厂的模具,决定生产什么形状的产品
-
位选(控制哪个数码管显示):就像工厂的生产线,决定在哪个工位生产
共阴vs共阳:数码管的"性格差异"
数码管分为两种类型,它们的"性格"正好相反:
| 类型 | 公共端 | 点亮条件 | 工作方式比喻 |
|---|---|---|---|
| 共阴极 | 所有LED的负极连接在一起 | 段选端给高电平(1)点亮 | 像一群害羞的孩子,需要"鼓励"(高电平)才愿意发光 |
| 共阳极 | 所有LED的正极连接在一起 | 段选端给低电平(0)点亮 | 像一群活跃的孩子,需要"制止"(低电平)才愿意安静发光 |
蓝桥杯比赛中使用的是共阳极数码管
二、数码管的"语言":段码与位码
段码:告诉数码管显示什么数字
段码生成原理(以共阳极为例):
-
确定LED的排列顺序:通常为 DP G F E D C B A(从高位到低位)
-
需要哪个笔划亮,对应位就设置为0;不需要亮的设置为1
-
将8位二进制数转换为十六进制,就是段码
示例:显示数字"0"(需要A、B、C、D、E、F亮,G和小数点DP不亮)
-
二进制:1100 0000(DP=1,G=1,F=0,E=0,D=0,C=0,B=0,A=0)
-
十六进制:0xC0
常用段码表(共阳极):
// 数字0-9的段码,最后一个是熄灭状态
unsigned char Seg_Dula[] = {
0xC0, // 0: 显示数字0
0xF9, // 1: 显示数字1
0xA4, // 2: 显示数字2
0xB0, // 3: 显示数字3
0x99, // 4: 显示数字4
0x92, // 5: 显示数字5
0x82, // 6: 显示数字6
0xF8, // 7: 显示数字7
0x80, // 8: 显示数字8
0x90, // 9: 显示数字9
0xFF // 熄灭所有段
};
位码:选择哪个数码管工作
位码原理:
-
对于共阳极数码管:给0选中该数码管,给1则不选中
-
如果有6个数码管,位码通常设计为依次只有一个位是0
// 6个数码管的位码(共阳极)
unsigned char Seg_Wela[] = {
0xFE, // 1111 1110:选中第1个数码管
0xFD, // 1111 1101:选中第2个数码管
0xFB, // 1111 1011:选中第3个数码管
0xF7, // 1111 0111:选中第4个数码管
0xEF, // 1110 1111:选中第5个数码管
0xDF // 1101 1111:选中第6个数码管
};
硬件连接:
-
段码数据送到P0口
-
通过P2_6和P2_7控制锁存器,实现数据的分时传输
三、静态数码管显示
数码管显示函数
/* 数码管显示函数 */
void Seg_Disp(unsigned char wela, unsigned char dula)
{
// 第一步:消影处理(先熄灭所有数码管)
P0 = 0x00; // P0口输出全0(共阳极数码管全亮?需要确认)
P2_6 = 1; // 打开段选锁存器(像打开一扇门)
P2_6 = 0; // 关闭段选锁存器(数据锁存,门关上)
// 第二步:选择显示位置(位选)
P0 = Seg_Wela[wela]; // 输出位码,选择要显示的数码管
P2_7 = 1; // 打开位选锁存器
P2_7 = 0; // 关闭位选锁存器
// 第三步:显示数字(段选)
P0 = Seg_Dula[dula]; // 输出段码,显示对应的数字
P2_6 = 1; // 打开段选锁存器
P2_6 = 0; // 关闭段选锁存器
}
锁存器比喻:
可以把锁存器想象成快递柜:
-
把快递(数据)放进柜子(P0口)
-
按确认键(P2_6/P2_7=1然后=0),柜门关闭,快递被锁定
-
这样即使后续往P0口放其他东西,柜子里的快递也不会改变
静态显示的问题
问题:如果我们想同时让两个数码管显示不同的数字,不能简单地这样写:
Seg_Disp(0, 1); // 第一个数码管显示1
Seg_Disp(1, 2); // 第二个数码管显示2
为什么不行?
因为单片机执行速度很快,当你执行第二条语句时,第一个数码管已经不再被选中,所以实际上你只能看到第二个数码管显示2。
解决方案:动态显示技术!
四、动态数码管:人眼的"视觉暂留"魔法
动态显示原理
核心思想:利用人眼的"视觉暂留"效应(约0.1秒)
工作方式:
-
让第一个数码管显示1毫秒
-
让第二个数码管显示1毫秒
-
让第三个数码管显示1毫秒
-
…依次轮流
-
由于切换速度很快(全部6个数码管显示一遍只要6毫秒),人眼会认为它们同时亮着
比喻:
就像电影放映机,虽然是一帧一帧地播放,但因为切换够快,我们看到的却是连续的画面。
定时器:单片机的"心跳"
为了实现精确的1毫秒切换,我们需要使用定时器中断。
定时器初始化
/* 定时器初始化函数 */
void Timer0_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0x7F; // 定时器时钟12T模式(标准模式)
TMOD &= 0xF0; // 清除定时器0模式位
TMOD |= 0x01; // 设置定时器0为16位定时器模式
// 计算初值:1ms @ 12MHz
// 定时器每计数一次 = 1/12MHz × 12 = 1μs
// 1ms需要计数1000次,所以初值 = 65536 - 1000 = 64536 = 0xFC18
TL0 = 0x18; // 低8位
TH0 = 0xFC; // 高8位
TF0 = 0; // 清除溢出标志
TR0 = 1; // 启动定时器0
// 使能中断
ET0 = 1; // 允许定时器0中断
EA = 1; // 开启总中断开关
}
定时器中断服务函数
/* 定时器0中断服务函数 */
void Timer0Server() interrupt 1 // 定时器0中断号为1
{
// 重装初值,保证下次还是1ms后中断
TL0 = 0x18;
TH0 = 0xFC;
// 数码管位置切换(0-5循环)
if(++Seg_Pos == 6) {
Seg_Pos = 0;
}
// 显示当前数码管
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]);
}
中断比喻:
定时器中断就像闹钟:
-
设置闹钟每1毫秒响一次(定时器初始化)
-
闹钟响时,自动暂停手头工作(中断当前程序)
-
执行闹钟响时要做的任务(中断服务函数)
-
完成后继续原来的工作(返回主程序)
五、完整示例代码解析
/* 头文件声明区域 */
#include <REGX52.H>
/* 全局变量声明 */
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
// 段码表(共阳极)
unsigned char Seg_Dula[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99,
0x92, 0x82, 0xF8, 0x80, 0x90, 0xFF};
// 位码表(6个数码管)
unsigned char Seg_Wela[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF};
unsigned char Seg_Pos = 0; // 当前显示的数码管位置
unsigned char Seg_Buf[6] = {0, 0, 0, 0, 0, 0}; // 显示缓冲区
/* 延时函数 */
void Delay1ms(unsigned char xms)
{
unsigned char i, j;
while(xms--) {
i = 2;
j = 239;
do {
while(--j);
} while(--i);
}
}
/* 按键读取函数(4×4矩阵键盘) */
unsigned char Key_Read()
{
unsigned char temp = 0;
// 扫描第一行
P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
if(P3_4 == 0) temp = 1;
if(P3_5 == 0) temp = 2;
if(P3_6 == 0) temp = 3;
if(P3_7 == 0) temp = 4;
// 扫描第二行
P3_0 = 1; P3_1 = 0; P3_2 = 1; P3_3 = 1;
if(P3_4 == 0) temp = 5;
if(P3_5 == 0) temp = 6;
if(P3_6 == 0) temp = 7;
if(P3_7 == 0) temp = 8;
// 扫描第三行
P3_0 = 1; P3_1 = 1; P3_2 = 0; P3_3 = 1;
if(P3_4 == 0) temp = 9;
if(P3_5 == 0) temp = 10;
if(P3_6 == 0) temp = 11;
if(P3_7 == 0) temp = 12;
// 扫描第四行
P3_0 = 1; P3_1 = 1; P3_2 = 1; P3_3 = 0;
if(P3_4 == 0) temp = 13;
if(P3_5 == 0) temp = 14;
if(P3_6 == 0) temp = 15;
if(P3_7 == 0) temp = 16;
// 恢复所有行为高电平
P3_0 = 1; P3_1 = 1; P3_2 = 1; P3_3 = 1;
return temp;
}
/* 数码管显示函数 */
void Seg_Disp(unsigned char wela, unsigned char dula)
{
// 消影处理:先熄灭所有段
P0 = 0xFF; // 共阳极:全1表示所有段都不亮
P2_6 = 1;
P2_6 = 0;
// 选择显示位置
P0 = Seg_Wela[wela];
P2_7 = 1;
P2_7 = 0;
// 显示数字
P0 = Seg_Dula[dula];
P2_6 = 1;
P2_6 = 0;
}
/* 定时器0初始化 */
void Timer0_Init(void)
{
TMOD &= 0xF0; // 清除定时器0模式位
TMOD |= 0x01; // 设置定时器0为16位模式
TL0 = 0x18; // 设置定时初值低8位
TH0 = 0xFC; // 设置定时初值高8位
TF0 = 0; // 清除溢出标志
TR0 = 1; // 启动定时器0
ET0 = 1; // 允许定时器0中断
EA = 1; // 开启总中断
}
/* 定时器0中断服务函数 */
void Timer0Server() interrupt 1
{
TL0 = 0x18; // 重装定时初值
TH0 = 0xFC;
// 切换数码管位置(0-5循环)
if(++Seg_Pos == 6) {
Seg_Pos = 0;
}
// 显示当前数码管
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]);
}
/* 主函数 */
void main()
{
Timer0_Init(); // 初始化定时器
// 设置显示缓冲区:让前两个数码管显示1
Seg_Buf[0] = 1; // 第一个数码管显示1
Seg_Buf[1] = 1; // 第二个数码管显示1
while(1)
{
// 按键处理
Key_Val = Key_Read(); // 读取当前按键状态
Key_Down = Key_Val & (Key_Val ^ Key_Old); // 检测按键按下瞬间
Key_Up = ~Key_Val & (Key_Val ^ Key_Old); // 检测按键松开瞬间
Key_Old = Key_Val; // 保存当前状态,用于下次比较
// 根据按键操作更新显示
// 例如:按下按键1,让第三个数码管显示2
if(Key_Down == 1) {
Seg_Buf[2] = 2;
}
// 这里可以添加更多应用逻辑
}
}
六、关键技巧与注意事项
1. 消影处理:消除"鬼影"
问题:在动态显示中,如果切换速度不够快或处理不当,会出现"鬼影"(多个数码管同时显示部分内容)
解决方案:
// 在显示新内容前,先熄灭所有段
P0 = 0xFF; // 共阳极:全1表示所有段都不亮
P2_6 = 1;
P2_6 = 0;
2. 显示缓冲区:数据分离设计
设计思想:将数据处理和显示刷新分离
-
Seg_Buf[]:存储每个数码管要显示的内容(0-9) -
定时器中断只负责从缓冲区读取数据并显示
-
主程序只需要更新缓冲区,不影响显示刷新
优点:
-
代码结构清晰
-
显示稳定,不受主程序复杂逻辑影响
-
便于实现复杂的显示效果
3. 中断使用原则
-
快进快出:中断服务函数要尽量简短
-
避免阻塞:不要在中断中执行延时等耗时操作
-
保护现场:如果中断中修改了共享变量,要考虑数据一致性问题
七、实际应用扩展
显示多位数字
/* 显示一个4位整数(如1234) */
void Show_Number(unsigned int num)
{
// 分解各位数字
Seg_Buf[0] = num / 1000; // 千位
Seg_Buf[1] = (num % 1000) / 100; // 百位
Seg_Buf[2] = (num % 100) / 10; // 十位
Seg_Buf[3] = num % 10; // 个位
// 如果高位是0,可以熄灭显示(消零处理)
if(Seg_Buf[0] == 0) Seg_Buf[0] = 10; // 10对应熄灭
if(Seg_Buf[1] == 0 && Seg_Buf[0] == 10) Seg_Buf[1] = 10;
}
添加小数点显示
/* 带小数点的段码表 */
unsigned char Seg_Dula_WithDP[] = {
0x40, // 0. (0带小数点)
0x79, // 1.
0x24, // 2.
0x30, // 3.
0x19, // 4.
0x12, // 5.
0x02, // 6.
0x78, // 7.
0x00, // 8.
0x10, // 9.
0xFF // 熄灭
};
总结
数码管显示是单片机入门的重要技能,掌握它需要理解:
-
硬件原理:共阳/共阴的区别,段码和位码的作用
-
显示技术:静态显示简单但有限,动态显示灵活但复杂
-
中断机制:利用定时器中断实现稳定的动态显示
-
编程技巧:显示缓冲区、消影处理、模块化设计