☰
IIR滤波器从设计到落地:选型、结构、量化与调试实战
2026/10/12 18:55:13 网站建设 项目流程

1. 从“IIR滤波器(2)”说起:为什么第一篇讲完还得再开一篇

如果你看过我之前写的那篇IIR滤波器入门,大概会记得我们把双线性变换法从头推了一遍,最后落到了一个能跑的低通滤波器上。那篇发出去之后,后台收到最多的一类问题不是“公式怎么推”,而是“我照着搭出来了,但结果跟想象的不一样”——有人发现截止频率对不上,有人发现输出过几个采样点之后开始发散,还有人问“为什么我改了一下系数,滤波器就彻底不能用了”。

这些问题其实都指向同一件事:IIR滤波器的设计流程里,公式推导只是前半程,真正决定它能不能用的,是后半程那些看起来不起眼的细节——系数量化、结构选型、稳定性边界、暂态处理。这些东西在教科书里往往被压缩成一句话带过,但在实际项目里,它们才是让你反复调试的根源。

所以这篇“IIR滤波器(2)”不打算再重复变换公式,而是把重心放在“从设计到落地”这一段。我会围绕四个方向展开:第一,把IIR和FIR放在一起做一次彻底的选型对比,说清楚什么场景下你该选谁;第二,拆解几种主流实现结构,讲明白为什么同样的传递函数,换个结构结果可能完全不同;第三,给出一套可以直接抄的实操流程,从参数计算到代码验证;第四,把我在实际调试中踩过的坑整理成一份速查表。

这篇文章适合已经了解IIR基本概念、但一到实际实现就卡壳的读者。如果你连零极点、传递函数都还没接触过,建议先翻回去看第一篇,把基础补上再回来。如果你已经能独立写出一个滤波器但总在细节上翻车,那这篇就是为你准备的。

2. IIR与FIR的选型对比:别再用“IIR省资源”一句话糊弄自己

2.1 选型的本质是拿什么换什么

很多人对IIR和FIR的区别只有一个模糊印象:IIR用反馈、阶数低、省资源;FIR不用反馈、阶数高、但线性相位。这个印象不算错,但它太粗了,粗到没法指导实际决策。真正做选型的时候,你要问自己的是:我到底愿意拿什么去换什么。

IIR的核心特征是“用反馈换阶数”。它把过去的输出重新引回来参与当前计算,相当于让滤波器自己“记住”了历史信息,所以用很低的阶数就能做出很陡的过渡带。代价是相位非线性,而且反馈意味着稳定性不再是天然保证的,你得时刻盯着极点有没有跑到单位圆外面去。

FIR的核心特征是“用阶数换稳定和线性相位”。它没有反馈,输出只依赖输入,所以永远稳定,而且可以做到严格的线性相位。代价是要达到同样的过渡带陡峭程度,阶数往往要高出一个数量级,计算量和延迟都上去了。

我一般会用一个很朴素的判断:如果这个信号后续还要做波形分析、要做相位对齐、要做多通道相干处理,那相位失真就是致命的,优先考虑FIR;如果只是做能量估计、做包络提取、做实时性要求高的滤波,相位不那么重要,那IIR的低阶数优势就非常香。

2.2 一张表把关键差异摆清楚

下面这张表是我自己整理选型时用的,把几个最关键的维度拉出来对比。注意这里的“阶数”指的是达到相近过渡带宽度时的大致比例,不是绝对数字。

对比维度IIR滤波器FIR滤波器
相位特性非线性,需额外补偿可严格线性相位
稳定性需验证极点位置天然稳定
同性能所需阶数低(约为FIR的1/5到1/10)高
计算量低高
延迟低高(与阶数成正比)
设计复杂度高(需考虑稳定性、量化)低(系数直接对应冲激响应)
对系数量化敏感度高,尤其高阶低
适用场景实时滤波、资源受限、相位不敏感相位敏感、多通道相干、高保真

