FPGA 入门 22:数码管与 LED 阵列——动态扫描与时分复用
更新时间:2026-09-12。本文是
fpga/入门层第 24 篇。扫描节拍用 计数器与分频 的 tick,显示数据来自状态/计数见 状态机入门。
本文要回答的问题
- 多位数码管物理上明明只有一位在亮,为什么人眼觉得四位同时常亮?视觉暂留和刷新率到底怎么换算?
- 段选、位选、共阴、共阳、段码表、位序 dp/g/f/e/d/c/b/a 这些概念如何对应到实际电路?
- 动态扫描的「换段」和「换位」为什么要消隐,鬼影(拖影/串字)在电路时序上是怎么产生的?
- 一个可综合的多位扫描模块应该由哪几部分构成(扫描计数、当前位取数、段码译码、位选译码、消隐时序)?
- 8×8 LED 点阵和数码管扫描为什么是同一个原理?行选、列数据、595 串转并、恒流驱动各自解决什么?
- 亮度、刷新率、占空比、限流电阻和电流之间怎么权衡?位数变多(8 位、16 位)会带来什么问题?
- 字模、小数点、闪烁、滚动显示等常见效果在扫描框架上如何扩展?仿真怎么验证扫描时序?
一、七段数码管结构
aaa
f b
f b
ggg
e c
e c
ddd dp- 段选:a~g + dp 共 8 根,多位并联共用;
- 位选:每位一根公共端,决定当前亮哪位;
- 共阴:段选高有效;共阳:段选低有效(段码取反)。
共阴段码(位序 dp g f e d c b a):0=8'h3F, 1=8'h06, 2=8'h5B, 3=8'h4F, 4=8'h66, 5=8'h6D, 6=8'h7D, 7=8'h07, 8=8'h7F, 9=8'h6F, A=8'h77, F=8'h71。
二、动态扫描 4 位数码管
module seg7_scan(
input wire clk, rst_n,
input wire [15:0] din, // 4 位 BCD/十六进制数
output reg [7:0] seg, // 段选 a..dp
output reg [3:0] an // 位选(低有效示例)
);
reg [15:0] tick_cnt; reg [1:0] scan; // scan 轮询 4 位
reg [3:0] hex;
wire tick = (tick_cnt == 16'd49999); // 50MHz->1ms
always @(posedge clk) if(tick_cnt==16'd49999) tick_cnt<=0; else tick_cnt<=tick_cnt+1;
always @(posedge clk or negedge rst_n)
if(!rst_n) scan<=0; else if(tick) scan<=scan+1;
always @(*) case(scan) // 取当前位数字
0: hex=din[3:0]; 1: hex=din[7:4];
2: hex=din[11:8]; 3: hex=din[15:12];
endcase
reg [7:0] seg_c; // 共阴段码
always @(*) case(hex)
4'h0:seg_c=8'h3F;4'h1:seg_c=8'h06;4'h2:seg_c=8'h5B;4'h3:seg_c=8'h4F;
4'h4:seg_c=8'h66;4'h5:seg_c=8'h6D;4'h6:seg_c=8'h7D;4'h7:seg_c=8'h07;
4'h8:seg_c=8'h7F;4'h9:seg_c=8'h6F;4'hA:seg_c=8'h77;4'hB:seg_c=8'h7C;
4'hC:seg_c=8'h39;4'hD:seg_c=8'h5E;4'hE:seg_c=8'h79;default:seg_c=8'h71;
endcase
always @(posedge clk) begin
an <= ~(4'b1 << scan); // 先只选一位(低有效)
seg <= ~seg_c; // 共阳示例:段码取反;共阴直接 seg_c
end
endmodule防鬼影:切换时可先
seg<=8'hFF(全灭)一拍再送新段码(消隐→换段→点亮)。
三、扫描参数
| 参数 | 典型值 | 说明 |
|---|---|---|
| 每位驻留 | 1~2 ms | 太长闪、太短暗 |
| 整屏刷新 | ≥60 Hz(4 位即每位短于 4ms 轮完) | 视觉暂留约 16ms |
| 段选有效 | 共阴高 / 共阳低 | 看原理图 |
| LED 阵列 | 行选 1 行+列送图案,逐行扫 | 常用 595/恒流驱动 |
四、刷新频率、占空比与亮度的定量关系
理解动态扫描要先算清三组数字,它们决定了「不闪」和「够亮」两个目标如何同时满足。
- 刷新率:假设每位驻留 1ms,4 位轮一遍是 4ms,整屏刷新 250Hz;8 位就是 125Hz。人眼对周期性闪烁的融合阈值约 50~60Hz,所以只要整屏周期短于约 16ms 就看不到明显闪烁,工程上常留数倍裕量取 100~250Hz。
- 占空比:任一时刻只有一位亮,每位实际点亮时间占整屏周期的比例约为 1 除以位数。4 位时约 25%,8 位时约 12.5%,16 位时只有约 6%。位数越多,每位平均亮度越低,这就是多位扫描要加大瞬时电流或提高驱动能力的原因。
- 瞬时电流:为了在低占空比下仍有足够平均亮度,扫描时允许 LED 通过比静态点亮更大的峰值电流(在器件占空比额定值内),平均电流等于峰值电流乘占空比。必须查数码管/点阵手册的峰值电流与允许占空比,不能只看静态电流额定值。
由此得到几个工程结论:驻留时间不是越短越好——太短每位占空比更低、峰值电流要求更高、扫描计数器更快;也不是越长越好——太长整屏刷新掉到 60Hz 以下就会闪。常见折衷是每位 0.5~2ms,并随位数增加适当缩短驻留时间以维持刷新率。用 计数器时钟使能 的 tick 驱动扫描计数器,既保证走在同一个时钟域,又能精确控制每位驻留时长。
五、扫描模块的分层结构
把前面的单文件示例拆开看,一个规范的多位扫描显示系统通常是三层,职责清晰便于复用和调试:
- 时间基底层:毫秒或半毫秒 tick 发生器,决定每位驻留时间(纯计数器,见 计数器篇)。
- 扫描控制层:一个「位指针」计数器在 tick 上从 0 循环到位数减 1;它同时做三件事——按指针从显示数据总线选出当前位的 4 位 nibble、产生当前位的位选 one-hot、给出消隐时序。
- 译码与输出层:纯组合的段码查找表(nibble 到 8 位段码),按共阴/共阳决定是否取反;位选经三极管/驱动芯片驱动公共端;需要时在切换点插入一拍全灭消隐。
显示数据(例如要显示的数字、温度、计时值)由完全独立的业务逻辑计算后放到一个寄存器数组或总线上,扫描层只负责「不停刷新当前内容」,二者通过数据寄存器解耦。这样更新显示值时不会打断扫描,业务逻辑也完全不用关心扫描细节。这种「数据与显示分离」的结构在 VGA、LCD、LED 大屏中反复出现,是显示控制器的通用范式。
六、鬼影是怎么产生的:换段与换位的时序竞争
鬼影(相邻位隐约串出别的字形)是动态扫描最典型的问题,根因是段选和位选两组信号不可能在物理上同时改变。考虑一个切换瞬间:旧位仍被位选接通,而段码寄存器已经更新成新位的字形,那么在这短暂重叠期里,旧位就会用新段码点亮一次;每个刷新周期都重复,肉眼就看到稳定的串字。即使 RTL 里在同一时钟沿更新两者,板上段选、位选经过的驱动芯片、走线延迟不同,到达 LED 的时刻仍有先后。
解决方法是在切换序列里显式插入消隐(blanking):
- 先关显示——段码送全灭,或位选全部关闭;
- 等一拍(让关断真正生效,也覆盖驱动延迟);
- 再设置新位的段码;
- 最后才接通新位的位选。
也就是「灭→换→亮」,确保任何时刻被点亮的那个位,其段码一定属于它自己。消隐只占每个驻留周期里极小的比例,对亮度影响可忽略,却彻底消除串字。实现上多用一个小的扫描状态机(关断/建立/点亮三态)或让输出寄存器比数据晚一拍使能。另一种鬼影来源是段码位序排错或某段常通,那种是字形错误而非时序串字,要靠核对段码表区分。
七、段码表推导:别只会抄表
能自己推出段码表,换任何开发板都不怕。七段管的段命名为 a 到 g 加小数点 dp,给它们约定一个字节位序(本文用最低位 a、依次 b…f、最高位 dp),数字 0~9、字母 A~F 各点亮哪些段,逐位置 1 即得段码:
- 数字 0 亮 a b c d e f、灭 g,段码为二进制 00111111,即十六进制 3F;
- 数字 1 只亮 b c,为 06;数字 8 全亮为 7F;
- 字母 A 亮 a b c e f g 为 77,F 亮 a e f g 为 71。
共阳极公共端接高电平,段脚给低才形成电流,所以同样字形的段码是共阴值的按位取反:3F 变 C0、7F 变 80。上电前务必从原理图确认三件事:公共端是接 GND(共阴)还是 VCC(共阳)、段脚到 FPGA 之间有没有反相驱动或三极管、小数点 dp 在字节的哪一位。任何一个不符,现象都是「该亮不亮、不该亮的亮」或字形错乱。把段码表放进一个 case 组合函数或 ROM,比到处写常量好维护;带 16 个完整字形(0~F)和几个特殊符号(减号、上下标、全灭)的表通常一次写好长期复用。
八、多位、小数点与显示效果扩展
在稳定的扫描框架上,常见显示需求都只是小改动:
- 小数点 dp:每位再存一个 dp 使能位,扫描到该位时把它拼进段码最高位,用于显示电压、频率等小数;不要把小数点固定写死在字形里。
- 闪烁:用一个约 2~4Hz 的使能信号,在需要闪烁的位上周期性强制段码全灭(只关显示不改数据),常用于设置菜单的当前编辑位。
- 亮度调节:整体加一个 PWM 式的亮度使能,在每个驻留窗口里只点亮一部分子拍,或缩短有效驻留时间;本质是再做一级占空比控制(PWM 原理见 计数器篇)。
- 滚动/移位显示:位数不够显示长内容时,维护一个比物理位数长的数据缓冲,按时间把「窗口」在缓冲上移动,扫描层不变。
- 数字与 BCD:显示十进制前先把二进制转成 BCD(常用移位加三算法),否则 nibble 会出现 A~F 的异常字形。
九、8×8 LED 点阵:同一套时分复用
把数码管的「一位」抽象成「一行」,点阵扫描几乎是平移过来的:8 行公共端相当于位选(行选),8 根列线相当于段选(送这一行哪几列亮的 8 位图案)。某一时刻只选通一行、在列上送该行的 8 位点阵数据,驻留约 1ms 后切下一行;8 行轮完约 8ms、刷新约 125Hz,人眼就看到一幅稳定的 8×8 图像。多块点阵拼接时行数/列数增加,原理不变,只是占空比更低、更需要驱动芯片。
点阵相对数码管有两个突出的工程问题。其一是电流:一行 8 个 LED 可能同时亮,还要按低占空比给足峰值电流,FPGA 的 IO 无法直接灌入,通常用三极管/MOS 管驱动行、用 74HC595 这类串入并出移位寄存器扩列(SPI 风格串行送数、锁存输出),专业屏用恒流驱动芯片保证各点亮度一致。其二是数据组织:把图像/字模按「每行一个字节」存在 ROM/RAM 里,扫描时按行指针读出送列,做动画就是按帧更换这块显存——字模表、帧缓冲可以放进 BRAM。消隐、刷新率、占空比的计算与数码管完全一致,理解了一个就理解了一类。
十、驱动电路与限流:为什么不能直接灌
FPGA 的 IO 引脚能流出或吸入的电流有限(一般毫安量级,且受整个 Bank 总电流约束),而数码管位选公共端要同时给 8 段供电、点阵行选要给一整行供电,直接连接会超出额定值,轻则偏暗、发热,重则损伤引脚。常见驱动方式:
- 位选/行选用三极管或 MOS 管:共阴管公共端接 NPN/N-MOS 到地,共阳管用 PNP/P-MOS 上拉到电源,FPGA 只驱动三极管基极/栅极,主电流由电源经开关管提供,注意共阴、共阳所需的有效电平可能因此再反相一次。
- 段选/列用 74HC595 等移位寄存器:串行时钟、数据、锁存三根线即可扩展出 8 位、16 位并行输出,片上还带一定驱动能力;多片级联能驱动更长的段/列,节省大量 IO。
- 恒流驱动芯片:LED 大屏和高密度点阵用恒流器件,保证各点电流一致、亮度均匀,并带输出使能便于整体调光和消隐。
- 限流电阻:每段或每列串电阻把单 LED 电流限制在额定范围,阻值按「电源电压减去 LED 导通压降除以目标电流」估算;扫描驱动要用峰值电流与占空比一起核算平均功耗。
设计前一定对照开发板原理图,很多板卡已经焊好三极管、位选驱动和限流电阻,你只需给出正确有效电平;自己画硬件时这些元件不能省。
十一、仿真怎么验证扫描时序
扫描逻辑时间尺度在毫秒级,仿真不必真跑几十毫秒,把 tick 分频常数参数化、用很小的值(例如让 tick 每 8 拍来一次)即可快速观察整个扫描周期。验证要点:
- 位指针循环:复位后 scan 是否从 0 依次走到位数减 1 再回卷,有无死状态;
- 取数正确:每个位指针下,送出的 nibble/段码是否对应显示缓冲中的那一位;
- 位选 one-hot:任一时刻 an 是否恰好只有一位有效,多位同时有效就是译码错;
- 消隐时序:切换边界是否存在「全灭」间隔,段码更新是否发生在关断期间;
- 占空比与周期:用游标量某位两次被选通的间隔是否等于设计的整屏周期,某位段码有效的驻留时长是否正确。
仿真方法和波形读法见 Testbench 与 看波形。上板时如果看到闪烁,先用 ILA 量整屏周期确认刷新率;亮度不足先查占空比和驱动电流;个别位不亮查位选,某一段所有位都不亮查段选,能快速把故障定位到「扫描时序、译码、硬件驱动」三类中的哪一类(ILA 见 ILA 第一次用)。
十二、可复用设计建议与检查清单
把扫描显示写成一个可配置通用件:位数、段码有效电平、位选有效电平、驻留时钟数都用 parameter,段码表用函数或 ROM,业务侧只暴露一个「显示数据 + 小数点 + 闪烁使能」接口。这样换 4 位、8 位板,换共阴、共阳硬件,都只改参数和接线。整个模块单时钟、扫描状态寄存器显式复位、段码译码保持纯组合并设默认值以防 latch(锁存器问题见 意外锁存器)。
交付前按清单自查:
- 整屏刷新率是否高于约 60Hz、工程上是否留到 100~250Hz;
- 段码位序、共阴/共阳、驱动反相是否与原理图逐一核对;
- 切换是否做了「灭→换→亮」消隐,鬼影是否在板上消失;
- 位选、段选电流是否在 FPGA 和驱动器件额定值内,限流电阻是否齐备;
- 数据更新与扫描是否解耦,改显示值时画面是否稳定无毛刺;
- tick、扫描指针、位宽是否按实际时钟频率计算(方法见 计数器篇);
- 仿真是否覆盖回卷、消隐边界、多位取数,上板是否用仪器确认刷新率与亮度。
数码管和点阵是学习「时间换资源」的第一课:用快速轮询和人眼暂留,十几根 IO 加少量驱动就能控制几十个发光单元。这套时分复用思想往后会在 LED 大屏、VGA 行场扫描、键盘矩阵乃至高速复用上反复出现,把扫描时序、消隐、占空比和电流这几件事一次弄明白,后面所有扫描类外设都是同一模型的扩展。
十三、二进制到十进制:BCD 转换细节
数码管每位显示的是 0~9 的十进制数字,而寄存器里存的是二进制,直接按 4 位一组取 nibble 会在数值大于 9 时显示出 A~F 的怪字符。所以多位十进制显示前必须做二进制到 BCD 的转换。入门最常用的是「移位加三(Double Dabble)」算法:准备一个宽度为「4 乘十进制位数」的 BCD 寄存器与二进制输入拼接,从高位到低位逐拍左移;每移一位之前,检查 BCD 中每个 nibble,若当前值大于等于 5 就先加 3 再移,否则直接移;所有位移完后,各 nibble 里就是独立的十进制数字。加 3 的原因是左移等于乘 2,而十进制逢十进位,先加 3 再左移恰好实现「大于等于 5 时移位后正确产生进位」的调整。
工程上有三种落地方式:纯函数式组合转换(位数少时简单但组合路径较长)、多周期逐拍移位(一个小 FSM 加计数器,十几个时钟完成,时序好、推荐)、或直接调 BCD IP。转换结果按「千、百、十、个」放进显示缓冲,每位再各带一个小数点和消隐使能(例如前导零不显示)。负数要单独处理符号位、十六进制调试显示则直接取 nibble 不需要 BCD——理解「每一位 nibble 的含义由你决定」,就不会在计数到 10 时看到奇怪的 A 而困惑。温度、电压、频率等仪表显示还要先把定点数按 定点数基础 定标,再走 BCD 和扫描显示。
十四、从数码管到 VGA:扫描模型的延伸
本节的动态扫描模型可以直接迁移到 VGA 显示器,便于体会知识的复用关系。数码管是「逐位轮询、每一位驻留一毫秒」,VGA 是「逐行、逐帧扫描」:一个像素时钟驱动行计数器,行同步期间送出该行各像素颜色,一行结束产生行同步脉冲并回到行首;行计数到总行数产生场同步并回到第一帧。消隐在这里对应 VGA 的行/场消隐区间——回扫期间关闭像素输出;驻留时间对应每个像素只有一个像素时钟的有效窗口;刷新率对应 60Hz 或更高的帧率;显存对应按行列组织的帧缓冲,只不过 VGA 数据量大、要放在 BRAM 甚至外部 DDR 里。区别主要在速度和规模:数码管是毫秒级、人眼暂留,VGA 是纳秒级、靠显示器和显示器控制器严格约定的时序;但「扫描指针 + 当前位置取数 + 同步与消隐」的骨架完全一致。
练习路径建议这样安排:先做 4 位静态字形(固定显示 1234,验证段码与极性);再接扫描计数器实现动态 4 位;加消隐消除鬼影;接上按键和 消抖模块 实现按一下加一;再做 BCD 让计数正确显示十进制;最后做 8×8 点阵字符或一个简单 VGA 彩条。每一步都单独综合、仿真、上板,确认后再叠加,问题永远只可能出在最新加的那一层。走完这条线,你会同时掌握计数器、时钟使能、扫描、译码、存储取数和显示时序,这正是后续接口与图形类项目的共同地基。最后记住一个排障口诀:多位一起异常先查扫描时序(tick、位指针、消隐),单个位异常查位选和该位驱动,单个段在所有位都异常查段选和段码位序,整体偏暗查占空比与驱动电流,肉眼闪烁查刷新率。把故障按「时间维度、空间维度、字形维度、亮度维度」分类,绝大多数显示问题都能在不看复杂波形的情况下快速定位。调试时还要坚持单一变量原则:一次只改极性、驻留时长、段码位序或扫描频率中的一项,改完立刻上板对比,避免多因素同时变化后无法判断是哪一处修复起了作用。把每次现象、猜测、改动和结果记入笔记,三轮之内通常就能锁定根因。
十五、人眼的局限与扫描设计的工程裕量
动态扫描之所以能用"快速轮流点亮"骗过人眼,靠的是人眼的视觉暂留——光源熄灭后,亮度感觉还会残留一小段时间;只要熄灭间隔短到一定程度,大脑就把断续的闪烁融合成连续的稳定光。但要把这个物理现象变成可靠的产品参数,不能只凭"看起来不闪",因为人眼对闪烁的敏感度随视野、环境光、年龄和注意力变化很大:余光比直视敏感、暗环境比亮环境敏感,手机摄像头、相机和手机慢动作拍摄还会把肉眼看不出的闪烁放大成滚动条纹,这也是很多数码管"人看着挺好、一拍视频全是水波纹"的原因。因此设计扫描节拍时要留足裕量,工程上通常让每位数码管的整屏刷新频率做到 100Hz 以上(每位被点亮的重复频率,而非单管的 50Hz 临界值),户外或要上镜头的显示甚至更高,用"明显高于融合阈值"来覆盖个体和拍摄差异。
占空比则关系到亮度和寿命:N 位扫描时,每位平均只有约 1/N 的时间通电,为达到足够亮度,常把段电流设得比静态驱动大(瞬时更亮,靠平均电流约束器件和限流电阻),但平均电流、峰值电流都必须在数码管和 FPGA IO 的允许范围内,不能为了亮无限加大。还要注意"鬼影"——切换位选时上一位的段数据若还没更新,会在下一位短暂显示错误段码,规范做法是先关显示(或先切段到无效电平)、再换段码、最后开新位选,保证切换期间没有错误的亮段组合。理解了刷新率(决定闪不闪)、占空比与限流(决定亮不亮、安不安全)、消隐(决定干不干净)这三个量,你就掌握了所有时分复用显示——从数码管、点阵到 VGA——共用的调参方法论:用远高于感知/采样阈值的刷新保证稳定,用占空比和电流约束管理亮度,用严格的切换顺序消除串扰。
十六、从数码管扫描到存储器/总线的时分复用
动态扫描的思想并不只属于显示领域,它其实是数字系统里一个普适范式——用一套共享资源在时间上轮流服务多个对象——的最直观实例。数码管是"一套段驱动 + 一只限流电阻网络,配合多位选择开关,轮流点亮多个位置";把这句话里的词换掉,就得到大量更复杂的结构。存储器里,同一组地址/数据总线和一个读写端口,靠行地址、列地址分拍送入(DRAM 的地址复用),让海量存储单元共享少量引脚;显示里,VGA/HDMI 用一组 RGB 通道和一套行场同步,靠逐像素、逐行扫描让整块屏幕的上百万像素共享三条颜色通道,这与数码管扫描在数学模型上几乎同构——都是计数器生成坐标、译码出当前对象、在恰当时刻送出对应数据。
通信里,并串转换(SPI、UART、收发器)让一根物理线路在不同比特时隙传不同数据位;计算里,分时复用一个昂贵的乘法器,让多路数据排队在不同周期使用它,是面积换时间、时间换面积的经典操作。识别出这个共同模式有两个实际价值。第一,参数可类比:所有时分复用系统都有"对象数量 N、单次服务时间、整轮刷新/服务频率、每个对象占空比"这几个量,学数码管时练会的刷新率与占空比计算,可直接迁移去理解 VGA 时序、DRAM 刷新和总线带宽。第二,约束可类比:都要处理"切换瞬间别串扰"(数码管消隐对应总线的方向切换与建立保持)、"刷新不够快会失效"(人眼闪烁对应 DRAM 丢数据、显示器闪屏)、"供给跟不上就降质"。建议在学完数码管后回看本文的计数器和译码器,再去对照 VGA 行场时序图,你会发现后者只是把"第几位"换成了"第几像素/第几行"、把"段码表"换成了"像素颜色"。看穿这层同构,就能用一个心智模型串起显示、存储、通信和资源共享四大主题,而不必把每个外设当成互不相干的新知识。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| 共阴/共阳搞反 | 显示全反/不亮 | 看原理图,段码取反 |
| 切换不消隐 | 鬼影/拖影 | 先全灭再换段 |
| 刷新低于 60Hz | 肉眼可见闪烁 | 每位 1~2ms |
| 位选/段选电平错 | 多位串显 | 核对有效电平 |
| 段码位序排错 | 字形错乱 | 固定 dp g…b a 顺序 |
| 阵列直接灌大电流 | 烧 IO/偏暗 | 加驱动芯片/限流电阻 |
相关与延伸
节拍:计数器与分频;数据来源:状态机入门、按键消抖(按键调数字、数码管显示)。 深入:进阶 低速接口 UART/SPI/I2C(595 扩 IO 用 SPI)、BRAM 存储(字模/图案表放 ROM)。
一句话总结
多位七段数码管和 8×8 LED 阵列都靠动态扫描(时分复用)驱动:段选/列线并联共用、位选/行选逐个选通,同一时刻只亮一个“位/行”,靠人眼视觉暂留(整屏刷新 ≥60Hz、每位驻留 1~2ms)看起来全部常亮——用十几根 IO 就能驱动几十个灯。七段管要先按原理图分清共阴(段高有效)/共阳(段低有效、段码取反),按 dp g f e d c b a 位序建段码表;切换时“先消隐(全灭)→换段码→点亮该位”防止相邻位串字的鬼影。LED 阵列一一对应:行选≈位选、列图案≈段码,逐行送图案即出整幅图,大电流需 595/恒流驱动。刷新太低闪烁、太高偏暗,60~250Hz 是亮度与无闪的折中。
上一篇:24 按键消抖(同步/稳定计时/边沿检测) | 下一篇:26 UART 回环(帧/波特率/收发) | FPGA 系列目录