【第05期】数据的微观世界 (五) —— 浮点数 vs 定点数:MCU的数学课

在PC软件开发中,我们习惯了随手写下 double a = 3.14;。但在嵌入式领域,尤其是资源受限的MCU(如Cortex-M0/M3或8位机)上,随意使用浮点数可能是性能的"隐形杀手",也是通信故障的"幕后黑手"

这一期,我们深入MCU的数学世界,解开浮点数从存储运算传输的所有秘密。


1. 浮点数的"富贵病" (性能代价)

我们先看一行平平无奇的代码:

// 假设在一个没有FPU的单片机上(如STM32F103)

float voltage = adc_value * 3.3f / 4095.0f;

你以为CPU只是做了两次简单的乘除法?

实际上,编译器在背后为你引入了数百行汇编代码。

1.1 软浮点 (Soft-Float) 的代价

大多数低端MCU没有硬件浮点运算单元 (FPU)。

当你写下 a * b(如果是float)时,编译器会调用一个巨大的库函数(例如 __aeabi_fmul),用一堆整数移位、加减法逻辑来模拟浮点运算。

  • 执行速度暴跌

    • 整数乘法:约 1~3 个时钟周期。

    • 软浮点乘法:约 50~100 个时钟周期

    • 软浮点除法:高达数百个周期。

      如果这段代码在中断服务函数 (ISR) 或 高频控制回路(如电机FOC)里,系统极易过载。

1.2 即使有FPU也有坑

即使你用的是带FPU的芯片(如STM32F4/H7),也不要掉以轻心:

  1. Double的陷阱 :大多数MCU的FPU只支持单精度 (float) 。如果你写了 3.14(C语言默认是double),FPU会罢工,转而调用慢速的软件库。修正:必须显式加 f 后缀 -> 3.14f

  2. 上下文切换:在RTOS中,任务使用FPU会导致上下文切换时需要保存额外的FPU寄存器,增加了中断延迟和内存开销。


2. 深入解剖:浮点数的"肉体" (IEEE 754 存储详解)

在内存里,int32_t a = 12 存的是 0x0000000C,直观易懂。

但是,float f = 12.5 在内存里存的是 0x41480000。

这俩长得完全没关系!为什么?

因为MCU遵循 IEEE 754 单精度浮点数标准,它把 32个 Bit 切成了三块:

  1. 符号位 (Sign, 1 bit): 最高位 (Bit 31)。0代表正,1代表负。

  2. 指数位 (Exponent, 8 bits): Bit 23-30。用于存储科学计数法中的 2^N。

    • 注意:采用"移码" (Bias) 设计,真实指数 = 存储值 - 127。
  3. 尾数位 (Mantissa, 23 bits): Bit 0-22。存储小数点后的有效数字。

    • 注意:IEEE 754 省略了整数部分的 1,以节省一位空间。

内存透视实战:

当你在调试器 (Memory View) 里看到 00 00 48 41 (小端模式) 时,如何确认它是 12.5?

  1. 重组0x41480000 -> 二进制 0100 0001 0100 1000 ...

  2. 符号:0 -> 正数。

  3. 指数1000 0001 (129)。真实指数 129 - 127 = 2。即 2^2 = 4。

  4. 尾数1.100... (二进制)。1 + 0.5 = 1.5。

  5. 结果:4 x 1.5 = 6 ... 等等,这里只是简算,实际上尾数解析是 1 + 2^{-1} + 2^{-4} 等的组合。

    工程结论:你只需要知道,浮点数在内存里是一堆被编码过的位,不能直接当整数解读。


3. 工程实战:如何传输与存储浮点数?

这是嵌入式中最常见的场景:你采集了一个电压值 3.14159,现在要通过串口发给上位机,或者存进 Flash 里。

方法 A:ASCII 字符串法 (笨办法)

char buf[16];

sprintf(buf, "%.5f", val); // 转成 "3.14159"

HAL_UART_Transmit(&huart1, buf, strlen(buf), 100);

  • 缺点:慢(sprintf是耗时大户)、占带宽(发了7个字节)、精度丢失(文本转回数字会有误差)。

方法 B:联合体法 (Union) ------ 优雅且高效 (推荐)

利用 union 共用内存的特性,直接把这 4个 Byte 拿出来。

typedef union {

float f_val;

uint8_t bytes[4];

} FloatConverter_t;

void send_float(float val) {

FloatConverter_t u;

u.f_val = val;

// 直接发送原始字节,速度最快,精度无损

HAL_UART_Transmit(&huart1, u.bytes, 4, 100);

}

