ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA驱动数码管重影消除:从现象到状态机根治方案

FPGA驱动数码管重影消除:从现象到状态机根治方案 调FPGA驱动数码管的时候最让人头疼的不是不亮而是“亮得不干净”。明明在代码里把四位数码管的数据分别设成1、2、3、4下载到板子上一眼看过去却是“1234”旁边飘着一层淡淡的残余数字个位那边还像叠影一样晃就像你在电脑屏幕上盯着一张打印重影的照片。做动态扫描的时候重影尤其明显而且刷新率越低越看得出来刷新率调高一点稍微好点但波形一抓还是一堆问题。这篇就把FPGA场景下多位数码管刷新重影的来龙去脉一次讲透从现象分类、硬件根因到软件状态机根治方案都有适合刚接触FPGA开发的学生、用开发板做课设的朋友以及做小项目被重影问题卡住的工程师。1. 重影的本质先分清你看到的到底是哪种“脏显示”重影不是一个单一原因导致的很多教程把它归为“刷新太快/太慢”或者“代码写错了”这太笼统。实际调试中至少有三类看起来相似但根因完全不同的现象先搞清楚你遇到的是哪一种后续才不会白折腾。1.1 三种典型的“脏显示”重影、拖影、幽灵位第一种是真正的重影表现为某个数字旁边出现另一个数字的淡淡轮廓。比如本来只该显示“2”的那一位旁边隐隐约约能看到“3”的影子位置有偏移但不完全重叠。这种情况绝大多数是段码切换和位选切换不同步造成的。第二种是拖影更像一个数字在水平方向拖了一条浅浅的尾巴。这个通常和刷新率偏低、人眼在移动中观察有关也可能是指数管驱动的波形边沿太缓视觉上产生“尾迹”感。第三种是幽灵位这个最诡异——某一位根本没有被选通但它在正确显示数据之外的另一个位置突然亮了一下或者所有位都显示同一个数字。这个问题我见最多的原因是位选信号在关断瞬间没有真正归零加上数码管公共端之间存在泄漏路径导致本不该亮的位“偷看”到了段码总线上的数据。这三种现象在FPGA开发板上都很常见很多人不分青红皂白就改代码结果改了刷新率没用、加了延时也没用就是因为没定位到具体是哪一类。1.2 重影和闪烁的边界一个是“亮错东西”一个是“亮了又灭”顺带说一个容易混淆的点闪烁Flicker和重影Ghosting是两回事。闪烁是扫描频率低于人眼临界融合频率导致人眼能感觉到每一帧的亮灭交替通常通过提高扫描频率或者降低每位占空比来解决。重影则是每一帧内部某一位显示的内容混入了不该有的信息即使刷新率很高重影也依然存在。可以用一个类比来理解动态扫描就像很多人在抢同一个麦克风轮流发言闪烁的问题是轮流速度太慢大家明显感到声音断断续续重影的问题是麦克风还没完全静音下一个人就开始说话两个声音叠在了一起。所以提高麦克风切换速度并不能解决静音不彻底的问题。1.3 动态扫描为什么能骗过人眼视觉暂留的工程化利用既然要根治重影得先理解数码管动态扫描的基本原理不然你看代码都不知道自己在改什么。多位数码管共用一组段码总线a~g、dp共8根线每一位的公共端com由独立的IO口控制。所有位都靠“瞬间轮流点亮”来工作——某一时刻只有一位在亮其他位全灭。只要每一位在单位时间内被点亮足够多次人眼的视觉暂留效应就会把离散的亮灭“脑补”成一幅连续的完整画面。举个例子4位数码管每位点亮时间是总周期1/4只要整个扫描周期小于约1/500秒即每位刷新率大于125Hz正常肉眼看过去就是稳定的连续显示。工程上常见的做法是把扫描周期做到1ms到2ms即整个扫描频率500Hz到1kHz左右这样每一位的刷新率也在250Hz上下视觉上非常稳定。2. 重影的时间轴段码与位选切换的竞争冒险是主因明白了动态扫描的原理就可以进一步看重影发生的具体时间窗口了。这个问题本质上是数字电路里的竞争冒险Race Condition而不是单纯“写错了”的问题。2.1 动态扫描的基本时序模型每个扫描周期都在切三样东西一个标准的动态扫描周期必然涉及三件事关闭当前位的位选选中信号撤掉把段码总线切换到下一位要显示的数据打开下一位的位选选中信号建立顺序上正确的做法是严格串行先关掉旧的位选再把段码换掉最后打开新的位选。三步之间必须有明确的先后关系中间还要留出足够的稳定时间。问题恰恰出在这里——很多初版代码里段码和位选是在同一个时钟沿同时更新的。在状态机里表现为一个状态同时输出“新的段码”和“新的位选”没有任何中间消隐。这样在物理上会有一个极短的窗口纳秒级段码总线已经切换成了下一位的数据但当前位的位选还没来得及关断或者已经关闭但驱动管的电荷没有泄放完当前位就会以新段的段码闪烁一下。这个闪烁太短肉眼看到的就是一层“淡影”。2.2 关键窗口位选选通瞬间段码到底稳不稳我们再放大一点看这个竞争窗口。位选信号要真正作用到数码管上需要经过FPGA的IO输出缓冲、PCB走线、驱动三极管或MOS管、到达公共端。段码信号同样要经过IO、走线、限流电阻到达数码管的段引脚。由于路径不同两者的延迟天然有差异。哪怕你在代码里让位选和段码同时翻转到达数码管的时间也不一样。如果位选先到段码后到那么在这一小段“位选已开、段码未稳”的时间里当前位显示的是段码总线上的旧值就会闪一下不该出现的数字。如果段码先到位选后到就更麻烦——段码总线已经变成新值但旧的位选还没完全关断旧位就“偷看”到了新数据。两种情况最终都表现为重影。所以关键结论就是必须人为在时序上错开这两个信号的切换绝不能依赖“恰好同时”。2.3 硬件层面的“帮凶”三极管存储时间、极间电容、漏电流软件上的竞争冒险是主因但硬件上的几个因素会让问题放大很多倍。如果你改了代码重影还去不掉往这三个方向查第一是驱动三极管的存储时间。数码管位选常用PNP三极管做高边驱动共阳数码管或者用NPN三极管做低边驱动共阴数码管。三极管从饱和导通切换到截止需要释放基区存储电荷存在一个“存储时间”典型值是几百纳秒到几微秒不等。也就是说你代码里已经关断位选了但三极管还在半导通状态赖着不走段码一变它就漏过去了。换成MOS管开关速度会好很多后面硬件选型部分我会展开说。第二是极间电容和走线电容。位选线上如果带了比较长的PCB走线分布电容会让信号边沿变缓关断沿拖得越长和段码切换重叠的时间窗口就越大。这也是为什么同一套代码在面包板上问题严重换到设计良好的PCB上就好了。第三是漏电流。位选关断后驱动路径应该完全截断但如果电路中有上拉/下拉电阻配置不当或者驱动管的反向漏电流比较大公共端会残留一个微弱电压让LED处于一个“若即若离”的半亮状态。这时候段码总线上任意变化都会被这微弱亮度的位数“偷看”到。2.4 刷新率与重影的关系为什么改慢不一定有用一个很常见的误会是“重影是不是刷新太快了我调慢点试试”。刷新率确实会影响重影的可感知程度但不会消除重影的根因。把扫描频率降低比如从1kHz降到200Hz每一位点亮时间变长消隐间隔看起来也变长了有些人觉得重影好像淡了。但这是因为人眼对低频慢变的感知和视觉暂留合成的结果你看到的可能只是“闪烁变严重掩盖了重影”本质上问题还在那里。用逻辑分析仪看段码和位选的竞争窗口一点都没变。反过来如果把刷新率调得特别高比如几十kHz重影会变得非常淡因为每次串扰的持续时间在总周期中占比变小了亮度积分下来就不明显了。但这样会牺牲每一位的导通时间导致数码管整体变暗还得额外加大驱动电流。所以我更推荐的做法是从时序骨架上解决问题而不是靠调刷新率去“掩盖”问题。3. 手把手排查链路从现象到根因的四个定位实验重影问题抽象讲了一大堆接下来上点实操。下面这四个实验是我在调试多个项目时总结出来的定位链路按顺序做一遍基本能把重影的来源锁定到“软件串扰”“硬件泄漏”“个别位异常”这三个方向中的某一个。3.1 实验一冻结位选定位是硬件残影还是软件串扰第一个实验最简单目的是区分问题是出在位选还是段码。把代码改成固定只点亮某一位比如第一位其他位全部关闭。然后让第一位循环显示0到9周期放慢一点比如每秒切换一次数字。这时候观察当第一位显示“3”时其他灭着的位上有没有淡淡的“3”的影子如果其他位也跟着显示“3”的淡影问题基本可以判定为硬件层面的位选泄漏——该关的位没关干净段码被泄漏过去了。如果固定位显示数字时其他位干干净净那硬件泄漏因素小重影更可能是动态扫描切换瞬间的软件时序问题。这个实验成本极低改动量小但信息量很大。我做过的项目里大概有一半重影问题在这个实验之后就基本锁定了方向。3.2 实验二全灭段码测试确认位选泄漏如果把扫描运行起来所有位都正常切换然后持续输出段码全灭的值共阳数码管是全部输出高电平共阴则输出低电平正常状态下整个数码管应该完全熄灭一个点都不亮。这时候如果还能看到某些位有微弱发光的现象说明位选控制根本没有完全截断通路电流通过某种路径漏到了LED上。为了进一步定位可以关掉扫描把所有位选都置为无效保持段码全灭看是否还亮。如果还亮那问题就不是动态扫描造成的而是固定接线或者IO配置的问题比如某个IO口复用冲突、外部上下拉电阻没接对。3.3 实验三逻辑分析仪抓交叉点用波形说话到这一步基本可以确定是动态切换时的时序问题。这时候该上逻辑分析仪或示波器了抓几个关键信号段码总线中的某一位、位选信号、以及一个锁定状态机状态的内部信号可以在代码里把状态寄存器的值引到空闲IO上便于观察。看波形重点观察两处位选信号的下降沿关断沿和段码总线变化点之间的时间差。如果段码变化点在位选关断沿之前说明还没有做到“先关位选再换段码”这基本就是重影的直接证据。位选信号下降沿是否干净利落。如果边沿拖长、呈现缓慢下降说明驱动电路开关能力不足或者走线电容过大这就是硬件因素了。我自己抓波形时还习惯顺便看一下同一根位选线在不同扫描周期内的波形一致性。如果每个周期的关断沿位置都漂移说明状态机内部存在异步逻辑或者时钟分频产生了抖动这种情况引发的重影是不稳定闪动型和固定偏移型不同修的时候也要区分对待。3.4 实验四缩小扫描范围排除单点硬件故障最后一个实验是排查个别位的硬件异常。有时候重影只出现在某一位上比如从“1”切到“2”时有重影但从“3”切到“4”时又正常。这种“挑位”的故障通常不是全局性的时序问题而是特定位的驱动电路异常。把扫描范围缩小到两位比如先只扫第一、第二位重复实验三的观察。如果两位扫描正常再逐步增加位数看是哪一位加入后触发了问题。我有一次就是通过这个办法定位到一块板上第三位的串接电阻虚焊导致该位驱动电流偏小、关断异常最终表现就是“从第二位切到第三位时第二位总有残影”。如果四位都正常那问题更可能是代码里动态扫描逻辑本身存在时序漏洞而不是某一位的硬件问题。这时候直接进入下一章的状态机重写环节。4. 根治方案带消隐周期的扫描状态机设计实验做完了根因也锁定了接下来就是真正动手改代码。这一章给出一个可以直接用的Verilog实现重点讲清楚每个状态存在的原因而不是堆一段代码让你自己猜。4.1 核心思路先关位选再送段码等稳定最后开位选要根治动态扫描重影软件上的黄金法则是在切换位选时强制插入一个消隐周期。消隐周期的具体动作是先把所有位选信号都关闭无论当前是哪一位也不管下一步要切到哪一位。在保持位选全关的状态下把段码总线切换为下一位要显示的数据。等待一小段时间让段码总线上的信号充分稳定位选线上残余电荷也泄放干净。最后打开下一位的位选让这一位真正显示出来。这套流程的关键在于位选全关期间段码的任何变化都不会影响到任何一位数码管等段码稳稳当当建立好了再打开位选出来的就是一个“纯正”的显示内容没有交叉污染。你可能觉得位选全关的那段时间会不会导致数码管变暗。答案是“会但影响很小”。只要消隐时间控制在每个扫描周期占比的几个百分点以内亮度损失几乎看不出来。很多工程师纠结的“消隐写多了变暗、写少了重影”其实是个伪矛盾因为真正需要消隐的只是段码切换的那一瞬间几微秒到几十微秒就够了不用把整个扫描周期的四分之一都拿来消隐。4.2 Verilog实现四拍状态机代码与注释下面给出一段完整的Verilog代码实现带消隐的4位动态扫描。为了让你理解透彻我把代码做了适度简化但核心的状态切换逻辑保留完整。// 带消隐周期的数码管动态扫描控制器 // 核心思想关位选 - 换段码 - 等待稳定 - 开位选 module seg_scan_ctrl #( parameter CLK_FREQ 50_000_000, // 系统时钟频率(Hz) parameter SCAN_FREQ 1_000 // 扫描频率(Hz)整个周期扫完4位 )( input wire clk, input wire rst_n, input wire [7:0] seg_data_0, // 第1位段码 input wire [7:0] seg_data_1, // 第2位段码 input wire [7:0] seg_data_2, // 第3位段码 input wire [7:0] seg_data_3, // 第4位段码 output reg [7:0] seg_out, // 段选输出接数码管 a~g, dp output reg [3:0] pos_out // 位选输出1 表示选中该位 ); // ---- 分频计数器产生扫描节拍 ---- localparam DIV_CNT CLK_FREQ / SCAN_FREQ; localparam DIV_W 16; reg [DIV_W-1:0] div_cnt; wire scan_pulse; assign scan_pulse (div_cnt DIV_CNT - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) div_cnt 16d0; else if (scan_pulse) div_cnt 16d0; else div_cnt div_cnt 1b1; end // ---- 状态机每次scan_pulse前进一个状态 ---- reg [1:0] state; reg [1:0] cur_pos; // 当前正在处理第几位0~3 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state 2b00; seg_out 8b1111_1111; // 假设共阳全灭 pos_out 4b0000; // 位选全关 cur_pos 2b00; end else if (scan_pulse) begin case (state) // 状态0关闭所有位选彻底切断旧的显示通道 2b00: begin pos_out 4b0000; state 2b01; end // 状态1切换段码总线为当前位的数据 2b01: begin case (cur_pos) 2b00: seg_out seg_data_0; 2b01: seg_out seg_data_1; 2b10: seg_out seg_data_2; 2b11: seg_out seg_data_3; endcase state 2b10; end // 状态2等待稳定让段码和残余电荷都有时间归位 2b10: begin state 2b11; end // 状态3打开当前位位选同时准备下一次切换到下一位 2b11: begin pos_out (4b0001 cur_pos); cur_pos cur_pos 2b01; state 2b00; end default: state 2b00; endcase end end endmodule这个状态机的每个状态持续一个scan_pulse周期。假设系统时钟50MHzSCAN_FREQ1000那么一个扫描周期1ms分给4位每位2.5ms消隐状态占了其中3个状态周期约3ms这个消隐占比其实偏高所以如果你直接把这段代码跑在点亮占空比敏感的电路上亮度会偏暗。把它当成演示核心思想的教学代码是没问题的真要工程用的话需要把状态细分到更高的时钟域让消隐和等待稳定只占现有周期的几个时钟周期而不是一个完整分频周期这个我在4.4里继续讲。4.3 代码之外的关键点段码极性、复位状态、时钟分频光有状态机还不够几个配套的地方也要格外注意。段码极性是第一个坑。共阳数码管的段码低电平有效要点亮a段段码位应输出0。共阴数码管高电平有效要点亮a段输出1。我的代码注释里假定的是共阳所以seg_out的复位值是全1表示全灭。如果你用的是共阴要把全灭值改成全0否则上电瞬间数码管可能全亮看起来像第一次上电就中了重影。复位状态是第二个坑。FPGA上电或者复位释放的瞬间IO引脚在配置完成前可能是未定义状态。如果位选默认输出刚好是“选中”数码管就会在上电阶段乱闪一下甚至全亮。解决方式是在复位状态里先把位选全部置为无效段码置为全灭值然后再开始扫描。时钟分频是第三个坑。上面代码用了计数分频产生scan_pulse这在FPGA内部是安全的。但如果你把分频计数器写成异步复位、又在外面对分频后的信号做了边沿检测很容易引入毛刺毛刺一旦作为状态机触发信号状态转移就会乱跳显示内容也跟着乱闪。保持同步设计只用一个时钟域不要到处生成门控时钟这是FPGA开发的基本功。4.4 实测效果重影消失之后的一些新感悟把上面这段代码下载到板子上重影基本就消失了。再配合逻辑分析仪看波形你会发现位选关断沿始终在段码变化沿之前两条线交叉污染的时间窗口被彻底消除。我用这套思路修复过一个比较极端的案例客户做一款仪表8位数码管动态刷新原方案上了74HC245做段码驱动、PNP三极管做位选重影严重到数字边缘全都是毛边。看了波形发现段码在位选关闭前200ns就开始变化属于典型的“先换段码再关位选”时序错误。改用四拍状态机之后毛边完全消失客户说“跟换了一批数码管似的”。这件事给我的感悟是动态扫描的重影问题九成出在时序设计上不是器件不行。修硬件之前先把自己的扫描状态机画成时序图10分钟就能看明白问题在哪。后面讲的所有进阶优化本质上都是在“解码时序不打架”的前提下做得更好。5. 从“修好这一次”到“以后不再踩”架构级预防解决了一次重影不代表以后就不会再遇到。在FPGA项目里只要涉及多位数码管交互比如按键切换、外部数据更新、多个模块同时访问显示缓存重影这个问题会换个马甲再次出现。这章聊几个我从多次踩坑中总结出的预防思路。5.1 双缓冲与数据更新同步把“显示数据”和“扫描节拍”解耦基础扫描状态机有个隐患如果外部模块在扫描进行到一半的时候把某一位的段码数据改掉了状态机读取数据时会读到“改了一半”的值。具体表现为某一位偶尔闪出一个错误的数字或者数字切换的瞬间旧数字和新数字叠在一起这其实也是一种重影。解决方式是加一层双缓冲。外部模块只往“后台缓冲”里写数据扫描状态机只从“前台缓冲”里读数据。在一帧扫描结束的边界比如扫描到最后一位时用一个节拍把后台数据一次性搬运到前台。这样做的好处是把“数据更新”和“数据显示”解耦。更新方想什么时候写就什么时候写扫描方只认前台缓冲区两者不互相干扰。代价是多花一组寄存器FPGA里最不缺的就是寄存器这个成本几乎可以忽略。5.2 硬件选型的避坑点驱动管开关时间、位选线下拉、走线布局软件架构做得再漂亮硬件不给力也会拖后腿。这部分总结几个我选型和画板时特别注意的点。驱动管优先选MOS管而不是三极管。三极管的存储时间是导致位选关断延迟的主要元凶MOS管是多数载流子器件没有存储效应开关速度通常快一个数量级以上。如果项目对成本不敏感用AO3401、AO3402这类小信号MOS管驱动位选重影问题会好处理很多。位选线上加下拉电阻给关断路径提供低阻泄放通道。这样做能显著缩短位选从高到低的电压回落时间。下拉电阻取值通常在10kΩ到47kΩ之间太小会增加静态功耗太大起不到泄放作用。我习惯先按10kΩ起调再看波形决定要不要加大。PCB走线方面位选线和段码线尽量不要长距离平行敷设避免互相耦合。如果空间实在有限可以在两条线之间加一条地线隔离。段码总线属于数字信号切换速率快位选线相对敏感两者紧贴着走容易引起串扰。5.3 动态扫描亮度的平衡刷新率、占空比与峰值电流动态扫描在工程上还有一个永远绕不开的平衡亮度够不够、重影明不明显、寿命稳不稳。这三者往往互相拉扯。刷新率太低肯定闪刷新率太高每位导通时间太短亮度不够只能加大电流。加电流又受到数码管峰值电流规格的限制。查数码管数据手册的时候留意“Peak Forward Current”这个参数它通常比持续工作电流高好几倍比如持续20mA的管子峰值可以到100mA。动态扫描就是靠“高电流低占空比”来等效出“平均电流适中亮度充足”的效果。关键是不能让平均电流超限。假设扫描周期为1ms每一位占空比25%那么每一位的平均电流 峰值电流 × 25%。如果让峰值电流跑到80mA平均电流就是20mA刚好处在额定的持续电流范围内。如果你为了省事降低了扫描频率占空比不变但每位点亮时间变长LED结温上升长时间工作会加速光衰甚至烧管。所以每次调整刷新率都要回头算一遍平均电流不能只看亮度。5.4 上电与复位瞬间的“幻影”防止板子一上电就显示错误最后一个预防点往往被忽略上电瞬间的幻影显示。FPGA芯片在配置完成前IO引脚处于高阻或未定义状态这时候如果外部电路把位选“拉”到了选的逻辑电平数码管就可能在上电一瞬间闪出奇怪的数字或者整排数码管突然全亮一下。解决办法分软件和硬件两层。软件上复位状态里强制位选无效、段码全灭这个前面提过。硬件上位选控制路径上加下拉电阻让未配置状态下位选电平默认落在无效区域也是一种很有效的兜底。这块处理的难点在于“看不见”——问题只在通电瞬间发生如果你不盯着数码管看根本察觉不到但用户如果每次上电都看到一次“闪亮”体验会很差。我在一个新项目的调试中还遇到过一种隐蔽的变体FPGA配置完成后复位释放的一瞬间状态机还没跑到第一个有效的状态位选输出就因组合逻辑的干扰跳了一下导致第2位闪了半秒才恢复正常。排查时发现是复位释放和时钟起振不同步状态机的复位撤销沿落在了时钟边沿上产生了亚稳态。解决办法是给复位信号做一个两级同步器让它和时钟域完美对齐这一下就把“每次上电都有杂音”的怪问题治好了。现在我再做项目写扫描代码之前会先画一张包括位选、段码、数据更新边界的时序草图把每个状态切换的时间都标清楚。重影这类问题多数是“想清楚”就能避免的不是“调出来”的。希望这篇能帮你少走几轮弯路。
返回列表