上周给实验室做一个小盒子,里面是一块 STC89C52,外面挂四位共阳数码管显示温度。面包板上先把程序跑通了,显示 1234 挺好看,我就直接照着焊到洞洞板上。结果同一套程序,焊完以后问题全冒出来了:数字 8 明显比 1 暗,最右一位有点发虚,连续显示十几分钟以后 P0 口附近摸着发烫,拿手机拍照还能看到横纹。

我一开始以为是扫描时序写错了,查了半天软件没查出问题。后来把四个现象拆开,才发现只有一个跟软件有关,另外三个都在硬件上。这篇就按我排查的顺序,把数码管从「能亮」到「能稳定显示」中间的几个关键点讲清楚。

一、先把共阴共阳和段码定死

数码管本质就是 8 个 LED(7 段加一个小数点)共用一端。共阳是 8 个 LED 的阳极连在一起,公共端接高电平,哪一段的阴极被拉低,哪一段就亮;共阴正好反过来。

所以段码表的方向是相反的。我用的是共阳管,段脚接 P0,低电平点亮,段码是这样:

// 共阳数码管段码,bit 顺序 dp g f e d c b a,0 表示该段点亮
code unsigned char SEG[] = {
    0xC0, // 0
    0xF9, // 1
    0xA4, // 2
    0xB0, // 3
    0x99, // 4
    0x92, // 5
    0x82, // 6
    0xF8, // 7
    0x80, // 8
    0x90, // 9
    0xFF  // 全灭
};

为什么先强调这个:我见过不止一次把共阴的码表抄到共阳管上,显示出来一片乱码,然后回头怀疑接线。先把极性确认了,后面的问题才有意义。

还有一个容易漏的点:51 的 P0 口是开漏结构,做段驱动时如果没有上拉,输出高电平时只是「浮空」,灭掉的那一段拉不高,会漏一点点光。白天看不出来,晚上关灯看就是一排淡淡的残影,很容易被误判成程序没消影。我后来在 P0 上并了一排 10K 上拉排阻,这一项才算彻底干净。

二、动态扫描的时间账

四位共用一组段线,靠轮流点亮骗过眼睛。每一位只亮一小段时间,四位循环一圈,人眼就看成同时亮。

刷新率 = 1 / (每位导通时间 × 位数)。每位 1ms、四位,刷新率就是 250Hz。人眼对 50Hz 以上的刷新基本不敏感,所以 250Hz 从「不闪」这个角度看是足够的。但刷新率也不是越高越好:每位导通时间越短,平均亮度越低,同时定时器中断的频率越高,MCU 花在刷新上的开销越大。这是一个很直接的取舍。

扫描节奏我用定时器中断来做,而不是在主循环里 delay。原因很实际:主循环里还要读传感器、收串口数据,每次耗时不一样,如果扫描跟着主循环走,节奏就会忽快忽慢,显示会抖。把刷新放进定时器中断,就和主逻辑解耦了,主循环卡一下也不影响显示。

12MHz 晶振、12T 模式下,一个机器周期是 1µs,1ms 需要 1000 个计数,定时器 16 位模式初值就是 65536 - 1000 = 64536 = 0xFC18。

三、限流电阻放在段上还是位选上

这是「亮度不均」的根因,也是我这次踩得最深的一个坑。

我最初的做法很省事:在每一位的公共端串一个 220Ω,只放四个电阻。这种叫位电阻,它限的是「这一位数码管的总电流」。问题在于,显示 1 只亮 2 段,显示 8 要亮 7 段。总电流被这一个电阻卡住,7 段去分和 2 段去分,每段拿到的电流完全不一样。所以 1888 里,开头那个 1 刺眼,后面三个 8 发灰。

正确做法是把限流电阻放在每一段上,每位 8 个,共 32 个。这样每一段的电流由它自己的电阻决定,跟这一位显示几段无关,亮度自然就一致了。

电流怎么算。共阳管段脚拉低导通,设段电阻为 R,红光 LED 正向压降按典型 1.8V 估,IO 低电平接近 0:

I = (5 − 1.8) / R

按这个式子,不同阻值下的峰值段电流大致如下:

段电阻 R 峰值段电流 8 段全亮时该位总电流 适用场合
100Ω 约 32mA 约 256mA 已远超普通 IO 能力,必须外加驱动
220Ω 约 14.5mA 约 116mA 需 74HC245 等缓冲,不能直连 P0
330Ω 约 9.7mA 约 78mA 加三极管位选后的常用值
470Ω 约 6.8mA 约 54mA 光线暗的环境,优先保 IO 安全

这里要特别注意:表里是峰值电流,不是平均电流。动态扫描每位只亮 1/4 时间,人眼看到的是平均亮度,所以很容易为了「更亮」把电阻一路改小,峰值电流悄悄就越过了 IO 的上限。P0 单个脚的灌电流、整组 8 个脚同时灌电流,手册里都有明确上限,而且 STC89C52 和 AT89C52 给的数值并不完全一样,一定要查自己手上那颗的手册。不能拿「我试了没烧」当依据——IO 是慢慢劣化的,当时不坏不代表没事。