这张表里我想特别强调“对系数量化敏感度”这一行。IIR滤波器在高阶的时候,系数的微小扰动可能导致极点大幅偏移,甚至直接把系统推到不稳定。这是很多人从浮点仿真转到定点实现时翻车的核心原因,后面我会专门展开讲。

2.3 什么情况下我会坚决选IIR

说几个我实际项目里选IIR的具体场景,你可以对照自己的需求。

第一种是采样率不高、但要求过渡带很窄的情况。比如某个传感器信号采样率只有几百赫兹,我要把工频干扰压下去,过渡带可能只有几赫兹宽。这种条件下如果用FIR,阶数可能要上千,延迟和计算量都受不了,IIR用个四阶六阶就能搞定。

第二种是嵌入式平台算力实在紧张的情况。有些低功耗芯片连乘法器都是复用的,每秒钟能做的乘加运算就那么多次。这时候IIR的低阶数优势直接决定了你能不能实时跑完。

第三种是相位失真可以通过其他手段补偿的情况。比如我在滤波之后还要做一次全通均衡,或者整个系统对相位本来就不敏感,那IIR的非线性相位就不构成障碍。

反过来,如果我要做的是音频重放、要做波束形成、要做任何需要多通道严格对齐的处理,我会毫不犹豫选FIR,哪怕阶数高、延迟大。因为相位失真在这些场景里会直接破坏信号的空间信息或时间信息,后期补不回来。

3. 实现结构决定成败:同样的传递函数,换个结构结果天差地别

3.1 直接I型、直接II型与转置结构

拿到一个传递函数,最直觉的实现方式就是直接按差分方程写。假设传递函数是:

H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 + a1·z⁻¹ + a2·z⁻²)

直接I型就是先算分子部分的延迟链,再算分母部分的延迟链,两套延迟单元各管各的。这种结构最直观,但延迟单元用得多,而且中间变量的动态范围可能很大。

直接II型把两套延迟链合并成一套,因为分子和分母共享同一组延迟信号。这样延迟单元数量减半,是实际实现中最常见的结构。但它的中间节点同时承载了反馈路径和前馈路径的信号,定点实现时更容易溢出。

转置结构是把直接II型的信号流图整个转过来,输入输出互换。它的好处是每个节点的动态范围更可控,而且在高阶时数值特性通常更好。我在做定点实现时,如果直接II型出现溢出,第一反应就是换成转置结构试试。

这三种结构的传递函数完全一样,理论上的频率响应也完全一样,但在有限精度下表现可能差很多。这就是为什么我说“结构决定成败”——你不能只看传递函数对不对,还要看它在你的数值精度下能不能稳定工作。

3.2 级联结构与并联结构:高阶滤波器的正确打开方式

当你需要四阶以上的IIR滤波器时,直接型结构就开始出问题了。原因很简单:高阶多项式的根对系数极其敏感,系数量化的一点点误差就可能让极点跑出单位圆。

正确的做法是把高阶传递函数分解成多个二阶节的乘积,这就是级联结构。每个二阶节单独实现,单独验证稳定性,最后串起来。这样做的好处是每个二阶节的极点位置只受自己那几个系数影响,量化误差不会跨节累积。

分解的方法通常是先求分子分母的根,然后把共轭复根配对成二阶节。配对的时候有个技巧:把距离单位圆最近的极点跟距离最近的零点配在一起,这样可以让每个二阶节的增益曲线更平缓,减少中间节点的动态范围。

并联结构是把传递函数拆成多个部分分式之和,每个部分分式单独实现,最后相加。它的好处是各个节之间完全独立,一个节出问题不会影响其他节。但并联结构对系数量化更敏感,而且需要更多的乘法器,所以实际中用得比级联少。

我自己的习惯是:四阶到八阶用级联,八阶以上如果非要IIR,会考虑并联或者更复杂的结构。但说实话,超过八阶的IIR在实际项目中很少见,因为这时候FIR的阶数虽然高,但稳定性和可预测性带来的好处往往更划算。

