news 2026/10/20 6:42:22

基于Verilog的时钟监视模块设计:检测停振与频率异常

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Verilog的时钟监视模块设计:检测停振与频率异常

做硬件设计的人应该都有过这种经历:系统上电之后一切正常,主控和外设握手顺畅,可运行到一半,某个外设突然“失联”了。排查半天,总线和电源都没问题,最后把示波器探到时钟引脚上才发现,晶振或者PLL输出的时钟早就停了,或者频率偏得离谱。更麻烦的是,掉时钟这种事往往不会立刻触发复位,主控根本收不到任何通知,只能在超时之后干瞪眼。所以后来我就习惯在系统里面放一个clk monitor模块——用Verilog写,专门盯时钟是否健康。它的职责很简单:检测时钟是否存在、频率是否落在允许范围内,一旦异常立刻拉高报警信号,让主控有时间切备用时钟或者做安全复位。这个模块不算复杂,逻辑不到一百行,但里面的边沿处理、窗口计数、阈值计算这些细节如果没想清楚,做出来的东西不是误报就是漏报。这篇文章我就把整个实现思路、RTL代码、仿真验证和调参踩坑的记录完整放出来,适合正在写时钟监视模块的FPGA/IC工程师,也适合想给自己系统加一层稳妥保护的硬件开发者。

1. clk monitor 到底在解决什么问题?先搞清楚需求再动手

1.1 时钟出问题时,系统里会发生什么

时钟是数字系统的“心跳”,它掉了,系统不会瞬间冒烟,但会以各种诡异的方式开始表现:寄存器状态机卡在某个状态不动了、FIFO读写指针原地踏步、串行协议因为采样点错位开始疯狂报CRC错误、ADC采出来的数据突然全是同一个值。最难受的是,这些问题往往没有立即可感知的物理特征,因为电源灯还亮着,芯片温度正常,看起来一切都没死,但系统就是“活”不过来了。

时钟异常还不只是“停”一种。PLL失锁之后频率可能往上跳,也可能往下飘;外部晶振虚焊、谐振电容老化,输出频率会慢慢偏离标称值。频率偏高时,处理器可能超频跑但不稳定;频率偏低时,通信波特率、PWM周期、音频采样率全都会跟着错。总之,凡是依赖时钟周期的功能,都会受到牵连。

clk monitor的价值就在这里:它不关心总线状态,不看复位,不读寄存器,只做一件事——持续观察某一路时钟是不是“正常”。一旦异常,立刻输出报警信号。这个报警可以作为中断源通知CPU,也可以硬连线直接触发时钟切换逻辑或者系统复位。在很多对可靠性要求比较高的设备里,这种模块属于典型的安全监测单元,和看门狗平级。

1.2 什么情况下才值得自己写一个

商用IP库里通常也有现成的时钟监视单元,功能甚至更全,但很多时候不太适合直接用。一是授权成本问题,一套IP license可能比整个FPGA芯片还贵;二是配置太重,我要的只是“有无/偏高/偏低”三态判定,商用IP给出的却是频率精测、自动校正、中断矩阵这一大堆东西,集成成本反而上去了;三是参数不便改,不同板子上被监测时钟的频率不一样,商用IP的配置寄存器方案每次都要重新弄一遍驱动。

用Verilog自己写一个就不一样了。逻辑简单、占资源少、阈值参数可调,例化几十份也毫无压力。我在之前的模拟项目中就用它同时监视了三路时钟,分别是给ADC的、给串口的、给PLL反馈的,三路频率不同,阈值不同,但共享同一个参考时钟。总共占不到一千个逻辑单元,效果非常稳定。

自己写的另一个理由是可以完全理解它的行为,出问题时不至于对着黑盒IP抓瞎。下面就先解决一个核心问题:怎么判断时钟“不健康”。

2. 检测思路与方案选型:参考时钟对比最省心

2.1 三种常见思路对比

做时钟监视有几种主流思路,各有利弊,我一开始也是纠结过的。这里直接对比一下:

检测思路实现方式能检测停振能检测频率偏高/偏低需要额外硬件复杂度
超时检测法用待测时钟驱动计数器,设置超时值能不能自振荡器或长延迟链最低
独立基准振荡器法用RC振荡器/PLL生成基准,对比频率能能基准振荡器/PLL中等
参考时钟边沿计数法用较高频率参考时钟采样待测时钟,统计窗口内边沿数能能无需额外,复用已有参考时钟中等偏低

