news 2026/9/20 2:51:40

浮点频率计:等精度测频、Verilog实现与STM32小数位校准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
浮点频率计:等精度测频、Verilog实现与STM32小数位校准

简介:面向电子技术、数字电路课程设计场景的浮点频率计设计文档,适合电子信息类专业学生、实验课教师及刚接触数字系统设计的爱好者参考。内容围绕量程达1MHz的浮点式数字频率计展开,依次覆盖技术指标与任务分析、系统框图、秒脉冲电路、节拍控制电路以及分频与计数显示电路,并给出74LS290、74LS160、74LS1153等器件的具体接法与置数逻辑,同时附上Multisim 12仿真波形和面包板硬件调试结果,可作为课程设计报告撰写与电路搭建的完整参照。压缩包内仅1个docx文档,约1.49MB,图文并茂地呈现原理图、控制时序与仿真截图,便于直接阅读与引用。目前已有211人学习下载,适合需要快速理清时基、分频、计数与启停控制设计思路,并希望对照实例完成自身方案验证的读者。

1. 浮点频率计最先要解决的,是小数位敢不敢信

很多人做频率计卡住的地方不是数不对,而是小数位不敢信。一个 10 MHz 的信号,整数计数在 1 秒闸门里数出 10000000,看着挺准;换成 1 kHz 的信号,1 秒闸门才数到 1000 个脉冲,分辨率只有 1 Hz,第 4 位往后全靠猜。浮点频率计要解决的就是这件事:让读数带上可信的小数位,而且换量程不用换算法。它适合做电子设计类课程与竞赛题的人——南邮电子设计这类题目里频率计几乎是常客——也适合需要在嵌入式平台上现场量频率的工程师,以及被"高频准、低频虚"折磨过的调试者。核心手段是等精度(多周期同步)测量,把 ±1 误差锁死在标准时钟那一侧。

2. 等精度测频原理与方案选型:闸门、误差和主控怎么定

动手写代码之前,先要把误差从哪来算清楚。电子设计里做频率计,最常见的翻车不是代码写错,而是选错了测量方法:用一个固定闸门去数脉冲,高频段看着很好,一测几十赫兹的音频信号就开始跳。选型的依据不是"哪种更高级",而是"我需要的分辨率对应的误差项,落在哪一侧"。

2.1 直接测频、测周法与等精度法,误差差在哪

方法实现方式计算公式主要误差适用频段
直接测频固定闸门 T 内数被测脉冲fx = Nx / T±1 个被测脉冲,相对误差 1/Nx中高频
测周法数被测信号一个周期内的标准时钟fx = fs / M±1 个标准时钟,相对误差 1/M低频
等精度闸门两端对齐被测信号边沿,同时数 Nx 与 Nsfx = Nx · fs / Ns±1 个标准时钟,相对误差 1/Ns全频段

关键在于:等精度法把 ±1 量化误差从"被测信号侧"挪到了"标准时钟侧"。Ns = fs · T,只跟闸门时间和标准频率有关,跟被测信号频率无关。所以 1 Hz 的信号和 100 MHz 的信号,只要闸门时间一样,相对分辨率就是同一个数量级。完整误差写成

δ = ±( 1/Ns + Δfs/fs )

其中 1/Ns 是量化误差,Δfs/fs 是晶振本身的频率准确度与温漂。前者靠加长闸门解决,后者只能靠校准扣掉。

2.2 从目标分辨率倒推闸门时间与标准时钟频率

不要先定闸门再去算精度,顺序反过来:先说清楚"我要在 1 MHz 上分辨到 0.1 Hz",再倒推需要多长的闸门。下面这段脚本可以直接改参数试算,用来选闸门和标准时钟,省得在板子上反复改 Verilog 参数。