超过能力就得加驱动,两条路:段线用 74HC245 做缓冲,或者位选用三极管、ULN2803 之类的管子去承担电流。我最后选的是后者:段电阻 330Ω,位选用 S8550 这种 PNP 三极管,基极串 1K。三极管接在公共阳极上,P2 输出低电平时导通,把 5V 送到这一位的公共端。本来 P0 要直接灌几十毫安,现在这个负担交给了三极管,IO 只负责给基极一点点电流,P0 发烫的问题也就没了。

两种方案的差别可以这样对照:

方案 限流位置 亮度一致性 IO 负担 元件数量
位电阻直驱 公共端 4 个 差,随显示段数变化 大 少
段电阻 + 三极管位选 每段 32 个 好 小 多
段电阻 + 74HC245 每段 32 个 好 小 中等

四、消影:顺序错了就有残影

动态扫描切换位的时候,如果先送新段码、再关旧位,旧位还亮着的那一瞬间就会把新段码显示出来,形成上一段的「影子」。这个现象在显示变化快的数字上最明显,比如秒表。

正确顺序只有三步,而且不能换:先关所有位选 → 再送段码 → 最后打开当前位。

P2 |= 0x0F;                      // 1. 关闭全部位选,消影
P0  = SEG[disp_buf[scan_idx]];   // 2. 送新段码
P2 &= ~(1 << scan_idx);          // 3. 打开当前位

P0 上的拉高排阻和「先关位」这两件事是配合的:先关位保证旧段码不会漏到新位,上拉保证灭掉的段真的灭干净。两个都做到,残影基本就看不见了。

五、完整代码:定时器中断扫描

把上面的结论落成代码,主循环只负责更新显示缓冲区,完全不碰端口:

#include <reg52.h>

typedef unsigned char u8;

code u8 SEG[] = {
    0xC0, 0xF9, 0xA4, 0xB0, 0x99,
    0x92, 0x82, 0xF8, 0x80, 0x90,
    0xFF
};

u8 disp_buf[4] = {0, 0, 0, 0};   // 四位要显示的数字 0~9
u8 scan_idx = 0;

void timer0_init(void)
{
    TMOD &= 0xF0;
    TMOD |= 0x01;      // 定时器0,模式1,16 位
    TH0 = 0xFC;        // 12MHz、12T 下约 1ms
    TL0 = 0x18;
    ET0 = 1;
    EA  = 1;
    TR0 = 1;
}

void timer0_isr(void) interrupt 1
{
    TH0 = 0xFC;        // 重装初值
    TL0 = 0x18;

    P2 |= 0x0F;                      // 关全部位选
    P0  = SEG[disp_buf[scan_idx]];   // 送段码
    P2 &= ~(1 << scan_idx);          // 开当前位
    scan_idx = (scan_idx + 1) & 0x03;
}

void main(void)
{
    P2 |= 0x0F;        // 上电先把所有位关掉,避免复位瞬间全亮
    P0  = 0xFF;
    timer0_init();

    disp_buf[0] = 1;
    disp_buf[1] = 2;
    disp_buf[2] = 3;
    disp_buf[3] = 4;

    while (1) {
        // 这里做传感器、串口、按键,随时可以改 disp_buf
    }
}

这段代码有个前提:位选是低电平有效(PNP 三极管基极拉低导通),段码是共阳低电平点亮。如果你的位选用的是 NPN 或者直接接 IO,极性要反过来,P2 |= 0x0F 和 P2 &= ~(1 << scan_idx) 这两句都要改。

六、怎么判断「显示正常」

焊完以后不要只看「亮不亮」,我按这几条验收:

  1. 显示 1111 和 8888,四位亮度应当基本一致。如果 8 明显发灰,说明限流还是放在了公共端。
  2. 手机用慢动作或者连拍几帧,看有没有缺位、横纹。有些问题肉眼反应不过来,但相机里很明显。
  3. 连续跑半小时,摸一下 IO 和三极管。这里只是定性检查有没有异常发热,真要判断电流是否超标还是得串万用表量。
  4. 断电再上电,看有没有哪一位整位闪一下。有的话说明上电初始化没先关位。

小结

这次的问题看起来是四件事,其实是一条链:位电阻导致亮度不均,段线无限流导致发烫,P0 开漏没上拉加重了残影,扫描顺序不对带来切换拖影。每一处单独看都能「跑起来」,叠在一起才让整块板子显得很不专业。

如果只让我记两句话:限流电阻放在每一段上,不要放在公共端;刷新交给定时器中断,主循环只管数据。 其余的三极管位选、上拉排阻、消影顺序,都是在这两条基础上把电流和时序收拾干净。