FPGA新手必看:用Verilog在Vivado里从零撸一个带按键调时的数字时钟
FPGA新手实战用Verilog在Vivado中构建可调时数字时钟第一次接触FPGA开发时最令人兴奋的莫过于将代码转化为硬件电路的实际效果。数字时钟作为经典的入门项目不仅能巩固Verilog语法基础还能让你完整体验从设计到烧录的全流程。本文将手把手带你用Xilinx Vivado工具链实现一个带按键调时功能的数字时钟特别适合已经学过Verilog基础但尚未完成过完整项目的开发者。1. 开发环境准备与工程创建在开始编码前我们需要准备好开发环境。Xilinx Vivado是当前主流的FPGA开发工具支持从设计到烧录的全流程。建议使用2019.1或更新版本这些版本对Basys3等常见开发板有更好的支持。安装完成后启动Vivado并选择Create Project指定项目名称如Digital_Clock和存储路径选择RTL Project类型添加Verilog源文件可先创建空文件选择目标设备型号如Basys3开发板对应xc7a35tcpg236-1提示如果找不到确切型号选择相同系列芯片即可但部分引脚功能可能需要调整创建工程后建议立即设置仿真工具。Vivado自带的仿真器XSim足够应付本项目但如果你习惯使用ModelSim需要在Tools → Options中配置仿真器路径。2. 时钟模块设计与分频实现数字时钟的核心是精确的计时功能。我们首先需要将开发板提供的高频时钟如Basys3的100MHz分频为1Hz信号。以下是分频模块的关键代码module clock_divider( input clk_100MHz, input rst_n, output reg clk_1Hz ); reg [26:0] counter; always (posedge clk_100MHz or negedge rst_n) begin if (!rst_n) begin counter 0; clk_1Hz 0; end else if (counter 50_000_000 - 1) begin counter 0; clk_1Hz ~clk_1Hz; end else begin counter counter 1; end end endmodule这段代码实现了将100MHz时钟分频为1Hz信号的功能。需要注意分频系数计算100MHz → 1Hz需要50,000,000次计数每个1Hz周期包含50M个原时钟周期非阻塞赋值()确保时序逻辑正确性异步复位设计保证系统可初始化3. 计时逻辑与按键调时实现有了1Hz时钟信号后我们可以构建计时逻辑。完整的数字时钟需要处理时、分、秒的进位关系同时支持通过按键调整时间。以下是核心计时模块module digital_clock( input clk_1Hz, input rst_n, input hour_adj, input minute_adj, input second_adj, output reg [4:0] hour, output reg [5:0] minute, output reg [5:0] second ); // 按键消抖处理 reg [19:0] hour_adj_db, minute_adj_db, second_adj_db; wire hour_pulse, minute_pulse, second_pulse; // 消抖逻辑以hour_adj为例 always (posedge clk_1Hz) begin hour_adj_db {hour_adj_db[18:0], hour_adj}; minute_adj_db {minute_adj_db[18:0], minute_adj}; second_adj_db {second_adj_db[18:0], second_adj}; end assign hour_pulse hour_adj_db; assign minute_pulse minute_adj_db; assign second_pulse second_adj_db; // 秒计时逻辑 always (posedge clk_1Hz or negedge rst_n) begin if (!rst_n) second 0; else if (second_pulse) second (second 59) ? 0 : second 1; else if (second 59) second 0; else second second 1; end // 分计时逻辑 always (posedge clk_1Hz or negedge rst_n) begin if (!rst_n) minute 0; else if (minute_pulse) minute (minute 59) ? 0 : minute 1; else if ((minute 59) (second 59)) minute 0; else if (second 59) minute minute 1; end // 时计时逻辑 always (posedge clk_1Hz or negedge rst_n) begin if (!rst_n) hour 0; else if (hour_pulse) hour (hour 23) ? 0 : hour 1; else if ((hour 23) (minute 59) (second 59)) hour 0; else if ((minute 59) (second 59)) hour hour 1; end endmodule这段代码有几个关键设计点按键消抖处理机械按键会产生抖动我们通过移位寄存器实现简单的消抖条件判断优先级按键调整的优先级高于自动计时进位逻辑正确处理59秒→00分123:59:59→00:00:00等边界情况4. 约束文件配置与引脚分配要让设计在开发板上运行必须正确配置约束文件(.xdc)。以Basys3开发板为例我们需要定义时钟、按键和显示接口## 时钟信号100MHz set_property PACKAGE_PIN W5 [get_ports clk_100MHz] set_property IOSTANDARD LVCMOS33 [get_ports clk_100MHz] create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} [get_ports clk_100MHz] ## 复位按钮中央按钮 set_property PACKAGE_PIN U18 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] ## 时间调整按钮右、中、左按钮 set_property PACKAGE_PIN T17 [get_ports hour_adj] set_property IOSTANDARD LVCMOS33 [get_ports hour_adj] set_property PACKAGE_PIN W19 [get_ports minute_adj] set_property IOSTANDARD LVCMOS33 [get_ports minute_adj] set_property PACKAGE_PIN T18 [get_ports second_adj] set_property IOSTANDARD LVCMOS33 [get_ports second_adj] ## 七段数码管控制信号 set_property PACKAGE_PIN W7 [get_ports {seg[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}] ...约束文件配置要点信号类型开发板对应注意事项系统时钟板载晶振频率必须准确复位信号中央按钮通常低电平有效功能按键方向按钮需考虑人体工学布局显示接口七段数码管共阴/共阳配置要正确5. 功能仿真与调试技巧在烧录前进行仿真可以节省大量调试时间。我们创建测试平台验证核心功能timescale 1ns / 1ps module tb_digital_clock(); reg clk_100MHz; reg rst_n; reg hour_adj, minute_adj, second_adj; wire [4:0] hour; wire [5:0] minute, second; // 实例化被测模块 digital_clock uut ( .clk_1Hz(clk_1Hz), .rst_n(rst_n), .hour_adj(hour_adj), .minute_adj(minute_adj), .second_adj(second_adj), .hour(hour), .minute(minute), .second(second) ); // 生成100MHz时钟 initial begin clk_100MHz 0; forever #5 clk_100MHz ~clk_100MHz; end // 测试场景 initial begin // 初始化 rst_n 0; hour_adj 0; minute_adj 0; second_adj 0; #100; // 释放复位 rst_n 1; #200; // 测试秒调整 second_adj 1; #1000; second_adj 0; // 测试分调整 minute_adj 1; #1000; minute_adj 0; // 测试时调整 hour_adj 1; #1000; hour_adj 0; // 观察自动计时 #5000; $finish; end endmodule仿真中常见问题及解决方法计时不准确检查分频系数是否正确验证时钟约束是否正确定义按键无响应确认消抖逻辑工作正常检查约束文件中引脚分配是否正确显示异常验证七段数码管的编码逻辑检查共阴/共阳配置是否匹配硬件6. 显示驱动设计与优化大多数FPGA开发板使用七段数码管显示时间。我们需要将二进制时间转换为七段显示编码module display_driver( input clk_100MHz, input [4:0] hour, input [5:0] minute, input [5:0] second, output reg [6:0] seg, output reg [3:0] an ); reg [1:0] sel; reg [3:0] digit; reg [15:0] refresh_counter; // 数码管刷新控制约1kHz刷新率 always (posedge clk_100MHz) begin refresh_counter refresh_counter 1; if (refresh_counter 100000) refresh_counter 0; end // 多路复用选择 always (posedge clk_100MHz) begin if (refresh_counter 0) sel sel 1; end // 根据选择信号输出对应数字 always (*) begin case (sel) 2b00: digit second % 10; 2b01: digit second / 10; 2b10: digit minute % 10; 2b11: digit hour % 10; endcase end // 七段译码 always (*) begin case (digit) 4h0: seg 7b1000000; 4h1: seg 7b1111001; 4h2: seg 7b0100100; 4h3: seg 7b0110000; 4h4: seg 7b0011001; 4h5: seg 7b0010010; 4h6: seg 7b0000010; 4h7: seg 7b1111000; 4h8: seg 7b0000000; 4h9: seg 7b0010000; default: seg 7b1111111; endcase end // 位选信号 always (*) begin case (sel) 2b00: an 4b1110; 2b01: an 4b1101; 2b10: an 4b1011; 2b11: an 4b0111; endcase end endmodule显示驱动优化建议动态扫描频率保持在60-1kHz之间过低会闪烁过高会增加功耗亮度均衡通过PWM调节各数码管亮度显示格式可添加冒号分隔符增强可读性7. 系统集成与板级调试将各模块集成到顶层文件中module top( input clk_100MHz, input rst_n, input hour_adj, input minute_adj, input second_adj, output [6:0] seg, output [3:0] an ); wire clk_1Hz; wire [4:0] hour; wire [5:0] minute, second; clock_divider divider_inst( .clk_100MHz(clk_100MHz), .rst_n(rst_n), .clk_1Hz(clk_1Hz) ); digital_clock clock_inst( .clk_1Hz(clk_1Hz), .rst_n(rst_n), .hour_adj(hour_adj), .minute_adj(minute_adj), .second_adj(second_adj), .hour(hour), .minute(minute), .second(second) ); display_driver display_inst( .clk_100MHz(clk_100MHz), .hour(hour), .minute(minute), .second(second), .seg(seg), .an(an) ); endmodule板级调试常见问题排查完全无显示检查电源和复位信号确认比特流文件烧录成功显示错误数字验证七段译码逻辑检查引脚分配是否正确按键不灵敏调整消抖参数检查按键物理连接在Basys3开发板上实际测试时发现按键响应有时不够灵敏。通过增加消抖时间到20ms将移位寄存器位数从19增加到1,999,999问题得到解决。这种实际调试经验是教程中很少提及但非常重要的细节。