前言
傅里叶变换能将信号从时域转换到频域。在频域中,系统响应等于信号与系统传递函数的乘积,这比直接在时域进行卷积运算更容易实现。传统的傅里叶变换针对连续时间信号,而工程实践中只能对离散信号采样,因此诞生了离散傅里叶变换(DFT)。由于 DFT 算法的时间复杂度为 O(n^2),计算量过大,快速傅里叶变换(FFT)将其优化至 O(nlogn),成为数字信号处理中的核心算法。
本文重点介绍如何在 FPGA 中利用 IP 核实现 FFT,完成时域到频域的变换及基波频率采集。虽然原理部分较为经典,但实际工程调试中涉及到的参数配置、数据流控制及资源权衡往往容易被忽视,以下结合实战经验分享具体方案。
一、原理与目的
1. 傅里叶变换基础
傅里叶变换将任意信号分解为不同频率正弦信号的叠加。通过频率与幅值的对应关系,可以得到信号的幅频特性曲线。加入离散时间信号应用后即为离散傅里叶变换(DFT),在此基础上优化算法得到快速傅里叶变换(FFT)。
FFT 的核心在于蝶形运算和旋转因子。常见的实现方式有时域抽取(DIT)和频域抽取(DIF),两者在单向运算中互为逆序排列。重点关注二进制逆序排列的操作时机,这是确保 FFT 正确性的关键。
2. 实现目的
本文旨在说明如何配置 FFT IP 核,获取频谱数据,并利用这些数据进行后续的信号分析操作。
二、配置 FFT IP 核
在 IP Catalog 中搜索 FFT,双击进入配置界面。相比 NCO 或 PLL,FFT 的配置选项更为集中,建议按从上到下的顺序逐项调整。
1. Transform(变换配置)
- Length(长度):即 N 点 FFT。N 值与采样频率 Fs 和频率分辨率 dF 的关系为 dF = Fs / N。N 越大,频率分辨率越高,但占用的 FPGA 资源也越多。考虑到通信频率升高带来的采样率提升需求,以及资源限制,本例选用 1024 点 FFT。
- Direction(方向):支持正向 FFT(Forward)、逆向 FFT(Reverse)或双向变换(Bi-directional)。根据需求选择,本例采用双向变换以兼容逆变换场景。
2. I/O(接口配置)
-
Data Flow(数据流模式):
- Streaming(流水线):高速性高,输入一个周期数据后下一个周期即可输出,但资源占用较大。
- Burst(爆发):资源占用低,适合内存受限场景。
- Variable Streaming / Buffered Burst:介于两者之间。
若对内存管理不够熟悉,推荐直接使用 Streaming 模式。虽然资源消耗较高,但运算速度极快,且 Input Order 和 Output Order 通常固定为 Nature(自然数)。
3. Data and Twiddle(数据精度)
- Representation(表示方式):
- Fixed Point(定点):资源消耗最低,但存在溢出风险。
- Single Floating Point(单浮点):精度高,资源占用多。
- Block Floating Point(块浮点):折中方案,动态公共指数,兼顾精度与资源,是大多数 FFT 模块的默认推荐。
- Width(位宽):Data Input Width 决定输入精度,Twiddle Width 决定旋转因子精度。位宽越宽,数值表示越精确。
三、实例化与连接
生成 HDL 文件后,确认模块引脚定义。实例化过程可分为数据准备、芯片接入、数据处理三个阶段。
1. 芯片接入
- clk / reset_n:标准时钟与低电平复位信号。
- inverse:0 代表正变换,1 代表逆变换。
- sink_valid:直接置 1,表示始终有有效数据输入(部分模块需配合 ready 握手)。
- sink_ready:芯片反馈外部是否可以继续输入数据的信号。
- sink_error:输入 2'b00。高低位分别代表实部和虚部校验,高电平表示错误,不进行运算。
- sink_sop / sink_eop:起始与结束标识,高电平有效。二者间隔通常为 N 倍时钟周期。
- sink_real / sink_imag:输入信号的实部和虚部。正变换时通常只有实部,虚部补 0;反变换则需同时提供。
- source_exp:配合 source_real/imag 使用,用于块浮点模式下的公共指数值。
2. 数据准备
关键在于起始符(SOP)和结束符(EOP)的时序控制。SOP 上升沿标志数据开始,EOP 上升沿标志结束。建议在仿真环境中验证时序,避免标志符错位导致数据错乱。
3. 数据处理
获取输出数据的实部、虚部及指数后,首先将补码转换为原码,再进行能量谱转换。注意幅频特性线的对称性,N 点 FFT 只需取前 N/2 部分频谱,后半部分与前半部分对称。
4. 代码实现
以下为中间截取的关键逻辑代码,展示了 1024 点 FFT 的峰值搜索与幅值计算流程。
// 1024 points FFT and search peak
// Use 1024kHz sampling frequency, for the resolution frequency will be about 1kHz
// The two signal frequencies in the FFT will be saved in Var fft_out[0] and fft_out[1]
wire signed [11:0] Data_signed;
assign Data_signed = Din[11] ? Din - 12'h800 : Din + 12'h800; // 偏移至有符号范围(-2048~2047)
reg [9:0] sample_count; // 0~1023
always @(posedge clk1024k) begin
if (sample_count == 10'd1023)
sample_count <= 0;
else
sample_count <= sample_count + 1;
end
// Gain FFT frame
wire sink_sop;
wire sink_eop;
assign sink_sop = (sample_count == 10'd0);
assign sink_eop = (sample_count == 10'd1023);
wire [11:0] real_out;
wire [11:0] imag_out;
wire source_valid;
wire freq_start;
wire freq_end;
wire [5:0] exp;
fft1024 u_fft (
.clk(clk1024k),
.reset_n(1),
.inverse(0),
.sink_valid(1),
.sink_sop(sink_sop),
.sink_eop(sink_eop),
.sink_real(Data_signed),
.sink_imag(12'b0),
.sink_error(2'b00),
.source_ready(1),
.source_valid(fft_valid),
.source_real(real_out),
.source_imag(imag_out),
.source_error(),
.source_sop(freq_start),
.source_eop(freq_end),
.source_exp(exp)
);
// Gain magnitude
wire [11:0] real_num;
wire [11:0] imag_num;
wire [23:0] power;
assign real_num = real_out[11] ? (~real_out + 1) : real_out;
assign imag_num = imag_out[11] ? (~imag_out + 1) : imag_out;
assign power = (real_num * real_num) + (imag_num * imag_num);
wire [11:0] magnitude_temp;
wire [17:0] magnitude;
sqrt u_sqrt(
.radical(power),
.q(magnitude_temp)
);
assign magnitude = magnitude_temp << exp;
// Get spectrum
reg [9:0] cnt;
reg freq_end_d;
always @(posedge clk1024k) begin
if (freq_start)
cnt <= 0;
else if (fft_valid)
cnt <= cnt + 1;
if (fft_valid && cnt <= 10'd512) begin
// magnitude 即为当前频率下的幅频特性
end
end
四、注意事项
在实际调试过程中,常见的问题集中在数据输入模式的选择与时序控制上。
- 数据流模式:如果不熟悉内存管理,强烈建议使用 Streaming 模式。其他模式如 Burst 可能涉及复杂的内存读写逻辑,容易导致输出错乱。
- 标志符时序:使用 Streaming 模式时,务必仔细推演 SOP 和 EOP 的时序。标志符错位会导致整个帧的数据解析错误,建议配合仿真工具验证。
总结
对于满足工程需求的场景,推荐使用系统提供的 IP 核进行配置,效率更高且稳定性好。虽然基于原理手写代码(如 8 点 FFT)有助于理解底层机制,但资源消耗较大,难以直接应用于复杂系统。若需深入练习算法原理,手写实现不失为一种好的方法,但在实际工程中应优先考虑成熟 IP 核方案。
以上方案基于实际调试经验整理,难免存在细节疏漏,欢迎指正讨论。