# 等精度测频参数试算:给定标准时钟与闸门,算相对分辨率和某频点的绝对分辨率 def design(fs_hz: float, gate_s: float, fx_hz: float = None): ns = round(fs_hz * gate_s) # 闸门内标准时钟计数 Ns rel = 1.0 / ns # 仅考虑量化误差的相对分辨率 out = [f"fs={fs_hz/1e6:.1f}MHz gate={gate_s*1e3:.0f}ms Ns={ns} rel={rel:.2e}"] if fx_hz is not None: out.append(f" 在 {fx_hz/1e6:.3f}MHz 处绝对分辨率 = {fx_hz*rel:.4f} Hz") return "\n".join(out) for g in (0.01, 0.1, 1.0): print(design(50e6, g, 1e6)) # 50MHz 标准时钟,分别用 10ms / 100ms / 1s 闸门

跑出来的结果很直观:50 MHz 标准时钟配 10 ms 闸门,Ns = 500000,相对分辨率 2×10⁻⁶,在 1 MHz 上的绝对分辨率是 2 Hz,小数点后一位都是虚的;换成 1 s 闸门,Ns = 5×10⁷,相对分辨率 1e-8,同频点绝对分辨率 0.01 Hz,小数点后两位才有意义。所以闸门时间本质上是在"刷新率"和"小数位"之间做交易:1 s 闸门意味着每秒才更新一次读数,做实时扫频显示就会很卡。

标准时钟的来源也有讲究。板载有源晶振(50 MHz、100 MHz 都常见)比 MCU 内部 RC 好一个量级,内部 RC 的温漂动辄 ±1%,小数位再准也没意义。晶振误差是系统误差,可以靠一次校准折算掉,所以选型时更该关注的是它的短期抖动,而不是标称精度那一行字。

2.3 主控与前端选型:FPGA 计数还是 STM32 一把梭

常见的两条路:

  • FPGA + MCU:FPGA 负责被测信号同步、闸门生成和两组计数器,MCU 只做浮点运算和外设显示。好处是高频段能真正跑到几十上百 MHz,闸门两端都能对齐到被测信号边沿。缺点是板子复杂、要写状态机。
  • 单 STM32 方案:一个定时器配成外部计数模式(TIMx_ETR)数被测信号,另一个定时器做闸门定时,中断里读计数值。成本低、代码短,但 ETR 引脚的输入带宽有限(几十 MHz 量级),而且软件闸门没法精确对齐被测信号边沿,等于在 Nx 上又加回一个 ±1 误差。做几十 kHz 到几 MHz 的场合够用,做高频就不合适。
  • 专用计数器芯片:外围简单,但量程和接口灵活性差,现在课程设计里基本没人用了。

前端整形同样别省。比较器加正反馈迟滞(LM311、TLV3501 一类),把正弦、三角、方波统一整形为干净的 CMOS 电平。迟滞量太小,小幅值信号在零点附近反复翻转,读数会莫名其妙翻倍;迟滞量太大,小信号直接测不到。高频段还可以先做前置分频,但要注意分频会引入 ±1 个输入周期的误差,分频比越大这个误差折算到频率上越明显。

3. 用 Verilog 把等精度测频的闸门与计数逻辑跑通

原理清楚之后,落到 RTL 上的核心只有三件事:把被测信号同步到标准时钟域、让闸门的开与关都对齐到被测信号边沿、把两组计数值在关闸门那一刻原子地锁存下来。

3.1 模块端口与时序约定

端口方向位宽含义
clk输入1标准时钟 fs,直接来自板载晶振
rst_n输入1异步复位,低有效
sig_in输入1整形后的被测信号
start输入1单次启动脉冲,高电平一个 clk 周期即可
nx_cnt输出48闸门内被测信号整周期数 Nx
ns_cnt输出48闸门内标准时钟周期数 Ns
done输出1数据锁存完成,给 MCU 的中断源

时序约定里最重要的一条:clk 和 sig_in 是异步的,sig_in 绝不能直接进组合逻辑参与判断,必须先打两到三拍同步。

3.2 开闸门与关闸门为什么都要等被测信号边沿

