资讯详情

资讯详情

Verilog数字时钟设计:从分频到闹铃的FPGA实现与避坑指南

简介这份资源是面向FPGA初学者与数字电路学习者的Verilog时钟项目工程包围绕时钟、闹铃、报时与按键去抖等实用功能展开适合作为HDL入门练手或课程设计参考。压缩包共101个文件约3.83MB以Quartus工程文件为主包含cdb、hdb、ddb、rdb等编译数据库与报告文件以及qpf、qsf工程配置、sof与pof下载文件、v源文件、rpt报告和readme说明完整保留了从源码到综合、布局布线的工程痕迹。已有1527人学习下载。读者可从中获得可综合的Verilog时钟模块实现思路理解秒、分、时计数器与进位逻辑掌握闹铃比较器、整点报时触发以及按键去抖状态机的写法并借助工程文件与报告复盘综合结果加深对FPGA可编程逻辑与同步设计的理解。1. 从一块“走不准”的闹铃板子说起Verilog 数字时钟到底能干什么前阵子帮一个做 FPGA 的朋友看他的课程设计一块开发板上跑着数码管时钟能显示时分秒也能设闹铃但问题一堆按键按下去要等半秒才反应闹铃响了关不掉跑一晚上能快出十几秒。他问我是不是代码写错了我看了下其实不是“错”是典型的数字时钟设计里几个关键点没处理干净——按键消抖、跨时钟域同步、BCD 计数进位、闹铃比较逻辑任何一个环节偷懒板子就会用各种玄学现象教你做人。这份 Verilog 时钟带闹铃等功能资源就是围绕这类需求做的完整实现以 FPGA 为平台用纯 Verilog 描述一个可综合的数字时钟系统核心功能包括时分秒计数与显示、按键校时、闹铃设置与触发、整点报时等。它适合正在做数字逻辑课程设计、FPGA 入门练手或者想找一个“麻雀虽小五脏俱全”的时序逻辑参考工程的人。你不需要先精通 Verilog但至少要能看懂 always 块和状态机否则后面调分频和消抖时会比较痛苦。下面我按“先搞清楚它怎么搭起来再动手改最后避开那几个必踩的坑”的顺序拆一遍。2. 拆开看架构分频、计数、显示、闹铃四条线怎么接2.1 系统时钟从哪来分频链与使能脉冲的取舍FPGA 开发板上一般有一个固定的晶振常见是 50MHz。时钟模块要产生 1Hz 的秒信号最直接的做法是写一个分频计数器把 50MHz 分到 1Hz。但这里有个选型问题你是分出一个 1Hz 的时钟信号去驱动后续逻辑还是分出一个 1Hz 的使能脉冲让后续逻辑仍然跑在 50MHz 时钟下、只在使能有效时动作我一般推荐后者。原因是 FPGA 内部对时钟网络有专门布线资源随便用逻辑分频出来的信号当时钟容易产生毛刺和时序问题综合工具也会报一堆警告。用使能脉冲的方式整个设计只有一个时钟域时序分析干净得多。常见做法是// 50MHz - 1Hz 使能脉冲cnt 计到 50_000_000-1 时拉高一个周期 module clk_enable ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 低电平复位 output reg en_1hz // 1Hz 使能宽度一个 clk 周期 ); reg [25:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 26d0; en_1hz 1b0; end else if (cnt 26d49_999_999) begin cnt 26d0; en_1hz 1b1; end else begin cnt cnt 1b1; en_1hz 1b0; end end endmodule这段代码里cnt的位宽要算够50_000_000 需要 26 位2^26 67_108_864。参数说明上如果你板子晶振不是 50MHz把比较值改成频率-1即可。en_1hz只拉高一个时钟周期后续计数器用它当“允许走一秒”的开关。这样写的好处是后面所有逻辑都挂在同一个clk上不会出现“这个模块用 1Hz 时钟、那个模块用 50MHz”的跨时钟域麻烦。2.2 时分秒计数BCD 还是二进制进位怎么处理时间计数有两种常见写法一种是用二进制计数器记总秒数显示时再转 BCD另一种是直接维护六个 BCD 位秒个位、秒十位、分个位……。对于数码管显示的场景我更倾向直接维护 BCD因为省掉了每次显示前的除法转换逻辑直观调试时在仿真波形里一眼就能看出“现在是 12:34:56”。核心是一个秒计数模块在en_1hz有效时递增// 秒计数00~59 循环产生分进位 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sec_lo 4d0; sec_hi 4d0; carry_min 1b0; end else if (en_1hz) begin if (sec_lo 4d9) begin sec_lo 4d0; if (sec_hi 4d5) begin sec_hi 4d0; carry_min 1b1; // 59-00向分进位 end else begin sec_hi sec_hi 1b1; carry_min 1b0; end end else begin sec_lo sec_lo 1b1; carry_min 1b0; end end else begin carry_min 1b0; end end这里的关键是进位脉冲carry_min只维持一个周期分计数模块看到它才加一。分、时的逻辑同理只是时的范围是 00~23判断条件变成“个位到 9 且十位到 2 时归零”。参数上要注意en_1hz和carry_min不能同时被两个 always 块驱动进位链要单向传递否则综合出多驱动板子行为不可预测。2.3 数码管动态扫描位选与段选的时序配合时间值有了要显示到数码管上。多位数码管一般是动态扫描轮流点亮每一位靠人眼视觉暂留形成“同时亮”的效果。扫描频率通常取 1kHz 左右太低会闪太高则亮度不足。常见做法是用一个计数器分出一个扫描使能再配合位选信号切换。// 扫描计数器50MHz 分到约 1kHz 换位 reg [15:0] scan_cnt; reg [2:0] scan_sel; always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt 16d0; scan_sel 3d0; end else if (scan_cnt 16d49_999) begin scan_cnt 16d0; scan_sel scan_sel 1b1; // 6 位数码管0~5 循环 end else begin scan_cnt scan_cnt 1b1; end endscan_sel用来选择当前点亮哪一位同时决定段选输出哪一段码。段码表一般用 case 语句实现把 0~9 对应的七段编码列出来。这里容易翻车的地方是位选和段选的更新时机如果位选变了、段选还没跟上会出现“鬼影”——某一位短暂显示上一位的数字。解决办法是在同一个时钟沿更新位选和段选或者在位选切换后插入一个消隐周期。2.4 闹铃比较相等判断还是“到点触发”闹铃功能的本质是把当前时间和设定的闹铃时间做比较。最直觉的写法是if (hour alm_hour min alm_min)相等就拉高闹铃输出。但这样有个问题如果闹铃时间设成当前时间它会一直响因为条件持续成立。更合理的做法是做一个“到点触发”的边沿检测——当比较结果从“不相等”变成“相等”的那一刻产生一个脉冲去置位闹铃标志标志再由按键清除。// 闹铃触发比较结果上升沿产生一个周期脉冲 reg match_d; always (posedge clk or negedge rst_n) begin if (!rst_n) match_d 1b0; else match_d time_match; // time_match 是组合比较结果 end wire alm_trigger time_match ~match_d; // 上升沿alm_trigger拿去置位一个alm_flag寄存器蜂鸣器输出就看这个 flag。清除只能靠按键这样闹铃响后不会自己停符合真实闹钟的行为。参数上闹铃时分的存储用两组 BCD 寄存器设置模式下由按键修改正常模式下只读。3. 动手改一版按键消抖、校时状态机与闹铃设置3.1 按键消抖为什么你的按键要按半天才有反应机械按键按下和松开时触点会有几毫秒到十几毫秒的抖动FPGA 跑在 50MHz这个抖动会被采集成几十万个跳变。如果不消抖按一次键可能被识别成几十次状态机直接跑飞。常见消抖方案是检测到按键电平变化后启动一个计数器持续稳定 10~20ms 才确认新状态。// 20ms 消抖50MHz 下计到 1_000_000 module debounce ( input wire clk, input wire rst_n, input wire key_in, // 原始按键按下为低 output reg key_press // 消抖后的按下脉冲 ); reg [19:0] cnt; reg key_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_d 1b1; key_press 1b0; end else begin key_d key_in; if (key_d ! key_in) begin cnt 20d0; // 电平变化重新计时 end else if (cnt 20d1_000_000) begin cnt cnt 1b1; end else if (key_d 1b0 key_press 1b0) begin key_press 1b1; // 稳定按下输出一个脉冲 end else begin key_press 1b0; end end end endmodule参数说明1_000_000对应 20ms如果你觉得按键太“肉”可以降到 500_00010ms。key_press只输出一个周期脉冲方便状态机做“按一次加一”的操作。注意key_d和key_in的比较是在同一时钟域下不要引入异步复位之外的跨时钟信号。3.2 校时状态机正常、设时、设分、设闹铃怎么切时钟需要几种工作模式正常走时、设置小时、设置分钟、设置闹铃小时、设置闹铃分钟。用一个有限状态机来管理最清晰。状态转移由按键驱动一个“模式”键循环切换状态一个“加”键在当前状态下递增对应值。// 简化的模式状态机 localparam NORMAL 3d0, SET_HR 3d1, SET_MIN 3d2, SET_AH 3d3, SET_AM 3d4; reg [2:0] state, next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) state NORMAL; else state next_state; end always (*) begin next_state state; if (mode_press) begin case (state) NORMAL : next_state SET_HR; SET_HR : next_state SET_MIN; SET_MIN : next_state SET_AH; SET_AH : next_state SET_AM; SET_AM : next_state NORMAL; default : next_state NORMAL; endcase end endmode_press是消抖后的模式键脉冲。在SET_HR状态下inc_press让小时加一到 23 归零其他状态类似。这里有个细节设置模式下时钟还走不走我一般让它在设置时暂停走时退出设置再恢复否则你设分钟的时候秒还在跑设完可能已经差了几秒。暂停的实现很简单在en_1hz前面加一个门控en_1hz_gated en_1hz (state NORMAL)。3.3 闹铃设置与蜂鸣器输出怎么让响铃可控闹铃的设定值存在alm_hr、alm_min两组 BCD 寄存器里在SET_AH、SET_AM状态下由inc_press修改。比较逻辑用前面说的边沿触发方式产生alm_trigger置位alm_flag。蜂鸣器输出可以直接接alm_flag也可以做一个 1kHz 左右的方波驱动无源蜂鸣器响起来音调更正常。// 蜂鸣器alm_flag 有效时输出约 1kHz 方波 reg [15:0] buzz_cnt; reg buzz_out; always (posedge clk or negedge rst_n) begin if (!rst_n) begin buzz_cnt 16d0; buzz_out 1b0; end else if (alm_flag) begin if (buzz_cnt 16d24_999) begin buzz_cnt 16d0; buzz_out ~buzz_out; end else begin buzz_cnt buzz_cnt 1b1; end end else begin buzz_cnt 16d0; buzz_out 1b0; end end24_999对应 50MHz 下 1kHz 半周期。alm_flag的清除由“关闹铃”按键完成按下后alm_flag 0。注意alm_flag和buzz_out不要互相驱动各管各的否则综合会报组合环。4. 避坑排查仿真对、上板错的那几个经典原因4.1 现象仿真波形完全正确烧到板子上数码管乱跳原因通常是引脚约束没写对或者数码管共阴共阳搞反了。仿真里你看到的是逻辑值板子上还要经过引脚分配、电平标准、驱动能力这几道关。解决先确认约束文件里时钟引脚、复位引脚、数码管段选位选引脚和原理图一致再确认段码表是按共阴还是共阳写的共阳要取反。拿万用表量一下段选引脚静态电平能快速判断。4.2 现象按键按一次时间跳了好几下原因就是没做消抖或者消抖计数器没在电平变化时清零。解决按 3.1 的写法检测到key_d ! key_in就把计数器清零重新计时稳定 10~20ms 才输出脉冲。另外确认按键脉冲只在一个周期内有效状态机里用边沿检测而不是电平判断。4.3 现象闹铃到点不响或者响了关不掉不响的常见原因是比较逻辑用了组合输出直接驱动蜂鸣器但alm_flag没被正确置位或者闹铃时间寄存器在设置模式下被时钟走时逻辑覆盖了。关不掉则多半是清除逻辑写成了电平敏感按键松开后 flag 又被置位。解决触发用边沿脉冲置位 flag清除用按键脉冲复位 flag两者分开写不要用同一个 always 块里的 if-else 互相干扰。4.4 现象跑一晚上时间快了几秒这是分频参数和晶振实际频率的偏差问题。50MHz 是标称值实际晶振可能有几十 ppm 的误差一晚上累积下来就是几秒。解决如果只是课程设计这个精度可以接受如果要更准可以把分频比较值微调或者用板上的 RTC 芯片。纯 FPGA 逻辑分频做到“一晚上不差”不太现实心里要有这个边界。4.5 现象综合警告一堆说信号被优化掉常见于你定义了一个寄存器但从来没在输出或条件里用过综合工具直接把它删了。解决检查每个中间信号是否真的参与了后续逻辑尤其是调试用的计数器和标志位。如果确实需要保留观察可以把它接到一个空闲的 LED 或输出引脚上。5. 进阶一点把整点报时和闹铃做成可配置的基础功能跑通之后可以在这个工程上做两个小升级既不复杂又能让设计更完整。第一个是整点报时。思路很简单在分计数从 59 跳到 00、且秒也归零的那一刻产生一个报时脉冲驱动蜂鸣器短鸣一声。实现上就是在分进位逻辑里加一个条件判断输出一个单周期脉冲再拿去触发一个 200ms 左右的定时器控制蜂鸣器。注意报时和闹铃可能同时触发两个输出要做优先级或者叠加处理别让蜂鸣器控制信号打架。第二个是把闹铃时间做成可存储的。纯 FPGA 内部寄存器掉电就丢如果你想让闹铃设置保留可以外挂一片 I2C EEPROM在设置完成后把alm_hr、alm_min写进去上电时读回来。I2C 控制器网上有很多参考实现移植时注意时序参数——标准模式 100kHz、快速模式 400kHz你的分频计数器要按这个算。写 EEPROM 还有页写和写周期的等待别连续写太密。验证方法上我习惯先在仿真里跑一个“加速版”把分频比较值改小让 1 秒变成几个时钟周期这样几分钟就能看到时分秒进位、闹铃触发、整点报时的完整波形。确认逻辑对了再改回真实分频上板测。这个习惯帮我省过很多次“上板等半天才发现进位写错”的时间。从那以后我每次做时序逻辑都强制先跑加速仿真、再上板绝不跳过。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →