1. 从一个“闪烁”的数码管说起玩过单片机或者自己焊过小玩意儿的朋友大概率都碰过这么个场景明明程序里写的是让数码管显示“1234”结果烧录进去一看要么只亮了一位数要么四位数字疯狂闪烁亮度还忽明忽暗像接触不良的老式霓虹灯。更离谱的是有时候手一碰电路板显示又正常了松手又乱套。这种“薛定谔的显示”背后十有八九不是硬件坏了而是动态扫描的时序没调对。数码管和点阵屏本质上都是LED阵列。单个LED点亮很简单给电就亮。但当你需要同时显示多位数字或者驱动一块8x8甚至更大的点阵屏时问题就来了如果每个LED都单独接一个IO口那引脚数量会爆炸。比如四位八段数码管共阴极的话8个段选加4个位选12个引脚还能接受但换成16x16点阵屏256个LED直接驱动需要256个IO任何单片机都扛不住。于是动态扫描这个方案就登场了。它的核心思路极其朴素利用人眼的视觉暂留效应让LED轮流点亮只要轮换速度够快眼睛就会觉得它们是在同时发光。这篇文章我想从实际调试的角度把动态扫描这件事掰开揉碎聊一聊。不管你是刚入门的电子爱好者还是写过几行单片机代码想驱动点阵屏的开发者只要你对“为什么我的数码管会闪”“点阵屏亮度不均匀怎么办”“扫描频率到底设多少合适”这些问题感兴趣下面的内容应该能帮你省下不少抓头发的时间。我会从原理、硬件选型、代码实现、参数计算到常见故障排查一条线讲下来尽量说人话少堆公式多讲我踩过的坑。2. 动态扫描到底是怎么“骗”过眼睛的2.1 视觉暂留与临界闪烁频率人眼有一种特性叫视觉暂留。当一个光源消失后视网膜上的影像不会立刻消失会保留大约几十毫秒。早期电影就是靠这个原理每秒播放24帧静态画面观众看到的是连续动作。动态扫描利用的正是这个机制假设我有4位数码管我先点亮第1位持续1毫秒然后关掉再点亮第2位持续1毫秒关掉依次到第4位然后循环。只要这个循环的总时间小于人眼的感知阈值你就不会觉得它们在轮流亮而是四位同时亮着。那这个阈值是多少通常认为刷新率高于50Hz人眼就基本感觉不到闪烁。50Hz意味着每20毫秒完成一轮所有位的扫描。对于4位数码管每位分到的时间是20ms / 4 5ms。但实际工程中我们不会卡着50Hz这个下限因为LED亮度、驱动能力、单片机中断响应都有波动留点余量更稳妥。我一般会把刷新率设在100Hz到200Hz之间对应每轮10ms到5ms每位1ms到2.5ms以4位为例。这个范围既能保证无闪烁又不会因为扫描太快导致每位点亮时间太短、亮度不足。这里有个容易混淆的概念刷新率和扫描频率。刷新率是指整个显示阵列更新一遍的频率单位是Hz扫描频率有时指单个位的切换频率比如4位扫描刷新率100Hz那单个位被点亮的频率也是100Hz但占空比是25%。我习惯统一用刷新率来讨论避免歧义。2.2 占空比与亮度的关系动态扫描的代价是亮度损失。假设一位数码管在静态驱动时电流20mA亮度是100%。改成4位动态扫描后每位只在1/4的时间里点亮平均电流变成了20mA × 25% 5mA亮度自然降到大约25%实际人眼感知不是线性的但确实会暗很多。为了弥补亮度通常有两种做法一是提高瞬时驱动电流比如静态时20mA动态时给到40mA甚至60mA只要不超过LED的峰值电流和平均电流限制二是降低扫描位数能用3位就不用4位但这是显示需求决定的没法随意改。注意提高瞬时电流不是无限制的。普通LED的连续电流上限一般是20mA峰值电流可以到100mA甚至更高但前提是占空比足够低。比如占空比1/4平均电流 峰值电流 × 0.25。如果峰值电流设60mA平均电流就是15mA还在安全范围内。但如果你把峰值电流拉到100mA平均电流25mA长期运行LED会加速光衰。我一般会把峰值电流控制在50mA到80mA之间配合1/4到1/8的占空比实测下来亮度和寿命比较平衡。2.3 动态扫描的两种常见架构从驱动方式看动态扫描主要分两类位选扫描和段选扫描。位选扫描是固定段选信号轮流选通位选线段选扫描则反过来。实际应用中位选扫描更常见因为段选信号通常需要8根线八段位选线数量等于位数。以4位数码管为例段选8根位选4根总共12根IO。扫描时先输出第1位的段码然后拉低第1位的位选共阴极延时关闭再输出第2位的段码拉低第2位位选延时关闭……循环。点阵屏也是同样的逻辑只是段选和位选变成了行和列。8x8点阵屏有8行8列扫描时逐行点亮每行输出该行的列数据然后选通该行延时换下一行。16x16点阵屏则是16行每行16位列数据通常需要级联两个8x8的驱动芯片或者使用专用驱动IC。3. 硬件选型别让驱动能力拖了后腿3.1 数码管共阴还是共阳共阴极数码管的公共端是LED的阴极接地段选线接阳极给高电平点亮。共阳极则相反公共端接正极段选线给低电平点亮。选哪种主要看你的驱动电路。如果用单片机IO直接驱动共阳极更常见因为单片机拉电流能力通常比灌电流弱而共阳极需要IO输出低电平来点亮段相当于灌电流很多51单片机灌电流能到20mA拉电流只有几百微安。但如果你用专用驱动芯片比如74HC595或者TM1640那就无所谓芯片输出能力强共阴共阳都能推。我个人的习惯是小规模、直接IO驱动用共阳大规模、带驱动芯片用共阴。共阴数码管的段选线接驱动芯片的输出位选线接三极管或者MOS管这样电流从驱动芯片流出经过LED再通过位选管流入地路径清晰计算限流电阻也方便。3.2 位选驱动三极管还是MOS管位选线需要承受所有段选电流的总和。假设8段全亮每段峰值电流60mA那一位的总电流就是480mA。这个电流远超单片机IO的承受能力必须加驱动。常用的方案是NPN三极管如S8050或者N沟道MOS管如2N7002。三极管便宜但基极需要限流电阻而且饱和压降有0.1V到0.3V会损失一点亮度。MOS管压降更小导通电阻几毫欧到几十毫欧但成本稍高。我一般用S8050做位选基极串1k电阻发射极接地集电极接数码管公共端。对于共阴数码管公共端是阴极接三极管集电极三极管导通时把阴极拉低该位就被选通。这里有个细节三极管的基极电阻不能太大否则基极电流不足三极管进入放大区而不是饱和区压降增大发热严重。假设单片机IO输出5V基极电阻1k基极电流约(5V - 0.7V) / 1k 4.3mAS8050的放大倍数按100算集电极能流过430mA足够驱动480mA不够所以基极电阻要减小到680Ω甚至470Ω。我实测用470Ω基极电流约9mA集电极能到900mA驱动4位数码管绰绰有余。3.3 限流电阻怎么算限流电阻是保护LED的关键。假设电源5VLED正向压降2V峰值电流60mA那限流电阻R (5V - 2V - 三极管压降0.2V) / 60mA 2.8V / 0.06A ≈ 46.7Ω。取标准值47Ω。但注意这是峰值电流下的计算平均电流 60mA × 占空比。如果占空比1/4平均电流15mA在安全范围内。实际选电阻时我会先按峰值电流算出一个值然后上电实测亮度再微调。如果太暗适当减小电阻但不要低于33Ω否则峰值电流可能超过100mALED寿命堪忧。如果太亮增大电阻到100Ω甚至220Ω但亮度会明显下降。我常用的范围是47Ω到100Ω具体看LED的规格书。提示不同颜色的LED正向压降不同。红色约1.8V到2.2V绿色约2.0V到2.4V蓝色和白色约3.0V到3.4V。算限流电阻时一定要按实际颜色来否则蓝色LED用红色LED的电阻值电流会小很多亮度不足。4. 代码实现定时器中断是灵魂4.1 为什么不能用delay新手最容易犯的错误是在主循环里用delay_ms(1)来切换位选。这种写法在简单实验里能跑但一旦系统里还有其他任务比如按键扫描、串口通信显示就会卡顿甚至闪烁。因为delay是死等CPU被占住其他事情干不了。正确的做法是用定时器中断来驱动扫描。定时器每隔固定时间比如1ms触发一次中断在中断里切换下一位。这样主循环可以自由处理其他逻辑显示刷新完全由硬件定时器保证稳定可靠。以51单片机为例假设晶振12MHz定时器0工作在模式116位定时要产生1ms中断需要计数1000个机器周期。12MHz晶振下1个机器周期 1μs所以初值 65536 - 1000 64536 0xFC18。初始化代码大概是这样void Timer0_Init() { TMOD 0xF0; TMOD | 0x01; // 定时器0模式1 TH0 0xFC; TL0 0x18; ET0 1; // 允许定时器0中断 EA 1; // 开总中断 TR0 1; // 启动定时器0 }中断服务函数里做两件事关闭当前位切换到下一位输出段码。void Timer0_ISR() interrupt 1 { TH0 0xFC; TL0 0x18; // 关闭所有位选共阴数码管位选低电平有效所以拉高关闭 DIGIT_PORT 0xFF; // 更新位选索引 current_digit; if (current_digit 4) current_digit 0; // 输出段码 SEG_PORT seg_code[current_digit]; // 选通当前位 DIGIT_PORT ~(1 current_digit); }这段代码里SEG_PORT接段选线DIGIT_PORT接位选线。seg_code数组存放0到9的段码。注意关闭位选和选通新位之间要留一点死区时间避免残影。虽然1ms的间隔通常够用但如果发现相邻位有微弱串扰可以在关闭后加几个nop。4.2 段码表的生成段码表是动态扫描的基础。以共阴数码管为例段选线a到g和dp分别对应一个字节的bit0到bit7。数字0需要点亮a、b、c、d、e、f不点亮g和dp所以二进制是00111111即0x3F。数字1点亮b、c即0x06。完整的0到9段码表如下数字段码共阴段码共阳00x3F0xC010x060xF920x5B0xA430x4F0xB040x660x9950x6D0x9260x7D0x8270x070xF880x7F0x8090x6F0x90共阳段码是共阴的按位取反。这个表可以直接抄但建议自己用计算器验证一遍加深印象。我见过有人把a段和dp段搞反结果显示的数字总是多一个点或者少一横查了半天才发现是段码表写错了。4.3 点阵屏的扫描代码点阵屏的扫描逻辑和数码管类似只是数据量更大。以8x8点阵屏为例需要8行8列共16根线。通常用两个74HC595级联一个控制行一个控制列。扫描时先送列数据再送行选通信号然后延时换下一行。void Matrix_Scan() { static uint8_t row 0; // 关闭所有行假设行选通低电平有效 ROW_PORT 0xFF; // 送列数据 COL_PORT display_buffer[row]; // 选通当前行 ROW_PORT ~(1 row); // 下一行 row; if (row 8) row 0; }这段代码放在定时器中断里每1ms调用一次刷新率就是125Hz8行每行1ms一轮8ms。如果觉得亮度不够可以把定时器中断改成500μs刷新率250Hz但每位点亮时间减半亮度会下降。这时候就需要提高峰值电流来补偿。5. 参数计算刷新率、占空比与亮度的平衡5.1 刷新率设多少合适刷新率的下限是50Hz但实际建议100Hz以上。原因有三一是50Hz虽然人眼不觉得闪但用手机摄像头拍屏幕时会出现滚动条纹100Hz以上条纹会细很多二是如果系统里有其他中断比如串口接收可能会短暂延迟扫描中断导致某一帧变长50Hz的余量不够容易看出闪烁三是LED在低刷新率下如果占空比不均匀亮度波动更明显。上限呢理论上越高越好但受限于两个因素一是每位点亮时间不能太短否则亮度太低需要更大的峰值电流二是中断频率太高会占用CPU资源。比如1ms中断一次CPU开销约5%假设中断服务函数10μs如果改成100μs中断一次开销就变成10%对于资源紧张的单片机来说不划算。我一般把刷新率定在100Hz到200Hz对应4位数码管每位1.25ms到2.5ms8行点阵屏每行0.625ms到1.25ms。5.2 占空比与峰值电流的计算实例假设我用4位共阴数码管电源5V红色LED正向压降2V限流电阻47Ω三极管压降0.2V。峰值电流 (5V - 2V - 0.2V) / 47Ω 2.8V / 47Ω ≈ 59.6mA。占空比1/4平均电流 59.6mA / 4 14.9mA。这个平均电流在LED的连续电流上限20mA以内安全。如果我觉得亮度不够想把平均电流提到20mA那峰值电流需要80mA。重新算限流电阻R 2.8V / 80mA 35Ω取标准值33Ω。此时峰值电流 2.8V / 33Ω ≈ 84.8mA平均电流21.2mA略微超标。可以接受但长期运行建议留余量或者把占空比提高到1/3比如用3位扫描平均电流降到28.3mA不对占空比1/3平均电流 84.8mA / 3 28.3mA更超了。所以占空比越高平均电流越大越容易超标。这也是为什么多位数码管通常亮度偏暗因为占空比太低。注意计算平均电流时不要忽略三极管的压降和电源波动。如果电源是电池供电电压从4.2V降到3.7V峰值电流会下降约18%亮度明显变暗。这时候要么用恒流驱动芯片要么在软件里做电压补偿比如检测电池电压动态调整PWM占空比。5.3 点阵屏的亮度均匀性调整点阵屏有个比数码管更棘手的问题亮度不均匀。因为扫描是逐行进行的每一行点亮的时间相同但不同行的LED正向压降可能有差异导致亮度不一致。更常见的是如果列驱动能力不足远离驱动芯片的LED会暗一些。解决这个问题硬件上可以加列驱动芯片如74HC595软件上可以做逐行亮度补偿。比如在显示缓冲区里对每一行的数据乘以一个补偿系数但这样会牺牲灰度等级。我试过一个简单办法在扫描时对亮度偏暗的行多给一点时间。比如正常每行1ms暗行给1.2ms亮行给0.8ms总刷新率不变。但这样会引入非线性显示动态内容时可能有拖影。更稳妥的做法是选用恒流驱动IC比如MBI5024或者TLC5925它们能保证每路LED电流一致亮度均匀性大幅提升。6. 常见问题与排查技巧实录6.1 数码管闪烁、亮度不稳现象显示的数字整体闪烁或者某一位特别暗某一位特别亮。排查思路先看定时器中断是否正常触发。用示波器测位选线正常应该是周期性的脉冲频率等于刷新率。如果脉冲频率不对检查定时器初值。如果脉冲正常但亮度不稳检查位选三极管的基极电阻是否太大导致三极管没有完全饱和。用万用表测三极管集电极和发射极之间的电压饱和时应该小于0.3V如果大于0.5V说明基极电流不足。如果某一位特别暗检查该位的限流电阻是否焊错或者该位的LED老化。可以交换位选线看问题是否跟着走。如果整体闪烁但频率正常可能是中断服务函数执行时间太长导致主循环被拖慢。检查中断里有没有浮点运算或者长延时。我的经验有一次调试4位数码管发现第3位总是比别的暗。查了半天发现是第3位的位选三极管基极电阻用了10k其他位是1k。换掉后亮度一致。这个坑很小但很典型。6.2 点阵屏出现“鬼影”或“拖尾”现象显示动态图案时LED熄灭后还有微弱余光或者相邻行有串扰。原因动态扫描时关闭当前位和选通下一位之间没有死区时间导致上一行的数据短暂地出现在下一行。或者列驱动芯片的响应速度慢数据还没稳定就选通了行。解决在关闭位选后加几个nop或者微秒级延时再选通新位。我一般加2μs到5μs。如果用的是74HC595检查锁存信号LATCH的时序。先送数据再锁存最后选通行。顺序错了就会串扰。降低刷新率给驱动芯片更多稳定时间。但刷新率不能低于50Hz否则闪烁。提示鬼影问题在点阵屏上特别常见尤其是用软件模拟SPI驱动74HC595时。如果时序不对数据移位过程中就会短暂输出错误电平。解决办法是先把数据准备好再一次性锁存输出。6.3 扫描频率与摄像头拍摄的条纹现象用手机拍数码管或点阵屏画面上出现黑色滚动条纹。原因摄像头采样频率和LED刷新率不同步产生拍频效应。这是正常现象不是故障。但如果条纹太粗或者滚动太快说明刷新率偏低。解决把刷新率提高到200Hz以上条纹会变细甚至消失。但提高刷新率会降低亮度需要权衡。如果产品不需要被拍摄可以忽略这个问题。6.4 常见问题速查表现象可能原因排查方法解决方案整体闪烁刷新率低于50Hz测位选脉冲频率提高刷新率到100Hz以上某位暗位选三极管未饱和测三极管Vce减小基极电阻某位不亮位选线断路或LED坏交换位选线补焊或更换LED鬼影无死区时间示波器看位选和段选时序加2-5μs死区亮度不均列驱动能力不足测各列电流换恒流驱动IC摄像头条纹刷新率与摄像头不同步拍其他屏幕对比提高刷新率或忽略7. 进阶技巧PWM调光与灰度控制动态扫描只能控制“亮”和“灭”如果要实现灰度比如点阵屏显示渐变色就需要在扫描的基础上叠加PWM调光。思路是在每一位的点亮时间内再分成多个小时间段通过改变高电平的占空比来调节亮度。比如把1ms分成10份每份100μs如果PWM占空比50%就前5份点亮后5份熄灭。这样每位就有10级灰度。实现方式有两种一是用硬件PWM比如定时器输出比较模式但需要多个通道资源消耗大二是用软件PWM在定时器中断里计数每进一次中断判断是否达到灰度阈值。软件PWM简单但会占用CPU时间。我试过在8x8点阵屏上做16级灰度定时器中断频率提高到10kHzCPU开销约30%对于51单片机有点吃力换成STM32就轻松很多。注意灰度扫描时刷新率和灰度等级是矛盾的。刷新率100Hz8行扫描每行1.25ms。如果灰度16级每级78μs中断频率至少12.8kHz。这个频率下中断服务函数必须非常精简否则CPU全被占用。建议用DMA或者硬件PWM来减轻负担。8. 我踩过的那些坑第一个坑限流电阻共用。早期为了省事8段共用一个限流电阻。结果显示数字“1”时只亮两段电流全集中在两段上亮度刺眼显示“8”时亮八段电流被分流亮度暗淡。后来改成每段独立限流电阻问题解决。虽然多焊了7个电阻但显示效果均匀多了。第二个坑位选三极管选型错误。用S8550PNP驱动共阴数码管结果逻辑全反了该亮的时候灭该灭的时候亮。后来换成S8050NPN一切正常。选三极管时一定要注意极性共阴用NPN共阳用PNP。第三个坑中断里调用延时函数。在定时器中断里用了delay_ms(1)结果中断嵌套程序跑飞。中断服务函数里绝对不要用阻塞延时所有操作都要是非阻塞的。第四个坑电源功率不足。4位数码管全亮时峰值电流接近2A8段 × 60mA × 4位 1.92A用USB供电直接掉压显示乱码。后来换了个5V 3A的电源问题消失。动态扫描的瞬时电流很大电源一定要留足余量。9. 最后再分享几个实用小技巧如果你用的是STM32或者ESP32这类有硬件SPI的单片机驱动74HC595会非常方便。SPI的时钟频率可以到几十MHz送8位数据只要几微秒扫描中断里几乎不占时间。我实测用STM32的SPI驱动16x16点阵屏刷新率做到500Hz亮度均匀无鬼影。另外段码表可以用宏定义来生成减少手写错误。比如#define SEG_A (10) #define SEG_B (11) // ... #define NUM_0 (SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F)这样写虽然啰嗦但可读性好改起来也方便。如果追求简洁可以用查表法但一定要在注释里写清楚每个bit对应哪一段。还有一点调试动态扫描时示波器比万用表好用一百倍。万用表测的是平均电压看不出时序问题。示波器能直接看位选和段选的波形有没有死区、频率对不对、占空比多少一目了然。如果没有示波器可以用逻辑分析仪几十块钱的就能用抓一下时序比盲猜快得多。动态扫描这件事说穿了就是“快”和“稳”。快是指刷新率要够高稳是指时序要准。把这两个字吃透数码管和点阵屏的显示问题基本都能迎刃而解。剩下的就是多动手、多测量、多总结。我到现在还记得第一次让4位数码管稳定显示“1234”时的兴奋感那种“原来如此”的瞬间比任何教程都值钱。
阅读完成 · 觉得有帮助?