嵌入式C语言浮点数运算优化:无FPU MCU上的高效定点数替代方案
Cortex-M0/M3(无FPU)上做浮点运算,编译器会调用软浮点库,一条float乘法可能消耗几十到上百个CPU周期。对于传感器校准、PID控制、音频处理等场景,浮点运算占比过高会导致系统响应迟缓。定点数(fixed-point)用整数模拟小数,完全绕过浮点库,速度提升数倍,且精度可控。本文以温度PID控制器为例,展示如何将浮点运算改造成高效的Q15定点数。
一、定点数原理:用整数表示小数
定点数的核心是约定一个“小数点”的位置。例如Q15格式:16位有符号整数,低15位表示小数部分,最高位为符号位,取值范围-1到0.999969。数值 = 整数编码 / 32768。加法直接相加,乘法后需要右移15位恢复比例。
// Q15定点数定义
typedef int16_t q15_t;
// 浮点转定点
q15_t float_to_q15(float f) {
if (f > 0.999969f) f = 0.999969f;
if (f < -1.0f) f = -1.0f;
return (q15_t)(f * 32768.0f);
}
// 定点转浮点(调试用,实际运行不需要)
float q15_to_float(q15_t q) {
return (float)q / 32768.0f;
}
// Q15乘法:a*b,结果右移15位
q15_t q15_mul(q15_t a, q15_t b) {
return (q15_t)(((int32_t)a * (int32_t)b) >> 15);
}
注意:乘法前需要将操作数扩展到32位,否则16位相乘会溢出。
二、浮点PID改造为定点PID
一个标准的浮点PID增量式算法:
float kp = 0.5f, ki = 0.1f, kd = 0.05f;
float err, last_err, integral, derivative, output;
void pid_update(float setpoint, float feedback) {
err = setpoint - feedback;
integral += err;
derivative = err - last_err;
output = kp*err + ki*integral + kd*derivative;
last_err = err;
}
改造为Q15定点数,需要将系数和变量都转为Q15格式:
#define Q15_0_5 16384 // 0.5 * 32768
#define Q15_0_1 3277 // 0.1 * 32768
#define Q15_0_05 1638 // 0.05 * 32768
q15_t q_err, q_last_err, q_integral, q_derivative, q_output;
q15_t q_kp = Q15_0_5;
q15_t q_ki = Q15_0_1;
q15_t q_kd = Q15_0_05;
void pid_update_q15(q15_t q_setpoint, q15_t q_feedback) {
q_err = q_setpoint - q_feedback;
q_integral += q_err;
q_derivative = q_err - q_last_err;
// output = kp*err + ki*integral + kd*derivative
q_output = q15_mul(q_kp, q_err) +
q15_mul(q_ki, q_integral) +
q15_mul(q_kd, q_derivative);
q_last_err = q_err;
}
注意:积分项q_integral可能会溢出,需要根据实际系统选择合适的Q格式或增加防饱和处理。
三、性能实测对比
在STM32G030(Cortex-M0+,64MHz)上测试1000次PID运算:
方案 耗时(us) 代码大小(Flash)
浮点(软浮点) 1820 4.2KB
Q15定点 240 0.6KB
定点数速度快7.5倍,Flash占用减少85%。对于需要高频率执行的PID环路,这个差距决定了系统能否跑起来。
四、精度与范围权衡
Q15的精度约为0.00003(1/32768),对于大多数传感器反馈(12位ADC)绰绰有余。如果数值范围超过[-1,1),可以选用Q31(32位,精度更高)或自定义Q格式。例如Q24表示范围[-128,128),精度1/16777216。选择原则:先确定最大值范围,再确定所需精度,算出小数位数。
// Q24格式示例:32位,8位整数+24位小数
typedef int32_t q24_t;
#define Q24_SCALE 16777216.0f
q24_t float_to_q24(float f) {
return (q24_t)(f * Q24_SCALE);
}
q24_t q24_mul(q24_t a, q24_t b) {
return (q24_t)(((int64_t)a * (int64_t)b) >> 24);
}
五、三个翻车高发点
乘法溢出:Q15乘法必须先用32位暂存再移位,否则16位相乘直接溢出。使用int32_t中间变量。
加法饱和:多个Q15数相加可能超过32767,导致符号翻转。建议在累加前缩放到Q14或使用32位累加器。
常数转换错误:浮点0.1转换为Q15是3277,但0.1 * 32768=3276.8,四舍五入后误差0.006%。如果系数精度要求高,用roundf或手动调整。





