欢迎光临
我们一直在努力

蓝桥杯单片机入门培训笔记(三)

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

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

数码管是单片机系统中最常用的显示设备之一,它可以看作是多个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 // 熄灭
    };

    总结

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

  • 硬件原理:共阳/共阴的区别,段码和位码的作用
  • 显示技术:静态显示简单但有限,动态显示灵活但复杂
  • 中断机制:利用定时器中断实现稳定的动态显示
  • 编程技巧:显示缓冲区、消影处理、模块化设计
  • 赞(0)
    未经允许不得转载:171主机测评 » 蓝桥杯单片机入门培训笔记(三)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址