四、常用算术运算的定点化:加法、乘法、除法、开方
算术运算是无线通信算法的基石。你想想看,从最简单的 FIR 滤波,到复杂的信道估计,底层全是加、乘、除、开方这些基本操作。定点化说白了,就是用有限的比特位去近似无限精度的实数运算。这里头的门道,我踩过的坑可不少。
核心原则:定点算术的核心是处理好 Q 格式(小数点位)的对齐与扩展。位宽每增加1比特,动态范围翻倍,但硬件成本也线性增长。
4.1 加法运算的定点实现
加法是最简单的,但也是最容易出问题的。我见过太多新手在加法上栽跟头——不是溢出,就是精度白白损失。
关键点:两个 Q 格式相同的数才能直接相加。如果格式不同,必须先对齐小数点。
// 示例:Q15 格式加法(16位有符号,1符号位+15小数位)
int16_t a_q15 = 0x4000; // 0.5
int16_t b_q15 = 0x6000; // 0.75
int32_t sum_q30; // 为防止溢出,用32位暂存
sum_q30 = (int32_t)a_q15 + (int32_t)b_q15; // 结果:0x0000A000,即1.25
// 注意:两个Q15相加,结果自动变成Q15格式,但位宽需要扩展1位
我曾经犯过的错:在做一个 OFDM 接收机时,我直接用16位累加器做 IFFT 的蝶形运算。结果信噪比一高,累加器就溢出,星座图直接糊成一团。后来改成32位中间累加,问题才解决。
误差控制策略:
- 饱和截位:当结果超出表示范围时,直接取最大值或最小值。适合控制信号路径。
- 舍入截位:丢弃低位时,加上0.5再截断。能减小直流偏置,我一般用在数据路径。
- 噪声整形:把截断误差反馈到下一次运算。这个在 Σ-Δ 调制器里常用,普通算法用得少。
4.2 乘法运算的定点实现
乘法比加法麻烦一点。两个 Q15 数相乘,结果其实是 Q30 格式。你需要决定怎么处理这多出来的15位小数。
// 示例:Q15 * Q15 = Q30,再截断回Q15
int16_t a_q15 = 0x4000; // 0.5
int16_t b_q15 = 0x6000; // 0.75
int32_t prod_q30;
int16_t prod_q15;
prod_q30 = (int32_t)a_q15 * (int32_t)b_q15; // 0x18000000,即0.375
// 截断回Q15:右移15位,并做舍入
prod_q15 = (int16_t)((prod_q30 + 0x4000) >> 15); // 加0x4000做四舍五入
我的个人习惯:在做乘法链时,我倾向于保持中间结果的高精度(比如用Q30),只在最后输出时做一次截断。这样能最大程度保留信号质量。代价是多占一点寄存器,但现在的 FPGA 资源没那么金贵了。
乘法误差来源:
- 截断误差:丢弃低位小数时引入。误差大小取决于你保留的位数。
- 溢出误差:两个大数相乘,结果超过位宽。我一般用保护位(guard bits)来避免。
4.3 除法运算的定点实现
除法是定点化的老大难。硬件除法器面积大、延迟高,所以实际工程里很少直接用除法器。我常用的替代方案有几种。
方案一:查表法(LUT)
把除数的倒数预先存好,除法变成乘法。适合除数变化范围不大的场景。
// 示例:用查表法实现定点除法 y = x / d
// 假设 d 在 [0.5, 1.0) 范围内,用Q15格式
uint16_t lut_recip[256]; // 预存 1/d 的Q15值
// 查表
int16_t recip_q15 = lut_recip[(d_q15 >> 7) & 0xFF]; // 取高8位做索引
int32_t result_q30 = (int32_t)x_q15 * (int32_t)recip_q15;
方案二:牛顿-拉夫逊迭代法
用迭代逼近除数的倒数。收敛快,精度高,但需要多次迭代。
// 牛顿迭代求 1/d
int32_t estimate = 0x7FFF; // 初始猜测,约1.0
for (int i = 0; i < 3; i++) {
// estimate = estimate * (2 - d * estimate)
int32_t temp = (int32_t)d_q15 * estimate >> 15;
temp = 0x4000 - temp; // 2 - d*estimate,注意Q格式
estimate = (int32_t)estimate * temp >> 15;
}
避坑指南:我曾经在 LTE 上行信道估计里用除法器,结果时序跑不过。后来换成牛顿迭代法,3次迭代就能达到16位精度,面积还小了70%。
方案三:CORDIC 算法
只用移位和加法就能实现除法。适合硬件实现,但延迟较大。我一般在需要高精度且不着急的场景用。
4.4 开方运算的定点实现
开方在无线通信里很常见,比如功率计算、信道均衡。定点开方我推荐两种方法。
方法一:牛顿迭代法求平方根
// 求 x 的平方根,x 为 Q15 格式
int16_t sqrt_q15(int16_t x_q15) {
if (x_q15 <= 0) return 0;
int32_t guess = x_q15 >> 1; // 初始猜测
for (int i = 0; i < 4; i++) {
// guess = (guess + x/guess) / 2
int32_t temp = ((int32_t)x_q15 << 15) / guess; // x/guess,Q30格式
guess = (guess + (int16_t)(temp >> 15)) >> 1;
}
return guess;
}
方法二:CORDIC 算法求平方根
CORDIC 可以同时求平方根和反正切,适合需要多种运算的场景。我曾在 MIMO 检测器里用 CORDIC 统一处理开方和角度计算,省了不少资源。
我的经验:开方运算的精度和迭代次数直接相关。一般来说,4次牛顿迭代就能达到16位精度。如果要求不高,2次迭代也够用。我在做蓝牙接收机时,就用2次迭代,省了30%的功耗。
4.5 误差分析与性能调优
定点化的误差分析,说白了就是回答三个问题:误差有多大?误差怎么分布的?能不能接受?
| 运算类型 | 主要误差来源 | 典型误差范围(16位) | 调优方向 |
|---|---|---|---|
| 加法 | 溢出、截断 | ±1 LSB | 增加保护位、饱和处理 |
| 乘法 | 截断、舍入 | ±0.5 LSB | 保留更多中间位、舍入模式 |
| 除法 | 查表精度、迭代次数 | ±2 LSB(查表法) | 增大LUT、增加迭代 |
| 开方 | 迭代收敛、初始猜测 | ±1 LSB(4次迭代) | 优化初始值、增加迭代 |
性能调优的几条铁律:
- 先仿真,后实现:我习惯先用浮点模型跑通,再定点化。定点化后一定要做 bit-true 比对。
- 关注最差情况:不要只看平均误差。无线通信里,峰值误差往往决定了系统性能。
- 善用工具:MATLAB 的 Fixed-Point Designer 很好用。我一般用它做位宽扫描,找到性能和资源的平衡点。
- 留有余量:设计时给信号路径留2-3比特的余量。别问我怎么知道的——问就是流片回来发现不够用。
总结一下:定点化不是简单的截位,而是一个系统工程。加法注意溢出,乘法注意截断,除法和开方选对算法。每一步都做误差分析,才能保证最终性能。