当前位置:首页 > EDA > 电子设计自动化
[导读]无FPU的Cortex-M0/M0+/M3上写float,编译器会引入软浮点库,乘法可能几十到上百周期、额外占数KB Flash,实时回路很容易抖。替代思路是用Q格式把“小数点位置定死”:一个整数乘2^n当小数用,运算全走整数加减、乘法和移位。


无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全整数化,既保实时又控误差。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