超时检测法最朴素:让待测时钟自己去驱动一个计数器,如果计数器到达某个上限还不见清零,那就说明时钟一个脉冲都没来,直接报停振。这个方法对“停”很敏锐,但对“频率偏移”完全无能为力,因为时钟只要在跑,哪怕频率只有正常值的一半,计数器照样能超时。只适合做最简单的心跳检测。

独立基准振荡器法比较直接,拿一个已知稳定的振荡器当尺子去量待测时钟,频率高低一看便知。但它需要额外硬件,如果系统里没有现成基准源,得自己折腾,板级成本会上去。

2.2 为什么选参考时钟边沿计数

我最终选择的是“参考时钟边沿计数”路线,核心思路是:用一个频率足够高的参考时钟去采样待测时钟,把待测时钟的上升沿转换成参考时钟域的单周期脉冲,然后在固定长度的窗口里统计脉冲个数。这个计数值和待测时钟频率成正比,落在阈值区间就算正常,否则报警。

这个路子有三个很实在的优点。第一,不需要任何额外硬件,系统里一般都有现成的稳定时钟源,直接拿来当参考。第二,整个模块除了一个两级同步器之外,全部逻辑都跑在参考时钟域,综合和约束都特别好处理,不存在乱七八糟的跨时钟域控制信号。第三,精度可调,窗口开得越长,对频率的分辨能力就越精细,可以灵活适配不同场景。

它也有个明显的适用边界:参考时钟频率必须明显高于待测时钟。原因很简单,参考时钟相当于在给待测时钟的上升沿“拍照”,如果待测时钟太快,两张照片之间可能漏掉一个上升沿,计数就会偏小,导致误报警。按我的经验,参考频率最好是待测频率的5倍以上,最少不能低于2倍。如果你要监视的时钟比参考时钟还快,那这套方案就不适合了,老老实实找带高频支持的专用时钟监视IP吧。

2.3 参数到底该怎么定

这个模块的核心参数有三个:窗口长度、下限阈值、上限阈值。很多人在这里想当然,随手填一个阈值就上板,结果就是疯狂误报。

先给公式。窗口的有效时间长度是:

T_win = (WIN_LEN - 1) / f_ref

在窗口内,待测时钟上升沿的理论计数值是:

N_expected = T_win × f_mon

阈值一般按允许的容差来定,比如允许频率偏差在±15%以内,那就是:

LOW_LIMIT = N_expected × (1 - 15%) HIGH_LIMIT = N_expected × (1 + 15%)

举一个具体数字。假设参考时钟 100MHz,待测时钟 10MHz,窗口计数值上限 WIN_LEN 取 65536。那么一个窗口的有效长度是 65535 个参考时钟周期,也就是 655.35 微秒。在这段时间里,10MHz 时钟理论上会出现大约 6553.5 个上升沿。因为窗口起始点和时钟相位不固定,实际计数值可能是 6553 或 6554,这都属于正常。如果允许±15%偏差,下限取 5570,上限取 7536,对应频率大约 8.5MHz 到 11.5MHz。

窗口长度选多少,本质上是分辨率和响应时间的折中。窗口越长,每个计数代表频率变化量越小,监测越精细,但报警反应越慢;窗口越短,响应快,可是频率分辨率差。用上面的参数,一个计数对应大约 1.5kHz 的频率变化。如果你的系统只需要判断大概范围,窗口可以缩短到 4096 或者 1024,报警延迟会小很多。

3. 模块实现:边沿检测加窗口计数器,逻辑不到一百行

3.1 接口设计

这个模块的接口我设计得比较精简,方便集成。先定义信号:

信号方向位宽功能说明
clk_refinput1参考时钟,要高于待测时钟
rst_ninput1异步复位,低有效
clk_moninput1待监测时钟
eninput1监视使能,高电平开始工作
clr_alarminput1清除报警锁存信号
alarmoutput1报警输出,高有效
last_cntoutputWIN_WIDTH上一个窗口的实际计数值,供调试用

alarm之所以设计成锁存输出,是因为它可能连接到一个响应速度没那么快的主控。如果报警信号只在检测到故障的瞬间拉高,下一拍就被自动清掉,主控万一没来得及采样,故障就被漏掉了。所以一旦报警,alarm保持为高,直到外部控制器明确送一个 clr_alarm 脉冲才会清除。这在实际系统里非常重要。

3.2 运作时序

整个模块的工作流程可以这样理解:从复位释放开始,只要 en 拉高,模块就进入一个周而复始的测量循环。参考时钟域里有一个窗口计数器,从 0 到 WIN_LEN-1 循环计数。在这个循环中,除了最后一个周期之外,其余时间都作为“有效窗口”。有效窗口期间,模块统计的是待测时钟上升沿产生的脉冲个数;等窗口计数器走到最后一个周期,停止计数,把本次计数值和阈值比较,更新报警输出;紧接着下一个窗口自动开始,继续新一轮测量。

