简介:这是一套基于 Quartus II 13.1 与 Verilog HDL 实现的等精度数字频率计完整工程,适合 FPGA 学习者、电子类课程设计与需要精确测频的开发者使用。设计目标覆盖 1Hz~150MHz 宽频率范围,测量误差小于 0.01%,工程内包含分频、同步计数、译码显示、串口通信以及 Modelsim 仿真验证等模块,思路完整。压缩包共 390 个文件,约 17.56MB,主要文件类型包括 tdf、v 源码文件,qsf/qpf 工程配置,sof 下载文件,以及大量 cdb/hdb/ddb 编译中间文件,便于查看综合、适配与仿真过程。已有 1408 人学习下载。通过该工程可掌握等精度测频原理、Verilog 模块化设计、FPGA 时序分析与仿真排错思路,也能直接修改参数并部署到开发板验证。
Quartus II 13.1 + Verilog 等精度数字频率计:从原理到上板的全流程记录
手里正好有一批 Cyclone IV 的开发板,平时做流水灯、串口收发这类实验总觉得离“正经工程”差点距离。这次干脆把等精度数字频率计完整做了一遍——从测量原理推导、Verilog 顶层到底层模块编码,再到 Quartus II 13.1 环境下的仿真、引脚约束和板级实测,整个过程踩了不少坑,也把很多细节想通了。这篇就把完整方案和调试心得写下来,给正在做类似项目的朋友一个可直接参考的模板。
这个项目适合两类人:一是 FPGA 入门后想系统练一遍时序控制的同学,等精度测频涉及边沿同步、双计数器、锁存与计算、动态扫描显示,刚好覆盖基础到进阶的知识点;二是实验室或项目里真需要频率测量的朋友,它比单纯买一台频率计更能理解底层逻辑,而且成本就是一块 FPGA 开发板加几个电阻电容。
1. 为什么选择等精度测频方案
1.1 传统测频法的痛点
在做频率测量时,最常见的两种思路是测频法和测周法。测频法是在固定闸门时间内对被测信号计数,比如 1 秒闸门内数到 1200 个脉冲,频率就是 1.2kHz。这个方案在信号频率高时精度不错,但信号一旦低到几十赫兹,一个闸门内可能只数到几十个脉冲,计数误差 1 个脉冲就会带来百分之几的偏差。测周法刚好反过来,它测被测信号一个周期内有多少个标准时钟脉冲,低频信号周期长、数到的时钟多、精度高,但高频信号周期短,数不到几个时钟,精度就崩了。两种方法本质上是把“量化误差”往哪个频段推的问题,没法在宽频范围内同时保证精度。
我做频率计的目标很直接:希望 1Hz 到 10MHz 甚至更高频段内,测量误差都控制在万分之一左右,而不是只在某个窄频段好用。显然,传统的单一测频或测周方案做不到,这时候就得正视“测量原理本身决定了精度上限”这个事实,换一条路走。
1.2 等精度测量的核心思路
等精度测频法,也叫多周期同步测频法,核心思想是不再直接固定闸门时间,而是让实际闸门与被测信号同步。先产生一个预置闸门信号,比如预置 1 秒,但真正去计数的闸门并不是这个 1 秒,而是等被测信号的上升沿到来后,才把闸门拉高;闸门结束也不是硬生生掐表,而是等到预置闸门结束后的第一个被测信号上升沿才把闸门拉低。这样实际闸门宽度严格等于整数个被测信号周期,所以对被测信号的计数 Nx 是精确的,不存在被测信号本身的量化误差。
在这个同步闸门内,同时用标准时钟计数得到 Ns。标准时钟频率 fs 是已知的,比如开发板上 50MHz 晶振。被测频率就可以用公式 fx = (Nx / Ns) × fs 计算出来。这里误差来源主要在 Ns 上,而 Ns 数值很大——1 秒闸门下 50MHz 时钟可以数到五千万,误差从 1 个脉冲折算到相对值就是 1/50000000,也就是 2×10⁻⁸ 量级,远优于万分之一的目标。这就是为什么等精度方案可以在宽频率范围内保持误差一致,它不是把误差消掉,而是把误差从“被测信号端”搬到了“标准时钟端”,而标准时钟的脉冲密度远高于被测信号,量化误差自然被摊薄了。
2. 系统架构与关键参数设计
2.1 模块划分与数据流
整个频率计的硬件架构分为五块:闸门控制模块、双计数器模块、数据锁存模块、频率计算模块和数码管显示模块。闸门控制模块负责把预置闸门信号同步到被测信号的上升沿,生成真正的计数闸门;双计数器模块包含两个 32 位计数器,一个统计被测信号脉冲数,一个统计标准时钟脉冲数;数据锁存模块在闸门结束时锁存两个计数结果,避免计算过程中数据变化导致的错误;频率计算模块执行乘除法得到频率值;显示模块负责动态扫描驱动数码管。
数据流是一条典型的“采集-锁存-计算-显示”链路。每次测量周期结束后,锁存器里的数据被计算模块读取,算出频率值后更新显示,同时计数器清零,进入下一轮测量。这个流程里最容易出问题的是时序:清零动作必须发生在锁存完成之后,否则会把还没用过的数据覆盖掉。实际代码里我用一个状态机来控制“等待闸门结束-锁存-计算-清零”的顺序,保证每个周期严格串行。
2.2 参数选择与精度估算
标准时钟频率 fs 直接决定测量精度。开发板上的 50MHz 有源晶振精度一般在几十 ppm 级别,对绝大多数测量场景够用。如果追求更高精度,可以换成温补晶振,并把 fs 的精确值校准后写进计算参数里。
预置闸门时间也需要权衡。闸门越长,Ns 越大,精度越高,但测量刷新速率越慢。我用 1 秒预置闸门,刷新率是每秒一次,显示值稳定不跳变。如果测的是音频范围内信号,想快速看变化趋势,可以改成 0.1 秒,但显示分辨率会下降。计数器的位宽也要提前算清楚:50MHz 时钟在 1 秒内最多计数 50000000,需要 26 位;如果闸门加长到 10 秒,就需要 29 位。统一用 32 位计数器给后续扩展留足余量,是省事的做法。
3. Verilog 核心代码实现细节
3.1 同步闸门控制逻辑
闸门控制是整个设计的灵魂,写不好会直接导致测量值错乱。核心思路是先检测被测信号的上升沿,再用这个上升沿去同步预置闸门。我用两级触发器对被测信号打拍,避免亚稳态传播,然后通过组合逻辑生成上升沿脉冲信号,最后用这个脉冲信号去更新输出闸门。
reg sig_d1, sig_d2; wire sig_rise = sig_d1 & ~sig_d2; always @(posedge clk_std or negedge rst_n) begin if (!rst_n) begin sig_d1 <= 1'b0; sig_d2 <= 1'b0; end else begin sig_d1 <= signal_in; sig_d2 <= sig_d1; end end always @(posedge clk_std or negedge rst_n) begin if (!rst_n) gate_actual <= 1'b0; else if (gate_pre) gate_actual <= gate_actual | sig_rise; else gate_actual <= gate_actual & ~sig_rise; end这里有个细节值得注意:输出闸门 gate_actual 的打开和关闭都依赖 sig_rise,也就是被测信号的真实边沿。预置闸门 gate_pre 只是一个“允许打开”和“请求关闭”的控制信号,它不直接决定计数闸门的边界。这样一来,实际计数区间一定是整数个被测周期,这是等精度方案最关键的保证。
实测中我发现一个容易忽略的点:如果被测信号在 gate_pre 有效期间没有上升沿,gate_actual 会一直保持高电平,导致闸门异常拉长。解决方法是给 gate_actual 设置一个最大超时限制,或者确保预置闸门宽度远大于被测信号周期。对于极低频信号(比如 1Hz 以下),建议先用分频器把信号倍频后再接入测量模块,或者延长预置闸门到 10 秒。
3.2 双计数器与锁存实现
双计数器的设计思路是在 gate_actual 为高时,每个标准时钟上升沿对 clk_cnt 加一,每个被测信号上升沿对 sig_cnt 加一。上升沿检测同样用两级触发器打拍,然后生成脉冲作为计数使能。
always @(posedge clk_std or negedge rst_n) begin if (!rst_n) clk_cnt <= 32'd0; else if (gate_actual) clk_cnt <= clk_cnt + 1'b1; else if (clear_flag) clk_cnt <= 32'd0; end always @(posedge signal_in or negedge rst_n) begin if (!rst_n) sig_cnt <= 32'd0; else if (gate_actual) sig_cnt <= sig_cnt + 1'b1; else if (clear_flag) sig_cnt <= 32'd0; end注意被测信号计数器时钟用的是 signal_in 本身,这意味着被测信号的毛刺会直接影响计数结果。我在 signal_in 输入 FPGA 引脚前,串了一个施密特触发器芯片(比如 74HC14)做整形,把缓慢变化的模拟信号转换成陡峭的数字脉冲,能去掉不少噪声干扰。如果没有整形器件,也可以在 FPGA 内部对 signal_in 做 3-5 次连续采样再输出,等效于一个数字滤波器,但过度滤波会损失高频信号,需要根据测量范围权衡。
锁存部分比较好理解:在 gate_actual 的下降沿到来时,把 clk_cnt 和 sig_cnt 分别锁存到 reg_clk_cnt 和 reg_sig_cnt。这个下降沿也通过边沿检测电路抓取,生成 latch_flag 信号。锁存完成后,主状态机进入计算态,同时给出 clear_flag 清零计数器。清零点必须等到锁存动作完成之后至少一个时钟周期,我是直接用状态机分拍控制的,比单独写延迟逻辑更可靠。
3.3 频率计算与显示驱动
频率计算公式是 fx = (sig_cnt / clk_cnt) × fs。直接先除再乘会损失精度,比如 sig_cnt = 1234,clk_cnt = 40000000,先除得到 0,再乘得到 0Hz,显然不对。正确做法是先乘后除:fx = (sig_cnt × fs) / clk_cnt。sig_cnt 最大也就千万级,乘以 50M 后达到 10¹⁴,需要 64 位乘法和除法。
Quartus II 13.1 里,64 位整数的乘除法可以直接用*和/运算符写,综合器会自动映射到 DSP 或片上逻辑。但 64 位除法在 Cyclone IV 上综合出来的组合逻辑路径很长,时序会很紧张。实际做法是调用 LPM_DIVIDE IP 核或者自己写一个基于移位减法的时序除法器。考虑到这是学习项目,我用了 LPM_DIVIDE,设成流水线模式,牺牲几个周期的延迟换来更干净的时序。
wire [63:0] numerator = sig_cnt * 50_000_000; wire [31:0] quotient, remainder; lpm_divide #( .LPM_WIDTHN(64), .LPM_WIDTHD(32), .LPM_NREPRESENTATION("UNSIGNED"), .LPM_PIPELINE(8) ) freq_div ( .clock(clk_std), .numer(numerator), .denom(clk_cnt), .quotient(quotient), .remainder(remainder) );显示模块我用的是 6 位数码管,动态扫描显示频率值,单位固定为 Hz。扫描时钟频率设在 1kHz 左右,每一位点亮 1ms,循环刷新,人眼视觉暂留看起来就是六位同时亮。频率值先用 BCD 转换模块把二进制转成 6 位十进制数,再按位选通数码管。BCD 转换可以用多次比较的加法器实现,也可以用查表法,代码量不大,是整个工程里最简单但最容易忽视的部分——很多同学的板子数码管一亮一暗或者闪烁,基本都是扫描频率太低或者段选位选没共地。
4. Quartus II 13.1 工程实操记录
4.1 工程建立与引脚约束
Quartus II 13.1 虽然版本老,但对 Cyclone IV 支持非常完善,而且网上资料多,出错好排查。新建工程的流程是:File -> New Project Wizard -> 选择芯片型号(我用的是 EP4CE6E22C8) -> 添加 Verilog 文件 -> 编译综合。综合遇到语法报错时,双击错误信息能直接跳到对应代码行,这比很多开源工具链好在易用性上。
引脚分配用 Pin Planner 操作,界面上按引脚名查找到对应 FPGA 管脚,填入编号保存。50MHz 晶振接到 PIN_E1,数码管段选接到对应 GPIO 口,位选接到另外几个 GPIO。这里有个细微但致命的点:如果某个引脚被 Quartus 默认识别为特殊功能脚(比如配置相关引脚),必须先在 Device and Pin Options 里把"Use as regular I/O"打开,否则编译虽然过,下载后电平无论如何驱动不起来。
我踩过最典型的一个坑是:编译成功、烧录成功、示波器看不到任何输出,查了两小时发现是引脚被软件默认锁死为 VCC 或者 GND。所以第一次上板前,强烈建议先在 Pin Planner 里把每个输出脚和"Actual Location"逐一对一遍,再结合原理图确认,别嫌麻烦。
4.2 ModelSim 仿真验证流程
Quartus 自带 ModelSim-Altera Starter Edition,可以直接调用。仿真步骤是:Assignments -> Settings -> EDA Tool Settings -> Simulation,选择 ModelSim-Altera,Verilog 仿真库选对应芯片系列。然后新建一个 tbench 文件,例化被测模块,生成激励信号。
testbench 里我模拟了一路 123456Hz 的被测信号。用 Verilog 写了个 always 块,周期取倒数换算到仿真时间:
parameter PERIOD = 1_000_000_000.0 / 123456; // 单位ns always #(PERIOD/2) sig_in = ~sig_in;注意仿真器的时间精度要设成 1ps 或更小,否则高频信号仿真会有明显截断误差。预置闸门在仿真里不能真的等 1 秒,否则仿真跑完要很久。我直接把预置闸门设成 1000 个标准时钟周期,验证逻辑正确后再改回实际值。模型仿真里,等精度测频算法可以做到 fx 计算值几乎等于 123456,误差只在最后几位。
4.3 上板实测与结果分析
上板前先检查电源:开发板有 3.3V 和 1.2V 两个核心电源,很多故障都源于电源纹波太大。系统跑起来后用信号发生器输入 1kHz、10kHz、100kHz、1MHz、5MHz 五组正弦波,经过整形电路接入 FPGA。数码管显示结果和信号发生器设定值对比如下:
| 信号源设定频率 | 频率计显示值 | 相对误差 |
|---|---|---|
| 1.000 kHz | 0.999 kHz | 0.1% |
| 10.000 kHz | 10.001 kHz | 0.01% |
| 100.000 kHz | 100.003 kHz | 0.003% |
| 1.000 MHz | 0.999 MHz | 0.1% |
| 5.000 MHz | 5.002 MHz | 0.04% |
低频 1kHz 时误差偏大,原因不是等精度算法本身,而是我的信号发生器输出正弦波在整形后有个 AGC 稳定过程,边沿抖动偏大。换用 TTL 电平的方波信号源后,1kHz 时误差立刻降到 0.01% 以下。这说明等精度方案对边沿质量是敏感的,前面的整形电路非常有必要。
5. 常见问题与调试心得
5.1 典型问题速查
碰到问题先不要怀疑算法,九成是时序或环境问题。下面这张表是调试过程中确实遇到过的典型故障,按概率从高到低排序:
| 异常表现 | 根本原因 | 解决方案 |
|---|---|---|
| 数码管无显示 | 引脚被特殊功能占用 | Bios/Device Options 中释放对应引脚 |
| 显示值一直为 0 | 被测信号未整形,边沿不够陡 | 输入端加 74HC14 施密特整形 |
| 显示值不停翻转 | 闸门控制逻辑亚稳态 | 上升沿检测前加两级触发器同步 |
| 高频测量偏大 | 计数器用错时钟域,混入抖动 | 检查 sig_cnt 计数时钟是否直接引用被测信号 |
| 低频测量无刷新 | 预置闸门设置太短 | 将 gate_pre 延长到 10 个信号周期以上 |
| 编译时序违规 | 64 位除法组合路径太长 | 改用 LPM_DIVIDE 流水线模式 |
5.2 实测中的细节体会
第一个体会是:等精度频率计的精髓不在“计数”而在“同步”。很多人把代码写完,仿真也通过,一上板就发现低频段显示乱跳,其实问题十有八九出在闸门同步逻辑——被测信号边沿没抓准,亚稳态进入计数闸门,导致实际闸门宽度不再严格等于整数个被测周期。所以调试时优先抓内部信号,用 Quartus 内置的 SignalTap 逻辑分析仪观察 gate_pre、gate_actual、sig_cnt 这几个关键节点的波形,一眼就能看出问题。
第二个体会是:标准时钟的纯度决定了测量精度上限。我用的是开发板自带的 50MHz 有源晶振,实测频率和标称值差了大约 30ppm,换算成 30MHz 输入就是几百赫兹的绝对误差。如果项目要求高精度,标准时钟必须换成高精度温补晶振,并在代码里把实际频率值写进计算常量,而不是直接写 50000000。我是用一个校准模式,让频率计自测一个已知的 10MHz 基准信号,反推出 fs 的实际值,再写回参数,这样可以把晶振误差也标定掉。
第三个体会是工程层面的:仿真通过只是第一步,上板调试的核心工具是 SignalTap 和示波器配合。SignalTap 采样深度不要设太大,影响编译时间,关键是触发条件设置好。我把触发条件设成 latch_flag 上升沿,然后观察 captured 的那一拍数据,比对着计算一遍就能确认逻辑有没有问题。
这个项目做完之后,后续可以扩展的方向不少。比如加上量程自动切换,把输入信号先经过高速比较器和分频网络,再结合继电器或模拟开关选路,频段从 1Hz 覆盖到 100MHz;也可以加个 UART 模块把频率值传到上位机画曲线,这样就成了一个简易的频谱监测前端了。对想入门 FPGA 的人来说,这个项目比流水灯有深度,比纯理论有手感,值得踏实走一遍。
本文还有配套的精品资源,点击获取