第2章:嵌入式音频处理基础:定点数 vs 浮点数、Q格式详解、避免溢出的技巧
好,咱们进入正题。
做助听器算法,第一个绕不开的坎就是——数字表示。你想想看,一颗助听器芯片,主频可能就几十兆赫兹,RAM 可能只有几十 KB。在这种资源下跑音频算法,浮点数?想都别想。我刚开始从 PC 端算法转到嵌入式时,第一个被坑的就是这个。
2.1 定点数 vs 浮点数:为什么嵌入式必须选定点?
浮点数,说白了就是用科学计数法存小数。比如 3.14159,存成 3.14159 × 10^0。好处是动态范围大,精度高。但坏处也明显——硬件开销大,功耗高。
定点数呢?就是固定小数点位置。比如我们用 16 位整数来表示 -1 到 1 之间的数,小数点约定在第 15 位后面。这样加减乘除就是整数运算,快得很。
我个人的经验是:在助听器这种低功耗场景下,浮点运算单元(FPU)基本是奢侈品。就算有,为了省电,我也会尽量用定点。我在项目中遇到过一款芯片,开了 FPU 功耗直接翻倍,电池续航从 18 小时掉到 10 小时。你说你敢用吗?
2.2 Q格式详解:小数点到底放哪?
Q 格式,就是约定小数点位置的规则。比如 Q15 表示 16 位有符号数,小数点在第 15 位后面,范围是 -1 到 0.999969。Q1.15 也是同样的意思,1 位整数位,15 位小数位。
常见的 Q 格式有这些:
| Q格式 | 位数 | 整数位 | 小数位 | 范围 | 精度 |
|---|---|---|---|---|---|
| Q15 | 16 | 1 | 15 | -1 ~ 0.999969 | 3.05e-5 |
| Q1.15 | 16 | 1 | 15 | -1 ~ 0.999969 | 3.05e-5 |
| Q8.8 | 16 | 8 | 8 | -128 ~ 127.996 | 3.9e-3 |
| Q4.12 | 16 | 4 | 12 | -8 ~ 7.9997 | 2.44e-4 |
怎么选?我一般遵循两个原则:
- 看动态范围:音频信号一般归一化到 ±1,所以 Q15 最常用。但中间计算可能会放大,比如滤波器系数可能大于 1,这时候就要用 Q8.8 或 Q4.12。
- 看精度需求:自适应滤波器的系数更新,精度要求高,我会用 Q31(32 位)。但输出到 DAC 时,16 位就够了。
coeff_q15、input_q31。这样三个月后回来看代码,一眼就知道数据在什么范围。
2.3 定点数的加减乘除:别踩坑
定点数的加减法很简单,只要两个数 Q 格式一样,直接加就行。但要注意——结果可能溢出。
举个例子:
int16_t a = 0x7FFF; // Q15 格式,约等于 0.999969
int16_t b = 0x7FFF; // Q15 格式
int16_t c = a + b; // 结果 0xFFFE,变成 -2!
为什么会这样?因为 0x7FFF + 0x7FFF = 0xFFFE,在 16 位有符号数里是 -2。这就是溢出。
乘法更要注意。两个 Q15 数相乘,结果是 Q30,但只有 32 位寄存器。所以乘完后要右移 15 位,再转回 Q15。
int16_t a_q15 = 0x4000; // 0.5
int16_t b_q15 = 0x4000; // 0.5
int32_t temp = (int32_t)a_q15 * b_q15; // 结果是 Q30
int16_t c_q15 = (int16_t)(temp >> 15); // 右移15位,变回Q15
// c_q15 = 0x2000,即 0.25,正确
2.4 避免溢出的技巧:实战经验
溢出是定点算法的头号敌人。我总结了几个实用技巧:
- 预留保护位:中间计算用 32 位甚至 64 位。比如两个 16 位数相乘,用 32 位存结果。累加时用 64 位。
- 饱和截断:不要直接截断高位,要做饱和。比如结果大于 0x7FFF 就钳位到 0x7FFF。
- 动态缩放:根据信号能量动态调整 Q 格式。信号大时右移,信号小时左移。我在 AGC 算法里常用这招。
- 块浮点:一组数据共用一个指数。比如 16 个采样点,找到最大值,统一缩放。这样既保证精度,又避免溢出。
嗯,这里要注意——块浮点虽然好用,但实现起来有点麻烦。我一般只在 FFT 里用,因为 FFT 的中间结果动态范围特别大。
2.5 实战:一个简单的定点 FIR 滤波器
光说不练假把式。咱们写一个定点 FIR 滤波器:
#define TAP_NUM 32
#define Q_SHIFT 15
int16_t fir_q15(int16_t *coeff_q15, int16_t *buffer, int16_t input) {
int32_t acc = 0;
int i;
// 更新缓冲区
for(i = TAP_NUM - 1; i > 0; i--) {
buffer[i] = buffer[i-1];
}
buffer[0] = input;
// 乘累加,用32位累加器
for(i = 0; i < TAP_NUM; i++) {
acc += (int32_t)coeff_q15[i] * buffer[i];
}
// 饱和截断回Q15
acc >>= Q_SHIFT;
if(acc > 0x7FFF) acc = 0x7FFF;
if(acc < -0x8000) acc = -0x8000;
return (int16_t)acc;
}
你看,核心就是三步:乘、累加、饱和截断。我在项目中用这个结构跑了无数个滤波器,从来没出过问题。
下一章,咱们聊聊音频信号处理中最常用的——FFT 和滤波器设计。到时候我会分享一些我在助听器反馈抑制算法里的实战经验,保证让你少走弯路。