
简介本资源是一套基于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_00026位宽足够 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter 26d0; clk_1s 1b0; end else begin if (counter 26d49_999_999) begin // 从0计数到49,999,999 counter 26d0; clk_1s ~clk_1s; // 每5000万次翻转一次得到1Hz end else begin counter counter 1b1; 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 26d0; en_1s 1b0; end else begin en_1s 1b0; // 默认拉低 if (counter 26d49_999_999) begin counter 26d0; en_1s 1b1; // 在计满的瞬间产生一个周期的高脉冲 end else begin counter counter 1b1; 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 2b00; localparam S_SET_HOUR 2b01; localparam S_SET_MIN 2b10; localparam S_SET_SEC 2b11; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin hour 6d0; minute 6d0; second 6d0; 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 6d59) begin second 6d0; if (minute 6d59) begin minute 6d0; if (hour 6d23) begin hour 6d0; end else begin hour hour 1b1; end end else begin minute minute 1b1; end end else begin second second 1b1; end end end S_SET_HOUR: begin if (inc_key_db) hour (hour 6d23) ? 6d0 : hour 1b1; if (dec_key_db) hour (hour 6d0) ? 6d23 : hour - 1b1; end S_SET_MIN: begin if (inc_key_db) minute (minute 6d59) ? 6d0 : minute 1b1; if (dec_key_db) minute (minute 6d0) ? 6d59 : minute - 1b1; end S_SET_SEC: begin if (inc_key_db) second (second 6d59) ? 6d0 : second 1b1; if (dec_key_db) second (second 6d0) ? 6d59 : second - 1b1; 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 1b1; // 默认未按下高电平 key_sync1 1b1; cnt 20d0; key_out 1b1; end else begin // 同步器链 key_sync0 key_in; key_sync1 key_sync0; // 边沿检测与计数逻辑 if (key_sync1 ! key_out) begin // 检测到输入与当前输出状态不同 if (cnt 20d999_999) begin // 已稳定约20ms key_out key_sync1; // 更新输出状态 cnt 20d0; end else begin cnt cnt 1b1; end end else begin // 状态相同计数器清零 cnt 20d0; 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 16d0; else scan_cnt scan_cnt 1b1; end assign scan_clk_en (scan_cnt 16d49_999); // 每50000周期一个脉冲 // 位选扫描计数器0-5 reg [2:0] sel_index; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) sel_index 3d0; else if (scan_clk_en) begin if (sel_index 3d5) sel_index 3d0; else sel_index sel_index 1b1; end end // 位选信号译码 always (*) begin seg_sel 6b111111; // 默认全灭 seg_sel[sel_index] 1b0; // 选中当前位 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) 3d0: data_to_display sec_unit; 3d1: data_to_display sec_ten; 3d2: data_to_display min_unit; 3d3: data_to_display min_ten; 3d4: data_to_display hour_unit; 3d5: data_to_display hour_ten; default: data_to_display 4d0; endcase end // 七段译码器共阴数码管段选高电平点亮 always (*) begin case (data_to_display) 4h0: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b1000000}}; // 0 4h1: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b1111001}}; // 1 4h2: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0100100}}; // 2 4h3: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0110000}}; // 3 4h4: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0011001}}; // 4 4h5: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0010010}}; // 5 4h6: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0000010}}; // 6 4h7: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b1111000}}; // 7 4h8: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0000000}}; // 8 4h9: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b0010000}}; // 9 default: {hex5, hex4, hex3, hex2, hex1, hex0} {6{7b1111111}}; // 灭 endcase end endmodule关键点解析扫描时钟动态扫描需要一个比人眼视觉暂留频率高得多的时钟来控制切换。这里用计数器生成了一个约1KHz的使能信号scan_clk_en。1KHz意味着每位显示约1ms6位循环周期约6ms刷新率160Hz足够平滑。位选扫描sel_index计数器在扫描使能下循环递增通过一个译码逻辑生成seg_sel信号实现轮流选中。BCD转换时分秒计数器输出是二进制数如6d23但显示需要十进制的“2”和“3”。这里用除法和取余运算实现了简单的二进制到BCD的转换。对于更通用的转换可以查找专门的“二进制转BCD Verilog”算法。数据选择与译码根据当前sel_index从6个BCD数字中选择一个送给data_to_display然后通过一个查找表case语句将其转换为对应的7段码。注意这里我简化了写法将6个数码管的段选信号同时赋值了相同的值因为同一时刻只有一位被点亮。在实际中如果硬件连接是每个数码管段选独立则需要为每个hex信号单独赋值逻辑会更复杂一些但原理相同。共阴/共阳段码表如7b1000000对应数字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的值是否正确。例如当second5时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下20d999_999对应约20ms。如果按键特性不同可以适当调整这个值如19d499_999对应10ms。检查电平用万用表测量按键按下和释放时输入到FPGA引脚的实际电压。确认你的Verilog代码中对按键状态的判断1b0还是1b1表示按下与实际硬件一致。问题5设置时间时其他位也在变化。可能原因状态机逻辑有误。在设置S_SET_HOUR状态时en_1s信号可能没有被屏蔽导致秒和分仍在正常计时。排查仔细检查time_counter模块中S_NORMAL状态和其他设置状态的执行条件。确保在非S_NORMAL状态下en_1s不会触发计时更新。同时检查按键消抖后的信号inc_key_db等是否真的只在按键按下时产生一个单脉冲。如果消抖模块输出在按键期间持续为低可能会导致在设置状态下时间连续快速变化。调试是一个系统工程需要耐心和逻辑。遵循“从简单到复杂”、“分模块隔离”的原则。先确保时钟和复位信号正确再确保显示驱动能固定显示一个数字然后加入计时逻辑看是否自动走时最后测试按键设置功能。每一步都通过仿真或板级测试验证就能最终让这个Verilog数字钟在DE2上稳定可靠地运行起来。这个过程本身就是对数字电路设计和FPGA开发流程最深刻的实践学习。本文还有配套的精品资源点击获取