IEEE 754 的真正目的

不是为了加密,而是为了解决这个问题:

 // 问题:不同设备之间怎么传输小数?
 ​
 设备A(你的STM32):
 float voltage = 3.3f;  // 怎么发给设备B?
 ​
 设备B(上位机):
 // 怎么知道收到的是 3.3?

如果没有统一标准,就会出现这种混乱:

 厂商A:用2字节定点数(3.3 → 0x014D,精度0.01)
 厂商B:用4字节定点数(3.3 → 0x00000CCD,精度0.0001)
 厂商C:用字符串(3.3 → "3.30",占4字节)
 厂商D:用自定义格式...
 ​
 结果:互相看不懂!❌

IEEE 754 的作用 = 大家说好都用同一种格式存小数,就像:

  • 全世界都用做长度单位

  • 全世界都用UTF-8编码文本

  • 全世界都用IEEE 754表示浮点数

原理

IEEE 754 是表示浮点数的国际标准,在本次比赛报文的设计中要求使用单精度 32 位浮点数格 式来表示测量值。

32 位单精度浮点数由三部分组成:

  • 符号位(S): 1 位(最高位)

  • 指数位(E): 8 位(中间)

  • 尾数位(M): 23 位(最低位)

结构表示: [S] [EEEEEEEE] [MMMMMMMMMMMMMMMMMMMMMMM]

浮点数值计算公式为(−1)S (1.M) 2E−127

其中:

S: 0 表示正数, 1 表示负数

M:小数部分的二进制表示

E:指数字段的无符号整数值

127:单精度浮点数的偏移值

以 40533333 为例,将其转换为二进制为 0 10000000 10100110011001100110011 ,其中符号位 S=0(正数),指数 E=10000000(二进制)=128,尾数 M=10100110011001100110011。

代入公式(−1)S (1.M) 2E−127 计算得到结果 = 1.10100110011001100110011×2^1 ≈ 3.3V。

原理的讲解

一、为什么需要IEEE 754?
问题:计算机只认识0和1,怎么存小数?
 整数很简单:
 int a = 100;  // 二进制:01100100
 ​
 但小数怎么办?
 float b = 3.14;  // 二进制:???

IEEE 754标准就是定义了一套规则,把小数转成二进制存储!


二、IEEE 754单精度(32位)结构

核心结构(就像身份证号码)
 32位浮点数 = [符号位(1位)][指数位(8位)][尾数位(23位)]
 ​
 例如:3.3V 的IEEE 754表示
 二进制:0 10000000 10100110011001100110011
        ↑     ↑               ↑
       符号   指数            尾数
        S      E               M
三个部分的作用
部分 位数 作用 例子
符号位(S) 1位 0=正数,1=负数 0 = 正数
指数位(E) 8位 决定小数点位置 10000000 = 128
尾数位(M) 23位 存储有效数字 10100110011...

三、计算公式(核心!)

公式:
 浮点数 = (-1)^S × (1.M) × 2^(E-127)

解释

  • (-1)^S:决定正负

  • (1.M):有效数字(1.xxx的形式)

  • 2^(E-127):决定数量级

为什么指数要减127?

127是偏移量(bias),用来表示负指数:

 E的范围:0~255(8位能表示)
 实际指数:E-127 = -127~128
 ​
 例如:
 E=127 → 实际指数 = 0   → 2^0 = 1
 E=128 → 实际指数 = 1   → 2^1 = 2
 E=126 → 实际指数 = -1  → 2^-1 = 0.5

四、实际例子:把 0x40533333 转成 3.3V

步骤1:拆分二进制
 十六进制:0x40533333
 转二进制:0100 0000 0101 0011 0011 0011 0011 0011
 ​
 拆分:
 [0] [10000000] [10100110011001100110011]
  ↑      ↑              ↑
  S      E              M
步骤2:提取各部分
 S = 0               → 正数
 E = 10000000(二进制) = 128(十进制)
 M = 10100110011001100110011(二进制)
步骤3:代入公式
 浮点数 = (-1)^0 × (1.10100110011001100110011) × 2^(128-127)
        = 1 × 1.10100110011001100110011(二进制) × 2^1
        = 1.10100110011001100110011(二进制) × 2
