ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA等精度频率计设计:原理、Verilog实现与调试实战

FPGA等精度频率计设计:原理、Verilog实现与调试实战 做了快十年测量相关的嵌入式项目我最大的一个感受是频率计这东西原理看着简单做起来全是坑。尤其是一开始我用最常规的“直接计数法”写了个FPGA频率计低频段测出来惨不忍睹1 kHz信号在1秒闸门里只能数到几千个脉冲分辨率差到没法看。后来改成等精度频率计reciprocal frequency counter才把1 Hz到几十MHz的测量精度真正拉平。这篇文章就把我当时从原理分析、Verilog模块划分到硬件调试翻车的全过程完整复盘一遍。工程里几个核心模块的源码我会贴在后面给正在做频率计类课程设计、或者想在自己系统里加一个高精度测频模块的朋友做个参考。内容是项目实战向的不会只丢给你一堆代码重点会放在“为什么这样设计”和“调试时到底在查什么”上。1. 直测法为什么测不快闸门时间与量化误差同涨同跌很多人一开始接触频率计用的都是最朴素的思路给一个1秒的闸门信号数这1秒内被测信号来了多少个上升沿频率自然就是计数值。原理听着没问题可一旦真的拿去做宽范围测量马上就会发现这个方案有硬伤。1.1 直接计数法的“两头吃亏”直接计数法的公式是fx N / Tgate其中N是闸门内被测脉冲个数。这个方案在测高频时很舒服比如测10 MHz信号1秒闸门里能数到10,000,000个脉冲分辨率和精度都不错。但测低频就尴尬了测一个1 kHz的信号1秒闸门里只有1000个脉冲分辨率直接掉到1 Hz量级测1 Hz信号1秒里只能数到1个脉冲误差直接是100%甚至测不出来。那有人说我反过来用“周期法”行不行用基准时钟去量被测信号一个周期有多长再取倒数。这个方案测低频确实很准但测高频时一个周期只有几十纳秒基准时钟也数不了几个数误差又上来了。所以直接计数法和周期法本质上是一回事只是把“数脉冲”和“数周期”这两件事放到了不同频率段。无论你用哪种都会遇到“高频好用低频崩、低频好使高频废”的问题。根因在哪儿在于计数对象本身太不稳定——高频时脉冲太密低频时脉冲太稀一个闸门里永远只有一小段有效信息。1.2 量化误差的数学本质1/N不可逃如果非要用一句话概括直接计数法为什么不行那就是它的量化误差公式太苛刻Δfx / fx ±1 / NN是闸门内测到的脉冲数。要提高精度只能加大闸门时间或者倍频被测信号。比如测1 kHz信号想要万分之一误差N至少要10,000也就是闸门至少10秒想要十万分之一就得等100秒。这在工程上基本不可接受。我当时算完这个公式第一反应是能不能想办法让闸门内的N永远足够大换句话说不管被测频率是1 Hz还是10 MHz我能不能都让闸门内被测信号刚好包含一个非常大的“可数脉冲数”这就是等精度测频的核心出发点我不再直接数被测信号本身有多密而是把被测信号当作闸门的“标尺”用它的整数个周期去卡基准时钟。1.3 换一个思路让被测信号自己决定测量窗口等精度测频的关键设计思想就是让测量窗口的开启和关闭都发生在被测信号的上升沿上。预置一个“大致闸门时间”比如1秒但实际计数的窗口是从预置闸门开始后的第一个被测信号上升沿开始到预置闸门结束后的第一个被测信号上升沿截止。这样一来实际测量的窗口天然就是被测信号周期的整数倍。在这个窗口里被测信号被数了多少个上升沿就是一个精确值没有±1的量化误差真正存在量化误差的只剩基准时钟在那个窗口里数出来的脉冲数。因为基准时钟通常非常高100 MHz、200 MHz甚至更高窗口哪怕是0.5秒基准时钟也能数到5000万个数量化误差就被压到了天花板级别。2. 等精度测频的系统结构与同步使能设计理清了原理接下来就是怎么用FPGA把这套逻辑落地。这里有一个必须想清楚的点预置闸门和实际计数闸门不是同一个信号。很多人写代码时直接把“1秒闸门”拿来当计数使能那就又变成直接计数法了。2.1 顶层数据流三个模块之间的握手关系等精度频率计在FPGA里至少需要几个核心部分预置闸门发生器、被测信号边沿同步检测、基准时钟计数器、被测信号计数器、数据锁存与计算、显示输出。它们之间的数据流关系大概是这样预置闸门发生器产生一个宽度可配置的pretime_gate信号本质就是一个按基准时钟计数的定时窗口。被测信号clk_fx_in进入FPGA后先经过两级同步寄存器消除亚稳态再做上升沿检测产生一个单周期的脉冲信号sig_rise。核心的状态机负责生成真正的计数使能gate_en当pretime_gate为高之后等待第一个sig_rise然后立刻拉高gate_en当pretime_gate变低之后继续等到一个sig_rise再拉低gate_en。基准计数器在gate_en期间对基准时钟计数得到N0被测信号计数器在gate_en期间对sig_rise脉冲计数得到Nx。gate_en下降沿到来时把N0和Nx同时锁存进寄存器然后输出fx Nx × f0 / N0。这套流程里最关键的就是第3步。gate_en的开启和关闭完全由被测信号的上升沿来“框定”而不是由预置闸门直接框定。这也是“等精度”三个字的核心来源。2.2 参考时钟的稳定度决定了精度的天花板等精度测频虽然消除了计数法对低频信号的不友好但它并没有消除对参考时钟的依赖。你可以把fx Nx × f0 / N0里的f0理解成一个“乘法比例因子”基准时钟只要自己漂了测量结果一定跟着漂。所以工程上有一个优先级参考时钟的稳定度 代码逻辑的正确性。我当时用的是开发板上板载的普通晶振频率稳定度大概是几十ppm量级配合FPGA内部PLL倍频到200 MHz做参考时钟。这样做出来的精度已经能到10^-7量级左右。如果产品里要求更高下一步就是换温补晶振TCXO或者恒温晶振OCXO把设备本身的时基稳定度拉上去。有一个容易踩的坑不要拿被测信号自己做过零检测形成的“边沿”去当时钟信号再用这个时钟去驱动计数器逻辑。原因有两个一是被测信号本身可能有噪声沿位置抖动会直接污染计数二是跨时钟域握手很容易引入亚稳态。更稳妥的做法是把被测信号只当作一个“异步输入”在参考时钟域内同步化之后作为脉冲使能去加计数而不是作为真正的时钟源。2.3 预置闸门发生器与边沿检测模块预置闸门发生器实现起来很简单本质就是按参考时钟计数的可配置定时器。以100 MHz参考时钟为例想要0.1秒闸门就数10,000,000个时钟周期想要1秒闸门就数100,000,000个周期。module gate_gen ( input wire clk_ref, // 参考时钟例如100MHz input wire rst_n, input wire [31:0] gate_cycles, // 预置闸门对应的时钟周期数 output reg pretime_gate ); reg [31:0] cnt; always (posedge clk_ref or negedge rst_n) begin if (!rst_n) begin cnt 32d0; pretime_gate 1b0; end else if (cnt gate_cycles - 1) begin cnt 32d0; pretime_gate ~pretime_gate; // 翻转形成高/低闸门 end else begin cnt cnt 1b1; end end endmodule注意这里我把预置闸门做成了周期翻转模式预置闸门的高电平宽度就是测量窗口的“大致时间”低电平宽度也相同。这样连续测量时高电平结束一次就产生一组测量结果低电平期间可以用来做数据锁存和显示刷新给了下游时间余量。边沿检测模块同样不复杂但细节值得多说一句module edge_sync ( input wire clk_ref, input wire rst_n, input wire sig_in, output reg sig_r2, output wire sig_rise ); reg sig_r1; always (posedge clk_ref or negedge rst_n) begin if (!rst_n) begin sig_r1 1b0; sig_r2 1b0; end else begin sig_r1 sig_in; sig_r2 sig_r1; end end assign sig_rise sig_r2 ~sig_r1; // 检测sig_r2的上升沿 endmodule两次寄存器的目的是把被测信号同步到参考时钟域避免第一级寄存器采到变化的信号时出现亚稳态。这里有一个很多教程不会写、但实际非常关键的细节边沿检测用的是sig_r2和~sig_r1也就是检测同步寄存器的输出沿而不是直接对原始输入做边沿检测。如果直接拿原始输入做边沿检测那个沿和参考时钟没有任何同步关系极容易在后续组合逻辑里产生毛刺。2.4 计数器与锁存对被测信号计数不是拿它当clk接下来是计数器部分。最经典的做法有两种一种是真的把被测信号直接接到一个计数器的时钟端另一种是把同步化以后的脉冲信号当作计数使能。我强烈建议用后一种也就是被测信号的同步脉冲作为cnt_fx的加1使能而不是作为时钟。原因有两点。第一被测信号直接进全局时钟网络会占用宝贵的时钟资源而且信号质量不可控第二如果有多个计数器需要在同一测量窗口内同时工作拿异步信号当时钟会导致不同模块之间的时序关系非常难约束。而统一在clk_ref域内做脉冲使能计数整个逻辑都是单一时钟域RTL一眼就能看穿时序关系。计数使能gate_en的产生可以用一段简单的状态机来实现预置闸门为高后等待第一个上升沿再开启计数器预置闸门为低后等待下一个上升沿再关闭计数器。gate_en下降沿同时产生一个锁存脉冲把N0和Nx写入寄存器。这里值得注意的一个工程点是锁存时序必须走寄存器同步不要用组合逻辑去直接触发锁存。很多第一次写频率计的人直接在赋值语句里让锁存器在gate_en下降沿读数据结果下游计算模块拿到的是还没有稳定的中间值显示数字乱跳。正确做法是锁存脉冲采用独立的寄存信号在sig_rise确认关闭窗口之后再打一拍确保N0和Nx都已经完成最后一次递增。3. Verilog模块划分与关键代码同步使能逻辑是灵魂这一章我直接把工程里的模块清单和几个核心代码片段整理出来。完整的工程除了下面这些模块还包括数码管扫描显示、按键量程切换和简单滤波逻辑但最核心的测频部分基本都在这里了。3.1 工程文件清单与模块分工文件/模块职责备注freq_meter_top顶层模块完成信号例化与端口连接包含输入引脚、按键、数码管接口gate_gen预置闸门发生器宽度通过gate_cycles配置sync_gate_ctrl实际计数闸门状态机根据预置门和sig_rise产生gate_enedge_sync被测信号双级同步 上升沿检测所有跨时钟域信号都经过这里counter_bankN0/Nx计数器与锁存寄存器锁存脉冲由sync_gate_ctrl给出calc_result用整数运算计算显示频率值实际项目里我把计算放在了后级显示模块seg_display7段数码管动态扫描显示8位有效数字的显示缓冲这个模块划分是我反复调过之后的版本。最开始版本里我把预置闸门状态机和边沿检测混在一个模块里结果调试的时候一个问题不知道是出在闸门状态机还是出在同步逻辑上非常难定位。切成独立模块以后每个模块都可以单独用仿真波形验证排查问题效率高很多。3.2 实际计数使能状态机的代码逻辑这一段是等精度频率计的“灵魂”所在。我把它独立成一个模块就是为了让那几步“等一个被测沿再开/关门”的逻辑够清晰module sync_gate_ctrl ( input wire clk_ref, input wire rst_n, input wire pretime_gate, input wire sig_rise, output reg gate_en, output reg lock_pulse ); localparam IDLE 2d0, OPEN 2d1, WAIT_CLOSE 2d2; reg [1:0] state; wire open_ready pretime_gate sig_rise; // 预置门高且被测沿到来 wire close_ready ~pretime_gate sig_rise; // 预置门低且被测沿到来 always (posedge clk_ref or negedge rst_n) begin if (!rst_n) begin state IDLE; gate_en 1b0; lock_pulse 1b0; end else begin lock_pulse 1b0; case (state) IDLE: begin gate_en 1b0; if (open_ready) state OPEN; end OPEN: begin gate_en 1b1; if (close_ready) begin gate_en 1b0; state IDLE; lock_pulse 1b1; end end endcase end end endmodule这个状态机的流程说白了就是“按信号节奏开门、按信号节奏关门”。lock_pulse会在关门那一刻打一拍给下游锁存器提供一个明确的写使能。你会看到这里面没有专门为“预置门刚变高、第一个沿还没来”设置暂停状态因为open_ready本身就是“预置门为高且被测沿到来”的组合条件只要当前是IDLE拉高gate_en的时机就是预置门高电平期间的第一个被测沿完全符合需求。3.3 数据计算Nx、N0与f0之间的关系当lock_pulse有效时counter_bank里已经寄存了Nx和N0。计算频率的公式是fx Nx × f0 / N0比如参考时钟f0 100 MHz某次测量Nx 500,000N0 50,000,000那么fx 500,000 × 100,000,000 / 50,000,000 1,000,000 Hz也就是1 MHz。在FPGA里做这个除法工程量不大但需要想清楚数据位宽。N0最大可能到几千万甚至上亿Nx也同样可能到几百上千万所以至少要用32位计数器。而计算频率时如果直接做64位乘法再除以N0综合出来的资源也不会太大因为这是一次性的低速计算不是每个时钟周期都跑。我实际用的是更省资源的方式给显示模块传原始Nx和N0然后在显示模块里用整数除法配合移位把结果转成BCD码给数码管。毕竟频率计显示的刷新率也就每秒几十次用“慢速除法器 状态机”完全够用没必要为这个功能专门上一个硬件除法器IP核。3.4 显示刷新与测量节奏的配合这里有个容易被忽略的工程细节显示刷新频率和测量闸门时长必须分开考虑。如果测量一次需要1秒数码管却每2毫秒扫描一次那不是问题但如果显示模块在测量窗口尚未结束时就去读计数器的当前值看到的就是一堆还在跳变的数字看起来就像频率计“不稳定”。我的做法是在lock_pulse到来后先把Nx、N0锁存进一个“显示数据缓冲”显示模块每2毫秒从缓冲里读一次而不是直接读计数器。这样显示出来的数字就是上一次测量已冻结的结果完全不会闪跳。加上动态扫描数码管本身有刷新频率要求这个缓冲区的价值就体现出来了测量模块和显示模块彻底解耦。4. 调试实录自检全对、外接信号却偏差一个数量级的排查链路代码写完之后我原以为按原理图接好线、编译下载就能收工。结果真正上板调试的时候被一个现象折磨了一个下午用开发板自带的信号测精度非常好换上外部信号发生器的信号低频段误差直接差了一个数量级。这个坑非常典型我把完整排查链路写出来希望你能少走点弯路。4.1 先复现现象自检源和外接源的对比开发板上一般会有一个由板上晶振分频出来的固定信号比如10 MHz、5 MHz之类。我用自检源测无论怎么换预置闸门误差都稳在10^-7量级数据非常漂亮。然后我把信号发生器输出调到1 kHz方波接进同一个输入引脚结果测出来的频率显示成了1.024 kHz左右偏差超过百分之二。1024这个数字太显眼了让我立刻怀疑是不是计数器多计了24个脉冲。进一步测试把信号发生器输出调到100 kHz偏差变成了大约0.6 Hz调到10 MHz偏差又变得很小。很明显偏差集中发生在输入频率较低、边沿较缓的区间。这和代码本身的量化误差规律对不上因为如果是量化误差频率越低应该越准才对。于是我开始怀疑输入通路本身在搞鬼。4.2 排查方向一自检源为什么“骗人”第一反应我想查是不是信号发生器的电平标准不匹配。因为开发板输入引脚一般是3.3V LVCMOS标准而信号发生器输出我习惯性设成了50欧姆负载下的高电平摆幅可能只有0到2V阈值判断时可能会出现不稳定的逻辑翻转。但这里有个更深的原因自检源和参考时钟来自同一颗晶振、同一个时钟树它们之间的相位关系是固定的测量自然准。外接信号发生器和参考晶振是两套独立时基一旦输入信号边沿不够陡或者电平不够干净跨时钟域同步时就会出问题。所以自检源通过只能证明代码逻辑基本正确完全不能证明输入通路可靠。排查的第一步是把信号发生器输出调到CMOS电平标准、占空比50%、上升沿时间压到最低重新测。结果偏差确实缩小了但没完全消除。这说明问题不止在电平摆幅上。4.3 排查方向二输入整形电路的迟滞与噪声看示波器波形问题变得很直观信号发生器输出的方波在1 kHz时经过连接线和板上走线后上升沿带有明显的振铃。在FPGA输入缓冲器的翻转阈值附近信号可能来回穿过阈值好几次也就是所谓的“多次翻转”。每一次超过阈值edge_sync都当成一个上升沿于是Nx凭空多出几十个甚至上百个。我原来的输入电路非常简单信号进来直接接FPGA引脚靠引脚内部的上拉/下拉和施密特触发器整一下形。低频率下信噪比不足时这种简单整形根本压不住振铃。解决方案是做一个小型输入调理前端信号进来先经过高速比较器比较器加一点正反馈电阻构成迟滞再把比较器输出接到FPGA引脚。迟滞的宽度大约设成几十毫伏保证噪声无法在阈值附近反复触发。改完之后1 kHz信号读到的Nx从“多出24个”变成了“完全不差”。这也是我在实际工程里最强烈的一个建议外接信号做频率计输入调理电路不是选项是必需品。4.4 排查方向三跨时钟域同步寄存器的位置是否放对继续深挖我还发现一个隐藏比较深的问题。最早的代码里我是把sig_in先接到计数器的时钟端然后再在计数器内部做同步。这样即便同步逻辑本身没问题计数器时钟端接收到的脉冲仍然来自外部信号域gate_en和clk_ref之间的时钟关系在综合时序分析里是没法好好约束的。按照我前面建议的做法把edge_sync放在输入引脚之后、计数逻辑之前会带来一个立竿见影的好处整个计数逻辑完全在参考时钟域里所有时序约束都变得清晰可查。改完之后我直接在时序报告里看到所有路径都有明确的setup/hold余量而不是一堆“clock crossing warning”。这个改动和迟滞整形加在一起才算真正把输入通路的问题清零。4.5 排查方向四测量窗口宽度与实际能达到的分辨率还有一个偏“参数配置”的坑。不到万不得已不要为了追求刷新速度把预置闸门设得太短。等精度测频的理论量化误差是|Δfx| / fx ≈ ±1 / (f0 × T实际)如果参考时钟是100 MHz实际测量窗口是1毫秒那么误差就只有10^-5量级看起来还行。但如果你用的是50 MHz参考时钟、又开了10毫秒的短闸门误差会掉到2×10^-5配合输入整形不干净显示结果就会在最后一位上不停抖动。我当时为了调试方便把预置门设成了10毫秒结果发现测1 kHz信号的显示结果总是在最后2到3位跳动。后来我加了一档“长测”模式把预置门放大到1秒安定下来之后读数稳如磐石。经验是调试阶段用短闸门看逻辑通不通验收阶段用长闸门看精度够不够。5. 测量时间、显示位宽与实用扩展精度不是唯一指标等精度频率计做到这一步核心功能已经稳定了。但真正拿到项目里去用还有一些工程取舍需要想清楚。我把我在实测中积累的参数选择经验和扩展思路也整理在下面。5.1 不同测量闸门下理论和实际能达到的精度以100 MHz参考时钟为例不同预置闸门下理论量化误差大约如下表预置闸门实际测量窗口约N0数量级理论量化误差10 ms10 ms10^610^-6100 ms100 ms10^710^-71 s1 s10^810^-8这只是一次测量的量化误差。放在实际环境里晶振本身的频率稳定度、输入信号边沿抖动、温度漂移都会叠加进来。我用普通晶振做参考时钟测1 kHz信号1秒闸门下的实测重复性大约在10^-7到10^-8之间也就是说显示出来的最后两位偶尔会跳一下但前6位非常稳定。这就完全满足一般工程测量了。所以更实用的建议是如果设备工作在实验室环境、测量对象是静止频率采用1秒闸门如果是动态扫频、需要快速响应的场景100毫秒闸门配合适当位宽限制比硬撑1秒闸门更好用。5.2 显示位宽与刷新速度的权衡8位数码管实际能稳定显示的位数受限于闸门时长。1秒闸门下100 MHz参考时钟能分辨到10^-8显示8位有效数字没问题100毫秒闸门下最多稳定显示6位。如果你在100毫秒闸门下强行显示8位最后两位就是纯粹的数字噪声除了让读数变“花哨”以外毫无意义。我在设计里加了一个简单的位宽控制按键循环切换“快速模式”100毫秒闸门显示6位和“精确模式”1秒闸门显示8位。这个交互设计简单粗暴但非常实用。测量仪器最忌讳的就是给用户看一堆没有意义的变化数字有效数字个数必须和实际测量不确定度匹配。另外如果你希望把数据传到上位机完全可以把Nx和N0通过串口发出来让上位机做浮点计算。FPGA这边只做计数计算交给上位机既省逻辑资源又减少了在FPGA里做除法带来的位宽和舍入问题。5.3 可扩展的方向周期、脉宽与占空比等精度测频的本质是用被测信号的沿作为测量窗口的“标尺”。沿着这个思路你能做的远不止频率。比如把gate_en改成“测量连续两个上升沿之间的时间差”就是高精度的周期测量把测量窗口改成“上升沿到下降沿”就是脉宽测量再结合频率测量占空比也就出来了。我在这个工程之后把同一套同步计数结构复用到了一台简易脉冲参数分析仪里改动很小核心的edge_sync和sync_gate_ctrl模块基本原封不动。这也是FPGA可重构性的典型体现一套稳定的时序骨架换一下计数目标和状态机约束就能衍生出好几种测量功能。6. 关于调试工具和波形观察的一点经验结构部分基本讲完了最后补一点关于调试工具的心得。虽然代码逻辑可以通过仿真验证但等精度频率计的很多问题只有接通硬件之后才能暴露。尤其输入通路上的信号完整性问题仿真里根本不可能出现。我的调试流程一般是先用一个干净的方波信号源从1 Hz扫到几十MHz每个数量级选几个点用上位机或者数码管人工记录读数和信号源设定值对比画出误差曲线。一旦某个频段误差突然变大立刻用示波器看那个频段的输入波形。大多数时候问题都出在边沿不够陡、振铃过大或者触发电平不对上。调完输入整形电路误差曲线基本能做到全程平直。如果你做的是课程设计没有专门的信号发生器也可以用另一块FPGA板输出频率可调的方波来当信号源。关键是保证输出的信号边沿干净尽量用逻辑电平输出而不要用灌电流驱动长线。等精度测频这个主题听起来是教科书里的经典电路但真正写到FPGA里、跑到真实信号上才发现原理理解和工程实现之间隔着整整一条“调试栈”。如果你正在被频率计的显示乱跳、低频测不准、高频毛刺这些问题折磨希望这篇复盘能帮你少走点弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表