换句话说,这个模块并不是只在开机时检测一次,而是持续检测,每隔一个窗口时间就刷新一次判断结果。就算当前窗口报了警,只要外部不清除,它保持报警;如果想要故障恢复后自动解除报警,也很容易改,把锁存逻辑去掉就行。我在代码里保留了两种风格的切换点,后面会有说明。

3.3 Verilog实现(核心代码)

下面是完整的可综合RTL代码,我尽量加了注释。参数默认值是参考 100MHz,待测 10MHz,窗口 65536 个参考周期,阈值允许±15%。

module clk_mon #( parameter WIN_WIDTH = 16, // 窗口计数器位宽 parameter WIN_LEN = 16'd65536, // 窗口长度(按参考时钟周期计) parameter LOW_LIMIT = 16'd5570, // 下限阈值 parameter HIGH_LIMIT = 16'd7536 // 上限阈值 )( input wire clk_ref, input wire rst_n, input wire clk_mon, input wire en, input wire clr_alarm, output reg alarm, output wire [WIN_WIDTH-1:0] last_cnt ); // ---------------------------------------------------------- // 1. 将 clk_mon 同步到参考时钟域,并检测上升沿 // ---------------------------------------------------------- reg [1:0] mon_sync; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) mon_sync <= 2'b00; else begin mon_sync[0] <= clk_mon; mon_sync[1] <= mon_sync[0]; end end // 上升沿脉冲:当输入上升沿传播到第一级同步器且尚未传播到第二级时产生 wire mon_rise = mon_sync[0] & ~mon_sync[1]; // ---------------------------------------------------------- // 2. 窗口计数器 // ---------------------------------------------------------- reg [WIN_WIDTH-1:0] win_cnt; wire win_done = (win_cnt == WIN_LEN - 1'b1); wire win_active = en & ~win_done; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) win_cnt <= 'd0; else if (en) begin if (win_done) win_cnt <= 'd0; else win_cnt <= win_cnt + 1'b1; end else win_cnt <= 'd0; end // ---------------------------------------------------------- // 3. 窗口内待测时钟上升沿计数 // ---------------------------------------------------------- reg [WIN_WIDTH-1:0] mon_cnt; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) mon_cnt <= 'd0; else if (win_done) mon_cnt <= 'd0; // 窗口末尾清零,为下一窗口准备 else if (win_active & mon_rise) mon_cnt <= mon_cnt + 1'b1; end // ---------------------------------------------------------- // 4. 判决与报警锁存 // ---------------------------------------------------------- reg [WIN_WIDTH-1:0] last_cnt_r; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin alarm <= 1'b0; last_cnt_r <= 'd0; end else if (clr_alarm) begin alarm <= 1'b0; // 外部手动清报警 end else if (win_done & en) begin last_cnt_r <= mon_cnt; // 记录本次窗口的计数值 if (mon_cnt < LOW_LIMIT || mon_cnt > HIGH_LIMIT) alarm <= 1'b1; // 超范围,报警并锁存 end end assign last_cnt = last_cnt_r; endmodule

这个代码的功能可以概括成一句话:参考时钟域里开一扇固定宽度的“计数窗”,窗内数一下待测时钟上升沿来了多少次,窗关上的瞬间判断这个数是否正常。

3.4 代码里的几个隐藏细节

第一点,注意 clk_mon 是被当成普通数据信号采样的,不是接到时钟网络上。这一点很多人会误解。我们把待测时钟作为输入引脚进来,经过两级触发器同步,再在参考时钟域里做边沿检测。这样 clk_mon 完全不需要进入全局时钟树,不会占用宝贵的时钟资源,也不需要额外的硬核时钟缓冲。代价就是它必须慢于参考时钟,这个前提我们前面已经分析了。

第二点,两级同步器只能消除亚稳态传递到后续逻辑的风险,并不能保证每个上升沿都恰好被采到。待测时钟的上升沿落在参考时钟采样点附近时,可能会在同步器里多“犹豫”一拍,导致边沿脉冲晚一个周期出现。单看一个上升沿这算正常,但窗口边界处就可能出现多一个或者少一个计数值的误差。解决办法就是阈值留余量,别把容差卡得太死。

第三点,这个模块用的复位是异步复位,但所有同步器输出本身也是参考时钟域内的寄存器,所以复位不存在跨时钟问题。只要参考时钟本身稳定,复位释放之后第一个窗口内就一定能开始测量。如果参考时钟也长时间没有,那系统已经整体失效,不在这个模块的管辖范围内。

