简介:本资源是一套基于Verilog HDL在DE2开发板上实现数字钟的完整FPGA工程,面向数字逻辑设计初学者、嵌入式与EDA课程学习者及FPGA实践爱好者,解决从理论计数逻辑到硬件落地显示的核心问题。压缩包共144个文件,568KB,涵盖12个核心Verilog源文件(如clock.v、clk.v、change.v等)、4个顶层模块备份(.bak)、多类Quartus II工程文件(.qpf/.qsf/.sof)、综合报告(.rpt)、仿真波形(.vwf)、映射数据库(.hdb/.ddb)及HTML格式说明文档,结构完整,支持直接编译下载验证。已有697人学习下载,资源包含可运行的分频器、BCD计数器、时间格式转换与LED显示驱动四大功能模块,代码注释清晰,模块划分合理,同步设计规范,并附带测试平台与硬件约束文件,便于理解时序控制、状态复位与跨模块信号交互等关键实践要点。
1. 从零到一:一个Verilog数字钟项目的完整构建逻辑
如果你手头有一块DE2开发板,想用它来做一个数字钟,并且在网上找到了一个名为clock2.rar的压缩包,里面可能包含了一些Verilog代码,那么这篇文章就是为你准备的。我将以一个做过多次类似项目的经验,带你完整地走一遍从理解需求、分析代码、到上板调试的全过程。这不仅仅是一个“如何运行”的指南,更是一次关于如何“理解”和“掌控”一个FPGA数字钟设计的深度探讨。你会发现,一个看似简单的数字钟,背后涉及了时钟分频、按键消抖、数码管动态扫描、时间设置逻辑、状态机设计等多个核心的数字电路概念。通过这个项目,你不仅能点亮一个时钟,更能透彻理解Verilog描述数字系统的思维方式。
2. 项目解构:clock2.rar里可能有什么,以及我们真正需要什么
拿到一个以.rar结尾、名字像clock2这样的压缩包,第一步不是急着解压运行。我们需要先建立一个清晰的项目蓝图,知道一个完整的、能在DE2上运行的数字钟应该由哪些模块构成。这样,即使压缩包里的代码不完整或有错误,我们也能知道如何修补或重写。
2.1 核心功能模块拆解
一个基础的数字钟,其Verilog设计通常遵循自顶向下的模块化思想。我们可以将其分解为以下几个核心部分:
- 时钟管理模块:这是系统的心跳。DE2板载的晶振通常是50MHz。我们的数字钟以1秒为基本单位,因此需要一个分频器,将50MHz的高频时钟分频成1Hz的秒脉冲信号。这里就涉及“滑动窗口滤波verilog”或“verilog滑动平均滤波”等热词相关的技术,用于对分频后的时钟进行滤波,确保稳定性。
- 计时核心模块:这是一个计数器链。接收1Hz的秒脉冲,实现秒(0-59)、分(0-59)、时(0-23)的计数和进位逻辑。这本质上是多个“verilog计数器”的级联,并需要处理进制转换(六十进制和二十四进制)。
- 显示驱动模块:DE2开发板上通常有多个7段数码管。我们需要一个“数码管动态扫描”驱动电路。该模块将计时核心产生的二进制时分秒数据,转换为每个数码管对应的段选信号(a-g, dp)和位选信号(选择点亮哪个数码管)。动态扫描的原理是利用人眼的视觉暂留,快速轮流点亮各个数码管。
- 输入控制模块:用于设置时间。通常通过板载的按键(Key)来实现。这里的关键技术是“按键消抖”。机械按键在按下和弹起时会产生物理抖动,在Verilog中必须用滤波逻辑(如延时采样或“verilog状态机”检测稳定状态)来消除抖动,否则一次按键会被误认为是多次按下。
- 顶层模块:将以上所有模块像搭积木一样“例化”(
verilog例化)并连接起来,定义整个系统的输入输出端口(与DE2的引脚约束文件对应)。
2.2 DE2开发板的硬件接口考量
DE2板子的资源是固定的,我们的设计必须与之匹配。你需要关注:
- 时钟引脚:连接50MHz晶振的引脚(如
CLOCK_50)。 - 按键引脚:通常有多个按键(
KEY[0],KEY[1]...),我们可能用其中两三个作为“设置”、“加”、“减”功能键。 - 数码管引脚:需要查手册明确段选信号(
HEX0_D[6:0]...)和位选信号(HEX0_DP或专用的位选控制信号)对应的引脚。 - 复位信号:可能使用一个按键作为全局复位。
理解这些,你就知道顶层模块的端口列表应该长什么样,也为后续编写Quartus的引脚分配文件(.qsf)打下了基础。
3. 深度剖析:关键模块的Verilog实现与避坑指南
现在,我们深入到每个模块的代码层面。我会给出一种可靠、易于理解的实现方式,并穿插讲解从“verilog语法基础”到实际调试中会遇到的各种坑。
3.1 时钟分频与滤波:从50MHz到1Hz的稳定之路
这是最容易出问题的地方。一个粗糙的分频器代码如下:
module clk_divider ( input wire clk_50m, // 50MHz时钟 input wire rst_n, // 低电平复位 output reg clk_1s // 1Hz时钟输出 ); reg [25:0] counter; // 需要计数到50_000_000,26位宽足够 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter <= 26'd0; clk_1s <= 1'b0; end else begin if (counter == 26'd49_999_999) begin // 从0计数到49,999,999 counter <= 26'd0; clk_1s <= ~clk_1s; // 每5000万次翻转一次,得到1Hz end else begin counter <= counter + 1'b1; end end end endmodule为什么这样写?50MHz时钟周期是20ns。要得到1Hz(周期1秒),需要计数50,000,000个周期。因为每两个1Hz时钟沿之间对应50M个原时钟周期,所以我们在计数到50M-1时翻转clk_1s信号。
注意:这里有个巨大隐患!
clk_1s是作为一个普通的寄存器信号产生的,它在FPGA内部属于“行波时钟”。将其直接驱动其他模块的时钟输入端,会导致严重的时序问题,如建立/保持时间违例,这在复杂设计或高速系统中是禁止的。这是新手常踩的坑。
正确的做法——时钟使能方案: 我们不应该生成一个新的时钟域,而是生成一个1Hz的时钟使能脉冲(en_1s),其他模块依然在clk_50m的上升沿工作,只是当en_1s为高时才更新状态。这完全符合同步设计原则。
module clk_en_gen ( input wire clk_50m, input wire rst_n, output reg en_1s // 1Hz时钟使能,高电平有效一个周期 ); reg [25:0] counter; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter <= 26'd0; en_1s <= 1'b0; end else begin en_1s <= 1'b0; // 默认拉低 if (counter == 26'd49_999_999) begin counter <= 26'd0; en_1s <= 1'b1; // 在计满的瞬间产生一个周期的高脉冲 end else begin counter <= counter + 1'b1; end end end endmodule关于滤波:如果担心分频计数器因噪声产生毛刺,可以考虑使用“滑动窗口滤波”的思路。例如,连续采样几次en_1s预期的跳变边沿,当多数次采样一致时才确认有效。但对于基础数字钟,上述稳定计数器方案通常已足够可靠。
3.2 计时核心:计数器链与状态保持
这是一个典型的层次化计数器设计。我们使用时钟使能方案。
module time_counter ( input wire clk_50m, input wire rst_n, input wire en_1s, // 来自时钟使能模块 input wire set_mode, // 进入设置模式 input wire inc_key, // 增加按键 input wire dec_key, // 减少按键 output reg [5:0] hour, // 0-23 output reg [5:0] minute, // 0-59 output reg [5:0] second // 0-59 ); // 按键消抖后的信号 wire set_mode_db, inc_key_db, dec_key_db; // 实例化按键消抖模块(下文详述) debounce db0(clk_50m, rst_n, set_mode, set_mode_db); debounce db1(clk_50m, rst_n, inc_key, inc_key_db); debounce db2(clk_50m, rst_n, dec_key, dec_key_db); // 状态定义:正常计时和设置时分秒 reg [1:0] state; localparam S_NORMAL = 2'b00; localparam S_SET_HOUR = 2'b01; localparam S_SET_MIN = 2'b10; localparam S_SET_SEC = 2'b11; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin hour <= 6'd0; minute <= 6'd0; second <= 6'd0; state <= S_NORMAL; end else begin // 状态转移逻辑 if (set_mode_db) begin case (state) S_NORMAL: state <= S_SET_HOUR; S_SET_HOUR: state <= S_SET_MIN; S_SET_MIN: state <= S_SET_SEC; S_SET_SEC: state <= S_NORMAL; default: state <= S_NORMAL; endcase end // 计时与设置逻辑 case (state) S_NORMAL: begin if (en_1s) begin // 只有使能有效时才更新 if (second == 6'd59) begin second <= 6'd0; if (minute == 6'd59) begin minute <= 6'd0; if (hour == 6'd23) begin hour <= 6'd0; end else begin hour <= hour + 1'b1; end end else begin minute <= minute + 1'b1; end end else begin second <= second + 1'b1; end end end S_SET_HOUR: begin if (inc_key_db) hour <= (hour == 6'd23) ? 6'd0 : hour + 1'b1; if (dec_key_db) hour <= (hour == 6'd0) ? 6'd23 : hour - 1'b1; end S_SET_MIN: begin if (inc_key_db) minute <= (minute == 6'd59) ? 6'd0 : minute + 1'b1; if (dec_key_db) minute <= (minute == 6'd0) ? 6'd59 : minute - 1'b1; end S_SET_SEC: begin if (inc_key_db) second <= (second == 6'd59) ? 6'd0 : second + 1'b1; if (dec_key_db) second <= (second == 6'd0) ? 6'd59 : second - 1'b1; end endcase end end endmodule关键点解析:
- 状态机:使用状态机(
verilog状态机)来管理“正常计时”和“设置时/分/秒”几种模式,逻辑清晰,易于扩展。 - 参数化:使用
localparam定义状态,提高了代码可读性和可维护性。这也是“verilog parameter写法”的一种应用。 - 条件判断顺序:在
S_NORMAL状态下,注意if-else的嵌套顺序,它精确描述了秒、分、时的进位关系。 - 设置逻辑:在设置状态下,计时停止(不响应
en_1s),只响应按键操作。这里使用了条件运算符(?:),简洁地实现了循环增减。
3.3 按键消抖模块:确保一次按键只触发一次
机械按键的抖动通常在5-20ms。我们需要一个滤波器来检测稳定的按键状态。
module debounce ( input wire clk, input wire rst_n, input wire key_in, // 原始按键输入,按下为低电平(假设DE2按键按下为低) output reg key_out // 消抖后输出,稳定低电平时表示按键按下 ); reg [19:0] cnt; // 计数器,50MHz下计数到1,000,000约为20ms reg key_sync0, key_sync1; // 两级同步器,消除亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 <= 1'b1; // 默认未按下(高电平) key_sync1 <= 1'b1; cnt <= 20'd0; key_out <= 1'b1; end else begin // 同步器链 key_sync0 <= key_in; key_sync1 <= key_sync0; // 边沿检测与计数逻辑 if (key_sync1 != key_out) begin // 检测到输入与当前输出状态不同 if (cnt == 20'd999_999) begin // 已稳定约20ms key_out <= key_sync1; // 更新输出状态 cnt <= 20'd0; end else begin cnt <= cnt + 1'b1; end end else begin // 状态相同,计数器清零 cnt <= 20'd0; end end end endmodule为什么用两级同步器?按键信号是异步于系统时钟clk的,直接采样可能产生亚稳态。key_sync0和key_sync1构成一个两级同步器,极大降低了亚稳态传播到后续逻辑的概率。这是处理异步信号的经典且必要的方法。
消抖逻辑:只有当检测到key_sync1(同步后的信号)与当前稳定输出key_out不同,并且这个不同状态持续了足够长的时间(约20ms),我们才认为这是一个有效的、稳定的按键动作,并更新key_out。这样就过滤掉了抖动期间的多次跳变。
3.4 数码管动态扫描显示:驱动多位数码管的核心
DE2上的数码管通常是共阳或共阴的,并且位选信号独立控制。动态扫描的原理是,在每一个很短的时间片内,只点亮一位数码管,并给出该位对应的段码,然后快速切换到下一位。由于切换速度很快(>60Hz),人眼看到的是所有位同时稳定显示。
module seg_driver ( input wire clk_50m, input wire rst_n, input wire [5:0] hour, minute, second, output reg [6:0] hex0, hex1, hex2, hex3, hex4, hex5, // 6个数码管的段选(a-g) output reg [5:0] seg_sel // 位选信号,低电平选中(假设共阴数码管) ); // 扫描时钟,约1KHz (50MHz / 50000) reg [15:0] scan_cnt; wire scan_clk_en; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) scan_cnt <= 16'd0; else scan_cnt <= scan_cnt + 1'b1; end assign scan_clk_en = (scan_cnt == 16'd49_999); // 每50000周期一个脉冲 // 位选扫描计数器(0-5) reg [2:0] sel_index; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) sel_index <= 3'd0; else if (scan_clk_en) begin if (sel_index == 3'd5) sel_index <= 3'd0; else sel_index <= sel_index + 1'b1; end end // 位选信号译码 always @(*) begin seg_sel = 6'b111111; // 默认全灭 seg_sel[sel_index] = 1'b0; // 选中当前位 end // 二进制转BCD码(用于显示十进制) wire [3:0] hour_ten, hour_unit, min_ten, min_unit, sec_ten, sec_unit; assign hour_ten = hour / 10; assign hour_unit = hour % 10; assign min_ten = minute / 10; assign min_unit = minute % 10; assign sec_ten = second / 10; assign sec_unit = second % 10; // 根据当前选中的位,输出对应的段码 reg [3:0] data_to_display; always @(*) begin case (sel_index) 3'd0: data_to_display = sec_unit; 3'd1: data_to_display = sec_ten; 3'd2: data_to_display = min_unit; 3'd3: data_to_display = min_ten; 3'd4: data_to_display = hour_unit; 3'd5: data_to_display = hour_ten; default: data_to_display = 4'd0; endcase end // 七段译码器(共阴数码管,段选高电平点亮) always @(*) begin case (data_to_display) 4'h0: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b1000000}}; // 0 4'h1: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b1111001}}; // 1 4'h2: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0100100}}; // 2 4'h3: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0110000}}; // 3 4'h4: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0011001}}; // 4 4'h5: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0010010}}; // 5 4'h6: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0000010}}; // 6 4'h7: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b1111000}}; // 7 4'h8: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0000000}}; // 8 4'h9: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b0010000}}; // 9 default: {hex5, hex4, hex3, hex2, hex1, hex0} = {6{7'b1111111}}; // 灭 endcase end endmodule关键点解析:
- 扫描时钟:动态扫描需要一个比人眼视觉暂留频率高得多的时钟来控制切换。这里用计数器生成了一个约1KHz的使能信号
scan_clk_en。1KHz意味着每位显示约1ms,6位循环周期约6ms,刷新率>160Hz,足够平滑。 - 位选扫描:
sel_index计数器在扫描使能下循环递增,通过一个译码逻辑生成seg_sel信号,实现轮流选中。 - BCD转换:时分秒计数器输出是二进制数(如
6'd23),但显示需要十进制的“2”和“3”。这里用除法和取余运算实现了简单的二进制到BCD的转换。对于更通用的转换,可以查找专门的“二进制转BCD Verilog”算法。 - 数据选择与译码:根据当前
sel_index,从6个BCD数字中选择一个送给data_to_display,然后通过一个查找表(case语句)将其转换为对应的7段码。注意,这里我简化了写法,将6个数码管的段选信号同时赋值了相同的值,因为同一时刻只有一位被点亮。在实际中,如果硬件连接是每个数码管段选独立,则需要为每个hex信号单独赋值,逻辑会更复杂一些,但原理相同。 - 共阴/共阳:段码表(如
7'b1000000对应数字0)取决于数码管是共阴(高电平点亮段)还是共阳(低电平点亮段)。务必根据DE2的原理图确认!位选信号的有效电平也需确认。
4. 顶层整合与DE2工程实践:让代码在板子上跑起来
有了所有子模块,我们需要一个顶层文件来把它们连接起来,并映射到DE2的实际物理引脚。
4.1 顶层模块设计
module digital_clock_de2 ( // 时钟与复位 input wire CLOCK_50, // DE2 50MHz时钟 input wire KEY0, // 复位键(假设低电平复位) // 按键输入(假设按下为低电平) input wire KEY1, // 模式设置键 input wire KEY2, // 增加键 input wire KEY3, // 减少键 // 数码管输出(假设为共阴数码管,位选低有效) output wire [6:0] HEX0, HEX1, HEX2, HEX3, HEX4, HEX5, output wire [5:0] HEX_SEL // 假设这是位选控制信号 ); wire rst_n = KEY0; // 假设KEY0是复位键,低电平有效 // 内部连线 wire en_1s; wire [5:0] hour, minute, second; // 模块实例化 clk_en_gen u_clk_en_gen ( .clk_50m(CLOCK_50), .rst_n(rst_n), .en_1s(en_1s) ); time_counter u_time_counter ( .clk_50m(CLOCK_50), .rst_n(rst_n), .en_1s(en_1s), .set_mode(~KEY1), // 注意:KEY1按下为低,但我们的模块可能期望高电平触发,这里取反 .inc_key(~KEY2), .dec_key(~KEY3), .hour(hour), .minute(minute), .second(second) ); seg_driver u_seg_driver ( .clk_50m(CLOCK_50), .rst_n(rst_n), .hour(hour), .minute(minute), .second(second), .hex0(HEX0), .hex1(HEX1), .hex2(HEX2), .hex3(HEX3), .hex4(HEX4), .hex5(HEX5), .seg_sel(HEX_SEL) ); endmodule注意:按键的电平逻辑需要根据硬件设计和模块需求仔细调整。例如,DE2的按键未按下时通常为高电平,按下为低电平。如果我们的消抖模块检测低电平有效,那么在顶层连接时,可能需要将~KEY1(取反)送给模块,表示按键按下时给模块输入高电平。这取决于你模块内部是如何定义有效按键信号的。这是一个常见的混淆点,务必结合原理图和模块代码确认。
4.2 Quartus II工程设置与引脚分配
这是将逻辑设计映射到物理芯片的关键一步。
- 创建工程:打开Quartus II,选择正确的器件型号(如Cyclone II EP2C35F672C6,这是DE2上常见的FPGA型号)。
- 添加文件:将所有
.v文件(顶层和子模块)添加到工程中。 - 分析与综合:先运行一次,检查语法错误。
- 引脚分配:
- 点击
Assignments -> Pin Planner。 - 根据DE2的用户手册或原理图,将顶层模块的端口(如
CLOCK_50,KEY0,HEX0[0]等)分配到对应的FPGA引脚编号上。 - 例如:
CLOCK_50->PIN_N2KEY0->PIN_G26KEY1->PIN_N23HEX0[0]->PIN_AF10(这只是一个示例,必须查手册!)HEX_SEL[0]->PIN_V13
- 这是最容易出错的地方!引脚分配错误会导致功能异常或完全没反应。建议在网上搜索“DE2 pin assignment”参考现成的
.qsf文件,或者仔细阅读DE2手册的附录。
- 点击
- 全编译:运行全编译(包括综合、布局布线、时序分析、生成编程文件)。
- 编程与配置:用USB-Blaster连接DE2和电脑,通过
Tools -> Programmer将生成的.sof文件下载到FPGA中。注意选择JTAG模式。
4.3 仿真验证:使用ModelSim提前排除逻辑错误
在烧录到板子之前,强烈建议使用ModelSim进行功能仿真。这可以节省大量硬件调试时间。
- 编写测试平台:创建一个
testbench.v文件。`timescale 1ns/1ns module tb_digital_clock(); reg clk_50m; reg rst_n; reg set_key, inc_key, dec_key; wire [6:0] hex0, hex1, hex2, hex3, hex4, hex5; wire [5:0] seg_sel; // 实例化被测设计 digital_clock_de2 uut ( .CLOCK_50(clk_50m), .KEY0(rst_n), .KEY1(set_key), .KEY2(inc_key), .KEY3(dec_key), .HEX0(hex0), .HEX1(hex1), .HEX2(hex2), .HEX3(hex3), .HEX4(hex4), .HEX5(hex5), .HEX_SEL(seg_sel) ); // 生成50MHz时钟 initial clk_50m = 0; always #10 clk_50m = ~clk_50m; // 周期20ns // 初始化与激励 initial begin // 初始化 rst_n = 0; set_key = 1; // 假设按键常态为高 inc_key = 1; dec_key = 1; #100; rst_n = 1; // 释放复位 #200; // 测试正常计时(仿真几秒) $display("开始正常计时测试..."); #2_000_000_000; // 仿真2秒(实际时间很长,可缩短分频系数在仿真中) // 测试设置模式 $display("进入设置模式..."); set_key = 0; #200_000; set_key = 1; // 模拟按下设置键(注意消抖时间) #1_000_000; inc_key = 0; #200_000; inc_key = 1; // 模拟按下增加键 #1_000_000; $stop; // 停止仿真 end endmodule - 在ModelSim中运行:添加设计文件和测试平台,运行仿真。观察波形图中的
en_1s、hour、minute、second以及数码管驱动信号seg_sel和hex0等,验证逻辑是否符合预期。 - 调试技巧:如果仿真结果不对,可以逐步缩小范围。例如,先单独仿真
clk_en_gen模块,看en_1s脉冲是否正确;再仿真time_counter,看计时和按键设置逻辑是否正确。使用$display在控制台打印中间变量值也是有效的调试手段。
5. 调试实战与问题排查:当数字钟不“走”时怎么办
即使代码编译通过,成功下载到板子,也可能遇到各种问题。以下是一些常见故障及排查思路:
问题1:数码管完全不亮或全亮。
- 可能原因:引脚分配错误;段码或位选信号的电平逻辑弄反(共阴/共阳);
seg_sel位选信号始终为一个固定值,没有扫描。 - 排查:
- 首先确认引脚分配文件(.qsf)是否正确加载,并与顶层端口名一致。
- 用示波器或逻辑分析仪(如果条件允许)测量
seg_sel信号。它应该是一个频率在几百Hz的循环扫描信号。如果没有,检查seg_driver模块中的扫描计数器逻辑。 - 检查段码表。写一个最简单的测试程序,固定输出一个数字(如8)到所有数码管,看是否显示。这可以隔离是显示驱动问题还是前级计时问题。
问题2:时间显示乱码或部分段不亮。
- 可能原因:BCD转换错误;段码表错误;某个数码管的引脚连接错误或虚焊。
- 排查:
- 在仿真中检查
data_to_display的值是否正确。例如,当second=5时,sec_unit和sec_ten是否分别为5和0。 - 检查
case语句中的段码值,是否与硬件匹配。可以对照DE2的示例代码中的段码表。 - 如果某个数码管的特定段(如a段)始终不亮,而其他数字显示正常,可能是该段的FPGA引脚损坏或电路问题。
- 在仿真中检查
问题3:计时不准,走时过快或过慢。
- 可能原因:分频计数器计算错误;
en_1s使能信号产生了毛刺或频率不对。 - 排查:
- 计算验证:确认
clk_en_gen模块中的计数器终值。50MHz分频1Hz,终值应为50_000_000 - 1。如果为了仿真方便改小了,下载到板子时一定要改回来。 - 逻辑分析:如果条件允许,用逻辑分析仪抓取
en_1s信号,看其周期是否为准确的1秒。也可以间接观察:让秒位从0计数到9,用秒表测量实际时间是否为10秒。 - 时钟使能方案:确保你使用的是“时钟使能”方案,而不是“行波时钟”方案。后者会导致时序问题,可能引起计数错误。
- 计算验证:确认
问题4:按键设置不灵敏或连击。
- 可能原因:消抖时间参数不合适;按键信号在顶层连接时电平逻辑弄反;按键本身硬件接触问题。
- 排查:
- 仿真消抖模块:在ModelSim中给
debounce模块输入一个带有抖动的模拟按键信号,观察key_out是否只在稳定按下后才跳变一次。 - 调整消抖时间:
debounce模块中的计数器终值决定了消抖时间。50MHz下,20'd999_999对应约20ms。如果按键特性不同,可以适当调整这个值(如19'd499_999对应10ms)。 - 检查电平:用万用表测量按键按下和释放时,输入到FPGA引脚的实际电压。确认你的Verilog代码中对按键状态的判断(
1'b0还是1'b1表示按下)与实际硬件一致。
- 仿真消抖模块:在ModelSim中给
问题5:设置时间时,其他位也在变化。
- 可能原因:状态机逻辑有误。在设置
S_SET_HOUR状态时,en_1s信号可能没有被屏蔽,导致秒和分仍在正常计时。 - 排查:仔细检查
time_counter模块中S_NORMAL状态和其他设置状态的执行条件。确保在非S_NORMAL状态下,en_1s不会触发计时更新。同时,检查按键消抖后的信号inc_key_db等是否真的只在按键按下时产生一个单脉冲。如果消抖模块输出在按键期间持续为低,可能会导致在设置状态下时间连续快速变化。
调试是一个系统工程,需要耐心和逻辑。遵循“从简单到复杂”、“分模块隔离”的原则。先确保时钟和复位信号正确,再确保显示驱动能固定显示一个数字,然后加入计时逻辑看是否自动走时,最后测试按键设置功能。每一步都通过仿真或板级测试验证,就能最终让这个Verilog数字钟在DE2上稳定可靠地运行起来。这个过程本身,就是对数字电路设计和FPGA开发流程最深刻的实践学习。
本文还有配套的精品资源,点击获取