尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

等精度测频原理与Verilog实现:电赛数字频率计设计全解析

等精度测频原理与Verilog实现:电赛数字频率计设计全解析 简介一套基于Verilog的等精度测频完整工程方案源自2015年电赛F题适合FPGA初学者、电子竞赛参赛者及数字系统设计相关课程实践。工程采用50MHz采样时钟通过计数器测量输入信号边沿并借助UART串口将频率结果发送至上位机覆盖从测量原理到接口通信的完整链路。资源共428个文件压缩包约31.63MB除Quartus工程配置qpf/qsf/qws和主源码v文件外还包含大量编译中间文件、备份文件及仿真相关数据便于还原工程环境、学习Quartus使用和对比调试。目前已有1316人学习下载。设计中包含等精度测量、同步计数、UART波特率配置等关键知识点主逻辑与uarttx、clkdiv、Average等子模块分离结构清晰适合参赛者深入理解测频原理、复现实验或在其基础上扩展功能是一份兼顾原理讲解与工程实践的典型电赛参考资源。 2015年的电赛F题里等精度测频算是一个绕不开的硬骨头。当时题目要求做一个数字频率计频率范围从低频一直覆盖到几十MHz还要求精度高、响应快。很多人一上来就用传统的测频法或者测周法结果低频段误差大、高频段又追不上折腾到最后才发现真正适合这种宽范围高精度场景的方案就是等精度测频。这篇文章我就把这套方案的原理、Verilog实现、仿真调试经验一并写出来代码也是当时实际跑过的结构给你一条尽量少踩坑的路。这篇文章适合正在准备电赛、课程设计或者刚接触FPGA频率测量的朋友。你需要的基础是Verilog的基本语法懂一点时序逻辑和计数器概念就够。我会先讲为什么等精度测频能做到全频段统一精度再拆系统模块然后给核心代码并逐段解释最后把调试中容易翻车的几个点都列出来。1. 为什么等精度测频能同时兼顾宽范围和精度先说结论等精度测频的核心思路是让测量精度不再依赖被测频率本身而是只取决于参考时钟的质量和闸门时间的长短。这一点让它和传统方法有本质区别。传统测频法是在固定闸门时间内数被测信号的脉冲个数频率等于计数值除以闸门时间。它的误差来源主要是闸门边缘和被测信号边缘不对齐会产生±1个脉冲的计数误差。被测频率越低单位时间内脉冲越少这±1个脉冲的占比就越大误差也就越难看。比如1Hz的信号闸门1秒只数到1个脉冲误差可能高达50%。传统测周法则刚好反过来它测量被测信号一个周期内有多少个参考时钟脉冲适合低频但高频时一个周期太短参考时钟计数太少误差又被放大。等精度测频的办法很巧妙它不再用固定闸门去数被测信号而是让实际闸门跟随被测信号对齐。具体做法是用一个大的预置闸门信号然后用被测信号的上升沿去同步这个闸门得到一个“实际闸门”。在这个同步后的闸门内同时数被测信号脉冲数和参考时钟脉冲数然后按照下面的公式计算频率Fx Nx / Ns * Fs其中Nx是被测信号在闸门内被数到的完整脉冲数Ns是同一闸门内参考时钟的脉冲数Fs是参考时钟频率。因为实际闸门是被测信号上升沿同步过的所以Nx理论上没有±1误差严格说是被测信号整周期计数。误差主要来自Ns的±1。随着闸门时间和参考时钟频率变高Ns变得很大所以相对误差就被压得很低了。而且这个误差公式里不再出现被测频率也就是说测1Hz和测10MHz理论精度是一样的。这一点对电赛那种宽范围题目来说直接决定了方案可行性。2. 系统模块划分与关键设计思路2.1 顶层架构该拆成哪几个模块这套频率计在FPGA内部至少需要这样几个部分闸门控制模块、同步模块、两个计数器、数据锁存与运算模块、显示或串口输出模块。你也可以加一个简单的按键量程切换不过等精度测频本身不需要量程切换这是它另一个优势。闸门控制模块负责产生预置闸门信号比如100ms或者1s。这个预置闸门不需要严格精确因为它只是“粗闸门”真正决定测量时长的实际闸门还要经过被测信号同步。所以有人直接用计数器分频产生预置闸门没有问题。同步模块是整个设计的核心之一。因为被测信号和FPGA内部系统时钟不是同一个时钟源直接拿预置闸门去开计数闸门会出现亚稳态。正确的做法是用被测信号的上升沿去同步预置闸门产生一个与被测信号对齐的enable信号。两个计数器分别对Nx和Ns进行计数。Nx计数器在enable有效时每个被测信号上升沿加1Ns计数器在enable有效时每个系统时钟上升沿加1。注意Ns计数器的时钟必须和被测信号无关一般直接用系统时钟同时系统时钟也是参考时钟源。数据锁存模块的作用是在实际闸门结束后把两个计数器的值同时锁存下来。这里要特别注意“同时”不能先锁Nx再锁Ns否则闸门已经结束时两个计数值不是同一段时间内的计算结果就是错的。正确的做法是用闸门结束信号被测信号的下降沿作为锁存触发一个时钟周期内同时把Nx和Ns寄存器更新。运算模块做除法Fx Nx / Ns * Fs。FPGA做除法通常会有时序压力尤其是还要接BCD显示的话工作量不小。这里有几个选择一是直接用除法器IP核二是自己用流水线写一个移位减法的除法器三是在上位机或单片机上做运算FPGA只把Nx和Ns发出去。电赛场景下我自己是优先用FPGA算好然后直接送数码管但如果你接了MCU或者板子上的FPGA资源比较吃紧把原始计数值发出去由上位机算是更省事的路径。2.2 实际闸门时序到底怎么产生很多第一次做等精度测频的人都会在这里卡住。预置闸门和实际闸门的关系我建议画一个简单时序图预置闸门gate_pre拉高之后并不立即开启计数。等到被测信号的下一个上升沿到达才把实际闸门gate_act拉高。预置闸门gate_pre拉低之后也不立即停止计数。等到被测信号的下一个上升沿到达才把实际闸门gate_act拉低同时产生一个锁存脉冲。这样设计的目的是确保实际闸门内包含的Nx一定是被测信号的整数个周期从而消除Nx的量化误差。实现上这个逻辑用状态机或者简单的边沿检测都能写清楚。我见过有人直接拿预置闸门作为计数使能结果频率一变误差就飘忽不定测低频尤其明显。问题根源就在于没有做信号对齐。3. Verilog核心代码逐段解析3.1 预置闸门产生模块预置闸门用系统时钟分频产生即可。假设系统时钟是50MHz想要100ms的闸门那么计数值就是5_000_000。我用一个计数器循环计数输出gate_pre。需要注意的是gate_pre拉高和拉低的时间并不要求精确相等只要总周期够长就行实际以最后Ns计数为准所以这里的“粗”是完全允许的。module gate_gen #( parameter CLK_FREQ 50_000_000, parameter GATE_MS 100 )( input wire clk, input wire rst_n, output reg gate_pre ); localparam CNT_MAX (CLK_FREQ / 1000) * GATE_MS - 1; localparam CNT_HALF CNT_MAX / 2; reg [$clog2(CNT_MAX1)-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt d0; gate_pre 1b0; end else if (cnt CNT_MAX) begin cnt d0; gate_pre ~gate_pre; end else begin cnt cnt 1b1; end end endmodule这里参数化做得很直接改闸门时间只需要改GATE_MS。不过要注意CNT_MAX的计算用了乘法如果CLK_FREQ很大位数会变宽但FPGA综合器通常会优化掉不会真做一个乘法器因为是常量运算。3.2 同步与实际闸门生成这一块是整个设计的关键。预置闸门gate_pre是系统时钟域的信号要过渡到被测信号fx的时钟域。我采用的方法是用fx的上升沿对gate_pre进行采样然后生成同步的gate_act。module sync_gate ( input wire fx, // 被测信号 input wire gate_pre, // 预置闸门 input wire rst_n, output reg gate_act // 实际闸门 ); reg gate_pre_d1; reg gate_pre_d2; always (posedge fx or negedge rst_n) begin if (!rst_n) begin gate_pre_d1 1b0; gate_pre_d2 1b0; end else begin gate_pre_d1 gate_pre; gate_pre_d2 gate_pre_d1; end end wire gate_pre_syn gate_pre_d2; always (posedge fx or negedge rst_n) begin if (!rst_n) begin gate_act 1b0; end else if (gate_pre_syn !gate_act) begin gate_act 1b1; end else if (!gate_pre_syn gate_act) begin gate_act 1b0; end end endmodule这里做两级触发器同步主要是为了降低亚稳态传播到控制路径的风险。虽然被测信号的频率通常不高但工业级可靠性还是建议至少打两拍。注意这里的逻辑是在fx的时钟域下工作的gate_act的翻转沿和fx上升沿严格对齐。3.3 双计数器与同时锁存有了gate_act之后两个计数器就可以工作了。Nx计数器在fx的上升沿计数Ns计数器在系统时钟clk的上升沿计数两者都受gate_act控制。module freq_counter ( input wire clk, input wire fx, input wire gate_act, input wire rst_n, output reg [31:0] nx, output reg [31:0] ns, output reg data_valid ); reg [31:0] nx_tmp; reg [31:0] ns_tmp; always (posedge fx or negedge rst_n) begin if (!rst_n) begin nx_tmp 32d0; end else if (gate_act) begin nx_tmp nx_tmp 1b1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin ns_tmp 32d0; end else if (gate_act) begin ns_tmp ns_tmp 1b1; end end always (posedge fx or negedge rst_n) begin if (!rst_n) begin nx 32d0; ns 32d0; data_valid 1b0; end else if (!gate_act) begin nx nx_tmp; ns ns_tmp; data_valid 1b1; end end endmodule这个写法里有几个细节要说明。锁存条件用的是fx的上升沿且gate_act为低也就是实际闸门结束后的第一个fx上升沿。此时nx_tmp和ns_tmp都已经稳定反映了整个闸门内的计数值此时同时锁存是安全的。data_valid拉高一个周期用于通知后续模块取数。寄存器位宽32位按100ms闸门、50MHz参考时钟来算Ns最大约5_000_00032位远远够用。如果闸门时间要进一步加大可以自己算一下再扩位宽。3.4 频率数值计算与BCD输出运算部分我用了一个简单的移位减除法器来计算fx nx / ns * fs。这里fs通过参数传入例如50_000_000。module calc_freq ( input wire clk, input wire rst_n, input wire [31:0] nx, input wire [31:0] ns, input wire data_valid, output reg [31:0] freq_hz ); // 用移位减实现除法以nx / ns为例 reg [63:0] dividend; reg [63:0] divisor; reg [31:0] quotient; reg [5:0] bit_cnt; reg state; localparam IDLE 1b0; localparam DIVIDE 1b1; always (posedge clk or negedge rst_n) begin if (!rst_n) begin dividend 64d0; divisor 64d0; quotient 32d0; bit_cnt 6d0; state IDLE; freq_hz 32d0; end else begin case (state) IDLE: begin if (data_valid) begin dividend {32d0, nx}; divisor {32d0, ns}; quotient 32d0; bit_cnt 6d31; state DIVIDE; end end DIVIDE: begin if (dividend divisor) begin dividend dividend - divisor; quotient {quotient[30:0], 1b1}; end else begin quotient {quotient[30:0], 1b0}; end dividend dividend 1; if (bit_cnt 0) begin freq_hz quotient * 32d50_000_000; state IDLE; end else begin bit_cnt bit_cnt - 1b1; end end endcase end end endmodule移位减实现除法优点是纯组合逻辑加状态机不用依赖IP核缺点是慢。32位的除法需要32个时钟周期才能出结果但频率计一个闸门就是100ms期间有足够的时间去算所以完全够用。最后freq_hz得到的是整数频率值单位Hz。如果还想显示到小数点后几位需要把Fs乘以10的倍数再算除法这个可以根据显示需求灵活调整。BCD转换可以用类似“双四舍五入加3算法”Double Dabble把二进制转成十进制位然后送数码管。这部分属于常见代码我就不展开了网上有很多写法可以直接抄。4. 亚稳态处理和时钟域安全这块最容易翻车4.1 为什么要同时处理两个时钟域这个设计里天然存在两个不同频率的时钟系统时钟clk比如50MHz晶振和被测量信号fx。gate_act是在fx时钟域下生成的但Ns计数器需要在clk时钟域下工作Nx计数器则需要在fx时钟域下工作。如果直接把gate_act接到clk时钟域的寄存器上就可能出现建立时间不满足的情况寄存器采到不确定电平进而导致后续状态错乱。解决办法其实已经在代码里体现了同步模块把gate_pre从clk域转移到fx域这个转移过程本身就是在fx时钟下打两拍。但要注意转移后的gate_act不能反向再拿到clk域直接用因为它和clk之间依然没有同步关系。那为什么Ns计数器还是能可靠计数呢因为在count模块里Ns计数器只是在gate_act为高时使能累加即使gate_act从0变1或从1变0的时刻相对clk来说有一定相位差也不会造成计数逻辑的不稳定最多是使能信号变化的那一拍Ns可能不多计或者少计而这一点恰好通过后续除法公式补偿掉了这正是等精度测频的原理优势。4.2 打两拍真的够吗打两拍并不能完全消除亚稳态它只能大幅降低亚稳态传播到逻辑电路的概率。对频率计这种低频测量场景fx最高也就几十MHzclk用50MHz两级寄存器的MTBF已经非常大实际中几乎不会触发。如果你想做更严谨可以在两级同步后再加一级边沿检测并且确保同步寄存器到后续逻辑之间有足够的时间余量。还有一个容易忽略的点如果被测信号是占空比非常小的窄脉冲直接采样的风险会上升。这时候可以先用一个高速时钟对fx做展宽或者直接用外部整形电路把信号变成标准的方波再进FPGA。电赛时信号源输出的通常已经是标准方波问题不大但如果接的是传感器输出就必须额外注意。5. 仿真验证和板级调试经验5.1 仿真时如何验证精度写testbench的时候我给fx输入一个非整数频率比如105.35kHz。之所以选这种“不整”的频率是因为整数频率很容易掩盖计数误差。通过设置系统时钟为50MHz周期20nsfx为105.35kHz仿真运行200ms以上然后检查freq_hz寄存器里的结果是否接近105350。仿真里要注意一个问题如果你直接让fx和clk完全同步比如都用同一个时钟分频产生那测出来的精度会好得不真实因为两个信号相位完全固定误差被隐藏了。更贴近实际的仿真应该让fx相对clk有一定的随机相位偏移甚至在testbench里动态改变fx的频率和相位。我的testbench大致结构是这样module tb_freq_meter; reg clk; reg rst_n; reg fx; wire [31:0] freq_hz; wire data_valid; // 被测信号周期约9.491us initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin fx 0; forever begin #4745 fx ~fx; // 约105.35kHz end end initial begin rst_n 0; #100 rst_n 1; #250 test_complete 1; #50 $finish; end // 实例化顶层 endmodule跑完仿真重点看data_valid拉高后的freq_hz是不是落在105250到105450之间。如果偏差过大先查计数器有没有在gate_act结束时正确锁存再查除法器有没有出0。5.2 板级调试的几个实用技巧上板之后我建议按下面的顺序排查效率最高。先测直流状态用信号源输出一个1kHz标准方波观察数码管或串口出来的数值确认系统能测出大致频率此时哪怕误差100Hz都不用慌先确认链路通。再做精度校验用信号源分别输出1kHz、10kHz、100kHz、1MHz、10MHz记录每个频点的测量误差。如果高频误差突然变大多半是信号质量或者接线问题而不是算法问题。PCB板走线过长、测试夹引入的电容都会让方波边沿变缓导致FPGA采不到干净的上升沿。最后做长时间稳定性测试连续跑10分钟观察频率值有没有漂移。如果数值来回跳优先怀疑板子上的晶振温度漂移或者信号源本身就不稳定。我在实验室测过一次某个频点前后两次测量差了30Hz换了一台信号源后立刻恢复正常说明问题出在信号源而非FPGA。5.3 除法器的时序问题如果你用的是我上面那种移位减除法器要注意它整个运算需要32个时钟周期左右。如果在data_valid之后立刻读取freq_hz可能读到的还是旧值。我的做法是提供一个busy信号或者直接检查状态机状态。电赛版本里我没做busy而是直接延迟了N个时钟周期再取数省事但不够严谨。更规范的做法是在除法器输出有效时拉一个done信号后续模块看到done再更新显示。这个我在升级版本中改掉了可靠性提高不少。6. 常见问题与排查技巧实录6.1 数码管显示乱跳怎么定位先别怀疑工作原理很多情况下是显示刷新问题。等精度测频本身每次测量的结果应该在几十赫兹以内波动如果显示乱跳先确认你是不是每次data_valid到达后才刷新显示而不是让显示模块以极快的频率不断读freq_hz。另一种可能是你的除法器还在运算中就更新了显示寄存器出现“半成品”数值。解决办法是给显示模块加上一个与data_valid同步的锁存。6.2 低频测量误差非常大如果是低频比如50Hz以下误差还很大那就要回头检查实际闸门逻辑。等精度测频对低频依然有优势但前提是预置闸门必须足够长。假设你要测50Hz信号如果预置闸门只有20ms实际闸门可能连1个周期都包不住Nx最多只有1误差就会很大。正确做法是低频时加大闸门时间到500ms甚至1s。6.3 被测信号幅度太小FPGA采不到FPGA引脚对输入电平有阈值要求一般TTL电平是1.5V左右。如果你直接测传感器输出的小信号必须先进比较器或者整形电路不能指望FPGA引脚自己识别。电赛题里会给标准TTL信号但我们扩展功能时需要特别注意。6.4 锁存时序竞争产生错误计数值这是一个容易踩的坑。如果用两个不同的always块分别锁存nx和ns而且触发条件不完全一致就可能出现一个已更新、另一个未更新的问题。解决方法是把两个锁存放在同一个always块里用同一个触发条件。我这篇代码里就是这样做的也算是一个提前避坑。6.5 gate_act信号毛刺导致计数异常如果gate_act在外部出现毛刺可能会造成计数器在闸门外误计数。这种毛刺多半来自异步信号跨时钟域时的中间态。虽然打两拍能抑制大多数情况但如果你在布局布线后发现某些板子仍然偶发异常可以在内部对gate_act做一次滤波或者用状态机对gate_act的连续电平进行判断只有连续多个周期都为高才拉高。这在工业级设计里很常见但电赛条件下我一般不打补丁直接从源头保证信号干净即可。7. 后续可扩展的方向频率计做完之后不要闲着。一个很自然的扩展是把它改成周期测量或者脉宽测量。等精度测频的原理反过来用就能得到等精度测周两者本质相通。你还可以把结果通过UART发送到上位机画波形或者加入自动增益控制电路让频率计能适应更宽幅度的输入信号。这些扩展对加深FPGA理解非常有帮助。从2015年做到现在E题频率计这个经典题目依然值得反复练习核心就在等精度测频的理解。希望这些代码和调试经验能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表