
简介这是一份来自2015年全国大学生电子设计竞赛F题的等精度测频Verilog实现面向FPGA学习者和电赛备赛者解决如何用50MHz采样时钟对输入信号精确计数并测频的问题。设计采用等精度测量原理通过计数器捕捉信号边沿并借助UART串口将频率值上传至上位机兼顾同步设计与资源优化。压缩包共428个文件、大小31.63MB除核心Verilog源文件.v/.vhd外还包含Quartus工程配置.qpf/.qsf、UART的qsys设置以及大量综合仿真辅助文件如cdb、hdb、tdf等方便从代码到硬件工程的完整对照学习。资源内含等精度测频核心模块、UART收发逻辑、分频与平均值模块等并有工程备份和仿真记录可直接借鉴其设计思路也可在此基础上复现或改造。目前已有1316人学习下载适合需要系统掌握等精度测量原理与FPGA实现流程的读者参考。 如果你正在备赛电子设计竞赛或者手上正好有一块FPGA开发板想练测频等精度测频绝对是绕不过去的一道坎。2015年电赛F题“数字频率计”的题面看着简单测频率、测周期。可真到码代码的时候你会发现用Verilog写一个能出数的测频模块并不难难的是让1Hz的低频信号和几十MHz的高频信号测出来误差都足够小——这恰恰是等精度测频存在的意义。这篇文章不打算只贴一段代码而是把原理、模块划分、核心RTL设计和上板踩坑一次性说清楚。不管是准备电赛的本科生还是刚接触FPGA想做测量类项目的工程师都能照着搭出一台自己的数字频率计。1. 为什么2015年电赛F题最稳的方案是等精度测频1.1 传统测频法为什么在低频段翻车很多第一次做频率计的人拿到题目后的第一版方案都是“数脉冲”给定一个固定闸门时间T在T内对被测信号计数N频率直接算成N/T。这种办法叫直接测频法也叫M法。看着没问题但你把被测信号换成低频试试。假设闸门时间是1秒被测信号是1Hz那这1秒内只数到1个脉冲计数结果有±1的固有误差算出来的频率可能是0Hz、1Hz或者2Hz误差高达100%。哪怕把闸门拉长到10秒误差也只是降到10%左右。根本原因是M法的相对误差近似为1/(f×T)频率越低相同闸门时间内脉冲越少误差越大。那反过来先测周期再算频率行不行这就是测周法也叫T法。测量被测信号一个周期内有多少个基准时钟脉冲然后用基准频率除以计数值。这种办法测高频信号时又会翻车100MHz信号的一个周期只有10ns基准时钟如果也是50MHz一个周期内可能连1个时钟都数不到误差同样不可接受。所以M法适合高频T法适合低频但你要的是一个宽量程的频率计不能靠“看频率段切换算法”来糊弄。1.2 等精度测频误差与被测频率彻底解耦等精度测频的思路很巧妙既然固定闸门时间在低频时采不到几个被测脉冲那我就让闸门跟着被测信号走保证无论什么频率闸门内都包含整数个被测信号周期。具体做法是先用一个基准时钟产生一个“预置闸门”信号这个预置闸门宽一点窄一点无所谓因为真正干活的不是它。预置闸门送入被测信号的时钟域用被测信号的两个上升沿把它“修整”成一个实际的计数闸门。这样实际闸门的上升沿和下降沿都严格对齐被测信号所以闸门内被测信号的计数值是精确的整数没有误差。真正有±1误差的是对基准时钟的计数但基准时钟频率很高这个误差折算出来极小。频率计算公式也很直观。设实际闸门内被测信号计数为D基准时钟计数为S基准时钟频率为F0则实测频率为f (D / S) × F0等精度测频的相对误差约为1/(F0 × τ)其中τ是实际闸门时间。注意这个误差表达式里没有被测频率f也就是说你用1Hz的信号和用100MHz的信号相对误差都在同一个量级。基准时钟50MHz、实际闸门0.1秒时理论相对误差约2×10^-7。这才叫“等精度”。测量方法相对误差近似式适合频率段等精度测频对比M法直接测频1/(f×T)高频低频段误差迅速恶化T法测周法1/(F0×T)低频高频段误差无法接受等精度测频1/(F0×τ)全频段误差与输入频率无关2. 顶层架构一台数字频率计应该拆成哪几个模块2.1 需求拆解与模块划分电赛F题要求的不只是“测出频率”这一件事通常还包括周期、脉宽、占空比等参数甚至要求自动量程切换。如果不做模块划分直接在一个always块里写完所有逻辑后期调试会非常痛苦。我在做这个题的时候把系统拆成了五个清晰的模块。前端整形把外部信号整形为FPGA可接收的方波。虽然这不是Verilog代码部分但直接影响测量上限。闸门同步把预置闸门同步到被测信号域产生实际计数闸门。这是等精度测频的核心。双计数器一个在被测信号域对被测信号计数一个在基准时钟域对基准时钟计数。测量控制状态机控制什么时候开始、什么时候锁存、什么时候清零、什么时候计算。运算与显示把两个计数值换算成频率值送数码管、LCD或者串口。顶层模块的信号也可以这样定义module freq_meter #( parameter CLK_FREQ 50_000_000 // 基准时钟频率 )( input wire clk_ref, // 基准时钟 input wire rst_n, input wire fx_in, // 被测信号经前端整形 output wire [31:0] freq_out, // 计算出的频率值 output wire data_valid // 测量完成标志 );这样的顶层只是用来接线真正干活的是子模块。初期写成这样测试哪个模块出问题直接看哪个模块的信号就行不必在密密麻麻的代码里找线。2.2 两个闸门预置闸门和实际闸门理解等精度测频的关键是分清两个闸门的概念。很多第一次写的人会直接把一个计数器使能信号当成闸门在系统时钟域里生成一个固定宽度的高电平然后让两个计数器在这个高电平期间计数。这样得到的计数值并不对齐被测信号低频测量误差和M法没什么区别。真正的做法是预置闸门只是一个“大致范围”它的作用是告诉系统“大概在这个时间段内测量一次”。预置闸门信号进入被测信号域后要经过两级触发器同步得到实际闸门。实际闸门的沿都来自被测信号时钟域的寄存器输出所以天然与被测信号对齐。这带来的好处是被测信号计数值永远是一个准确整数误差只留在基准时钟计数上。我在做电路时把预置闸门宽度设成了可调参数通过按键或串口可以切换100ms、200ms、500ms和1s四档。实际测下来闸门越宽精度越高但测量更新越慢赛场上要根据题目要求的速度指标做权衡。3. 核心Verilog代码逐段拆解3.1 闸门同步模块跨时钟域的第一道防线先把最关键的闸门同步代码亮出来。这里用两级触发器把预置闸门同步到被测信号fx_in的时钟域特别注意两级触发器的时钟必须是被测信号而不是基准时钟。module gate_sync ( input wire clk_fx, // 被测信号作为同步时钟 input wire rst_n, input wire gate_pre, // 预置闸门 output wire gate_real // 实际闸门 ); reg [1:0] sync_ff; always (posedge clk_fx or negedge rst_n) begin if (!rst_n) sync_ff 2b00; else sync_ff {sync_ff[0], gate_pre}; end assign gate_real sync_ff[1]; endmodule这里有两个细节要注意。第一为什么必须用两级触发器因为gate_pre相对被测信号完全是异步的单级触发器采样时有可能落在建立保持时间边界上输出进入亚稳态整个计数器逻辑都可能出错。两级触发器虽然不能彻底消除亚稳态但能把亚稳态传播下去的概率压到极低这是跨时钟域处理的基本功。第二gate_real相对clk_fx来说是同步的吗是的因为sync_ff的更新沿就是clk_fx所以gate_real的上升沿和下降沿都出现在clk_fx的某个触发沿之后不久和被测信号天然对齐。这就是等精度测频“实际闸门是被测信号整数倍”的由来。3.2 双计数器模块实际闸门有了两个计数器就简单了。一个计数器在gate_real为高时对被测信号计数另一个对基准时钟计数。代码可以写成这样reg [31:0] cnt_fx; reg [31:0] cnt_ref; // 被测信号计数器 always (posedge clk_fx or negedge rst_n) begin if (!rst_n) cnt_fx 32d0; else if (gate_real) cnt_fx cnt_fx 1b1; else cnt_fx 32d0; end // 基准时钟计数器 always (posedge clk_ref or negedge rst_n) begin if (!rst_n) cnt_ref 32d0; else if (gate_real) cnt_ref cnt_ref 1b1; else cnt_ref 32d0; end这个写法里有几个地方值得推敲。清零的时机非常关键。我采用了“gate_real为低时清零为高时计数”的策略。实测中发现清零点不能放在gate_real的下降沿上直接清零那样会和锁存逻辑产生竞争。稳妥的做法是先锁存计数值再等gate_real完全变低后清零这也正是测量状态机的用途。第二个问题是计数器溢出的风险。基准时钟50MHz闸门1秒那cnt_ref最多计到50000000用32位足够了。被测信号高频时1秒内也可能有几千万个脉冲同样不会溢出。但如果以后把基准时钟频率提高记得同步扩大位宽。3.3 测量状态机与数据锁存要让整个测量过程有序进行需要一个简单的状态机。我的做法是分四个状态空闲、计数、锁存、结束。IDLE等待启动信号拉高预置闸门COUNT实际闸门有效期间两个计数器工作LATCH检测到实际闸门下降沿后同时锁存两个计数值DONE产生data_valid脉冲然后回到IDLE。localparam IDLE 2d0; localparam COUNT 2d1; localparam LATCH 2d2; localparam DONE 2d3; reg [1:0] state; reg [31:0] cnt_fx_latch; reg [31:0] cnt_ref_latch; always (posedge clk_ref or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt_fx_latch 32d0; cnt_ref_latch 32d0; end else begin case (state) IDLE: begin gate_pre 1b1; state COUNT; end COUNT: begin if (!gate_real) begin cnt_fx_latch cnt_fx; cnt_ref_latch cnt_ref; state LATCH; end end LATCH: begin gate_pre 1b0; state DONE; end DONE: begin data_valid 1b1; state IDLE; end default: state IDLE; endcase end end这里有个容易忽略的问题状态机用的是基准时钟clk_ref但检测gate_real的下降沿时gate_real是被测信号域的同步信号对基准时钟域来说仍然是异步的。所以如果竞赛时间紧张可以直接用一种更省心的做法把gate_real再做两级同步到clk_ref域然后检测同步后的下降沿。这个开销很小但能避免不少时序上的不确定性。3.4 频率换算把两个计数值变成Hz两个计数值锁存出来后接下来就是算频率。公式前面已经给了f cnt_fx × F0 / cnt_ref。如果直接用FPGA做这个运算有两条路。一条是用除法器IP核简单可靠但需要额外配置IP在电赛现场如果对IP不熟容易卡壳。另一条是写一个简单的时序除法器。这里分享一个我常用的无符号32位恢复余数除法器逻辑清晰适合现场快速移植module div32( input wire clk, input wire rst_n, input wire start, input wire [31:0] dividend, input wire [31:0] divisor, output reg [31:0] quotient, output reg done ); reg [63:0] tmp; reg [5:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 6d0; done 1b0; quotient 32d0; end else if (start) begin tmp {32d0, dividend}; cnt 6d0; done 1b0; end else if (!done) begin tmp {tmp[62:0], 1b0}; if (tmp[63:32] divisor) begin tmp[63:32] tmp[63:32] - divisor; tmp[0] 1b1; end cnt cnt 1b1; if (cnt 6d31) done 1b1; end if (done) quotient tmp[31:0]; end endmodule这个除法器每32个时钟周期算出一次结果。调用的地方要先做一次乘法让cnt_fx先乘以一个代表基准频率的常数再除以cnt_ref。要注意乘法结果可能超过32位乘法结果放到64位里处理。如果项目里已经有软核或者外面接了单片机完全可以把两个计数值直接输出让软件去算。这种方案在电赛中反而更快更稳因为FPGA里省掉一大块运算逻辑综合时序压力也小很多。4. 仿真验证与上板实测几个必须踩的坑4.1 仿真时如何验证闸门对齐写完代码第一件事不是上板而是仿真。用ModelSim或者Vivado自带的仿真器搭一个能产生不同频率激励的testbench我一般会同时给三个被测频率做回归检查低频的1Hz、中频的1kHz、高频的10MHz。仿真时重点看两个波形gate_pre和gate_real。你会看到gate_real比gate_pre延迟几个被测信号周期这正常。关键是gate_real的高电平宽度内包含的被测信号上升沿个数一定是整数这就是等精度测频的底层特征。如果发现gate_real的跳变沿和被测信号有明显的错位那基本可以断定两级同步时钟用错了。还有一个小技巧把基准时钟计数cnt_ref拉出来看。基准时钟频率已知如果仿真测出的cnt_ref对应时间和预置闸门时长差太多说明同步逻辑把实际闸门拉宽或窄了得回头查同步模块。4.2 上板实测的误差来源与校准仿真通过只是第一步上板才是各种问题的集中爆发区。我在第一次上板实测时用信号发生器给了一个10MHz方波频率计显示9.999326MHz误差大约0.007%。这个误差主要来自两处一是开发板上的有源晶振本身有精度误差通常是无源晶振更容易受温度影响二是被测信号和基准时钟两个时钟域的起始相位差异不过这部分在等精度方案里已经压得很低。如果发现误差明显偏大先不要怀疑算法先检查晶振。用高精度频率计或者示波器测一下开发板的基准时钟实际频率然后在代码里把参数CLK_FREQ改成实际值。比如标称50MHz的晶振实测是49.99982MHz就把参数改成49999820误差立刻小一个数量级。如果误差还是大就需要看计数结果本身。把cnt_fx和cnt_ref通过串口或者调试探头拉出来用已知频率手工计算一遍看是哪一路计数出了偏差。这个方法能把问题精确锁定到模块级别比盲调快得多。4.3 被测信号带毛刺怎么办比赛现场的信号不可能像信号发生器输出那样干净。电机、开关电源、数字电路耦合过来的噪声都可能让被测信号带上毛刺一个毛刺就是一个错误脉冲频率计结果就会跳动。这时候前端整形电路的作用就体现出来了。比较器加迟滞施密特触发结构是首选能在物理层面滤掉大部分噪声。如果信号幅度不大先用运放做一级放大和整形再进比较器。我在比赛中把这部分电路放在了主控板外侧的小板上调试很方便也不会干扰FPGA的电源。如果前端已经来不及改Verilog这边也有补救手段。对输入的被测信号先做三选二或五选三的滤波也就是连续采几个周期少数服从多数输出。这种方式能滤掉窄毛刺但也有代价会引入额外的延迟对高频信号响应变差。所以这个滤波逻辑最好设计成可以开关的只在信号确实脏的时候打开。现象可能原因排查手段低频测量跳动大预置闸门太短同步时序不对加长闸门检查gate_real是否对齐全部频段误差方向一致基准晶振频率不准实测晶振频率修改参数高频测量异常前端整形带宽不够毛刺干扰换高速比较器加滤波偶发亚稳态导致卡死异步信号未同步检查是否用了两级触发器同步5. 从这道题收获的工程经验5.1 跨时钟域处理是FPGA的通用基本功做完这个题目你会发现等精度测频本质上是在处理跨时钟域问题预置闸门在基准时钟域产生真实闸门在被测信号域生成计数器在两个不同时钟域里工作最后数据还要回到基准时钟域去计算。整个系统的核心并不是“测频”而是“如何让两个时钟域安全地协作”。这个经验放在更大的项目里同样成立。无论是UART收发、I2C读写EEPROM、SPI接口还是DDR3读写控制只要是FPGA和外部设备通信都绕不开跨时钟域数据传递。我把F题里学到的两级同步、乒乓缓存思路用在后面的项目中至少少踩了七八个时序导致的坑。5.2 用“公式先行”的思路做设计调试过程中我还有一个很深的体会写Verilog之前一定要先把数学关系在纸上推清楚。比如频率计算公式是否会有位宽溢出、除法器结果怎么对齐、显示时小数点该放在哪里这些问题如果等代码写完再想改起来牵一发动全身。我现在的习惯是先在文档里写出频率值的整数算式把所有中间变量的位宽标好再开始写RTL。等精度测频的公式并不复杂但如果没有事先规划位宽很有可能在综合时报出关键路径问题或者运算结果莫名其妙少几位。5.3 给备赛同学的建议如果你正在为电赛备赛我想说拿这道题练手是性价比很高的选择。它涉及的知识点覆盖面广——跨时钟域、计数器设计、状态机、除法器、显示驱动、硬件整形电路每一样都是FPGA开发的常规武器。另外建议提前把仿真环境调好把串口调试工具写好把信号发生器、电源、示波器这些仪器的使用流程跑熟。代码能力只是比赛的一部分调试效率往往决定最终成绩。把一套固定的调试流程提前练成肌肉记忆赛场上遇到问题才不会慌。最后分享一个我踩过之后才明白的细节不管你等精度测频的代码写得多漂亮前级调理电路的整形质量决定了频率计的上限。信号发生器直接输出方波时怎么测都准一旦接上真实电路里的信号毛刺、幅度波动、占空比畸变全都会冒出来。所以做这个题建议拿出一半时间调硬件前端。这句话等你上板实测的时候就懂了。本文还有配套的精品资源点击获取