文章目录
- 引言
- 1 Vivado现有FFT相关IP选择
-
- 1.1 定位与适用场景差异
- 1.2 核心参数配置与功能差异
- 1.3 选型建议
- 2 FFT IP配置
-
- 2.1 IP端口
- 2.2 IP生成及配置选择
- 3 仿真
-
- 3.1 产生原始数据
- 3.2 基于matlab进行FFT处理
- 3.3 基于Modelsim的FFT仿真
-
- 3.3.1 Verilog实现FFT处理
- 3.3.2 激励文件产生
- 3.3.3 modelsim仿真
- 4 验证
- 5 总结
引言
FFT(快速傅里叶变换)是数字信号处理的核心算法,而FPGA凭借并行处理、低延迟、可定制的硬件特性,成为FFT高速实时实现的理想平台,其应用覆盖算法实现、工程开发、场景落地等多个层面。 博主在项目应用中刚接触FFT这个IP时也曾十分迷茫不知如何下手,因此就想将自己之前的一些调试记录整理总结分享给大家,从IP核的配置到IP的调用仿真以及结合matlab进行结果验证几个方面跟大家一起回顾FFT的基本应用。
1 Vivado现有FFT相关IP选择
在Vivado的IP catalog中搜索FFT,会出现FFT和LTE FFT两种IP,如下图所示。
那么我们在实际应用中如何正确选择我们需要的IP呢?LTE FFT是什么?它和FFT有什么区别?什么时候使用它?让我们一起往下看吧。
1.1 定位与适用场景差异
●标准FFT IP核:通用型FFT实现,基于Cooley-Tukey算法,支持8 ~ 65536点(2³ ~ 2¹⁶)的2ⁿ标准点数,适用于雷达、医疗影像、通用频谱分析等大多数非LTE通信场景。 ●LTE FFT IP核:专为3G/4G LTE通信标准定制,仅针对LTE协议的特殊FFT点数和参数优化,非LTE通信系统一般无需使用。
1.2 核心参数配置与功能差异
●LTE专属功能:LTE FFT IP核针对LTE协议的资源元素(URE)、子载波间隔等参数做了底层优化,无需用户手动配置LTE相关参数;标准FFT IP核无此类协议级优化,需用户自行适配通信协议要求。 ●其他通用功能:两者均基于AXI4-Stream接口,支持块浮点/用户定义缩放/无缩放选项,支持循环前缀(CP)插入、配置通道/数据通道/状态信号等通用FFT功能。
1.3 选型建议
●若设计涉及LTE通信场景、需要使用384/768/1536/3072等非2ⁿ点数,优先选择LTE FFT IP核。 ●若为通用信号处理、雷达、医疗影像、自定义频谱分析场景,选择标准FFT IP核,可根据资源和性能需求灵活选择架构。 由于博主本人所处领域不涉及通信领域,据了解我们大部分人实际应用场景也都是对FFT的应用需求,所以下面博主将主要对标准FFT IP的使用以及仿真验证做详细的介绍。
2 FFT IP配置
在Vivado的IP catalog中搜索FFT,会显示出FFT和LTE FFT,那我们该如何选择呢
2.1 IP端口
当我们在vivado软件打开Fast Fourier Transform(9.1) IP就可以看见如下图所示IP接口(部分接口和实际配置选择相关)
我们重点关注S_AXIS_DATA为输入数据端口,我们要进行FFT的数据需要通过这根线输入IP核;S_AXIS_CONFIG为输入配置端口,这个信号包含了对数据进行FFT还是IFFT、缩放因子、FFT变换点数等信息;FFT变换后的数据从M_AXIS_DATA端口输出。 其他接口定义我们可以参考官方手册:PG109。
2.2 IP生成及配置选择

