当前位置: 首页 > news >正文

FPGA新手必看:用Verilog在Vivado里从零撸一个带按键调时的数字时钟

FPGA新手实战:用Verilog在Vivado中构建可调时数字时钟

第一次接触FPGA开发时,最令人兴奋的莫过于将代码转化为硬件电路的实际效果。数字时钟作为经典的入门项目,不仅能巩固Verilog语法基础,还能让你完整体验从设计到烧录的全流程。本文将手把手带你用Xilinx Vivado工具链,实现一个带按键调时功能的数字时钟,特别适合已经学过Verilog基础但尚未完成过完整项目的开发者。

1. 开发环境准备与工程创建

在开始编码前,我们需要准备好开发环境。Xilinx Vivado是当前主流的FPGA开发工具,支持从设计到烧录的全流程。建议使用2019.1或更新版本,这些版本对Basys3等常见开发板有更好的支持。

安装完成后,启动Vivado并选择"Create Project":

  1. 指定项目名称(如Digital_Clock)和存储路径
  2. 选择"RTL Project"类型
  3. 添加Verilog源文件(可先创建空文件)
  4. 选择目标设备型号(如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

这段代码有几个关键设计点:

  1. 按键消抖处理:机械按键会产生抖动,我们通过移位寄存器实现简单的消抖
  2. 条件判断优先级:按键调整的优先级高于自动计时
  3. 进位逻辑:正确处理59秒→00分+1,23: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

仿真中常见问题及解决方法:

  1. 计时不准确

    • 检查分频系数是否正确
    • 验证时钟约束是否正确定义
  2. 按键无响应

    • 确认消抖逻辑工作正常
    • 检查约束文件中引脚分配是否正确
  3. 显示异常

    • 验证七段数码管的编码逻辑
    • 检查共阴/共阳配置是否匹配硬件

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) 2'b00: digit = second % 10; 2'b01: digit = second / 10; 2'b10: digit = minute % 10; 2'b11: digit = hour % 10; endcase end // 七段译码 always @(*) begin case (digit) 4'h0: seg = 7'b1000000; 4'h1: seg = 7'b1111001; 4'h2: seg = 7'b0100100; 4'h3: seg = 7'b0110000; 4'h4: seg = 7'b0011001; 4'h5: seg = 7'b0010010; 4'h6: seg = 7'b0000010; 4'h7: seg = 7'b1111000; 4'h8: seg = 7'b0000000; 4'h9: seg = 7'b0010000; default: seg = 7'b1111111; endcase end // 位选信号 always @(*) begin case (sel) 2'b00: an = 4'b1110; 2'b01: an = 4'b1101; 2'b10: an = 4'b1011; 2'b11: an = 4'b0111; 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

板级调试常见问题排查:

  1. 完全无显示

    • 检查电源和复位信号
    • 确认比特流文件烧录成功
  2. 显示错误数字

    • 验证七段译码逻辑
    • 检查引脚分配是否正确
  3. 按键不灵敏

    • 调整消抖参数
    • 检查按键物理连接

在Basys3开发板上实际测试时,发现按键响应有时不够灵敏。通过增加消抖时间到20ms(将移位寄存器位数从19增加到1,999,999),问题得到解决。这种实际调试经验是教程中很少提及但非常重要的细节。

http://www.jsqmd.com/news/587528/

相关文章:

  • seo外贸网站模板需要定期更新吗
  • 华盟新媒黄博:AI流量引爆与AI全网获客,重塑增长新格局 - 企业推荐官【官方】
  • HP服务器硬件运维避坑手册:内存对称安装与RAID卡配置的常见错误
  • 实战应用构建:基于快马平台与openclaw tavily开发企业级竞品动态监控系统
  • 效率革命:用快马ai平台5分钟构建可交互python web应用原型
  • Redis Stream消息队列避坑指南:Spring Boot项目里如何防止消息丢失和积压?
  • mmsegmentation实战二:ISBI2012数据集预处理与模型调优全流程
  • 2026年双缸剪刀片实力厂家怎么选?认准高效耐用更省心! - 企业推荐官【官方】
  • 2026届毕业生推荐的五大AI辅助论文神器解析与推荐
  • novel-downloader:高效工具实现多平台小说一键下载与本地阅读
  • 网站 SEO 优化有哪些常用方法_网站 SEO 优化中的视频优化应该如何进行
  • 2026年废钢双缸剪刀片厂家怎么选?专业智造才能更可靠! - 企业推荐官【官方】
  • 2026年全国比较好的废液焚烧炉参数推荐,有机废气焚烧炉/气气板式换热器,废液焚烧炉厂家哪家好 - 品牌推荐师
  • 伯爵官方售后服务中心新址实地考察报告(2026年4月最新版) - 亨得利官方服务中心
  • IPv6配置实战:从零开始搭建你的第一个IPv6网络(附详细命令)
  • 新能源化工泵阀如何选择:安全合规、高效节能与长期稳定 - 企业推荐官【官方】
  • 周博宇团队提出OnFly:端侧零样本空中视觉语言导航,告别云端依赖 - MKT
  • Cosmos平台解析:英伟达如何用世界基础模型重塑机器人及自动驾驶未来?
  • 2026届毕业生推荐的十大AI科研神器实际效果
  • 学历越高越容易被 AI 取代?这组数据太颠覆认知了!
  • AI辅助开发:让快马AI分析蓝屏日志,智能生成定制化的kernel32.dll修复方案
  • 关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)
  • 网红旺仔蓝牙音响详细教程 | 制作成本不到50!
  • Qwen3.6-Plus 技术深度拆解:500K 超长上下文与 MoE 架构的再进化
  • 炉石传说脚本终极指南:3小时变8分钟的智能游戏体验
  • Ollama-for-amd全攻略:AMD GPU本地AI部署革新性实践指南
  • 激活函数选型指南:从Sigmoid到Swish,实战中如何根据任务和框架做选择?
  • Android ImageButton进阶实战:从基础到自定义状态与交互优化
  • 实战指南:基于快马AI生成简易CPU模拟器,深入理解指令执行全流程
  • 为什么门禁时灵时不灵?你可能忽略了识别距离