4. 仿真验证:怎么证明这个模块真的会报警

4.1 testbench搭建思路

写仿真之前先想清楚要验证什么。我的测试场景很明确:正常频率不报警、频率偏低报警、频率偏高报警、停振报警、报警锁存和清除逻辑正确。每种场景至少跑完两个窗口以上,确保看到完整的判决结果。

如果直接用默认的大窗口参数,仿真一次要等 655 微秒,虽然看起来不长,但在仿真器里跑几万个参考时钟周期还是有点浪费时间。我习惯在仿真例化时把窗口参数压小,比如 WIN_LEN 取 1024,阈值也按比例算好,验证逻辑行为后再用真实参数做一次回归。这种“参数量级缩放”的方法在仿真调试里特别实用。

下面给一个可用的testbench框架,重点不是完整仿真库,而是把思路演示清楚:

module tb_clk_mon; parameter REF_PERIOD = 10; // 参考时钟 100MHz reg clk_ref = 0; always #(REF_PERIOD/2) clk_ref = ~clk_ref; reg rst_n = 0; reg clk_mon = 0; reg en = 0; reg clr_alarm = 0; wire alarm; wire [15:0] last_cnt; // 例化被测模块,窗口和阈值都缩小 clk_mon #( .WIN_WIDTH (16), .WIN_LEN (16'd1024), .LOW_LIMIT (16'd90), .HIGH_LIMIT (16'd115) ) u_clk_mon ( .clk_ref (clk_ref), .rst_n (rst_n), .clk_mon (clk_mon), .en (en), .clr_alarm (clr_alarm), .alarm (alarm), .last_cnt (last_cnt) ); // 生成指定频率的待测时钟 task set_mon_freq(input real freq_mhz); begin clk_mon = 0; // 根据频率算周期,半周期翻转;不支持动态实时改频率, // 但在仿真中通过停止再重新启动的方式模拟不同场景 end endtask integer i; initial begin // 复位和启动 rst_n = 0; #100; rst_n = 1; #50; en = 1; // 场景1:正常频率 10MHz clk_mon = 0; forever #50 clk_mon = ~clk_mon; // 10MHz // 实际运行两个窗口,检查 alarm 维持 0 // ... // 场景2:切换到偏低频率 5MHz // 重新生成 5MHz 时钟,再跑两个窗口,检查 alarm 变为 1 // ... // 场景3:清除报警 // 发送 clr_alarm 脉冲,检查 alarm 变回 0 // ... $finish; end // 自动检查 initial begin wait (alarm === 1'b1); $display("ALARM raised as expected at time %0t", $time); end endmodule

注意testbench里不能真的在一个forever语句启动后随便改频率,实际要用disable或换用可变量周期生成时钟,甚至在场景间停止一个进程再启动另一个。上面只是演示框架,不是完整可跑的仿真,但思路是准确的。

4.2 测试用例与预期结果

用真实参数(100MHz参考,窗口65536)来计算几种场景的预期计数:

场景待测频率理论计数值阈值范围预期alarm
正常10MHz约65535570~75360
偏低5MHz约32775570~75361
偏高20MHz约131075570~75361
停振0Hz05570~75361

仿真时重点观察两处波形:一是 win_done 信号上升沿附近,last_cnt 是否更新成预期值;二是 alarm 是否在窗口结束后紧接着拉高。如果 last_cnt 和预期值差得比较多,就先查同步采样和边沿检测的正确性,再查窗口长度算得对不对。

5. 实测踩坑与调参心得

5.1 误报警的三大来源

第一个来源是阈值设置太紧。很多初学者看到10MHz的时钟就取±1%容差,结果实测时被时钟jitter、PLL锁定暂态、温度漂移这些因素直接打脸。参考时钟和待测时钟都是真实器件,不可能像仿真里那么理想。我的经验是:如果只是做健康监测,±15%到±20%是比较合理的起点;如果对频率精度要求很高,说明你需要的不是监视器,而是频率计。

第二个来源是上电时序。待测时钟如果是PLL生成的,PLL锁定之前输出频率是乱的,甚至可能没有输出。如果这个模块一复位就开始监测,第一个窗口几乎必然报警。这不一定是缺陷,但需要系统设计者配合:要么等PLL locking信号有效之后再拉高en;要么给模块加一个初始等待时间;要么把这个报警当成上电过程中的预期事件,在控制器里屏蔽掉。我实际做的时候是在上层逻辑里控制en的时机,模块保持通用。

第三个来源是待测频率和参考频率关系太接近。如果 clk_mon 频率超过了 clk_ref 的一半,边沿检测就会开始丢失上升沿。丢一个沿,计数值就小一点,频率越高丢得越多,最终误报警。这个属于选型问题,不是调参能解决的。

