欢迎光临
我们一直在努力

FPGA应用技巧之Xilinx IP Fast Fourier Transform(FFT)应用及仿真实例说明

文章目录

  • 引言
  • 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:N1) / 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:N1) * 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[i1];
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_LENGTH1];
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:(N1))* 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仿真文件的小伙伴评论区留下联系方式或直接私信博主。


上述过程如有不理解或描述不准确的欢迎各位一起交流学习。 在这里插入图片描述 不要忘记关注我,点赞收藏,我会为你带来更多优质内容!

赞(0)
未经允许不得转载:171主机测评 » FPGA应用技巧之Xilinx IP Fast Fourier Transform(FFT)应用及仿真实例说明
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址