4、定点数基础:Q格式表示、定点加减乘除、溢出与饱和处理、舍入模式(截断、四舍五入、收敛舍入)

在CMSIS-DSP滤波器链路设计中,浮点运算虽然直观,但在嵌入式MCU(特别是无FPU的Cortex-M0/M3内核)上,浮点运算会带来显著的周期开销与代码体积膨胀。定点数(Fixed-Point)通过将实数映射到整数域,利用硬件整数运算单元完成信号处理,是工业级音频、电机控制、传感器融合等场景的基石。本章将系统讲解Q格式的数学本质、定点四则运算的位宽管理、溢出与饱和策略,以及三种舍入模式的工程取舍,为后续FIR/IIR滤波器定点化实现打下坚实基础。

定点数运算链路与Q格式映射 Q格式表示 Qm.n (m整数位, n小数位) 数值范围: [-2^(m-1), 2^(m-1)-2^(-n)] 精度: 2^(-n) 例: Q1.15 → [-1, 0.99997] 定点四则运算 加/减: 需Q格式对齐 乘法: 位宽翻倍, 需移位 除法: 先移位再除 CMSIS-DSP: q15/q31 溢出与舍入 饱和: 钳位到最大/最小值 截断: 直接丢弃低位 四舍五入: 加0.5后截断 收敛舍入: 就近取偶 CMSIS-DSP 定点滤波器链路 arm_fir_q15 / arm_biquad_cascade_df1_q31 内部累加器: q63 (64位) 防溢出 输出: 饱和 + 舍入 → q15/q31 定点化核心:格式选择 → 运算位宽管理 → 溢出/舍入策略 → CMSIS-DSP API映射

4.1 Q格式的数学本质与表示方法

Q格式(Qm.n)是一种将实数映射到整数域的定点表示法,其中m表示整数部分位数(含符号位),n表示小数部分位数。对于32位系统,常见的Q15(即Q1.15)表示16位有符号定点数,Q31(Q1.31)表示32位定点数。其核心思想是:实数x = 整数码值 × 2^(-n)。例如,Q15格式下,整数码值32767对应实数32767 × 2^(-15) = 0.999969,而-32768对应-1.0。

选择Q格式时需要权衡动态范围与精度。动态范围由整数位m决定,精度由小数位n决定。对于音频信号(通常幅度在±1.0内),Q15足够;对于中间累加结果(如FIR滤波器卷积和),可能需要Q31甚至Q63(CMSIS-DSP内部累加器)。工程上,Q格式的选择应遵循“先定范围,再定精度”的原则:先估算信号最大幅度,确定m;再根据所需信噪比确定n。

Q格式 位宽 数值范围 精度 典型应用
Q1.15 16位 [-1.0, 0.99997] 3.05e-5 音频采样、FIR系数
Q1.31 32位 [-1.0, 0.9999999995] 4.66e-10 高精度滤波、IIR状态
Q8.23 32位 [-128.0, 127.9999999] 1.19e-7 传感器数据融合
Q16.15 32位 [-32768, 32767.99997] 3.05e-5 电机控制、功率转换
关键认知:Q格式的本质是“整数码值 + 缩放因子”。在C语言中,Q15变量就是int16_t,Q31就是int32_t。所有定点运算都是整数运算,只是在乘法/除法后需要额外的移位操作来恢复正确的缩放因子。

4.2 定点加减乘除运算规则

加减法:两个定点数相加/减,必须保证Q格式一致(即小数点对齐)。若Q格式不同,需先通过左移/右移将较小精度数对齐到较大精度。例如,Q15数a与Q31数b相加,需先将a左移16位转换为Q31,再执行加法。CMSIS-DSP中,arm_add_q15要求两个输入均为Q15,输出也为Q15,内部自动处理饱和。

乘法:定点乘法的位宽会翻倍。Qm1.n1 × Qm2.n2 = Q(m1+m2).(n1+n2)。例如,两个Q15相乘得到Q30(实际为Q2.30,因为两个符号位相乘会多出1位整数位)。工程上通常需要将结果右移n位以回到目标Q格式。CMSIS-DSP提供arm_mult_q15,内部将两个Q15相乘后左移1位再截断为Q15,避免溢出。

