ARTICLE DETAIL

资讯详情

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

基于FPGA的等精度测频法实现低频方波频率与占空比测量

基于FPGA的等精度测频法实现低频方波频率与占空比测量 做低频方波测量很多人第一反应是拿单片机定时器数一数就行了。但真要同时把频率和占空比都测准还要应对不同频率范围、边沿抖动、实时输出这些需求单片机方案往往会在精度和灵活度上卡脖子。这篇笔记就是记录我用FPGA做低频方波频率与占空比测量的完整过程从选型思路、测频原理、RTL实现到仿真调试和板级验证都走了一遍。内容主要面向正在入门FPGA、或者要自己搭一个信号测量模块的工程师原理和代码都给了可以直接照着复现。FPGA做这类测量的核心优势不是它算得快而是它的逻辑并行能力和时间确定性。单片机测频率一般靠定时器捕获测量过程会受中断响应、指令周期、定时器位数这些因素干扰FPGA用硬件逻辑直接对信号边沿和参考时钟计数所有模块同时跑不受软件执行顺序影响测量窗口想定多少拍就定多少拍这在低频段尤其重要。低频信号周期长测量窗口稍微抖动一下误差就会被放大。我当时做的项目要求输入信号频率范围在1Hz到10kHz占空比从10%到90%都能测误差要控制在0.1%以内。这个指标用普通单片机做比较吃力但用FPGA加一个100MHz的参考时钟源等精度测频法很容易就能达到甚至有余量。接下来我把整套方案的原理和实现细节拆开讲。1. 方案选型与整体设计思路1.1 为什么优先选FPGA而不是单片机或专用芯片测频率占空比市面上也有专用的频率计芯片比如老的74系列计数器搭方案或者用单片机定时器。但在这个项目里我评估了三个方案后发现FPGA是综合成本最低、扩展性最强的选择。单片机方案的问题在于频率测量精度取决于定时器时钟源和捕获方式。大多数ARM内核的定时器能轻松捕获高频信号但对低频信号测一个周期要等很久而且占空比测量需要同时捕获上升沿和下降沿再计算两个边沿的时间差。这个逻辑本身不难难在过程中如果有中断占用、DMA配置、时钟分频误差计数值就会带抖动。低频方波一个周期可能几毫秒甚至几秒中断稍微晚来一点测出来的占空比偏差就是几个百分点。专用频率计芯片的问题则是灵活性差。芯片测量范围固定调节闸门时间、输出格式都要靠外部寄存器配置想加个校准逻辑、想输出特定协议的数据帧都得看芯片手册脸色。后面如果需求要从方波扩展到PWM、测周期、测脉宽芯片方案几乎要重新选型。FPGA方案正好避开这两个痛点计数逻辑用硬件描述语言写死不受软件干扰功能扩展就是加模块的事今天测频率占空比明天想测两路相位差、想做数字滤波都能在同一个工程里加。一块入门级开发板两百块左右对于学习和原型验证来说性价比很高。1.2 测量方案的整体框架整个测量系统我拆成了四个部分输入调理、边沿检测与同步、计数与锁存、计算与输出。输入调理负责把外部方波信号整形成FPGA引脚能直接接受的电平。如果输入信号是5V或12V的电路输出直接用3.3V的FPGA引脚去接轻则测不准重则烧IO。我在板子上加了一级比较器整形电路把任意幅度的方波统一整形成3.3V逻辑电平同时还能滤掉一部分噪声毛刺。边沿检测与同步是FPGA内部的核心前置逻辑。外部信号进入FPGA后先经过两级寄存器同步消除亚稳态然后通过组合逻辑判断出上升沿和下降沿脉冲。这一步是整个测量的基础边沿判断错了后面数出来的频率和占空比全是错的。计数与锁存模块负责在测量窗口内分别对被测信号边沿和参考时钟计数。窗口信号由逻辑自己控制保证被测信号的整周期对齐。窗口结束时把计数值锁存到寄存器里再给后续模块一个数据有效标志。计算与输出模块把锁存的计数值换算成频率值和占空比百分比。FPGA内部可以做定点除法也可以直接把原始计数值送到上位机或者单片机去算。我做的是把结果换算成BCD码驱动数码管显示同时通过UART发到电脑方便实时观测。1.3 频率测量方法的选型直接计数、周期测量、等精度测量频率测量常见的三种方法直接测频法、周期法、等精度测频法对应不同的频率范围。直接测频法就是在固定闸门时间内数被测信号的上升沿个数频率等于计数值除以闸门时间。在闸门时间定为1秒时测量1Hz信号计数值是1误差可能达到百分之几十测量1MHz信号计数值是100万误差就很小。所以直接测频法适合高频段低频段精度很差。周期法是反过来测量被测信号一个周期内包含多少个参考时钟周期频率等于参考时钟频率除以计数值。这个方法测低频信号效果很好因为低频信号一个周期长能数到很多个参考时钟周期但测高频信号时一个周期内可能只数到几十个参考周期误差迅速变大。等精度测频法把两者的优点结合起来用一个由被测信号边沿同步控制的闸门在闸门时间内同时数被测信号的周期数和参考时钟周期数。因为闸门开启和关闭都在被测信号的上升沿上所以被测信号的计数值没有误差误差只取决于参考时钟的±1计数。这个方法在全频段都能保持较高的相对精度尤其适合低频信号。我用表格对比一下三种方法的误差特性测量方法低频精度高频精度实现复杂度适合场景直接测频法差好低高频信号周期法好差低低频信号等精度法好好中宽频段通用这个项目要求1Hz到10kHz且精度统一等精度法是不二选择。后面频率测量的代码就是基于这个方法写的。2. 等精度测频原理与核心实现2.1 等精度测频的数学原理等精度测频法的核心思想是让闸门时间T_gate不是固定值而是被测信号整数个周期的长度。假设在闸门时间内被测信号一共出现了N_sig个上升沿参考时钟一共出现了N_clk个上升沿参考时钟频率为F_clk那么被测信号的频率F_sig可以表示为F_sig N_sig x F_clk / N_clk为什么这个式子误差小因为N_sig是整数个完整周期它没有计数量化误差而N_clk虽然可能存在±1的计数误差但在低频段N_clk数值很大比如1Hz信号测量窗口长达1秒100MHz参考时钟能数到1亿个周期±1的误差对结果的影响只有千万分之一级别。即便被测信号是10kHz窗口取100个周期即10毫秒N_clk也有100万个周期±1误差带来的相对误差依然很小。从公式也能看出这个方法的本质是把被测信号的频率测量转化为对参考时钟的计数测量参考时钟越准结果越准。所以FPGA板上的晶振一定要选温漂小的至少要30ppm以内有条件可以采用恒温晶振或者GPS驯服时钟但对这个项目来说普通有源晶振足够。2.2 输入同步与边沿检测模块外部信号进入FPGA引脚后不能直接用组合逻辑去判断边沿必须先经过同步。原因很简单外部信号与FPGA内部时钟没有任何相位关系直接用内部时钟采样一个异步信号会不满足寄存器的建立保持时间输出进入亚稳态导致采样结果不一定稳定。我设计了一个标准的边沿检测模块两级同步再加一级打拍。两级同步的作用是让亚稳态在两个寄存器之间收敛稳定虽然不能完全消除亚稳态但能把发生概率降到极低。核心代码如下module edge_detector ( input wire clk, // 100MHz参考时钟 input wire rst_n, // 异步复位 input wire din, // 输入方波信号 output reg pos_edge, // 上升沿脉冲 output reg neg_edge // 下降沿脉冲 ); reg sync1, sync2, sync_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync1 1b0; sync2 1b0; sync_d 1b0; end else begin sync1 din; sync2 sync1; sync_d sync2; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin pos_edge 1b0; neg_edge 1b0; end else begin pos_edge sync2 ~sync_d; // 当前为高且前一拍为低 neg_edge ~sync2 sync_d; // 当前为低且前一拍为高 end end endmodule这里有一个容易被忽略的细节pos_edge和neg_edge不是组合逻辑输出而是寄存器输出。这样设计的好处是后续模块用这两个信号做使能或计数时不会引入组合逻辑的毛刺。边沿脉冲宽度正好是一个参考时钟周期在100MHz时钟下是10ns用来触发计数和锁存非常干净。注意边沿检测和后续计数逻辑共用同一个时钟域也就是100MHz参考时钟所以整个测量系统都在同一个时钟域内工作时序约束相对简单。实际项目中我建议所有涉及测量精度和实时性的逻辑都跑在同一个高速时钟下千万不要在内部再用一个低频率派生时钟去采外部信号那样会重新引入异步问题。2.3 等精度测频的RTL实现等精度测频的核心逻辑分成三块闸门控制、参考时钟计数、被测信号边沿计数。闸门控制的规则是检测到被测信号上升沿时如果当前闸门未开就拉高gate信号开始计数当闸门打开后计数器先数一个预设的基准周期数比如1000个被测信号周期到达后不再立即关闸门而是等到下一个被测信号上升沿到来时再拉低gate保证闸门始终对齐被测信号的完整周期。闸门打开的时间是不固定的取决于被测信号频率但被测信号计数值始终是预设值。等待下一个上升沿关闭闸门这个设计是等精度测频的灵魂。如果直接按时间关闸门被测信号可能在闸门中间被切断N_sig就会出现±1的误差整个方法的精度优势就没了。参考时钟计数比较简单闸门为高时每个时钟周期加1。被测信号边沿计数不仅数上升沿还需要数下降沿因为后面占空比测量要用的。所以我在测频模块里同时维护两个计数器一个是参考时钟计数器cnt_ref一个是方波上升沿计数器cnt_sig还有一个是后续占空比用到的边沿计数状态机。核心计数代码如下module freq_counter ( input wire clk, input wire rst_n, input wire pos_edge, // 上升沿脉冲 input wire neg_edge, // 下降沿脉冲 output reg [31:0] cnt_ref, // 参考时钟计数 output reg [31:0] cnt_sig, // 上升沿计数 output reg data_valid, // 数据有效标志 output reg gate_state // 闸门状态 ); localparam TARGET_CNT 32d1000; // 目标测量周期数 reg [31:0] cycle_cnt; // 已测信号周期计数 reg gate_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin gate_en 1b0; cnt_ref 32d0; cnt_sig 32d0; cycle_cnt 32d0; data_valid 1b0; gate_state 1b0; end else begin data_valid 1b0; if (!gate_en) begin if (pos_edge) begin gate_en 1b1; cnt_ref 32d0; cnt_sig 32d0; cycle_cnt 32d0; end end else begin if (pos_edge) begin if (cycle_cnt TARGET_CNT) begin gate_en 1b0; data_valid 1b1; end else begin cycle_cnt cycle_cnt 1b1; end end if (pos_edge) cnt_sig cnt_sig 1b1; cnt_ref cnt_ref 1b1; end gate_state gate_en; end end endmodule这段代码里有个关键细节gate_en拉高时第一个上升沿已经被消耗在开门动作上所以cnt_sig从0开始但第一个边沿实际上也属于测量窗口内的信号。为了保持计数一致性我把cnt_sig的递增放在pos_edge条件下这样第一次开门后第一个上升沿就被计为第1个周期后续每个边沿递增测量窗口结束时cnt_sig正好等于预设的目标周期数TARGET_CNT。这样N_sig是准确的不会出现多一个少一个的情况。cnt_ref的计数逻辑是只要gate_en为高每个时钟周期都加1。这里我用了一个很朴素的做法在同一个always块里同时检测pos_edge和时钟上升沿因此gate_en拉高后的第一个clk上升沿cnt_ref也会加1。由于cnt_ref在开门时被清零闸门持续期间总共能计到多少个参考时钟完全取决于实际时间长度这个计数值就是等精度公式里的N_clk。2.4 测量窗口与误差分析在这个实现里测量窗口大约是被测信号的1000个周期。对于1Hz信号测一次需要1000秒显然太慢了。所以我对TARGET_CNT做成了可配置参数低频时取小一些比如10个周期高频时取大一些比如1000个周期这样能在实时性和精度之间平衡。具体怎么配置我按这个原则保证测量一次的时间不超过1秒同时N_clk尽量大。如果信号是1Hz取10个周期窗口10秒还是太久这时候最好是只测一个周期也就是TARGET_CNT取1。低频信号本身周期长一个周期内能数到的参考时钟已经很多了精度不会差。如果信号是10kHz取1000个周期只要0.1秒N_clk大概是100万个误差很小。误差主要来自两个方面。一是参考时钟的晶振频率偏差这个直接线性影响测量结果所以选晶振要谨慎。二是cnt_ref的±1计数误差在低频和高频下都极小。实测下来用100MHz参考时钟测量1kHz方波连续测10次结果都在999.98Hz到1000.02Hz之间跳动这个精度对于绝大多数应用足够了。3. 占空比测量的设计与实现3.1 占空比测量的两种思路占空比的定义很简单高电平时间占整个周期的百分比。在FPGA里采样占空比常见有周期捕获法和高频计数法两种。周期捕获法是用一个高速时钟连续记录相邻上升沿和下降沿的时间戳高电平时间就是上升沿到下降沿的时间戳差整个周期就是相邻两个上升沿的时间戳差相除就是占空比。这个方法比较灵活但需要维护时间戳队列逻辑复杂度高一些。高频计数法更直接用一个高速时钟对高电平期间计数得到cnt_high对低电平期间计数得到cnt_low。占空比就等于cnt_high除以cnt_high加cnt_low。这个方法的优点是逻辑简单计数过程不涉及复杂的边沿状态管理适合低频信号因为低频信号每个电平持续的时间都足够长能数到大量参考时钟周期。我选的是高频计数法原因是它和等精度测频模块天然兼容。前文提到的pos_edge和neg_edge边沿脉冲已经提取出来了计数只需要在输入信号为高时累加cnt_high为低时累加cnt_low测量窗口结束时锁存即可。3.2 高电平和低电平分别计数高频计数法的实现比测频还简单。关键在于采样的同步计数用参考时钟的上升沿作为采样时刻每个时钟周期判断一次外部输入信号当前是高还是低。因为输入信号已经过边沿检测模块同步所以这里判断的信号就是sync2寄存器输出它是和参考时钟同步的稳定信号。module duty_counter ( input wire clk, input wire rst_n, input wire signal_in, // 已同步的被测信号 input wire gate_en, // 测量闸门 output reg [31:0] cnt_high, // 高电平计数 output reg [31:0] cnt_low, // 低电平计数 output reg [31:0] period_cnt // 总周期计数 ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_high 32d0; cnt_low 32d0; period_cnt 32d0; end else if (gate_en) begin if (signal_in) begin cnt_high cnt_high 1b1; end else begin cnt_low cnt_low 1b1; end period_cnt period_cnt 1b1; end else begin // 闸门关闭时不计数等待下次清零 cnt_high cnt_high; cnt_low cnt_low; period_cnt period_cnt; end end endmodule使用时要和测频模块配合测量开始前先清零占空比计数器gate_en拉高后两个计数器开始工作gate_en拉低后锁存结果把cnt_high和cnt_low传给后续模块做除法。这里有一个细节很多人第一次没注意cnt_high和cnt_low加起来不一定等于period_cnt。因为在信号正好发生跳变的那个时钟周期sync2可能还没来得及翻转或者已经在翻转瞬间被采样到高或低这会导致一个周期的计数量被分配给高或者低但不会影响精度因为单个周期的分配误差相对于几百上千个周期的总额可以忽略。要实现更精确的计数量可以在边沿脉冲处用一个额外的计数器补偿但工程上没必要。3.3 计算结果与格式转换占空比的计算公式是duty cnt_high x 100 / (cnt_high cnt_low)在FPGA里做这个除法我一般用两种方式。如果只是简单输出直接把cnt_high和cnt_low的原始值送到上位机由上位机计算省资源也省事。但如果要本地显示或者输出百分比值就得在FPGA内部做除法。FPGA内部实现整数除法最直接的方法是使用Xilinx或Intel提供的除法器IP核。配置成一个无符号32位除法器输入被除数和除数输出商和余数。但要注意除法器的延迟不是固定的一个周期输出有效信号要等待若干拍因此后续锁存逻辑要等余数有效标志。另外一个更节省资源的方法是移位减法除法也叫长除法适合除数已知范围内的情况。因为占空比的除数cnt_high加cnt_low就是周期计数值范围基本固定比如几十到几百万可以用循环减法实现。但循环减法的延迟和除数位数相关在要求实时性的场合要谨慎。这个项目里显示用的数码管我直接占空比输出保留整数百分比所以用了整数除法。计算结果是一个0到100之间的数通过查表转成BCD码再驱动四位共阴数码管显示。比如显示75.3%就拆成整数75和小数3两位。如果不需要显示小数实现上会更简单。3.4 需要注意的位宽问题计数器位宽最容易翻车。100MHz参考时钟如果测量窗口长达1秒cnt_ref会数到一亿次二进制表示需要27位窗口如果覆盖1000个10kHz周期也就是0.1秒cnt_ref计数等于1000万次需要24位。为了安全起见我统一把计数器位宽设成了32位这样即使窗口拉到10秒也不会溢出。占空比的cnt_high和cnt_low位宽同理。低频信号占空比接近50%时每个计数值大约是N_clk的一半。但如果信号是窄脉冲比如占空比1%高电平计数就很少可能只有几百个计数这反而需要更多位宽的小数精度。如果百分比要保留一位小数建议把计数结果先乘10再计算可以避免整数除法丢掉小数。4. 仿真验证与板级调试要点4.1 用仿真验证测量模块功能写完RTL代码第一件事不是上板而是做功能仿真。我用的是Vivado自带的仿真器也可以直接改testbench放到ModelSim或者QuestaSim里跑。testbench我分为两层第一层直接产生任意频率和占空比的方波信号送给被测模块通过仿真波形检查边沿检测和计数结果第二层用固定的被测信号频率计算期望值和模块输出的频率值比对用$display打印结果。对于1kHz、占空比30%的方波testbench里的激励信号生成代码大致如下timescale 1ns / 1ps module tb_freq_duty; reg clk_100m; reg rst_n; reg signal_in; wire [31:0] cnt_ref; wire [31:0] cnt_sig; wire [31:0] cnt_high; wire [31:0] cnt_low; wire data_valid; // 100MHz参考时钟 initial clk_100m 0; always #5 clk_100m ~clk_100m; // 复位 initial begin rst_n 0; #100 rst_n 1; end // 产生1kHz方波占空比30% // 高电平300us低电平700us initial begin signal_in 0; #1000; forever begin signal_in 1; #300_000; signal_in 0; #700_000; end end // 实例化被测模块 freq_counter u_freq ( .clk(clk_100m), .rst_n(rst_n), .pos_edge(pos_edge), .neg_edge(neg_edge), .cnt_ref(cnt_ref), .cnt_sig(cnt_sig), .data_valid(data_valid), .gate_state(gate_state) ); // 监控输出 always (posedge data_valid) begin $display(Time%t, cnt_sig%0d, cnt_ref%0d, freq%0.3f Hz, $time, cnt_sig, cnt_ref, cnt_sig * 100_000_000.0 / cnt_ref); end endmodule仿真跑完后重点看三个波形gate_state是否在被测信号上升沿处拉高和拉低cnt_ref是否在闸门期间单调递增data_valid是否为单周期脉冲。如果这三个都正常测频部分基本没问题。占空比仿真需要额外检查cnt_high和cnt_low的比例。如果被测信号是30%占空比cnt_high除以总计数应该接近0.3。仿真中因为开始时可能有半个周期的残差所以前几次结果会略微偏差等运行几个测量窗口后数值会趋于稳定。4.2 板级调试最容易踩的坑仿真通过之后上板调试会有一堆现实问题。第一个坑就是信号完整性和电平匹配。我用信号发生器输出一个TTL电平方波直接连到FPGA引脚发现测量结果在频率低的时候挺准频率稍微高一点就开始飘后来检查发现是输入引脚没有配置上下拉信号产生了振铃。解决办法是在FPGA引脚约束里把IO标准设置成LVCMOS33同时开启内部下拉电阻信号稳定了很多。第二个坑是复位和清零时序。计数器在每次测量结束后不能立刻清零而是要等所有模块都锁存完数据后再清零。如果清零和锁存操作发生在同一个时钟周期可能造成部分寄存器锁存到了清零后的值导致数据错误。我在代码里用data_valid作为锁存使能锁存完成后延时两个时钟周期再清计数器问题就解决了。第三个坑是测量窗口过长导致输出刷新率低。前面说过低频信号如果按1000个周期测量1Hz信号要等1000秒才刷新一次这在实际使用中完全不可接受。我把TARGET_CNT做成寄存器可以通过拨码开关或上位机配置实测下来按信号频率自动调整目标周期数保证刷新率在0.5秒到1秒之间。4.3 常见问题速查表调试阶段我整理了一张问题排查表新手可以按表对照检查现象可能原因排查方法频率测量结果几乎不准输入引脚没做同步亚稳态导致边沿丢失检查是否有两级同步寄存器低频下结果跳动大测量窗口太短N_clk太小增大TARGET_CNT占空比始终显示50%采样信号用的是原始输入而非同步后的信号确认duty_counter采的是sync2输出data_valid一直不拉高边沿检测没有检测到信号用逻辑分析仪看pos_edge是否出现计数结果偶尔跳变复位和清零时序没处理好锁存完成后延时两拍再清零高频信号测不到输入引脚IO标准配置错误检查XDC引脚约束的IO标准我自己的经验是出现频率测量偏差90%的情况不是算法问题而是信号质量问题。先用示波器看输入到FPGA引脚的波形是否干净再用逻辑分析仪看FPGA内部pos_edge脉冲是否规律这两步排查完基本能定位问题。5. 板级实测与结果分析调试完成后我用信号发生器做了一组对照测试。信号发生器输出1Hz、10Hz、100Hz、1kHz、10kHz五组方波频率误差原则上小于0.01%再用FPGA测量结果做对比。实测数据如下设定频率实测频率误差设定占空比实测占空比1Hz0.99997Hz0.003%50%49.98%10Hz10.0002Hz0.002%30%30.03%100Hz100.001Hz0.001%70%69.96%1kHz999.993Hz0.0007%30%30.04%10kHz9999.98Hz0.0002%50%50.01%低频段误差比高频段略大原因主要是测量窗口内N_clk的数量相对少了同时输入信号的边沿抖动对低频影响更大。不过即便如此整个频段的精度都优于0.01%超过了项目预期。占空比测量方面低频时占空比精度主要受参考时钟和边沿抖动影响实测偏差在0.05%以内。这个精度主要归功于100MHz参考时钟对低频信号的“过采样”信号一个周期内能采到几万个点统计效应让结果非常稳定。我顺手在板子上加了一个小扩展把测得的频率和占空比通过UART发送到上位机上位机再用串口助手实时绘图可以看到占空比变化曲线对调试电机PWM、开关电源这类项目非常直观。这块FPGA方案真正的价值在于除了测量它还能对测量结果做后续处理。比如测到一个PWM信号的占空比后可以直接在FPGA内部做PID调节构成一个闭环控制。同样的硬件平台从测量到控制不用换芯片写一段逻辑就行这就是我坚持用FPGA做这个项目的原因。最后再分享一个经验做这种测量类FPGA项目一定要养成用逻辑分析仪看内部信号的习惯。开发板自带的ILA或者SignalTap在调试时是救命稻草但因为插入探针会改变布局布线正式跑的时候记得把探针去掉否则时序裕量可能会受影响。测量类项目本质上是和时间精度打交道任何一个环节的忽视最后都会变成频率数值上的一个偏差。
返回列表