Vivado 2019.2纯Verilog双模时钟:记时+秒表共存设计
简介本资源是一份基于Vivado 2019.2平台、采用纯Verilog语言实现的FPGA数字时钟设计工程面向数字电路初学者与FPGA入门开发者解决基础时序逻辑建模、多模态计时实时钟秒表功能集成及硬件部署全流程实践问题。压缩包含86个文件涵盖16个.rst综合报告、15个.pb物理实现文件、11个.rpt时序分析报告、5个.dcp设计检查点、4个.sh脚本及关键源码display.v和顶层工程文件project_7.xpr等完整呈现从RTL编写、综合、实现到比特流生成的标准化开发链路总大小1.52MB。已有2788人学习下载。读者可直接导入Vivado复现全部功能获得带约束文件.xdc、可运行仿真project_7.sim、已验证时序rpt报告与完整目录结构的开箱即用工程尤其适合掌握时钟分频、BCD计数、进位控制与显示驱动协同设计的实战训练。1. 在 Vivado 2019.2 中用纯 Verilog 实现双模数字时钟记时与秒表共存于同一 FPGA 物理资源你手头只有一块 Basys3 或 Nexys4 DDR 开发板没用任何 IP 核、不调用 Xilinx 的 Clocking Wizard、不依赖 Block Design 图形界面——仅靠.v文件和约束文件就能让数码管或七段显示同时跑两个独立计时器一个走标准 24 小时制的系统时间记时另一个支持启停/复位/分段的高精度毫秒级秒表。这不是教学 Demo而是真实工程中常被忽略的「状态隔离」问题两个计时逻辑共享同一 100MHz 板载时钟但必须互不干扰、可异步切换、且在按键抖动下保持状态稳定。Vivado 2019.2 是当前高校课程与中小 FPGA 项目最广泛部署的稳定版本其综合器对always (posedge clk)块内多级 case 分支、异步复位推断、以及reg [31:0]计数器的寄存器映射行为已有明确规范。本文面向已掌握基础 Verilog 语法、能写计数器和译码器但尚未在真实约束下完成多模式时序协同的工程师——重点不是“怎么显示时间”而是“如何让记时和秒表在同一个 always 块里不抢资源、不漏脉冲、不因按键毛刺跳变”。2. 从顶层模块拆解双模时钟的信号流与状态机设计2.1 为什么必须用 Mealy 型状态机而非简单 if-else 切换很多初学者会写类似if(mode b0) begin ... end else begin ... end的结构但这在 Vivado 2019.2 综合时极易导致锁存器latch推断尤其当mode由按键同步后未做完整赋值覆盖时。更可靠的做法是将「记时」与「秒表」抽象为两个正交状态域并通过顶层状态机统一调度。我们定义主状态机main_state为 3-bit其中IDLE: 系统初始化所有计数器清零显示默认值CLOCK_RUN: 记时模式运行中每秒更新hour/min/secSTOPWATCH_RUN: 秒表模式运行中每 10ms 更新min/sec/ms对应 100MHz → 分频得 100Hz → 再分频得 10msSTOPWATCH_PAUSE: 秒表暂停保留当前值允许继续启动STOPWATCH_LAP: 暂存当前秒表值用于分段计时不重置提示Vivado 2019.2 默认启用-no_latch综合选项但若代码中存在未覆盖分支仍可能在 Implementation 阶段报WARNING: [Synth 8-6159]。务必保证每个case分支下next_state和所有输出寄存器均有显式赋值。2.2 顶层模块接口定义与物理约束映射// top_clock.v module top_clock ( input wire clk_100m, // 板载 100MHz 时钟Basys3 为 clk100mhz input wire btn_rst, // 硬件复位按钮低电平有效 input wire btn_mode, // 模式切换短按切记时/秒表长按进设置 input wire btn_start, // 秒表启停仅在秒表模式下生效 input wire btn_lap, // 秒表分段仅在秒表运行/暂停时有效 output reg [3:0] seg_sel, // 7段数码管位选共阴4位 output reg [7:0] seg_data // 段码a~g dp );对应 Basys3 的 XDC 约束文件关键行Vivado 2019.2 要求使用set_property显式声明 I/O 标准# basys3.xdc set_property PACKAGE_PIN E18 [get_ports clk_100m] set_property IOSTANDARD LVCMOS33 [get_ports clk_100m] set_property PACKAGE_PIN U18 [get_ports btn_rst] set_property IOSTANDARD LVCMOS33 [get_ports btn_rst] set_property PULLUP true [get_ports btn_rst] # 外部上拉按下为低 set_property PACKAGE_PIN U17 [get_ports btn_mode] set_property IOSTANDARD LVCMOS33 [get_ports btn_mode] set_property PULLUP true [get_ports btn_mode] # 其余按键同理... set_property PACKAGE_PIN T17 [get_ports btn_start] set_property PACKAGE_PIN T18 [get_ports btn_lap] # 数码管位选与段码 set_property PACKAGE_PIN T10 [get_ports seg_sel[0]] set_property PACKAGE_PIN R10 [get_ports seg_sel[1]] set_property PACKAGE_PIN K16 [get_ports seg_sel[2]] set_property PACKAGE_PIN K14 [get_ports seg_sel[3]] set_property IOSTANDARD LVCMOS33 [get_ports seg_sel] set_property PACKAGE_PIN M17 [get_ports seg_data[0]] # a set_property PACKAGE_PIN N17 [get_ports seg_data[1]] # b set_property PACKAGE_PIN P18 [get_ports seg_data[2]] # c set_property PACKAGE_PIN P17 [get_ports seg_data[3]] # d set_property PACKAGE_PIN M18 [get_ports seg_data[4]] # e set_property PACKAGE_PIN N18 [get_ports seg_data[5]] # f set_property PACKAGE_PIN P14 [get_ports seg_data[6]] # g set_property PACKAGE_PIN T14 [get_ports seg_data[7]] # dp注意Vivado 2019.2 对PULLUP属性支持稳定但若使用 Artix-7 以外器件如 Kintex需确认PULLUP是否被综合器识别。实测 Basys3Artix-7 XC7A35T无兼容性问题。2.3 双计时器核心独立分频链与状态同步机制记时与秒表对时间精度要求不同记时只需秒级更新1Hz而秒表需 10ms 级响应100Hz。若共用同一分频器会导致秒表分辨率不足若各自分频则需解决跨时钟域采样问题。正确做法是统一用 100MHz 主时钟驱动两个独立计数器但通过使能信号enable控制更新节奏。// 分频使能生成放在 clock_gen.v 中 reg [26:0] cnt_1s; // 100MHz → 1Hz100_000_000 - 1 27bit 计数器 reg en_1s; reg [19:0] cnt_10ms; // 100MHz → 100Hz1_000_000 - 1 20bit reg en_100hz; always (posedge clk_100m or negedge btn_rst) begin if (!btn_rst) begin cnt_1s 0; en_1s 0; cnt_10ms 0; en_100hz 0; end else begin if (cnt_1s 27d99999999) begin cnt_1s 0; en_1s 1; end else begin cnt_1s cnt_1s 1; en_1s 0; end if (cnt_10ms 20d999999) begin cnt_10ms 0; en_100hz 1; end else begin cnt_10ms cnt_10ms 1; en_100hz 0; end end end关键点在于en_1s和en_100hz是单周期脉冲仅在对应计数值溢出时拉高 1 个clk_100m周期。这避免了在always (posedge clk_100m)块中直接用if(cntMAX)导致的组合逻辑延迟累积问题。Vivado 2019.2 综合器能准确识别此类脉冲生成模式并映射为高效计数器比较器结构。3. 实现记时与秒表的独立计数逻辑及按键消抖3.1 记时模块24 小时制 BCD 编码计数器记时逻辑必须满足秒0–59 循环进位到分分0–59 循环进位到时时0–23 循环次日归零所有字段以 BCD 存储非二进制便于后续七段译码// clock_counter.v module clock_counter ( input wire clk, input wire rst_n, input wire en_1s, // 1Hz 使能 output reg [7:0] hour_bcd, // BCD: [7:4]十位, [3:0]个位 output reg [7:0] min_bcd, output reg [7:0] sec_bcd ); reg [5:0] sec_cnt, min_cnt, hour_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sec_cnt 0; min_cnt 0; hour_cnt 0; {hour_bcd, min_bcd, sec_bcd} 0; end else if (en_1s) begin // 秒计数0–59 if (sec_cnt 6d59) begin sec_cnt 0; // 进位到分 if (min_cnt 6d59) begin min_cnt 0; // 进位到时 if (hour_cnt 6d23) hour_cnt 0; else hour_cnt hour_cnt 1; end else begin min_cnt min_cnt 1; end end else begin sec_cnt sec_cnt 1; end // BCD 转换实时计算不存中间二进制 sec_bcd {4b0, sec_cnt[5:4]} * 4d10 {4b0, sec_cnt[3:0]}; min_bcd {4b0, min_cnt[5:4]} * 4d10 {4b0, min_cnt[3:0]}; hour_bcd {4b0, hour_cnt[5:4]} * 4d10 {4b0, hour_cnt[3:0]}; end end endmodule说明此处sec_bcd ...使用组合逻辑实时转换避免额外寄存器开销。Vivado 2019.2 综合器会将其优化为查找表LUT实现比存储 BCD 状态再查表更省资源。* 4d10被综合为移位加法3 1无乘法器占用。3.2 秒表模块毫秒级精度与分段存储秒表需支持毫秒计数0–9993 位十进制秒0–59分0–59LAP 值缓存最多保存 1 组历史值可扩展为 FIFO// stopwatch_counter.v module stopwatch_counter ( input wire clk, input wire rst_n, input wire en_100hz, // 10ms 使能即 100Hz input wire start_en, // 启动/暂停使能同步后 input wire lap_en, // 分段捕获使能 output reg [7:0] min_bcd, output reg [7:0] sec_bcd, output reg [11:0] ms_bcd, // 12bit[11:8]百, [7:4]十, [3:0]个 output reg [7:0] lap_min_bcd, output reg [7:0] lap_sec_bcd, output reg [11:0] lap_ms_bcd ); reg [9:0] ms_cnt, sec_cnt, min_cnt; // 毫秒计数器0–999 → 10bit reg [5:0] sec_cnt_reg, min_cnt_reg; reg [9:0] ms_cnt_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ms_cnt 0; sec_cnt 0; min_cnt 0; ms_cnt_reg 0; sec_cnt_reg 0; min_cnt_reg 0; {min_bcd, sec_bcd, ms_bcd} 0; {lap_min_bcd, lap_sec_bcd, lap_ms_bcd} 0; end else if (start_en) begin if (ms_cnt 10d999) begin ms_cnt 0; if (sec_cnt 6d59) begin sec_cnt 0; if (min_cnt 6d59) min_cnt 0; else min_cnt min_cnt 1; end else begin sec_cnt sec_cnt 1; end end else begin ms_cnt ms_cnt 1; end // 锁存当前值到寄存器供 BCD 转换用 ms_cnt_reg ms_cnt; sec_cnt_reg sec_cnt; min_cnt_reg min_cnt; end end // BCD 转换毫秒三位独立计算 always (*) begin ms_bcd[11:8] (ms_cnt_reg / 100) % 10; // 百位 ms_bcd[7:4] (ms_cnt_reg / 10) % 10; // 十位 ms_bcd[3:0] ms_cnt_reg % 10; // 个位 end always (*) begin sec_bcd {4b0, sec_cnt_reg[5:4]} * 4d10 {4b0, sec_cnt_reg[3:0]}; min_bcd {4b0, min_cnt_reg[5:4]} * 4d10 {4b0, min_cnt_reg[3:0]}; end // LAP 捕获在 lap_en 上升沿锁存 reg lap_en_dly; always (posedge clk) lap_en_dly lap_en; always (posedge clk) begin if (lap_en !lap_en_dly) begin // 检测上升沿 lap_ms_bcd ms_bcd; lap_sec_bcd sec_bcd; lap_min_bcd min_bcd; end end endmodule注意lap_en必须先经两级寄存器同步lap_en_dly否则跨时钟域采样会导致亚稳态。这是 Vivado 2019.2 中处理异步按键的标准实践不可省略。3.3 按键消抖模块基于计数器的硬件级防抖所有按键btn_mode,btn_start,btn_lap均需消抖。软件延时不可靠必须用硬件计数器实现 ≥20ms 滤波// key_debounce.v module key_debounce ( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [19:0] cnt; reg key_sync0, key_sync1; // 同步输入两级寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 1; key_sync1 1; end else begin key_sync0 key_in; key_sync1 key_sync0; end end // 消抖计数器检测稳定低电平 ≥20ms100MHz 下为 2_000_000 个周期 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; key_out 1; end else if (key_sync1 0) begin // 按下时开始计数 if (cnt 20d1999999) begin cnt 0; key_out 0; // 确认按下 end else begin cnt cnt 1; end end else begin // 释放时清零并输出高 cnt 0; key_out 1; end end endmodule参数说明20d1999999对应 20ms100MHz × 0.02s 2,000,000减 1 是因为计数从 0 开始。该模块输出key_out为干净的单周期低脉冲可直接接入状态机。4. 数码管动态扫描与双模显示切换逻辑4.1 四位共阴数码管扫描时序控制Basys3 的 4 位数码管采用动态扫描需以 ≥100Hz 频率轮询每位否则会出现闪烁。我们复用en_100hz10ms 周期作为扫描使能// seg_scan.v reg [1:0] scan_cnt; // 0→1→2→3→0... reg [3:0] seg_sel_reg; always (posedge clk_100m or negedge btn_rst) begin if (!btn_rst) begin scan_cnt 0; seg_sel_reg 4b1110; // 初始选第 0 位最低位 end else if (en_100hz) begin case (scan_cnt) 2b00: begin seg_sel_reg 4b1110; scan_cnt 2b01; end 2b01: begin seg_sel_reg 4b1101; scan_cnt 2b10; end 2b10: begin seg_sel_reg 4b1011; scan_cnt 2b11; end 2b11: begin seg_sel_reg 4b0111; scan_cnt 2b00; end endcase end end assign seg_sel seg_sel_reg;4.2 双模数据路由与七段译码根据main_state选择显示源CLOCK_RUN时显示hour_bcd/min_bcd/sec_bcdSTOPWATCH_RUN/PAUSE时显示min_bcd/sec_bcd/ms_bcd毫秒占两位秒表显示为 MM:SS:mmSTOPWATCH_LAP时显示lap_min_bcd/lap_sec_bcd/lap_ms_bcd。// 数据多路选择在顶层模块中 wire [7:0] disp_hour, disp_min, disp_sec, disp_ms; wire [11:0] disp_ms_full; // 记时模式HH:MM:SS → 6 位前两位为小时00–23 // 秒表模式MM:SS:mm → 6 位毫秒占最后两位00–99 // 故统一取 6 位 BCD 数据{disp[23:20], disp[19:16], disp[15:12], disp[11:8], disp[7:4], disp[3:0]} // 对应数码管位[3][2][1][0] → 显示为 [H1][H0]:[M1][M0]:[S1][S0] 或 [M1][M0]:[S1][S0]:[m1][m0] always (*) begin case (main_state) CLOCK_RUN: begin disp_hour hour_bcd; disp_min min_bcd; disp_sec sec_bcd; disp_ms_full 12h000; // 不显示毫秒 end STOPWATCH_RUN, STOPWATCH_PAUSE, STOPWATCH_LAP: begin if (main_state STOPWATCH_LAP) begin disp_min lap_min_bcd; disp_sec lap_sec_bcd; disp_ms_full lap_ms_bcd; end else begin disp_min sw_min_bcd; disp_sec sw_sec_bcd; disp_ms_full sw_ms_bcd; end end default: begin disp_hour 8h23; disp_min 8h59; disp_sec 8h59; disp_ms_full 12h000; end endcase end // 拆分为 6 个 4bit BCD 字段 wire [3:0] bcd0 (scan_cnt 2b00) ? disp_hour[3:0] : (scan_cnt 2b01) ? disp_min[3:0] : (scan_cnt 2b10) ? disp_sec[3:0] : disp_ms_full[3:0]; // 个位毫秒 wire [3:0] bcd1 (scan_cnt 2b00) ? disp_hour[7:4] : (scan_cnt 2b01) ? disp_min[7:4] : (scan_cnt 2b10) ? disp_sec[7:4] : disp_ms_full[7:4]; // 十位毫秒 wire [3:0] bcd2 (scan_cnt 2b00) ? 4h0 : // 小时十位仅一位高位补 0 (scan_cnt 2b01) ? disp_hour[3:0] : // 分个位 (scan_cnt 2b10) ? disp_min[3:0] : // 秒个位 disp_sec[3:0]; // 毫秒百位 wire [3:0] bcd3 4h0; // 未使用置 0 // 七段译码共阴 function [7:0] seg_decode; input [3:0] bcd; begin case (bcd) 4h0: seg_decode 8b11000000; // 0 4h1: seg_decode 8b11111001; // 1 4h2: seg_decode 8b10100100; // 2 4h3: seg_decode 8b10110000; // 3 4h4: seg_decode 8b10011001; // 4 4h5: seg_decode 8b10010010; // 5 4h6: seg_decode 8b10000010; // 6 4h7: seg_decode 8b11111000; // 7 4h8: seg_decode 8b10000000; // 8 4h9: seg_decode 8b10010000; // 9 default: seg_decode 8b11111111; endcase end endfunction assign seg_data seg_decode({bcd3,bcd2,bcd1,bcd0});关键细节seg_data是组合逻辑输出必须确保bcdx在scan_cnt变化后稳定建立。Vivado 2019.2 的静态时序分析STA会自动检查此路径若出现hold violation可在seg_data前加一级寄存器reg [7:0] seg_data_r但 Basys3 100MHz 下通常无需。5. Vivado 2019.2 工程构建、综合与关键排错技巧5.1 创建工程与添加文件的命令行等效操作虽然多数人用 GUI但掌握 Tcl 命令对批量构建和 CI/CD 至关重要。在 Vivado 2019.2 Tcl Console 中执行# 创建工程 create_project top_clock ./top_clock -part xc7a35tcpg236-1 # 添加源文件Verilog add_files -fileset sources_1 ./src/top_clock.v add_files -fileset sources_1 ./src/clock_counter.v add_files -fileset sources_1 ./src/stopwatch_counter.v add_files -fileset sources_1 ./src/key_debounce.v add_files -fileset sources_1 ./src/seg_scan.v # 添加约束文件 add_files -fileset constrs_1 ./constrs/basys3.xdc # 设置顶层模块 set_property top top_clock [current_fileset] # 启动综合等效 GUI 中的 Run Synthesis launch_runs synth_1 wait_on_run synth_1 # 启动实现 launch_runs impl_1 wait_on_run impl_1 # 生成比特流 launch_runs impl_1 -to_step write_bitstream wait_on_run impl_1提示Vivado 2019.2 的wait_on_run命令会阻塞直到任务完成适合脚本化。若需后台运行改用launch_runs -jobs 4 synth_1并配合get_runs查询状态。5.2 三类高频报错及定位方法报错类型Vivado 2019.2 典型提示定位方法解决方案未连接时钟网[Place 30-600] This design contains 1 high-fanout net...在Synthesis后打开Open Synthesized Design→Report Clock Networks检查clk_100m是否被create_clock约束若未约束Vivado 会尝试自动推断但 Basys3 必须手动指定create_clock -period 10.000 -name clk_100m -waveform {0 5} [get_ports clk_100m]异步复位推断失败[Synth 8-439] Failed to infer register for ... due to asynchronous reset查看Synthesis报告中的Critical Warning区域确保所有always (posedge clk or negedge rst_n)块中rst_n仅出现在if(!rst_n)分支且该分支内所有寄存器均有赋值包括next_state数码管显示错位下载后某位常亮/全暗/乱码在Hardware Manager中打开VIO核监测seg_sel和seg_data实时波形用ILA插入点右键seg_sel→Set as Debug重新综合下载后在Waveform窗口观察seg_sel是否按 0→1→2→3 循环seg_data是否随scan_cnt正确变化5.3 使用 ILA 实时观测秒表计数器状态ILAIntegrated Logic Analyzer是 Vivado 2019.2 调试时序逻辑的利器。在top_clock.v中插入探针(* mark_debug true *) reg [9:0] ila_sw_ms_cnt; (* mark_debug true *) reg [5:0] ila_sw_sec_cnt; (* mark_debug true *) reg [5:0] ila_sw_min_cnt; (* mark_debug true *) reg [2:0] ila_main_state; always (posedge clk_100m) begin ila_sw_ms_cnt sw_ms_cnt; ila_sw_sec_cnt sw_sec_cnt; ila_sw_min_cnt sw_min_cnt; ila_main_state main_state; end然后在 Tcl Console 执行# 自动识别 debug nets refresh_device -update_hw_probes # 创建 ILA core采样深度 1024 create_debug_core u_ila_1 ila set_property C_DATA_DEPTH 1024 [get_debug_cores u_ila_1] set_property C_TRIG0_SIZE 1 [get_debug_cores u_ila_1] # 将信号绑定到探针 connect_debug_port u_ila_1/Probe0 [get_nets ila_sw_ms_cnt] connect_debug_port u_ila_1/Probe1 [get_nets ila_sw_sec_cnt] connect_debug_port u_ila_1/Probe2 [get_nets ila_sw_min_cnt] connect_debug_port u_ila_1/Probe3 [get_nets ila_main_state] # 重新实现并生成比特流 launch_runs impl_1 -to_step write_bitstream下载后在Hardware Manager→Open Hardware Analyzer→u_ila_1中设置触发条件如ila_main_state 3b011即STOPWATCH_RUN即可捕获秒表运行时毫秒计数器的精确跳变沿验证en_100hz是否每 10ms 准确触发一次。最后一行技术内容在stopwatch_counter.v中若将ms_cnt改为reg [10:0]11bit并设上限为11d1999则可实现 20ms 分辨率秒表——此时需同步修改en_100hz生成逻辑为cnt_10ms 20d1999999→en_20ms并在显示逻辑中丢弃个位毫秒仅取十位与百位构成两位毫秒显示。本文还有配套的精品资源点击获取