3、数字滤波器设计:FIR滤波器原理、IIR滤波器原理、滤波器系数计算、滤波器实现优化
数字滤波器,说白了就是给音频信号做「整形」的工具。你想想看,一个音箱出来的声音好不好听,很大程度上取决于你怎么处理中高低频。我个人做了这么多年嵌入式音频,滤波器这块可以说是基本功中的基本功。
今天咱们就聊聊两种最常用的滤波器:FIR和IIR。它们各有各的脾气,用好了是神器,用不好就是坑。
3.1 FIR滤波器原理
FIR,全称是Finite Impulse Response,有限冲激响应。名字听着唬人,其实意思很简单:你给滤波器一个脉冲信号,它的输出在有限时间内一定会归零。
FIR的核心公式长这样:
y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] + ... + bN*x[n-N]
说白了就是:当前输出 = 当前输入乘以系数 + 前一个输入乘以系数 + 前前一个输入乘以系数……一直加到N个样本之前。
FIR最大的优点是什么?线性相位。什么意思?就是不同频率的信号经过滤波器后,延迟时间是一样的。这对音频来说太重要了——如果不同频率延迟不一样,波形就会失真,听起来就是「糊」的。
关键特性:
- 绝对稳定(没有反馈,不会自激)
- 线性相位(群延迟恒定)
- 系数对称时,相位响应是线性的
- 计算量大(阶数通常较高)
我在项目中遇到过一件事:有一次做一款高端Soundbar,客户要求分频点处相位必须对齐。我二话不说上了FIR,虽然MCU算力吃紧,但效果确实好。嗯,这里要注意——FIR的阶数越高,延迟越大。如果你做的是实时对讲设备,延迟超过10ms就麻烦了。
3.2 IIR滤波器原理
IIR,Infinite Impulse Response,无限冲激响应。和FIR相反,你给它一个脉冲,它的输出理论上会一直震荡下去,永远不会完全归零。
IIR的公式稍微复杂一点:
y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
看到了吗?输出不仅依赖输入,还依赖之前的输出。这就是「反馈」——也是IIR的精髓所在。
IIR最大的优势是什么?效率高。同样的滤波效果,IIR可能只需要5阶,FIR却要100阶。算力差距摆在那里。
但IIR也有个让人头疼的问题:相位非线性。不同频率的延迟不一样,听起来可能会有「相位失真」。另外,IIR有可能不稳定——如果系数算错了,或者定点实现时精度不够,滤波器可能直接「炸」掉。
我曾经踩过的坑:
有一次做低音炮的DSP,用了高阶IIR做低频提升。结果在某个特定频率下,滤波器自激了,输出直接削波,低音炮「噗噗」响。后来查了半天,发现是定点实现时系数量化误差导致极点移到了单位圆外。从那以后,我每次做IIR都会留个心眼:先仿真,再定点,最后上硬件。
3.3 滤波器系数计算
系数怎么算?说白了就是「你想要什么样的频率响应,我就给你算什么样的系数」。常用的方法有几种:
3.3.1 FIR系数计算
最经典的方法是窗函数法。步骤很简单:
- 先确定理想滤波器的频率响应(比如低通、高通)
- 做逆傅里叶变换,得到无限长的冲激响应
- 用窗函数截断(汉明窗、布莱克曼窗等)
- 得到有限长的FIR系数
举个例子,设计一个48kHz采样率、截止频率12kHz的低通FIR:
// 伪代码示例
采样率 fs = 48000
截止频率 fc = 12000
归一化截止频率 wc = 2*pi*fc/fs = pi/2
阶数 N = 64
for n = 0 to N:
if n == N/2:
h[n] = wc/pi
else:
h[n] = sin(wc*(n-N/2)) / (pi*(n-N/2))
// 加汉明窗
h[n] = h[n] * (0.54 - 0.46*cos(2*pi*n/N))
我个人习惯用Python的scipy.signal库先算好系数,然后导出C数组。省时省力,不容易出错。
3.3.2 IIR系数计算
IIR系数通常用双线性变换法。先把模拟滤波器的s域传递函数映射到数字z域。常用的模拟原型有:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 巴特沃斯 | 通带最平坦 | 一般音频处理 |
| 切比雪夫I型 | 通带有纹波 | 需要陡峭截止 |
| 切比雪夫II型 | 阻带有纹波 | 需要陡峭截止 |
| 椭圆 | 最陡峭 | 过渡带极窄 |
举个例子,设计一个二阶巴特沃斯低通:
// 二阶IIR低通系数计算
fs = 48000
fc = 1000
omega = tan(pi*fc/fs)
b0 = omega^2 / (1 + sqrt(2)*omega + omega^2)
b1 = 2*b0
b2 = b0
a1 = 2*(omega^2 - 1) / (1 + sqrt(2)*omega + omega^2)
a2 = (1 - sqrt(2)*omega + omega^2) / (1 + sqrt(2)*omega + omega^2)
小技巧:我建议你把IIR滤波器拆成二阶节(biquad)串联。这样做有两个好处:一是数值稳定性好,二是调试方便——哪个频段出问题,直接定位到对应的二阶节。
3.4 滤波器实现优化
理论说完了,咱们聊聊实战。嵌入式平台上跑滤波器,算力和内存都是有限的。怎么优化?我总结了几个要点。
3.4.1 FIR优化
对称系数优化:FIR系数如果是对称的,可以只存一半系数。计算时把对称的输入相加,再乘系数。这样乘法次数减半。
// 对称FIR优化示例
for i = 0 to N/2-1:
sum = x[n-i] + x[n-N+i]
y[n] += coeff[i] * sum
// 中间点单独处理
if N为偶数:
y[n] += coeff[N/2] * x[n-N/2]
SIMD指令加速:如果MCU支持DSP指令或NEON,可以用一条指令同时做多个乘加。我曾在Cortex-M7上用CMSIS-DSP库,同样的FIR,速度提升了4倍。
3.4.2 IIR优化
直接I型 vs 直接II型:直接II型需要的状态变量更少,省内存。但要注意,直接II型对定点溢出的容忍度更低。
定点实现注意事项:
- 系数用Q15或Q31格式
- 中间结果用高精度累加(比如Q31乘Q31得到Q63,累加完再截断)
- 防止溢出:可以右移后再累加,但会损失精度
// 定点IIR二阶节实现(Q15格式)
int16_t b0_q15, b1_q15, b2_q15;
int16_t a1_q15, a2_q15;
int16_t w0, w1, w2; // 状态变量
int16_t iir_biquad(int16_t x) {
int32_t acc;
// 计算中间变量
acc = (int32_t)x << 15; // 扩展到Q30
acc -= (int32_t)a1_q15 * w1;
acc -= (int32_t)a2_q15 * w2;
w0 = (int16_t)(acc >> 15); // 截断回Q15
// 计算输出
acc = (int32_t)b0_q15 * w0;
acc += (int32_t)b1_q15 * w1;
acc += (int32_t)b2_q15 * w2;
// 更新状态
w2 = w1;
w1 = w0;
return (int16_t)(acc >> 15);
}
注意:定点IIR实现时,一定要做极限测试。我曾经遇到过:输入一个满幅度的正弦波,IIR内部状态变量溢出了,输出直接变成直流偏置。后来加了饱和处理才解决。
3.4.3 选择建议
到底用FIR还是IIR?我的经验是这样的:
- 分频器、均衡器:用IIR,效率高,效果够用
- 相位校正、线性相位需求:用FIR,没得选
- 算力充足:优先FIR,省心
- 算力紧张:用IIR,但要做好稳定性验证
嗯,滤波器设计这块内容确实不少。但说白了,你只要掌握了原理,剩下的就是多动手、多踩坑。我刚开始做的时候也经常翻车,但每次翻车都是进步的机会。
下一章咱们聊聊更高级的话题——自适应滤波和回声消除。到时候我会分享一个我在车载免提项目中踩过的「大坑」,保证让你少走弯路。