板子焊好、程序跑起来,能显示数字,高兴了大概十分钟。然后发现不对:每一位数字的左边都搭着一点上一位的笔画。显示 18 的时候,8 的左边多一道竖线,看着像 1I8。数字越小越明显,因为笔画少,多出来的那点特别扎眼。

我一开始怀疑是段码表写错了,把段码表一位一位对着手册核了一遍,没问题。又怀疑是限流电阻不对导致漏光,量了一遍,也没问题。最后才想到是扫描时序 —— 确实就是它,而且改法只有三行。

现象:8 的左边有一道暗线

把要显示的内容全改成 1,也就是所有位都只有右边两条竖线。这时候如果每一位都正常,屏幕上应该是 12 条整齐的竖线。实际看到的是每条竖线左边还压着一道很淡的影子,亮度大概只有正常笔画的三分之一。

这个「三分之一」很有用,它说明影子不是一直亮着的,是只在某一段时间里亮。如果影子跟正常笔画一样亮,那又是另外一回事了。

扫描是怎么点亮一位的

先说清接线。我的板子是 12 位,段码共用一组 IO,位选各占一根。同一时刻只有一位能被点亮,靠不停地换,让人眼看起来是同时亮的。

一次扫描要做三件事:给段码口送这一位的字形、把这一位的位选打开、等一段时间。等够了换下一位。12 位轮一圈,就是人眼看到的「一帧」。

我这边一个位的时间是 830 微秒,12 位轮一圈大约 10 毫秒,也就是一秒钟 100 帧。这个数不是随便定的,帧率低于 50 就会看出整屏在抖,具体怎么算见另一篇讲闪烁的。

重影只有一个来源:切换顺序

问题出在「换下一位」这个动作上。段码口是共用的,它不会自己清零 —— 上一位的段码在换位之后,还稳稳地停在口上。

如果代码是先换位选、再送段码,那就有那么一小段时间,下一位已经被点亮了,段码口上却是上一位的字形。这段时间很短,但它是真的导通、真的发光。12 位轮一圈,每一位都这样蹭一下,看起来就是每个数字左边都挂着上一位的一点笔画。

反过来想就明白了:正确的顺序必须是「先让屏幕全灭,再换位,再送新段码」,不能让「新位选」和「旧段码」有任何一刻同时存在。

三种消隐写法

下面按共阳接法写:段码口输出低电平点亮,位选低电平有效。共阴的话把电平反过来,逻辑一样。

第一种,先关位选,再送段码,最后开位选。这是最稳的,因为它先把发光这件事彻底断掉,再改内容。

/* T0 中断,每 830us 一次 */
void T0_ISR(void) interrupt 1
{
    TH0 = T0_H;
    TL0 = T0_L;

    P1 = 0xFF;                  /* 1. 位选全关,屏幕全灭 */
    P0 = seg_code[buf[now]];    /* 2. 送这一位的段码 */
    P1 = ~(1 << now);           /* 3. 只开这一位 */

    if (++now >= DIGITS) now = 0;
}

第二种,先把段码清零,再换位,再送段码。效果一样,区别是关的是段码这一头。

    P0 = 0xFF;                  /* 段码全灭 */
    P1 = ~(1 << now);
    P0 = seg_code[buf[now]];

这两种都要求中间那步和下一步之间不要有别的活。中间插一个函数调用、插一次串口等待,灭的时间就长了,亮度会掉。

第三种,用输出使能。如果位选后面挂了 74HC245 或者带 OE 脚的缓冲器,把 OE 拉高让输出变高阻,比改数据口更干脆。代价是多一根控制线,多占一个 IO。

我用的第一种。灭的时间是两条指令,一两个微秒,占 830 微秒的千分之二,肉眼看不出来亮度变化。

C 语言里还有一个很容易忽略的点:P1 = 0xFF 之后要紧跟下一步,中间千万别插 if 判断或者延时。我看过一版代码在中间加了个「如果亮度等级是低就多延时」,结果灭的时间变成几十微秒,屏幕明显变暗,还以为是自己把占空比算错了。

用 74HC595 的,问题出在锁存

位数多了以后很多人用 595 串行扩展,位选和段码都用它。这时候不是「口上留着旧数据」的问题,是锁存顺序的问题。

595 的工作方式:数据一位一位打进去,这时候输出不变;给一个锁存脉冲,刚才打进去的内容才一次性出现在输出上。所以只要一次锁存的内容是完整的、自洽的,就不会串。

容易出错的写法是分两次锁存:先锁位选,再锁段码。中间那一瞬间,新位选已经生效,段码还是上一轮的 —— 跟直接 IO 那版一模一样的重影。

正确做法是把「这一位的位选 + 这一位的段码」一起打完,只发一次锁存脉冲:

void show_one(u8 idx)
{
    OE = 1;                                  /* 关输出,防止移位过程上屏 */
    shift_out(seg_code[buf[idx]]);           /* 先打段码 */
    shift_out(1 << idx);                     /* 再打位选 */
    RCK = 0; RCK = 1;                        /* 只有这一次锁存 */
    OE = 0;                                  /* 开输出 */
}

里面那个 OE 很多人不接,觉得有锁存就够了。接上是白捡的保险,因为移位过程中如果 OE 一直开着,有些板子会看到明显的一扫而过的亮线。

驱动关断要时间

就算代码顺序全对,还是可能留一点点残影。这时候要看硬件:位选如果是用三极管做的开关,它从深度饱和退回截止需要时间,专业叫存储时间,几微秒到几十微秒都有。这期间电流还在流,段码已经换了,就蹭上了。

两个方向:一是别把三极管推到太深的饱和,基极电阻别给太小;二是软件上给一点余量 —— 先关位选,中间空一两条 _nop_(),再送段码。我板子上没加,因为顺序改对之后残影就没了。

还有一种特别的情况:位选是三极管、段码直连单片机。关掉位选后,段码口的电压会通过数码管内部的二极管爬到别的位上,形成全屏都很淡的「鬼火」。这是漏电流,不是时序,得在段码口加下拉或者换驱动。

怎么一眼确认是这个问题

别猜,把扫描速度调慢直接看。把定时器的重装值临时改大,让一位亮 50 毫秒、一轮 600 毫秒,也就是一秒钟不到两帧。这时候能清楚看到每一位轮流亮起来。

如果在这种慢速下,每一位亮的时候左边还带着上一位的笔画跟着亮,那就是切换顺序问题,改代码。如果慢速下完全干净、只有快速时才糊,那要往驱动和漏电流那边查。如果静态点亮(只点一位不动)就有残影,跟扫描一点关系都没有,是硬件或者段码表的问题。

这个测试花不了五分钟,能省掉半天对着代码看的时间。我第一次排查时是反过来做的,先怀疑段码表、再怀疑电阻,把硬件拆了两遍。

别把亮度不匀当成重影

最后区分两个经常被混在一起的现象。

重影是位置上多出来的笔画,笔画之间有间隙,看的是「多了一个形状」。亮度不匀是每一位的亮度不一样,第一位数比第八位数亮,但形状是对的。

亮度不匀通常是限流电阻的误差、或者位选驱动能力不够(同一时刻只开一位不该有这个问题,除非用了移位寄存器且输出内阻不同)。拿万用表量一遍每位对应的电阻,值差得超过百分之十,就先换电阻,别去动代码。

判断顺序我给一条:先看形状对不对,再看亮度匀不匀。形状不对是时序,亮度不对是硬件,两件事不要一起改,不然改完了也不知道是哪一步起的作用。