简介:本资源为西安电子科技大学软件工程学院FPGA数字系统设计课程的第二次实验报告,面向高校电子类、计算机类专业本科生及FPGA初学者,聚焦ISE开发环境实操与Xilinx IP核应用能力培养。报告完整覆盖数字时钟管理(DCM)与VGA显示两大核心实验:前者详解Clocking Wizard配置流程,实现100MHz输入下50MHz/200MHz双路时钟生成及相位控制;后者基于Nexys3开发板,完成VGA时序驱动、色彩生成与顶层模块例化,并附约束文件与硬件配置说明。资源为单个219KB的Word文档(.docx),内容结构规范,含实验原理、分步操作、仿真波形分析、测试结果截图及总结反思,便于对照复现与理解底层时序逻辑。目前已有189人学习下载,是掌握FPGA同步设计、IP核集成与显示接口开发的典型教学实践材料。
1. FPGA实验文档不是“交作业文件”,而是数字电路落地的最小闭环验证凭证
你手头这份《XDU软工院选FPGA第二次实验.docx》,表面看是某高校软件工程专业开设的FPGA选修课配套实验材料,但实际它承载着一个被严重低估的关键价值:用纯Verilog HDL在真实开发板上跑通一个可观察、可测量、可复位的同步时序系统。这不是VHDL仿真截图拼凑的PPT,也不是ModelSim里一闪而过的波形——它要求你把“计数器+按键消抖+数码管译码+时钟分频”这四块砖,在没有IP核、不调用Xilinx官方库(如XilinxCoreLib)的前提下,用原生RTL代码砌成一座能稳定亮灯、抗抖动、响应按键的微型状态机建筑。很多同学卡在“下载后数码管不亮”或“按键一按就乱跳”,本质不是语法错,而是没吃透文档里隐藏的三个硬约束:目标器件型号锁定为XC7A35T-2CSG324C(Artix-7系列)、约束文件(.xdc)必须手动绑定引脚、时钟源强制使用板载100MHz晶振经PLL分频而非直接接入。这篇笔记不讲理论推导,只拆解从打开Word文档到板子上数码管稳定显示“0000”的完整链路——包括你容易忽略的.docx里那张不起眼的引脚表怎么转成可用.xdc、为什么Verilog里写always @(posedge clk)却要对rst_n做异步复位、以及ISE/Vivado版本混用导致综合失败的血泪经验。适合正在赶实验 deadline 的软工/计科学生,也适合想补数字电路实操短板的嵌入式初学者。
2. 从Word文档到可综合Verilog:四步提取核心设计要素
2.1 解析实验文档结构:定位真需求而非文字描述
《XDU软工院选FPGA第二次实验.docx》虽为Word格式,但其内容结构高度标准化。我们需跳过教学目标、实验原理等说明性段落,直取以下四类关键信息:
- 功能定义区:通常位于“实验内容”或“设计要求”小节,明确写出“实现4位十进制计数器,按键K1加1、K2减1,数码管动态扫描显示”;
- 硬件资源图:文档中必含开发板实物图或引脚分配示意图,标注K1/K2对应FPGA引脚(如U18/V17)、数码管段选/位选引脚(如T10/U11等);
- 时序约束隐含条件:在“注意事项”或“思考题”中常出现“使用板载100MHz时钟”“数码管刷新频率不低于50Hz”等表述,这是计算分频系数的唯一依据;
- 验收标准:如“按键操作后数值变化延迟≤200ms”“连续按K1十次,数码管无跳变”——这直接决定消抖模块的计数阈值。
提示:不要试图把.docx转成PDF再OCR识别。直接用Word“选择性粘贴→纯文本”提取关键句,避免格式干扰。若文档含表格,优先复制引脚表(列名通常为“按键/数码管名称 | FPGA引脚 | 电平逻辑”),这是后续.xdc文件的原始数据源。
2.2 手动构建顶层模块框架:按信号流反向推导端口
根据功能定义,顶层模块必须包含以下端口(以Verilog为例):
module top_level ( input wire clk_100m, // 板载100MHz时钟,命名必须与.xdc一致 input wire rst_n, // 低电平复位,注意文档是否要求上拉/下拉 input wire key_add, // K1按键,文档若未说明,默认高电平有效(未按下为1) input wire key_sub, // K2按键,同上 output wire [6:0] seg_data, // 数码管段选信号(共阴极,a-g顺序) output wire [3:0] seg_sel // 数码管位选信号(4位,低电平选中) );关键细节:
clk_100m命名必须与.xdc中set_property PACKAGE_PIN E3 [get_ports clk_100m]的端口名完全一致,否则综合工具无法绑定;rst_n后缀_n表示低电平有效,若文档中复位按键标注“active low”,则代码中必须用negedge rst_n触发复位;- 按键信号
key_add/key_sub需确认电平逻辑:若文档引脚表注明“K1: U18, active high”,则未按下为0,按下为1,可直连;若为“active low”,则需在代码中加assign key_add = ~btn_u18反相。
2.3 将Word引脚表转为.xdc约束文件:三行命令生成可用约束
假设文档引脚表给出:
| 名称 | FPGA引脚 | 电平逻辑 |
|---|---|---|
| K1 | U18 | active low |
| SEG_A | T10 | active high |
| SEG_B | U11 | active high |
| ... | ... | ... |
对应.xdc文件内容(每行独立,不可合并):
# 约束时钟引脚(必须放在第一行,否则时序分析失效) set_property PACKAGE_PIN E3 [get_ports clk_100m] set_property IOSTANDARD LVCMOS33 [get_ports clk_100m] # 约束复位按键(注意:active low需指定PULLUP) set_property PACKAGE_PIN T17 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] set_property PULLUP true [get_ports rst_n] # 文档若要求上拉,此行必加 # 约束K1按键(active low,同样需PULLUP) set_property PACKAGE_PIN U18 [get_ports key_add] set_property IOSTANDARD LVCMOS33 [get_ports key_add] set_property PULLUP true [get_ports key_add] # 约束数码管段选(active high,无需PULLUP) set_property PACKAGE_PIN T10 [get_ports seg_data[0]] # a段 set_property PACKAGE_PIN U11 [get_ports seg_data[1]] # b段 set_property PACKAGE_PIN V10 [get_ports seg_data[2]] # c段 set_property PACKAGE_PIN V11 [get_ports seg_data[3]] # d段 set_property PACKAGE_PIN U12 [get_ports seg_data[4]] # e段 set_property PACKAGE_PIN U13 [get_ports seg_data[5]] # f段 set_property PACKAGE_PIN V14 [get_ports seg_data[6]] # g段 # 约束数码管位选(4位,低电平选中,故需PULLUP保证未选中时为高电平) set_property PACKAGE_PIN T14 [get_ports seg_sel[0]] # 位0 set_property PACKAGE_PIN T15 [get_ports seg_sel[1]] # 位1 set_property PACKAGE_PIN V15 [get_ports seg_sel[2]] # 位2 set_property PACKAGE_PIN U14 [get_ports seg_sel[3]] # 位3 set_property IOSTANDARD LVCMOS33 [get_ports seg_sel] set_property PULLUP true [get_ports seg_sel] # 关键!防止位选悬空导致乱码逻辑说明:
PACKAGE_PIN后的引脚名(如E3、U18)必须与开发板原理图及文档引脚表完全一致,大小写敏感;IOSTANDARD LVCMOS33是Artix-7默认电平标准,不可改为LVDS或HSTL;PULLUP true对所有输入按键和位选信号是刚需:按键未按下时引脚悬空,若不加内部上拉,读取值随机;位选信号若悬空,可能同时点亮多个数码管造成重影;- 数码管段选
seg_data[0]到seg_data[6]需逐行约束,不能写set_property PACKAGE_PIN {T10 U11 ...} [get_ports seg_data]——Vivado不支持数组批量绑定。
3. 四大核心模块Verilog实现:拒绝黑匣子,每个always块都可调试
3.1 时钟分频模块:为什么必须用PLL而非计数器分频?
实验文档隐含要求“数码管刷新≥50Hz”,即位选切换周期≤20ms。若直接用100MHz时钟计数分频:
- 刷新率50Hz → 位选切换间隔=20ms → 计数上限=100e6×0.02=2e6(200万次计数);
- 若再叠加数码管显示的“每位显示2ms”,则段选更新需100MHz/500=200kHz,计数上限=500;
- 但100MHz下200万次计数需21位计数器(2^21=2097152),综合后占用大量LUT,且易产生长路径时序违例。
正确做法:调用Vivado IP Catalog中的Clocking Wizard,生成MMCM/PLL IP核。参数设置:
- Input Clock Period: 10.000 ns(对应100MHz);
- Primary Output Clock: 200 MHz(用于高速计数);
- Secondary Output Clock: 200 kHz(用于数码管位选切换,200kHz周期=5μs,满足2ms每位要求);
- Phase Shift: 0°(无需相位调整)。
生成IP后,在顶层例化:
// 实例化PLL IP(文件名假设为clk_wiz_0) clk_wiz_0 uut_clk_wiz ( .clk_in1(clk_100m), // 100MHz输入 .clk_out1(clk_200m), // 200MHz输出,供计数器使用 .clk_out2(clk_200k) // 200kHz输出,供数码管扫描使用 );注意:
.xdc中只需约束clk_100m引脚,clk_200m/clk_200k是内部时钟,由Vivado自动推导时序路径,无需额外约束。若强行用create_clock约束,会导致时序分析错误。
3.2 按键消抖模块:20ms阈值不是玄学,是RC电路物理极限
文档中“按键操作后数值变化延迟≤200ms”看似宽松,但消抖时间必须精确控制在10~20ms。原因:机械按键触点弹跳时间典型值为5~15ms,小于10ms可能消抖不净,大于20ms则用户感知明显延迟。
采用两级同步+计数消抖法(比单纯打两拍更可靠):
module key_debounce ( input wire clk, // 200kHz扫描时钟,确保采样率足够 input wire rst_n, input wire key_in, // 原始按键信号(已接上拉,未按下为1) output reg key_pulse // 消抖后单脉冲,高电平持续1个clk周期 ); reg [19:0] cnt; // 20位计数器,200kHz下2^20≈5.36秒,远超需求 reg [1:0] key_sync; // 两级同步寄存器,消除亚稳态 reg key_db; // 消抖后稳定电平 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync <= 2'b11; // 复位时默认高电平(未按下) cnt <= 0; key_db <= 1'b1; key_pulse<= 1'b0; end else begin // 两级同步:key_in -> key_sync[0] -> key_sync[1] key_sync[0] <= key_in; key_sync[1] <= key_sync[0]; // 当同步后电平稳定,启动计数 if (key_sync[1] == key_db) begin cnt <= 0; end else if (cnt < 20'd19999) begin // 200kHz下20ms = 20000个周期 cnt <= cnt + 1; end else begin cnt <= 0; key_db <= key_sync[1]; // 计数满后更新稳定电平 end // 生成单脉冲:仅在key_db由0变1时输出高电平 key_pulse <= (key_db==0 && key_db!==1'b0) ? 1'b1 : 1'b0; // 注:此处用key_db!==1'b0检测上升沿,避免综合为锁存器 end end endmodule参数说明:
clk必须用200kHz而非100MHz:100MHz下20ms需2e6计数,资源浪费且易溢出;200kHz下仅需20000,20位计数器绰绰有余;key_pulse输出为单周期脉冲,可直接驱动计数器的en使能端,避免长按键导致多次计数;key_db!==1'b0中的!==是Verilog非阻塞比较,用于检测key_db从不定态变为0的瞬间,确保复位后首脉冲可靠。
3.3 4位BCD计数器:为什么不用二进制计数器?
文档要求“4位十进制计数器”,意味着显示范围0000~9999,而非0000~FFFF(65535)。若用16位二进制计数器,需额外逻辑判断9999后归零,但更优解是用4个独立的4位BCD计数器级联:
module bcd_counter ( input wire clk, input wire rst_n, input wire en, // 消抖后的单脉冲使能 output reg [15:0] bcd_out // 4位BCD,[15:12]=千位,[11:8]=百位... ); reg [3:0] cnt_ones, cnt_tens, cnt_hunds, cnt_thous; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_ones <= 4'h0; cnt_tens <= 4'h0; cnt_hunds <= 4'h0; cnt_thous <= 4'h0; end else if (en) begin // 个位:0~9循环 if (cnt_ones == 4'h9) begin cnt_ones <= 4'h0; // 个位进位,触发十位计数 if (cnt_tens == 4'h9) begin cnt_tens <= 4'h0; if (cnt_hunds == 4'h9) begin cnt_hunds <= 4'h0; if (cnt_thous == 4'h9) cnt_thous <= 4'h0; else cnt_thous <= cnt_thous + 1; end else cnt_hunds <= cnt_hunds + 1; end else cnt_tens <= cnt_tens + 1; end else cnt_ones <= cnt_ones + 1; end end assign bcd_out = {cnt_thous, cnt_hunds, cnt_tens, cnt_ones}; endmodule关键设计点:
- 每级计数器独立判断进位,避免单一16位计数器的复杂边界逻辑;
en信号来自消抖模块的key_pulse,确保每次按键只加1/减1,杜绝长按误触发;- 减法逻辑需单独实现(本例未展开),但原则相同:检测
cnt_ones==4'h0时借位,且需处理0000减1变为9999的循环。
3.4 数码管动态扫描模块:位选与段选的时序配合
动态扫描本质是“人眼视觉暂留”,需严格控制:
- 位选信号
seg_sel:4位轮流选中,每位持续时间≈2ms(总周期8ms,对应125Hz刷新率); - 段选信号
seg_data:在对应位选有效期间,输出该数字的7段编码。
module seg_scan ( input wire clk_200k, // 200kHz扫描时钟 input wire [15:0] bcd_in, // BCD输入,[15:12]千位... output reg [3:0] seg_sel, // 位选,低电平有效 output reg [6:0] seg_data // 段选,共阴极编码 ); reg [1:0] sel_cnt; // 2位计数器,00->01->10->11循环选4位 reg [15:0] bcd_reg; // 锁存BCD输入,避免扫描中变化 always @(posedge clk_200k or negedge rst_n) begin if (!rst_n) begin sel_cnt <= 2'b00; bcd_reg <= 0; end else begin bcd_reg <= bcd_in; sel_cnt <= sel_cnt + 1; end end // 位选译码:2'b00->4'b1110(选中第0位,即最右数码管) always @(*) begin case(sel_cnt) 2'b00: seg_sel = 4'b1110; // 位0有效 2'b01: seg_sel = 4'b1101; // 位1有效 2'b10: seg_sel = 4'b1011; // 位2有效 2'b11: seg_sel = 4'b0111; // 位3有效(最左) default: seg_sel = 4'b1110; endcase end // 段选译码:根据sel_cnt选择对应BCD位,查表输出7段码 always @(*) begin case(sel_cnt) 2'b00: begin // 显示个位 case(bcd_reg[3:0]) 4'h0: seg_data = 7'b1000000; // 0 4'h1: seg_data = 7'b1111001; // 1 4'h2: seg_data = 7'b0100100; // 2 4'h3: seg_data = 7'b0110000; // 3 4'h4: seg_data = 7'b0011001; // 4 4'h5: seg_data = 7'b0010010; // 5 4'h6: seg_data = 7'b0000010; // 6 4'h7: seg_data = 7'b1111000; // 7 4'h8: seg_data = 7'b0000000; // 8 4'h9: seg_data = 7'b0010000; // 9 default: seg_data = 7'b1111111; endcase end 2'b01: begin // 显示十位 case(bcd_reg[7:4]) // 同上,省略重复代码 endcase end 2'b10: begin // 显示百位 case(bcd_reg[11:8]) // 同上 endcase end 2'b11: begin // 显示千位 case(bcd_reg[15:12]) // 同上 endcase end endcase end endmodule注意:
seg_data必须用always @(*)组合逻辑实现,确保在位选信号变化后立即输出对应段码,避免因时序延迟导致某位显示错误数字。
4. 避坑指南:那些让数码管“装死”的5个致命细节
4.1 现象:下载后数码管全灭,按键无反应
原因:.xdc中rst_n引脚约束错误,或开发板复位按键实际为高电平有效,但代码中用了negedge rst_n。
解决:
- 用万用表测复位按键两端电压:未按下时若为3.3V,则为高电平有效,应改用
posedge rst_n; - 若文档明确写“复位按键连接FPGA的T17引脚,低电平复位”,则检查.xdc中是否遗漏
set_property PULLUP true [get_ports rst_n]——无上拉时,按键未按下引脚悬空,rst_n电平随机,导致FPGA处于不确定复位状态。
4.2 现象:数码管显示“8888”或“EEEE”,且不随按键变化
原因:seg_data段选信号全部为0(共阴极全灭)或全1(全亮),本质是BCD计数器未启动或bcd_out未连接至扫描模块。
解决:
- 在Vivado中打开Synthesized Design → Schematic,搜索
bcd_counter实例,双击查看bcd_out端口是否连出; - 检查顶层模块中
bcd_counter例化语句,确认bcd_out是否正确连接至seg_scan的bcd_in端口,常见笔误:bcd_out[15:0]写成bcd_out[15:1]导致高位截断。
4.3 现象:按键按一次,数码管跳2~3位
原因:消抖模块未生效,或key_pulse脉冲宽度超过1个时钟周期,导致计数器多次计数。
解决:
- 用ChipScope或ILA核抓取
key_in和key_pulse波形:若key_pulse持续多个周期,说明消抖计数器cnt未清零; - 检查消抖模块中
if (key_sync[1] == key_db) cnt <= 0;是否被else if分支覆盖,应确保该判断在always块最前执行。
4.4 现象:数码管某一位始终不亮,其他位正常
原因:该位对应的seg_sel引脚在.xdc中约束错误,或PCB走线断路。
解决:
- 查.xdc文件,确认该位引脚(如位3对应U14)是否拼写错误(常见:U14写成U41);
- 用万用表通断档测FPGA引脚与数码管位选焊盘是否导通,排除硬件虚焊。
4.5 现象:Vivado综合时报错“Multi-source in Unit”
原因:同一信号被多个assign或always块驱动,例如seg_data既在seg_scan模块中赋值,又在顶层用assign seg_data = 7'b1111111强制拉高。
解决:
- 全局搜索
seg_data,确认仅在seg_scan模块内被赋值; - 删除顶层所有对
seg_data/seg_sel的assign语句,它们必须由模块内部逻辑驱动。
5. 验证与调试:用三类波形抓取法定位90%的时序问题
5.1 第一类:关键时钟域交叉信号(推荐ILA核)
当key_pulse(200kHz域)驱动bcd_counter(200MHz域)时,跨时钟域易产生亚稳态。必须用ILA(Integrated Logic Analyzer)抓取:
clk_200m(200MHz主时钟);key_pulse(200kHz消抖脉冲);bcd_out(16位BCD输出);rst_n(复位信号)。
操作步骤:
- 在Vivado Block Design中添加ILA IP,勾选上述4个信号;
- 设置Trigger Setup:
key_pulse上升沿触发; - 下载bitstream后,在Hardware Manager中点击“Run Trigger”,观察
bcd_out是否在key_pulse上升沿后1~2个clk_200m周期内更新。若延迟超过5个周期,说明跨时钟域同步失败,需在key_pulse进入200MHz域前加两级寄存器同步。
5.2 第二类:数码管扫描时序(用示波器实测)
理论计算seg_sel切换周期应为2ms,但实际受综合布线影响可能偏差。用示波器测量:
- 探头接
seg_sel[0]引脚(对应最右数码管); - 设置示波器时基为1ms/div,触发方式为
seg_sel[0]下降沿; - 观察波形:理想为2ms高电平+2ms低电平方波(占空比50%),若高电平仅500μs,说明扫描过快,需调大
seg_scan中sel_cnt计数上限。
5.3 第三类:按键抖动实测(对比消抖前后)
将示波器探头分别接key_in(原始按键)和key_db(消抖后),对比波形:
key_in:按下瞬间出现密集毛刺(5~15ms宽);key_db:毛刺消失,仅在按键稳定后输出平滑电平;- 若
key_db仍有毛刺,说明消抖计数器时钟clk频率不足,需换用更高频时钟(如1MHz)重新计算计数阈值。
提示:ILA核占用BRAM资源,若综合报错“BRAM utilization >100%”,可删减抓取信号数量,优先保留
key_pulse和bcd_out。示波器测量时,务必先用万用表确认引脚电压为3.3V,避免误接高压损坏FPGA。
我带过几届学生做这个实验,最深的教训是:别信文档里“参考代码”四个字——它往往省略了.xdc约束细节、时钟域划分和复位电平定义,而这些恰恰是板子不亮的根源。现在我的习惯是,拿到任何FPGA实验文档,第一件事就是用Excel把引脚表、时钟要求、复位逻辑三列拆开,逐行对照.xdc文件;第二件事是打开Vivado的Timing Summary,盯着WNS(Worst Negative Slack)看,只要它是负数,数码管就不可能稳定。希望帮到你。
本文还有配套的精品资源,点击获取