这是等精度法唯一的"魔法"所在。闸门如果按标准时钟的节拍开、按标准时钟的节拍关,那么闸门边界上必然切到被测信号的不完整周期,Nx 就会带上 ±1 个被测周期的误差,低频时这个误差占比很大。把开、关两端都推迟到被测信号的下一个上升沿执行,闸门内就是整数个被测周期,Nx 本身没有量化误差,误差全部集中到 Ns 的 ±1 上。代价是闸门时间会比预设值多出最多一个被测周期,做低频测量时这个多出来的时间不能忽略——1 Hz 信号下就是 1 秒。

3.3 可上板的 RTL:同步、计数、锁存一次写完

module epm_counter #( parameter integer FS_HZ = 50_000_000, // 标准时钟频率,必须和实际晶振一致 parameter integer GATE_MS = 100, // 预置闸门时间,单位 ms parameter integer CNT_WIDTH = 48 // 计数器位宽,按 fs_max * T_max * 2 估算 )( input wire clk, input wire rst_n, input wire sig_in, input wire start, output reg [CNT_WIDTH-1:0] nx_cnt, output reg [CNT_WIDTH-1:0] ns_cnt, output reg done ); localparam integer GATE_CYCLES = FS_HZ / 1000 * GATE_MS; // 预置闸门对应的 clk 周期数 // ---- 被测信号三级同步,抑制亚稳态 ---- reg [2:0] sig_sync; always @(posedge clk or negedge rst_n) if (!rst_n) sig_sync <= 3'b000; else sig_sync <= {sig_sync[1:0], sig_in}; wire sig_rise = sig_sync[1] & ~sig_sync[2]; // 上升沿,已同步到 clk 域 // ---- 闸门状态机 ---- localparam [1:0] S_IDLE = 2'd0, S_WAIT = 2'd1, S_MEAS = 2'd2, S_DONE = 2'd3; reg [1:0] state; reg gate_open; // 闸门实际打开标志 reg close_req; // 计时到点,请求关闸门 reg [31:0] gate_timer; reg [CNT_WIDTH-1:0] nx_acc, ns_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; gate_open <= 1'b0; close_req <= 1'b0; gate_timer <= 32'd0; nx_acc <= {CNT_WIDTH{1'b0}}; ns_acc <= {CNT_WIDTH{1'b0}}; nx_cnt <= {CNT_WIDTH{1'b0}}; ns_cnt <= {CNT_WIDTH{1'b0}}; done <= 1'b0; end else begin done <= 1'b0; case (state) S_IDLE: if (start) begin // 收到启动,清零后进入等待 nx_acc <= {CNT_WIDTH{1'b0}}; ns_acc <= {CNT_WIDTH{1'b0}}; gate_timer <= 32'd0; close_req <= 1'b0; state <= S_WAIT; end S_WAIT: if (sig_rise) begin // 对齐到被测信号上升沿再开闸门 gate_open <= 1'b1; state <= S_MEAS; end S_MEAS: if (gate_open) begin ns_acc <= ns_acc + 1'b1; // 标准时钟计数,误差来源 gate_timer <= gate_timer + 1'b1; if (gate_timer >= GATE_CYCLES - 1) close_req <= 1'b1; // 预置时间到,等下一个边沿 if (sig_rise) begin nx_acc <= nx_acc + 1'b1; // 只数完整周期 if (close_req) begin // 关闸门同样对齐到边沿 gate_open <= 1'b0; nx_cnt <= nx_acc + 1'b1; ns_cnt <= ns_acc; state <= S_DONE; end end end S_DONE: begin done <= 1'b1; // 单周期完成脉冲给 MCU state <= S_IDLE; end endcase end end endmodule

逻辑上要注意三处。第一,nx_acc在开闸门那个边沿不计数、在关闸门那个边沿计数,所以nx_cnt赋的是nx_acc + 1,表示闸门内包含的完整周期数为 N。第二,ns_cnt锁存的是关闭前一刻的累加值,与真实时间存在最多一个 clk 周期的偏差,这个 ±1 正是误差公式里的 1/Ns,属于正常损耗,不用去"修"。第三,整个模块是单次触发式,测完回 IDLE,MCU 读到 done 之后重新给 start,测量节拍由软件控制,方便做多次平均。

