4、双二阶滤波器实现:直接I型与直接II型结构、系数计算、定点数优化

双二阶滤波器,说白了就是数字音频处理里的「万能积木」。你想想看,不管是低通、高通、带通,还是参量均衡、陷波器,归根结底都能用双二阶滤波器搭出来。我在项目里做过一个10段图示均衡器,底层就是5个双二阶滤波器级联。嗯,今天咱们就把这东西彻底讲透。

4.1 直接I型结构:直观但费内存

直接I型是最容易理解的结构。它直接把差分方程翻译成信号流图:

y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]

这个公式看着挺长,其实逻辑很简单——当前输出由当前输入、前两次输入、前两次输出共同决定。我刚开始学的时候,觉得这结构挺自然的,直到在嵌入式平台上跑起来才发现问题。

注意:直接I型需要存储4个历史状态(x[n-1], x[n-2], y[n-1], y[n-2])。在定点数实现中,每个状态都是32位整数,4个状态就是16字节。如果做10段均衡器,光状态存储就要160字节。这还不算大问题,真正要命的是——

直接I型的动态范围控制比较麻烦。因为b系数和a系数可能相差很大,中间计算结果容易溢出。我曾经在一个16位定点DSP上调试,发现低频频段输出全是噪声,查了两天才发现是中间变量溢出了。

4.2 直接II型结构:省内存的优选方案

直接II型把延迟单元合并了。它先计算反馈部分,再计算前馈部分。核心思想是引入一个中间变量w[n]:

w[n] = x[n] - a1*w[n-1] - a2*w[n-2]
y[n] = b0*w[n] + b1*w[n-1] + b2*w[n-2]

你看,这里只需要存储2个状态(w[n-1]和w[n-2]),比直接I型省了一半内存。在嵌入式系统里,内存就是钱啊。我个人习惯在资源紧张的MCU上优先选用直接II型。

核心对比:

特性直接I型直接II型
状态变量数4个2个
内存占用
动态范围容易溢出相对可控
系数灵敏度

不过直接II型也有坑。它的中间变量w[n]可能比输入信号大很多,尤其是在低频高增益的情况下。我曾经在做一个60Hz低频提升时,w[n]的峰值达到了输入信号的20倍,差点把定点数给撑爆了。

4.3 系数计算:从模拟到数字的桥梁

双二阶滤波器的系数不是拍脑袋定的,而是从模拟滤波器原型通过双线性变换映射过来的。这里我直接给出一组常用的计算公式:

低通滤波器(截止频率fc,采样率fs,品质因数Q):

ω0 = 2*π*fc/fs
α = sin(ω0)/(2*Q)

b0 = (1 - cos(ω0))/2
b1 = 1 - cos(ω0)
b2 = (1 - cos(ω0))/2
a0 = 1 + α
a1 = -2*cos(ω0)
a2 = 1 - α

// 归一化
b0 /= a0; b1 /= a0; b2 /= a0
a1 /= a0; a2 /= a0

你可能会问,为什么最后要除以a0?因为差分方程的标准形式要求a0=1。我在项目中见过有人忘了这步,结果滤波器完全不工作,调试了半天才发现系数没归一化。

小技巧:实际工程中,我建议把系数计算做成一个独立的函数,输入参数是fc、Q、fs,输出是b0、b1、b2、a1、a2。这样换滤波器类型时只需要改计算公式,框架不用动。

4.4 定点数优化:把浮点算法塞进整数芯片

大多数嵌入式音频芯片没有浮点单元(FPU),或者浮点运算太慢。这时候就得用定点数。说白了,就是把小数乘以一个缩放因子,变成整数来算。

Q格式定点数:

// Q15格式:1位符号 + 15位小数
// 浮点数转定点数:float_val * 2^15
// 定点数转浮点数:int_val / 2^15

// 示例:系数b0=0.1234,转为Q15
int16_t b0_q15 = (int16_t)(0.1234 * 32768.0 + 0.5);  // 四舍五入

这里有个关键点:乘法运算后结果要右移。比如两个Q15数相乘,结果是Q30格式,需要右移15位回到Q15。我见过新手直接拿乘法结果往下传,结果数值越来越大,最后全溢出了。

避坑指南:我曾经在一个项目里用Q15格式做48kHz采样率的均衡器,低频段一切正常,高频段(8kHz以上)全是失真。查了好久才发现,高频时系数b0、b1、b2非常接近,定点量化误差导致滤波器特性完全变了。后来我把系数精度提升到Q31,问题就解决了。

定点数优化的几个原则:

  • 先算加法,后算乘法:减少中间结果溢出的概率
  • 使用饱和运算:溢出时自动钳位到最大值,避免wrap-around
  • 系数提前缩放:把系数放大到接近满量程,提高信噪比
  • 状态变量用双精度:如果芯片支持,用32位存状态,16位做运算

4.5 实战代码:直接II型定点实现

下面是我在一个Cortex-M4项目里实际用过的代码,做了精简:

typedef struct {
    int32_t b0, b1, b2;  // Q31格式系数
    int32_t a1, a2;      // Q31格式系数
    int32_t w1, w2;      // 历史状态
} BiquadQ31;

int16_t biquad_process(BiquadQ31 *f, int16_t x) {
    int32_t w, y;
    
    // 计算中间变量 w[n] = x[n] - a1*w[n-1] - a2*w[n-2]
    w = ((int32_t)x << 15);  // 输入转为Q31
    w -= (int64_t)f->a1 * f->w1 >> 31;
    w -= (int64_t)f->a2 * f->w2 >> 31;
    
    // 计算输出 y[n] = b0*w[n] + b1*w[n-1] + b2*w[n-2]
    y = (int64_t)f->b0 * w >> 31;
    y += (int64_t)f->b1 * f->w1 >> 31;
    y += (int64_t)f->b2 * f->w2 >> 31;
    
    // 更新状态
    f->w2 = f->w1;
    f->w1 = w;
    
    // 饱和钳位到16位输出
    if (y > 32767) y = 32767;
    if (y < -32768) y = -32768;
    
    return (int16_t)(y >> 15);
}

你看,这里用了int64_t做中间乘法,防止32位乘法溢出。虽然64位运算慢一点,但换来的是稳定性。在实时音频里,宁可慢一点,也不能有爆音。

性能数据:这段代码在72MHz的Cortex-M3上,处理一个采样点大约需要0.5μs。如果做10段均衡器(10个双二阶级联),处理一个采样点需要5μs。48kHz采样率下,每个采样间隔是20.8μs,所以CPU占用率大约24%。完全够用。

4.6 结构选择建议

说了这么多,到底该用哪种结构?我的建议是:

  • 资源极度紧张(RAM < 1KB):用直接II型,省内存
  • 有浮点单元:用直接I型,代码简单,调试方便
  • 需要高精度(24位以上):用直接II型,系数灵敏度低
  • 实时性要求极高:两种结构性能差不多,选你熟悉的

嗯,双二阶滤波器这块内容就这些。说白了,结构选型看资源,系数计算靠公式,定点优化靠经验。你把这些搞明白了,均衡器实现就成功了一大半。下一章咱们聊聊如何把这些双二阶滤波器级联起来,做成真正的多段均衡器。