零基础入门三:数码管与定时器实战
一、 认识数码管原理(大白话版)
我们不用晦涩的术语,继续沿用“房间与灯”的例子来理解。
1. 核心概念
把 4 位数码管想象成 4 个并排的房间:
-
房间里的灯(a-dp):每个房间墙上都有 8 根灯管,拼成“8”字。
-
总电路:所有房间的同位置灯管是连在一起的(比如所有房间的“a”灯管都连着同一根电线)。
关键角色
-
段选 (Segment) —— “笔画/形状”
-
作用:决定画出来是个什么数字(是“1”还是“8”)。
-
比喻:就像你手里的印章。你想印个“5”,所有房间里就都准备好了“5”的形状。
-
对应代码:
dula(段锁存)。
-
-
位选 (Digit) —— “地盘/位置”
-
作用:决定哪个房间的灯亮起来。
-
比喻:就像房间的总电闸。
-
对应代码:
wela(位锁存)。 -
规则:只有“印章盖下去”(段选)且“电闸拉上去”(位选),数字才会亮。
-
2. 进阶:如何让大家显示不一样的数字?(动态扫描)
如果你想显示 1234,既然大家共用一套“印章”,怎么做到不一样的显示?
答案:欺骗眼睛(视觉暂留)。
-
第 1 毫秒:拿出“1”的印章,只开第 1 个房间的灯。(第 1 位显示 1)
-
第 2 毫秒:拿出“2”的印章,只开第 2 个房间的灯。(第 2 位显示 2)
-
第 3 毫秒:拿出“3”的印章,只开第 3 个房间的灯…
-
循环:只要跑得够快,眼睛看来这 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;
}
}
三、 为什么这么改?(优化点解析)
-
中断负责扫描,主循环负责逻辑:
-
以前:你在中断里写死了
seg_disp(1, mode),这导致数码管只能在一个位置亮,做不到多位显示。 -
现在:中断里用
seg_pos变量轮流切换位置。主循环只负责改数据 (seg_buf),不用管显示。这就是“显示与逻辑分离”的思想,非常重要!
-
-
修复
unsigned char下溢出:-
问题:
unsigned char范围是 0~255。当mode = 0时,执行mode--会变成 255,而不是 -1。 -
解决:使用
if(mode == 0) mode = 9;手动处理边界。
-
-
消影的位置:
- 为了防止鬼影,我在
seg_disp的末尾加了清零操作。这样每次点亮并保持短暂时间(定时器间隔)后,在切换到下一位之前,先熄灭灯光,画面会更干净。
- 为了防止鬼影,我在