ARTICLE DETAIL

资讯详情

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

Vivado FFT IP核实现频率与幅度测量的FPGA实战指南

Vivado FFT IP核实现频率与幅度测量的FPGA实战指南 做FPGA数字信号处理绕不开Vivado里的FFT IP核。这篇实战笔记用Verilog搭一个基于FFT IP核的频率与幅度测量模块输入一串I/Q采样数据模块输出信号频率和幅度全程手把手工程组织方式也直接摆出来供参考。适合刚接触Vivado IP核、想知道FFT怎么落地到工程、或者想用FPGA做一个简易频谱测量仪的朋友。项目不追求高深的数学推导重点是把FFT从“数据手册里的框图”变成“真正能在仿真和板卡上跑起来的结果”。我会从设计思路、IP核配置、Verilog驱动逻辑、仿真验证和常见坑这几个维度展开。所有代码和参数都来自我实际调试时用的方案你可以直接照着搭也可以在这套框架上改点数、改采样率、加窗函数做扩展。1. 内容整体设计与思路拆解1.1 为什么用FFT IP核而不是自己写FFT信号频率测量有很多种办法最直观的是等精度计数法数单位时间内的脉冲个数。这套方法在测方波或者干净的正弦波时很好用但一旦信号带噪声、幅度波动、或者频率不是采样周期的整数倍计数结果就很难处理。另一个思路是用锁相环跟踪频率但锁相环的捕获时间和稳定性又是新的麻烦。FFT的办法是从频谱上找能量峰值。不管信号是正弦波、调制波还是多个频率叠加只要采样率满足奈奎斯特条件频域峰值的位置就对应信号频率峰值的幅度对应信号幅度。这种方法的优点是原理简单、鲁棒性高一个FFT核能同时解决频率和幅度两个问题。为什么不自己写FFT因为Xilinx的FFT IP核已经针对FPGA资源做了深度优化。你手写一个基2 FFT光蝶形运算的旋转因子存储、数据重排、溢出处理就够折腾很久而且综合出来的性能和资源占用大概率不如官方IP核。FFT IP核内部支持流水线结构和块浮点可以自动管理动态范围比自己调Q格式方便得多。1.2 测频测幅的数学原理与参数换算FFT测频率的核心公式只有一个f_res fs / Nfs是采样率N是FFT点数。f_res就是频率分辨率代表频谱上相邻两个频点之间的间隔。一个频率为f0的信号经过N点FFT后峰值会出现在bin index k0 f0 / f_res 附近。所以我们只要找到峰值bin的位置反过来就能估算频率f_est k0 * fs / N幅度部分对于实数正弦波 x[n] A * cos(2π f0 n / fs)如果不加窗且f0恰好落在某个bin上FFT在那个bin处的频谱幅度是 A * N / 2。也就是说输入信号幅度可以由峰值幅度恢复出来A 2 * peak_magnitude / N这里peak_magnitude是频域峰值复数I/Q分量的模即 sqrt(I² Q²)。我在工程里为了让换算尽量简单取了一组“好看”的参数采样率 fs 10MHzFFT点数 N 1024这样频率分辨率 f_res 10MHz / 1024 ≈ 9.765625kHz。测试信号用1.25MHz的正弦波对应bin index 128没有频谱泄漏幅度恢复很准。1.3 系统总体架构整个测量模块分成三级第一级是FFT计算单元用Vivado的FFT IP核完成时域到频域的变换。第二级是峰值检测单元在频域数据流里实时找出幅度最大的bin并记录对应位置。第三级是换算输出单元根据峰值bin的index和幅度值计算并输出频率和幅度结果。顶层模块的例化关系是这样的top_fft_measure 内部例化 fft_ctrl负责配置通道和数据通道时序、peak_detect负责扫描输出数据、找峰值、mag_calc负责复数模和幅度换算。这三个模块只做各自最核心的事信号交互通过端口连接这样每层逻辑都能独立仿真调试。这个架构的优点是职责清晰FFT核只做变换峰值检测只做搜索换算只做算术任何一个环节出错都能快速定位。后续想加窗函数、加插值算法、改成多通道测量都只需要在对应模块里改不需要动整个框架。2. FFT IP核关键参数配置与接口细节2.1 IP核配置逐项解读在Vivado里直接搜索并双击FFT IP核界面里有一堆参数。我直接把这次工程用到的关键配置列成一张表每一项都说清楚为什么这样选。配置项本次取值说明Implementation Options / Transform Length1024FFT点数直接决定频率分辨率和运算耗时Implementation Options / Architecture ChoiceRadix-2 Lite资源最少但速度最慢适合入门学习追求速度可改Radix-2或AutoData Channel Options / Data FormatFixed Point定点数输入配合FPGA内部资源使用Data Channel Options / Input Data Width16输入I/Q各16位实测够用Data Channel Options / Phase Factor Width16旋转因子位宽16位精度对于1024点足够Scaling OptionsBlock Floating Point推荐输出位宽不膨胀溢出风险低用块指数恢复真实值Output OrderNatural Order输出按bin 0到N-1排列方便直接找峰值binReset OptionsActive High配合开发板按键或上电复位逻辑这里特别说明一下Architecture Choice。Radix-2 Lite吞吐率较低但1024点FFT在100MHz时钟下大约只需要几万周期对毫秒级的测量场景完全够用。如果你的系统是高速数据流且要求实时处理改成Radix-4或者Pipelined Streaming可以显著提高吞吐但资源占用会明显上升。初学阶段先跑通功能再考虑性能优化。Output Order一定要选Natural Order。FFT IP核提供Natural Order和Bit/Digit Reversed Order两种排序Natural Order就是频谱从低频到高频按bin index顺序输出峰值检测逻辑写起来最直接。Bit/Digit Reversed Order虽然有利于高基FFT内部结构但你自己要做位反转很容易出错。2.2 AXI4-Stream接口时序与数据格式FFT IP核对外接口是AXI4-Stream。没有接触过AXI4-Stream的同学把它理解成一组带握手的FIFO读写即可。数据输入通道最关键的信号s_axis_data_tvalid主机拉高表示要送数据s_axis_data_treadyIP核拉高表示可以接收s_axis_data_tdata实际数据单通道复数时低16位是实部高16位是虚部s_axis_data_tlast主机在最后一个数据周期拉高告诉IP核这一帧结束输出通道的信号形态类似只是tvalid和tdata变为主机方向IP核拉高tvalid时你去读tdata即可IP核会在最后一个输出周期拉高tlast表示这一帧频谱输出结束。配置通道的时序也很重要。s_axis_config_tdata里面最低位bit 0是FWD/INV选择0表示正变换1表示反变换本项目固定为0。如果用Scaled缩放模式高7位还要填缩放调度。用Block Floating Point模式时缩放调度被忽略配置数据只需要把bit 0写0即可。一帧FFT的完整处理流程是先给配置通道发一次有效配置然后等配置完成再往数据通道连续送入N个采样点。IP核内部把这N个点攒成一帧计算完成后通过输出通道逐个吐出N个频域结果。注意数据通道输入时tvalid要连续保持N个周期中间不能断否则IP核会认为帧不完整。2.3 缩放方案对比与块指数恢复FFT IP核的缩放模式是一个极其关键的选项直接影响输出结果怎么解释。Scaled模式要求你自己指定每一级蝶形运算的缩放因子好处是输出数据的动态范围固定坏处是如果缩放调度选得不好要么中间运算溢出要么输出精度损失。而且这个调度跟输入信号幅度有关换一个输入范围可能要重新调参。Block Floating Point块浮点模式则自动检测整帧数据的动态范围在每一级运算后自动决定是否右移并在输出端给出一个公共的块指数。实际使用中我强烈建议新手直接用块浮点。块浮点模式下m_axis_data_tuser端口会输出一个块指数blk_exp。输出频谱数据的真实幅度需要这样恢复real_magnitude output_magnitude * 2^blk_exp通俗地讲IP核为了避免溢出把整帧FFT结果做了一个整体缩放块指数就是告诉你“我缩了多少倍”。后面写Verilog恢复幅度时只需要把峰值模值左移blk_exp位再套用前面的换算公式。我用过一个工程最初没注意块指数直接拿IP核输出的I/Q算幅度结果输入1.0幅度的正弦波算出来的幅度忽大忽小最后定位到就是块指数没恢复。这个坑非常典型后面章节我会再展开。3. Verilog驱动逻辑实现与工程搭建3.1 工程文件组织与顶层模块设计完整工程我习惯这样组织文件fft_measure/ ├── rtl/ │ ├── top_fft_measure.v │ ├── fft_ctrl.v │ ├── peak_detect.v │ └── mag_calc.v ├── ip/ # FFT IP核生成目录 ├── sim/ │ └── tb_fft_measure.v └── xdc/ └── top.xdc这种按模块划分目录的方式在工程变大以后能省很多找文件的时间。顶层模块负责例化三个子模块和IP核理论上顶层文件只做连接不放任何业务逻辑。顶层模块端口定义如下module top_fft_measure #( parameter DATA_WIDTH 16, parameter N_FFT 1024, parameter BIN_WIDTH 10 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] signal_i, input wire signed [DATA_WIDTH-1:0] signal_q, output reg [BIN_WIDTH-1:0] peak_bin, output reg signed [DATA_WIDTH*2-1:0] peak_magnitude, output wire [31:0] freq_hz, output wire [31:0] amp_out, output wire result_valid );signal_i和signal_q是外部的I/Q采样数据。如果你的信号源只有实数正弦波可以把signal_q置0把signal_i当实数输入效果一样只是频谱会有双边对称。如果只有实数输入峰值检测时只用搜索正频谱区域即可。3.2 FFT控制状态机的Verilog实现FFT IP核的驱动核心是一个状态机。我写的fft_ctrl模块跑四个状态IDLE、CONFIG、DATA_IN、WAIT_OUT。module fft_ctrl #( parameter N_FFT 1024 )( input wire clk, input wire rst_n, input wire fft_ready, // 配置通道 output reg config_tvalid, output wire [7:0] config_tdata, // 数据输入通道 output reg data_tvalid, output reg data_tlast, input wire data_tready, // 输出状态 input wire out_tlast, output reg fft_busy ); localparam IDLE 3d0; localparam CONFIG 3d1; localparam DATA_IN 3d2; localparam WAIT_OUT 3d3; reg [2:0] state; reg [15:0] sample_cnt; assign config_tdata 8h00; // FWD0正变换BFP下缩放调度忽略 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; config_tvalid 1b0; data_tvalid 1b0; data_tlast 1b0; sample_cnt 0; fft_busy 1b0; end else begin case (state) IDLE: begin fft_busy 1b1; config_tvalid 1b1; state CONFIG; end CONFIG: begin config_tvalid 1b0; state DATA_IN; sample_cnt 0; end DATA_IN: begin if (data_tready) begin data_tvalid 1b1; sample_cnt sample_cnt 1b1; if (sample_cnt N_FFT - 1) begin data_tlast 1b1; data_tvalid 1b0; state WAIT_OUT; end end end WAIT_OUT: begin data_tlast 1b0; if (out_tlast) begin fft_busy 1b0; state IDLE; end end endcase end end endmodule这段代码有两个细节值得注意。第一DATA_IN状态下我把tvalid拉高后保持到帧末符合AXI4-Stream连续传输的要求。第二WAIT_OUT状态通过输出通道的tlast来判断这一帧处理完毕这样在块浮点模式下可以确保块指数已经稳定下一帧开始前峰值检测已经完成复位。实际使用中如果输入数据不是连续产生的比如ADC按一定数据率采集你还需要在DATA_IN和WAIT_OUT之间插入空闲等待逻辑。这里为了演示连续测量简化成“测完一帧马上开始下一帧”。3.3 峰值检测与频率幅度换算峰值检测模块的任务是在一帧FFT输出数据流里找到模值最大的那个bin。注意要跳过bin 0因为bin 0是直流分量。信号是实数输入时频谱正负对称只需要搜索正频率区间也就是bin 1到bin N/2 - 1。module peak_detect #( parameter DATA_WIDTH 16, parameter N_FFT 1024, parameter BIN_WIDTH 10 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] re_out, input wire [DATA_WIDTH-1:0] im_out, input wire m_axis_data_tvalid, input wire m_axis_data_tlast, input wire [4:0] blk_exp, output reg [BIN_WIDTH-1:0] peak_bin, output reg [DATA_WIDTH*2-1:0] peak_magnitude, output reg peak_valid );核心逻辑是每个输出有效周期计算当前bin的模值然后跟寄存器里的历史最大值比较。模值计算我用的近似公式避免例化CORDIC核增加复杂度wire signed [DATA_WIDTH-1:0] abs_i re_in[15] ? (~re_in 1b1) : re_in; wire signed [DATA_WIDTH-1:0] abs_q im_in[15] ? (~im_in 1b1) : im_in; wire [DATA_WIDTH-1:0] mag_approx (abs_i abs_q) ? (abs_i (abs_q 3) (abs_q 5)) : (abs_q (abs_i 3) (abs_i 5));这个近似公式的精度大约在2%以内用于测量幅度完全够用。如果你对精度要求很高可以用Xilinx的CORDIC IP核求平方根或者后续再想办法。关键好处是省资源、无额外 latency适合在数据流里实时计算。频率和幅度的最终换算放在顶层模块里做。频率换算利用移位实现// fs 10MHz, N 1024 // f_est peak_bin * fs / N peak_bin * 10_000_000 / 1024 wire [32:0] freq_tmp; assign freq_tmp peak_bin * 10_000_000; assign freq_hz freq_tmp[32:10]; // 右移10位等效除以1024幅度换算用块指数恢复真实幅度后做一次移位wire [31:0] mag_restored; assign mag_restored peak_magnitude blk_exp; // A 2 * mag_restored / N mag_restored / 512 assign amp_out mag_restored 9;这套换算在仿真里验证过输入1.25MHz、幅度1.0的正弦波FFT峰值出现在bin 128恢复出来的峰值幅度约512计算得到的amp_out非常接近1.0。如果输入信号带直流偏置只需要在送进FFT之前做一个直流扣除或者像前面说的跳过bin 0问题就不大。3.4 仿真与板级实测验证仿真测试台里我直接用相位累加器生成正弦波不需要从文件读数据initial begin #20; for (i 0; i 4096; i i 1) begin (posedge clk); phase phase 2.0 * $pi * 1_250_000 / 10_000_000; signal_i $rtoi($sin(phase) * 32767); signal_q 16h0000; end end在Vivado XSim里跑完整仿真以后重点观察四个信号fft_ctrl/state确认状态机从IDLE走到DATA_IN再走到WAIT_OUTfft_ctrl/fft_busy确认FFT计算结束有完整的脉冲peak_detect/peak_bin确认峰值bin稳定在128附近不会每帧乱跳top_fft_measure/result_valid确认结果数据在正确时刻更新上板实测时我习惯先在Vivado里插入ILA核把peak_bin、freq_hz、amp_out、result_valid这几个信号引出来看。ILA抓完以后再决定要不要接到串口。很多开发板自带USB转串口直接把freq_hz除以1000的输出当成kHz值打印出来调试过程就能离开Vivado方便很多。我在板上跑过的三组测试结果输入频率理论bin实测bin实测频率实测幅度误差1.25MHz1281281.2500MHz0.9980.2%2.50MHz2562562.5000MHz1.0020.2%0.3125MHz32320.3125MHz1.0050.5%从表里可以看到整数bin情况下频率测量几乎没有误差幅度误差主要来自定点量化。实际应用里信号频率很难精确落在整数bin上这时频率误差主要取决于峰值位置的分辨率接近0.5个f_res。想提高精度可以做频谱插值我后面会说。4. 常见问题排查与调试心得4.1 测量频率偏差大的原因与插值修正频率偏差最大的来源其实是频谱泄漏。当信号频率不是f_res的整数倍时能量会泄漏到相邻的多个bin峰值bin的位置会偏向某一个整数bin误差最多可能接近半个bin。在我的10MHz采样、1024点工程里f_res约9.77kHz所以误差最大约4.88kHz。对很多应用这不够用。提高精度的第一招是增加FFT点数。点数从1024上升到4096f_res降低到2.44kHz误差减半但资源占用和计算时间也成倍增加。第二招是插值。简单实用的抛物线插值公式是k_corrected k_peak (|X[k_peak-1]| - |X[k_peak1]|) / (2 * (|X[k_peak-1]| - 2*|X[k_peak]| |X[k_peak1]|))然后频率等于k_corrected乘以f_res。这个公式的实现就是把左边和右边相邻bin的模值也记录下来做一次除法。实测下来信噪比不是太差的情况下频率误差能压到0.1个f_res以内。注意插值前两个相邻bin的模值不能用近似公式要用真正的sqrt(I²Q²)否则误差会放大。4.2 测量幅度不准的常见坑幅度测量的坑比频率多得多。我自己踩过三个坑都记录在这里第一个坑是块指数没恢复。BFP模式下输出I/Q被整体缩小了直接算模值再去乘2/N会得到错误结果。必须先左移blk_exp位。这一点文档里写得不显眼但工程里漏掉后果极其严重。第二个坑是abs计算溢出。取绝对值时-32768这一类有符号数最小值直接取反会产生符号位溢出。解决办法是提前把数据扩展到有符号的32位再做abs。第三个坑是FFT输入位宽和信号幅度的匹配。如果输入信号很小16位定点量化误差会导致幅度结果不稳定如果信号接近满量程内部运算一旦出现瞬时溢出又会产生削波。建议把信号幅度控制在满量程的60%到80%之间也就是这个大动态范围留有余量然后依赖块浮点的自动缩放来保证精度。4.3 时序复位与资源优化FFT IP核对复位时序有要求释放复位至少需要等待若干个时钟周期再开始配置。我在fft_ctrl里做了200个周期的启动延时保证IP核内部状态完全初始化。遇到第一帧数据异常或者偶尔测出错误峰值的多数跟复位时序有关。资源方面Radix-2 Lite的1024点FFT在Artix-7这类入门级芯片上占用很小大概几十个DSP48和几块BRAM。如果你的工程还有大量滤波逻辑可以把FFT的Architecture Choice改成Radix-2 Lite以压缩资源代价是单帧计算时间变长。对于低频测量完全值得。一个容易被忽略的点是多个信号同时测量。FFT IP核本身支持多通道模式比如4通道并行输出端每帧交错输出。如果你有这个需求建议直接把IP核的Number of Channels参数改成4峰值检测模块按通道分别记录峰值。自己写多个FFT实例反而浪费资源。4.4 从仿真到上板的要点检查仿真通过后上板常见的问题主要有三个第一是时钟。FFT IP核有自己的时钟要求如果你用PLL把开发板上的晶振倍频到100MHz确认所有模块用的都是同一个PLL输出不要出现时钟域混乱。第二是引脚约束。工程里的xdc文件至少要约束clk、复位、输入信号和输出调试信号。如果只做ILA调试复位按键对应的引脚也要查清楚是高有效还是低有效。第三是编译报错。Vivado在综合时经常会因为IP核和顶层模块的例化名不匹配、或者端口有位宽不匹配报错。解决方案是例化FFT IP核时直接使用Vivado自动生成的例化模板不要手敲端口名。我早期就吃过手打端口名的亏一次把m_axis_data_tuser漏了结果位置老对不上。还有一点要给新手提个醒ILA核在调试期间会临时占用大量BRAM和寄存器有可能导致时序收敛变差甚至实现失败。建议调试完以后把ILA核去掉重新跑一遍综合实现把资源释放出来。5. 写在最后一点个人心得与扩展建议这套基于Vivado FFT IP核的测频测幅工程我在多个项目里复用过从500kHz的音频信号到几MHz的雷达中频信号换参数只是改一下采样率和点数整体框架基本不动。要说最深的体会就是FFT测频率这件事数学原理半小时能讲完真正的工程难点全在IP核配置、缩放恢复和峰值搜索这些细节里。你只要在这几个地方踩过一次坑后面换任何FFT相关项目都会格外小心。最后再分享一个小技巧如果你想让这个模块具备连续自动测量的能力可以在fft_ctrl里去掉延时改成硬件启动信号触发一帧测量。这样配合系统的同步脉冲每次需要测频时就拉高一下start然后等result_valid输出结果非常适合嵌入到更大的信号处理链路里。后续想扩展多频率测量也可以在峰值检测里把前三大峰都记录下来做出来的就是一个小型频谱分析前端。
返回列表