4. 数码管动态扫描:多位数码管动态显示原理、扫描时序、消影处理、显示多位数字
好,咱们接着聊数码管。上一章我们搞定了单个数码管的静态显示,但实际项目中,谁会用单个数码管呢?电梯楼层至少两位数起步,对吧?
这一章,我们就来啃下「动态扫描」这块硬骨头。说白了,就是用一套硬件,分时复用,让多个数码管「看起来」同时亮。
4.1 动态显示原理:人眼的「小把戏」
先问大家一个问题:为什么多个数码管可以轮流亮,我们却感觉它们一直在亮?
答案是人眼的视觉暂留效应。简单讲,画面消失后,人眼还能保留约 20ms 的影像。只要我们在 20ms 内把所有数码管轮流点亮一遍,大脑就会自动「脑补」成同时点亮。
我刚开始做这个的时候,犯过一个低级错误。我把刷新周期设成了 50ms,结果数码管闪得跟迪斯科灯球似的。后来才明白,刷新频率至少要 50Hz 以上,也就是每位数码管点亮时间不能超过 20ms / 位数。
核心公式:
单个数码管点亮时间 = 刷新周期 / 数码管个数
刷新周期 ≤ 20ms(建议 10ms 以内)
举个例子,4 位数码管,刷新周期设为 10ms,那么每个数码管点亮时间就是 10ms / 4 = 2.5ms。这个时间足够亮了,而且人眼完全感觉不到闪烁。
4.2 扫描时序:谁先亮,谁后亮?
动态扫描的时序,说白了就是「分时轮询」。我习惯用一个状态机来控制,每个状态对应一个数码管。
咱们以 4 位共阴极数码管为例,看看扫描时序怎么设计:
| 时间段 | 位选信号 | 段选数据 | 显示内容 |
|---|---|---|---|
| T0 ~ T1 | 位选1(低电平有效) | 千位段码 | 千位数字 |
| T1 ~ T2 | 位选2(低电平有效) | 百位段码 | 百位数字 |
| T2 ~ T3 | 位选3(低电平有效) | 十位段码 | 十位数字 |
| T3 ~ T4 | 位选4(低电平有效) | 个位段码 | 个位数字 |
每个时间段只点亮一个数码管,其他三个全部熄灭。这样循环往复,就完成了动态扫描。
嗯,这里要注意:每个数码管点亮的时间必须严格相等,否则会出现亮度不均。我曾经在项目里偷懒,用延时函数来分配时间,结果千位明显比个位亮。后来改用定时器中断,才解决了这个问题。
4.3 消影处理:别让「鬼影」毁了你的作品
动态扫描最头疼的问题是什么?鬼影!
什么是鬼影?就是本该熄灭的数码管,微微发亮,或者显示上一帧的残影。你想想看,电梯里显示楼层,数字旁边还带个淡淡的残影,多掉价。
鬼影产生的原因很简单:位选信号切换时,段选数据还没更新。比如,上一帧显示的是数字「1」,段选数据是 0x06。切换到下一帧时,位选先变了,但段选还是 0x06,于是不该亮的数码管就闪了一下「1」的残影。
怎么解决?我总结了三个步骤:
- 先关位选,再更新段选:切换前,先把所有位选信号置为无效(共阴极置高,共阳极置低)。
- 更新段选数据:把下一帧要显示的段码送到数据线上。
- 再开位选:最后才打开当前要显示的数码管位选。
用代码表示就是:
// 消影处理示例(共阴极数码管)
void display_digit(uint8_t pos, uint8_t num) {
// 第一步:关闭所有位选
GPIO_ResetBits(GPIOB, 0x0F); // 全部置低,关闭所有数码管
// 第二步:更新段选数据
uint8_t seg_code = seg_table[num]; // 查表获取段码
GPIO_Write(GPIOA, seg_code); // 输出段码
// 第三步:打开当前位选
GPIO_SetBits(GPIOB, (1 << pos)); // 只打开当前位
}
我曾经踩过的坑:
有一次我图省事,把「关闭所有位选」和「更新段选」的顺序搞反了。结果鬼影没消掉,反而多了一个闪烁的亮点。后来排查了半天,才发现是时序顺序的问题。记住:先关后开,中间更新数据,这个顺序不能乱。
4.4 显示多位数字:从数据到显示的完整流程
好了,原理和消影都搞定了,现在我们来显示一个具体的数字,比如「2024」。
显示多位数字,核心就两步:
- 拆位:把整数拆成千位、百位、十位、个位。
- 查表:每个位对应的数字,查段码表得到段选数据。
拆位的方法很简单,用除法和取模:
uint16_t number = 2024;
uint8_t thousands = number / 1000; // 千位:2
uint8_t hundreds = (number % 1000) / 100; // 百位:0
uint8_t tens = (number % 100) / 10; // 十位:2
uint8_t ones = number % 10; // 个位:4
然后,在定时器中断里,依次显示这四个位:
// 定时器中断服务函数(每 2.5ms 触发一次)
void TIM_IRQHandler(void) {
static uint8_t current_pos = 0;
switch(current_pos) {
case 0: display_digit(0, thousands); break;
case 1: display_digit(1, hundreds); break;
case 2: display_digit(2, tens); break;
case 3: display_digit(3, ones); break;
}
current_pos++;
if(current_pos >= 4) current_pos = 0;
}
个人经验:
我建议把显示刷新放在定时器中断里,而不是主循环。主循环里可能有按键扫描、通信处理等任务,一旦被阻塞,数码管就会闪烁。用定时器中断,刷新频率是固定的,显示效果稳定得多。
4.5 实战避坑指南
最后,分享几个我实际项目中踩过的坑,希望能帮大家少走弯路:
- 亮度不均:如果发现某个数码管特别亮或特别暗,检查一下每个位的点亮时间是否一致。用示波器抓一下位选信号,一目了然。
- 闪烁问题:刷新频率低于 50Hz 必闪。我一般设到 100Hz,也就是 10ms 刷新一次,4 位数码管每 2.5ms 切换一次。
- 段码冲突:如果多个数码管共用了段选数据线,切换时一定要先关位选。否则数据线上的毛刺会导致鬼影。
- 电源纹波:动态扫描时,所有数码管瞬间电流变化很大。记得在电源端加 100μF 电解电容和 0.1μF 瓷片电容,否则单片机可能会复位。
好了,这一章的内容就到这里。动态扫描是嵌入式显示的基础,掌握了它,后面做更复杂的显示(比如点阵屏、LCD)就轻松多了。下一章,我们聊聊如何用按键控制电梯楼层,实现真正的「上下楼」功能。