3.3 系数量化:从浮点到定点的惊险一跃

这是IIR实现里最容易翻车的地方,没有之一。你在浮点仿真里跑得好好的滤波器,一旦把系数截断成定点,可能直接就不工作了。

问题的根源在于:IIR滤波器的极点位置对系数极其敏感。一个二阶节的极点由a1和a2两个系数决定,当极点靠近单位圆时,a1和a2的微小变化会导致极点大幅移动。如果量化后的极点跑到单位圆外面,系统就发散了。

我一般会做三件事来降低风险。第一,在浮点阶段就把系数打印出来,看看极点的模值离1有多远。如果某个极点的模值是0.999,那量化几乎必然出问题,这时候要么降低滤波器的选择性,要么提高系数的位宽。

第二,用比预期更高的位宽来存系数。比如我最终数据通路是16位,但系数我会用24位甚至32位来存。系数占用的存储空间很小,多给几位不会显著增加成本,但能大幅降低量化风险。

第三,在定点实现之后,一定要做一次冲激响应测试。给一个单位冲激,看输出是不是在有限步内衰减到零。如果输出持续振荡或者发散,那就是极点出问题了,需要回头调整系数位宽或者滤波器结构。

注意:系数量化后的稳定性验证不能只看频响曲线。频响曲线在量化后可能看起来还行,但时域冲激响应已经发散了。一定要做时域测试。

4. 从参数到代码:一套可以直接抄的实操流程

4.1 第一步:明确指标,别急着写代码

我见过太多人一上来就开始调系数,调了半天发现指标根本没定义清楚。在动手之前,你必须先回答这几个问题:

  • 采样率是多少?这决定了归一化频率的基准。
  • 通带截止频率和阻带截止频率分别是多少?过渡带有多宽?
  • 通带允许的最大衰减是多少分贝?阻带需要的最小衰减是多少分贝?
  • 相位有没有要求?如果有,能不能接受非线性相位?
  • 目标平台的数值精度是多少?浮点还是定点?定点的话位宽是多少?

这几个问题里,最容易被忽略的是最后一个。很多人用浮点设计完直接往定点平台上搬,结果就是前面说的量化翻车。我的建议是:在设计阶段就用目标平台的精度来做仿真,哪怕慢一点,也比后期返工强。

4.2 第二步:确定滤波器阶数

阶数的确定有两种路子。一种是查表法,根据通带阻带指标和过渡带宽度,查巴特沃斯、切比雪夫或椭圆滤波器的阶数对照表。另一种是直接用工具算,比如在Python里用scipy的cheb1ord、cheb2ord、ellipord这些函数,输入指标就能返回最小阶数。

我一般会先用工具算一个理论最小阶数,然后往上加一阶作为余量。因为理论阶数是在理想条件下的结果,实际实现时由于量化、结构等因素,往往需要稍微放宽一点。

选择滤波器类型的时候,巴特沃斯最平缓但过渡带最宽,切比雪夫有通带或阻带纹波但过渡带更窄,椭圆滤波器过渡带最窄但通带阻带都有纹波。如果对纹波敏感就选巴特沃斯,如果对过渡带要求苛刻就选椭圆。切比雪夫介于两者之间,是我用得最多的。

4.3 第三步:计算系数并检查极点

确定阶数和类型之后,就可以算系数了。以Python为例,假设我要设计一个采样率1000Hz、通带截止100Hz、阻带截止150Hz、通带衰减1dB、阻带衰减40dB的低通滤波器:

import numpy as np from scipy import signal fs = 1000.0 wp = 100.0 / (fs / 2) ws = 150.0 / (fs / 2) gpass = 1.0 gstop = 40.0 # 计算椭圆滤波器的最小阶数 N, Wn = signal.ellipord(wp, ws, gpass, gstop) print(f"最小阶数: {N}") # 设计滤波器 b, a = signal.ellip(N, gpass, gstop, Wn, btype='low') print(f"分子系数: {b}") print(f"分母系数: {a}") # 检查极点 poles = np.roots(a) print(f"极点模值: {np.abs(poles)}")