步骤4:计算尾数部分
 1.10100110011001100110011(二进制) 转十进制:
 ​
 整数部分:1
 小数部分:
   1×2^-1 + 0×2^-2 + 1×2^-3 + 0×2^-4 + ...
 = 0.5 + 0.125 + 0.0078125 + ...
 ≈ 0.649999...
 ​
 所以:1.649999... × 2 ≈ 3.3

C语言实现(超简单)

方法1:用联合体(Union)直接转换

 #include <stdint.h>
 #include <stdio.h>
 ​
 // 定义联合体(妙招!)
 typedef union {
     float f;      // 浮点数
     uint32_t u;   // 32位整数
 } FloatConverter;
 ​
 // IEEE 754 转浮点数
 float ieee754ToFloat(uint32_t ieee_value) {
     FloatConverter converter;
     converter.u = ieee_value;  // 把整数赋值给u
     return converter.f;        // 直接读出浮点数!
 }
 ​
 // 浮点数转 IEEE 754
 uint32_t floatToIEEE754(float value) {
     FloatConverter converter;
     converter.f = value;       // 把浮点数赋值给f
     return converter.u;        // 直接读出整数!
 }
 ​
 int main(void) {
     // 测试:0x40533333 → 3.3
     uint32_t ieee_val = 0x40533333;
     float result = ieee754ToFloat(ieee_val);
     printf("0x%08X → %.2f\n", ieee_val, result);
     // 输出:0x40533333 → 3.30
     
     // 测试:100.0 → IEEE 754
     float test = 100.0f;
     uint32_t ieee = floatToIEEE754(test);
     printf("%.2f → 0x%08X\n", test, ieee);
     // 输出:100.00 → 0x42C80000
     
     return 0;
 }

原理:联合体的内存共享,floatuint32_t都是32位,直接读取同一块内存!

在串口协议中的应用

场景:传输电压值 3.3V

 // ===== 发送端 =====
 float voltage = 3.3f;
 ​
 // 转成IEEE 754
 uint32_t ieee_val = floatToIEEE754(voltage);
 // 结果:0x40533333
 ​
 // 拆成4个字节发送(大端序)
 uint8_t data[4];
 data[0] = (ieee_val >> 24) & 0xFF;  // 0x40
 data[1] = (ieee_val >> 16) & 0xFF;  // 0x53
 data[2] = (ieee_val >> 8) & 0xFF;   // 0x33
 data[3] = ieee_val & 0xFF;          // 0x33
 ​
 // 通过串口发送
 HAL_UART_Transmit(&huart1, data, 4, 100);
 ​
 // ===== 接收端 =====
 uint8_t recv[4];  // 接收到:[0x40, 0x53, 0x33, 0x33]
 ​
 // 组合成32位整数(大端序)
 uint32_t ieee_recv = (recv[0] << 24) | (recv[1] << 16) | 
                      (recv[2] << 8) | recv[3];
 // 结果:0x40533333
 ​
 // 转成浮点数
 float voltage_recv = ieee754ToFloat(ieee_recv);
 // 结果:3.3

常见浮点数的IEEE 754表示

浮点数 IEEE 754(十六进制) 二进制拆分
0.0 0x00000000 0 00000000 00000000000000000000000
1.0 0x3F800000 0 01111111 00000000000000000000000
2.0 0x40000000 0 10000000 00000000000000000000000
3.3 0x40533333 0 10000000 10100110011001100110011
100.0 0x42C80000 0 10000101 10010000000000000000000
-1.0 0xBF800000 1 01111111 00000000000000000000000

特殊值(了解即可)

IEEE 754 说明
+∞ (正无穷) 0x7F800000 指数全1,尾数全0
-∞ (负无穷) 0xFF800000 符号位1,指数全1,尾数全0
NaN (非数字) 0x7FC00000 指数全1,尾数非0

为什么比赛要用IEEE 754?

优点

  1. 标准化:全世界都用这个标准

  2. 硬件支持:CPU直接支持,计算快

  3. 精度高:单精度能表示7位有效数字

  4. 范围大:能表示±3.4×10^38

对比其他方案

方案 优点 缺点
整数×100 简单 精度低(只能到小数点后2位)
定点数 可控 需要自己设计格式
IEEE 754 标准、通用 需要理解格式

调试技巧

在线工具验证

