IIR滤波器Verilog实现:从MATLAB仿真到FPGA硬件的稳定设计
2026/7/19 3:36:11 网站建设 项目流程

前几天有个做 FPGA 信号处理的朋友问我:“IIR 滤波器在 MATLAB 里仿真效果很好,但一用 Verilog 实现就出问题,要么不稳定,要么资源占用太高,到底该怎么写才能既稳定又高效?” 这个问题其实挺典型的——很多从算法转到硬件的工程师都会卡在 IIR 数字滤波器的 Verilog 实现上。IIR 滤波器因为反馈结构的存在,在硬件实现时需要考虑量化误差、稳定性、流水线冲突等一系列在软件仿真中不会暴露的问题。

如果你也在用 Verilog 写 IIR 滤波器,可能会发现单看理论公式很简单,但真正把差分方程转化成可综合、可稳定工作的硬件电路时,处处是坑。这篇文章不会只给你一段“看起来能工作”的代码,而是会从 IIR 滤波器的核心特性出发,解释为什么直接实现会出问题,以及如何通过结构选择、量化策略和流水线设计来避开这些坑。

1. 先搞清楚 IIR 滤波器在硬件里真正难在哪里

IIR(Infinite Impulse Response)滤波器和 FIR(Finite Impulse Response)最大的区别就是有反馈回路。这个反馈让 IIR 可以用较低的阶数实现尖锐的频响,但同时也带来了硬件实现上的三个核心挑战。

1.1 量化误差会被反馈回路放大

在 MATLAB 里,你用的都是双精度浮点数,系数可以精确到小数点后十几位。但在 FPGA 里,你需要把系数量化成有限的位数(比如 12bit、16bit)。这个量化误差在 FIR 滤波器里影响相对较小,因为 FIR 没有反馈。

但在 IIR 里,每次计算的误差都会通过反馈回路累积起来。如果滤波器的极点靠近单位圆,这种累积可能导致输出饱和或极限环振荡。这就是为什么在 MATLAB 里完美的设计,到硬件上可能完全变样。

// 直接型结构的危险示例(不推荐实际使用) always @(posedge clk) begin if (rst) begin y <= 0; end else begin // y[n] = b0*x[n] + b1*x[n-1] + ... - a1*y[n-1] - a2*y[n-2] - ... y <= b0 * x + b1 * x_d1 + b2 * x_d2 - a1 * y_d1 - a2 * y_d2; end end

这种直接I型实现看起来直观,但对量化误差极其敏感。

1.2 反馈回路限制了时序性能

在 FIR 滤波器中,你可以通过插入流水线寄存器来提升工作频率,因为 FIR 是纯粹的前馈结构。但 IIR 的反馈回路创造了一个组合逻辑的闭环:当前的输出依赖于之前的输出。

这意味着你不能在反馈路径上随意插入流水线寄存器,否则会破坏时序关系。如果滤波器的采样率要求很高,这个反馈回路可能成为时序瓶颈。

1.3 稳定性判断在硬件中更复杂

软件中的稳定性可以通过计算极点位置来判断,所有极点都在单位圆内就稳定。但在硬件中,由于量化效应,原本在单位圆内的极点可能会被"推"到单位圆外。

更麻烦的是,某些情况下滤波器会在量化误差的影响下产生极限环振荡——即使输入为零,输出也会在小范围内持续振荡。这种现象在理论分析中很难预测,只有在实际硬件测试中才会暴露。

2. 选择适合硬件的滤波器结构比优化代码更重要

面对上述挑战,直接照搬教科书上的直接型结构通常不是好选择。你需要根据具体需求选择更适合硬件实现的结构。

2.1 二阶节级联:最实用的折中方案

将高阶滤波器分解为多个二阶节(Biquad)级联,是硬件实现中最常用的方法。每个二阶节可以独立处理,降低了设计复杂度,也减少了量化误差的敏感性。

// 单个二阶节的直接II型转置结构 module iir_biquad #( parameter WIDTH = 16, parameter COEF_WIDTH = 12 )( input clk, input rst, input signed [WIDTH-1:0] x_in, output signed [WIDTH-1:0] y_out ); // 系数定义(需要根据具体滤波器设计确定) wire signed [COEF_WIDTH-1:0] b0, b1, b2, a1, a2; // 状态寄存器 reg signed [WIDTH-1:0] s1, s2; reg signed [WIDTH-1:0] x_d1, x_d2; // 中间计算结果(需要足够的位宽防止溢出) wire signed [WIDTH+COEF_WIDTH:0] sum1, sum2; always @(posedge clk) begin if (rst) begin s1 <= 0; s2 <= 0; x_d1 <= 0; x_d2 <= 0; end else begin // 更新状态寄存器 s1 <= sum1[WIDTH+COEF_WIDTH:COEF_WIDTH]; // 截断到合适位宽 s2 <= sum2[WIDTH+COEF_WIDTH:COEF_WIDTH]; x_d1 <= x_in; x_d2 <= x_d1; end end // 组合逻辑计算 assign sum1 = x_in * b0 + s2; assign sum2 = x_d1 * b1 - s1 * a1 + s2; assign y_out = s1; endmodule

