简介:本资源是基于Xilinx EGO1 FPGA开发板的数码管滚动显示实践项目,面向数字电路与FPGA初学者及课程设计学生,解决“如何在真实硬件上实现学号动态滚动显示”这一典型教学需求。项目完整覆盖数码管驱动原理、Verilog逻辑设计、时序控制(含扫描计数器与移位寄存器)、约束文件(.xdc)配置及Vivado工程构建全流程,具备即下载即运行的工程完整性。压缩包共92个文件,以16个RTL源码(.v/.rst)、12个综合实现报告(.rpt)、4个二进制配置文件(.bit/.dcp)、4个约束与脚本(.xdc/.tcl/.sh)为核心,辅以仿真日志、布局布线文件及HTML工程报告,总大小616KB,目录结构严格遵循Vivado标准工程规范。目前已有3179人学习下载,读者可直接获取可复现的完整FPGA工程、逐位滚动学号的参考代码、关键时序调试参数及常见显示异常排错说明,快速掌握嵌入式数字系统显示模块的软硬协同实现方法。
1. 用 EGO1 FPGA 驱动数码管显示学号,不是接上线就亮——它卡在时序、扫描和段码映射三道关
你把数码管显示.zip解压进 Vivado 工程,烧录到 EGO1 开发板,数码管却只闪一下、全灭、或乱码?这不是代码没编译成功,而是 FPGA 和数码管之间存在三重隐性契约:共阴/共阳极性必须匹配硬件电路、动态扫描频率必须落在 60Hz–1000Hz 可视舒适区、每个数字/字母的段码必须严格对应 EGO1 底板上 7 段数码管的真实物理引脚定义。EGO1 板载的是 4 位共阴极 7 段数码管(非 16 段),其段选信号(a–g, dp)和位选信号(DIG0–DIG3)全部通过 XC6SLX9 的 Bank 0 引脚直连,没有驱动芯片缓冲——这意味着 FPGA IO 必须直接承担灌电流负载,IO 标准必须设为LVCMOS33,且不能忽略引脚电流能力限制(单 IO 最大 24mA,多位同时点亮时需均流)。本篇不讲“FPGA 是什么”,只聚焦如何让滚动学号在 EGO1 上稳定、清晰、无鬼影地跑起来:从底层引脚约束开始,到计数器分频逻辑,再到段码查表与位选轮询的协同时序,最后给出可直接替换学号、适配不同起始位置的参数化 Verilog 模板。
2. EGO1 数码管硬件约束与基础驱动框架:先让引脚说人话,再让时钟听指挥
2.1 精确锁定 EGO1 底板数码管物理连接与 IO 约束文件生成
EGO1 开发板(基于 Xilinx Spartan-6 XC6SLX9)的 4 位 7 段数码管采用共阴极接法:所有数码管的公共端(COM)分别连接至 DIG0–DIG3,而段信号 a–g、dp 则并联后接入 FPGA。查阅 EGO1 官方原理图(EGO1_Schematic.pdf第 12 页),关键引脚映射如下:
| 信号名 | FPGA 引脚 | Bank | 电压标准 | 物理作用 |
|---|---|---|---|---|
seg_a | P14 | 0 | LVCMOS33 | 段 a(右下竖) |
seg_b | N14 | 0 | LVCMOS33 | 段 b(右上竖) |
seg_c | N15 | 0 | LVCMOS33 | 段 c(右下竖) |
seg_d | P15 | 0 | LVCMOS33 | 段 d(下横) |
seg_e | R18 | 0 | LVCMOS33 | 段 e(左下竖) |
seg_f | T16 | 0 | LVCMOS33 | 段 f(左上竖) |
seg_g | U16 | 0 | LVCMOS33 | 段 g(中横) |
seg_dp | T15 | 0 | LVCMOS33 | 小数点 |
dig0 | R17 | 0 | LVCMOS33 | 第 1 位(最左)COM |
dig1 | U15 | 0 | LVCMOS33 | 第 2 位 COM |
dig2 | V16 | 0 | LVCMOS33 | 第 3 位 COM |
dig3 | W16 | 0 | LVCMOS33 | 第 4 位(最右)COM |
注意:EGO1 的
dig0对应最左侧数码管,dig3对应最右侧——这与多数初学者直觉相反,若顺序写反会导致学号镜像显示。必须在 XDC 文件中逐行声明,不可依赖自动生成。
将上述映射写入ego1_digit.xdc,并确保在 Vivado 中正确添加约束文件:
# ego1_digit.xdc —— 必须保存为 UTF-8 无 BOM 格式 set_property PACKAGE_PIN P14 [get_ports seg_a] set_property IOSTANDARD LVCMOS33 [get_ports seg_a] set_property PACKAGE_PIN N14 [get_ports seg_b] set_property IOSTANDARD LVCMOS33 [get_ports seg_b] set_property PACKAGE_PIN N15 [get_ports seg_c] set_property IOSTANDARD LVCMOS33 [get_ports seg_c] set_property PACKAGE_PIN P15 [get_ports seg_d] set_property IOSTANDARD LVCMOS33 [get_ports seg_d] set_property PACKAGE_PIN R18 [get_ports seg_e] set_property IOSTANDARD LVCMOS33 [get_ports seg_e] set_property PACKAGE_PIN T16 [get_ports seg_f] set_property IOSTANDARD LVCMOS33 [get_ports seg_f] set_property PACKAGE_PIN U16 [get_ports seg_g] set_property IOSTANDARD LVCMOS33 [get_ports seg_g] set_property PACKAGE_PIN T15 [get_ports seg_dp] set_property IOSTANDARD LVCMOS33 [get_ports seg_dp] set_property PACKAGE_PIN R17 [get_ports dig0] set_property IOSTANDARD LVCMOS33 [get_ports dig0] set_property PACKAGE_PIN U15 [get_ports dig1] set_property IOSTANDARD LVCMOS33 [get_ports dig1] set_property PACKAGE_PIN V16 [get_ports dig2] set_property IOSTANDARD LVCMOS33 [get_ports dig2] set_property PACKAGE_PIN W16 [get_ports dig3] set_property IOSTANDARD LVCMOS33 [get_ports dig3]2.2 构建可靠时钟分频器:从 100MHz 系统时钟到 1kHz 扫描节拍
EGO1 板载 100MHz 晶振(clk100m),但数码管动态扫描不能直接用此频率——若以 100MHz 切换位选,人眼无法分辨,且 FPGA IO 切换过快会引发串扰与功耗激增。经验表明,位选切换周期需控制在 1ms–16ms(即扫描频率 1kHz–62.5Hz)之间:低于 60Hz 会出现明显闪烁;高于 1kHz 则段码刷新来不及稳定,导致亮度下降或拖影。我们取中间值 1kHz(周期 1ms),用计数器对 100MHz 分频:
// digit_ctrl.v —— 核心扫描控制器 module digit_ctrl ( input wire clk100m, input wire rst_n, output reg [3:0] digit_sel, // 位选信号:0001=仅dig0亮,1000=仅dig3亮 output reg [7:0] seg_data // 段码输出(低7位a-g,bit7=dp) ); reg [15:0] cnt_1ms; // 16位计数器,100MHz下计满100000次≈1ms always @(posedge clk100m or negedge rst_n) begin if (!rst_n) cnt_1ms <= 0; else if (cnt_1ms == 16'd99999) // 精确计数:100MHz / 100000 = 1000Hz cnt_1ms <= 0; else cnt_1ms <= cnt_1ms + 1; end // 位选轮询:每1ms切换一位,4ms完成一轮扫描 reg [1:0] digit_idx; always @(posedge clk100m or negedge rst_n) begin if (!rst_n) digit_idx <= 0; else if (cnt_1ms == 16'd99999) // 在1ms边界触发切换 digit_idx <= digit_idx + 1; end // 位选译码:digit_idx=0→dig0有效(低电平),其余高阻 always @(*) begin case (digit_idx) 2'b00: digit_sel = 4'b1110; // dig0=0, others=1 → 仅dig0导通 2'b01: digit_sel = 4'b1101; // dig1=0 2'b10: digit_sel = 4'b1011; // dig2=0 2'b11: digit_sel = 4'b0111; // dig3=0 default: digit_sel = 4'b1110; endcase end // 段码暂存寄存器(关键!避免段码在位选切换瞬间变化) reg [7:0] seg_latch; always @(posedge clk100m or negedge rst_n) begin if (!rst_n) seg_latch <= 8'h00; else if (cnt_1ms == 16'd99999) // 与位选同步更新段码 seg_latch <= get_seg_code(digit_idx); // 调用查表函数 end assign seg_data = seg_latch; // 查表函数:返回当前位应显示的段码(共阴极,0=亮) function [7:0] get_seg_code; input [1:0] idx; reg [3:0] digit_val; begin // 此处填入学号数字,例如学号20231234 → 4位:2,0,2,3 // 实际使用时替换为参数化数组 case (idx) 2'b00: digit_val = 4'h2; // 第1位显示'2' 2'b01: digit_val = 4'h0; // 第2位显示'0' 2'b10: digit_val = 4'h2; // 第3位显示'2' 2'b11: digit_val = 4'h3; // 第4位显示'3' default: digit_val = 4'h0; endcase case (digit_val) 4'h0: get_seg_code = 8'b11000000; // 0: abcdef = 11000000, dp=0 4'h1: get_seg_code = 8'b11111001; // 1: bc = 11111001 4'h2: get_seg_code = 8'b10100100; // 2: abged = 10100100 4'h3: get_seg_code = 8'b10110000; // 3: abgcd = 10110000 4'h4: get_seg_code = 8'b10011001; // 4: fgbc = 10011001 4'h5: get_seg_code = 8'b10010010; // 5: afgcd = 10010010 4'h6: get_seg_code = 8'b10000010; // 6: afgcde = 10000010 4'h7: get_seg_code = 8'b11111000; // 7: abc = 11111000 4'h8: get_seg_code = 8'b10000000; // 8: abcdefg = 10000000 4'h9: get_seg_code = 8'b10010000; // 9: abcdfg = 10010000 default: get_seg_code = 8'b11111111; // 全灭 endcase end endfunction endmodule提示:段码表必须按共阴极设计(0=点亮),若误用共阳极码会导致全黑。
seg_data[7]为小数点位,当前未启用;如需显示小数点(如学号带分隔符),在对应digit_val分支中置get_seg_code[7] = 1即可。
3. 滚动学号实现:用移位寄存器+状态机驱动 8 位学号在 4 位数码管上平滑游走
3.1 学号数据结构化:从字符串到可索引的 4-bit 数组
滚动显示的本质是让一个长学号(如 8 位20231234)在固定宽度(4 位)的窗口中匀速平移。常见错误是直接用for循环拼接,但 FPGA 不支持运行时索引——必须在综合前确定所有可能访问的偏移。因此,我们将学号预处理为 8 个独立的 4-bit 向量,并用 3-bit 计数器scroll_pos控制当前窗口起始位置:
scroll_pos | 显示内容(4位) | 对应学号索引 |
|---|---|---|
| 3'b000 | digits[0:3] | 2023 |
| 3'b001 | digits[1:4] | 0231 |
| 3'b010 | digits[2:5] | 2312 |
| 3'b011 | digits[3:6] | 3123 |
| 3'b100 | digits[4:7] | 1234 |
| 3'b101 | digits[5:7]+0 | 2340(补零) |
| 3'b110 | digits[6:7]+00 | 3400 |
| 3'b111 | digits[7]+000 | 4000 |
该方案避免了 RAM 查找,全程组合逻辑实现,资源消耗极低。
3.2 滚动状态机与位选联动:确保每位数码管显示正确的数字
在digit_ctrl模块中扩展滚动逻辑,将digit_idx(0–3)与scroll_pos(0–7)结合,计算出当前扫描位实际应显示的学号数字:
// 续接 digit_ctrl.v,添加以下逻辑 localparam STUDENT_ID_LEN = 8; reg [3:0] digits [0:STUDENT_ID_LEN-1]; // 学号数组:digits[0]=MSB // 初始化学号(例:20231234) initial begin digits[0] = 4'h2; digits[1] = 4'h0; digits[2] = 4'h2; digits[3] = 4'h3; digits[4] = 4'h1; digits[5] = 4'h2; digits[6] = 4'h3; digits[7] = 4'h4; end // 滚动位置计数器:每 500ms 移动一格(可调) reg [18:0] scroll_cnt; reg [2:0] scroll_pos; // 3-bit,支持0-7共8种偏移 always @(posedge clk100m or negedge rst_n) begin if (!rst_n) begin scroll_cnt <= 0; scroll_pos <= 0; end else if (scroll_cnt == 19'd50000000) begin // 100MHz下500ms scroll_cnt <= 0; scroll_pos <= scroll_pos + 1; end else scroll_cnt <= scroll_cnt + 1; end // 根据 digit_idx(0-3)和 scroll_pos 计算当前位显示的数字 function [3:0] get_display_digit; input [1:0] idx; // 当前扫描位:0=dig0, 1=dig1, ... input [2:0] pos; // 当前滚动起始位置 begin reg [2:0] abs_idx; // 绝对索引 = pos + idx abs_idx = pos + idx; case (abs_idx) 3'b000: get_display_digit = digits[0]; 3'b001: get_display_digit = digits[1]; 3'b010: get_display_digit = digits[2]; 3'b011: get_display_digit = digits[3]; 3'b100: get_display_digit = digits[4]; 3'b101: get_display_digit = digits[5]; 3'b110: get_display_digit = digits[6]; 3'b111: get_display_digit = digits[7]; 3'b1000: get_display_digit = 4'h0; // 超出范围补零 default: get_display_digit = 4'h0; endcase end endfunction // 替换原 get_seg_code 中的 digit_val 获取方式 always @(*) begin case (digit_idx) 2'b00: digit_val = get_display_digit(2'b00, scroll_pos); 2'b01: digit_val = get_display_digit(2'b01, scroll_pos); 2'b10: digit_val = get_display_digit(2'b10, scroll_pos); 2'b11: digit_val = get_display_digit(2'b11, scroll_pos); default: digit_val = 4'h0; endcase end3.3 关键时序协同:为什么cnt_1ms必须驱动digit_idx和seg_latch?
动态扫描的稳定性取决于两个信号的严格同步:
digit_idx决定哪一位数码管被选中(位选);seg_latch决定该位显示什么(段码);- 二者必须在同一个
cnt_1ms边沿更新,否则会出现“位选已切到 dig1,但 seg_data 还是 dig0 的旧码”的鬼影。
上述代码中,digit_idx在cnt_1ms == 99999时加 1,seg_latch也在同一条件触发更新,保证了原子性。若将seg_latch改为always @(posedge clk100m),则段码可能在位选切换中途变化,导致某位短暂显示错误数字。
4. 7 段数码管段码验证与学号定制:用 Python 快速生成 EGO1 兼容段码表
4.1 手动验证段码:用万用表测通断,比仿真更可靠
仿真波形看不出段码是否真能点亮。最直接的方法是:将seg_data固定为8'b10100100(数字 2 的段码),digit_sel固定为4'b1110(仅 dig0 有效),烧录后用万用表二极管档测量seg_a(P14)对dig0(R17)是否导通。若导通,说明 a 段物理连接正确;依次测试 b–g,记录实际点亮段与理论是否一致。常见问题:
- 测量
seg_a与dig0不导通,但seg_a与 GND 导通 → 共阴极接法确认,dig0未拉低 → 检查digit_sel是否为4'b1110且dig0引脚约束正确; - 所有段对
dig0均不导通 →dig0引脚配置为LVCMOS33但未设置DRIVE属性(Vivado 中需在 XDC 加set_property DRIVE 24 [get_ports dig0])。
4.2 Python 脚本批量生成学号段码:告别手敲 8 行 case
将学号字符串转为 Verilog 初始化数组,避免手动转换出错。以下 Python 脚本(保存为gen_id.py)输入学号,输出digits初始化代码:
#!/usr/bin/env python3 # gen_id.py —— 运行:python gen_id.py 20231234 import sys SEG_MAP = { '0': '11000000', '1': '11111001', '2': '10100100', '3': '10110000', '4': '10011001', '5': '10010010', '6': '10000010', '7': '11111000', '8': '10000000', '9': '10010000' } if len(sys.argv) != 2 or not sys.argv[1].isdigit(): print("Usage: python gen_id.py <student_id>") exit(1) sid = sys.argv[1] print(f"// Generated for student ID: {sid}") print(f"initial begin") for i, d in enumerate(sid): seg_bin = SEG_MAP.get(d, '11111111') print(f" digits[{i}] = 4'h{int(seg_bin[:4],2):x}; // '{d}' -> {seg_bin}") print("end")运行python gen_id.py 20231234输出:
// Generated for student ID: 20231234 initial begin digits[0] = 4'h2; // '2' -> 10100100 digits[1] = 4'h0; // '0' -> 11000000 digits[2] = 4'h2; // '2' -> 10100100 digits[3] = 4'h3; // '3' -> 10110000 digits[4] = 4'h1; // '1' -> 11111001 digits[5] = 4'h2; // '2' -> 10100100 digits[6] = 4'h3; // '3' -> 10110000 digits[7] = 4'h4; // '4' -> 10011001 end直接复制进 Verilog 即可,零错误。
4.3 滚动速度与显示效果权衡:三个可调参数及其物理意义
| 参数 | Verilog 变量 | 默认值 | 调整效果 | 物理依据 |
|---|---|---|---|---|
| 扫描频率 | cnt_1ms比较值 | 99999 | >1000Hz:亮度降、功耗升;<60Hz:肉眼可见闪烁 | 人眼视觉暂留时间约 1/16s |
| 滚动步长 | scroll_cnt比较值 | 50000000(500ms) | 值越大滚动越慢,适合长学号阅读 | 避免人眼来不及识别 |
| 位选驱动强度 | XDC 中DRIVE属性 | 未显式设置时默认 8mA | 若数码管亮度不足,加set_property DRIVE 24 [get_ports dig*] | EGO1 IO 最大驱动 24mA,需匹配数码管典型压降 2.0V@10mA |
修改scroll_cnt比较值即可改变滚动节奏,无需重写逻辑。例如改为19'd100000000,滚动间隔变为 1s,更适合教学演示。
5. EGO1 数码管进阶技巧:添加启动标志、消隐控制与多学号切换
5.1 启动消隐:解决上电瞬间乱码的硬件级方案
FPGA 配置完成后,内部寄存器处于随机态,digit_sel和seg_data可能输出非法值,导致数码管炸屏。简单做法是在复位释放后延迟 10ms 再启用扫描:
reg [19:0] rst_delay_cnt; reg rst_delay_done; always @(posedge clk100m or negedge rst_n) begin if (!rst_n) begin rst_delay_cnt <= 0; rst_delay_done <= 0; end else if (rst_delay_cnt == 20'd1000000) begin // 10ms rst_delay_done <= 1; end else rst_delay_cnt <= rst_delay_cnt + 1; end // 在 digit_sel 和 seg_data 驱动前加入使能 assign digit_sel_en = rst_delay_done; assign seg_data_en = rst_delay_done; // 修改 digit_sel 和 seg_latch 的 always 块,添加使能条件 always @(posedge clk100m or negedge rst_n) begin if (!rst_n) digit_idx <= 0; else if (rst_delay_done && cnt_1ms == 16'd99999) digit_idx <= digit_idx + 1; end5.2 多学号切换:用拨码开关(SW)选择不同学号组
EGO1 板载 8 个拨码开关(sw[7:0]),可将其作为学号选择输入。例如sw[1:0]选择 4 组学号:
sw[1:0] | 学号组 | Verilog 实现 |
|---|---|---|
| 2'b00 | A组 | digits = id_a; |
| 2'b01 | B组 | digits = id_b; |
| 2'b10 | C组 | digits = id_c; |
| 2'b11 | D组 | digits = id_d; |
在顶层模块中例化四组digits数组,并用always @(*)根据sw[1:0]选择:
reg [3:0] id_a [0:7], id_b [0:7], id_c [0:7], id_d [0:7]; // ... 初始化四组学号 ... wire [1:0] id_sel = sw[1:0]; reg [3:0] digits [0:7]; always @(*) begin case (id_sel) 2'b00: digits = id_a; 2'b01: digits = id_b; 2'b10: digits = id_c; 2'b11: digits = id_d; endcase end烧录后拨动 SW0/SW1 即可切换显示不同学号,无需重新编译。
5.3 小数点动态启用:在滚动中插入分隔符提升可读性
学号20231234滚动时易误读为20231234,若在第 2 位后加小数点(20.231234),可显著提升辨识度。只需修改get_display_digit函数,在特定abs_idx位置强制设置seg_data[7] = 1:
// 在 get_display_digit 返回前添加: if (abs_idx == 3'b010) // 在第3个绝对位置(即滚动到第2位数字后)点亮dp get_seg_code[7] = 1;此时当scroll_pos=0且digit_idx=2(即显示学号第3位)时,小数点自动亮起,形成20.2的视觉分组。该技巧不增加额外资源,纯组合逻辑实现。
提示:所有修改均在
digit_ctrl.v内完成,无需改动约束文件或顶层模块。将digit_ctrl作为独立 IP 复用到其他项目时,只需替换digits初始化部分与scroll_cnt周期即可适配任意学号与节奏。
本文还有配套的精品资源,点击获取