5.2 调试技巧

调试这个模块时,last_cnt 比 alarm 值钱得多。alarm只告诉你“坏了”,last_cnt却告诉你“坏到什么程度”。用FPGA上集成的片上逻辑分析仪抓一下last_cnt,和示波器实测的频率对照,马上就能看出阈值该往哪个方向调。

我还在项目里加了一个很实用的扩展:连续故障计数。每次窗口结束,如果超范围,故障计数器加一;如果正常,故障计数器清零;连续三到五次超范围才正式拉高alarm。这样能滤掉偶发的抖动尖峰,对于工业现场环境特别管用。代价只是报警延迟增加几个窗口周期,完全可以接受。

5.3 综合与约束注意事项

这个模块虽然简单,但毕竟是跨时钟设计。综合时一定要把那一段异步路径约束正确:clk_mon 输入到第一个同步触发器之间是异步路径,可以设成 false path;第一个同步触发器到第二个同步触发器之间是同步器本身,不能设置 false path,必须让时序工具去优化。如果两级同步器之间的路径没有约束好,合成的电路抗亚稳态能力会缩水。这个细节看起来不起眼,但在高可靠系统里就是天壤之别。

另外,如果你在一个FPGA工程里例化了多份 clk_mon,共享同一个 clk_ref,那么每一路 clk_mon 的输入到各自同步器的异步约束都要单独设置。很多工具支持按引脚本批量生成,别偷懒用全自动约束,最好检查一遍有没有遗漏。

留给你的一个进阶扩展

如果觉得这个模块还不够“稳”,可以再做两件小事。一是把“连续N次故障”逻辑加进去,这个小改动可以让误报率降一个量级。二是把阈值改成两个,一路用于“过低报警”,一路用于“过高报警”,这样主控不仅知道时钟坏了,还能知道是往哪个方向坏的,对故障处理非常有帮助。这两处改动的量都不大,但实用性提升明显,我建议你拿到代码之后第一件事就是加上这两块。

我个人的体会是,clk monitor这类模块做起来不难,真正难的是对边界情况的处理和足够保守的参数设计。它本质上不是一个精密测量仪器,而是一张安全网。宁可报警阈值设得宽一点,也不要因为频繁误报导致这个模块被系统工程师嫌弃。宽松的阈值加上连续确认,换来的是“要么不响、响则真有问题”的可靠行为,这才是硬件设计里最需要的安全感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 6:42:06

Multi-Agent开会机制:如何解决复杂工程中的上下文碎片化问题

1. 先把痛点聊清楚&#xff1a;单模型为什么压不住复杂工程我最近在推进某跨平台系统升级项目&#xff0c;需求面拉得很宽&#xff1a;前端交互、后端调度、数据迁移、性能优化都要同步推进。最初我图省事&#xff0c;把全部项目背景塞进单个模型的上下文&#xff0c;让它一口气…

作者头像 李华
网站建设 2026/10/20 6:41:37

artcraft 创意编程实战:从设计思路到工程交付的完整拆解

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是“艺术”加“手艺”的组合词。直觉告诉我&#xff0c;这不是一个纯技术框架的名字&#xff0c;也不是某个具体产品的品牌名&#xff0c;它…

作者头像 李华
网站建设 2026/10/20 6:41:37

artcraft:艺术与工艺融合的创意工程化实践指南

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是&#xff1a;这大概率是一个把“艺术创作”和“手工实现”揉在一起的项目。art 是艺术&#xff0c;craft 是手艺、工艺&#xff0c;合在一…

作者头像 李华
网站建设 2026/10/20 6:41:37

2024 AI大模型产品实战指南:会用AI的人这样提效

2024年聊得最多的一句话&#xff0c;大概就是这句&#xff1a;“打败你的不是AI&#xff0c;而是会用AI的人”。这句话不是贩卖焦虑&#xff0c;而是过去一年里真真切切发生在很多团队里的事。同样做一个活动方案、同样做一份行业调研、同样整理一堆客户数据&#xff0c;有人用…

作者头像 李华
网站建设 2026/10/20 6:41:10

【仓颉语言入门 · 第二篇 第 9 课】

【仓颉语言入门 第二篇 第 9 课】查询&#xff1a;SELECT 与结果集遍历 上一课我们建表又删表&#xff0c;折腾的都是结构&#xff1b;而 sys_user 里躺着的 9 行真实用户数据&#xff0c;一直等着被读。增删改查里&#xff0c;查是最高频的一个&#xff0c;也是唯一"有去…

作者头像 李华