
1. 从一段真实经历说起为什么 FIR 设计没那么简单我接过不少数字信号处理相关的任务设计 FIR 滤波器算是其中出现频率最高的需求之一。很多人一开始觉得这事简单MATLAB 里一行fir1或者 Python 里一句signal.firwin就完事了。但实际做起来尤其是把滤波器真正落到工程里、和系统其他模块对接的时候各种问题就全冒出来了通带纹波超标、阻带衰减不够、群延迟导致时序对不上、定点化之后性能劣化……每个坑都够你折腾半天的。这篇文章想把“设计 FIR 滤波器”这件事完整地拆开讲一遍。目标不是教你怎么抄代码而是让你理解设计思路、参数选择背后的原理以及在工程中真正落地时需要盯住的细节。适合刚接触 FIR 设计、被各种概念绕晕的同学也适合已经会用工具但总感觉“差点意思”的工程师。先交代一下 FIR 滤波器能做什么。简单说它是一种数字滤波器输入输出关系只取决于当前和过去的输入样本因为它的单位脉冲响应是有限长的所以叫“有限长单位脉冲响应”英文 FIRFinite Impulse Response。它能完成的工作包括从信号里挑出你想要的频率成分、滤掉噪声干扰、对信号做整形和预加重再复杂一点还能做匹配滤波、微分器、希尔伯特变换这类特殊用途。它最大的优点是可以在整个频带范围内做到严格线性相位——这意味着信号经过滤波器后不同频率成分的延迟保持一致波形除了幅度变化之外不会发生相位畸变。这一点在通信系统、音频处理、生物医学信号分析等对相位敏感的场合是致命的。这篇文章会从 FIR 和 IIR 的选型逻辑讲起逐步拆解设计指标怎么定、窗函数怎么选、阶数怎么估算给出可以直接参考的 MATLAB 和 Python 实现代码。最后用一套完整的低通滤波器设计案例把从指标到实现、再到验证排查的全流程串起来。你在实操中遇到的大多数问题应该都能在文章里找到对应的解决方案。2. 设计之前的核心抉择FIR 还是 IIR线性相位到底有多重要2.1 FIR 与 IIR 的取舍逻辑很多人一上来就问“哪个滤波器好”这个问题本身就不成立。FIR 和 IIR 各有完全不同的性格选谁取决于你的应用场景。IIR 滤波器的典型特征是传递函数有极点因此可以用很低的阶数实现非常陡峭的过渡带计算量也小。但它的相位响应是非线性的频率越高信号各成分之间的相位关系会被扭曲。FIR 滤波器则恰好相反没有极点全部是零点同样的指标往往需要高很多的阶数但换来的是可以设计成严格线性相位。我在实际项目里的大致选择标准是这样的如果系统对相位不敏感比如只是做能量检测、电平测量或者信号本身是宽带随机噪声IIR 往往更划算省 FPGA 资源、省运算时间。但如果信号是语音、心电、振动这类需要保真的波形或者后级要做相干解调、相关检测那就老老实实选 FIR线性相位在这一类系统里几乎等于生命线。举一个我实际处理过的例子一套心电信号预处理系统目标是从工频干扰和肌电噪声中恢复出干净的 PQRST 波形。一开始图省事用了四阶的巴特沃斯带通滤波器波形放大之后能看到明显的相位畸变——QRS 波的峰值位置产生了偏移。心电诊断里ST 段的抬升幅度、QT 间期的长度都是直接关系到临床判断的指标波形相位一旦变了这些测量结果全都不准。后来换成等阶数的 FIR 滤波器之后波形形状终于和原始信号吻合各种间期测量也变得稳定了。从那以后凡是涉及医学信号的项目我基本无脑上线性相位 FIR。2.2 线性相位的本质和四种 FIR 类型FIR 实现线性相位的原理并不复杂核心是让滤波器系数满足对称性。一个长度为 N 的 FIR 滤波器如果系数满足h[n] h[N-1-n]偶对称或者 h[n] -h[N-1-n]奇对称那么它的相位响应就是频率的线性函数群延迟恒定等于 (N-1)/2 个采样周期。这个恒定群延迟就是 FIR 最宝贵的财产不管你输入的信号是什么样滤波后各个频率成分抵达的时间完全一致。线性相位 FIR 按照长度奇偶和对称类型可以分成四种类型I 型N 为奇数系数偶对称最通用可以设计任意形式的滤波器包括低通、高通、带通、带阻。II 型N 为偶数系数偶对称频率响应在 π即奈奎斯特频率处必然为零所以不能用来设计高通或带阻只能做低通和带通。III 型N 为奇数系数奇对称频率响应在 0 和 π 处都为零最适合做希尔伯特变换器和微分器。IV 型N 为偶数系数奇对称频率响应在 0 处为零适合做希尔伯特变换器和微分器但不能做普通低通。这是新手最常忽略的约束。我见过有人用fir1设计了一个偶数阶高通滤波器仿真结果怎么调都不对后来发现是忘了 II 型 FIR 在高频端有天然的系统零点。所以在定指标之前先想清楚你是要低通还是高通滤波器长度打算取奇数还是偶数这两点直接决定了可行性边界。2.3 工程场景下的选型心法我的建议是不必迷信“FIR 一定比 IIR 好”这种论调。音频混响器、均衡器这类创意类处理很多时候非线性相位的“染色”反而成了特色IIR 是标配。通信基带的成形滤波器、匹配滤波器对相位一致性和码间串扰有苛刻要求必须用 FIR。低功耗的嵌入式传感器节点计算资源和内存都紧张IIR 往往是唯一现实的选择。一句话总结先看信号的用途再谈滤波器的结构。相位敏感选 FIR资源紧张选 IIR中间地带根据你的实现平台和指标要求权衡。3. 设计指标的制定和参数估算这一步错了后面全白干3.1 频域指标到底在描述什么设计 FIR 滤波器之前必须先把一组明确的技术指标写下来。这些指标不是随便拍的它们直接决定滤波器的阶数和性能边界。最重要的四个参数是通带截止频率 ωp在这个频率以下信号被保留幅度误差控制在通带纹波范围内。阻带截止频率 ωs在这个频率以上信号被衰减衰减量至少达到阻带衰减指标。通带纹波 δ1通带内允许的幅度波动。用 dB 表示时通常很小0.1dB 到 1dB 是常见取值。阻带最小衰减 As阻带内允许的最大幅度典型指标是 40dB、60dB甚至更高。这几个参数的关系是通带到阻带的过渡带越窄滤波器阶数就需要越高纹波要求越严格阶数也会相应上升。工程上有一句经验之谈——每减少过渡带宽度一半需要的滤波器阶数大约翻倍。所以指标定得越“苛刻”你在硬件上付出的资源代价就越大。设计好的指标是在性能需求和成本之间找到平衡点。3.2 阶数估算的经验公式与查表法在选窗函数设计法时滤波器阶数 N 有一个非常实用的经验估算公式。经典教材里常引用的公式是N ≈ (As - 7.95) / (2.285 × Δω)其中 Δω 是过渡带宽度归一化频率As 是阻带衰减的 dB 值。这个公式来源于凯泽窗的逼近特性对大多数窗函数设计都能给出合理的起点。举个例子要求阻带衰减 60dB采样率 16kHz通带截止 3.4kHz阻带截止 4.6kHz那过渡带宽度就是 (4.6-3.4)/16 0.075归一化到采样频率。代入公式N ≈ (60 - 7.95) / (2.285 × 0.075) ≈ 303.5取偶数为 304。如果要求更高的阻带衰减 80dB同样过渡带下N ≈ (80 - 7.95) / (2.285 × 0.075) ≈ 420.2取 422。能看到规律吗阻带衰减要求提高 20dB阶数增加了 120 多阶。所以我在项目前期最常做的事情之一就是先拿这个公式估算一下看看阶数是否超出了系统的计算预算。如果在 FPGA 上跑这会直接决定你要用多少 DSP 乘法器资源如果在 DSP 上跑它决定你要分配多少程序周期给滤波函数。3.3 窗函数选择的逻辑没有免费的午餐窗函数设计法的本质是将理想滤波器的无限长脉冲响应截断并加权。理想低通的脉冲响应是 sinc 函数它是无限长的直接截断会产生吉布斯效应——阻带纹波和通带振铃。加窗就是为了抑制这种截断带来的泄漏。选窗函数的核心权衡是主瓣越宽过渡带越宽旁瓣越低阻带衰减越大但没办法两者兼得。实际工程里常用的窗函数和它们的关键特性如下矩形窗旁瓣衰减仅 -21dB过渡带最窄但效果太差几乎不用于实际滤波器设计。汉宁窗旁瓣衰减 -44dB过渡带宽度约为 3.1ΔfΔf 为频率分辨率适合过渡带要求不高、衰减 40~50dB 的场景。海明窗旁瓣衰减 -53dB过渡带宽度约为 3.3Δf和汉宁窗属于同一梯队但阻带衰减略好。布莱克曼窗旁瓣衰减 -74dB过渡带宽度约为 5.5Δf适合要求高衰减、过渡带宽裕量较大的场景。凯泽窗通过参数 β 连续调节旁瓣衰减β 越大衰减越大但过渡带越宽是工程上最灵活的选择。凯泽窗的 β 参数有近似经验公式β 0.1102(As - 8.7)适用于 As 50dB 的情况。如果 As 在 30 到 50dB 之间β 的公式略有不同会在下文实际案例中给出。值得注意的是窗函数的阻带衰减是以牺牲过渡带为代价换来的。很多人指标里同时要求过渡带极窄、阻带衰减极高这种情况下窗函数法会非常吃力你可能要转向最优设计法等纹波设计Parks-McClellan 算法后面会有专门的讨论。4. 三种主流设计方法拆解窗函数法、频率采样法与最优设计4.1 窗函数法最直观也最容易被坑窗函数法的流程可以概括为五步根据指标确定理想滤波器的幅度响应和截止频率通常是理想低通的边界频率取通带和阻带截止频率的中点。对理想频率响应做逆离散时间傅里叶变换得到无限长脉冲响应对低通就是一个 sinc 序列。根据阻带衰减要求选择合适的窗函数。根据过渡带宽度要求估算滤波器长度 N。将理想脉冲响应与窗函数相乘得到最终 FIR 系数。用 MATLAB 实现时代码非常简洁fc 3800; % 截止频率 3.8kHz fs 16000; % 采样率 16kHz N 304; % 阶数由估算公式确定 win kaiser(N1, 4.82); % 凯泽窗beta 值根据阻带衰减确定 b fir1(N, fc/(fs/2), low, win); freqz(b, 1, 1024, fs); % 绘制频率响应验证Python 版本用scipy.signal也一样方便import numpy as np from scipy.signal import firwin, freqz fs 16000 fc 3800 N 304 beta 4.82 taps firwin(N1, fc, fsfs, window(kaiser, beta)) freqz(taps, fsfs)这段代码里需要重点理解的是fc/(fs/2)这个归一化操作。MATLAB 和 SciPy 里要求截止频率是相对于奈奎斯特频率的归一化值奈奎斯特频率是采样率的一半。无数人忘掉这个归一化直接把频率塞进去结果滤波器行为完全不对。我见过有人在 Python 里用firwin(65, 2000, fs10000)设计通带 2kHz 的滤波器看起来正常但切换到 MATLAB 时直接把2000代入归一化参数滤波器通带变成了 10kHz完全错乱。设计前务必想清楚你用的 API 需要什么单位。4.2 频率采样法灵活但有代价频率采样法的思路是另一种方向直接在频域指定你要的幅度响应在 0 到 π 之间均匀取 M 个点把这 M 个点的频域值做逆 DFT得到时域系数。它的实现简单MATLAB 的fir2就是干这个的。在我处理的某些特殊需求场景里比如设计一个斜率不是标准低通曲线的滤波器频率采样法就非常灵活。但频率采样法有一个明显的问题你只在 M 个离散点上约束了响应这些采样点之间的响应是完全不受控的。如果过渡带的采样点选择不合适频点之间的纹波可能远超预期。要改善就需要增大采样点数但又会导致滤波器长度变长、实现成本上升。我很少把频率采样法作为首选除非我需要的是某种特殊形状的响应而且愿意在事后花时间反复调整频点来压纹波。对标准低通、高通、带通、带阻这几种常规需求窗函数法和最优设计已经足够好没必要用频率采样法给自己找麻烦。4.3 最优设计等纹波逼近的工程级解法最优设计法在工程中通常指 Parks-McClellan 算法它的核心思想是在通带和阻带内让误差的峰值均匀分布等纹波并在给定阶数的条件下让峰值误差最小化。它是真正意义上的“最优”因为它在数学上被证明是在最大误差最小准则下的最优解。在相同阶数下等纹波设计能提供比窗函数法更低的通带纹波和更高的阻带衰减反过来在相同性能要求下等纹波设计需要的阶数更少。这是它的巨大优势。但它也有自身的考量项等纹波设计把误差均匀分布在通带和阻带使得滤波器的群延迟响应在某些极端设计下可能出现异常尤其当阶数估算不准确时会明显看到通带内的吉布斯效应。MATLAB 的firpm和 Python 的remez都实现了这一算法。以刚才的指标为例在 Python 中这样实现from scipy.signal import remez fs 16000 pass_zero 3400 stop_begin 4600 N 303 # 等纹波设计通常可以比窗函数法略低 taps remez(N, [0, pass_zero, stop_begin, fs/2], [1, 0], fsfs)注意这里传的边缘频率和期望增益的配置方式第一组边界 [0, pass_zero] 对应增益 1第二组边界 [stop_begin, fs/2] 对应增益 0。参数顺序弄错了滤波器的通带阻带就完全反了。我在给团队的同事 review 代码时看到过太多次这种错误。5. 实操落地全流程一套低通滤波器从指标到验证5.1 完整的案例指标设定下面我把刚才提到的各个碎片串成一个完整案例。假设需要设计一个音频前置去噪低通滤波器采样率 fs 16kHz通带截止频率 fp 3.4kHz阻带起始频率 fst 4.6kHz通带最大纹波 Rp ≤ 1dB阻带最小衰减 As ≥ 60dB通带 1dB 纹波换算成线性幅度大约是 1.12 倍左右阻带 60dB 对应幅度 0.001。这个指标组合在语音预处理里非常典型——既要保留语音的主要频段3.4kHz 内又要滤掉带外噪声和可能的超声干扰。先按凯泽窗估算阶数。过渡带宽度 Δf 4.6 - 3.4 1.2kHz归一化到采样率是 1.2/16 0.075。凯泽 β 参数β 0.1102 × (60 - 8.7) 5.65阶数估算N ≈ (60 - 7.95) / (2.285 × 0.075) ≈ 303.8取偶数N 304系数长度为 305。5.2 两种方法的实现对照窗函数法的关键代码上面已经给出。我下面把等纹波设计也完整写出来方便直接对照。Python 等纹波设计实现from scipy.signal import remez, freqz import numpy as np fs 16000 N 303 # 等纹波设计可以尝试更低的阶数 bands [0, 3400, 4600, 8000] desired [1, 0] taps remez(N, bands, desired, fsfs) w, h freqz(taps, 1, worN2048, fsfs) mag_db 20 * np.log10(np.abs(h) 1e-12)跑出来看一下通带纹波和阻带衰减如果阻带衰减没到 60dB可以适当增加阶数如果超过指标很多说明阶数有余量可以降下来节省资源。这是设计闭环里最常规的迭代操作。MATLAB 等纹波设计对照fs 16000; fpass 3400; fstop 4600; dev [0.028 0.001]; % 通带 1dB 纹波约等于线性幅度 0.028阻带统一 0.001 [n, fo, ao, w] firpmord([fpass fstop], [1 0], dev, fs); b firpm(n, fo, ao, w); freqz(b, 1, 1024, fs);firpmord是 MATLAB 里做阶数估计的函数它根据你给出的边缘频率和允许偏差自动计算最小阶数。它能省掉你手动估算阶数的步骤但底层使用的不是之前那个解析公式而是工程数值优化的结果。5.3 时域验证不要只看频谱图很多人在滤波器设计阶段只看频率响应就下结论这在纯理论验证里没问题但放到工程系统里往往不够充分。我习惯至少做三件事的验证第一给滤波器输入一个单位脉冲观察输出是否为有限长度序列确认 FIR 结构正确。第二给滤波器输入一个包含多个频率分量的合成信号例如 1kHz、4kHz、7kHz 的正弦叠加在时域观察 1kHz 分量是否完整保留、4kHz 以上的分量是否被有效衰减。第三检查群延迟补偿。群延迟对 FIR 是恒定值 (N-1)/2 个采样点对于 304 阶滤波器来说延迟是 152 个采样周期。在 16kHz 采样率下这相当于 9.5ms 延迟。如果你的系统里还有其他通路比如高通旁路、原始信号直通一定要把滤波器的延迟考虑进去否则两个通路叠加时会看到明显的相位对不齐。下面这段代码演示了如何用 Python 构造多频合成信号并验证滤波效果import numpy as np from scipy.signal import lfilter fs 16000 t np.arange(0, 0.1, 1/fs) x np.sin(2*np.pi*1000*t) 0.5*np.sin(2*np.pi*4000*t) 0.3*np.sin(2*np.pi*7000*t) y lfilter(taps, 1, x) # 计算滤波器延迟并对齐比较 delay (len(taps) - 1) // 2 aligned_x x[:len(y)-delay] aligned_y y[delay:]对比aligned_x和aligned_y就能直观看到1kHz 的正弦波几乎不变4kHz 和 7kHz 分量被大幅削弱。如果这个系统后级还有特征提取的算法请务必用对齐后的数据不要用原始滤波输出直接计算否则延迟引入的时间偏移可能直接毁掉你的特征时序。5.4 定点化实现和资源估算在硬件上实现 FIR 滤波器时浮点运算通常是不现实的至少是不划算的。FPGA 和 DSP 上更多采用定点实现。定点化有两个核心问题需要处理一是系数量化二是运算过程的数据位宽。系数量化的影响可以用一个简单的实验来体会。比如系数本来是浮点小数当你用 16 位定点数表示时每个系数会产生约 1.5e-5 级别的量化误差。这种误差会直接抬高阻带的地板让本来能到 -80dB 的阻带衰减退化到 -70dB 左右。如果系数位宽只有 12 位阻带衰减可能退化到 -60dB 以下。所以要求高阻带衰减的系统系数量化位宽必须留够这是我在 FPGA 设计中的一条硬经验。下面是一段定点化前移操作的示例。滤波器系数量化到 16 位整数时先归一化到 [-1, 1)乘 2^15 后做四舍五入取整import numpy as np h taps / np.sum(taps) # 归一化让直流增益为 1 q16 np.round(h * (2**15))运算过程也需要考虑中间结果的溢出。对 N 阶 FIR输入信号 x[n] 乘以系数并累加 N1 次中间累加器需要预留额外的位宽来避免溢出。比如输入是 16 位系数是 16 位乘积是 32 位累加 N 项时需要额外增加 log2(N) 位来容纳进位用 32ceil(log2(N)) 位做累加器是比较稳妥的做法。对 304 阶滤波器log2(304) ≈ 8.25累加器至少 40 位。6. 常见问题与调试技巧踩过的坑集中复盘6.1 截止频率设对了但行为不对归一化单位问题这是最高频的入门问题。同一个滤波器你可以在 MATLAB 看到好的结果但把系数搬到 C 语言的定点实现里行为却完全不对。原因十有八九出在归一化单位换算上。设计工具的截止频率往往需要归一化到奈奎斯特频率或者采样频率而 C 语言的滤波实现常常用弧度频率或者直接使用数字频率。我的建议是在项目开始时就在文档里明确写出所有频率参数的参考系并且每个实现的代码里用常量统一转换比如#define FS 16000 #define FC_NORM 3800.0 / (FS / 2) // 归一化到奈奎斯特频率另外千万记住数字角频率是模拟频率除以采样率再乘以 2π不是直接除以采样频率。搞反这个换算关系滤波器设计和实现的效果必然天差地别。6.2 纹波超标和阻带衰减不够阶数与窗函数的重新权衡遇到阻带衰减不够的情况第一反应不要是盲目增加阶数。先确认衰减指标和窗函数能力是否匹配。矩形窗只能做到 -21dB 的旁瓣汉宁窗 -44dB海明窗 -53dB布莱克曼窗 -74dB。如果你选的窗函数本身只能做到 -50dB那你再怎么增加阶数也没办法把阻带压到 -70dB。换窗函数比加阶数更快。如果换了凯泽窗还是不行那才考虑增加阶数来缩小过渡带、间接改善阻带性能。等纹波设计里阻带不足则大概率是阶数估计偏低了。用firpmord或自己手算的公式重新核算一下给阶数留一点裕量通常在估算值的 1.1 到 1.2 倍之间比较稳妥。6.3 定点化后性能劣化严重系数精度和累加器位宽FPGA 项目里经典的疑难杂症浮点仿真一切正常一到板级调试频谱图就出现明显的噪声地板上浮。这个现象基本可以定位到两点要么系数位宽不够要么累加器截断太早。系数位宽每减少一位阻带衰减大约劣化 6dB这是一个非常直观的经验规律。累加器如果在每次乘法后立即截断到与输入同宽截断噪声就会被累积放大正确的做法是只在最后一级输出时做一次舍入中间全程保持扩展精度。6.4 群延迟导致系统时序混乱延迟补偿的三种方案群延迟导致的时序问题在实时系统里最常见。典型的场景是滤波器输出和另一路直通信号做叠加或比较结果出现时间上的错位。这个问题有三个常规解法给另一路信号也加相同长度的延迟让两路信号的延迟对齐。调整整个系统的时间基准把滤波器的延迟计入总的链路预算。如果允许相位非线性改用最小相位 FIR但这样会丧失线性相位特性对某些应用不可接受。第一种方案在绝大多数工程里最实用实现成本也最低。6.5 一个快速问题速查表整理一份我在项目里经常对照的排查表遇到问题可以直接按表格定位现象可能原因解决对策阻带衰减明显低于设计值窗函数旁瓣能力不足换用旁瓣更低的窗如布莱克曼或凯泽通带边缘出现剧烈振铃过渡带设置过窄阶数不够增加阶数或放宽过渡带定点输出噪声明显增大系数位宽不足、累加器截断过早增加系数位宽保留中间累加精度截止频率和预期偏差大归一化单位换算错误统一频率参考系写注释说明高频完全消失但低频也有畸变IIR 型 FIR 类型选错检查滤波器类型偶数阶不能做高通时域滤波后信号与参考时序歪斜未补偿群延迟对参考通路补等量延迟7. 一点个人经验和最终建议做完这么多 FIR 设计的项目我最大的体会是设计工具只是帮你算出系数的“计算器”真正决定滤波器质量的是你在设计之前对指标的理解、对实现平台的考虑以及设计之后对系统级问题的验证。几个我觉得特别值得养成的习惯第一每一个滤波器设计任务开始前先花 15 分钟把指标的物理意义想清楚确认通带、阻带、过渡带的边界值都能对应到你系统里的实际信号特性。第二验证时不要只用一个正弦波一定要用接近真实信号的场景来测试。第三定点化从一开始就考虑不要等浮点原型做完再回头补那时候返工成本最高。如果后续想继续深入你可以接着研究多速率信号处理中的滤波器组设计以及自适应 FIR 滤波器的 LMS 算法实现。掌握了这些FIR 滤波器就会从一个个孤立的系数集合变成你系统设计中随手可用的基本功。