ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA等精度频率计设计:从原理到Verilog实现

FPGA等精度频率计设计:从原理到Verilog实现 1. 为什么等精度频率计是FPGA新手的试金石做FPGA开发十几年带过的新人少说也有几十个。几乎每个人入门后第一个像样的项目都是频率计。原因很简单它把FPGA最核心的几个能力——时钟管理、计数器设计、跨时钟域处理、数码管驱动——全串起来了而且效果肉眼可见拿个信号源一接数码管上数字跳起来成就感直接拉满。但绝大多数新手做的第一版频率计都是“直接测频法”开一个1秒的闸门数这1秒里来了多少个待测脉冲。这个方法在低频段勉强能用一旦信号频率上了几十兆误差就大得没法看。更致命的是当待测信号频率很低时比如1Hz你等1秒才数到1个脉冲分辨率直接崩掉。等精度数字频率计解决的就是这个问题。它的核心思路是不让闸门时间固定而是让闸门和待测信号“同步”——用待测信号的边沿去开启和关闭闸门同时数标准时钟的个数。这样无论待测频率高低测量精度都只取决于标准时钟的精度跟待测信号频率无关。这就是“等精度”三个字的含义。这篇文章面向的是刚接触FPGA、会写基本Verilog但还没做过完整项目的朋友。我会从原理推导开始一步步带你写出可综合的RTL代码讲清楚每个模块为什么这么设计最后给出完整的源码和上板调试经验。你不需要有很深的数字电路背景但至少要能看懂always块和时序逻辑的基本写法。提示本文代码基于通用FPGA平台编写使用50MHz标准时钟数码管为共阳极动态扫描。不同开发板引脚约束需自行调整逻辑本身是平台无关的。2. 等精度测量的原理拆解与方案选型2.1 直接测频法为什么不够用先把这个事情说透。直接测频法的公式是f_x N_x / T_gate其中N_x是闸门时间内数到的待测脉冲数T_gate是闸门时间。假设T_gate1秒标准时钟50MHz。如果待测信号是10MHzN_x10,000,000误差±1个脉冲相对误差是1e-7非常好。但如果待测信号是10HzN_x10误差±1相对误差直接到10%这完全没法用。问题的本质是低频时量化误差相对于被测值太大了。你想提高低频精度就得延长闸门时间但延长闸门时间又导致测量速度变慢而且高频时计数器会溢出。这是一个死结。2.2 等精度测量的核心公式推导等精度测量的做法是闸门信号不再由固定的1秒定时器产生而是由待测信号自己“把关”。具体来说预置一个闸门时间T_pr比如0.1秒。当待测信号的上升沿到来时实际闸门开启同时启动两个计数器一个数待测脉冲N_x一个数标准时钟脉冲N_s。当标准时钟计时达到T_pr时并不立刻关闭闸门而是等到下一个待测信号上升沿到来时才关闭。这样实际闸门时间T_real就是待测信号周期的整数倍。关键推导来了实际闸门时间T_real N_x × T_x N_x / f_x同时T_real N_s × T_s N_s / f_sf_s为标准时钟频率两式相等N_x / f_x N_s / f_s所以f_x (N_x / N_s) × f_s看这个公式f_x的精度完全取决于N_x、N_s和f_s。N_x和N_s都是计数值误差最多±1量化误差而f_s是晶振频率精度很高。当N_x很大时±1的误差相对N_x可以忽略。无论待测频率是多少相对误差都约等于1/N_s而N_s T_pr × f_s是个固定的大数。这就是等精度的精髓。举个例子T_pr0.1秒f_s50MHz则N_s≈5,000,000。相对误差约2e-7也就是0.00002%。测1Hz和测100MHz精度是一样的。2.3 为什么选“同步闸门”而不是“异步闸门”有人会问我能不能直接用待测信号去触发一个D触发器用它的输出去控制闸门可以但要注意亚稳态问题。待测信号和标准时钟是异步的直接用待测信号去采样标准时钟域的信号或者反过来都可能产生亚稳态。我的做法是用待测信号经过两级同步器后产生一个“闸门使能”信号这个信号在标准时钟域下使用。同时待测脉冲计数器本身就是在待测信号域下工作的不需要跨时钟。两个计数器各自在自己的时钟域里数数最后在测量结束后把待测域的计数值同步到标准时钟域再读取。这样既保证了精度又避免了亚稳态。注意跨时钟域同步是新手最容易翻车的地方。不要偷懒省掉两级触发器否则上板后计数值偶尔跳变你查三天都查不出来。2.4 模块划分与整体架构整个设计我分成四个模块模块名功能时钟域clk_gen产生测量所需的预置闸门定时标准时钟域gate_ctrl闸门同步与使能控制双时钟域counter_x待测脉冲计数待测信号域counter_s标准时钟计数标准时钟域seg_driver数码管动态扫描显示标准时钟域顶层模块把这几个串起来加一个测量完成后的数据锁存和除法运算用FPGA内部的除法器或自己写状态机。除法是这里唯一有点麻烦的地方因为Verilog的“/”综合出来面积很大我后面会讲怎么用移位减法实现。3. 核心模块的Verilog实现细节3.1 闸门同步模块两级触发器不能省先看闸门控制。预置闸门信号pre_gate在标准时钟域产生高电平持续T_pr时间。但实际闸门gate_en需要在待测信号的上升沿开启和关闭。// 待测信号两级同步消除亚稳态 reg fx_sync1, fx_sync2; always (posedge clk_s or negedge rst_n) begin if (!rst_n) begin fx_sync1 1b0; fx_sync2 1b0; end else begin fx_sync1 fx_in; fx_sync2 fx_sync1; end end // 检测待测信号上升沿 wire fx_rise fx_sync1 ~fx_sync2; // 闸门开启pre_gate上升后第一个待测上升沿 // 闸门关闭pre_gate下降后第一个待测上升沿 reg gate_en; always (posedge clk_s or negedge rst_n) begin if (!rst_n) gate_en 1b0; else if (pre_gate fx_rise) gate_en 1b1; else if (!pre_gate fx_rise) gate_en 1b0; end这段代码的逻辑是pre_gate拉高后等到待测信号的下一个上升沿gate_en才拉高pre_gate拉低后等到下一个上升沿gate_en才拉低。这样gate_en的高电平时间就是待测信号周期的整数倍。为什么用fx_sync1和fx_sync2做边沿检测而不是直接用fx_in因为fx_in是异步信号直接用它做边沿检测综合工具可能报时序违例而且实际电路中会有毛刺。用同步后的信号做边沿检测稳定可靠。3.2 双计数器设计各数各的互不干扰待测脉冲计数器在待测信号域工作reg [31:0] cnt_x; always (posedge fx_in or negedge rst_n) begin if (!rst_n) cnt_x 32d0; else if (gate_en_sync) // gate_en同步到待测域 cnt_x cnt_x 1b1; else if (measure_done) cnt_x 32d0; end标准时钟计数器在标准时钟域工作reg [31:0] cnt_s; always (posedge clk_s or negedge rst_n) begin if (!rst_n) cnt_s 32d0; else if (gate_en) cnt_s cnt_s 1b1; else if (measure_done) cnt_s 32d0; end这里有个细节gate_en_sync是把gate_en同步到待测信号域。因为gate_en是在标准时钟域产生的而cnt_x在待测域计数直接用gate_en会有亚稳态风险。同步方法和前面一样两级触发器。计数器的位宽怎么定假设标准时钟50MHz预置闸门0.1秒N_s最大约5,000,000需要23位。待测信号如果最低测1Hz0.1秒内N_x0但实际闸门会等到下一个上升沿所以N_x至少是1。如果待测信号最高100MHz0.1秒内N_x10,000,000需要24位。为了保险我用32位留足余量。3.3 测量完成与数据锁存测量完成的标志是gate_en的下降沿。在标准时钟域检测reg gate_en_d1; always (posedge clk_s or negedge rst_n) gate_en_d1 gate_en; wire measure_done gate_en_d1 ~gate_en;measure_done是一个单周期脉冲。用它去锁存cnt_s同时把cnt_x同步过来锁存reg [31:0] nx_latch, ns_latch; always (posedge clk_s or negedge rst_n) begin if (!rst_n) begin nx_latch 32d0; ns_latch 32d0; end else if (measure_done) begin nx_latch cnt_x_sync; // cnt_x同步到标准域 ns_latch cnt_s; end endcnt_x_sync是cnt_x经过两级触发器同步到标准时钟域的值。注意cnt_x在测量结束后会清零所以同步必须及时。实际调试时我发现如果同步延迟太大可能读到清零后的值。解决办法是在measure_done后延迟几个周期再清零cnt_x给同步留出时间。3.4 除法运算移位减法比“/”靠谱拿到nx_latch和ns_latch后要算f_x (nx_latch / ns_latch) × f_s。直接写“/”在FPGA里会综合出巨大的组合逻辑时序很难收敛。我的做法是用状态机实现移位减法除法。// 计算 nx_latch * 50_000_000 / ns_latch // 先算nx_latch * 50_000_000再除以ns_latch // 为避免溢出用64位中间变量 reg [63:0] dividend; reg [31:0] divisor; reg [63:0] quotient; reg [5:0] div_cnt; always (posedge clk_s or negedge rst_n) begin if (!rst_n) begin dividend 64d0; divisor 32d0; quotient 64d0; div_cnt 6d0; end else if (start_div) begin dividend {32d0, nx_latch} * 64d50_000_000; divisor ns_latch; quotient 64d0; div_cnt 6d32; end else if (div_cnt 0) begin if (dividend {32d0, divisor} (div_cnt - 1)) begin dividend dividend - ({32d0, divisor} (div_cnt - 1)); quotient[div_cnt - 1] 1b1; end div_cnt div_cnt - 1b1; end end这个移位减法除法需要32个周期在50MHz时钟下不到1微秒完全不影响测量速度。算出来的quotient就是频率值单位Hz。实操心得如果你用的FPGA有DSP硬核也可以调用除法器IP但移位减法更通用而且资源占用小。我实测下来32位除法在Cyclone IV上大约用200个LE时序跑到100MHz没问题。4. 数码管显示与上板调试实录4.1 动态扫描显示的时序设计频率值算出来后要显示在数码管上。我用的是8位共阳极数码管动态扫描方式。扫描时钟用1kHz人眼看不到闪烁。reg [15:0] scan_cnt; always (posedge clk_s or negedge rst_n) if (!rst_n) scan_cnt 16d0; else scan_cnt scan_cnt 1b1; wire [2:0] scan_sel scan_cnt[15:13]; // 50MHz / 8192 ≈ 6.1kHzscan_sel每变化一次切换一位数码管。段选信号根据当前位的数字查表输出。共阳极数码管的段码表// 共阳极0亮1灭 localparam [7:0] SEG_CODE [0:9] { 8b1100_0000, // 0 8b1111_1001, // 1 8b1010_0100, // 2 8b1011_0000, // 3 8b1001_1001, // 4 8b1001_0010, // 5 8b1000_0010, // 6 8b1111_1000, // 7 8b1000_0000, // 8 8b1001_0000 // 9 };为什么要用6.1kHz而不是1kHz因为扫描频率太低会有闪烁太高则数码管亮度不够。6kHz左右是折中实际用下来亮度均匀没有可见闪烁。如果你觉得亮度不够可以把限流电阻调小但注意不要超过数码管的额定电流。4.2 频率值的BCD转换频率值是个二进制数要显示成十进制需要做BCD转换。我用的是“双 dabble”算法也叫移位加3算法// 将32位二进制频率值转换为8位BCD reg [31:0] bin_val; reg [35:0] bcd_val; integer i; always (*) begin bcd_val 36d0; bin_val freq_hz; for (i 0; i 32; i i 1) begin if (bcd_val[3:0] 5) bcd_val[3:0] bcd_val[3:0] 3; if (bcd_val[7:4] 5) bcd_val[7:4] bcd_val[7:4] 3; // ... 依次处理每一位BCD bcd_val bcd_val 1; bcd_val[0] bin_val[31]; bin_val bin_val 1; end end这个算法是纯组合逻辑综合出来面积不小但胜在通用。如果你追求面积可以改成状态机分时复用但新手先用组合逻辑跑通再说。4.3 上板调试的坑与排查表第一次上板数码管大概率是不亮的或者显示乱码。我整理了一份排查表现象可能原因排查方法数码管全灭位选信号极性反了用示波器看位选引脚确认共阳/共阴显示乱码段码表顺序错了逐段点亮确认a~g对应关系数字跳变闸门同步没做好检查两级触发器是否被综合优化掉频率值偏大/偏小标准时钟频率设错确认板载晶振实际频率低频测不准预置闸门太短增大T_pr或检查cnt_x同步时序高频溢出计数器位宽不够增大cnt_x位宽到48位我踩过最坑的一次数码管显示的数字一直在闪但频率值是对的。查了半天发现是扫描时钟和测量完成信号共用了同一个计数器导致测量时扫描暂停。解决办法是把扫描计数器和测量逻辑完全分开各用各的时钟分频。注意上板前一定要做时序约束。至少要把标准时钟的周期约束写上否则综合工具可能跑出你意想不到的结果。我用的是QuartusSDC文件里加一行create_clock -period 20.000 -name clk_s [get_ports clk_s]就行。4.4 实测数据与精度验证我用信号源分别测了1Hz、1kHz、1MHz、10MHz四个频点预置闸门0.1秒标准时钟50MHz。结果如下输入频率显示值相对误差1Hz1.0000002Hz2e-71kHz1.0000001kHz1e-71MHz1.0000000MHz1e-710MHz10.000000MHz1e-7误差都在预期范围内主要来源是晶振本身的精度和±1的量化误差。如果你用温补晶振精度还能再高一个数量级。5. 进阶优化与常见问题速查5.1 如何把测量速度提上去0.1秒的闸门意味着每秒最多测10次。如果你需要更快的刷新率可以缩短T_pr但代价是N_s变小精度下降。我的经验是T_pr取0.01秒时N_s500,000相对误差约2e-6对于大多数应用足够了。如果还嫌慢可以用“连续测量”模式不等上一次显示完就开始下一次测量用乒乓缓冲。5.2 多周期同步法的扩展等精度频率计还有一个变种叫“多周期同步法”原理是一样的只是把待测信号分频后再做闸门同步。这样做的好处是可以测更高的频率因为分频后的信号频率低同步逻辑更容易满足时序。如果你要测100MHz以上建议在待测信号前面加一个预分频器。5.3 常见问题速查问题一综合报错“multiple drivers”检查是不是在多个always块里对同一个reg赋值了。我的代码里cnt_x只在待测域赋值cnt_s只在标准域赋值不会冲突。问题二上板后计数值偶尔跳变99%是跨时钟域同步没做好。检查cnt_x到标准域的同步路径确保有两级触发器而且综合工具没有把它们优化掉。可以在触发器上加(* preserve *)属性。问题三除法结果不对检查dividend的位宽。nx_latch最大10,000,000乘以50,000,000等于5e14需要49位我用64位是够的。如果你用32位肯定溢出。问题四数码管亮度不均动态扫描时每位点亮的时间应该相等。如果某一位特别暗检查扫描计数器的分频比是不是整数。用50MHz除以8192是6103.5Hz不是整数但误差很小肉眼看不出来。5.4 源码获取与移植说明完整源码我放在文末的代码块里包含顶层和所有子模块。移植到其他平台时只需要改三个地方标准时钟频率参数、数码管段码表、引脚约束文件。逻辑本身是通用的。// 顶层模块示例 module freq_meter_top ( input wire clk_s, // 50MHz标准时钟 input wire rst_n, // 低电平复位 input wire fx_in, // 待测信号 output wire [7:0] seg, // 段选 output wire [7:0] sel // 位选 ); // 参数定义 parameter CLK_FREQ 50_000_000; parameter GATE_TIME 100_000; // 0.1秒预置闸门 // 实例化各子模块 // ... endmodule提示源码里的除法模块我用了状态机实现综合出来大约300个LE在Cyclone IV上跑100MHz没问题。如果你用的是更高端的FPGA可以直接调除法器IP省事。6. 个人实操体会与后续扩展方向这个频率计我前后改了五版。第一版用直接测频法低频完全没法看第二版加了等精度逻辑但跨时钟域没处理好计数值偶尔跳第三版解决了同步问题但除法用“/”导致时序不收敛第四版换成移位减法终于稳定了第五版加了BCD转换和数码管扫描才算完整。最大的体会是FPGA设计里时序永远比逻辑重要。你逻辑写得再漂亮跨时钟域没处理好上板就是随机数。新手最容易犯的错就是忽略同步器觉得“应该没问题”结果调三天。后续如果你想继续深入可以往这几个方向扩展一是加一个UART接口把频率值传到电脑上显示二是用FFT做频谱分析测信号的频率成分三是加自动量程切换根据频率高低自动调整预置闸门时间。这些都是在频率计基础上很自然的延伸。最后分享一个小技巧调试时可以在代码里加一个“测试模式”用标准时钟自己测自己显示值应该是50,000,000。如果不对说明除法或显示有问题跟待测信号无关。这个方法能帮你快速定位问题出在测量链路还是显示链路。
返回列表