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」的残影。

怎么解决?我总结了三个步骤:

  1. 先关位选,再更新段选:切换前,先把所有位选信号置为无效(共阴极置高,共阳极置低)。
  2. 更新段选数据:把下一帧要显示的段码送到数据线上。
  3. 再开位选:最后才打开当前要显示的数码管位选。

用代码表示就是:

// 消影处理示例(共阴极数码管)
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)就轻松多了。下一章,我们聊聊如何用按键控制电梯楼层,实现真正的「上下楼」功能。