搜索"IEEE 754 converter",输入十六进制值验证:

 输入:0x40533333
 输出:3.29999995231628418

用printf调试

 float val = 3.3f;
 uint32_t ieee = floatToIEEE754(val);
 ​
 printf("浮点数:%.10f\n", val);
 printf("IEEE 754:0x%08X\n", ieee);
 printf("二进制:");
 for (int i = 31; i >= 0; i--) {
     printf("%d", (ieee >> i) & 1);
     if (i == 31 || i == 23) printf(" ");  // 分隔符
 }
 printf("\n");

输出:

 浮点数:3.3000000000
 IEEE 754:0x40533333
 二进制:0 10000000 10100110011001100110011

常见坑点

坑1:字节序问题

// 错误:直接memcpy可能有字节序问题
 float val = 3.3f;
 uint8_t bytes[4];
 memcpy(bytes, &val, 4);  // 在STM32上可能是小端序!
 ​
 // 正确:用联合体 + 显式拆分
 uint32_t ieee = floatToIEEE754(val);
 bytes[0] = (ieee >> 24) & 0xFF;  // 大端序
 bytes[1] = (ieee >> 16) & 0xFF;
 bytes[2] = (ieee >> 8) & 0xFF;
 bytes[3] = ieee & 0xFF;

坑2:精度损失

 float val = 3.3f;
 uint32_t ieee = floatToIEEE754(val);
 float restored = ieee754ToFloat(ieee);
 ​
 printf("%.20f\n", val);       // 3.29999995231628418000
 printf("%.20f\n", restored);  // 3.29999995231628418000
 // 注意:不是精确的3.3!

两个坑,我们给出以下代码

 #include <stdint.h>
 #include <math.h>
 ​
 // ===== IEEE 754 工具函数 =====
 typedef union {
     float f;
     uint32_t u;
 } FloatConverter;
 ​
 uint32_t floatToIEEE754(float value) {
     FloatConverter c;
     c.f = value;
     return c.u;
 }
 ​
 float ieee754ToFloat(uint32_t value) {
     FloatConverter c;
     c.u = value;
     return c.f;
 }
 ​
 // ===== 大端序发送/接收(解决坑1)=====
 void send_float_safe(UART_HandleTypeDef *huart, float value) {
     uint32_t ieee = floatToIEEE754(value);
     uint8_t bytes[4];
     bytes[0] = (ieee >> 24) & 0xFF;  // 大端序
     bytes[1] = (ieee >> 16) & 0xFF;
     bytes[2] = (ieee >> 8) & 0xFF;
     bytes[3] = ieee & 0xFF;
     HAL_UART_Transmit(huart, bytes, 4, 100);
 }
 ​
 float receive_float_safe(uint8_t *bytes) {
     uint32_t ieee = (bytes[0] << 24) | (bytes[1] << 16) | 
                     (bytes[2] << 8) | bytes[3];
     return ieee754ToFloat(ieee);
 }
 ​
 // ===== 定点数方案(解决坑2)=====
 #define FIXED_SCALE 1000
 ​
 int32_t float_to_fixed(float value) {
     return (int32_t)(value * FIXED_SCALE + 0.5f);
 }
 ​
 float fixed_to_float(int32_t value) {
     return (float)value / FIXED_SCALE;
 }
 ​
 // ===== 浮点数比较(解决坑2)=====
 #define EPSILON 0.0001f
 ​
 bool float_equals(float a, float b) {
     return fabsf(a - b) < EPSILON;
 }
 ​
 // ===== 使用示例 =====
 int main(void) {
     // 方案A:用IEEE 754 + 安全发送
     float voltage1 = 3.3f;
     send_float_safe(&huart1, voltage1);
     
     uint8_t recv1[4];
     float voltage1_recv = receive_float_safe(recv1);
     printf("方案A:%.2f V\n", voltage1_recv);  // 3.30 V
     
     // 方案B:用定点数(更精确)
     int32_t voltage2_fixed = float_to_fixed(3.3f);  // 3300
     // 发送 voltage2_fixed(4字节整数,用之前讲的方法)
     
     float voltage2_recv = fixed_to_float(3300);
     printf("方案B:%.3f V\n", voltage2_recv);  // 3.300 V(精确!)
     
     return 0;
 }

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