3.4 参数取值与上板验证清单

参数建议取值说明
FS_HZ50_000_000 或 100_000_000必须与板上晶振实际频率一致,写错等于整体缩放
GATE_MS10 / 100 / 1000 三档低频用长闸门,高频用短闸门,可在 MCU 侧动态下发
CNT_WIDTH4848 位可覆盖 100 MHz × 数小时不溢出,综合资源也还能接受
GATE_CYCLESFS_HZ/1000*GATE_MS注意先用整数除再乘,避免中间结果溢出 32 位

上板前先在仿真里给一个 1 MHz 的 sig_in 和 50 MHz 的 clk,跑 100 ms 闸门,期望 Nx 约等于 100000、Ns 约等于 5000000,两者比值乘以 fs 约等于 1 MHz。仿真对得上再上板,上板后用信号源给 1 kHz,读数应该在 1000.xx 附近而不是 1000 或 999——如果是整数跳变,八成是 MCU 侧用了整数除法。

4. 从 Nx/Ns 到带小数的频率读数:浮点运算与格式化

FPGA 给过来的永远是两个整数,浮点频率计真正"浮"起来是在 MCU 这一侧完成的。这一步做错,前面 RTL 写得再漂亮,小数位也是假的。

4.1 fx = Nx·fs/Ns 的代码写法与精度边界

#include <stdint.h> #define FS_HZ 50000000.0 /* 标准时钟,必须与 Verilog 参数 FS_HZ 完全一致 */ /* 由 FPGA 锁存的两个计数值计算频率,返回单位 Hz */ double freq_from_counts(uint64_t nx, uint64_t ns) { if (ns == 0) return 0.0; /* 未开闸门或硬件异常,防除零 */ if (nx == 0) return 0.0; /* 被测信号幅度不够,前端没整形出边沿 */ /* 先除后乘:nx 与 ns 都是 48 位以内,转 double 后相对精度约 1e-16, 远小于 1/Ns 的量化误差,顺序不影响结果但能避免中间乘积溢出 */ return (double)nx / (double)ns * FS_HZ; }

为什么"先乘后除"在这个场景里要谨慎:nx 大、fs 大时,nx × 50e6 很容易冲到 10¹⁷ 量级,超出 double 的 53 位有效位(约 9×10¹⁵)就开始丢低位。写成先除后乘,两次运算各自都在 1e-16 相对误差内,和 Ns 的 ±1 量化误差(1e-7 量级)比起来完全不是瓶颈。真正的精度瓶颈永远在闸门和晶振上,不在浮点运算上,别把精力放错地方。

4.2 软件浮点、硬件 FPU 与定点输出的取舍

方案典型平台优点代价
软件浮点 doubleCortex-M0/M3 无 FPU精度足够,代码零改动一次运算几十到上百周期,显示刷新时略慢
硬件单精度 floatCortex-M4F只有 24 位有效位,48 位计数值直接转 float 会丢低位
硬件双精度 doubleCortex-M7 / 带双精度 FPU快且精度够芯片成本高,部分库仍需软件模拟
定点整数输出任意 MCU不链接浮点库,代码小量程跨度大时定标麻烦,单位换算要自己拼

选型上有个容易踩的坑:STM32F4 的 FPU 默认配置-mfpu=fpv4-sp-d16只支持单精度,代码里写 double 照样走软件模拟。如果对刷新率不敏感,就让 double 走软件模拟,精度更稳;如果确实要快,得把这些计数值先做定点缩放再转 float,别直接把 48 位整数塞进 float。

4.3 显示格式化:小数位由分辨率决定,不是想写几位写几位

