板子焊好、程序跑起来,能显示数字,高兴了大概十分钟。然后发现不对:每一位数字的左边都搭着一点上一位的笔画。显示 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 毫秒,也就是一秒钟不到两帧。这时候能清楚看到每一位轮流亮起来。
如果在这种慢速下,每一位亮的时候左边还带着上一位的笔画跟着亮,那就是切换顺序问题,改代码。如果慢速下完全干净、只有快速时才糊,那要往驱动和漏电流那边查。如果静态点亮(只点一位不动)就有残影,跟扫描一点关系都没有,是硬件或者段码表的问题。
这个测试花不了五分钟,能省掉半天对着代码看的时间。我第一次排查时是反过来做的,先怀疑段码表、再怀疑电阻,把硬件拆了两遍。
别把亮度不匀当成重影
最后区分两个经常被混在一起的现象。
重影是位置上多出来的笔画,笔画之间有间隙,看的是「多了一个形状」。亮度不匀是每一位的亮度不一样,第一位数比第八位数亮,但形状是对的。
亮度不匀通常是限流电阻的误差、或者位选驱动能力不够(同一时刻只开一位不该有这个问题,除非用了移位寄存器且输出内阻不同)。拿万用表量一遍每位对应的电阻,值差得超过百分之十,就先换电阻,别去动代码。
判断顺序我给一条:先看形状对不对,再看亮度匀不匀。形状不对是时序,亮度不对是硬件,两件事不要一起改,不然改完了也不知道是哪一步起的作用。