除法:定点除法需要先将被除数左移n位(n为目标小数位数),再执行整数除法。例如,Q15数a除以Q15数b,结果为Q15,需执行(int16_t)(((int32_t)a << 15) / b)。注意CMSIS-DSP没有直接提供Q15除法函数,通常用arm_div_q15(基于牛顿-拉夫逊迭代)或自行实现。

// 示例:Q15乘法(手工实现)
int16_t q15_mult(int16_t a, int16_t b) {
    int32_t temp = (int32_t)a * b;  // 结果为Q30
    temp >>= 15;                     // 右移15位回到Q15
    // 饱和处理
    if (temp > 32767) temp = 32767;
    if (temp < -32768) temp = -32768;
    return (int16_t)temp;
}

// 示例:Q15除法(手工实现)
int16_t q15_div(int16_t a, int16_t b) {
    if (b == 0) return 0;  // 除零保护
    int32_t temp = ((int32_t)a << 15) / b;  // 先左移15位
    // 饱和处理
    if (temp > 32767) temp = 32767;
    if (temp < -32768) temp = -32768;
    return (int16_t)temp;
}

4.3 溢出检测与饱和处理策略

定点运算最危险的场景是溢出——当运算结果超出目标位宽可表示范围时,数值会回绕(wrap-around),导致严重失真。例如,Q15数32767加1会变成-32768,这在音频中表现为爆音。饱和处理(Saturation)将结果钳位到最大或最小值,避免回绕。

CMSIS-DSP中,几乎所有Q15/Q31运算函数都内置饱和逻辑。例如arm_add_q15内部使用SSAT指令(Cortex-M3/M4)或软件比较实现饱和。对于自定义运算,需手动添加饱和判断:

// 饱和加法示例
int16_t sat_add_q15(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;
}

溢出检测有两种常用方法:位扩展检测法(将操作数扩展到更宽位宽后运算,检查结果是否超出目标范围)和符号位检测法(比较运算前后符号位是否变化)。在ARM Cortex-M系列中,可利用QADD/QSUB指令(如果编译器支持内联汇编)或CMSIS提供的__QADD8等内置函数。

风险提示:在IIR滤波器(如双二阶滤波器)中,状态变量可能持续累积,即使输入信号很小,内部节点也可能溢出。因此,IIR定点实现必须使用Q31格式并配合饱和,或者使用CMSIS-DSP的arm_biquad_cascade_df1_q31(内部使用Q63累加器)。

4.4 舍入模式:截断、四舍五入与收敛舍入

当定点运算需要从高精度降低到低精度时(如Q31转Q15),必须丢弃低位比特。不同的舍入模式会影响信号的信噪比和直流偏移。

  • 截断(Truncation):直接丢弃低位,等效于向下取整(对于正数)或向上取整(对于负数)。实现最简单(直接右移),但会引入直流偏移(bias),平均误差为-0.5 LSB。
  • 四舍五入(Round-to-Nearest):在截断前加上0.5 LSB(即1 << (n-1)),然后右移。实现为(x + (1 << (n-1))) >> n。消除了直流偏移,但最大值可能溢出(如32767四舍五入后变为32768),需要额外饱和。
  • 收敛舍入(Round-to-Even):当结果恰好为0.5 LSB时,向偶数舍入。避免统计意义上的舍入偏差累积。实现较复杂,通常用于高精度科学计算或需要严格无偏的场景。
舍入模式 实现方法 直流偏移 最大误差 适用场景
截断 x >> n -0.5 LSB 1 LSB 快速处理、误差不敏感
四舍五入 (x + (1<<(n-1))) >> n 0 0.5 LSB 音频、通用信号处理
收敛舍入 判断LSB后决定进位 0 0.5 LSB 高精度测量、控制环路
// 三种舍入模式实现(Q31转Q15)
int16_t truncate_q31_to_q15(int32_t x) {
    return (int16_t)(x >> 16);
}

int16_t round_q31_to_q15(int32_t x) {
    int32_t temp = x + (1 << 15);  // 加0.5 LSB
    if (temp > 0x7FFF0000) return 32767;  // 饱和
    return (int16_t)(temp >> 16);
}