#include <stdio.h> #include <math.h> /* 按分辨率决定显示位数:ns 决定相对分辨率,位数超过它就是假精度 */ void format_freq(char *buf, size_t len, double fx, uint64_t ns) { int digits = (int)floor(log10((double)ns)) - 1; /* 相对分辨率 1/Ns,留一位余量 */ if (digits < 0) digits = 0; if (digits > 6) digits = 6; /* 屏幕和库都不适合无限位数 */ if (fx >= 1e6) snprintf(buf, len, "%.*f MHz", digits, fx / 1e6); else if (fx >= 1e3) snprintf(buf, len, "%.*f kHz", digits, fx / 1e3); else snprintf(buf, len, "%.*f Hz", digits, fx); }

以 Ns = 5×10⁶ 为例,log10 约为 6.7,digits 取 5,显示到小数点后 5 位;换成 1 s 闸门的 Ns = 5×10⁷,digits 取 6,再多就是噪声了。这样做的好处是闸门时间切换时,显示位数自动跟着变,不会出现"闸门缩短了但小数位还挂着 6 位"的假精度。

注意在 STM32 上用 newlib-nano 时,%f默认被裁掉,snprintf会原样输出 "f" 或空字符串。需要在链接选项里加-u _printf_float,或者改用整数部分 + '.' + 小数部分手工拼字符串,后者能省下十几 KB 的 Flash,做小容量型号时更划算。

4.4 三个常翻车的类型与单位细节

第一,整数除法。uint64_t f = nx / ns * 50000000;nx/ns先算成 0,结果永远是 0。所有和频率相关的除法,两端必须至少有一个是浮点类型。

第二,计数位宽与打印格式不匹配。48 位计数在 MCU 里用uint64_t存,打印时用%llu,用%u会高位截断,读数会周期性跳变,这种 bug 在低频长闸门时才暴露,很难查。

第三,单位换算的常量写成整数。fx / 1000是整数除法,fx / 1e3才是浮点除法。一个字符的差别,一个量级的错误。

5. 校准与排错:让浮点频率计的小数位经得起复测

读数能出来只是及格线,能复测、能换板一致才算过关。等精度法本身消除了 Nx 的量化误差,剩下的误差主要来自晶振准确度和前端整形质量,这两项都是可以量化处理掉的。

5.1 单点校准:把晶振的系统误差扣掉

用一个已知准确度的信号源(或者一块标定过的有源晶振板)输入 10.000000 MHz,读回实测值,算修正系数存进 Flash,每次运算后乘一遍。

#define CAL_ADDR 0x0801F000UL /* 末页存校准系数,注意别和程序区重叠 */ typedef struct { double k; } cal_t; /* k = 真实频率 / 实测频率 */ double apply_cal(double fx_raw) { cal_t *c = (cal_t *)CAL_ADDR; /* 上电时已校验过标志位,这里直接用 */ if (c->k < 0.9 || c->k > 1.1) return fx_raw; /* 系数明显不合理,按未校准处理 */ return fx_raw * c->k; } /* 校准流程:测 10MHz 标准源得到 f_meas,则 */ /* k = 10000000.0 / f_meas; 再擦写 Flash 写入 */

逻辑说明:k 是乘性系统误差,标定一次就能把晶振的固定偏差压到 1e-8 以内,比换更贵的晶振划算得多。参数上要留合理性检查(0.9~1.1),防止 Flash 空白值被当成有效系数用,导致读数整体放大几十倍。注意 Flash 末页写入前要先擦除,并且把系数和它的反码一起存,上电做一次校验,掉电写坏也能识别出来。

5.2 现象对照表:读数不动、乱跳、高频偏小分别查哪里

现象优先怀疑检查点
读数恒为 0前端没出边沿示波器直接量比较器输出,看有没有方波
读数约为真实值两倍比较器无迟滞,过零点多次翻转加正反馈电阻,迟滞取信号幅值的 5%~10%
低频跳变大于 1 个字闸门太短,1/Ns 太大换长闸门,或确认 MCU 侧没有整数除法
高频段读数偏小前端带宽不足或分频器失效查比较器压摆率、走线,核对分频比
同一信号复测每次差固定值闸门边界没对齐信号边沿回看 RTL 里 S_WAIT、close_req 两处判断
上电初期正常,跑久了漂移晶振温漂或电源纹波换有源晶振,检查比较器供电去耦