跑完这段代码,你会得到一组系数和极点模值。关键看极点模值那一行:如果所有模值都明显小于1,比如小于0.99,那量化风险就比较低;如果有模值非常接近1,比如0.999以上,那就要小心了。

4.4 第四步:分解成二阶节

对于四阶以上的滤波器,直接使用b和a数组在数值上是不稳定的。需要把它分解成二阶节:

# 分解成二阶节 sos = signal.tf2sos(b, a) print("二阶节系数:") print(sos)

tf2sos函数会自动完成极零点配对和增益分配。输出的sos是一个N×6的数组,每一行是一个二阶节的[b0, b1, b2, a0, a1, a2]。a0通常归一化为1。

拿到sos之后,我建议逐个二阶节检查极点模值。如果某个节的极点模值特别接近1,可以考虑调整配对方式,或者对这个节单独提高系数精度。

4.5 第五步:定点仿真验证

这一步是很多人跳过的,但恰恰是最重要的。在把系数写进实际代码之前,先用定点仿真跑一遍。

具体做法是:把每个系数乘以2的Q次方然后取整,再用这些整数系数做滤波运算,每次乘加之后做移位和截断。比较定点输出和浮点输出的差异,看是否在可接受范围内。

def fixed_point_filter(x, sos, frac_bits=12): # 将系数转为定点整数 sos_fixed = np.round(sos * (2**frac_bits)).astype(np.int64) # 逐样本处理 # 这里省略具体实现,核心是模拟移位和截断 pass

如果定点输出和浮点输出的差异超过预期,说明系数位宽不够,需要增加frac_bits。如果增加位宽之后仍然有问题,那可能是结构选得不对,需要换转置结构或者级联顺序。

4.6 第六步:时域和频域双重验证

最后一步是验证。频域验证就是看幅频响应和相频响应是否符合指标,这个用工具很容易做。时域验证是给一个冲激信号,看输出是否在有限步内衰减到零。

我一般会做三个测试:冲激响应测试稳定性,阶跃响应测试暂态行为,正弦扫频测试频率选择性。三个都过了,才认为滤波器可以交付。

提示:冲激响应测试的时候,不要只看前几十个点。有些滤波器的暂态过程很长,要跑到输出幅度降到初始值的万分之一以下才算稳定。

5. 常见问题与排查技巧实录

5.1 输出发散:九成是极点跑到单位圆外了

这是最典型的问题。现象是滤波器刚开始工作还正常,过一段时间输出越来越大,最后饱和或者溢出。

排查思路很直接:把系数拿出来,算极点模值。如果模值大于等于1,那就是量化导致的极点偏移。解决办法有三个:提高系数位宽、降低滤波器阶数、换用对量化不敏感的结构。

我遇到过一次特别隐蔽的情况:浮点系数算出来的极点模值是0.998,看起来没问题,但定点量化之后变成了1.0003。后来把系数位宽从16位提到24位,问题就解决了。所以我现在养成了一个习惯:只要极点模值超过0.99,就默认用24位以上的系数位宽。

5.2 截止频率对不上:检查归一化频率的定义

这个问题也很常见。你设计的是100Hz截止,实际测出来可能是80Hz或者120Hz。

原因通常是归一化频率的定义搞混了。有的工具用fs/2做归一化基准,有的用fs。比如scipy的iirfilter函数,Wn参数如果小于1,就认为是归一化到Nyquist频率的;如果大于1,就认为是实际频率。这个细节不搞清楚,截止频率就会差一倍。

我的做法是:不管用什么工具,设计完之后一定用freqz函数把幅频响应画出来,在图上直接读-3dB点的频率,跟设计指标对比。图上看一眼比算半天都靠谱。

5.3 暂态过程太长:初始条件没设对