这种结构的优点是每个二阶节相对独立,便于测试和调试。如果某个二阶节出现问题,可以单独调整而不影响整个滤波器。

2.2 一阶节并联:更好的数值稳定性

对于高阶滤波器,并联结构(将滤波器分解为一阶或二阶节的并联组合)通常有更好的数值特性。因为每个节的误差不会传播到其他节,减少了误差累积。

不过并联结构的缺点是设计稍微复杂,需要完成部分分式分解。在实际工程中,除非对数值稳定性有极高要求,否则级联结构通常足够用。

2.3 直接型与级联型的对比选择

为了更直观地展示不同结构的优劣,这里有一个实用对比表:

特性直接型级联型并联型
硬件资源最省中等较多
数值稳定性最好
设计复杂度简单中等复杂
可调试性
适合场景低阶滤波器(1-3阶)通用选择高精度要求

对于大多数应用,我从经验上建议:4阶及以下可以考虑直接型,4阶以上强烈推荐级联型,只有在对数值误差极其敏感的应用中才考虑并联型。

3. 量化策略:决定滤波器实际性能的关键细节

系数和数据的量化方式直接影响滤波器的频率响应和稳定性。这里有几个容易忽略但至关重要的细节。

3.1 系数量化不能简单截断

MATLAB 生成的系数通常需要量化到硬件支持的位宽。最简单的办法是乘以 2^n 后取整,但这样可能改变滤波器的特性。

更好的做法是使用频率响应验证:量化前后在 MATLAB 中对比频响曲线,确保通带、阻带特性没有明显恶化。

% MATLAB 中的系数量化验证示例 [b_float, a_float] = butter(4, 0.2); % 原始浮点系数 scale = 2^11; % 11位小数位 b_fixed = round(b_float * scale) / scale; a_fixed = round(a_float * scale) / scale; % 比较频响 freqz(b_float, a_float); hold on; freqz(b_fixed, a_fixed); legend('浮点', '定点');

如果量化后频响变化太大,可能需要增加位数或选择对量化不敏感的滤波器结构。

3.2 中间计算结果需要足够的位宽

在定点运算中,乘法结果的位宽是操作数位宽之和。如果每一步都直接截断,会引入大量误差。

// 错误的做法:过早截断 wire signed [15:0] result = (a * b) >> 8; // 立即截断 // 正确的做法:保持足够位宽,最后统一截断 wire signed [31:0] temp = a * b; // 保持全精度 // ... 经过多个计算步骤 ... wire signed [15:0] final_result = accumulated_temp >>> 16; // 最后截断

一般规则是:内部计算位宽应该是输入位宽的 2 倍加上系数位宽,最后根据输出需求截断。

3.3 饱和处理与溢出保护

IIR 滤波器可能因为特定输入而产生很大的中间值,即使最终输出在正常范围内。需要在关键位置添加饱和处理:

// 饱和处理函数 function signed [15:0] saturate; input signed [31:0] value; begin if (value > 32767) saturate = 32767; else if (value < -32768) saturate = -32768; else saturate = value[15:0]; end endfunction // 在关键计算点应用饱和处理 wire signed [31:0] raw_result = a * b + c * d; wire signed [15:0] safe_result = saturate(raw_result);

4. 时序与流水线:在速度与稳定性间找到平衡

反馈回路的存在让 IIR 滤波器的流水线设计变得棘手,但并非完全没有优化空间。

4.1 仔细分析关键路径

使用综合工具的时序报告识别关键路径。如果乘法器是瓶颈,可以考虑:

  • 使用 FPGA 内置的 DSP 块
  • 将大位宽乘法拆分为多个小乘法
  • 使用 Booth 编码等优化算法

4.2 有限度的流水线优化

虽然不能在反馈回路中插入流水线,但可以在前馈路径中优化:

// 有限流水线优化示例 module pipelined_biquad #( parameter WIDTH = 16 )( input clk, rst, input signed [WIDTH-1:0] x_in, output signed [WIDTH-1:0] y_out ); // 第一级流水:输入寄存器 reg signed [WIDTH-1:0] x_reg; reg signed [WIDTH-1:0] s1_reg, s2_reg; // 第二级流水:乘法器输出 wire signed [2*WIDTH:0] prod1, prod2, prod3, prod4; reg signed [2*WIDTH:0] prod1_reg, prod2_reg, prod3_reg, prod4_reg; // 第三级流水:加法器树输出 wire signed [2*WIDTH+2:0] sum; reg signed [WIDTH-1:0] y_reg; always @(posedge clk) begin if (rst) begin // 复位所有寄存器 x_reg <= 0; s1_reg <= 0; s2_reg <= 0; prod1_reg <= 0; // ... 其他寄存器复位 end else begin // 流水线推进 x_reg <= x_in; prod1_reg <= prod1; // ... 其他寄存器更新 // 反馈状态更新(注意这里不能流水!) s1_reg <= sum[2*WIDTH+2:WIDTH+2]; // 适当截断 s2_reg <= s1_reg; end end // 组合逻辑 assign prod1 = x_reg * b0; assign prod2 = s2_reg * b2; // ... 其他乘法 assign sum = prod1_reg + prod2_reg + prod3_reg - prod4_reg; // 注意减法对应反馈项 assign y_out = y_reg; endmodule