5.3 平均、自适应闸门与刷新率的边界

连续测 N 次取算术平均能压低随机抖动,等效把分辨率提升约 √N 倍,代价是刷新率除以 N。8 次平均配 100 ms 闸门,刷新率还有 1.25 次/秒,屏幕看着不卡,分辨率相当于 800 ms 闸门,这是性价比比较高的组合。但平均消不掉系统误差,晶振偏了,平均多少次都还是偏,必须先校准再平均。

自适应闸门是另一个常用技巧:先测一次得到大致频率,再按"闸门内至少有 1000 个被测周期"反推该用多长的闸门,1 Hz 信号自动切到 1 s 甚至 10 s 档,1 MHz 信号切回 10 ms 档。判断依据直接用上一次的读数和当前 Nx,如果 Nx 小于 1000,下次换更长的闸门;大于 1e7,换更短的。上下限都要卡住(比如闸门限制在 10 ms 到 10 s 之间),否则遇到无信号的情况会一路加长到无限等待。

最后一句留给动手顺序:先拿一个已知频率源把修正系数标定出来写进 Flash,再去调小数位和显示位数;反过来做,小数位越多,错得越好看。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 2:49:38

基于Simulink搭建直流电网:建模、下垂控制与报告自动生成

简介&#xff1a;该资源是一份基于Matlab/Simulink搭建直流电网的课程报告PPT&#xff0c;面向电气工程、电力电子及高压直流输电方向的学生与研究者&#xff0c;适合作为课程设计、实验报告或答辩展示的参考模板。内容围绕直流配电网结构展开&#xff0c;包含与无穷大电源相连…

作者头像 李华
网站建设 2026/9/20 2:47:11

扩谱时钟SSC配置实战:EMC辐射超标的关键破局点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 2:45:48

BIM施工安全管理:规则化危险源识别与4D闭环

简介&#xff1a;这份文档资料面向建筑施工现场安全管理人员、工程管理专业学生及论文写作者&#xff0c;围绕BIM技术在施工安全管理中的应用展开系统论述。包内为1个doc格式文件&#xff0c;约20KB&#xff0c;正文含摘要、关键词与分级章节&#xff0c;条理清晰便于直接引用与…

作者头像 李华
网站建设 2026/9/20 2:45:38

工业解决方案的本质:从工具到生命体的跃迁

工业解决方案这几年一直是我关注的重点&#xff0c;但说实话&#xff0c;真正让我觉得行业要变天的&#xff0c;不是某台设备多智能&#xff0c;也不是哪套软件又多了个模块&#xff0c;而是越来越多项目开始呈现出一种此前从未有过的“活”的状态。过去我们谈工业解决方案&…

作者头像 李华
网站建设 2026/9/20 2:43:25

ComfyUI桌面版从安装到跑通文生图:完整避坑指南

作为一名常年在 AI 绘画工具里来回折腾的老玩家&#xff0c;我必须得说&#xff0c;ComfyUI 官方出的桌面版&#xff08;Comfy Desktop&#xff09;确实是今年最值得关注的变化之一。以前我们装 ComfyUI&#xff0c;要么搞秋叶整合包&#xff0c;要么手动扒 GitHub 源码配环境&…

作者头像 李华
网站建设 2026/9/20 2:41:30

大模型时代API调试新选择:GetCat系统原生渲染,替代Postman的实战体验

说说我最近换掉 Postman 的那点事。手头项目开始接入大模型相关的接口以后&#xff0c;原来的 API 调试工作流明显吃紧&#xff0c;正赶上看到 GetCat 的更新日志——主打系统原生界面渲染、定位大模型时代的 Postman 替代品&#xff0c;就顺手装来试了一个月。今天这篇就聊聊它…

作者头像 李华