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型,系数灵敏度低
- 实时性要求极高:两种结构性能差不多,选你熟悉的
嗯,双二阶滤波器这块内容就这些。说白了,结构选型看资源,系数计算靠公式,定点优化靠经验。你把这些搞明白了,均衡器实现就成功了一大半。下一章咱们聊聊如何把这些双二阶滤波器级联起来,做成真正的多段均衡器。