| Number of Channels | 通道数,取值范围:1~12 |
| Transform Length | 变换点数,取值范围: 8 ~ 65536 |
| Target Clock Frequency | 目标时钟频率(MHz),取值范围:1 ~ 1000 |
| Target Data Throughput | 目标数据比特率(MSPS),取值范围:1 ~ 1000 |
| Automatically select | 自动选择,由IP根据上面给定的时钟频率与比特率自行选择架构 |
| Pipelined, Streaming I/O | 流水线IO,允许连续数据处理,仅当使用单通道时,流水线IO可选 |
| Radix-4, Burst I/O | 蝶4突发IO,使用迭代方法分别加载和处理数据。它的资源消耗比流水线架构小,但转换时间更长 |
| Radix-2, Burst I/O | 蝶2突发IO,使用与 Radix-4 相同的迭代方法,但蝶形运算单元更小。它的资源消耗比 Radix-4 架构的尺寸更小,但转换时间更长 |
| Radix-2 Lite, Burst I/O | 蝶2-Lite突发IO,基于 Radix-2 架构,此变体使用分时复用方法实现蝶形运算单元,以实现更少的资源消耗,但代价是转换时间更长 |
| Run Time Configurable Transform Length | 选择运行时是否可配置转换长度。当不可配置时,内核使用更少的逻辑资源并具有更快的最大时钟速度;当可配置时,配置数据s_axis_config_tdata中的NFFT字段被激活,s_axis_config_tdata的位宽会增加8位。 |
上表中所提到的四种迭代方式资源消耗和吞吐量之间的关系: 
本次测试验证中使用单通道,32768采样点,时钟频率为100M,采用流水线方式进行FFT处理,选择静态配置,使用过程中无法配置寄存器,均使用默认配置(FFT IP核默认情况下是正向变换)

