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, btypelow) 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_bits12): # 将系数转为定点整数 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滤波器的那个又是另一个维度的坑了。