int16_t round_even_q31_to_q15(int32_t x) {
    int32_t lsb = (x >> 16) & 1;
    int32_t bias = (1 << 15) - 1 + lsb;  // 收敛舍入偏置
    return (int16_t)((x + bias) >> 16);
}

4.5 实战案例:音频FIR滤波器定点化链路

假设我们需要在Cortex-M4(无FPU)上实现一个128阶FIR低通滤波器,输入为16位ADC采样(Q15格式),输出驱动16位DAC。浮点原型滤波器系数已设计完成,现需定点化。

初始参数:采样率48kHz,截止频率8kHz,通带纹波0.1dB,阻带衰减60dB。浮点系数范围[-0.02, 0.2],系数个数128。

定点化步骤:

  1. 系数Q格式选择:浮点系数最大绝对值0.2,远小于1.0,因此Q15足够。将浮点系数乘以32768后四舍五入取整。
  2. 累加器位宽:128阶乘加,每个乘积为Q15×Q15=Q30,128个累加结果最大可能达到128×0.2×1.0≈25.6,超出Q15范围。因此CMSIS-DSP内部使用Q63累加器(64位),确保不溢出。
  3. 输出处理:累加结果右移15位回到Q15,并做饱和处理。CMSIS-DSP的arm_fir_q15自动完成此操作。
  4. 舍入模式:在系数转换时使用四舍五入,避免系数误差引入直流偏移。
// 使用CMSIS-DSP实现
#define NUM_TAPS 128
q15_t fir_coeffs[NUM_TAPS];
q15_t fir_state[NUM_TAPS + 16];  // 状态缓冲区
arm_fir_instance_q15 S;

// 初始化:浮点系数转Q15
for (int i = 0; i < NUM_TAPS; i++) {
    fir_coeffs[i] = (q15_t)(float_coeffs[i] * 32768.0f + 0.5f);
}
arm_fir_init_q15(&S, NUM_TAPS, fir_coeffs, fir_state, 128);

// 每个采样点处理
q15_t input = (q15_t)(adc_value);  // ADC 16位直接作为Q15
q15_t output;
arm_fir_q15(&S, &input, &output, 1);
dac_write(output);

结果对比:定点实现与浮点原型相比,通带纹波增加约0.05dB,阻带衰减降至58dB,满足系统指标(≥55dB)。处理周期从浮点的约1200周期/采样降至定点约180周期/采样,性能提升6.7倍。

经验总结:定点FIR滤波器设计的关键在于:①系数Q格式需保证精度(至少Q15);②累加器必须足够宽(Q63);③输出饱和必须启用;④系数转换时使用四舍五入而非截断。CMSIS-DSP已经封装好这些细节,但理解底层原理对调试异常输出至关重要。

4.6 常见误区与设计复盘

误区1:所有系数都用Q15。如果滤波器系数动态范围很大(如IIR反馈系数可能接近1.0),Q15精度不足,应使用Q31。判断标准:系数最小非零值对应的量化误差是否影响滤波器频率响应。

误区2:忽略中间节点溢出。在级联IIR实现中,每个二阶节的输出可能超过输入范围。必须逐级检查状态变量的最大值,必要时在级间插入缩放(scale)因子。

误区3:舍入模式一刀切。在反馈环路(如IIR)中,截断会导致极限环振荡(limit cycle),产生可闻噪声。此时应使用四舍五入或收敛舍入。而在前馈路径(如FIR)中,截断的直流偏移可通过后续高通滤波消除。

误区4:饱和处理只加在最终输出。在乘法链中,中间结果也可能溢出。例如Q15乘法后立即右移,但右移前结果可能超过int16范围。CMSIS-DSP的arm_mult_q15内部先左移1位再饱和,正是为了避免此问题。

复盘建议:在定点化调试时,建议先用MATLAB/Simulink的Fixed-Point Tool或Python的numpy.fixed_point进行位真仿真,对比定点与浮点输出差异。重点关注:①直流偏移(截断导致);②大信号下的饱和失真;③小信号下的量化噪声底。通过仿真提前发现位宽不足或舍入策略不当的问题,避免在硬件上反复调试。