1. 这篇文章解决什么问题
很多 FPGA 新人第一次用 Xilinx FFT IP 时,容易卡在几个地方:
s_axis_data_tdata里面实部、虚部怎么放;FWD_INV到底是 FFT 还是 IFFT;SCALE_SCH为什么会影响 MATLAB 对拍;tvalid / tready / tlast怎么配合;FPGA 输出为什么和 MATLAB 有 1LSB 左右误差。
本文不讲复杂 FFT 公式,重点讲工程里怎么把 Vivado 的 FFT IP 跑起来,并用 MATLAB 结果对拍。
Xilinx FFT IP 支持正向 FFT、反向 IFFT、定点/浮点接口、多种结构、自然顺序或位反转顺序输出等配置。PG109 中也明确说明,该 IP 使用 AXI4-Stream 接口,支持 forward/inverse complex FFT,并支持定点缩放配置。(赛灵思)
2. 本次 IP 配置
本次仿真使用的是 1024 点 IFFT,输入为 16bit 有符号复数数据。
| 配置项 | 本文配置 |
|---|---|
| Transform Length | 1024 |
| Run Time Config Length | Disabled |
| Architecture | Pipelined Streaming I/O |
| Data Format | Fixed-Point |
| Input Data Width | 16 |
| Phase Factor Width | 24 |
| Scaling Options | Scaled |
| Rounding Mode | Convergent Rounding |
| Output Ordering | Natural Order |
| Cyclic Prefix | Disabled |
| FWD_INV | 0,表示 IFFT |
| SCALE_SCH | 16'h1AA,本工程测试配置 |
这里重点说两个参数。
2.1 FWD_INV
FWD_INV = 1表示正向 FFT,FWD_INV = 0表示反向 IFFT。这个不是猜的,PG109 中明确写了这个定义。(赛灵思)
所以本文做的是 IFFT:
FWD_INV = 1'b0; // inverse FFT,也就是 IFFT2.2 SCALE_SCH
固定点 FFT IP 最容易出问题的地方就是缩放。
PG109 里明确说明,Xilinx FFT IP 的 IFFT 本身不自动实现 MATLABifft()里面的1/N缩放,缩放要靠 scaling schedule 等方式处理。(赛灵思)
所以不能简单认为:
Xilinx IFFT 输出 = MATLAB ifft 输出更准确地说,应该是:
Xilinx IFFT 输出 = Xilinx IFFT 内部计算结果 + SCALE_SCH 缩放 + 定点量化 + 舍入误差如果希望和 MATLABifft()对齐,就要特别关注总缩放量。
3. AXI-Stream 接口先看懂这几个信号
FFT IP 主要有三组 AXI-Stream 接口:
// 配置通道 s_axis_config_tdata s_axis_config_tvalid s_axis_config_tready // 输入数据通道 s_axis_data_tdata s_axis_data_tvalid s_axis_data_tready s_axis_data_tlast // 输出数据通道 m_axis_data_tdata m_axis_data_tvalid m_axis_data_tready m_axis_data_tlast m_axis_data_tuserPG109 中说明,s_axis_config_tdata用来传CP_LEN / FWD_INV / NFFT / SCALE_SCH等配置,s_axis_data_tdata用来传输入复数数据XN_RE / XN_IM,m_axis_data_tdata用来输出XK_RE / XK_IM。(赛灵思)
对新人来说,先记住一句话:
只有 tvalid = 1 且 tready = 1 的那个时钟沿,数据才真正传输成功。输入 1024 点数据时,前 1023 个点tlast = 0,最后一个点tlast = 1。
简单时序可以这么理解:
clk ↑ ↑ ↑ ↑ ↑ ↑ ↑ s_axis_data_tvalid 1 1 1 1 1 1 1 s_axis_data_tready 1 1 1 1 1 1 1 s_axis_data_tdata D0 D1 D2 D3 ... D1023 s_axis_data_tlast 0 0 0 0 ... 1注意:tlast不是用来告诉 IP “我要做多少点 FFT” 的。FFT 点数来自 IP 配置,比如本文固定为 1024 点。tlast主要用于帧边界检查,如果数量不对,会触发event_tlast_missing或event_tlast_unexpected这类事件。PG109 里也说明,输入TLAST主要用于生成这些事件。(赛灵思)
4. tdata 里面实部和虚部怎么放
本文是单通道、16bit 实部、16bit 虚部。
PG109 规定,输入数据从 LSB 开始依次打包:先放XN_RE,再放XN_IM。输出也是一样:先放XK_RE,再放XK_IM。(赛灵思)
所以 32bit 的tdata应该这样理解:
// 输入 s_axis_data_tdata[15:0] = XN_RE; s_axis_data_tdata[31:16] = XN_IM; // 输出 m_axis_data_tdata[15:0] = XK_RE; m_axis_data_tdata[31:16] = XK_IM;写成 Verilog:
wire signed [15:0] ifft_out_real; wire signed [15:0] ifft_out_imag; assign ifft_out_real = $signed(m_axis_data_tdata[15:0]); assign ifft_out_imag = $signed(m_axis_data_tdata[31:16]);这个地方非常容易写反。
比如输出数据是:
m_axis_data_tdata = 32'hff5c_00a2那么:
real = 16'h00a2 = 162 imag = 16'hff5c = -164这和本文仿真波形里的第二个输出点是对应的。
5. 本文测试输入
本次输入数据是 1024 个复数点:
x[0] = 0 + 0i x[1] = 1 + 1i x[2] = 2 + 2i x[3] = 3 + 3i ... x[1023] = 1023 + 1023iMATLAB 参考代码如下:
N = 1024; x_real = 0:N-1; x_imag = 0:N-1; x = x_real + 1i * x_imag; y = ifft(x); disp(y(1:10));MATLAB 前几个结果大致为:
511.5000 + 511.5000i 162.4742 - 163.4742i 80.9863 - 81.9863i 53.8234 - 54.8234i 40.2416 - 41.2416i ...仿真中 FPGA 输出前几个点为:
512 + 512i 162 - 164i 80 - 82i 54 - 54i 40 - 42i ...可以看到,符号、趋势、数量级都能对上。整数上存在 1LSB 左右差异是正常的,因为 FPGA IP 用的是 fixed-point,内部有缩放、相位因子量化和舍入;MATLAB 默认是 double 浮点计算。
所以对拍时不要要求每个点都和 MATLAB double 结果完全一样,应该结合定点位宽、缩放策略和允许误差判断。
6. 输入数据发送代码示例
下面给一个简化版 testbench 发送逻辑,只保留关键部分。
localparam integer N = 1024; reg aclk; reg aresetn; reg [31:0] s_axis_data_tdata; reg s_axis_data_tvalid; wire s_axis_data_tready; reg s_axis_data_tlast; reg [15:0] s_axis_config_tdata; reg s_axis_config_tvalid; wire s_axis_config_tready; wire [31:0] m_axis_data_tdata; wire m_axis_data_tvalid; reg m_axis_data_tready; wire m_axis_data_tlast; wire [23:0] m_axis_data_tuser; // 100MHz clock initial begin aclk = 1'b0; forever #5 aclk = ~aclk; end // reset initial begin aresetn = 1'b0; s_axis_data_tdata = 32'd0; s_axis_data_tvalid = 1'b0; s_axis_data_tlast = 1'b0; s_axis_config_tdata = 16'd0; s_axis_config_tvalid = 1'b0; m_axis_data_tready = 1'b1; repeat(10) @(posedge aclk); aresetn = 1'b1; end发送配置:
task send_fft_config; begin @(posedge aclk); // 注意: // 这里的 16'h01AA 是本文工程中的配置示例。 // 实际工程中 s_axis_config_tdata 的位宽和字段位置,要以 Vivado IP 配置界面为准。 s_axis_config_tdata <= 16'h01AA; s_axis_config_tvalid <= 1'b1; while (!s_axis_config_tready) begin @(posedge aclk); end @(posedge aclk); s_axis_config_tvalid <= 1'b0; end endtask发送 1024 点输入数据:
integer i; task send_one_frame; begin for (i = 0; i < N; i = i + 1) begin @(posedge aclk); s_axis_data_tvalid <= 1'b1; // 输入:x[i] = i + j*i // tdata[15:0] = real // tdata[31:16] = imag s_axis_data_tdata <= {i[15:0], i[15:0]}; if (i == N-1) s_axis_data_tlast <= 1'b1; else s_axis_data_tlast <= 1'b0; while (!s_axis_data_tready) begin @(posedge aclk); end end @(posedge aclk); s_axis_data_tvalid <= 1'b0; s_axis_data_tlast <= 1'b0; s_axis_data_tdata <= 32'd0; end endtask主流程:
initial begin wait(aresetn == 1'b1); // 建议先配置,再发数据。 send_fft_config(); repeat(5) @(posedge aclk); send_one_frame(); endPG109 也建议,在希望配置作用于当前帧时,配置通道的数据要在输入数据之前完成;如果配置写入太晚,当前帧可能使用旧配置。(赛灵思)
7. 输出数据抓取
输出端不要按固定延迟去抓数据,应该看m_axis_data_tvalid && m_axis_data_tready。
wire signed [15:0] f0_real; wire signed [15:0] f0_imag; assign f0_real = $signed(m_axis_data_tdata[15:0]); assign f0_imag = $signed(m_axis_data_tdata[31:16]); always @(posedge aclk) begin if (!aresetn) begin // reset end else if (m_axis_data_tvalid && m_axis_data_tready) begin $display("index = %0d, real = %0d, imag = %0d, tdata = %h", m_axis_data_tuser, f0_real, f0_imag, m_axis_data_tdata); end end如果开启了XK_INDEX,m_axis_data_tuser可以帮助你确认当前输出点编号。本文配置为 Natural Order,所以输出顺序就是:
0, 1, 2, 3, ...这也是为什么可以直接和 MATLABifft()输出数组逐点对比。
如果你配置成 bit reversed / digit reversed 输出,就不能直接按顺序对拍,需要先重排。
8. 仿真结果对拍
本文测试中,MATLAB 参考结果如下:
y[0] = 511.5000 + 511.5000i y[1] = 162.4742 - 163.4742i y[2] = 80.9863 - 81.9863iFPGA 仿真波形中可以看到:
f0_real = 512, 162, 80, ... f0_imag = 512, -164, -82, ...对应关系如下:
| 点编号 | MATLAB real | MATLAB imag | FPGA real | FPGA imag |
|---|---|---|---|---|
| 0 | 511.5000 | 511.5000 | 512 | 512 |
| 1 | 162.4742 | -163.4742 | 162 | -164 |
| 2 | 80.9863 | -81.9863 | 80 | -82 |
这个结果说明:
实部、虚部没有接反;
IFFT 方向配置正确;
输出顺序是 Natural Order;
缩放配置和 MATLAB 参考结果基本一致;
固定点误差在可接受范围内。
9. 新人最容易踩的几个坑
9.1 把 real 和 imag 放反
单通道 16bit 定点时:
tdata[15:0] 是 real tdata[31:16] 是 imag不要写反。
9.2 没有用 signed 看数据
如果直接看 16bit 十六进制,负数容易看错。
例如:
16'hff5c如果按无符号看,是 65372。
但它是 16bit 有符号数,实际是:
-164Verilog 里建议这样写:
wire signed [15:0] imag; assign imag = $signed(m_axis_data_tdata[31:16]);9.3 FWD_INV 方向弄反
记住:
FWD_INV = 1 -> FFT FWD_INV = 0 -> IFFT9.4 忽略 SCALE_SCH
固定点 FFT 不可能无限增长位宽,所以缩放策略非常重要。
如果缩放太少,容易 overflow;如果缩放太多,输出幅度会变小,精度也会损失。
PG109 说明,对于 Pipelined Streaming I/O,SCALE_SCH以 2bit 为一组,每组表示相邻两个 Radix-2 stage 的右移位数;00/01/10/11分别表示右移 0/1/2/3 bit。(赛灵思)
所以SCALE_SCH不是随便填一个数,它会直接影响最终输出幅度。
9.5 对拍时忘记 MATLAB 和 FPGA 的缩放差异
MATLAB 的ifft()默认带1/N。
Xilinx FFT IP 的 IFFT 本身不自动做这个1/N,所以需要通过 scaling schedule 或后级逻辑处理。否则 MATLAB 和 FPGA 的结果可能差一个固定比例。(赛灵思)
10. 总结
用 Xilinx FFT IP,不要一上来就纠结 FFT 数学公式。工程调试时先抓住下面几件事:
明确 FFT 还是 IFFT:
FWD_INV = 0是 IFFT;明确输入输出打包:低 16bit 是 real,高 16bit 是 imag;
明确帧长度:本文是 1024 点,最后一个点拉高
tlast;明确输出顺序:Natural Order 才能直接和 MATLAB 顺序对比;
明确缩放关系:固定点 IP 的输出会受
SCALE_SCH影响;对拍时允许 1LSB 左右误差,不要拿 FPGA fixed-point 和 MATLAB double 做绝对相等比较。
本文这个用例中,输入0+0i, 1+1i, 2+2i ...,MATLABifft()输出和 FPGA 仿真结果能够对应上,说明 IP 配置、AXI-Stream 输入、复数数据打包、输出顺序和基本缩放关系都是正确的。
参考资料:
Xilinx / AMD PG109 Fast Fourier Transform LogiCORE IP Product Guide。你上传的 PG109 文件索引:;本文关键参数校对参考官方在线 PG109。