
最近在一款音频产品里需要用线性相位 FIR 滤波器做分频和陷波规格不算复杂——通带平坦度 0.1dB阻带衰减 60dB还要在某个频点挖一个窄带陷波。结果我用现成的滤波器设计工具折腾了半天界面里填参数没问题但涉及到自定义幅度响应、系数定点化、还有最后导出成 C 数组和 Verilog这些工具要么不支持要么只能在“高级版”里用。最后干脆自己写了一个 FIR 滤波器设计软件把指标到系数、仿真验证、代码生成一条龙串起来。这篇文章就把这个软件的设计思路、关键算法和踩过的坑展开聊聊。写这个软件之前我特意翻了一下大家在社区里搜的滤波器关键词——从“fir滤波器”到“多相滤波器”“cic插值滤波器”再到“四阶巴特沃斯滤波器”“sallen-key滤波器”其实能看出一个问题很多人并不是分不清 FIR 和 IIR而是不确定在什么场景下该选哪种也不清楚滤波器的指标怎么转换成实际可用的抽头数。我的 FIR 设计软件最核心的价值就是把这些模糊地带变成可复现的参数流程。1. 为什么 FIR 设计不是“点一下出系数”软件要解决的第一性问题1.1 从规格到系数的链路比大多数人想的长很多人以为滤波器设计软件就是输入通带频率、阻带频率、衰减然后点一下“设计”系数就出来了。实际上这条链路至少有四个环节指标解析、滤波器类型选择、算法设计、验证与导出。任何一个环节出问题产线上的效果都会翻车。我先说指标解析。你给软件“通带 0 到 10kHz阻带 60dB过渡带 2kHz”这句话本身是有歧义的通带纹波到底是多少阻带起始频率是 12kHz 还是“过渡带结束于某个频率”我写软件的第一件事就是把这些字段拆开明确告诉用户通带纹波通常给 0.1dB阻带衰减给 60dB过渡带宽度决定了抽头数量而不是“通带边界”和“阻带边界”两个孤立值。然后是滤波器类型。FIR 滤波器按对称性分四种I 型奇数长度对称、II 型偶数长度对称、III 型奇数长度反对称、IV 型偶数长度反对称。II 型在 Nyquist 频率处响应为 0做高通和带阻会有问题III 型和 IV 型适合 Hilbert 变换器和微分器。设计软件必须在用户勾选“高通”时自动避开 II 型否则就会给出一个系数明明对称、但频响在 fs/2 处莫名塌陷的结果。1.2 FIR、IIR、还有模拟滤波器别让软件替你盲目决定热搜词里有“四阶巴特沃斯滤波器”“sallen-key滤波器”“二阶低通有源滤波器设计与仿真测试”这些都属于另一个阵营。我的 FIR 设计软件不排斥这些词它会把对比信息直接显示在界面上如果用户只是要平滑一个信号不关心相位IIR 的阶数可以低一个数量级如果用户做的是音频分频、数据同步、心电信号预处理这类对相位敏感的场合FIR 的线性相位几乎是不可替代的。举个实际对比设计一个采样率 48kHz、通带 0~10kHz、阻带衰减 60dB、过渡带 2kHz 的低通滤波器。用我的 FIR 设计软件抽头数约 89 个群延迟固定为 44 个采样点用四阶巴特沃斯 IIR只要 4 个极点就能达到差不多的幅频衰减但相位在通带边缘明显弯曲群延迟随频率变化时域波形会变形。而 Sallen-Key 这类有源滤波器用在模拟前端是另一个维度的事情——它受运放带宽、电阻电容精度影响的容差问题和纯数字系数完全是两码事。软件的作用是把这些取舍讲清楚而不是给一个黑盒结果。1.3 这个软件最终要交付什么我给自己定的交付标准很简单输入指标输出能在项目里直接用的东西。具体包括三样滤波系数支持浮点 double、16 位定点、24 位定点三种格式。验证报告包括幅度响应、相位响应、群延迟、脉冲响应、零极点图。目标代码C 头文件、Verilog 系数表或者给 DSP 编译器导入的 COE 文件。这三点听起来简单但现成工具往往只把第一点做完整后两点总要手工拼接。于是我做了一个类似“独立小工具”的东西算法引擎可以跑在命令行也可以挂上用 Tkinter 写的简易界面。函数库单独抽出来这样无头服务器上也能批量生成滤波器。2. 三种设计引擎的实现窗函数法、频率采样、Parks-McClellan2.1 窗函数法Kaiser 窗是这个方法里最实用的一个窗函数法的原理是截断理想滤波器的无限冲激响应再乘一个窗函数来抑制 Gibbs 现象。常见的窗有 Hamming、Hanning、Blackman、Kaiser 等。Hamming 窗简单阻带衰减大约 53dBBlackman 能到 74dB 左右但过渡带更宽。最常用的是 Kaiser 窗因为它有一个可调节的 beta 参数可以连续控制阻带衰减和过渡带宽。我的软件里对 Kaiser 窗使用两个工程近似公式。第一个公式根据阻带衰减 A 计算 betaA 50dBbeta 0.1102 × (A - 8.7)21dB A ≤ 50dBbeta 0.5842 × (A - 21)^0.4 0.07886 × (A - 21)A ≤ 21dBbeta 0第二个公式估算滤波器长度N ≈ (A - 7.95) / (2.285 × Δω) 1其中 Δω 是归一化过渡带宽单位是 rad/sample。实际实现时还要取奇数长度保证 I 型对称性。def kaiser_lowpass(fs, f_pass, f_stop, a_stop): dw 2 * np.pi * (f_stop - f_pass) / fs A a_stop if A 50: beta 0.1102 * (A - 8.7) elif A 21: beta 0.5842 * (A - 21) ** 0.4 0.07886 * (A - 21) else: beta 0.0 N int(np.ceil((A - 7.95) / (2.285 * dw) 1)) if N % 2 0: N 1 taps scipy.signal.firwin( N, f_pass, window(kaiser, beta), fsfs ) return taps, beta, N这段代码看起来简单但在软件里还有一层额外处理如果用户同时指定多个通带和阻带就要先解析成边界数组再调用 firwin2 或 remez。很多人在这一点上栽跟头——firwin 只能设计理想低通、高通这类标准响应不能直接做任意形状的幅度响应。2.2 频率采样法看起来直接坑最多频率采样法的思路是把理想频率响应在某些频点上采样然后做 IDFT 得到 FIR 系数。这种方法在软件实现上非常简单但有两个问题。第一采样点之间的频响不受控可能出现很大的过冲第二直接采样得到的系数没有任何平滑措施滤波器阶数稍微高一点频域波形就“振铃”。我在软件里保留了频率采样法但加了一个平滑选项在采样点之间用线性或三次插值过渡而不是生硬地在目标频点上跳变。插值后阻带衰减会变差需要用迭代一点一点修正。这种方法的实用性远不如 Remez 算法所以我把它放到了“高级”菜单里主要给那些需要完全自定义幅度响应的用户参考。2.3 Parks-McClellan/Remez等波纹设计的引擎窗函数法的问题是同样的指标下它不一定给出最小长度的滤波器而且误差分布不均匀靠近通带边缘的纹波往往比中间大。Parks-McClellan 算法通过 Remez 交换算法在通带和阻带内等波纹地逼近目标响应能显著降低滤波器长度。我的软件用 scipy.signal.remez 做底层但封装了一层友好的调用。用户输入频率边界、幅度目标和权重我负责把长度推算出来。长度推算不能闭着眼猜有一个实用的经验公式N ≈ (-10 × log10(δp × δs) - 13) / (14.6 × Δf / fs) 1δp 是通带纹波幅度δs 是阻带纹波幅度Δf 是过渡带宽。举个例子通带纹波 0.1dB 换算成线性值大约是 0.0115阻带衰减 60dB 对应 0.001那么分子约为 70分母在 Δf2kHz 时约为 0.608N 大约 116。这个结果比 Kaiser 窗的 89 大因为等波纹设计会把通带和阻带纹波同时压到指标内而 Kaiser 的计算只保证了阻带衰减通带纹波未必严格卡住。实际项目中我会把两种方法的计算结果都显示出来让用户自己判断如果只是快速验证Kaiser 窗出来的 89 个点可能就够用如果要做最终量产固件用等波纹设计的 116 个点更稳频响余量更大。3. 实例推演48kHz 采样率下一个 60dB 低通滤波器的完整计算3.1 先把指标写清楚很多人在社区搜索“fir 滤波器”“滤波器设计”但真正到设计软件里操作时会发现自己连指标都填不完整。我会先用一个具体例子把整个流程走一遍。假设采样率 fs 48kHz通带边界 fc 10kHz要求通带纹波 0.1dB阻带衰减 60dB阻带起始频率 fstop 12kHz。过渡带宽就是 2kHz。先计算归一化过渡带宽Δω 2π × (12000 - 10000) / 48000 0.2618 rad/sample用 Kaiser 窗计算A 60dBbeta 0.1102 × (60 - 8.7) 5.653N ≈ (60 - 7.95) / (2.285 × 0.2618) 1 ≈ 88取奇数长度 N 89。这意味着滤波器会有 89 个抽头群延迟为 (89-1)/2 44 个采样点对应时间 44/48000 0.9167ms。3.2 在软件里验证频率响应设计完成后软件会立刻画出频率响应曲线。我特别重视 60dB 衰减这个数字因为 0.001 的线性幅度对应很小的通带细节如果窗口选错衰减可能在 55dB 就掉下去了。Kaiser 窗在这种指标下给的 beta 能保证最小衰减略大于 60dB但如果没有软件里的可视化验证只看系数表很容易漏掉边缘频段的问题。验证时不能只用理想浮点系数跑一遍还要把系数量化到 16 位定点再看频响。16 位定点对 89 个抽头的滤波器来说量化噪声可能让阻带衰减从 60dB 恶化到 52dB 左右这是一个非常隐蔽的坑。我的软件会在用户选择“16 位定点导出”时自动重新计算一遍量化后的频响并叠加在图上用红色曲线标出“量化后”绿色曲线标出“浮点理想”相差超过 3dB 就弹警告。3.3 从低通向带通和陷波扩展实际项目里很少有单一低通的需求更多是带通、带阻和陷波。我的软件把低通设计当作基础原语带通可以通过频率变换得到也可以通过把低通系数与余弦调制相乘实现。后者有一个好处可以精确控制中心频率。陷波滤波器更有意思。简单做法是在目标频率处放一个极窄的阻带再用 Parks-McClellan 算法优化。但窄阻带会带来时域振铃阶数不够时阻带深度也达不到要求。常见替代方案是用 IIR 陷波器但 FIR 陷波器适合必须保持线性相位的系统比如音频测量设备。软件里加了一个“陷波深度”参数设计时会自动扫描附近频点保证陷波中心的衰减不低于 40dB同时观察阻带边缘有没有异常凸起。4. 从系数到可用固件软件的功能闭环4.1 参数化和批处理这个 FIR 设计软件在命令行模式下有一个参数化配置文件可以用 JSON 描述一批滤波器指标。这样设计多通道滤波器组比如 5 段音频均衡器时不需要反复打开图形界面直接跑一个脚本就能生成 5 组系数和 5 份报告。这个设计让我在后期调试 FPGA 时省了很多时间。JSON 配置示例[ { name: LP_10k, type: lowpass, fs: 48000, f_pass: 10000, f_stop: 12000, a_pass_db: 0.1, a_stop_db: 60, window: kaiser, quantize: 16 } ]这种批处理方式特别适合“滤波参数需要按温度或模式切换”的产品不同模式对应不同系数固件里预存多组数组即可。4.2 报告生成和交叉验证软件生成一个 HTML 报告里面嵌入频率响应图、相位响应图、群延迟图还有一个简化版“通过/不通过”检查表。这个检查表把所有设计约束列出通带最大纹波是否在范围内、阻带最小衰减是否达标、滤波器长度是否满足实时性要求、量化后是否满足要求。我在做“滤波器应用 matlab 仿真”这类验证时会把 MATLAB 仿真结果和这个报告放在一起对比双方频谱曲线重合才认为设计可信。4.3 导出 C 头文件和 Verilog 系数表代码生成是这类软件的分水岭。大多数算法工具只能给你一组系数我这边直接生成一个.h文件包含 static const float taps[] 或 int16_t taps[]。一个.coe文件用于 Xilinx FIR Compiler IP。一个.txt文件用于导入到 DSP 开发环境。生成 C 头文件时要注意对称性问题。FIR 系数是对称的理论上可以只保存一半系数另一半对称复用。但如果你在软件里做了 16 位量化必须把完整的量化后系数数组输出原因很简单偶对称系数相加时四舍五入误差可能累积人为做一半再镜像会导致直流增益不是整数。这不是小事——我在做音频设备时直流增益偏差 1 个 LSB 就可能带来可闻的直流偏移。软件默认导出完整数组并同时输出“对称化”选项让用户选择。5. 滑动窗口延迟和多相实现实时系统里躲不开的账5.1 线性相位的代价是固定延迟很多人在搜索“滑动窗口滤波器延迟”本质上就是在问 FIR 滤波器的群延迟问题。线性相位 FIR 的群延迟恒为 (N-1)/2 个采样点。对于一个实时控制系统这是实打实的纯延时不会因为系数优化得好而减少。举一个我在电机控制项目里遇到的例子采样率 10kHz需要滤除 50Hz 附近的窄带干扰同时保留 1Hz 到 30Hz 的信号。设计一个 64 阶的 FIR 带通群延迟是 32 个采样也就是 3.2ms。对电流环来说3.2ms 的相延可能直接导致环路稳定性下降这时候 FIR 就不一定是最优解IIR 或者状态观测器可能更合适。软件里专门加了一个“实时性预算”面板用户输入允许的最大延迟软件会反推最大抽头数。如果设计结果超了界面会提示“建议使用 IIR 或改用多相/多速率结构”这一点非常重要。5.2 多相分解用资源换时间“多相滤波器”也是热搜词里的常客。多相分解并不改变滤波器本身的频率响应它只是把原来的 N 个抽头按相位分成若干子滤波器每个子滤波器的阶数是原来的 1/M。典型的应用是插值和抽取在 48kHz 到 96kHz 的升采样场景里如果直接设计一个 96kHz 采样率下的滤波器N 会大得惊人但用多相结构可以先把原型滤波器拆成两个子滤波器每个时钟周期只计算其中一半的乘累加。我的 FIR 设计软件在“多速率”模式下会自动计算多相分支数并输出各分支系数。比如一个 96kHz 的低通滤波器设计成 2 倍插值总长度 96多相分解后每相长度 48计算量减半。多相结构配合 FIR 的线性相位特性是 FPGA 上非常高效的做法。5.3 CIC 插值滤波器什么时候更划算CIC 滤波器没有乘法器只用加法器和积分器适合超大倍数抽取/插值场景。软件里有一块对比面板如果用户设计的是 16 倍以上的抽取或者需要抗混叠但允许一定通带滚降CIC 比同规格 FIR 节省大量硬件资源。但 CIC 的通带不够平坦通常后面还要接一个 FIR 补偿滤波器。我的软件支持“CIC FIR 补偿”联合设计自动把 CIC 的频响算出来再生成一个反向补偿 FIR。这个功能在搜索引擎里对应“cic插值滤波器”的高频需求实际操作起来比很多人想象中简单。6. 我在调试滤波器设计软件时踩过的坑6.1 不要迷信“系数算对了就完事”有一次我设计了一个 256 抽头的带通 FIR浮点仿真完美阻带衰减 80dB结果在 FPGA 上跑出来的频谱却只有 65dB。查了半天发现不是系数问题而是定点乘法里的截断误差累加器位宽不够每级乘法结果被截断后噪声累积。从那以后我在软件里增加了“定点累加器位宽估算”模块根据 N 和系数位宽自动建议至少需要的乘法器输出位宽和累加器位宽。这个功能虽然不是滤波器设计本身但它能提醒每一个用户输出端的位宽设计至少要比输入端宽 log2(N) 位。6.2 窄带陷波和过采样组合时的振铃在音频测量设备里50Hz 工频陷波很常见。直接用 FIR 做窄带陷波时域振铃可能持续几十毫秒虽然幅度频响没问题但脉冲响应看起来很吓人。软件里的处理办法是给陷波器增加一个可调的“带宽因子”或者设计成级联两个宽度不同的陷波器一个粗陷波负责深度一个细陷波负责宽度。这个方法来自实际调试经验比单纯增加阶数更有效。6.3 与其他滤波器设计场景的边界还要提醒一句软件解决的是数字滤波器设计不等于所有滤波问题。比如并网逆变器里的 LCL 滤波器参数设计是功率级无源滤波的问题谐振频率、阻尼电阻、电感电容值完全由硬件拓扑决定和这里说的 FIR 系数是两回事。但它可以利用数字陷波补偿 LCL 谐振峰这就需要先算清楚 LCL 的谐振频率再回到 FIR/IIR 工具里设计一个窄带陷波。我的软件里的陷波设计功能在并网逆变器调试时也能用上只是最终参数要重新核验一遍。6.4 一张实用的测试清单我每次把软件生成的新滤波器带进项目都会走一套固定测试流程把系数导入 FPGA 或 DSP 后先看零输入响应确认没有直流偏移。输入一个理想正弦扫频信号从 20Hz 到 0.4fs 扫描用频谱仪观察输出是否有非线性分量。输入宽带白噪声对比输入输出功率谱确认滤波曲线与软件报告一致。把输出信号反转 180 度并与输入相减检查残余里有没有周期性分量。对量化后系数跑一遍浮点与定点联合仿真取得“硬件上线后可能的最差结果”。这个清单已经帮我查出了两次“软件报告正常、硬件不达标”的问题每次都是量化或累加器位宽导致的。现在我会把它直接内嵌进软件报告末尾让用户点一下就能看到。最后一点实在话我在写这个 FIR 滤波器设计软件的过程中最大的体会是滤波器设计软件不难写难的是让用户知道自己在设计什么。很多人卡住的点不是不会用某一行命令而是分不清等波纹和窗函数的区别也不理解抽头数和延迟之间的矛盾。如果这篇文章能让你意识到设计软件的价值不在于帮你点那一下“生成系数”而在于把指标可解释、过程可验证、结果可落地这三件事串起来那你再回头去看那些主流工具就会多一分清醒。下次再有人问“fir 滤波器设计到底用什么软件”我的回答永远是先把这个挖透再谈工具。