简介:本资源是一套基于Verilog HDL在DE2开发板上实现数字钟的完整FPGA工程,面向数字逻辑设计初学者与嵌入式硬件实践者,解决从理论计数逻辑到实际硬件部署的关键落地问题。压缩包共144个文件,568KB,涵盖13个综合报告(rpt)、13个Quartus编译日志(qmsg)、12个数据库文件(cdb)、10个硬件描述备份(hdb)、7个时序摘要(summary)及4个核心Verilog源码(.v),另有readme说明文档与多个.bak备份版本,体现模块化开发痕迹。已有697人学习下载,资源结构清晰呈现clock.v、clk.v等主控模块及分频器、计数器、时间格式转换与LED显示驱动等典型子模块,配套仿真波形(vwf)、引脚约束(qsf)与烧录文件(sof/pof),支持直接加载至DE2平台验证秒/分/时递进逻辑与同步时序设计规范。
1. 项目概述:从零构建一个基于DE2开发板的Verilog数字钟
最近在整理硬盘,翻出来一个老项目文件clock2.rar,里面是我当年用Verilog在Altera DE2开发板上实现的一个数字钟。这个项目虽然基础,但麻雀虽小五脏俱全,涵盖了从时钟分频、计时逻辑、按键消抖、数码管动态扫描到顶层模块设计的完整流程,是学习FPGA和数字逻辑设计的绝佳练手项目。如果你手头正好有一块DE2或者类似的FPGA开发板,想通过一个具体的案例把Verilog语法、Modelsim仿真、Quartus II综合布局布线这一套流程跑通,那这个数字钟的实现过程会给你提供一个非常清晰的路线图。它不仅仅是一个显示时间的钟,更是一个理解同步时序电路设计、状态机应用以及硬件描述语言如何映射到真实硬件的经典范例。
2. 核心需求与设计思路拆解
2.1 功能需求定义
一个基础的数字钟,核心功能无非是计时与显示。但放到FPGA上实现,我们需要将其拆解为具体的、可编码的硬件行为:
- 计时功能:实现时、分、秒的计数。秒和分是60进制,小时是24进制(或12进制可配置)。这需要三个计数器模块。
- 显示功能:将时、分、秒的数值(二进制)转换成能在7段数码管上显示的段码,并驱动6位数码管(通常格式为
HH:MM:SS)。 - 校时功能:通过开发板上的按键,能够分别对时、分、秒进行校准。这涉及到按键信号的检测与消抖处理。
- 时钟源:DE2板载的晶振通常是50MHz,我们需要通过分频得到精确的1Hz信号,作为秒计数的基准。
2.2 系统顶层架构设计
基于以上需求,整个系统的顶层模块(Top Module)可以这样规划:
- 输入:50MHz系统时钟(
clk_50m)、复位信号(rst_n)、三个校时按键(key_adj_hour,key_adj_min,key_adj_sec)。 - 输出:6位数码管的位选信号(
sel[5:0])和段选信号(seg[7:0])。 - 内部模块:
- 时钟分频模块(
clk_div):将50MHz时钟分频产生1Hz秒脉冲(clk_1hz)和用于数码管动态扫描的较高频率时钟(如1KHz,clk_1khz)。 - 按键消抖模块(
key_debounce):对三个按键输入进行消抖处理,输出稳定的按键使能脉冲信号。 - 计时核心模块(
time_counter):包含秒、分、时计数器,受clk_1hz和校时脉冲控制,完成计时逻辑。 - 数码管驱动模块(
seg_driver):将计时模块输出的二进制数转换为7段码,并利用clk_1khz实现6位数码管的动态扫描,控制sel和seg信号。
- 时钟分频模块(
顶层模块的作用就是将这些子模块像搭积木一样例化(Instantiate)并连接起来。这种模块化设计思想是FPGA工程的核心,便于调试、复用和维护。
注意:在实际DE2开发板上,按键通常是低电平有效(按下为0),复位信号也可能是低电平有效,具体需要查阅DE2的原理图。我们的设计需要与之匹配。
2.3 关键设计考量:同步与异步
在数字电路设计中,同步设计是黄金准则。这意味着所有的寄存器(如我们的计数器)都应该由同一个全局时钟(或由它产生的同步时钟)的上升沿触发。我们的设计中:
- 计时:秒计数器由同步分频得到的
clk_1hz驱动。 - 按键响应:不能直接用按键信号作为计数器的时钟!这会导致异步时钟域问题,产生不可靠的亚稳态。正确做法是:将按键消抖后产生的脉冲信号,作为计时模块的“使能”信号或“加载”信号,在
clk_1hz的同步下修改计数值。 - 数码管扫描:扫描时钟
clk_1khz也应由50MHz主时钟分频得到,保证整个系统时钟同源。
3. 核心模块的Verilog实现与解析
3.1 时钟分频模块:从50MHz到1Hz
生成精确的1秒时钟是整个系统准确计时的基础。50MHz意味着周期是20ns。要得到1Hz(周期1秒),需要分频系数N = 50,000,000。这是一个非常大的计数器,直接计数会消耗大量寄存器资源。
常见实现方案:两级分频
- 先分频到一个中间频率,比如1KHz(分频系数50000)。
- 再用1KHz时钟分频到1Hz(分频系数1000)。 这样做的好处是,1KHz时钟还可以直接给数码管扫描模块使用,一举两得。
module clk_div ( input wire clk_50m, // 50MHz主时钟 input wire rst_n, // 低电平复位 output reg clk_1khz, // 1KHz扫描时钟 output reg clk_1hz // 1Hz计时时钟 ); // 参数定义,提高代码可读性和可配置性 parameter CNT_1KHZ_MAX = 50_000; // 50MHz / 1KHz = 50000 parameter CNT_1HZ_MAX = 1_000; // 1KHz / 1Hz = 1000 // 生成1KHz时钟 reg [15:0] cnt_1khz; // 需要 log2(50000) ≈ 16 位计数器 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_1khz <= 16'd0; clk_1khz <= 1'b0; end else begin if (cnt_1khz == CNT_1KHZ_MAX/2 - 1) begin clk_1khz <= ~clk_1khz; // 计到一半时翻转,产生50%占空比 cnt_1khz <= 16'd0; end else begin cnt_1khz <= cnt_1khz + 1'b1; end end end // 用1KHz时钟生成1Hz时钟 reg [9:0] cnt_1hz; // 需要 log2(1000) ≈ 10 位计数器 always @(posedge clk_1khz or negedge rst_n) begin // 注意这里时钟是clk_1khz if (!rst_n) begin cnt_1hz <= 10'd0; clk_1hz <= 1'b0; end else begin if (cnt_1hz == CNT_1HZ_MAX/2 - 1) begin clk_1hz <= ~clk_1hz; cnt_1hz <= 10'd0; end else begin cnt_1hz <= cnt_1hz + 1'b1; end end end endmodule实操心得:分频系数的计算与资源权衡
CNT_1KHZ_MAX为什么是50000?因为50,000,000 Hz / 1,000 Hz = 50,000。我们在计数器计到一半(25000)时翻转时钟,这样产生的clk_1khz占空比是50%,波形更规整。- 这里采用了寄存器级联分频。第一级计数器
cnt_1khz是16位,第二级cnt_1hz是10位。如果直接用50MHz分频到1Hz,需要一个26位的计数器,虽然逻辑上更简单,但在一些综合工具中,超大位宽计数器可能不会被识别为标准的计数器IP,优化效果未必好。两级分频更符合典型设计。 - 关键细节:第二级计数器的时钟是
clk_1khz,而不是clk_50m。这体现了同步设计思想,clk_1khz是clk_50m的同步分频,用它再去驱动下一级电路,时钟域是清晰的。
3.2 按键消抖模块:将机械振动转化为稳定脉冲
机械按键在按下和弹起时,会产生持续数毫秒的抖动,在示波器上看就是一段毛刺。如果直接用它来控制计数器,一次按压可能会被误认为是多次触发。
消抖原理:检测到按键状态变化后,并不立即响应,而是等待一段时间(比如20ms),如果按键状态保持稳定,则确认这是一次有效的按压,并产生一个持续一个时钟周期的脉冲信号。
module key_debounce ( input wire clk, // 时钟 (建议用1KHz或更高,这里用1KHz) input wire rst_n, input wire key_in, // 原始按键输入,低电平有效 output reg key_pulse // 消抖后的按键脉冲,高电平有效一个周期 ); parameter DEBOUNCE_TIME = 20; // 消抖时间,单位毫秒 parameter CLK_FREQ = 1000; // 时钟频率,单位Hz (1KHz) localparam CNT_MAX = DEBOUNCE_TIME * CLK_FREQ / 1000; // 计算需要计数的周期数 reg [7:0] cnt; // 计数器,位宽根据CNT_MAX调整 reg key_in_r0, key_in_r1; // 两级同步寄存器,用于边沿检测 wire key_negedge; // 按键下降沿标志 // 同步级联,消除亚稳态 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_in_r0 <= 1'b1; // 默认按键未按下(高电平) key_in_r1 <= 1'b1; end else begin key_in_r0 <= key_in; key_in_r1 <= key_in_r0; end end // 检测下降沿(按键按下) assign key_negedge = key_in_r1 & (~key_in_r0); // 前一刻为1,此刻为0 // 消抖计数逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 8'd0; key_pulse <= 1'b0; end else begin if (key_negedge) begin // 检测到下降沿,开始计数 cnt <= CNT_MAX; end else if (cnt != 8'd0) begin // 计数未结束 cnt <= cnt - 1'b1; end // 当计数器减到1时,说明稳定按下时间已达到,产生脉冲 if (cnt == 8'd1) begin key_pulse <= 1'b1; end else begin key_pulse <= 1'b0; end end end endmodule为什么这样设计?
- 两级同步器 (
key_in_r0, key_in_r1):这是处理跨时钟域或异步输入(按键相对系统时钟是异步的)的标准方法。第一级寄存器可能进入亚稳态,第二级寄存器极大地降低了亚稳态传播到后续逻辑的概率。这是数字电路设计的必备常识。 - 边沿检测:通过比较
key_in_r1和key_in_r0,可以检测到按键从高到低(按下)或从低到高(释放)的跳变。这里我们只检测按下动作。 - 计数器消抖:一旦检测到下降沿,启动计数器。在计数期间,如果按键抖动导致
key_in变化,key_negedge会再次被触发,计数器会被重置为CNT_MAX,重新开始计时。只有按键稳定按下超过设定时间,计数器才能顺利数到0,进而产生一个脉冲。 - 参数化设计:
DEBOUNCE_TIME和CLK_FREQ用参数定义,方便适配不同的时钟频率和消抖要求。例如,如果扫描时钟是200Hz,需要修改CLK_FREQ为200,并重新计算CNT_MAX。
提示:这个消抖模块输出的是一个单时钟周期脉冲(
key_pulse)。在顶层,我们需要将三个按键的key_pulse信号分别连接到计时模块的校时控制端。
3.3 计时核心模块:状态机与计数器
这是数字钟的逻辑大脑。它需要处理两种事件:1秒脉冲自动计时,和按键脉冲手动校时。
module time_counter ( input wire clk_1hz, // 1秒时钟 input wire rst_n, input wire adj_sec_pulse, // 校秒脉冲 input wire adj_min_pulse, // 校分脉冲 input wire adj_hour_pulse, // 校时脉冲 output reg [5:0] hour, // 时 (0-23) output reg [5:0] minute, // 分 (0-59) output reg [5:0] second // 秒 (0-59) ); // 秒计数器 always @(posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin second <= 6'd0; end else if (adj_sec_pulse) begin // 校秒优先级最高 second <= second + 1'b1; if (second == 6'd59) second <= 6'd0; end else begin // 正常计时 second <= second + 1'b1; if (second == 6'd59) second <= 6'd0; end end // 分计数器:在秒进位或校分时触发 wire sec_carry = (second == 6'd59) && !adj_sec_pulse; // 正常计数到59,下一拍产生进位 always @(posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin minute <= 6'd0; end else if (adj_min_pulse) begin minute <= minute + 1'b1; if (minute == 6'd59) minute <= 6'd0; end else if (sec_carry) begin minute <= minute + 1'b1; if (minute == 6'd59) minute <= 6'd0; end end // 时计数器:在分进位或校时时触发 wire min_carry = (minute == 6'd59) && (sec_carry || adj_min_pulse); // 注意进位条件 always @(posedge clk_1hz or negedge rst_n) begin if (!rst_n) begin hour <= 6'd0; end else if (adj_hour_pulse) begin hour <= hour + 1'b1; if (hour == 6'd23) hour <= 6'd0; // 24小时制 end else if (min_carry) begin hour <= hour + 1'b1; if (hour == 6'd23) hour <= 6'd0; end end endmodule设计要点与陷阱:
- 统一的时钟域:所有计数器都由
clk_1hz驱动,这是同步设计的关键。校时信号(adj_*_pulse)是作为同步使能信号使用的。 - 进位逻辑:这是最容易出错的地方。
sec_carry信号在second等于59且不是由校秒操作导致的下一周期有效。为什么?因为如果当前是59秒,我按下校秒键,秒应该变成00,但分不应该进位。所以sec_carry = (second == 6'd59) && !adj_sec_pulse。min_carry的逻辑更复杂一些,需要同时满足“分是59”和“产生了有效的秒进位或校分操作”。 - 位宽选择:时、分、秒最大值为59或23,用6位二进制数(可表示0-63)存储足够,节省寄存器资源。输出为6位二进制,方便后续数码管驱动模块进行BCD转换或直接处理。
3.4 数码管驱动模块:动态扫描与译码
DE2开发板上的6位数码管是共阳的,并且位选和段选都是通过FPGA的I/O口直接驱动。为了节省I/O和内部逻辑,普遍采用动态扫描方式:快速轮流点亮每一个数码管,利用人眼的视觉暂留效应,看起来像是同时点亮。
module seg_driver ( input wire clk_1khz, // 扫描时钟,约1ms切换一位 input wire rst_n, input wire [5:0] hour, input wire [5:0] minute, input wire [5:0] second, output reg [5:0] sel, // 位选信号,低电平选中 output reg [7:0] seg // 段选信号,低电平点亮 (a,b,c,d,e,f,g,dp) ); // 将时分秒的二进制数拆成6个十进制数字 (十位和个位) wire [3:0] hour_ten, hour_one; wire [3:0] min_ten, min_one; wire [3:0] sec_ten, sec_one; assign hour_ten = hour / 10; assign hour_one = hour % 10; assign min_ten = minute / 10; assign min_one = minute % 10; assign sec_ten = second / 10; assign sec_one = second % 10; // 动态扫描计数器 reg [2:0] scan_cnt; // 0-5 对应6个数码管 always @(posedge clk_1khz or negedge rst_n) begin if (!rst_n) begin scan_cnt <= 3'd0; end else begin scan_cnt <= scan_cnt + 1'b1; if (scan_cnt == 3'd5) scan_cnt <= 3'd0; end end // 位选信号译码:根据scan_cnt选中对应数码管 always @(*) begin case (scan_cnt) 3'd0: sel = 6'b111110; // 最左边,小时十位 3'd1: sel = 6'b111101; // 小时个位 3'd2: sel = 6'b111011; // 分隔符(冒号),或分钟十位,这里我们先显示分十位 3'd3: sel = 6'b110111; // 分钟个位 3'd4: sel = 6'b101111; // 秒十位 3'd5: sel = 6'b011111; // 秒个位 default: sel = 6'b111111; endcase end // 段选信号译码:根据当前scan_cnt选择要显示的数字 reg [3:0] data_disp; // 当前要显示的4位BCD码 always @(*) begin case (scan_cnt) 3'd0: data_disp = hour_ten; 3'd1: data_disp = hour_one; 3'd2: data_disp = min_ten; 3'd3: data_disp = min_one; 3'd4: data_disp = sec_ten; 3'd5: data_disp = sec_one; default: data_disp = 4'd0; endcase end // 7段译码器:将4位BCD码转换为段码 (共阳数码管,低电平亮) always @(*) begin case (data_disp) 4'd0: seg = 8'b1100_0000; // 0 - a,b,c,d,e,f亮 4'd1: seg = 8'b1111_1001; // 1 - b,c亮 4'd2: seg = 8'b1010_0100; // 2 4'd3: seg = 8'b1011_0000; // 3 4'd4: seg = 8'b1001_1001; // 4 4'd5: seg = 8'b1001_0010; // 5 4'd6: seg = 8'b1000_0010; // 6 4'd7: seg = 8'b1111_1000; // 7 4'd8: seg = 8'b1000_0000; // 8 4'd9: seg = 8'b1001_0000; // 9 default: seg = 8'b1111_1111; // 全灭 endcase end endmodule动态扫描的精髓:
- 扫描时钟:
clk_1khz周期1ms,6位数码管轮流一遍是6ms,刷新率约166Hz,远高于人眼闪烁频率(~60Hz),显示稳定无闪烁。 - 位选与段选配合:在任一时刻,
sel信号只有一位是0(选中),seg输出对应被选中数码管要显示的数字的段码。下一个时钟周期,sel选中下一位,seg输出对应的数字段码。 - 二进制到BCD转换:小时(0-23)、分钟(0-59)、秒(0-59)是二进制数,需要转换成十位和个位两个BCD码。这里用了除法和取余操作(
/和%)。综合工具会将这些操作转换为相应的组合逻辑电路。对于FPGA,如果追求面积和速度,可以自己写查找表(LUT)或专用的BCD转换算法,但对于这个项目,这样写最清晰。 - 冒号显示:代码中暂时没处理冒号。DE2的数码管中间有两个独立的LED作为冒号。通常需要单独定义两个输出引脚来控制它们,并让它们以1Hz频率闪烁。可以在顶层模块增加一个1Hz驱动的寄存器来控制。
4. 顶层模块集成与引脚分配
4.1 顶层模块例化
顶层模块就像项目的总接线图,把各个子模块连接起来。
module top_digital_clock ( input wire CLOCK_50, // DE2 50MHz时钟 input wire [2:0] KEY, // DE2按键,假设KEY[0]是复位,KEY[1]校时,KEY[2]校分,KEY[3]校秒 output wire [5:0] HEX0_D, // 数码管段选,具体引脚名需查手册 output wire [5:0] HEX0_SEL // 数码管位选 ); // 注意:DE2的数码管接口命名可能不同,这里仅为示意。实际是HEX0-HEX5和对应的SEL。 // 内部连线声明 wire clk_1hz, clk_1khz; wire key_rst_pulse, key_hour_pulse, key_min_pulse, key_sec_pulse; wire [5:0] hour, minute, second; // 复位信号处理,假设KEY[0]低电平复位 wire sys_rst_n = KEY[0]; // 模块例化 clk_div u_clk_div ( .clk_50m(CLOCK_50), .rst_n(sys_rst_n), .clk_1khz(clk_1khz), .clk_1hz(clk_1hz) ); key_debounce u_key_rst ( .clk(clk_1khz), .rst_n(1'b1), // 消抖模块本身的复位常高,用系统复位 .key_in(KEY[0]), .key_pulse(key_rst_pulse) // 可能不需要,这里仅为示例 ); key_debounce u_key_hour ( .clk(clk_1khz), .rst_n(sys_rst_n), .key_in(KEY[1]), .key_pulse(key_hour_pulse) ); key_debounce u_key_min ( .clk(clk_1khz), .rst_n(sys_rst_n), .key_in(KEY[2]), .key_pulse(key_min_pulse) ); key_debounce u_key_sec ( .clk(clk_1khz), .rst_n(sys_rst_n), .key_in(KEY[3]), .key_pulse(key_sec_pulse) ); time_counter u_time_counter ( .clk_1hz(clk_1hz), .rst_n(sys_rst_n), .adj_sec_pulse(key_sec_pulse), .adj_min_pulse(key_min_pulse), .adj_hour_pulse(key_hour_pulse), .hour(hour), .minute(minute), .second(second) ); seg_driver u_seg_driver ( .clk_1khz(clk_1khz), .rst_n(sys_rst_n), .hour(hour), .minute(minute), .second(second), .sel(HEX0_SEL), // 连接到实际的位选引脚 .seg(HEX0_D) // 连接到实际的段选引脚 ); endmodule4.2 DE2开发板引脚分配
这是将设计映射到实际硬件的关键一步。你需要查阅DE2的用户手册或原理图,找到对应引脚。
| FPGA 引脚 (在Quartus中) | 对应信号 | DE2 板载资源 | 备注 |
|---|---|---|---|
PIN_N2 | CLOCK_50 | 50MHz 晶振 | 主时钟输入 |
PIN_G26 | KEY[0] | 按键0 | 复位键(低电平有效) |
PIN_N23 | KEY[1] | 按键1 | 校时键 |
PIN_P23 | KEY[2] | 按键2 | 校分键 |
PIN_W26 | KEY[3] | 按键3 | 校秒键 |
PIN_E2 | HEX0_D[0] | 数码管0段a | 段选信号低电平亮 |
PIN_F1 | HEX0_D[1] | 数码管0段b | ... |
| ... | ... | ... | (需根据原理图补全所有段选和位选引脚) |
PIN_J2 | HEX0_SEL[0] | 数码管0位选0 | 位选信号低电平选中 |
| ... | ... | ... | ... |
实操步骤(在Quartus II中):
- 完成Verilog代码编写和初步语法检查。
- 点击
Assignment -> Pin Planner。 - 在
Pin Planner窗口中,在Location列,为每个输入输出信号手动输入或选择对应的引脚号。 - 保存引脚分配,重新全编译工程。
警告:引脚分配错误是导致“下载后没反应”的最常见原因。务必仔细核对原理图,特别是数码管的段选和位选信号,共阳共阴、高低电平有效不能搞错。DE2的数码管是共阳的,位选低电平选中,段选低电平点亮。
5. 仿真测试与上板调试
5.1 使用ModelSim进行功能仿真
在综合布线前,先用仿真验证逻辑是否正确。编写Testbench(测试平台)文件。
`timescale 1ns/1ns module tb_digital_clock(); reg clk_50m; reg rst_n; reg [3:0] key; // 模拟按键 wire [5:0] sel; wire [7:0] seg; // 例化顶层模块 top_digital_clock uut ( .CLOCK_50(clk_50m), .KEY({rst_n, key[2:0]}), // 假设KEY[0]是复位 .HEX0_SEL(sel), .HEX0_D(seg) ); // 生成50MHz时钟 initial clk_50m = 0; always #10 clk_50m = ~clk_50m; // 周期20ns -> 50MHz // 测试流程 initial begin // 初始化 rst_n = 0; key = 4'b1111; // 按键全部未按下(高电平) #100; rst_n = 1; // 释放复位 #200; // 观察自动计时 #100_000_000; // 仿真100ms,观察秒计数器变化 // 测试校秒 key[0] = 0; // 按下校秒键 #20_000_000; // 按下20ms模拟抖动 key[0] = 1; // 释放 #50; key[0] = 0; #20_000_000; key[0] = 1; // 模拟一次完整按压 #100; // 测试校分、校时类似... // ... #50_000_000; $stop; // 结束仿真 end endmodule在ModelSim中运行仿真,观察hour,minute,second等信号的变化是否符合预期(每秒加1,按键按下时相应位加1,进位关系正确)。
5.2 上板调试与常见问题排查
将编译好的.sof文件通过USB-Blaster下载到DE2开发板后,可能会遇到以下问题:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 数码管完全不亮 | 1. 引脚分配错误或未分配。 2. 数码管位选/段选极性弄反。 3. 时钟未进入(复位一直有效)。 | 1. 检查Pin Planner,确认分配已保存并编译。 2. 用万用表或示波器测量位选和段选引脚电平。确认代码中的极性(共阳/共阴,高/低有效)与硬件一致。 3. 检查复位按键是否被按下,或复位逻辑是否写反。 |
| 数码管显示乱码(非数字) | 1. 段码译码表错误。 2. 动态扫描时序过快或过慢。 3. 二进制到BCD转换错误。 | 1. 核对seg_driver模块中的case语句,段码顺序(a,b,c,d,e,f,g,dp)是否与硬件对应。2. 调整 clk_div中生成clk_1khz的分频系数,尝试更慢的扫描速度(如200Hz)看是否稳定。3. 仿真检查 hour_ten,hour_one等中间信号的值是否正确。 |
| 计时不准(过快或过慢) | 1. 1Hz时钟分频错误。 2. 进位逻辑错误。 | 1. 检查clk_div模块的分频计数器参数。50MHz分频到1Hz,计数器要数50,000,000次,检查代码是否有误。2. 用SignalTap II嵌入式逻辑分析仪抓取 clk_1hz,second等信号,看clk_1hz周期是否为1秒,second是否在clk_1hz上升沿变化。 |
| 按键校时不灵敏或连跳 | 1. 按键消抖时间设置不当。 2. 消抖模块时钟频率错误。 3. 按键脉冲被计时时钟错过(亚稳态)。 | 1. 增加消抖时间参数DEBOUNCE_TIME,如从20ms改为50ms。2. 确认消抖模块的 clk输入是稳定的1KHz(或其它频率)时钟。3. 确保校时脉冲( adj_*_pulse)与计时时钟(clk_1hz)是同步的,且脉冲宽度足够被clk_1hz采到。可以在time_counter模块中,将按键脉冲用clk_1hz再打一拍作为最终使能信号。 |
| 显示有重影 | 动态扫描时,段码在位选切换前后没有完全同步改变,导致瞬间显示到错误的数码管上。 | 在seg_driver模块中,确保sel和seg的变化在同一时钟沿。可以将sel和seg的赋值都放在同一个always@(posedge clk_1khz)块中,先根据scan_cnt计算好data_disp和sel,再统一输出。 |
调试利器:SignalTap IIQuartus II自带的SignalTap II逻辑分析仪是FPGA调试的神器。它可以直接在FPGA运行时捕获内部信号的波形,无需外接仪器。
- 在Quartus中新建一个SignalTap II文件(.stp)。
- 添加你想观察的信号,如
clk_1hz,second,key_pulse,scan_cnt等。 - 设置采样时钟(通常用
clk_50m)和触发条件(如key_pulse上升沿)。 - 重新编译并下载包含SignalTap II探测点的sof文件。
- 运行,触发后就能看到真实的信号时序图,对排查计数器、状态机、按键响应等问题有极大帮助。
6. 项目优化与扩展思路
一个基础的数字钟完成后,你可以尝试以下扩展,让项目更具挑战性和实用性:
增加显示功能:
- 闪烁指示:校时状态下,对应的时、分、秒数字闪烁。
- 显示模式切换:通过按键在24小时制和12小时制(带AM/PM指示)之间切换。
- 显示日期:增加年月日计数器,并切换显示。
提高计时精度:
- 50MHz晶振有误差,长期运行会累积偏差。可以尝试实现时钟校准功能,例如通过另一个精确的1Hz参考信号(如GPS秒脉冲)来微调内部计数器。
增加闹钟功能:
- 设计一个闹钟设置模块,允许用户设置闹钟时间。当时钟走到闹钟时间时,驱动蜂鸣器或LED闪烁。这需要引入一个比较器和一个状态机来管理设置和报警状态。
使用状态机重构:
- 将整个系统,尤其是校时逻辑,用一个清晰的有限状态机(FSM)来描述。例如:
NORMAL(正常显示)、ADJ_HOUR(校时)、ADJ_MIN(校分)、ADJ_SEC(校秒)等状态。状态迁移由按键控制,每个状态下计时和显示行为不同。这样逻辑更清晰,易于扩展。
- 将整个系统,尤其是校时逻辑,用一个清晰的有限状态机(FSM)来描述。例如:
跨时钟域处理(CDC)的深入理解:
- 在本项目中,按键消抖模块的时钟(
clk_1khz)和产生按键的物理时钟域是异步的。我们通过两级同步器处理。你可以深入研究更复杂的CDC场景,比如将计时数据从一个时钟域传递到另一个时钟域,了解握手协议、异步FIFO等高级技术。
- 在本项目中,按键消抖模块的时钟(
这个基于DE2的Verilog数字钟项目,从分频、计时、消抖到扫描显示,串联了FPGA开发中最基础也最重要的几个知识点。把它吃透,不仅能让你在硬件描述语言编程上更进一步,更能建立起数字系统设计的核心思维——同步设计、模块化、时序分析。当你看到六位数码管按照你的设计精准地跳动时,那种对硬件直接操控的成就感,是软件编程无法比拟的。动手做一遍,遇到问题并解决它,这才是学习FPGA最有效的路径。
本文还有配套的精品资源,点击获取