FPGA实战(14):基于Xilinx FIR Compiler IP的数字滤波器设计实现与仿真测试
引言
在数字信号处理(DSP)应用中,有限长单位冲激响应(FIR)滤波器因其线性相位、稳定性和易于实现等优点被广泛使用。Xilinx Vivado 提供了FIR CompilerIP 核,能够高效地生成针对 FPGA 架构优化的 FIR 滤波器电路。本文将详细介绍如何使用该 IP 核快速搭建一个 16 位输入、40 位输出的 FIR 滤波模块,并编写完整的 Testbench 进行功能仿真。通过本文,读者可以掌握 FIR Compiler IP 的例化方法、顶层模块的封装技巧以及如何设计覆盖多种输入情况(正负交替、大动态范围)的测试激励。
一、功能点概述
- 输入数据:16 位有符号整数(
signed [15:0] i_din) - 输出数据:40 位有符号整数(
signed [39:0] o_dout) - 时钟与复位:
- 系统时钟
i_clk(周期 10ns) - 高有效复位
i_rst(IP 核内部为低有效复位,已做取反处理)
- 系统时钟
- 滤波功能:通过 FIR Compiler IP 配置的系数完成对输入序列的线性卷积滤波。
- 仿真激励:提供 80 多个不同幅值和符号的测试向量,覆盖正数、负数、零值以及大跳变场景,用于验证滤波器的响应特性。
二、顶层模块设计(top.v)
2.1 模块结构
顶层模块tops仅做三件事:
- 声明输入输出端口;
- 实例化 Xilinx FIR Compiler IP 核(
fir_compiler_0); - 将 IP 核的输出数据直接连接到模块输出端口。
2.2 关键连接说明
| IP 核端口 | 顶层连接 | 说明 |
|---|---|---|
aclk | i_clk | 系统时钟 |
aresetn | ~i_rst | 低有效复位,因此取反顶层的高有效复位 |
s_axis_data_tvalid | 1'b1 | 输入通道一直有效 |
s_axis_data_tdata | i_din | 输入数据 |
m_axis_data_tdata | w_fir_tdata | 滤波输出数据 |
o_dout | 直接赋值w_fir_tdata | 模块输出 |
💡注意:IP 核的
s_axis_data_tready和m_axis_data_tvalid在本设计中未使用,因为数据流是连续的,且我们只关心输出数据值。
2.3 完整代码
`timescale 1ns / 1ps module tops ( input i_clk, input i_rst, input signed [15:0] i_din, output signed [39:0] o_dout ); // wire wire [39:0] w_fir_tdata; // assign assign o_dout = w_fir_tdata; // inst fir_compiler_0 fir_compiler_0_u0 ( .aresetn (~i_rst ), // 低有效复位 .aclk (i_clk ), .s_axis_data_tvalid (1'b1 ), // 持续有效 .s_axis_data_tready ( ), .s_axis_data_tdata (i_din ), .m_axis_data_tvalid ( ), .m_axis_data_tdata (w_fir_tdata ) ); endmodule三、Testbench 设计(tb.v)
3.1 设计要点
- 时钟生成:
always #5 i_clk = ~i_clk;产生周期 10ns 的时钟。 - 复位时序:
i_rst初始为高,100ns 后拉低,保证滤波器内部状态正确初始化。 - 激励序列:每 40ns(即 4 个时钟周期)改变一次输入数据,便于观察输出响应。
- 数据覆盖:
- 正数:
1000, 2000, 5000, 1200, 4500, ... - 负数:
-3000, -2000, -2300, -4500, -5000, ... - 绝对值较小的数值:
50, 200, 300, ... - 交替正负,模拟真实信号变化。
- 正数:
3.2 为什么这样做?
- 充分激励:多种数值类型可以测试滤波器在不同输入幅度下的线性响应,验证是否出现溢出或饱和错误。
- 周期变化:40ns 的间隔留出足够的滤波建立时间(滤波器抽头数决定延迟),便于波形观察。
- 长序列:80 多个数据点,可以观察到滤波器从暂态到稳态的完整过程。
3.3 完整代码
`timescale 1ns / 1ps module test_tops; reg i_clk; reg i_rst; reg signed[15:0] i_din; wire signed[39:0] o_dout; tops tops_u( .i_clk (i_clk), .i_rst (i_rst), .i_din (i_din), .o_dout (o_dout) ); initial begin i_clk = 1'b1; i_rst = 1'b1; i_din = 16'd0; #100 i_rst = 1'b0; i_din = 16'd1000; #40 i_din = 16'd2000; #40 i_din = -3000; #40 i_din = 16'd5000; #40 i_din = 16'd1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = 16'd4500; #40 i_din = -2000; #40 i_din = -2300; #40 i_din = 16'd50; #40 i_din = 16'd1500; #40 i_din = 16'd200; #40 i_din = 16'd1200; #40 i_din = 16'd4200; #40 i_din = -2000; #40 i_din = -3000; #40 i_din = 16'd5000; #40 i_din = 16'd1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = -4500; #40 i_din = -2000; #40 i_din = 16'd2300; #40 i_din = 16'd50; #40 i_din = 16'd1500; #40 i_din = -200; #40 i_din = 16'd1200; #40 i_din = 16'd4200; #40 i_din = -2000; #40 i_din = -3000; #40 i_din = 16'd5000; #40 i_din = 16'd1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = -4500; #40 i_din = -2000; #40 i_din = -2300; #40 i_din = 16'd50; #40 i_din = 16'd1500; #40 i_din = 16'd200; #40 i_din = 16'd1200; #40 i_din = -4200; #40 i_din = 16'd2000; #40 i_din = 16'd3000; #40 i_din = -5000; #40 i_din = -1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = 16'd4500; #40 i_din = -2000; #40 i_din = -2300; #40 i_din = 16'd50; #40 i_din = 16'd1500; #40 i_din = 16'd200; #40 i_din = 16'd1200; #40 i_din = -4200; #40 i_din = 16'd2000; #40 i_din = 16'd3000; #40 i_din = -5000; #40 i_din = 16'd1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = 16'd4500; #40 i_din = -2000; #40 i_din = 16'd2300; #40 i_din = 16'd50; #40 i_din = 16'd1500; #40 i_din = -200; #40 i_din = 16'd1200; #40 i_din = 16'd4200; #40 i_din = 16'd2000; #40 i_din = -3000; #40 i_din = 16'd5000; #40 i_din = 16'd1000; #40 i_din = 16'd1200; #40 i_din = 16'd300; #40 i_din = -4500; #40 i_din = 16'd2000; #40 i_din = 16'd2300; #40 i_din = -50; #40 i_din = 16'd1500; #40 i_din = 16'd200; #40 i_din = -1200; #40 i_din = 16'd4200; end always #5 i_clk = ~i_clk; endmodule四、创新点与亮点
极简顶层封装
只需 3 行代码完成 IP 核集成(端口声明、连线、实例化),将复杂的 AXI4-Stream 接口简化为简单的输入输出信号,便于系统级调用。复位极性自适应
巧妙地利用~i_rst将顶层高有效复位转换为 IP 核所需的低有效复位,无需额外逻辑。高覆盖率的 Testbench
- 超过 80 组测试数据,包含正数、负数、零、大绝对值和小绝对值,充分检验滤波器线性范围和饱和特性。
- 每 40ns 切换数据,与 10ns 时钟配合,产生稳定的 4 个时钟周期输入间隔,符合典型滤波器数据率。
- 初始 100ns 复位保证所有寄存器进入已知状态。
便于仿真调试
代码中未使用复杂的文件操作或任务,直接在 initial 块中编写激励,易于修改和移植,适合教学演示和快速原型验证。
五、仿真结果预期
运行 ModelSim / Vivado Simulator 后,可以观察到:
- 复位期间输出为 0;
- 复位释放后,前几个周期输出仍保持为 0(滤波器初始延时);
- 随着有效数据输入,输出开始跟随输入的变化,但幅度和波形受滤波器系数平滑作用而变得缓和;
- 当输入出现正负大跳变时,输出呈现逐渐过渡的特性,无毛刺。
(读者可自行在仿真软件中加载波形,对比i_din和o_dout信号。)
六、总结
本文通过一个完整的 FIR 滤波器设计实例,展示了 Xilinx FIR Compiler IP 的使用方法、顶层模块的简洁封装以及高度结构化的 Testbench 编写技巧。该设计可以直接用于语音处理、通信信道均衡、传感器信号滤波等场景。
关键收获:
- 理解 FIR Compiler IP 的信号含义(tvalid, tdata, aresetn 等)。
- 学会用取反方式匹配复位极性。
- 掌握编写覆盖正负大动态范围的激励序列。
扩展建议:
- 根据实际需求修改 IP 核的系数文件(.coe)以实现低通、高通或带通滤波。
- 增加
s_axis_data_tready和m_axis_data_tvalid的握手逻辑,用于与非连续数据源对接。 - 将输出截位为 16 位以匹配 DAC 等外设。
希望本文能帮助读者快速上手 FPGA 数字滤波器设计。若有疑问,欢迎在评论区交流讨论。
附录:完整工程文件
top.v– 顶层模块tb.v– 仿真测试激励fir_compiler_0.xci– IP 核配置文件(需在 Vivado 中生成)
注:文中 FIR Compiler IP 的系数由用户根据滤波器指标自行配置,本文示例不限定具体系数。
