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。我总结了一个三步法:

  1. 分析动态范围:找出算法中所有变量的最大值和最小值。可以用浮点仿真跑一遍,记录极值。
  2. 选择Q格式:根据动态范围,确定整数位数。再根据精度要求,确定小数位数。
  3. 实现定点运算:把浮点运算替换成定点运算,注意溢出和舍入。

举个例子,一个简单的浮点乘法:

// 浮点版本
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。嗯,这就是典型的「精度饿死」问题。

怎么分析精度?我建议用「误差注入法」:

  1. 用浮点仿真跑一遍,得到「黄金参考值」。
  2. 用定点仿真跑一遍,得到「实测值」。
  3. 计算误差:绝对误差、相对误差、均方根误差。

如果误差太大,就调整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位的定点数,你会怎么分配整数和小数位?为什么?