简介一份Verilog数字钟课程设计文档基于FPGA现场可编程门阵列平台系统讲述了数字钟从需求分析、方案设计、模块拆分到仿真验证与下载上板的完整过程。文档围绕基本计时、校时、报时三大功能具体说明计数器、分频器、七段数码管显示编码、按键输入与状态控制等关键模块的设计思路并配有系统框图、模块说明、Modelsim仿真步骤以及FPGA实现流程。代码部分给出完整的Verilog源码实验心得记录常见问题与调试经验可直接用于课程设计报告撰写或项目改造。整个压缩包仅含1个docx文件大小68KB内容包含摘要、关键词、目录、正文、实验心得、参考文献等标准章节结构清晰便于查阅。已有205人学习下载适合数字系统设计、EDA技术等课程的本科生或Verilog初学者作为设计参考和模板。1. 写在前面这门课设到底在考你什么“Verilog数字钟课程设计”这个题目几乎每个学FPGA或者数字电路的人都会碰到。说实话单看代码量它不算大——核心模块加起来可能也就两三百行。但你别小看这个题目它几乎是把你前几学期学过的数字电路知识做了个大串联时序逻辑、组合逻辑、分频、计数、译码显示、按键消抖、仿真验证全部揉在一起。很多同学拿到题目第一反应是“不就是写个计数器吗”真动手才发现麻烦事一堆数码管乱跳、进位对不上、仿真的波形看起来对但上板就是不对甚至有人卡在工程配置上半天出不来结果。这篇博文我分几大块来聊先说清楚数字钟的整体架构和模块划分逻辑然后把分频、计时、显示、按键这几个核心模块的Verilog实现逐个拆开讲接着给出完整的testbench仿真方案和一个可以直接落地的工程代码结构最后整理我在调试过程中踩过的一堆坑包括新板子、旧板子的兼容问题怎么处理。无论你是第一次写课设的本科生还是想快速捡起Verilog的工程师这篇文章都能让你少走不少弯路。整个项目的核心思路是这样的外部晶振提供50MHz时钟经过分频模块产生1Hz的秒脉冲秒脉冲驱动秒计数器秒计数器计满60后向分计数器进位分计数器计满60后向时计数器进位时计数器按24小时制循环计数值通过BCD码或二进制码输出到显示模块显示模块用动态扫描的方式驱动4位或6位数码管另外加入按键模块实现校时、校分、秒表启动/暂停等功能。听起来不复杂但每一步都有细节。2. 整体架构设计模块化拆分比想象中更重要2.1 为什么要拆模块而不是一个always块写到底我见过不少同学的数字钟代码整篇就一个大always块时钟、计数、显示、按键全部堆在里面。能跑通吗能仿真也能出结果但稍微改一个功能就头疼而且代码复用性极差。课程设计如果只是应付验收这么写勉强能过但如果想着后面做课设升级、加功能、甚至毕设复用这种写法就是给自己埋雷。模块化拆分的好处有两个第一每个模块可以单独仿真验证出问题能快速定位是哪个环节的锅第二模块之间通过信号接口通信改其中一个模块不影响其他模块符合数字IC设计中“高内聚、低耦合”的基本思想。实际上华为、中兴这些公司面试时问到数字钟考官关注点从来不是你能不能写出来而是你如何组织你的代码结构。2.2 标准模块划分方案我建议按下面这五个模块来组织工程模块名功能关键端口top_module顶层例化连接所有子模块时钟、复位、按键输入、数码管位选/段选clk_div分频模块50MHz转1Hz以及扫描时钟等clk_50M, rst_n, clk_1scounter60秒/分计数器模60计数clk, rst_n, en, q[7:0], cocounter24时计数器模24计数clk, rst_n, en, q[5:0], cokey_debounce按键消抖输出单周期脉冲clk, key_in, key_pulseseg_display动态扫描显示BCD码转七段码clk_scan, data, seg, sel这里每个模块职责单一、接口清晰。顶层只做例化和连线不写任何逻辑。这样设计的好处是哪怕某一天你想把显示从数码管换成LCD只需要改写seg_display模块其他模块完全不用动。2.3 分频参数的计算为什么是50_000_000而不是49_999_999时钟分频是整个系统最基础的一环也是很多同学第一次写就写错的地方。假设外部晶振是50MHz要产生1Hz的时钟周期1秒分频系数是50_000_000。计数器需要从0数到49_999_999一共50_000_000个时钟周期数满后清零并翻转输出或产生一个脉冲。这里最容易搞错的就是边界值。如果你写的是“计数到50_000_000时清零”那实际分频系数就是50_000_001输出的频率会比1Hz略小一点点。虽然仿真看不出来跑上一天就会慢大约1.7秒。你想想如果课设验收时老师拿个秒表对着你的时钟看跑了半小时差了1秒多就问你尴尬不尴尬。提示在设计分频模块时最好把分频系数定义成parameter不要直接写死在代码里。不同的板子晶振频率可能不同有的25MHz、有的50MHz、有的100MHz用parameter可以做到一键适配。这也是面试时常见的加分点。3. 核心模块实现从分频到显示的每一个细节3.1 分频模块奇数分频和偶数分频的差别对于50MHz转1Hz5000万是偶数直接用一个计数器翻转输出就行了代码很简单module clk_div #(parameter DIV_N 50_000_000) ( input wire clk, input wire rst_n, output reg clk_out ); reg [31:0] cnt; always (posedge clk or negedge rst_n) begin if(!rst_n) begin cnt 32d0; clk_out 1b0; end else if(cnt (DIV_N/2 - 1)) begin cnt 32d0; clk_out ~clk_out; end else begin cnt cnt 1b1; end end endmodule但如果你后面遇到需要奇数分频的场景比如把50MHz分频成10MHz之外的某个非整数倍频率翻转法就不灵了。典型做法是“上升沿计数下降沿计数”双沿产生信号后再组合或者用状态机实现占空比接近50%的奇数分频。数字钟本身用不上奇数分频但在应聘笔试题里这是高频考点我这边顺便提一句别到时候被考懵了。另外要留意用这种计数翻转方式产生的1Hz信号它的上升沿和系统时钟不同步吗并不是它其实还是同步的因为翻转动作发生在时钟上升沿。只是它的占空比恰好是50%边沿质量没法和PLL产生的时钟相比。对于驱动计数器来说完全够用但如果要用1Hz信号去带动时序逻辑建议在内部再打一拍或者直接用脉冲而不是电平来驱动下一级。涉及到跨时钟域处理的场景务必小心。3.2 计时模块模60计数器的两种写法秒计数器和分计数器都是模60计数器这里有两种主流写法。第一是当计数到59后下一个时钟沿回到0同时产生进位第二是当计数到59且en有效时下一个沿回到0。两种写法在行为上差一拍但都能用。我们来看一个带使能端的模60计数器这个设计也是我实际工程中一直在用的模板module counter60 ( input wire clk, input wire rst_n, input wire en, output reg [6:0] q, output reg co ); always (posedge clk or negedge rst_n) begin if(!rst_n) begin q 7d0; co 1b0; end else if(en) begin if(q 7d59) begin q 7d0; co 1b1; end else begin q q 1b1; co 1b0; end end else begin co 1b0; end end endmodule需要注意q的位宽。60最大需要6位二进制因为2^664所以q定义成7位属于冗余设计写成[6:0]也没问题。但如果你习惯用BCD码输出即十位和个位各用4位二进制表示那么需要8位输出高4位是十位低4位是个位。两种风格各有优劣纯二进制计数代码简洁内部信号位宽省BCD码输出可以让显示模块直接译码不用再做二进制转BCD的运算。如果课设要求显示“分:秒”这种常见格式而且要求值是十进制的那么推荐直接用BCD码风格秒的十位取值范围0-5秒的个位0-9甚至可以对十位和个位分别写两个模6和模10计数器再拼接在一起。这种写法稍微繁琐但逻辑直观出错概率低。3.3 计时模块24小时制的边界处理小时计数器是模24比模60还简单但有一个地方容易写岔从23点跳到0点。这个处理其实和模60类似只是把判断值从59换成23。很多人问我秒、分、时三个计数器之间要不要用进位信号串联答案是肯定要。秒计数器产生进位co_sec这个信号作为分计数器的使能en_min分计数器产生进位co_min作为时计数器的使能en_hour。如果你不用使能的方式而是直接把秒计数器的co_sec接到分计数器的时钟端这种做法在原理上也能工作但它产生了计数器之间的级联时钟违背了同步设计的基本原则会导致时序分析和上板稳定性方面的隐患。课程设计可以不管但这个概念最好早点建立。我们再看进位链秒计数器每到59就产生一个高电平的co_sec脉冲持续一个时钟周期分计数器在en_min有效时加1然后继续等下一个co_sec。这样串下去整条链是纯同步的时序分析时只需要关心50MHz系统时钟不需要额外约束1Hz、1/60Hz这些派生时钟数据路径上的逻辑门比较器、加法器也都有一个完整时钟周期去收敛不容易出现亚稳态问题。有了这个基础后续你想加闹钟功能直接从计数器输出当前时间再和预设值比较即可同样只需要一个组合逻辑比较器架构上不用大改。4. 数码管显示与按键消抖两个最容易翻车的模块4.1 动态扫描显示的原理和刷新率计算数字钟显示部分典型的方案是用4位一体数码管或者6位通过动态扫描方式驱动。所谓动态扫描就是在极短的时间内轮流点亮每一位数码管利用人眼视觉暂留效应让观察者感觉所有位都在同时亮。这样可以用一套段码线多套位选线的方式大幅减少FPGA引脚占用。扫描频率怎么定这有个经验值每位数码管刷新率不要低于50Hz否则会看到明显闪烁。4位扫描每位占1/4的时间所以整体扫描周期要小于1/(50*4)5ms。取一个典型值用1kHz的扫描时钟每位亮1ms4位一轮是4ms相当于每位刷新250Hz肉眼完全看不出闪烁。扫描时钟怎么来同样用分频器从50MHz分频出1kHz或者1.5kHz都行。你不需要单独再写一个分频模块——在clk_div里同时输出两路时钟即可一路1Hz、一路1kHz或者更推荐输出一个更高频的使能脉冲信号来做分频。注意多路分频时钟在FPGA里并不建议用来驱动不同的always块因为会造成时钟偏斜。更好的做法是“单一高频时钟使能信号”例如50MHz下用计数器产生一个高电平持续1个周期的en_1k所有always块统一由50MHz驱动en_1k为高时逻辑才动作。这个属于编码风格层面的进阶建议但对你之后做更复杂的项目非常有帮助。扫描显示时容易踩的坑是“拖影”和“鬼影”。拖影的原因通常是位选切换和段码更新不同步你先换了位选但段码还是上一位的值当前位就会闪一下上一位的字形。解决办法是先关掉所有位选全置无效电平再改段码最后打开目标位选。这个顺序在代码里就三行但能避免显示上很多脏数据。4.2 按键消抖为什么不能直接读按键电平机械按键在按下和松开的过程中由于簧片弹跳会产生大约5~20ms不等的抖动。如果直接用按键的电平作为计数器的时钟或者使能信号会导致一次按键被识别成多次甚至一次跳好几秒。所以必须做消抖处理。最经典的消抖方式有两种。一种是延时采样检测到按键电平变化后等20ms再次采样如果电平仍然稳定就认为按键有效。另一种是计数消抖用计数器持续扫描按键电平只有电平连续稳定N个时钟周期才判定有效。后者更适合FPGA的并行思维。我给一个常用的按键消抖模块代码它的特点是输出单周期脉冲而不是持续的电平。这样后续逻辑只需要检测一次脉冲触发不会因为按键按着不放导致反复触发module key_debounce #(parameter DELAY_CNT 1_000_000) ( input wire clk, input wire rst_n, input wire key_in, output reg key_pulse ); reg [19:0] cnt; reg key_reg1, key_reg2; wire key_negedge; // 两级寄存器同步消除亚稳态 always (posedge clk or negedge rst_n) begin if(!rst_n) begin key_reg1 1b1; key_reg2 1b1; end else begin key_reg1 key_in; key_reg2 key_reg1; end end assign key_negedge key_reg2 ~key_reg1; // 检测按键按下瞬间 always (posedge clk or negedge rst_n) begin if(!rst_n) begin cnt 20d0; key_pulse 1b0; end else if(key_negedge) begin cnt 20d0; key_pulse 1b0; end else if(cnt DELAY_CNT) begin cnt 20d0; key_pulse key_in; // 如果20ms后按键仍为低输出一个脉冲 end else begin cnt 20d0; // 注意这里只是示意实际需保持计数 key_pulse 1b0; end end endmodule上面代码里我留了个注释实际写的时候要注意cnt必须持续计数而不是被清零。这里我只展示结构和思路代码细节你自己补全。需要说明的关键点有两个第一按键输入跨时钟域后先打两拍这是消除亚稳态的标准做法第二输出脉冲持续一个时钟周期而不是跟随按键电平这样后面接计时器时每按一次只触发一次操作。如果你用的是开发板按键按下时通常接地低电平有效所以检测下降沿还是上升沿要根据你的板子原理图来定。我这里以低电平有效按键为例。4.3 校时功能的设计思路加了按键模块之后校时逻辑就水到渠成了。常见的交互是按键1切换“运行/校时/校分/校秒”模式在对应模式下按键2每按一次对应计数器加1。这里要注意进入校准模式后要暂停正常计数否则会出现“你按一下加1秒但正常计数也在走”的冲突。暂停的逻辑也不复杂——顶层给秒计数器一个en_sec信号正常计时模式下en_sec等于分频模块输出的秒脉冲进入校准模式下en_sec被拉低秒计数器不走此时按键脉冲直接作为加数信号去修改计数值。这种“使能控制”的设计思路在后续做秒表、闹钟、倒计时等功能时都会反复用到。5. 仿真验证与调试实录让代码“看得见、查得清”5.1 Testbench怎么写才能有效验证写testbench不是随便给个时钟然后跑完就完事你需要有针对性地构造激励把关键功能点都覆盖到。数字钟的testbench至少要包含下面几项复位信号时钟信号模拟按键操作用#延迟和强制赋值或调用任务的方式产生按键翻转以及最重要的验证进位逻辑——比如让秒计数器从58跑到2看分钟是否正常进位让时计数器从23跑回0看日期是否翻转。Testbench的代码骨架大概是这样的timescale 1ns/1ps module tb_digital_clock; reg clk_50m; reg rst_n; reg key_set; reg key_adj; wire [7:0] seg_data; wire [3:0] seg_sel; initial begin clk_50m 0; forever #10 clk_50m ~clk_50m; // 50MHz周期20ns end initial begin rst_n 0; #100; rst_n 1; // 模拟按下按键 #20_000_000; // 20ms key_set 0; #20_000; key_set 1; // 继续仿真... end digital_clock_top u_top( .clk_50m (clk_50m), .rst_n (rst_n), .key_set (key_set), .key_adj (key_adj), .seg_data(seg_data), .seg_sel (seg_sel) ); endmodule这里有个技巧如果你不想费时间等真实的50M个时钟周期走完1秒你可以在testbench中给分频模块的parameter赋小一点的值比如让分频系数变成500而不是50_000_000。这一招在调试时极其有用——仿真时间直接从几十秒级别缩到几毫秒级别。具体做法是引用模块时用#(parameter值)重写参数。5.2 Modelsim仿真时最常踩的坑Modelsim本身功能没问题但很多同学在仿真时遇到“波形一片红”或者“结果不对”的情况多数不是RTL代码问题而是仿真环境设置问题。以下是我总结的高频坑点第一时间尺度设置不当。如果testbench里没有timescalemodelsim会默认使用全工程编译顺序里最先出现的timescale容易导致延迟时间误差。建议每个模块文件都写timescale 1ns/1ps。第二没有给仿真加初始值。如果初始时rst_n一直为高没有被拉低再拉高很多寄存器的初始值是x仿真波形自然是一堆红线。规范做法是在initial块中先拉低复位至少100ns再拉高让所有寄存器完成复位动作。第三Modelsim里仿真div_clk模块时如果cnt位宽不够导致计数溢出可能会得到意料之外的频率。遇到这种情况优先检查计数器的位宽50MHz计数到5000万需要26位二进制数因为2^2667,108,864所以[31:0]是安全的。别把位宽定义得太小这是很隐蔽的bug。第四当仿真结果看起来“完全正确”但上板却不工作大概率是复位方式或时钟约束问题。课程设计水平下板级调试前建议先跑一遍全编译并看Timing Report确保时序无违规。5.3 实测过程中排查问题的方法论上板调试FPGA和仿真完全不同仿真里一个bug改完重新跑也就几秒钟上板改一次要经历综合、实现、生成bitstream、下载好几个步骤。所以我强烈建议你保持一套固定的排查顺序先确认时钟有没有正常起振分频输出用示波器或者板载LED观察、再确认复位后的初始状态、接着逐个模块确认信号——比如观察秒计数器的计数值是否在跳变、进位信号是否产生、数码管是否正常点亮。如果是数码管乱码优先怀疑段码表编码位置不对或者位选和段码时序不匹配如果是按键没反应先确认按键的消抖模块有没有输出脉冲可以通过拉一个LED来观察如果只是计数值跳变得很快几乎可以断定是分频系数没有生效或者时钟频率与你的分频参数对不上。6. 常见问题排查速查表我把自己带过的课设里学生最容易问的问题整理成一个速查表供你调试时对照现象可能原因排查方法数码管全部不亮位选信号极性反了或扫描时钟没有产生检查段选/位选有效电平观察扫描时钟是否翻转数码管显示乱码段码表编码不对或数据位宽不匹配单独给显示模块输入固定数值验证数字跳得飞快分频系数N计算错误或时钟频率不匹配确认晶振频率换算分频系数用仿真确认1Hz周期计数器某一位始终为0复位后没有正常启动或者进位信号悬空检查使能端连接用chipscope或仿真观察内部信号按键一下跳好几秒消抖时间太短或没有消抖直接读电平增大消抖延时到20ms左右确认输出的脉冲宽度分到秒进位错乱进位信号保持时间不足或者valid时机不对检查co信号是否只持续一个时钟周期避免电平直接驱动下一级仿真正常上板异常可能是代码中有异步逻辑或者复位释放违例检查always块中的敏感列表尽量统一使用异步复位或同步复位这里要特别提醒第二行“数码管显示乱码”这个问题。很多开发板的数码管是共阳极段码要为0才点亮对应的段如果你按照共阴极去编码那结果就是亮灭完全颠倒。接线和极性问题在课程设计里出现频率极高入手前一定先看板子原理图。7. 踩坑复盘几个我没写在报告里的教训最后聊几个我做这个项目时印象特别深的坑不写进课设报告里但对你实际做项目非常有用。第一个是复位策略。数字钟这类教学项目大家习惯用异步复位也就是always (posedge clk or negedge rst_n)。这种写法本身没有错但如果你在顶层把rst_n接的按键和普通key_in混着用就可能出现复位与释放不同步的时序问题。建议把复位信号单独接板子上的专用复位按键不要跟功能按键复用一个引脚否则你按功能按键时容易误触全系统复位。第二个坑是扫描时钟和按键消抖时钟的分配。早期版本我图省事显示模块用clk_1k按键模块也用clk_1k结果出现了按键脉冲和显示扫描正好撞在同一时刻偶尔出现计数加1但显示闪一下的怪现象。后来我改成整个系统只有一个50MHz系统时钟所有模块靠不同的使能信号区分节奏问题就消失了。这也是前面我反复强调“通用时钟使能信号”的原因。第三个坑是关于扩展功能。很多人觉得数字钟做完就完事了其实这是一个很好的基础框架——秒表、倒计时、闹钟、日期显示都是在这个架构上扩展的。你可以试着加一个“60秒倒计时”模式把计数器从模60加法改成模60减法再把使能信号从秒脉冲换成按键脉冲难度低但扩展性强拿去参加院级比赛或者写进简历都是不错的素材。这个项目做到最后你会发现真正的收获不是会写一个数字钟而是理解了“一个稍微复杂的数字系统是怎么被拆解成一堆小模块又怎么被拼装起来的”这件事。后面无论你做通信接口、图像处理还是嵌入式软核底层思维都是相通的。本文还有配套的精品资源点击获取