前段时间调一块六位数码管,客户给的验收标准只有一句话:肉眼看着不闪。我一开始觉得这事简单,动态扫描嘛,六位而已,51单片机定时器扫就完了。结果第一次上电就翻车——日光灯下看着还行,手机一怼上去满屏黑色滚动条,再用余光瞟屏幕边缘,明显有节奏地一跳一跳。把示波器夹上去一看,问题根本不在“亮不亮”,而在“刷新率到底由谁决定、是不是稳定”。
这篇文章就把我从“能亮”到“敢说它不闪”的完整过程拆开讲。标题里说的188芯片,是一类串行接口的数码管驱动IC,三根线控制、自带扫描逻辑,市面上丝印188或兼容TM1628/TM1638时序的片子都很常见。文章会讲清楚:50Hz这个数是怎么来的、188芯片在扫描链路里的位置、怎么把刷新率和亮度算明白,以及我实际踩过的五个坑。适合正在做时钟、倒计时、仪表显示的同学参考,不管你是用51、STM32、Arduino还是FPGA,底层逻辑是一样的。
1. 动态扫描为什么会有“可见闪烁”和“半亮”这两个怪问题
1.1 动态扫描的本质:用1/N的占空比换引脚数
数码管这东西,一位就是7段加小数点,8个引脚。做六位静态驱动,每组都要独立锁存或独立驱动,板子面积和成本直接翻倍。动态扫描的思路很简单:所有位的相同段并联,然后靠位选轮流点亮,一帧之内每一时刻只有一位在亮。眼睛有视觉暂留,看到的效果是“六位同时在亮”。
但这里有个关键点:LED和荧光粉不一样,它没有余辉,电流来了立刻亮、电流没了立刻灭。所以动态扫描能不能骗过眼睛,完全取决于“每一位亮多久、一秒钟重复多少次”,这个重复次数就是刷新率,单位Hz。刷新率低了,眼睛看到的不是稳定数字,而是一格一格跳动的“断片”。
同时,每一位的导通时间只有静态驱动的1/N,6位就是1/6。如果峰值电流还按静态那点值算,平均亮度就只有静态的1/6,看起来就是“半亮”甚至“暗得离谱”。新手最容易懵的就是这里:我明明加电了,怎么比静态驱动暗这么多?因为你没有把峰值电流按N倍补上去。后面会反复用到这个“平均电流=峰值电流×占空比”的关系。
1.2 50Hz是怎么被人当成“及格线”的
人眼对闪烁的分辨有个大致范围,明亮环境下临界融合频率通常在50~60Hz之间。电影24帧不觉得闪,是因为每帧通过快门遮挡闪了2~3次,等效刷新率上去了。早期电视场频用50Hz(PAL制式),也是这个数字被人记住的重要原因。
但50Hz真的只是及格线。中央视觉通常看不出50Hz的闪烁,余光对闪烁却极其敏感,快速扫视、挥手遮挡、旁边再放一盏高频光源,50Hz很容易露馅。经常有人争论“50Hz到底闪不闪”,我的观点是:个体差异极大,有人连60Hz都敏感,别拿“我眼睛看不出来”当验收依据。所以我做东西的习惯是:设计按50Hz打底,实机目标跑到75Hz甚至100Hz。标题问“如何用188芯片实现50Hz无闪烁”,我会先泼一盆冷水:50Hz是把“不闪”当最低要求,不是当最佳目标。
1.3 “无闪烁”其实要分三个层次来看
我把“不闪”拆成三层,方便后面排查问题:
- 第一层:肉眼直视不闪。刷新率≥50Hz基本能做到。
- 第二层:余光、扫视、手机拍照不露馅。需要刷新率更高,或者扫描波形非常干净。
- 第三层:和环境光源不产生拍频、数据更新不产生撕裂。这跟刷新率数值无关,跟同步策略和缓冲策略有关。
很多人排错顺序是反的:一开始就怀疑芯片、怀疑代码,连“自己到底在哪个层次不达标”都没搞清楚。先把目标层次定下来,后面每一章都能对上号。
2. 188芯片在扫描链路里的真正位置
2.1 它不只是“帮MCU省事的三极管”
先说清楚我手上这颗188是什么。它三根线(STB、CLK、DIO),内部有显示RAM、有扫描时序、有段驱动和位驱动,上电后MCU只要把显示数据写进去,它自己就一帧一帧扫。这跟“三极管+MCU逐位扫描”是两码事——后者扫描完全靠MCU定时器中断来做,MCU一忙,刷新率就掉。
国内这个小封装芯片市场很有意思,丝印写着188的片子,命令格式往往和TM1628/1638这类串行显示驱动IC几乎一样。我焊之前都会花十分钟对一下数据手册的命令表和GRID数量,因为同一个丝印下,可能是8位版本也可能是10位版本。这一点不确认,后面代码全是白写。
188芯片的典型引脚功能大致是这样:
| 引脚 | 作用 |
|---|---|
| STB/CS | 片选,低电平有效,一帧命令的开始和结束 |
| CLK | 串行时钟,上升沿锁存数据 |
| DIO | 双向数据,写显示数据和读按键都走这根线 |
| SEG1~SEG8 | 段驱动,接数码管a~g和dp |
| GRID1~GRID6/8 | 位驱动,接共阴数码管的COM脚 |
| VCC/GND | 5V供电,GND要就近接地 |
2.2 和MCU直驱、74HC595、MAX7219方案的本质区别
做数码管显示,方案其实就那几种,选型先看“扫描由谁负责”:
| 方案 | 控制引脚 | 扫描由谁做 | 刷新稳定性 | 最典型的坑 |
|---|---|---|---|---|
| MCU直驱+三极管 | N+8 | MCU定时器 | 取决于软件 | 主循环阻塞导致刷新率掉到几十Hz |
| 74HC595+位驱动 | 3~5 | MCU定时器 | 取决于软件 | 锁存时序不对引发鬼影 |
| 188类串行驱动芯片 | 3 | 芯片内部 | 硬件保证 | 命令和RAM地址写错,显示乱码 |
| MAX7219 | 3(SPI) | 芯片内部 | 硬件保证 | 限流电阻精度影响亮度一致性 |
还有同学喜欢用4511这种硬件译码器,它能把BCD码变成段码,省去软件查表,但扫描还是要MCU做,本质上还是软件扫描这一列。用188这类芯片最大的好处是:扫描频率由芯片内部RC振荡器决定,不占CPU,也不会因为主循环里跑了个延时函数就闪。但这不代表“上了芯片就不会闪”——后面第四章的坑,大部分都是“芯片本身扫得很好,却被外部电路和软件更新策略搞砸”。
2.3 选型和替换时的兼容性判断
如果你手头也有一颗丝印188的片子,先别急着写代码,按这个顺序做三件事:
- 查数据手册的“命令表”:看有没有0x40(数据命令)、0xC0段地址、0x80/0x88(显示控制)这些命令字。有的话,基本上可以按TM16xx系列的逻辑写。
- 数GRID引脚:决定你最多接几位。6位显示用8位芯片当然可以,但空出来的GRID必须处理,否则会出现幽灵残影。
- 看SEG的灌电流/拉电流上限:有些兼容片标称每段20mA,有些能到40mA。这直接决定你能不能靠芯片直驱,还是需要外扩三极管驱动位选。
我自己的原则:采购时不要只报丝印,要让卖家提供PDF,并且上电后先写全0、再写全1,用示波器确认SEG/GRID波形极性对不对。这一步半小时能省后面三天。
3. 从“想要50Hz”到“代码和接线”:完整推演
3.1 刷新率、导通时间、峰值电流三者互相绑架
先说公式。动态扫描的整屏帧率由“每位数导通时间”和“位数”决定:
帧率 = 1 / (每位数导通时间 × 位数)
要50Hz帧率、6位显示,每位数导通时间就是20ms/6≈3.33ms。要跑到75Hz,就是13.3ms/6≈2.2ms/位。对于188这类内置扫描芯片,这个导通时间由芯片内部振荡器和GRID数决定,通常远高于这个值,所以我后面会单独说“MCU手动扫描时怎么算”;芯片自扫时你要关心的反而是数据更新那一层。
数字摆出来更直观:
| 位数N | 50Hz帧率下单管导通时间 | 75Hz帧率下 | 100Hz帧率下 | 等效静态亮度所需的峰值电流(以平均5mA为例) |
|---|---|---|---|---|
| 4位 | 5.0ms | 3.3ms | 2.5ms | 20mA |
| 6位 | 3.3ms | 2.2ms | 1.7ms | 30mA |
| 8位 | 2.5ms | 1.7ms | 1.25ms | 40mA |
这张表看着简单,里面有个工程约束:峰值电流不能随便拉。红色数码管一段连续工作电流一般按10~20mA设计,峰值可以到60mA甚至更高,但要查LED规格书。188这类芯片的段驱动上限通常也在30~40mA。我一般按“静态5mA等效亮度”来倒推峰值,也就是峰值=N×5mA,6位就是30mA,卡在芯片上限附近,留一点点余量。真嫌亮度不够,就外扩驱动,不要硬怼芯片。
3.2 188芯片的数据帧格式:先搞清楚你的芯片是“几字节一位”
这里给一个通用模板。兼容TM1628/1638时序的芯片,典型数据帧有三段:
- 命令1(数据命令):比如0x40,表示“写显示数据,地址自动加一”。
- 命令2(地址命令):0xC0加上起始地址,表示从哪个GRID开始写。
- 数据:连续写入若干字节,每字节对应一个GRID的段数据。
代码可以写成这样(我这里是GPIO模拟时序,Arduino同理):
// 假设 STB/CLK/DIO 已经定义好高低电平操作 static void write_byte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { CLK_LOW(); DIO = (dat & 0x01) ? 1 : 0; dat >>= 1; CLK_HIGH(); // 上升沿锁存 } } static void write_cmd(uint8_t cmd) { STB_LOW(); write_byte(cmd); STB_HIGH(); } // 显示缓冲,6位 static uint8_t seg_buf[6]; void display_update(void) { STB_LOW(); write_byte(0x40); // 数据命令:写显示、地址自增 STB_HIGH(); STB_LOW(); write_byte(0xC0); // 起始地址:第0位 for (uint8_t i = 0; i < 6; i++) { write_byte(seg_buf[i]); } STB_HIGH(); } void display_on(uint8_t brightness) // brightness: 0~7 { write_cmd(0x88 | (brightness & 0x07)); }有几句话必须交代:地址映射到底是“一个GRID一个字节”还是“一个GRID两个字节”,不同芯片不一样,必须以手上的数据手册为准。段码表也要自己整理,数码管a段对应字节的哪一位,网上给的段码表不一定和你PCB走线一致。我吃过这个亏,接好线后先写一个“扫段程序”,一位一位点亮,把实际对应关系记下来再做段码表。
3.3 初始化顺序:先关显示、再清RAM、最后开显示
这类芯片的RAM上电状态是随机的。如果上电就开显示,你会看到乱码闪一下,运气好是一堆不规律的段,运气不好整屏全亮。正确顺序是:
- VCC稳定后等10ms以上。
- 发送“显示关闭”命令。
- 给显示RAM全部写0。
- 发送“显示开启+亮度”命令。
- 再往RAM里写真正的显示数据。
顺序不能反。很多“上电闪一下”的板子,就是少了第2和第3步。代码上没多复杂,但顺序错了,排查起来会怀疑人生。
提示:清空显示不要用“显示关闭”来代替。显示关闭只是让扫描停掉,RAM里的内容还在,重新开显示时旧数据会原样回来。要清就写0。
3.4 更新策略:不要让“刷数据”变成“拖刷新”
188芯片自扫的刷新率是硬件保证的,但有一个常见误会:有人一秒钟只更新一次显示数据,更新的时候还先发一条“显示关”,写完再“显示开”。这么一做,等于人为把整屏刷新率干到了1Hz,肉眼直接看到闪——这是自己给自己挖坑。
正确的做法是:更新显示数据时,永远不要关显示。直接用“写数据命令”覆盖RAM即可,芯片内部扫描不会停下来等。6个字节用100kHz时钟写也就一两毫秒,在20ms一帧的尺度上完全无感,人眼根本看不出“撕裂”。
如果是手动扫描(MCU定时器中断方式),那就要注意撕裂问题:中断里切换位选时,先把所有段灭掉,再断位选,再送新段码,最后开下一位。只要在“灭段→断位→送码→开位”的间隙里更新缓冲区,就不会出现某一个位显示新旧混合数据的瞬间。更稳妥的办法是双缓冲:主循环往后台缓冲写,中断里在间隙时刻交换指针。
4. 我实际踩过的五个坑,以及每一个的根因
4.1 鬼影:先“灭段”还是先“断位”?
鬼影是所有动态扫描都会遇到的问题,表现形式是:你明明只显示了“123456”,却能在某个数字旁边看到前一位的淡淡残影,或者移动视线时字符边缘有拖影。
根因在切换瞬间。手动扫描时,如果先打开下一位的位选,再更新段码,那么开位选的瞬间,SEG线上还是上一位的数据,这段时间里上一位的段会在下一位上亮一下。这个时间虽然短,但亮度高、重复频率高,视觉上就是拖影。
正确顺序永远是:灭段 → 断位 → 送新段码 → 开下一位。对188这类芯片,内部扫描本来就带消隐间隙,正常不需要你操心。但如果你跟我一样,因为电流不够在外部加了位驱动三极管,那就要注意三极管的关断存储时间了。NPN位驱动三极管在关断时有几百纳秒到几微秒的拖尾,处理办法是基极对地接一颗10k电阻加速放电,或者干脆换成MOS管。
4.2 亮度不均:你以为在写代码,其实在搞电源
第二个坑是亮度不均。同事第一反应是“段码表错了”,其实大多数时候是电源被动态扫描电流脉冲打崩了。动态扫描下,每一瞬间只有一个位在工作,这一位的全部段电流同时从电源抽取,波形是一串脉冲。如果PCB上逻辑地和显示地共用一条细走线,这段走线的寄生电阻就会在脉冲来时产生压降,导致:点亮的段越多,电源跌得越狠,其他位看起来越暗。
我当时处理办法是:显示电源和逻辑电源分开走线,星形接地;在显示电源入口放100~470μF电解电容,每个数码管COM脚附近再放0.1μF陶瓷电容;段限流电阻每段一颗,绝不共用。段电阻共用是另一个高频错误——如果一位的8段共用一颗电阻,亮的段数一变,每段电流都变,亮度直接跟着数字内容走。
限流电阻计算给个例子:5V供电、红色数码管压降约2.0V、188芯片输出级压降按0.5V估,目标峰值电流20mA,电阻就是(5−2.0−0.5)/0.02≈125Ω,取120Ω。这个计算口径要记牢,很多人把驱动芯片的压降忘了,电阻选大了一圈,整体亮度就上不去。
4.3 市电50Hz环境的“拍频闪”
这个坑最隐蔽。有块板子拿回家一测就闪,在办公室却好好的。后来发现问题出在“数码管刷新率”和“环境光源100Hz纹波”之间的拍频。
室内LED灯和荧光灯,驱动电源绝大多数是整流后的脉动供电,光强本身有100Hz的纹波(50Hz市电的两倍频)。如果你的数码管帧率恰好接近50Hz,但又不是严格电网同步(比如晶振漂移导致49.7Hz),两个频率会叠加出一个慢变的亮暗周期,视觉上就是“呼吸感”。这其实不是数码管扫得不够快,而是两个光源在打架。
对策有三个,可以叠加使用:
- 把刷新率提高到75Hz或更高,避开和100Hz纹波的低频拍频。
- 显示供电加一级LDO或LC滤波,把电源纹波压下去,数码管亮度就不随市电波动。
- 如果项目必须锁在50Hz(比如要做电网同步显示),那就从交流零点检测来同步刷新,让显示帧率和市电同源。
另外提醒一句:用手机拍照验证是否闪烁时,室内LED灯的100Hz纹波会叠加进画面,让你误判数码管在闪。拍照只能做参考,下结论还得看示波器。
4.4 共阴共阳接反的“怪症状”
188这类芯片大多按共阴数码管设计,GRID做低电平扫描,SEG做高电平点亮。你要是不小心用了共阳数码管,症状会很怪:可能完全不亮,可能乱码,更常见的是“段和位之间微弱漏光”——因为电流会顺着错误的路径从SEG灌进去,经过LED再到COM,最后从另外的COM脚偷偷回来。
拿到新数码管先别急着焊,用万用表二极管档量:黑表笔接COM,红表笔轮流点各段,亮了就是共阴;反过来是共阳。像3611BS这类一体式模块,不同厂家丝印规则还不一样,COM脚定义差异很大,一定要实测确认。
解决方案就三条路:换成共阴数码管;如果手上只有共阳,加PNP三极管做位驱动反相,段驱动再加限流;或者选一颗专门支持共阳的驱动芯片。不要在共阳上硬套共阴代码,那不是改个取反就能救回来的,电流路径都不对。
4.5 初始化顺序和未用GRID的处理
最后一个坑,是用8位GRID的芯片驱动6位数码管时踩的。没用的两位不接数码管,但RAM地址还占着,如果里面的数据不是0,扫描每到这两个GRID时,外部引脚上会出现不该有的电平变化,在某些走线条件下会感应到旁边数字位上,形成“幽灵位”。
处理办法:初始化时把所有GRID的RAM都写0,包括不用的那几位;同时如果芯片支持“扫描位数设置”,把它设成实际位数,让芯片跳过空GRID。这样既省电,也杜绝幽灵残影。命令里如果看到类似“扫描配置寄存器”的东西,别跳过,看一眼。
5. 怎么证明它真的不闪:人眼、手机、示波器三板斧
5.1 人眼检验法:直视、余光、扫视
人眼是最快的初筛工具,但要用对方法。三步走:
- 正对着屏幕,盯住一个字符看10秒,看有没有抖动、跳动。
- 眼睛不看屏幕,用余光注意屏幕边缘,同时用手掌在屏幕前快速划过,看字符有没有“断开”或者“梳子状”分层。
- 左右快速摇头或滚动眼球,观察字符边缘有没有拖尾。
这三步比单纯“盯着看”敏感得多。余光对高频闪烁的感知比中央视觉强,扫视则会把时间上的亮度波动展开成空间上的条纹。如果三步都干净,基本可以确认视觉上无闪烁。
5.2 手机相机法:别把快门当成示波器
用手机拍数码管,很容易拍出黑色滚动条。原因不是你的板子在闪,而是手机CMOS是滚动快门,逐行曝光,和显示刷新率存在采样差频。显示刷新率越高,暗带越细越密;刷新率越低,暗带越宽、滚动越明显。所以拍照的正确用法是“横向对比”:同一个手机、同一个快门,分别拍已知的60Hz、100Hz方案和你的板子,对比暗带形态。如果你板子的暗带明显比100Hz方案宽很多,那大概率低于目标;如果暗带细密几乎看不见,就不用纠结。
还要补一句:手机显示“不闪”不等于人眼“不闪”——由于采样频率关系,手机可能漏拍某些频率的闪烁。所以手机法只用来过滤“明显不合格”,最终结论以人眼三步法和示波器为准。
5.3 示波器是唯一能下结论的工具
示波器才是判官。我常测四个点:
| 测试点 | 正常波形 | 异常现象 | 对应问题 |
|---|---|---|---|
| GRID1对GND | 周期20ms(50Hz),低电平宽度约3.3ms(6位) | 周期抖动、宽度漂移 | 扫描时序不稳/晶振问题 |
| SEG-X段对GND | 周期20ms的脉冲串,占空比约1/6 | 脉冲边缘拖尾、振铃 | 鬼影、驱动器件关断慢 |
| VCC对GND | 直流,纹波<50mV | 周期性跌落超过200mV | 电源去耦不足/走线太细 |
| DIO信号 | 仅写入时有一串脉冲 | 长期活跃/波形畸形 | 命令模式错误、STB时序不对 |
测的时候注意探头地线要短,别用长鳄鱼夹,否则看到的全是探头自己捡的噪声。举个例子:如果你量到GRID周期不是20ms而是30ms,说明实际帧率只有33Hz,肉眼不闪才怪;如果周期是20ms但低电平宽度忽大忽小,说明芯片配置或外部驱动在抖动,要回到第三章检查。还可以在COM回路里串一颗1Ω采样电阻,测位电流的峰值和平均值,反推实际占空比和亮度一致性。这套做完,“不闪”就不再是感觉,而是有波形图可查的结论了。
6. 从“不闪”到“更好看”:顺手能做的几个优化
6.1 用芯片的PWM亮度寄存器代替硬件减流
188这类芯片一般自带亮度控制,本质是改变扫描期间段的导通占空比,常见有8级或16级。调亮度优先用这个,而不是去改限流电阻。原因是:改限流电阻会让峰值电流偏离设计点,一方面亮度一致性变差,另一方面可能让驱动级进入线性区,波形变难看不说,芯片还可能发热。PWM亮度只是改占空比,扫描频率不变、峰值电流不变,闪烁特性不会变差。
6.2 给扫描留一点“呼吸时间”
如果你走的是MCU手动扫描路线,我会在每位数导通时间后面特意留50~100μs的全灭时间。以6位、50Hz为例,每周期20ms,每位分到3.3ms,扣掉0.1ms消隐,实际亮度只损失3%,换来的是切换瞬间鬼影彻底消失。这3%的亮度损失,靠把峰值电流提高一点点就补回来了,但鬼影问题不是提高电流能补的。我对这个“呼吸时间”的评价就一句:值得。
6.3 时钟/倒计时产品的局部更新建议
最后给做时钟、秒表、倒计时的同学一个建议:显示数据按秒变化时,不要为了省事每秒重发整个缓冲,除非你有刷新整屏的强迫症。更好的做法是写一个带“起始地址和长度”的局部更新函数,哪位变了就更新哪几位。这不光是为了省那几毫秒,更是为了减少每一次更新引入的EMI脉冲,以及降低多个任务抢总线时的冲突概率。代码层面只是把display_update改成带参数的版本,成本很低,效果很好。
我个人的体会是:数码管显示这块,九成问题不是芯片不够好,而是“以为扫描由芯片负责就不用管时序”和“以为代码对了就不用管电源”这两种心态。把帧率当设计指标、把消隐当基本礼仪、把电源当第一优先级,基本就不会翻车。188芯片用顺手之后,我只改一个寄存器就能从50Hz推到75Hz,肉眼几乎察觉不到差异,但手机拍出来、余光瞟过去,差距立刻就能看出来——这也是我为什么建议你把50Hz当底线而不是目标的原因。