方法 C:指针强转法

float val = 3.14f;

uint8_t *p = (uint8_t*)&val; // 欺骗编译器,把float地址当char地址用

HAL_UART_Transmit(&huart1, p, 4, 100);

关键警告:大小端问题

只要传输原始字节,必须考虑大小端!

  • MCU端 (STM32) :小端模式,内存里是 A B C D (低字节在前)。

  • 接收端

    • 如果是 Java 上位机 / 网络传输:通常是大端,期待 D C B A

    • 如果是 x86 PC (C++):通常也是小端。

  • 对策:制定协议时必须明确规定(例如:使用网络字节序/大端)。如果不匹配,发送前要先反转数组顺序。


4. 浮点数的精度噩梦

除了慢和存储复杂,浮点数还有一个致命弱点:不精确

经典反直觉案例

float a = 1.1f;

if (a == 1.1f) {

// 这里很可能会判断为 false!

}

为什么?因为 1.1 在二进制里是一个无限循环小数(就像十进制里的 1/3),计算机存不下,只能截断。

工程铁律永远不要用 == 去比较两个浮点数! 必须使用"Epsilon"范围比较:

if (fabs(a - 1.1f) < 0.00001f) { ... }

5. 穷人的法拉利:定点数 (Fixed-Point)

如果想要速度快(整数运算),又想要小数(精度),怎么办?

答案是:定点数。本质就是**"缩放"**。

5.1 十进制缩放 (简单易懂)

  • 浮点思维 :电压 1.23 V

  • 定点思维 :电压 1230 mV (放大1000倍)。存成 int

5.2 二进制缩放 (Q格式 - 高效之王)

计算机做乘除1000比较慢,但移位 极快。我们把缩放因子设为 2^N。这就是 Q格式

假设使用 Q15 (缩放 2^15 = 32768):

  • 1.5 (浮点) -> 1.5 * 32768 = 49152 (定点整数)。

运算规则

  1. 加减法:直接加减。

  2. 乘法:结果会"变大",需要右移归一化。

#define Q_SHIFT 10 // 放大 2^10 = 1024 倍

#define TO_FIX(x) ((int32_t)((x) * (1 << Q_SHIFT)))

int32_t a = TO_FIX(1.5); // 1536

int32_t b = TO_FIX(2.0); // 2048

// 乘法:先乘,再右移

// 注意:乘法结果可能超过int32,必须强转int64暂存!

int32_t prod = (int32_t)(((int64_t)a * b) >> Q_SHIFT);

// 结果 3072 -> 代表 3.0

6. 总结:什么时候该用什么?

场景 推荐方案 理由
人机交互 (UI) 浮点数 屏幕刷新慢,开发效率优先,代码可读性好。
复杂算法 (FFT/滤波) 浮点数 (FPU) 现代MCU带FPU,不用白不用。
高频控制 (电机/电源) 定点数 (Q格式) 必须在几微秒内算完,且无FPU时唯一的选择。
数据传输 (串口/CAN) 原始字节 (Union) 效率最高,但需注意大小端。
低功耗设备 定点数 FPU耗电大,定点运算让CPU更快休眠
相关推荐
G_H_S_3_2 小时前
【网络运维】企业级监控平台Zabbix:部署与实践指南
linux·运维·网络·zabbix
小周学学学2 小时前
Vcenter Auto Deploy安装与使用
linux·运维·服务器
polarislove02142 小时前
9.2 自制延迟函数-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
Cincoze-Johnny2 小时前
Windows系统-应用问题全面剖析Ⅳ:德承工控机DV-1000在Windows操作系统下[桌面图标消失]的解决方法
单片机·嵌入式硬件
量子信使2 小时前
量子力学的两大护法:叠加与纠缠
物联网·安全·密码学·信息与通信·量子计算
智嵌电子2 小时前
【笔记篇】【硬件基础篇】模拟电子技术基础 (童诗白) 第7章 波形的发生和信号的转换
笔记·嵌入式硬件
微爱帮监所写信寄信2 小时前
微爱帮监狱写信寄信工具服务器【Linux篇章】再续:TCP协议——用技术隐喻重构网络世界的底层逻辑
linux·服务器·开发语言·网络·网络协议·小程序·监狱寄信
VekiSon3 小时前
Linux网络编程——IO多路复用
linux·运维·网络
旖旎夜光3 小时前
Linux(3)(上)
linux·学习