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系数计算

最经典的方法是窗函数法。步骤很简单:

  1. 先确定理想滤波器的频率响应(比如低通、高通)
  2. 做逆傅里叶变换,得到无限长的冲激响应
  3. 用窗函数截断(汉明窗、布莱克曼窗等)
  4. 得到有限长的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,但要做好稳定性验证

嗯,滤波器设计这块内容确实不少。但说白了,你只要掌握了原理,剩下的就是多动手、多踩坑。我刚开始做的时候也经常翻车,但每次翻车都是进步的机会。

下一章咱们聊聊更高级的话题——自适应滤波和回声消除。到时候我会分享一个我在车载免提项目中踩过的「大坑」,保证让你少走弯路。