4. 定点数运算与浮点数模拟:定点数表示、Q格式、浮点转定点、精度分析
好,咱们进入第四章。这一章,说实在的,是雷达信号处理嵌入式优化的「硬骨头」,也是最能体现功力的地方。
你想想看,雷达信号处理,动不动就是几百兆的采样率,几千点的FFT,还有各种矩阵运算。如果用浮点,DSP或者FPGA的资源很快就烧光了。怎么办?用定点数。
但定点数不是简单的「砍掉小数位」。怎么表示?怎么保证精度?怎么把浮点算法转成定点?这里面门道很多。我当年刚入行时,就因为定点化没做好,导致一个测距算法偏差了十几米,被老大骂得狗血淋头。嗯,从那以后,我对这块就特别上心。
4.1 定点数表示:为什么不用浮点?
浮点数,比如IEEE754标准的float,有符号位、指数、尾数。好处是动态范围大,坏处是硬件实现复杂,功耗高,速度慢。
定点数就简单了。说白了,就是固定整数部分和小数部分的位数。比如一个16位的定点数,你可以规定高8位是整数,低8位是小数。
我个人习惯把定点数理解成「带刻度的尺子」。整数部分决定你能量多大的数,小数部分决定你量得多精细。
核心公式:
定点数值 = 整数部分 + 小数部分
实际值 = 定点编码值 × 2^(-小数位数)
举个例子:一个16位定点数,Q8格式(8位整数,8位小数)。编码值0x1234,实际值就是0x1234 / 256 = 4660 / 256 ≈ 18.203125。
4.2 Q格式:约定大于配置
Q格式是定点数最常用的表示法。它的写法是Qm.n,其中m是整数位数,n是小数位数。总位数就是m+n+1(符号位)。
常见的Q格式有:
| Q格式 | 总位数 | 整数位 | 小数位 | 动态范围 | 精度 |
|---|---|---|---|---|---|
| Q1.14 | 16 | 1 | 14 | [-1, 0.9999] | 2^(-14) ≈ 0.000061 |
| Q8.7 | 16 | 8 | 7 | [-128, 127.992] | 2^(-7) ≈ 0.0078125 |
| Q16.15 | 32 | 16 | 15 | [-32768, 32767.999] | 2^(-15) ≈ 0.0000305 |
我在项目中遇到过一个问题:用Q1.14格式做FFT,结果溢出得一塌糊涂。为什么?因为FFT的中间结果会放大,Q1.14的动态范围根本不够。后来我改用Q8.7,虽然精度差了点,但至少不溢出了。
我的经验:选择Q格式时,先估算信号的最大幅度,再留出3-6dB的余量。别贪心,精度和动态范围是矛盾的。
4.3 浮点转定点:三步走
把浮点算法转成定点,不是简单地把float换成int。我总结了一个三步法:
- 分析动态范围:找出算法中所有变量的最大值和最小值。可以用浮点仿真跑一遍,记录极值。
- 选择Q格式:根据动态范围,确定整数位数。再根据精度要求,确定小数位数。
- 实现定点运算:把浮点运算替换成定点运算,注意溢出和舍入。
举个例子,一个简单的浮点乘法:
// 浮点版本
float a = 1.5f;
float b = 2.3f;
float c = a * b; // c = 3.45
// 定点版本(Q8.7格式)
int16_t a_fixed = (int16_t)(1.5f * 128.0f); // 192
int16_t b_fixed = (int16_t)(2.3f * 128.0f); // 294
int32_t temp = (int32_t)a_fixed * b_fixed; // 192 * 294 = 56448
int16_t c_fixed = (int16_t)(temp >> 7); // 右移7位,得到441
// 实际值 = 441 / 128.0f = 3.4453125
你看,定点乘法需要先扩大,再缩小。中间结果要用更宽的位宽保存,不然精度就丢了。
注意:定点乘法时,中间结果一定要用32位或64位保存。我见过有人直接用16位乘16位,结果截断后误差大到离谱。
4.4 精度分析:别被误差坑了
定点化的精度损失主要来自三个方面:
- 量化误差:浮点转定点时,小数部分被截断或舍入。
- 运算误差:乘法后的右移,加法后的溢出。
- 累积误差:多次运算后,误差不断累积。
我曾经做过一个卡尔曼滤波的定点化,浮点版本精度0.1%,定点版本直接飙到5%。查了半天,发现是状态更新方程里,一个很小的增益系数被量化成了0。嗯,这就是典型的「精度饿死」问题。
怎么分析精度?我建议用「误差注入法」:
- 用浮点仿真跑一遍,得到「黄金参考值」。
- 用定点仿真跑一遍,得到「实测值」。
- 计算误差:绝对误差、相对误差、均方根误差。
如果误差太大,就调整Q格式,或者在某些关键路径上保留浮点运算。
避坑指南:我曾经在雷达脉冲压缩算法中,因为定点化精度不够,导致旁瓣电平抬高了3dB。后来在匹配滤波器的系数上用了Q16.15格式,其他地方用Q8.7,才把性能拉回来。记住:关键系数用高精度,普通数据用低精度。
4.5 实战技巧:混合精度与饱和处理
实际工程中,很少只用一种Q格式。我常用的策略是:
- 输入数据:Q8.7或Q1.14,取决于ADC位数。
- 滤波器系数:Q16.15,保证系数精度。
- 中间结果:Q8.7或Q16.15,看运算复杂度。
- 输出数据:Q8.7,方便后续处理。
另外,饱和处理也很重要。定点数溢出后,会从最大值翻到最小值,这在雷达信号里是灾难性的。我习惯在每次运算后加饱和判断:
// 饱和加法
int16_t sat_add(int16_t a, int16_t b) {
int32_t sum = (int32_t)a + b;
if (sum > 32767) return 32767;
if (sum < -32768) return -32768;
return (int16_t)sum;
}
你看,多了一行判断,但避免了灾难性的溢出。
好了,这一章就到这里。定点数运算,说白了就是「用精度换资源,用技巧保性能」。下一章我们讲FFT的定点化实现,那才是真正考验功力的时候。
课后思考:如果你有一个32位的定点数,你会怎么分配整数和小数位?为什么?