第二周 第三讲

零基础入门三:数码管与定时器实战

一、 认识数码管原理(大白话版)

我们不用晦涩的术语,继续沿用“房间与灯”的例子来理解。

1. 核心概念

把 4 位数码管想象成 4 个并排的房间

  • 房间里的灯(a-dp):每个房间墙上都有 8 根灯管,拼成“8”字。

  • 总电路:所有房间的同位置灯管是连在一起的(比如所有房间的“a”灯管都连着同一根电线)。

关键角色

  • 段选 (Segment) —— “笔画/形状”

    • 作用:决定画出来是个什么数字(是“1”还是“8”)。

    • 比喻:就像你手里的印章。你想印个“5”,所有房间里就都准备好了“5”的形状。

    • 对应代码dula (段锁存)。

  • 位选 (Digit) —— “地盘/位置”

    • 作用:决定哪个房间的灯亮起来。

    • 比喻:就像房间的总电闸

    • 对应代码wela (位锁存)。

    • 规则:只有“印章盖下去”(段选)且“电闸拉上去”(位选),数字才会亮。

2. 进阶:如何让大家显示不一样的数字?(动态扫描)

如果你想显示 1234,既然大家共用一套“印章”,怎么做到不一样的显示?

答案:欺骗眼睛(视觉暂留)。

  1. 第 1 毫秒:拿出“1”的印章,只开第 1 个房间的灯。(第 1 位显示 1)

  2. 第 2 毫秒:拿出“2”的印章,只开第 2 个房间的灯。(第 2 位显示 2)

  3. 第 3 毫秒:拿出“3”的印章,只开第 3 个房间的灯…

  4. 循环:只要跑得够快,眼睛看来这 4 个数就是同时亮着的。

3. 什么是“消影”?

  • 现象:数字模糊,或者本该不亮的地方有重影。

  • 原因:单片机换“房间”速度太快,上一个数字的数据(电平)还残留在电路上,就打开了下一个房间的开关。

  • 解决:在切换下一个数字之前,先把所有灯关掉(送空数据),相当于“清场”。


二、 优化后的代码实战

这里整合了定时器数码管动态扫描按键控制

1. 硬件定义与全局变量

假设硬件基于常见的 74HC573 锁存器:

  • P2_6 控制段选 (dula)

  • P2_7 控制位选 (wela)

C

 #include <REGX52.H>
 ​
 // --- 数据定义 ---
 // 共阴极数码管段码表 (0-9, 不带小数点)
 unsigned char code seg_dula[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00}; 
 // 数码管位码表 (根据你的原理图,这里假设是低电平选通)
 unsigned char code seg_wela[] = {0xFE,0xFD,0xFB,0xF7,0xEF,0xDF};
 ​
 // --- 全局变量 ---
 unsigned char seg_buf[] = {0, 0, 0, 0}; // 显示缓冲区,存放每一位要显示的数字
 unsigned char seg_pos = 0;              // 当前扫描到的位置 (0-3)
 unsigned char mode = 0;                 // 当前数值模式

2. 定时器初始化

C

 void Timer0Init(void) // 1毫秒@12.000MHz
 {
     TMOD &= 0xF0;   // 保留高4位
     TMOD |= 0x01;   // 设置定时器0为模式1 (16位)
     TL0 = 0x18;     // 设置定时初值 (65536 - 1000 = 64536 = 0xFC18)
     TH0 = 0xFC;     
     TF0 = 0;        // 清除溢出标志
     TR0 = 1;        // 定时器0开始计时
     ET0 = 1;        // 开启定时器0中断
     EA = 1;         // 开启总中断
 }

3. 数码管底层驱动 (带消影)

C

 void seg_disp(unsigned char wela_index, unsigned char dula_index)
 {
     // 1. 发送段码 (形状)
     P0 = seg_dula[dula_index];
     P2_6 = 1; // 打开段锁存
     P2_6 = 0; // 锁住数据
 ​
     // 2. 发送位码 (位置)
     P0 = seg_wela[wela_index]; 
     P2_7 = 1; // 打开位锁存
     P2_7 = 0; // 锁住数据
     
     // 3. 稍微延时让LED亮一会儿 (在中断里利用时间间隔,这里可以省略,依靠中断周期)
     
     // 4. 消影 (关键步骤:把段选全灭,防止显示到下一位去)
     // 放在这里其实是为下一次循环做准备,
     // 或者通常做法是在每次更新显示前先送 0x00 到段选
     P0 = 0x00; 
     P2_6 = 1; 
     P2_6 = 0; 
 }

4. 中断服务函数 (核心:负责刷新显示)

我们将显示逻辑移入中断,每 1ms 刷新一位数码管。

C

 void timer0server() interrupt 1
 {
     // 重装载初值
     TL0 = 0x18;
     TH0 = 0xFC;
     
     // --- 数码管扫描逻辑 ---
     // 显示缓冲区中对应位置的数字
     seg_disp(seg_pos, seg_buf[seg_pos]);
     
     seg_pos++; // 切换到下一位
     if(seg_pos >= 4) // 假设只有4位数码管 (根据实际情况修改)
         seg_pos = 0;
 }

5. 主函数与按键逻辑

优化了按键的减法逻辑,防止 unsigned char 溢出。

C

 #include "key_read.h" // 假设你已经有了这个头文件
 ​
 void main()
 {
     unsigned char key_val, key_down, key_old;
     
     Timer0Init(); // 启动“大脑”的心跳
 ​
     while(1)
     {
         // --- 按键处理 ---
         key_val = key_Read(); // 读取按键状态
         key_down = key_val & (key_val ^ key_old); // 检测下降沿
         key_old = key_val;
 ​
         switch(key_down)
         {
             case 1: // 按键1:重置为0
                 mode = 0; 
                 break;
             
             case 2: // 按键2:减小数值
                 if(mode == 0) 
                     mode = 9; // 如果是0,减1变成9 (循环)
                 else 
                     mode--;
                 break;
             
             case 3: // 按键3:增加数值
                 mode++;
                 if(mode > 9) mode = 0;
                 break;
         }
 ​
         // --- 数据更新 ---
         // 将 mode 的值放入显示缓冲区
         // 例子:让第1位显示 mode,其他位显示固定值或者黑屏
         seg_buf[0] = mode; // 第1位显示当前调节的数
         seg_buf[1] = 10;   // 第2位黑屏 (假设索引10是0x00)
         seg_buf[2] = 10;   
         seg_buf[3] = 10;   
     }
 }

三、 为什么这么改?(优化点解析)

  1. 中断负责扫描,主循环负责逻辑

    • 以前:你在中断里写死了 seg_disp(1, mode),这导致数码管只能在一个位置亮,做不到多位显示。

    • 现在:中断里用 seg_pos 变量轮流切换位置。主循环只负责改数据 (seg_buf),不用管显示。这就是“显示与逻辑分离”的思想,非常重要!

  2. 修复 unsigned char 下溢出

    • 问题unsigned char 范围是 0~255。当 mode = 0 时,执行 mode-- 会变成 255,而不是 -1。

    • 解决:使用 if(mode == 0) mode = 9; 手动处理边界。

  3. 消影的位置

    • 为了防止鬼影,我在 seg_disp 的末尾加了清零操作。这样每次点亮并保持短暂时间(定时器间隔)后,在切换到下一位之前,先熄灭灯光,画面会更干净。