简介:这份资源是2015年全国大学生电子设计竞赛F题的等精度测频完整工程,面向学习FPGA数字系统设计与Verilog编程的学生及参赛者,用于解决高频信号精确测量与串口回传的典型问题。压缩包共428个文件、约31.63MB,以Verilog源码(.v)、Quartus工程与综合中间文件(.qpf、.qsf、.cdb、.hdb)、报告日志(.rpt、.logdb)及UART系统文件(.sopcinfo、.qsys)为主,完整保留了从源码到布局布线的工程痕迹。已有1319人学习下载。读者可从中获得基于50MHz采样时钟的等精度测频核心逻辑、UART串行发送模块、时钟分频与均值处理等子模块实现,以及Quartus综合适配、时序约束与资源占用的参考配置,便于对照赛题要求复现系统、理解等精度测量原理并排查时序与通信问题。
1. 等精度测频到底解决了什么痛点
做过频率测量的工程师多半踩过同一个坑:用固定闸门去数脉冲,低频段误差大得离谱,高频段又浪费了计数器的位宽。等精度测频这套方法,核心就一句话——让闸门时间跟着被测信号走,而不是让被测信号迁就固定闸门。2015年电赛F题把这道题摆上台面,要求用FPGA在宽频范围内保持恒定相对误差,很多人第一次意识到:原来测频的精度瓶颈不在计数器位宽,而在闸门和被测信号之间的同步关系。
这篇笔记面向两类人:一是正在用Verilog做频率计、想把误差压到10^-4量级以内的FPGA初学者;二是做过传统测频、但对“等精度”三个字只停留在公式层面、没真正跑过时序的熟手。我会从原理推导讲到Verilog模块划分,再到实际综合后的参数配置和排错,把这条链路走通。你不需要有电赛背景,只要手上有块能跑50MHz以上时钟的FPGA开发板,就能跟着复现。
2. 等精度测频的原理拆解与误差边界
2.1 从传统测频的误差公式说起
传统测频有两种基本做法。第一种是测频法:在固定闸门时间T内数被测信号的周期数N,频率f = N / T。这种方法的量化误差是±1个计数,相对误差为1/N。当被测频率很低时,N很小,误差就很大。第二种是测周法:测一个被测周期内标准时钟的计数M,频率f = f_clk / M,相对误差为1/M。被测频率越高,M越小,误差越大。
两种方法的误差方向刚好相反,所以自然想到:能不能让闸门时间不是固定的,而是被测信号周期的整数倍?这就是等精度测频的出发点。实际闸门时间不再等于预设值,而是被测信号周期的整数倍,同时用标准高频时钟去测量这个实际闸门时间的长度。这样,被测信号的计数N没有±1误差(因为闸门边界和被测信号边沿对齐),误差只来自标准时钟的±1计数。
推导一下:设预设闸门时间为T_pr,实际闸门时间为T_real,标准时钟频率为f_clk,标准时钟计数为M,被测信号计数为N。则:
f_x = N / T_real = N / (M / f_clk) = N × f_clk / M
相对误差为:
|Δf_x / f_x| = |ΔM / M| ≤ 1 / M = 1 / (T_real × f_clk)
关键结论:相对误差只与标准时钟频率和实际闸门时间有关,与被测信号频率无关。这就是“等精度”的含义——在整个测量范围内,相对误差恒定。
2.2 实际闸门时间的生成逻辑
等精度测频的核心时序控制在于:预设闸门信号T_pr是一个由系统时钟产生的固定宽度脉冲,但它并不直接作为计数使能。真正控制计数的是“实际闸门”信号,它的开启和关闭都发生在被测信号的上升沿。
具体做法是:当T_pr的上升沿到来时,并不立即开始计数,而是等待被测信号的下一个上升沿,此时拉高实际闸门信号;当T_pr的下降沿到来时,也不立即停止计数,而是等待被测信号的下一个上升沿,此时拉低实际闸门信号。这样,实际闸门的宽度就是被测信号周期的整数倍,被测信号计数N不存在±1误差。
标准时钟计数M则在实际闸门有效期间对高频时钟计数,由于实际闸门的开闭与被测信号对齐,与标准时钟不同步,所以M存在±1误差。但因为f_clk通常远高于f_x,M远大于N,所以1/M远小于1/N,总误差被压得很低。
2.3 误差边界与参数选择
根据误差公式,要保证相对误差小于某个阈值,需要M足够大。例如要求误差小于10^-4,则M ≥ 10000。如果f_clk = 100MHz,则T_real ≥ 10000 / 100MHz = 100μs。如果被测信号最低频率为1kHz,周期为1ms,那么T_real至少需要包含100个被测周期,即T_real ≥ 100ms。此时M = 100ms × 100MHz = 10^7,误差为10^-7,远优于要求。
但T_real不能无限大,否则测量刷新率太低。实际工程中需要在精度和刷新率之间折中。常见做法是:预设闸门时间取10ms到1s之间,根据被测频率范围调整。对于电赛F题那种要求宽范围测量的场景,通常取T_pr = 100ms左右,配合100MHz标准时钟,可以在1Hz到100MHz范围内保持优于10^-5的相对误差。
还有一个容易被忽略的边界:被测信号频率不能超过标准时钟频率的一半,否则在实际闸门内被测信号计数N可能超过标准时钟计数M,虽然公式上没问题,但计数器位宽需要重新评估。另外,被测信号频率也不能太低,否则实际闸门时间会远大于预设值,刷新率骤降。
2.4 与多周期同步测频的区别
很多人把等精度测频和多周期同步测频混为一谈。两者确实很像,都是让闸门与被测信号同步,但有一个关键区别:多周期同步测频通常只用一个计数器对被测信号计数,用另一个计数器对标准时钟计数,实际闸门由被测信号同步;而等精度测频在实际实现中,往往还会对预设闸门进行同步处理,并且强调“等精度”这个结果——即相对误差恒定。在Verilog实现层面,两者结构几乎一样,但等精度测频更强调误差公式的推导和参数边界的计算。如果你在电赛F题里看到“等精度”三个字,直接按多周期同步的结构做,再补上误差分析,基本不会跑偏。
3. Verilog模块划分与核心代码实现
3.1 顶层模块与时钟域规划
整个设计建议分成三个模块:顶层控制模块、闸门同步模块、计数与计算模块。顶层负责例化各子模块并连接信号;闸门同步模块负责生成实际闸门信号;计数与计算模块负责在闸门有效期间计数,并在测量结束后计算频率值。
时钟域方面,系统时钟f_clk同时作为标准计数时钟和逻辑控制时钟。被测信号f_x作为异步输入,需要先经过两级寄存器同步,再用于边沿检测。实际闸门信号的拉高和拉低都发生在f_x的上升沿,但后续计数逻辑在f_clk域进行,所以实际闸门信号需要再同步回f_clk域,或者直接用f_x域的使能信号去控制f_clk域的计数器——后者更常见,但要注意跨时钟域时的使能信号宽度。
我一般会这样规划:f_x经过同步和边沿检测后,生成一个单周期脉冲f_x_pos,表示被测信号上升沿。闸门同步模块在f_clk域检测T_pr的上升沿和下降沿,然后分别等待f_x_pos到来时翻转实际闸门信号。实际闸门信号本身在f_clk域生成,但翻转条件依赖f_x_pos,所以它是一个“被f_x_pos置位/复位”的寄存器。
3.2 闸门同步模块的Verilog实现
// 闸门同步模块:生成实际闸门信号 module gate_sync ( input wire clk, // 系统时钟,也是标准计数时钟 input wire rst_n, // 低电平复位 input wire t_pr, // 预设闸门信号,由顶层产生 input wire f_x_pos, // 被测信号上升沿脉冲,单周期 output reg gate_real // 实际闸门信号 ); reg t_pr_d1, t_pr_d2; // 预设闸门打两拍,用于边沿检测 wire t_pr_rise, t_pr_fall; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin t_pr_d1 <= 1'b0; t_pr_d2 <= 1'b0; end else begin t_pr_d1 <= t_pr; t_pr_d2 <= t_pr_d1; end end assign t_pr_rise = t_pr_d1 & ~t_pr_d2; assign t_pr_fall = ~t_pr_d1 & t_pr_d2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin gate_real <= 1'b0; end else begin if (t_pr_rise && f_x_pos) gate_real <= 1'b1; else if (t_pr_fall && f_x_pos) gate_real <= 1'b0; end end endmodule这段代码的逻辑说明:t_pr是预设闸门信号,宽度固定,由顶层计数器产生。t_pr_d1和t_pr_d2用于检测t_pr的上升沿和下降沿。当检测到t_pr上升沿且同时f_x_pos有效时,gate_real拉高;当检测到t_pr下降沿且f_x_pos有效时,gate_real拉低。注意这里用了“同时”条件,实际实现中t_pr_rise和f_x_pos可能不在同一周期,所以更稳妥的写法是:检测到t_pr_rise后,等待下一个f_x_pos再拉高gate_real。上面的写法假设t_pr_rise和f_x_pos恰好同周期,实际综合后可能不满足,需要改成状态机形式。
参数说明:clk建议不低于50MHz,f_x_pos的脉冲宽度必须是一个clk周期,否则可能被漏采。t_pr的宽度决定了预设闸门时间,典型值100ms。
3.3 计数与频率计算模块
// 计数模块:在实际闸门有效期间计数 module counter ( input wire clk, input wire rst_n, input wire gate_real, input wire f_x_pos, output reg [31:0] cnt_std, // 标准时钟计数 output reg [31:0] cnt_sig // 被测信号计数 ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_std <= 32'd0; cnt_sig <= 32'd0; end else if (gate_real) begin cnt_std <= cnt_std + 1'b1; if (f_x_pos) cnt_sig <= cnt_sig + 1'b1; end end endmodule这段代码在gate_real有效期间,每个clk周期cnt_std加1,每检测到f_x_poscnt_sig加1。注意cnt_sig的位宽需要根据最大被测频率和闸门时间估算。例如最大被测频率100MHz,闸门时间100ms,则cnt_sig最大为10^7,需要24位以上。cnt_std在100MHz时钟下100ms计数为10^7,同样需要24位以上。实际工程中建议留足余量,用32位。
频率计算:f_x = cnt_sig × f_clk / cnt_std。这个除法在FPGA里直接做会消耗大量资源,通常的做法是:如果只需要显示频率值,可以用嵌入式软核或者状态机做逐次逼近除法;如果对刷新率要求不高,也可以把cnt_sig和cnt_std传到上位机,由软件计算。在电赛F题场景下,通常要求实时显示,所以建议在FPGA内用流水线除法器或者调用IP核。
3.4 预设闸门产生与测量结束标志
// 预设闸门产生模块 module t_pr_gen ( input wire clk, input wire rst_n, output reg t_pr, output reg done ); parameter T_PR_CNT = 32'd5_000_000; // 100ms @ 50MHz reg [31:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 32'd0; t_pr <= 1'b0; done <= 1'b0; end else begin if (cnt == T_PR_CNT - 1) begin cnt <= 32'd0; t_pr <= ~t_pr; // 翻转产生周期信号 done <= 1'b1; // 测量结束脉冲 end else begin cnt <= cnt + 1'b1; done <= 1'b0; end end end endmodule这里T_PR_CNT决定了预设闸门的半周期。如果clk = 50MHz,T_PR_CNT = 5_000_000,则半周期为100ms,t_pr的周期为200ms,高电平宽度100ms。done信号在t_pr翻转时产生一个脉冲,可用于触发频率计算或数据锁存。注意done的宽度只有一个clk周期,后续模块需要做边沿检测或脉冲展宽。
参数说明:T_PR_CNT需要根据实际时钟频率和所需闸门时间计算。闸门时间越长,测量精度越高,但刷新率越低。电赛F题通常要求1秒内至少刷新一次,所以闸门时间不宜超过500ms。
4. 避坑与常见问题排查
4.1 实际闸门信号毛刺导致计数错误
现象:综合后仿真或上板测试时,cnt_sig偶尔比预期多1或少1,频率计算结果跳变。
原因:gate_real信号在f_clk域生成,但它的置位/复位条件依赖f_x_pos,而f_x_pos是异步信号同步后的单周期脉冲。如果f_x_pos恰好出现在gate_real翻转的同一个clk周期,可能产生毛刺或亚稳态,导致计数器误动作。
解决:在gate_real输出前再加一级寄存器打拍,或者用状态机确保gate_real的翻转只发生在f_x_pos有效后的下一个clk周期。更稳妥的做法是:用f_x_pos作为使能,在f_clk域生成一个“请求翻转”标志,下一个周期再真正翻转gate_real。
4.2 标准时钟计数溢出
现象:测量高频信号时,cnt_std或cnt_sig达到最大值后回绕,频率计算结果完全错误。
原因:计数器位宽不足。例如用16位计数器,在100MHz时钟下100ms闸门,计数可达10^7,远超65535。
解决:根据最大闸门时间和最高时钟频率估算位宽。公式:位宽 ≥ log2(f_clk × T_real_max)。建议直接用32位,资源消耗可接受。如果资源紧张,可以用24位,但要做好边界保护。
4.3 被测信号频率接近标准时钟频率时精度下降
现象:当f_x接近f_clk时,相对误差明显增大,甚至超过理论值。
原因:等精度测频的误差公式假设M远大于1,且标准时钟计数不存在与被测信号的耦合。但当f_x接近f_clk时,实际闸门内被测信号周期数N和标准时钟计数M接近,±1误差对两者的影响都不可忽略。另外,如果f_x > f_clk/2,f_x_pos可能漏采。
解决:选择f_clk至少为被测信号最高频率的4倍以上。如果被测信号频率范围很宽,可以考虑分段测量,或者用更高频率的标准时钟。电赛F题通常要求测量范围到100MHz,所以标准时钟建议200MHz以上,或者用FPGA内部的高速时钟资源。
4.4 复位后首次测量结果异常
现象:上电复位后第一次测量的频率值明显偏大或偏小,后续测量正常。
原因:复位时cnt_std和cnt_sig没有清零,或者gate_real在复位后处于不确定状态,导致第一次计数包含了无效数据。
解决:确保复位时所有计数器和状态机都回到初始状态。gate_real复位后应为0,t_pr复位后应为0。另外,可以在复位后等待一个完整的t_pr周期再开始测量,避免第一次闸门不完整。
4.5 除法器资源消耗过大
现象:综合后布局布线失败,或者时序不满足,提示除法器路径过长。
原因:直接用“/”运算符综合出组合逻辑除法器,延迟大、资源多。
解决:改用流水线除法器,或者调用FPGA厂商提供的除法IP核。如果只是显示用,可以把cnt_sig和cnt_std通过UART传到上位机,由软件计算。在电赛F题中,如果要求实时显示,建议用状态机实现逐次逼近除法,每周期处理一位,32位除法需要32个周期,在50MHz时钟下不到1μs,完全可接受。
5. 从能跑到好用:三个进阶技巧
5.1 用双计数器交替测量提升刷新率
前面提到,闸门时间越长精度越高,但刷新率越低。一个实用的技巧是:用两组计数器交替工作。第一组在实际闸门有效期间计数,闸门结束后立即开始计算并输出结果;同时第二组在下一个闸门周期开始计数。这样,测量结果几乎连续输出,刷新率提高一倍。实现上需要两个gate_sync模块和两个counter模块,用状态机控制切换。代价是资源翻倍,但对于现代FPGA来说完全可以接受。
5.2 动态调整预设闸门时间
如果被测信号频率范围很宽,固定闸门时间可能在高频段浪费精度,在低频段刷新太慢。可以动态调整T_PR_CNT:先用一个较短的闸门时间快速测出大致频率,再根据频率值选择合适的闸门时间进行精确测量。例如,先测10ms,如果频率低于1kHz,则切换到1s闸门;如果频率高于1MHz,则保持10ms。这样在保证精度的同时,尽量提高刷新率。实现上需要一个状态机和一个查找表,根据cnt_sig的值决定下一周期的T_PR_CNT。
5.3 用在线仿真验证时序
等精度测频的时序比较微妙,尤其是gate_real的翻转和f_x_pos的关系。建议在综合前先用仿真工具跑一遍时序。测试平台可以这样写:产生一个50MHz的clk,一个频率可调的f_x,一个周期性的t_pr,然后观察gate_real的宽度是否等于f_x周期的整数倍,cnt_sig是否没有±1误差。如果仿真通过,上板基本不会有大问题。我一般会在仿真里扫频,从1kHz到100MHz,每个频点跑一次,记录误差曲线,确认在整个范围内相对误差都优于设计目标。
最后说一个血泪教训:等精度测频的公式很简单,但真正跑起来,坑都在跨时钟域和计数器位宽上。我见过太多人公式推得头头是道,上板一测发现误差比传统测频还大,最后查出来是f_x_pos漏采或者gate_real有毛刺。所以,仿真别省,时序约束别偷懒,计数器位宽别抠门。希望帮到你。
本文还有配套的精品资源,点击获取