这一篇在干嘛?
上一篇的静态显示有个致命局限:所有位只能显示同一个字符。这一篇用动态扫描破解它——8 位数码管其实每一瞬间只有一位在亮,只是切换速度快到人眼无法分辨,看起来就是 8 位同时显示各自的数字。你会学到扫描计数器、位选轮流通电、段码跟随切换这套经典的「轮播」结构。
实验目标与扫描原理
实验任务:
- 学习硬件扫描显示电路的设计;
- 掌握利用 FPGA 设计 8 位七段数码管扫描显示驱动电路的方法。
段码与位码
LED 数码管有段码和位码之分:
- 段码:让 LED 显示出某个字符(如「8.」)的那 8 位数据。一般要通过译码电路,将输入的 4 位二进制数转换为与 LED 显示对应的段码(上一篇的七段译码器干的就是这个活);
- 位码:LED 的显示使能端。对于共阳极的 LED 而言,低电平使能——位码中某一位为 0,对应的那位数码管才接通显示。
为什么轮流点亮却能「同时」显示
要让 8 个数码管各自显示不同数据,做法是:不停地循环扫描每一个数码管,在使能每一个数码管的同时,输入它所需显示数据对应的 8 位段码。
虽然 8 个数码管是依次显示的,但只要扫描频率足够高——大于 50Hz(即每位点亮的时间短于人眼视觉暂留的分辨极限)——受视觉分辨率的影响,看到的现象就是 8 个 LED 同时工作。这是利用人眼视觉暂留的经典设计,和电影每秒 24 帧看起来是连续画面是同一个道理。
板上电路回顾
HX4S20 板采用 8 位共阳数码管(数码管原理及驱动方式可参照实验 4,或查阅「使用手册」原理图)。8 位数码扫描显示电路中,每个数码管的 8 个段 h、g、f、e、d、c、b、a(h 是小数点)都分别连在一起,8 个数码管分别由 8 个选通信号 k1~k8 来选择。被选通的数码管显示数据,其余关闭。例如某一时刻 k3 为低电平、其余选通信号为高电平,这时仅 k3 对应的数码管显示来自段信号端的数据,其他 7 个数码管呈现关闭状态。
所以,要在 8 个数码管上显示想要的数据,就必须让 8 个选通信号 k1~k8 分别被单独选通,同时在段信号输入口加上该对应数码管要显示的数据——随着选通信号的扫变,就能实现扫描显示的目的。
8 位数码管实物如下图:

1.1 8 位数码实物图
扫描频率的两端约束
扫描太慢(低于 50Hz)会看到明显的闪烁;扫描太快则每位点亮时间过短、亮度下降。本实验用 16 位扫描计数器的最高 3 位
cnt_scan[15:13]分频选位:50MHz 时钟下计数器每 8×65536 ≈ 0.5ms 换一位,8 位轮完一轮约 1ms 多,整轮刷新率近千 Hz,远高于 50Hz,显示稳定无闪烁。
Verilog 实现
实现的动态数码管显示模块如下,可在第 3 个 always 里修改需要显示的数字(原文 OCR 排版有破碎,已按原文信号定义、段码表与逻辑补全还原):
module sm_scan(
clk,
rst,
dataout,
en
);
input clk, rst;
output[6:0] dataout;
output[7:0] en; // COM 使能输出
reg[6:0] dataout; // 各段数据输出
reg[7:0] en;
reg[15:0] cnt_scan; // 扫描频率计数器
reg[3:0] dataout_buf;
always @(posedge clk or negedge rst)
begin
if (!rst)
begin // 低电平复位
cnt_scan <= 0;
end
else
begin
cnt_scan <= cnt_scan + 1'b1;
end
end
always @(cnt_scan) // 段码扫描频率
begin
case(cnt_scan[15:13])
3'b000 : en = 8'b1111_1110;
3'b001 : en = 8'b1111_1101;
3'b010 : en = 8'b1111_1011;
3'b011 : en = 8'b1111_0111;
3'b100 : en = 8'b1110_1111;
3'b101 : en = 8'b1101_1111;
3'b110 : en = 8'b1011_1111;
3'b111 : en = 8'b0111_1111;
default : en = 8'b1111_1110;
endcase
end
always@(en) //对应 COM 信号给出各段数据,段码
begin
case(en)
8'b1111_1110: dataout_buf=1;//输入将要显示的数字
8'b1111_1101: dataout_buf=2;
8'b1111_1011: dataout_buf=3;
8'b1111_0111: dataout_buf=4;
8'b1110_1111: dataout_buf=5;
8'b1101_1111: dataout_buf=6;
8'b1011_1111: dataout_buf=7;
8'b0111_1111: dataout_buf=8;
default: dataout_buf=0;
endcase
end
always@(dataout_buf)
begin
case(dataout_buf) //将要显示的数字译成段码
4'b0000 : dataout <= 7'b1000_000;//7'b0111_111;
4'b0001 : dataout <= 7'b1111_001;//7'b0000_110;
4'b0010 : dataout <= 7'b0100_100;//7'b1011_011;
4'b0011 : dataout <= 7'b0110_000;//7'b1001_111;
4'b0100 : dataout <= 7'b0011_001;//7'b1100_110;
4'b0101 : dataout <= 7'b0010_010;//7'b1101_101;
4'b0110 : dataout <= 7'b0000_010;//7'b1111_101;
4'b0111 : dataout <= 7'b1111_000;//7'b0000_111;
4'b1000 : dataout <= 7'b0000_000;//7'b1111_111;
4'b1001 : dataout <= 7'b0010_000;//7'b1101_111;
4'b1010 : dataout <= 7'b0001_000;//7'b1110_111;
4'b1011 : dataout <= 7'b0000_011;//7'b1111_100;
4'b1100 : dataout <= 7'b1000_110;//7'b0111_001;
4'b1101 : dataout <= 7'b0100_001;//7'b1011_110;
4'b1110 : dataout <= 7'b0000_110;//7'b1111_001;
4'b1111 : dataout <= 7'b0001_110;//7'b1110_001;
default : dataout <= 7'b1000_000;//7'b0111_111;
endcase
end
endmodule这个模块是「三段接力」结构,数据流为:cnt_scan → en(位选)→ dataout_buf(当前该显示什么数字)→ dataout(段码):
① 扫描计数器(时序逻辑)
cnt_scan 是 16 位自由计数器,每个时钟加 1。它的低 13 位实际上在做高速分频,而最高 3 位 cnt_scan[15:13] 每 2^13 = 8192 个时钟才进位一次——用它的值来轮流选位,相当于自带了合适的人工扫描节拍。
② 位选轮播(组合逻辑)
case(cnt_scan[15:13]) 把 3 位值翻译成 8 位位选 en:1111_1110 选中第 0 位(最低位数码管),1111_1101 选中第 1 位……0111_1111 选中第 7 位。任一时刻只有一位为 0(低电平使能),8 个状态循环轮转——这就是「轮流点亮」的物理实现。
③ 位选→显示值映射(组合逻辑)
case(en) 决定当前被选通的那一位显示什么数字:选中第 0 位显示 1、第 1 位显示 2……第 7 位显示 8。改这个 always 里的数字,就能让每位显示你想要的内容——原文注释特意指出「可在第 3 个 always 里修改需要显示的数字」,实际指的就是这里的映射表。
④ 数字→段码译码(组合逻辑)
case(dataout_buf) 把 0~F 译成 7 位段码。段码表与七段译码器实验完全相同(共阳、亮 = 低电平、g 在最高位 a 在最低位)。每行注释里 // 后面还保留了一份极性相反的段码(如 7'b0111_111),那是共阴极数码管的版本——如果你的板子是共阴管,直接换成注释里的值即可,这是原文贴心埋好的对照表。
这里没有用时钟驱动的状态机
注意后三个 always 的敏感列表分别是
@(cnt_scan)、@(en)、@(dataout_buf)——全是组合逻辑。扫描的「时序感」完全来自cnt_scan这个唯一 的时序件。初学者常犯的错误是把位选也写成always @(posedge clk)的状态机,反而增加复杂度。记住这个分工:计数靠时序,翻译靠组合。
修改显示内容的三种玩法
① 把位选→数字映射改成每位显示同一个数字,可验证扫描通路是否正常;② 把映射改成
dataout_buf = ~en[0]之类的算式要慎重——先在纸上演算每个 en 值对应的结果;③ 想显示多位不同数字(如倒计时 87654321),就在第 3 个 case 里逐位填好你想要的值,段码译码层完全不用动。
引脚约束
set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { dataout[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { rst } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }整理成表格:
| 信号 | FPGA 引脚 | 说明 |
|---|---|---|
| clk | R7 | 50MHz 系统时钟 |
| rst | A2 | 复位按键,低电平复位 |
| dataout[0]~[6] | C15, C16, B6, A5, B5, A8, A7 | 七段段选(a~g) |
| en[0]~[7] | D11, E11, F10, C13, E10, E12, E16, F16 | 8 位位选(COM 使能) |
段选、位选引脚与静态数码管实验完全一致——它们物理上接的就是同一组数码管。
下载测试
按标准流程编译布线并下载(注意 DIG/LED 拨动开关拨到数码管一侧):

正常现象:8 位数码管「同时」显示 1、2、3、4、5、6、7、8 各位不同——实际上是每约 0.5ms 只有一位在亮,扫描速度快于人眼分辨极限。用手机相机对准数码管拍摄,画面里可能会看到某一位比其他位亮,或者出现滚动条纹——相机快门「冻结」了扫描过程,恰好反向证明了动态扫描的存在。
现象与总结
本实验的关键收获:
- 动态扫描三板斧:扫描计数器(定节拍)→ 位选轮播(选通一位)→ 段码跟随(给对数据)。三者时序上严丝合缝,位选切换的同一时刻段码必须已经就位;
- 视觉暂留阈值:整轮扫描频率 > 50Hz 就看不到闪烁。这是所有「分时复用」硬件设计(数码管、点阵屏、VGA)的通用法则;
- 组合/时序的分工:本模块只有
cnt_scan一个时序件,其余都是组合翻译层。结构越简单,时序越可控; - 共阳/共阴对照:段码表每行注释里藏着共阴版本,换板子时按极性选用。
至此,数码管三部曲(七段译码 → 静态显示 → 动态扫描)完成。你已经具备了做时钟、计分牌、倒计时器等数码管应用的全套基础。
自测
自测
- 动态扫描为什么能让「依次点亮」看起来像「同时点亮」?扫描频率需要满足什么条件?
- 代码里用
cnt_scan[15:13]而不是整个cnt_scan来做位选,为什么?这样扫描一轮大约需要多少时间?- 位选
en = 8'b1111_0111时,哪一位数码管被点亮?此时dataout_buf的值是多少?- 段码表中每行注释里保存的第二套段码(如 1 对应
7'b0000_110)是给什么类型数码管用的?它和主表的关系是什么?- 如果把扫描计数器换成一个 3 位计数器直接驱动位选(不经过 16 位分频),显示会出现什么现象?
- 想让 8 位数码管显示「HELLO—0」这样的自定义字符组合,需要修改代码的哪一部分?段码译码层需要改吗?