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滤波器的,那个又是另一个维度的坑了。