嵌入式C语言浮点数运算替代:无FPU芯片上定点数运算的精度控制技巧
无FPU的Cortex-M0/M0+/M3上写float,编译器会引入软浮点库,乘法可能几十到上百周期、额外占数KB Flash,实时回路很容易抖。替代思路是用Q格式把“小数点位置定死”:一个整数乘2^n当小数用,运算全走整数加减、乘法和移位。
一、先选Q格式:范围比精度更先定
Qm.n表示总位宽里m位整数(含符号)、n位小数;实数x转定点为round(x×2^n),回转除以2^n。常用取舍:
Q15(16位):范围约-1~0.99997,精度1/32768≈3.05e-5,适合归一化信号、滤波系数;
Q31(32位):精度约4.66e-10,适合高精度积分、控制输出;
Q7.8/Q8.8(16位):范围约±128,适合温度、电压、标定系数;
Q16.16(32位):范围±32768、精度1.5e-5,适合大动态累加。
规则:先列信号最大绝对值,留20%~30%余量定整数位;再按允许误差定小数位。ADC 12位、量纲0~3.3V,用Q12存电压小数足够,不必硬上Q15。
二、加减要对齐Q、防溢出
同Q格式直接加减,不同Q先移位对齐再算;结果用更宽类型暂存并饱和。
typedef int16_t q15_t;
typedef int32_t q31_t;
static inline q15_t q15_add_sat(q15_t a, q15_t b){
q31_t r = (q31_t)a + b;
if (r > 32767) r = 32767;
if (r < -32768) r = -32768;
return (q15_t)r;
}
多步累加(比如求平均值、积分)别一直用int16,中间转q31甚至int64,最后再右移回目标Q,避免多次截断把偏置放大。
三、乘法先乘后移,舍入别漏
两个Q15相乘得到Q30,两个Q31相乘得到Q62;必须宽类型暂存,右移n位还原,右移前加半步长做四舍五入。
static inline q15_t q15_mul_round(q15_t a, q15_t b){
q31_t p = (q31_t)a * b; // Q30
p += (1 << 14); // 四舍五入
p >>= 15; // 回Q15
if (p > 32767) p = 32767;
if (p < -32768) p = -32768;
return (q15_t)p;
}
static inline q31_t q31_mul_round(q31_t a, q31_t b){
int64_t p = (int64_t)a * b; // Q62
p += (1LL << 30);
p >>= 31;
return (q31_t)p;
}
常数乘可用移位替代,例如×0.5用>>1、×0.75用((x>>1)+(x>>2)),省乘法;但常数不是2的负幂时仍走乘加移,保证可复现。
四、除法与标度转换
A/B同Q格式,直接整数除会丢掉小数;把被除数先左移n位再除:
static inline q15_t q15_div_round(q15_t a, q15_t b){
if (b == 0) return 0;
q31_t r = ((q31_t)a << 15) + (b >> 1);
return (q15_t)(r / b);
}
ADC原始码转工程量建议分两步:原始值先减零点、乘标定系数(Q格式),再右移输出。比如12位ADC、参考3.3V:
#define Q12_ONE 4096
q15_t adc_to_q15(uint16_t raw){
q31_t v = (q31_t)raw * 3300; // 0~4095 -> 0~3300mV,中间用32位
v = (v << 15) / Q12_ONE; // 转Q15毫伏/千
return (q15_t)(v >> 15);
}
这样主回路全整数,串口打印再转float,不影响实时路径。
五、控制算法落地:一阶低通+增量PID
一阶低通y=k·x+(1-k)·y,k用Q15:
q15_t lpf_q15(q15_t x, q15_t *y, q15_t k){
q31_t t = (q31_t)k * x + (q31_t)(32767-k) * (*y);
t += (1<<14); t >>= 15;
*y = (q15_t)t; return *y;
}
增量PID误差e、输出du均Q15,积分累加用q31防饱和:
void pid_inc_q15(q15_t set, q15_t fb, q15_t kp, q15_t ki, q15_t kd,
q15_t *i, q15_t *pe, q15_t *out){
q15_t e = set - fb;
q31_t pi = (q31_t)kp * e;
*i += (q31_t)ki * e; // 积分器Q30级
if (*i > 0x3FFFFFFF) *i = 0x3FFFFFFF;
if (*i < -0x40000000) *i = -0x40000000;
q31_t pd = (q31_t)kd * (e - *pe);
q31_t du = pi + (*i >> 15) + pd;
du += (1<<14); du >>= 15;
*pe = e;
if (du > 32767) du = 32767; if (du < -32768) du = -32768;
*out = (q15_t)du;
}
M0+上Q15 PID比软浮点明显更省周期和Flash,工程样例可到数百微秒级千次运算、代码体积小几倍。
六、精度控制清单
常量系数编译期转Q:#define Kp_Q15 (int16_t)(0.5 * 32768.0f+0.5);
所有乘法中间用int32/int64,右移前舍入;
加减不同Q先统一标度,别混用raw和Q15;
积分、求和用更宽累加器,输出再饱和;
上板用“浮点黄金向量”对照:同输入跑float和Q版,统计最大误差、均方误差,再反调n位。
按这套做,无FPU MCU也能把传感器标定、滤波、PID全整数化,既保实时又控误差。