IIR滤波器是有记忆的,如果初始状态设为零,而输入信号一开始就有较大的值,输出会有一个明显的暂态过程。

解决办法是设置合适的初始条件。对于阶跃输入,可以预先计算稳态输出,把延迟单元初始化成对应的值。对于一般信号,可以用前几个样本做一次“预热”,等滤波器状态稳定了再开始正式输出。

我在做实时音频处理的时候,会在正式处理之前先送一段静音或者已知信号,让滤波器状态收敛。这段预热时间通常是滤波器阶数的几倍,具体多少要看极点离单位圆有多近。

5.4 定点溢出:中间节点动态范围没算清楚

定点实现时,中间节点的值可能超出预期范围,导致溢出。溢出之后结果完全错误,而且往往不会报错,只是输出变得乱七八糟。

避免溢出的方法是提前做动态范围分析。对于级联结构,每个二阶节的增益要单独算,确保信号经过每一节之后不会超出表示范围。如果某一节增益太大,可以在节之间插入缩放因子。

我一般会在每个二阶节后面加一个可选的移位操作,相当于除以2的整数次幂。这样可以在不改变滤波器整体特性的前提下,把信号幅度控制在安全范围内。

5.5 常见问题速查表

现象最可能原因排查方法解决手段
输出发散极点量化后出单位圆计算量化后极点模值提高系数位宽、降阶、换结构
截止频率偏差归一化频率定义混淆画幅频响应读-3dB点统一归一化基准
暂态过长初始条件为零观察前N点输出预热或设初始状态
定点溢出中间节点动态范围超限逐节检查最大值插入缩放因子
频响异常二阶节配对不当逐节画频响重新配对极零点
输出有直流偏置系数不满足直流增益检查H(1)的值调整系数或加隔直

5.6 几个我踩过的坑

第一个坑是“浮点仿真没问题就以为万事大吉”。有一次我做完浮点仿真直接交给同事做定点实现,结果他那边跑出来完全不对。后来发现是系数量化的问题,但已经浪费了两天时间。从那以后,我坚持在浮点阶段就做定点仿真。

第二个坑是“忽略二阶节的顺序”。级联结构里,哪个节在前哪个节在后,对中间节点的动态范围影响很大。我一般会把增益最小的节放在最前面,增益最大的放在最后面,这样中间信号不容易溢出。

第三个坑是“用错滤波器类型”。有一次我需要一个过渡带很窄的滤波器,随手选了巴特沃斯,结果算出来阶数高得离谱。后来换成椭圆滤波器,阶数直接降了一半。所以选类型之前一定要想清楚,你到底更在意纹波还是更在意过渡带。

第四个坑是“忘记考虑群延迟”。IIR滤波器的群延迟在通带内不是常数,靠近截止频率的地方群延迟会急剧上升。如果你的系统对时序敏感,这个群延迟变化可能会导致问题。我现在的做法是:设计完之后把群延迟曲线也画出来,看看在信号带宽内变化有多大。

6. 写在最后:一些个人体会

IIR滤波器这个东西,入门容易精通难。公式推导那一关过了之后,真正的挑战才刚开始。我做了这么多年信号处理,到现在每次设计IIR滤波器,还是会老老实实做定点仿真、做冲激响应测试、做动态范围分析。这些步骤看起来很繁琐,但比起后期调试时抓瞎,前期多花半个小时是绝对值得的。

如果你现在正在调一个IIR滤波器,怎么调都不对,我的建议是:先停下来,把极点模值算一遍,把定点仿真跑一遍,把冲激响应看一遍。这三个动作做完,大部分问题都能定位到。如果还是不行,那就把滤波器阶数降下来,从二阶开始重新搭,确认二阶没问题了再往上加。一步一步来,比一口气吃成胖子靠谱得多。

后续如果大家感兴趣,我可以再写一篇讲自适应IIR滤波器的,那个又是另一个维度的坑了。

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

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

立即咨询