ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FPGA数字时钟工程:Verilog模块化设计与ModelSim仿真验证

FPGA数字时钟工程:Verilog模块化设计与ModelSim仿真验证 简介一套完整的基于Verilog的FPGA数字时钟课程设计工程面向数字电路/EDA课程设计、FPGA入门学习者适用于需要完成时分秒显示、时间校准和闹钟功能的课设场景。整个电路结构按顶层、计时控制、数码管驱动、测试激励拆分功能模块清晰基于Quartus 17.1与Modelsim仿真环境可完整跑通设计并观察仿真波形。压缩包共78个文件大小约1.15MB核心是v格式的Verilog源码含顶层模块、计时控制模块、数码管驱动模块及测试激励同时提供qsf/qpf工程配置、do仿真脚本、png仿真图与README说明目录按源码、仿真、输出文件分层便于快速定位和二次修改。目前已有838人学习使用。直接打开工程即可复用或复现仿真结合RTL截图与模块注释能帮助理解数字钟的分频、计数、译码及按键校准逻辑适合作为课程设计、实验报告或FPGA综合练习的参考模板尤其适合需要提交完整工程源码的课程项目交付场景。1. 数字时钟在 FPGA 上比想象中更考验模块边界以前帮人看一个 FPGA 课设一个 always 块里从按键消抖写到七段译码仿真波形放大后全是毛刺。数字时钟几乎是 FPGA 入门绕不开的题目但大多数实现只是让它走起来一加时间校准、闹钟模块边界不清就把状态机改崩。这个工程正好是带校准和闹钟的版本顶层 TOP.v 只例化time_control.v 管计时display_ctrl.v 管数码管动态扫描三个模块互相不越界。平台是 Quartus 17.1 ModelSim仿真可用适合还在找 Verilog 工程模板的初学者也适合要把课程设计讲清楚、扛得住老师提问的在校生。2. 工程结构先读懂TOP 的握手线与三个模块的职责边界2.1 解压后的文件哪些值得留拿到工程先别急着双击 .qpf。解压后除了源码和测试文件其余都是 Quartus 编译产物。我一般先按这个表整理文件/目录作用是否可以删Digital_Clock.qpf/.qsf工程文件与 Quartus 配置否TOP.v顶层模块否time_control.v计时、校准、闹钟逻辑否display_ctrl.v数码管段选、位选驱动否tb_TOP.vModelSim 测试文件否db/incremental_db/综合和布局布线缓存是output_files/编译生成的报告/位流可选*.rpt*.summary资源占用和时序报告可选这种区分不只是洁癖。db 目录里存着旧网表和分区映射换台机器打开工程后路径变了旧缓存反而会触发Cant find database一类警告。我往课程设计里交文件前习惯把这些目录删掉重新完整编译一次能全绿再打包。2.2 为什么不能把计时和显示写在一个 always 里数字钟的显式特征是两个截然不同的时间尺度time_control 里每 50 MHz 才产生一次秒脉冲分钟和小时进位更慢display_ctrl 却要每毫秒扫描一位数码管。这两种节拍凑在一个 always 块里工具能综合但仿真时任何一处状态刷新都会影响另一个分支扫描位被秒进位打断会闪秒计数被扫描逻辑占用又会丢进位。用顶层把两者分开后time_control 输出的 BCD 时间对 display 来说就是一组缓慢变化的输入信号互补干扰。工程在顶层没有把三个校准按键全部拉出来常见接口是clk、rst_n、key_set、key_inc、alarm_en。我读这类工程习惯先画一张信号表比逐行读代码快信号方向宽度含义clkinput150 MHz 系统时钟rst_ninput1异步复位低有效key_setinput1进入校准并切换校准位key_incinput1校准位数值加一alarm_eninput1闹钟使能alarmoutput1闹钟触发输出sec/min/houroutput8BCD高四位十位低四位个位sec[3:0]和sec[7:4]这种编码方式在 time_control 和 display_ctrl 之间是统一的display 侧不需要再判断哪一段是十位直接按高低半字节取数即可。如果改动显示模块只要保持 8 位 BCD 格式不变顶层和其他模块都不需要动。2.3 TOP.v 的例化方式就是一份文档顶层文件本身几乎没逻辑只有模块例化和 wire 连接。看例化名就能知道数据的流向module TOP ( input clk, input rst_n, input key_set, input key_inc, input alarm_en, output alarm ); wire [7:0] sec, min, hour; time_control u_time ( .clk (clk), .rst_n (rst_n), .key_set (key_set), .key_inc (key_inc), .alarm_en (alarm_en), .alarm (alarm), .sec (sec), .min (min), .hour (hour) ); display_ctrl u_display ( .clk (clk), .rst_n (rst_n), .hour (hour), .min (min), .sec (sec) ); endmodule例化名u_time、u_display在 RTL 综合时不参与电路但 ModelSim 波形树里会用它们作为层次前缀起有语义的例化名能直接在\tb_TOP/dut/u_time/...下找到信号。这里的连接规则是time_control 是时间源display_ctrl 是时间使用者数据永远从前者单向流到后者。如果后面想加一个带闪烁的闹钟显示只需要在 display_ctrl 里读取alarm完全不用动 TOP。常见编译错Port sec not found in the namespace基本都是这里端口宽度或名称写得不一致。Quartus 对 Verilog 模块名大小写敏感time_control和Time_Control会被当成两个模块。3. time_control.v秒脉冲、BCD 自加与校准状态机3.1 分频输出脉冲而不是分频时钟数字钟的第一步是把 50 MHz 变成 1 Hz 的计数节拍。常见做法是定义一个 26 位计数器计满50_000_000 - 1就清零并给出一个周期宽度的高脉冲而不是直接把分频后的 1 Hz 接到另一个模块的时钟上。localparam CNT_1S 50_000_000; reg [25:0] cnt; reg clk_1s; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 26d0; clk_1s 1b0; end else if (cnt CNT_1S - 1) begin cnt 26d0; clk_1s 1b1; end else begin cnt cnt 26d1; clk_1s 1b0; end endclk_1s只在归零那个时钟周期为高其他时间保持低。这样整个设计始终只有一个时钟域秒、分、时都在posedge clk下工作不会产生跨时钟域的亚稳态问题。把分频输出直接接到另一个always (posedge clk_1s)是不少入门工程的写法仿真能过上板后由于占空比和相位偏移容易在秒和分钟进位交界处出现毛刺。CNT_1S在仿真时可以临时改成 2跑完验证再改回来这是最省时间的调参手段。注意仿真验证时可以把CNT_1S从 50_000_000 改成 2但上板前一定要改回去否则数字钟一秒走千万次。3.2 用 BCD 表示时间让进位直接可见工程里时间不是二进制计数而是每个十进制位占 4 bitsec[7:4]是秒十位sec[3:0]是秒个位。这种编码的优点是分凌晨、显示段码时不需要二进制转 BCD缺点是进位判断要多写几个比较条件。下面是秒的自加核心always (posedge clk or negedge rst_n) begin if (!rst_n) begin sec 8h00; end else if (clk_1s) begin if (sec[3:0] 4d9) begin sec[3:0] 4d0; if (sec[7:4] 4d5) begin sec[7:4] 4d0; // 这里产生一个 add_min 脉冲用来让 min 加一 end else sec[7:4] sec[7:4] 4d1; end else sec[3:0] sec[3:0] 4d1; end endsec在一个时钟周期内只改一个半字节先判断个位是不是 9再判断十位是不是 5两层嵌套对应59 - 00并进位。分钟和小时是完全对称的写法差别只在小时进位条件是hour 8h23时整体回到8h00也就是 24 小时制。这里有个容易踩的坑如果直接在else if (sec 8h59)里判断会在sec 8h59那个时钟周期把秒清零看起来是对的但分钟加一信号和秒清零发生在同一拍如果分钟清零又用min 8h59触发小时加一三级进位就会在同一拍完成综合后路径变长时序余量不足。常见做法是把秒进位到分的脉冲用一个独立 reg 存一拍下一拍再让 min 加一让每个进位沿都错开。3.3 校准按键和闹钟的工程处理校准功能在仿真里最容易写飘。按键不是直接当电平用而是先经过消抖和边沿检测产生一个单周期脉冲key_set_pulse状态机每收到一个脉冲才跳一次档位reg [1:0] mode; localparam IDLE 2b00, SET_SEC 2b01, SET_MIN 2b10, SET_HOUR 2b11; always (posedge clk or negedge rst_n) begin if (!rst_n) mode IDLE; else if (key_set_pulse) case (mode) IDLE : mode SET_SEC; SET_SEC : mode SET_MIN; SET_MIN : mode SET_HOUR; SET_HOUR: mode IDLE; default : mode IDLE; endcase endmode IDLE时正常计时进入SET_SEC后秒清零开始重新计数。key_inc_pulse只在非 IDLE 状态使能且每个脉冲只能让当前选定单位加一。如果直接在always (posedge clk)里判key_set电平仿真中一个 10 ns 的毛刺也会让状态跳多档。mode校准对象说明IDLE无正常计时SET_SEC秒秒清零重新计数SET_MIN分key_inc脉冲使分加一SET_HOUR时key_inc脉冲使时加一闹钟部分不需要单独计数只需要在每拍比较当前hour/min和预设闹钟值同时判断alarm_en。比较相等并允许触发时输出alarm高电平如果希望闹钟响一段时间可以在alarm拉高后启动一个持续计数达到设定秒数再自动关闭工程里常见的是由外部按键清掉。仿真验证闹钟时最快的方式是通过 force 命令直接把time_control里的hour/min改成与闹钟预设相等看一个时钟周期内alarm是否拉高而不是傻等。4. display_ctrl.v 动态扫描与 Quartus 工程配置4.1 扫描频率、位选和段码的配合六个数码管共用一组段码线同一时刻只能驱动一位。display_ctrl 的做法是用一个扫描计数器产生scan_pos0 到 5 的循环每到一位把那个位的 BCD 值送到段码译码同时选通对应的位选。刷新率是这类模块最重要的指标。分频目标计数常数结果单管停留 1 ms50_000 - 1一轮 6 ms刷新率约 166 Hz单管停留 2 ms100_000 - 1一轮 12 ms刷新率约 83 Hz单管停留 0.5 ms25_000 - 1一轮 3 ms刷新率约 333 Hz单管停留时间太短数码管点亮时间占比低亮度会发暗太长则刷新率跌破 60 Hz人眼能看出闪动。课程设计的板子上1 ms 是最妥协的值。reg [15:0] scan_cnt; reg [2:0] scan_pos; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt 16d0; scan_pos 3d0; end else if (scan_cnt 16d50_000 - 1) begin scan_cnt 16d0; if (scan_pos 3d5) scan_pos 3d0; else scan_pos scan_pos 3d1; end else scan_cnt scan_cnt 16d1; endscan_pos的 bit 宽是 3因为要覆盖 0~5 六个位置scan_cnt到50_000 - 150 MHz 对应 1 ms。随后在组合逻辑里根据scan_pos选出当前 BCD 值再用另一个always (*)做 4 bit 到 7 段码的查表。段码表不是固定的共阳极和共阴极对同一数字的编码刚好取反所以移植到新板子时先确认数码管极性再抄段码表。位选一般用低有效例如dig_sel在scan_pos 3d0时为6b111110只有第 0 位为低其他位全高这就让当前位被点亮其他位全部熄灭。这里的 6 位、低有效和前面 BCD 高半字节一样在工程内部是约定仿真里看不出发光效果只能看到dig_sel依次循环移位。4.2 在 Quartus 17.1 里重新建立工程拿到这个源码在另一台机器上重建工程步骤不多File - New Project Wizard工程名填Digital_Clock顶层设为TOP。Add Files 中加入TOP.v、time_control.v、display_ctrl.v这一步不要加tb_TOP.v测试文件本来就不参与 RTL 综合。器件选择 Cyclone IV 或 MAX 10 中手头有的一块仿真版本选哪个都行。Processing - Start Compilation观察Flow Summary里的逻辑单元占用。这个数字钟整块设计占 LE/Q 资源一般在 200 个以下。Quartus 有一个容易忽略的细节qsf 会记住源码文件的绝对路径。换电脑或者把 zip 解压到别的目录后即使芯片选择不变编译也可能报File cant be found。检查 qsf 里set_global_assignment -name VERILOG_FILE那一行的路径或者直接通过 Project - Add/Remove Files in Project 重新添加源码。4.3 上板需要的引脚约束与常见编译问题这个工程本身是仿真版本没有做管脚分配因此 RTL 仿真看的是 BCD 和位选脉冲而不是真实数码管亮度。上板前在 Assignment Editor 里至少要做四组约束系统时钟引脚、复位按键、位选dig_sel[5:0]、段选seg_data[7:0]。clk的 I/O 标准选 3.3-V LVTTL和板载晶振一致否则时序约束会卡在时钟输入上。如果编译后报Error (12007): Top-level design entity TOP is not defined先看 qsf 中顶层实体是TOP还是Digital_Clock顶层实体名必须和例化名字严格一致。如果报时钟相移之类的问题检查是否把 display_ctrl 的扫描时钟和 time_control 的秒脉冲混在同一个always敏感列表里了实际工程两者都应该只在系统时钟posedge下工作。5. ModelSim 仿真验证tb_TOP 与排错5.1 测试文件的三个任务tb_TOP.v 要做的事很简单给 20 ns 周期的时钟先复位 30 ns再模拟一次key_set和key_inc按键脉冲。关键代码可以简写成这样timescale 1ns/1ps module tb_TOP; reg clk, rst_n, key_set, key_inc, alarm_en; wire alarm; wire [7:0] sec, min, hour; TOP dut ( .clk(clk), .rst_n(rst_n), .key_set(key_set), .key_inc(key_inc), .alarm_en(alarm_en), .alarm(alarm), .sec(sec), .min(min), .hour(hour) ); always #10 clk ~clk; initial begin clk 0; rst_n 0; key_set 0; key_inc 0; alarm_en 0; #30 rst_n 1; #100 key_set 1; #20 key_set 0; #40 key_inc 1; #20 key_inc 0; #500 $finish; end endmodule#10反转一次 clk 产生 50 MHz 时钟#20的按键脉冲宽度有 20 ns约一个时钟周期足以被 key_set_pulse 的边沿检测捕获。如果测试校准后sec没有变化先看复位释放时间是 30 ns是否早于第一个时钟上升沿建议把复位拉到 100 ns 再释放让仿真器稳定。5.2 波形检查清单在 ModelSim 的 wave 窗口添加dut/u_time/sec、dut/u_time/min、dut/u_time/hour和dut/u_display/dig_sel。重点确认三件事秒个位从 0 到 9 循环秒十位在个位回 0 后加一分和时只在边界处变化并且进位脉冲错开一个时钟周期dig_sel的低位循环顺序为 0 到 5每格停留时间相同。校准模式下每按一次key_inc当前目标位只加一不能出现连续加多。闹钟可以用 force 命令把hour/min强制设成预设值观察alarm在使能拉高后一个时钟内响应。5.3 调不起 ModelSim 的常规检查顺序在 Quartus 中启动 RTL Simulation 报Cant launch ModelSim-Altera时按顺序排查Tools - Options - EDA Tool Options 中 ModelSim-Altera 路径指向安装根目录而不是 bin工程路径和用户目录不能带中文或空格tb_TOP.v 必须通过 Assignments - Settings - EDA Tool Settings - Simulation 设置为顶层 testbench否则报Cannot find module tb_TOP。如果以上三处都核对完还没起来把 ModelSim transcript 窗口里的输出直接搜fatal报错会精确到是哪一行 tb 端口写错。本文还有配套的精品资源点击获取
返回列表