| Data Format(数据格式) | Fixed Point:定点数;Floating Point:单精度(32 位)浮点格式。使用多通道时,仅可选择定点数。 |
| Scaling Options(缩放选项)(仅当数据格式选为定点数时可用) | Block Floating Point:块浮点,内核确定需要多少缩放才能充分利用可用动态范围,并将缩放因子报告为块指数;Scaled:缩放,用户定义的缩放计划决定了数据在 FFT 阶段之间的缩放方式;Unscaled:不缩放,运算过程中的所有位增长都会叠加到输出。输出实部位宽 = 输入实部位宽 + log2(变换点数) + 1。虚部位宽 = 实部位宽; |
| Rounding Modes(省入模式)仅当数据格式选为定点数时可用) | Truncation:截断,小数部分直接丢弃;Convergent Rounding:收敛省入,四省五入,特殊的:当小数部分 = 0.5时,如果整数部分是奇数,则收敛舍入向上舍入,如果整数部分为偶数,则向下舍入。收敛舍入可用于避免 DC 偏置,但会增加资源使用量和延迟而导致转换时间略有增加 |
| Precision Options(精度选项) | Input Date Width:输入数据宽度,8 ~ 32;当数据格式为浮点数时固定为32;Phase Factor Width:相位因子宽度,8 ~ 32;当数据格式为浮点数时仅可选24/25; |
| Control Signals(控制信号) | ACLKEN:时钟使能;ARESETn:同步复位,低电平有效且低电平至少要持续两个时钟周期; |
| Output Ordering(输出位序) | Bit/Digit Reversed Order:位/数反转排序;Normal Order:正常顺序;Cycle Prefix Insertion:循环前缀插入 |
| Optional Output Fields(可选输出字段) | XK_INDEX:输出序号,输出数据通道m_axis_data_tuser中的可选字段;OVFLO:数据溢出标志,输出数据通道m_axis_data_tuser中的可选字段,也是输出状态通道m_axis_status_tdata中的可选字段; |
| Throttle Scheme(节流方案) | Non Real Time:非实时;Real Time:实时; |
我们这个界面数据格式选择定点数,使用缩放(在静态配置时IP会使用默认缩放参数,选择动态配置时需要对处理采样点数,FFT变换方向以及缩放参数进行配置),使用正常顺序输出(花费时间稍长)。选择输出序号,非实时的方案。
动态配置相关说明(以32768为例):
wire [31:0]s_axis_config_tdata = 32'b00000000010101010101010100001111 ;
////[bit7:5 000 &bit4:0 (NFFT[4:0])] [bit8 fft:1 ifft:0] scale_sch 15*2bit :10
每一bit详细的解释说明我们可以查看官方给出的应用手册。
3 仿真
3.1 产生原始数据
我们这里使用matlab产生将要进行傅里叶变换的原始数据。 产生正弦波数据(32768点)作为fft变换的输入信号。 关键部分代码如下:
% 参数设置
A = 10; % 正弦波幅值
f = 5000000; % 正弦波频率 (Hz) 5M
fs = 100000000; % 采样率 (Hz) 100M
N = 32768; % 采样点数
phi = 0; % 初始相位
% 生成时间轴
t = (0:N–1) / fs;
% 生成纯净正弦波
x_clean = A * sin(2 * pi * f * t + phi);
x_int16 = int16(x_clean);
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%写入正常的数据文件
%%%%%%% % 打开文件准备写入
fid = fopen('sine_wave_32768_5m.txt', 'w');
%%%%将数据写入文件,每行一个16进制值
for i = 1:length(x_int16)
% 转换为16进制字符串 (4个字符,包含前导0)
hex_str = dec2hex(typecast(x_int16(i), 'uint16'), 4);
% 写入文件
fprintf(fid, '%s\\n', hex_str);
end
上述代码我们可以得到sine_wave_32768_5m.txt文件,里面保存原始数据。 我们使用matlab直观看波形。
figure;
subplot(2, 1, 1);
plot(t, x_clean);
title('纯净正弦波');
xlabel('时间 (s)');
ylabel('幅值');
grid on;
得到正弦波如下图所示。 
3.2 基于matlab进行FFT处理
我们基于上述所产生的原始数据,使用matlab自带的fft函数对其进行处理。 FFT后得到的n个复数值中,第x个(x从0开始)复数值对应的频率f(x)为
f(x) = x * (Fs / n)
% 计算FFT
X_clean = fft(x_clean);
X_noisy = fft(x_noisy);
% 计算频率轴
freqs = (0:N–1) * fs / N; % 0到fs的频率范围
% 计算单边频谱 (取前N/2+1点)
half_N = floor(N/2) + 1;
X_clean_mag = abs(X_clean(1:half_N)) * 2 / N; % 幅值归一化
% 绘制幅频特性
figure;
subplot(2, 1, 1);
plot(freqs(1:half_N), X_clean_mag);
title('纯净正弦波的FFT');
xlabel('频率 (Hz)');
ylabel('幅值');
xlim([0, fs/2]); % 显示0到Nyquist频率
grid on;
由上述代码处理得到结果如下图所示。 
3.3 基于Modelsim的FFT仿真
3.3.1 Verilog实现FFT处理
FFT处理之后的结果是一个复数,包含实部和虚部两部分。我们对复数进行求模运算。(涉及到cordic IP核的使用)
module fft_top(
input wire clk,
input wire resetn,
input wire [15:0] data_in_real,
input wire [15:0] data_in_imag,
input reg data_in_valid,
output wire [15:0] data_out_real,
output wire [15:0] data_out_imag,
output wire source_data_valid,
output wire [31:0] source_data_o,
output wire source_data_valid_before,
output wire data_out_valid,
output wire data_out_valid_before,
output wire m_axis_dout_tvalid,
output wire [23:0] m_axis_dout_tdata,
output [17:0]sync_index_en
);
// wire [31:0]s_axis_config_tdata = 32'b00000000010101010101010100001111 ; [bit7:5 000 &bit4:0 (NFFT[4:0])] [bit8 fft:1 ifft:0] scale_sch 15*2bit :10
wire [23:0]s_axis_config_tdata;
reg s_axis_config_tvalid ;
wire s_axis_config_tready ;
// FFT输入AXIS接口信号
wire [31:0] s_axis_tdata;
reg s_axis_tvalid;
wire s_axis_tready;
wire s_axis_tlast;
// FFT输出AXIS接口信号
wire [31:0] m_axis_tdata;
reg [15:0] data_real;
reg [15:0] data_imag;
reg [15:0] data_real_square;
reg [15:0] data_imag_square;
reg [31:0] source_data;
reg [2:0] delay_cnt;
reg [17:0] sync_index_en_buf;
wire m_axis_tvalid;
reg m_axis_tready;
reg m_axis_tvalid_d;
reg m_axis_tvalid_d1;
reg m_axis_tvalid_d2;
wire m_axis_tlast;
// 输入数据打包
assign s_axis_tdata = {data_in_imag, data_in_real};
// 输出数据解包
assign data_out_real = data_real;
assign data_out_imag = data_imag;
assign data_out_valid = m_axis_tvalid_d;
assign data_out_valid_before = m_axis_tvalid;
assign source_data_valid_before = m_axis_tvalid_d1;
assign source_data_valid = m_axis_tvalid_d2;
assign source_data_o = source_data;
// 始终准备接收输出数据
always @(posedge clk or negedge resetn) begin
if(!resetn) begin
m_axis_tready <= 1'b0;
s_axis_tvalid <= 1'b0;
end
else begin
s_axis_tvalid = data_in_valid;
m_axis_tready <= 1'b1;
end
end
always @(posedge clk or negedge resetn) begin
if(!resetn) begin
s_axis_config_tvalid <= 1'b0;
delay_cnt <= 0;
end
else begin
if (delay_cnt[2]==1) begin
delay_cnt <= delay_cnt;
s_axis_config_tvalid <= 1'b0;
end
else begin
delay_cnt <= delay_cnt + 1;
s_axis_config_tvalid <= 1'b0;
end
end
end
always @ (posedge clk or negedge resetn) begin
if(!resetn) begin
data_real <= 16'd0;
data_imag <= 16'd0;
m_axis_tvalid_d <= 0;
m_axis_tvalid_d1 <= 0;
m_axis_tvalid_d2 <= 0;
source_data <= 0;
end
else begin
m_axis_tvalid_d <= m_axis_tvalid;
m_axis_tvalid_d1 <= m_axis_tvalid_d;
m_axis_tvalid_d2 <= m_axis_tvalid_d1; //平方和之后
if(m_axis_tdata[15]==1'b0) begin //由补码计算原码
data_real <= m_axis_tdata[15:0];
end
else begin
data_real <= ~m_axis_tdata[15:0] + 1'b1;
end
if(m_axis_tdata[31]==1'b0) begin //由补码计算原码
data_imag <= m_axis_tdata[31:16];
end
else begin
data_imag <= ~m_axis_tdata[31:16] + 1'b1;
end
data_real_square <= data_real*data_real;
data_imag_square <= data_imag*data_imag;
source_data <= data_real_square + data_imag_square;
end
end
// FFT IP核实例化
fft_test my_fft_inst (
.aclk(clk),
.aresetn(resetn),
.s_axis_config_tdata (s_axis_config_tdata ),
.s_axis_config_tvalid (s_axis_config_tvalid ),
.s_axis_config_tready (s_axis_config_tready ),
.s_axis_data_tdata(s_axis_tdata),
.s_axis_data_tvalid(s_axis_tvalid),
.s_axis_data_tready(s_axis_tready),
.s_axis_data_tlast(s_axis_tlast),
.m_axis_data_tdata(m_axis_tdata),
.m_axis_data_tvalid(m_axis_tvalid),
.m_axis_data_tready(m_axis_tready),
.m_axis_data_tlast(m_axis_tlast)
);
cordic_sqrt cordic_sqrt_inst (
.aclk(clk),
.aresetn(resetn),
.s_axis_cartesian_tvalid(m_axis_tvalid_d2),
.s_axis_cartesian_tdata(source_data),
.m_axis_dout_tvalid(m_axis_dout_tvalid),
.m_axis_dout_tdata(m_axis_dout_tdata)
);
always @ (posedge clk or negedge resetn) begin:sync_index
integer i;
if(!resetn) begin
for(i = 0;i < 18;i=i+1) begin
sync_index_en_buf[i] <= 0;
end
end
else begin
for(i = 0;i < 18;i=i+1) begin
if (i== 0) begin
sync_index_en_buf[i] <= m_axis_tvalid_d2;
end
else begin
sync_index_en_buf[i] <= sync_index_en_buf[i–1];
end
end
end
end
assign sync_index_en = sync_index_en_buf;
endmodule
3.3.2 激励文件产生
激励文件产生激励信号,同时将处理结果输出并保存成txt格式,后面我们验证结果需要用到。 并将输入的数据打印在modelsim的Transcript。 废话不多说,直接上代码。
module fft_tb();
// 时钟和复位
reg clk;
reg resetn;
// 测试信号参数
localparam FFT_LENGTH = 32768;
localparam CLOCK_PERIOD = 10; // 100MHz
// 生成时钟
initial begin
clk = 0;
forever #(CLOCK_PERIOD/2) clk = ~clk;
end
// DUT接口
reg [15:0] fft_input [0:FFT_LENGTH–1];
reg [15:0] data_in_real;
reg [15:0] data_in_imag;
reg data_in_valid;
wire [15:0] data_out_real;
wire [15:0] data_out_imag;
wire data_out_valid;
wire data_out_valid_before;
wire source_data_valid_before;
wire source_data_valid;
wire [31:0]source_data_o;
wire [17:0]sync_index_en;
integer file_ptr_o;
// 实例化DUT
fft_top dut (
.clk(clk),
.resetn(resetn),
.data_in_real(data_in_real),
.data_in_imag(data_in_imag),
.data_in_valid(data_in_valid),
.data_out_real(data_out_real),
.data_out_imag(data_out_imag),
.data_out_valid(data_out_valid),
.data_out_valid_before(data_out_valid_before),
.source_data_valid_before(source_data_valid_before),
.source_data_valid(source_data_valid),
.source_data_o(source_data_o),
.sync_index_en(sync_index_en)
);
integer i;
integer file_out;
integer file_out1;
// 测试控制
initial begin
// 初始化
resetn = 0;
data_in_real = 0;
data_in_imag = 0;
data_in_valid = 0;
// 复位释放
#100;
resetn = 1;
#100;
$readmemh("sine_wave_32768_5m.txt", fft_input);
for (i = 0; i < FFT_LENGTH; i = i + 1) begin
@(posedge clk);
data_in_valid = 1'b1;
data_in_real <= fft_input[i]; // 每个时钟周期输入一个数据
$display("Input data[%d] = %h", i, data_in_real);
end
// $finish;
@(posedge clk);
data_in_valid = 1'b0;
#200;
// 保存结果到文件 实部 虚部 的平方和
@(posedge source_data_valid_before) begin
file_out1 = $fopen("fft_output_square_32768_5m.txt", "w");
while (1) begin
@(posedge clk);
if (source_data_valid) begin
//
$fwrite(file_out1, "%h\\n",
source_data_o
);
end
end
end
end
endmodule
3.3.3 modelsim仿真
新建工程加入我们的源文件以及激励文件。
完成文件的编译,确保没有报错
编译成功后选择tb进行仿真
我们将需要信号添加进wave界面,Transcript也有输入数据的打印 
最终输出的结果保存在fft_output_square_32768_5m.txt文件内,我们用matlab还原成波形便于和matalb函数处理出的结果做对比。 其实我们也可以使用modelsim自带的将数据的格式选择为analog格式但实际显示效果并不理想。 
4 验证
第3小节我们产生的结果fft_output_square_32768_5m.txt是求模之后的16进制的数据,我们将结果读出并转换成10进制,然后得到结果。
N = 32768;%1024; % 采样点数
filename = 'fft_output_square_32768_5M.txt';
fileID = fopen(filename, 'r');
hexCells = textscan(fileID, '%s');
fclose(fileID);
real_part = hexCells{1}; % 获取单元格数组
real_decValues = zeros(size(real_part));
for i = 1:length(real_part)
real_hexNum = real_part{i};
real_decValues(i) = hex2dec(real_hexNum);
end
sqrt_result = sqrt(real_decValues);
magnitude = abs(sqrt_result(1:(N/2)));
freq = (0:(N–1))* 100000000/N ; % 假设采样率100MHz
plot(freq(1:(N/2)), magnitude);
xlabel('Frequency (Hz)');
ylabel('Magnitude (dB)');
title('FFT Output Spectrum');
grid on;
和之前使用matlab处理得到的结果十分接近。
5 总结
我们这次的验证只是针对FFT IP核最基本的用法,其中涉及到matlab一些简单函数的使用以及cordic IP的使用,希望大家都可以自己动手实操过一遍加深印象。 想要完整工程、matlab完整代码、以及modelsim仿真文件的小伙伴评论区留下联系方式或直接私信博主。
上述过程如有不理解或描述不准确的欢迎各位一起交流学习。
不要忘记关注我,点赞收藏,我会为你带来更多优质内容!



