蓝桥杯单片机学习笔记(三)数码管

蓝桥杯单片机入门培训(三):数码管显示技术

一、数码管:单片机的"数字嘴巴"

数码管是单片机系统中最常用的显示设备之一,它可以看作是多个LED灯按照特定排列组成的数字显示器。想象一下电子手表上的数字显示,那就是数码管在起作用!

数码管的结构组成

基本构成:一个数码管由8个LED灯组成(7段笔划 + 1个小数点),正好形成一个"8"字形:

    A
   ---
 F |   | B
   --- G
 E |   | C
   ---
    D   DP(小数点)

生动比喻
把数码管想象成一个"数字工厂":

  • 段选(控制显示什么数字):就像工厂的模具,决定生产什么形状的产品

  • 位选(控制哪个数码管显示):就像工厂的生产线,决定在哪个工位生产

共阴vs共阳:数码管的"性格差异"

数码管分为两种类型,它们的"性格"正好相反:

类型 公共端 点亮条件 工作方式比喻
共阴极 所有LED的负极连接在一起 段选端给高电平(1)点亮 像一群害羞的孩子,需要"鼓励"(高电平)才愿意发光
共阳极 所有LED的正极连接在一起 段选端给低电平(0)点亮 像一群活跃的孩子,需要"制止"(低电平)才愿意安静发光

蓝桥杯比赛中使用的是共阳极数码管

二、数码管的"语言":段码与位码

段码:告诉数码管显示什么数字

段码生成原理(以共阳极为例):

  1. 确定LED的排列顺序:通常为 DP G F E D C B A(从高位到低位)

  2. 需要哪个笔划亮,对应位就设置为0;不需要亮的设置为1

  3. 将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;       // 关闭段选锁存器
 }

锁存器比喻
可以把锁存器想象成快递柜:

  1. 把快递(数据)放进柜子(P0口)

  2. 按确认键(P2_6/P2_7=1然后=0),柜门关闭,快递被锁定

  3. 这样即使后续往P0口放其他东西,柜子里的快递也不会改变

静态显示的问题

问题:如果我们想同时让两个数码管显示不同的数字,不能简单地这样写:

 Seg_Disp(0, 1);  // 第一个数码管显示1
 Seg_Disp(1, 2);  // 第二个数码管显示2

为什么不行
因为单片机执行速度很快,当你执行第二条语句时,第一个数码管已经不再被选中,所以实际上你只能看到第二个数码管显示2。

解决方案:动态显示技术!

四、动态数码管:人眼的"视觉暂留"魔法

动态显示原理

核心思想:利用人眼的"视觉暂留"效应(约0.1秒)

工作方式

  1. 让第一个数码管显示1毫秒

  2. 让第二个数码管显示1毫秒

  3. 让第三个数码管显示1毫秒

  4. …依次轮流

  5. 由于切换速度很快(全部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. 设置闹钟每1毫秒响一次(定时器初始化)

  2. 闹钟响时,自动暂停手头工作(中断当前程序)

  3. 执行闹钟响时要做的任务(中断服务函数)

  4. 完成后继续原来的工作(返回主程序)

五、完整示例代码解析

 /* 头文件声明区域 */
 #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. 中断使用原则

  1. 快进快出:中断服务函数要尽量简短

  2. 避免阻塞:不要在中断中执行延时等耗时操作

  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   // 熄灭
};

总结

数码管显示是单片机入门的重要技能,掌握它需要理解:

  1. 硬件原理:共阳/共阴的区别,段码和位码的作用

  2. 显示技术:静态显示简单但有限,动态显示灵活但复杂

  3. 中断机制:利用定时器中断实现稳定的动态显示

  4. 编程技巧:显示缓冲区、消影处理、模块化设计