在实际 FPGA 或 ASIC 数字信号处理系统中,IIR(无限脉冲响应)滤波器因其在相同阶数下能提供比 FIR 滤波器更陡峭的过渡带而备受青睐。然而,IIR 滤波器的反馈结构使其硬件实现,特别是定点化和稳定性处理,比 FIR 滤波器更具挑战性。本文将以一个具体的 4 阶低通 IIR 滤波器为例,详细讲解如何从零开始,使用 Verilog 语言在 FPGA 上实现一个稳定、可综合的数字滤波器。我们将从滤波器系数的生成与量化开始,逐步深入到直接 II 型结构的 RTL 设计、仿真验证,并重点分析定点运算中的精度控制、溢出处理以及实际工程中的关键注意事项。无论你是正在学习数字信号处理硬件实现的学生,还是需要在实际项目中部署 IIR 滤波器的工程师,本文提供的完整设计流程和代码都将为你提供清晰的参考。
1. IIR 滤波器基础与设计指标确定
1.1 IIR 滤波器核心概念与硬件实现挑战
IIR 滤波器的系统函数包含零点和极点,其输出不仅与当前及过去的输入有关,还与过去的输出有关。这种递归特性用差分方程表示为: ( y[n] = \sum_{k=0}^{M} b_k x[n-k] - \sum_{k=1}^{N} a_k y[n-k] ) 其中,( b_k ) 是前馈系数,( a_k ) 是反馈系数。
在硬件实现中,IIR 滤波器面临两个主要挑战:
- 稳定性:由于存在反馈环,极点的位置必须在单位圆内才能保证系统稳定。系数量化误差可能导致极点移动到单位圆外,从而引发振荡。
- 有限字长效应:FPGA 内部使用有限位宽的定点数进行运算。系数量化、乘法舍入以及溢出都可能引入误差,甚至导致滤波器功能异常。
1.2 明确设计指标与系数获取
假设我们的设计指标如下:
- 滤波器类型:4 阶巴特沃斯低通滤波器
- 采样频率:8 MHz
- 截止频率:2 MHz
- 阻带衰减:> 40 dB
- 量化位数:12 位
在 MATLAB 或 Python (scipy.signal) 中,可以轻松生成满足指标的滤波器系数。
% MATLAB 示例:生成 4 阶巴特沃斯低通滤波器系数 fs = 8e6; % 采样频率 8 MHz fc = 2e6; % 截止频率 2 MHz n = 4; % 滤波器阶数 % 计算归一化截止频率 Wn = fc/(fs/2); % 设计巴特沃斯滤波器 [b, a] = butter(n, Wn, 'low'); % 显示系数 fprintf('分子系数 b: '); disp(b); fprintf('分母系数 a: '); disp(a(2:end)); % a(1) 总是 1,通常不显式实现运行上述代码,可能得到如下浮点系数:
b = [0.145, 0.579, 0.579, 0.145](近似值)a = [1.000, -0.671, 0.252, -0.028](近似值,a[0]为 1)
注意:硬件实现时,我们通常处理的是a[1]到a[N],即-a_k。
2. 系数定点量化与直接 II 型结构选择
2.1 将浮点系数转换为定点整数
FPGA 内部使用二进制数进行运算,因此需要将浮点系数转换为定点数。我们选择 12 位量化,并采用 Q格式(例如 Q1.11,表示 1 位整数,11 位小数)来保证精度。
% 系数定点量化示例 Q = 11; % 小数部分位数 (Q1.11格式,总位宽12) scale = 2^Q; % 量化分子系数 b(假设值为示例) b_float = [0.145, 0.579, 0.579, 0.145]; b_quant = round(b_float * scale); % 可能得到:b_quant = [297, 1186, 1186, 297] (十进制) % 量化分母系数 a(注意符号,硬件实现通常用 -a_k) a_float = [-0.671, 0.252, -0.028]; % 对应 a[1], a[2], a[3] a_quant = round(a_float * scale); % 可能得到:a_quant = [-1374, 516, -57] (十进制) % 验证量化误差 b_error = b_float - b_quant / scale; a_error = a_float - a_quant / scale; fprintf('系数量化误差应足够小。\n');量化后的系数将作为常数在 Verilog 代码中使用。
2.2 为什么选择直接 II 型结构?
IIR 滤波器有多种实现结构(直接 I 型、直接 II 型、级联型、并联型)。对于 FPGA 实现,直接 II 型(规范型)是最常用的选择,因为它需要最少的延迟单元(寄存器),从而节省硬件资源。
直接 II 型结构将系统函数分解为两部分:
- 全零点部分(前馈):( w[n] = x[n] + \sum_{k=1}^{N} a_k w[n-k] )
- 全极点部分(反馈):( y[n] = \sum_{k=0}^{M} b_k w[n-k] )
其中,( w[n] ) 是中间状态变量。这种结构只需要max(N, M)个延迟单元。
3. Verilog RTL 设计与关键参数说明
3.1 模块接口与参数定义
首先定义滤波器的模块接口和用参数表示的量化系数。
module iir_lowpass_filter ( input wire clk, // 系统时钟 (假设与 8MHz 采样率同步或更高) input wire rst_n, // 异步低电平复位 input wire signed [11:0] data_in, // 12位有符号输入数据 output reg signed [11:0] data_out // 12位有符号输出数据 ); // -- 滤波器系数参数(Q1.11格式,12位有符号) -- // 这些系数应由 MATLAB/python 计算并量化后填入 localparam signed [11:0] B0 = 12'sd297; // b0 量化值 localparam signed [11:0] B1 = 12'sd1186; // b1 量化值 localparam signed [11:0] B2 = 12'sd1186; // b2 量化值 localparam signed [11:0] B3 = 12'sd297; // b3 量化值 localparam signed [11:0] A1 = -12'sd1374; // -a1 量化值 localparam signed [11:0] A2 = 12'sd516; // -a2 量化值 localparam signed [11:0] A3 = -12'sd57; // -a3 量化值 // -- 中间状态变量寄存器 -- // 用于存储 w[n-1], w[n-2], w[n-3] reg signed [11:0] w1, w2, w3; // 当前中间变量 w[n] wire signed [11:0] w_current; // -- 乘法结果需要更宽的位宽以防止溢出 -- // 假设输入和系数都是12位,乘法结果最多24位。 // 但连续累加可能使位宽增长,需要合理截断或舍入。 wire signed [23:0] feedback_sum; // 反馈部分求和 wire signed [23:0] feedforward_sum; // 前馈部分求和 // 定义 w_current 的计算逻辑 // w[n] = x[n] + A1*w[n-1] + A2*w[n-2] + A3*w[n-3] assign feedback_sum = (A1 * w1) + (A2 * w2) + (A3 * w3); // 注意:由于是Q1.11格式,乘法结果实际是Q2.22格式,需要调整。 // 一种简单做法是取中间有效位。这里先计算,后在always块中处理。 assign w_current = data_in + (feedback_sum >>> 11); // 近似除以 2^11,转换回 Q1.11 // 前馈部分求和:y[n] = B0*w[n] + B1*w[n-1] + B2*w[n-2] + B3*w[n-3] assign feedforward_sum = (B0 * w_current) + (B1 * w1) + (B2 * w2) + (B3 * w3); // 主时序逻辑:更新状态寄存器和输出 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位时清空状态 w1 <= 12'sb0; w2 <= 12'sb0; w3 <= 12'sb0; data_out <= 12'sb0; end else begin // 更新状态变量:移位寄存器 w3 <= w2; w2 <= w1; w1 <= w_current; // 处理前馈部分求和,并转换回Q1.11格式输出 data_out <= feedforward_sum >>> 11; // 近似处理 end end endmodule3.2 定点运算精度与溢出处理详解
上面的代码是一个简化模型,实际项目中必须仔细处理运算精度和溢出。
- 乘法位宽:12 位数乘以 12 位数,结果是 24 位。这个 24 位数是 Q2.22 格式(如果两个 Q1.11 数相乘)。
- 累加位宽:多个 24 位的数相加,位宽会增长。3 个系数相加,最少需要 24 + ceil(log2(3)) ≈ 26 位来保证不溢出。但在资源受限时,可能需要饱和处理或更高位宽。
- 截断与舍入:将宽位数的乘法累加结果(如 Q2.22)转换回输出格式(Q1.11)时,简单的右移 11 位是截断,会引入精度损失。更好的方法是舍入:
(sum + (1 << 10)) >>> 11。
一个更健壮的版本会对位宽进行扩展:
// 使用更宽的内部位宽进行运算 localparam INT_WIDTH = 2; // 整数部分位宽(根据系数最大值估计) localparam FRAC_WIDTH = 11; // 小数部分位宽 localparam MULT_WIDTH = INT_WIDTH * 2 + FRAC_WIDTH * 2; // 乘法后位宽 reg signed [MULT_WIDTH-1:0] feedback_sum_reg; // 用于寄存累加结果,防止长路径 // ... 在always块中顺序计算反馈和前馈部分,并应用舍入4. 测试平台与仿真验证
设计完成后,必须通过仿真验证滤波器的功能是否正确。编写一个 Testbench。
`timescale 1ns / 1ps module tb_iir_filter; // 输入输出信号 reg clk; reg rst_n; reg signed [11:0] data_in; wire signed [11:0] data_out; // 实例化被测试模块 iir_lowpass_filter uut ( .clk(clk), .rst_n(rst_n), .data_in(data_in), .data_out(data_out) ); // 生成时钟信号 (假设 100MHz 时钟,远快于 8MHz 采样率,实际可能用同步使能信号) initial begin clk = 0; forever #5 clk = ~clk; // 10ns 周期 -> 100MHz end // 测试激励 initial begin // 初始化 rst_n = 0; data_in = 0; #100; // 保持复位一段时间 // 释放复位 rst_n = 1; #20; // 测试案例 1: 输入一个脉冲信号 data_in = 12'sd1000; // 输入一个阶跃 #10; // 等待一个时钟周期(采样周期) data_in = 0; // 观察输出衰减波形,应符合IIR滤波器的脉冲响应 // 等待足够长时间观察响应 #500; // 测试案例 2: 输入正弦波(频率低于截止频率) // 这里可以用循环生成正弦波数据 // ... // 测试案例 3: 输入正弦波(频率高于截止频率) // 观察输出幅度是否被显著衰减 // ... #1000; $finish; end // 将信号变化记录到VCD文件,用于波形分析 initial begin $dumpfile("iir_filter.vcd"); $dumpvars(0, tb_iir_filter); end endmodule在仿真中,你需要:
- 观察脉冲输入的响应,确认输出是衰减震荡的,且稳定到零。
- 输入不同频率的正弦波,测量输出幅度,验证截止频率附近的衰减特性是否与理论设计相符(例如,2MHz 信号应有 -3dB 衰减)。
5. 常见问题与工程实践要点
5.1 稳定性与极限环振荡
即使理论系数是稳定的,系数量化也可能导致极点移动到单位圆外。此外,在零输入条件下,由于舍入误差,滤波器输出可能不会衰减到零,而是在一个小的范围内持续振荡,这称为极限环振荡。
应对策略:
- 系数灵敏度分析:在 MATLAB 中检查量化后系数的极点位置。
- 使用级联结构:将高阶滤波器分解为多个二阶节(SOS)级联。二阶节对系数量化误差更不敏感。
H(z) = H1(z) * H2(z) * ... - 舍入处理:采用收敛舍入(向最近偶数舍入)而不是直接截断。
- 饱和运算:在输出级加入饱和处理,防止溢出导致的大幅度振荡。
5.2 资源优化与速度权衡
| 优化目标 | 实现方法 | 潜在代价 |
|---|---|---|
| 减少逻辑资源 | 使用单乘法器/累加器,分时复用 | 处理速度降低,需要更高时钟和控制逻辑 |
| 提高吞吐率 | 使用全并行结构,每个系数配专属乘法器 | 消耗大量 DSP 片和寄存器资源 |
| 降低功耗 | 使用时钟门控,在无数据输入时关闭部分逻辑 | 增加设计复杂性 |
对于我们的 4 阶滤波器,如果目标 FPGA 的 DSP 资源充足,全并行实现是最简单的。如果资源紧张,可以考虑将 4 阶滤波器拆分为两个二阶节级联,每个二阶节复用一套计算单元。
5.3 同步与时序约束
- 采样率与时钟率:如果系统时钟(如 100MHz)高于采样率(8MHz),必须在数据输入端口使用一个采样使能信号(
data_valid),确保只在有效的采样点进行滤波计算。 - 时序约束:综合和实现后,必须对设计施加时序约束,确保乘法器和累加器的组合逻辑路径能够在一个时钟周期内稳定。否则会出现建立时间违例,导致功能错误。
// 带有使能信号的改进版接口 module iir_lowpass_filter ( input wire clk, input wire rst_n, input wire data_valid, // 数据有效信号,高电平表示 data_in 有效 input wire signed [11:0] data_in, output reg signed [11:0] data_out, output reg data_out_valid // 输出有效信号 );6. 进阶方向与最佳实践清单
掌握了基本实现后,可以考虑以下进阶方向来提升滤波器性能或适用性:
- 自适应滤波器:使系数能够根据输入信号动态调整,用于系统辨识、信道均衡等。
- 多速率滤波器:结合抽取和插值,实现采样率变换,常用于无线通信。
- CIC 滤波器:一种无需乘法器的特殊 IIR 结构,非常适合高倍率的采样率变换。
- 使用 IP 核:Xilinx 和 Intel FPGA 都提供了经过高度优化的 DSP IP 核(如 FIR Compiler),它们也支持某些 IIR 结构,可以节省开发时间并保证性能。
IIR 滤波器 Verilog 实现检查清单:
- [ ] 浮点系数设计是否满足频率响应指标?
- [ ] 定点量化方案(Q格式)是否合理?量化误差是否可接受?
- [ ] 仿真是否验证了脉冲响应和频率响应(通过 DFT/FFT)?
- [ ] 运算位宽是否足够宽,以防止中间结果溢出?
- [ ] 是否采用了舍入而非简单截断来减少精度损失?
- [ ] 输出是否有饱和处理机制?
- [ ] 在零输入条件下,仿真是否未出现明显的极限环振荡?
- [ ] 综合后的时序报告是否满足要求?
- [ ] 是否在真实信号或更复杂的测试向量下进行了验证?
从理论系数到稳定可靠的硬件实现是一个需要反复迭代和验证的过程。建议先在仿真环境中充分验证功能与性能,再上板调试。利用 MATLAB 或 Python 生成测试向量,并与 Verilog 仿真结果进行对比,是确保设计正确性的高效方法。