这种设计在反馈路径上保持单周期延迟,但在前馈路径上使用流水线,可以在不破坏稳定性的前提下提升频率。

4.3 时序约束与时钟域考虑

如果滤波器需要处理跨时钟域的数据,需要添加适当的同步电路:

// 输入数据同步 reg [WIDTH-1:0] sync1, sync2, sync3; always @(posedge clk) begin sync1 <= async_input; sync2 <= sync1; sync3 <= sync2; end // 使用 sync3 作为稳定的输入

同时,在综合约束文件中添加适当的时序约束,确保工具能够优化关键路径。

5. 测试与调试:确保滤波器在实际环境中可靠工作

写完 Verilog 代码只是第一步,充分的测试才能保证滤波器在实际应用中稳定工作。

5.1 分层测试策略

模块级测试:每个二阶节单独测试,验证基本功能

  • 输入单位脉冲,检查冲击响应
  • 输入直流信号,验证增益
  • 输入正弦波,验证频率响应

系统级测试:整个滤波器链测试

  • 扫频测试,测量实际频响
  • 噪声输入测试,检查稳定性
  • 边界值测试,检查溢出处理

5.2 实际信号测试用例

不要只满足于仿真测试,在 FPGA 上验证实际信号:

// 简单的测试信号生成 reg [15:0] test_counter; reg signed [15:0] test_signal; always @(posedge clk) begin test_counter <= test_counter + 1; // 生成正弦测试信号 test_signal = amplitude * $sin(2 * 3.14159 * frequency * test_counter / sampling_rate); end

5.3 性能监控与调试接口

在实际系统中添加调试接口,便于问题诊断:

// 可选的调试输出 output reg [31:0] debug_overflow_count; output reg [15:0] debug_max_value; always @(posedge clk) begin if (y_out == 16'h7FFF || y_out == 16'h8000) debug_overflow_count <= debug_overflow_count + 1; if ($abs(y_out) > debug_max_value) debug_max_value <= $abs(y_out); end

6. 从单滤波器到可配置滤波器的演进

在实际项目中,经常需要支持多种滤波器配置。这时候就需要设计参数化的滤波器架构。

6.1 系数可重载设计

module configurable_iir #( parameter NUM_STAGES = 2, parameter WIDTH = 16, parameter COEF_WIDTH = 12 )( input clk, rst, input config_enable, input [3:0] config_stage, // 配置哪个二阶节 input [COEF_WIDTH-1:0] config_b0, config_b1, config_b2, config_a1, config_a2, input signed [WIDTH-1:0] x_in, output signed [WIDTH-1:0] y_out ); // 系数存储器 reg [COEF_WIDTH-1:0] b0[0:NUM_STAGES-1], b1[0:NUM_STAGES-1], b2[0:NUM_STAGES-1]; reg [COEF_WIDTH-1:0] a1[0:NUM_STAGES-1], a2[0:NUM_STAGES-1]; // 系数更新逻辑 always @(posedge clk) begin if (config_enable) begin b0[config_stage] <= config_b0; b1[config_stage] <= config_b1; // ... 其他系数更新 end end // 实例化多个二阶节 generate genvar i; for (i = 0; i < NUM_STAGES; i = i + 1) begin: stage iir_biquad #(.WIDTH(WIDTH), .COEF_WIDTH(COEF_WIDTH)) biquad_inst ( .clk(clk), .rst(rst), .x_in(i == 0 ? x_in : stage[i-1].y_out), .y_out(stage[i].y_out), .b0(b0[i]), .b1(b1[i]), .b2(b2[i]), .a1(a1[i]), .a2(a2[i]) ); end endgenerate assign y_out = stage[NUM_STAGES-1].y_out; endmodule

这种设计允许在运行时改变滤波器特性,适合需要动态调整的应用场景。

6.2 自适应滤波器初步思路

对于更高级的应用,可以考虑自适应 IIR 滤波器。虽然实现复杂,但基本思路是在原有结构上添加系数更新逻辑:

// 简化的 LMS 自适应算法实现片段 always @(posedge clk) begin // 计算误差:期望信号与实际输出之差 error <= desired_signal - y_out; // 根据误差更新系数(需要适当的步长控制) b0 <= b0 + mu * error * x_in; b1 <= b1 + mu * error * x_delayed_1; // ... 其他系数更新 end

自适应滤波器对数值精度和稳定性要求更高,需要更仔细的设计和测试。

真正可靠的 IIR 滤波器实现不是一蹴而就的,需要经过仿真、板级测试、实际信号验证等多个阶段的迭代优化。关键是要理解算法与硬件之间的差距,并在设计初期就考虑数值稳定性、时序约束和测试方案。从简单的定点仿真开始,逐步推进到硬件实现,每一步都验证正确性,这样才能构建出既满足性能要求又稳定可靠的数字滤波器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询