这一篇在干嘛?
上一篇的七段译码器一次只能显示一位,这一篇把视野扩大到多位:让数码管从
00000000到FFFFFFFF循环递增,每个字符停留 0.5 秒。更重要的是工程结构升级——采用顶层模块 + 两个子模块的分层设计,这是 FPGA 工程走向规范化的第一步。
实验目标与电路原理
实验任务:设计 8 位数码管静态显示——实现 6 位数码管从 00000000——FFFFFFFF 循环显示,每个字符显示 0.5s 后变化。
(任务表述里「8 位数码管」「6 位数码管」并存,代码实现以 6 位位选 seg_sel[5:0] 为准,显示数值是 4 位十六进制数 num 从 0 循环到 F。)
数码管的公共段结构
HX4S20 板采用 8 位共阳数码管,其连接方式是理解一切数码管实验的基础(电路图可在「使用手册」原理图中查找)。8 位数码扫描显示电路中,每个数码管的 8 个段 h、g、f、e、d、c、b、a(h 是小数点)都分别连在一起,接段选信号 DIG0DIG7;8 个数码管分别由 8 个选通信号 k1~k8 控制(对应 SEL0SEL7)。
被选通的数码管显示数据,其余关闭。例如某一时刻 k3 为低电平、其余选通信号为高电平,这时仅 k3 对应的数码管显示来自段信号端的数据,其他 7 个数码管呈现关闭状态。
8 位数码管连接关系如下图:

图 1.1 8 数码管连接图
本实验是「静态」显示:所有位选同时有效(seg_sel <= 6'b000000,6 位全部选通),每一位数码管显示同一个字符 num。也就是说,本实验虽然点亮了 6 位,但 6 位显示的内容完全相同——「每位显示不同内容」需要下一篇的动态扫描技术。
「静态」与「动态」的分水岭
静态显示:位选全开,段选一路,所有位同显一个字符,代码简单但信息量少。动态显示:位选轮流通电、段选随之切换,利用视觉暂留让各位显示不同字符。本实验代码中「位选固定为 000000」这一行,正是静态显示的标志。
分层设计与 Verilog 实现
本实验采用三层结构:
- 顶层模块
seg_led_static_top:连接时钟复位,例化两个子模块,引出位选与段选; - 定时模块
time_count:每 0.5 秒产生一个时钟周期的脉冲信号flag,作为「该变数字了」的通知; - 显示模块
seg_led_static:收到通知后把显示数值 num 加 1,并按 num 译码输出段选。
顶层例化关系(原文给出的连接骨架):
seg_led_static u_seg_led_static (
.clk (sys_clk ),
.rst_n (sys_rst_n),
.add_flag (add_flag),
.seg_sel (seg_sel ),
.seg_led (seg_led )//wire define
wire add_flag; // 数码管变化的通知信号time_count #(
.MAX_NUM (TIME_SHOW)以下按模块给出完整代码(原文 OCR 排版有破碎,已按原文的信号定义、参数与逻辑补全还原)。
顶层模块 seg_led_static_top
module seg_led_static_top (
input sys_clk , // 系统时钟
input sys_rst_n, // 系统复位信号(低有效)
output [5:0] seg_sel , // 数码管位选
output [7:0] seg_led // 数码管段选
);
//parameter define
parameter TIME_SHOW = 25000_000; // 0.5s:50MHz 下 0.5s 计 25000_000 个时钟
//wire define
wire add_flag; // 数码管变化的通知信号
//例化定时模块:每 0.5s 输出一个脉冲
time_count #(
.MAX_NUM (TIME_SHOW)
) u_time_count(
.clk (sys_clk ),
.rst_n (sys_rst_n),
.flag (add_flag )
);
//例化数码管静态显示模块
seg_led_static u_seg_led_static (
.clk (sys_clk ),
.rst_n (sys_rst_n),
.add_flag (add_flag),
.seg_sel (seg_sel ),
.seg_led (seg_led )
);
endmodule定时模块 time_count
module time_count(
input clk, //时钟信号
input rst_n, //复位信号
output reg flag //一个时钟周期的脉冲信号
);
//parameter define
parameter MAX_NUM = 25000_000; // 计数器最大计数值
//reg define
reg [24:0] cnt; // 时钟分频计数器
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
flag <= 1'b0;
cnt <= 25'b0;
end
else if(cnt < MAX_NUM - 1'b1) begin
cnt <= cnt + 1'b1;
flag <= 1'b0;
end
else begin
cnt <= 25'b0;
flag <= 1'b1;
end
end
endmodule原理很直白:50MHz 时钟周期 20ns,0.5 秒 = 500ms = 25 000 000 个时钟周期,所以 MAX_NUM = 25000_000。cnt 从 0 计到 MAX_NUM - 1,计满瞬间把 flag 拉高一个时钟周期然后清零重计。flag 是单周期脉冲——这正是模块间「事件通知」的标准做法,下游模块用 if(add_flag) 捕捉一次即可,绝不会重复响应。
参数化计数值,模块才能复用
time_count把计数值做成parameter MAX_NUM,顶层例化时用.MAX_NUM(TIME_SHOW)传入 0.5 秒对应的值。想改成每秒变化一次,只需在顶层改参数,子模块一行不动。写「万能定时器」时养成参数化习惯,后面所有实验都能白嫖这个模块。
显示模块 seg_led_static
module seg_led_static (
input clk , // 时钟信号
input rst_n , // 复位信号(低有效)
input add_flag, // 数码管变化的通知信号
output reg [5:0] seg_sel , // 数码管位选
output reg [7:0] seg_led // 数码管段选
);
//reg define
reg [3:0] num; // 数码管显示的十六进制数
//控制数码管位选信号(低电平有效),选中所有的数码管
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
seg_sel <= 6'b111111;
else
seg_sel <= 6'b000000;
end
//每次通知信号到达时,数码管显示的十六进制数值加 1
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
num <= 4'h0;
else if(add_flag) begin
if (num < 4'hf)
num <= num + 1'b1;
else
num <= 4'h0;
end
else
num <= num;
end
//根据数码管显示的数值,控制段选信号
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
seg_led <= 8'b0;
else begin
case (num)
4'h0 : seg_led <= 8'b1100_0000;
4'h1 : seg_led <= 8'b1111_1001;
4'h2 : seg_led <= 8'b1010_0100;
4'h3 : seg_led <= 8'b1011_0000;
4'h4 : seg_led <= 8'b1001_1001;
4'h5 : seg_led <= 8'b1001_0010;
4'h6 : seg_led <= 8'b1000_0010;
4'h7 : seg_led <= 8'b1111_1000;
4'h8 : seg_led <= 8'b1000_0000;
4'h9 : seg_led <= 8'b1001_0000;
4'ha : seg_led <= 8'b1000_1000;
4'hb : seg_led <= 8'b1000_0011;
4'hc : seg_led <= 8'b1100_0110;
4'hd : seg_led <= 8'b1010_0001;
4'he : seg_led <= 8'b1000_0110;
4'hf : seg_led <= 8'b1000_1110;
default : seg_led <= 8'b1100_0000;
endcase
end
end
endmodule三个 always 块各司其职:
- 位选:复位时
seg_sel <= 6'b111111(全灭),正常工作时恒为6'b000000(6 位全部选通)——静态显示的关键一行; - 计数值:
num从 0 计到 F 循环,每次add_flag脉冲到来加 1,即每 0.5 秒变一次; - 译码:8 位段选
seg_led[7:0]的高位是第 8 段(小数点 h),低 7 位对应 g~a。注意这里的段码是 8 位且高电平为灭:例如显示 0 时输出8'b1100_0000——第 7、6 位(小数点和 g 段)为 1 灭掉,其余段为 0 点亮,与上一篇共阳数码管「亮 = 低电平」的规则完全一致,只是多了小数点位、位序也对调到了高 7 位。
对照检查:4'h1 : 8'b1111_1001——只亮 b、c 两段(bit1、bit0 为 0),显示「1」,正确。
两套段码表别混用
上一篇
LED7S[6:0]是 7 位、g 在最高位;本篇seg_led[7:0]是 8 位、a 在最低位、小数点 h 在最高位。同一个数字「0」在两套表里分别是7'b1000_000和8'b1100_0000。核对段码时务必先确认位序约定,这是移植代码时最容易踩的坑。
RTL 原理图
综合后可以查看 RTL 视图,验证顶层确实例化了 time_count 与 seg_led_static 两个模块,add_flag 作为模块间的单脉冲连线:

引脚约束
复位 rst 接键 1,8 位数码管使能接数码管位选,段选输出接数码管段选。完整约束如下:
set_pin_assignment { seg_led[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_led[7] } { LOCATION = A6; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[1] } { LOCATION = E11;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { seg_sel[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sys_clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { sys_rst_n } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }对照表格(段选引脚与上一篇 LED7S 完全一致,位选即上一篇的 B[7:0],另多出小数点段 seg_led[7] 接 A6):
| 信号 | FPGA 引脚 | 说明 |
|---|---|---|
| sys_clk | R7 | 50MHz 系统时钟 |
| sys_rst_n | A2 | 复位按键(键 1) |
| seg_led[0]~[7] | C15, C16, B6, A5, B5, A8, A7, A6 | 段选 a~g、h(小数点) |
| seg_sel[0]~[7] | D11, E11, F10, C13, E10, E12, E16, F16 | 位选 SEL0~SEL7 |
下载测试
按标准流程编译布线并下载。注意开发板上的拨动开关:DIG/LED 拨上,接通数码管;拨下接通 LED。把开关拨到数码管一侧后,观察显示的效果:

正常现象:6 位数码管同时显示相同的字符,字符从 0 开始每 0.5 秒递增一次,依次经过 0~F 后回到 0 循环。如果显示完全不亮,先检查 DIG/LED 拨动开关的位置;如果字符乱码,对照段码表核对位序。
现象与总结
本实验的关键收获:
- 分层设计:顶层只负责连线,定时(多久变一次)与显示(显示什么)各自封装成子模块,模块间用单周期脉冲
add_flag通信——这是可维护 FPGA 工程的标准形态; - 单周期脉冲握手:
flag只维持一个时钟周期,接收方if(add_flag)只会命中一次,天然避免了「长电平导致连续计数」的问题; - 静态显示的局限:位选全开、全部同显一个字符。想 6 位各显各的,就需要下一篇的动态扫描;
- 8 位段码的位序:本篇段码
seg_led[7:0]与上篇LED7S[6:0]位序约定不同,移植代码前先对表。
扩展练习:把 TIME_SHOW 改成 5000_000(0.1 秒一变)观察节奏变化;把位选改成只选通第 2 位,体会位选信号的作用。
自测
自测
- 0.5 秒对应的计数终值是怎么算出来的?
time_count的cnt为什么定义为 25 位?flag为什么设计成「只维持一个时钟周期的脉冲」?如果flag在 0.5 秒内一直为高电平,num会怎样变化?- 静态显示时代码里
seg_sel <= 6'b000000意味着什么?此时 6 位数码管显示的内容有什么特点?- 显示「0」时本篇输出
8'b1100_0000,上一篇输出7'b1000_000,为什么同一个字符两套段码不同?分别说明各位对应的段。- 本实验的分层设计中,顶层模块例化了哪两个子模块?它们之间通过什么信号通信?
- 上板后数码管完全不亮,应该先检查什么开关?