当0x1234变成0x3412:嵌入式通信里那个“反着来”的魔鬼细节
1. 什么是大端序和小端序?
假设一个 16 位数值0x1234,它由两个字节组成:高字节0x12,低字节0x34。它们在内存或通信流中的排列顺序有两种完全相反的约定。
| 模式 | 英文名 | 核心原则 | 0x1234在内存/通信流中的存储顺序 | 常见阵营 |
|---|---|---|---|---|
| 大端序 | Big-Endian | 高位字节存/发在低地址(前面) | 低地址/先发:0x12,然后:0x34 | 网络字节序(TCP/IP)、I2C 总线、SPI 总线、Motorola 处理器 |
| 小端序 | Little-Endian | 低位字节存/发在低地址(前面) | 低地址/先发:0x34,然后:0x12 | x86/AMD 处理器、STM32(Cortex-M)、龙芯 1C102(MIPS) |
记忆口诀:
大端序:高高在上,像我们读数字的习惯(先读高位"一百",再读低位"二十三")。
小端序:低前矮后,像反转的数字流。
2. 你学过的实例中,哪里会碰到?
实例一:MPU6050 的加速度数据(大端序通信)
MPU6050 的加速度 X 轴数据,存于ACCEL_XOUT_H (0x3B)和ACCEL_XOUT_L (0x3C)两个 8 位寄存器。它们是大端序传输:先读高字节,再读低字节。
你通过 I2C 连续读 0x3B 和 0x3C,会收到两个字节:
先收到的字节:0x12 后收到的字节:0x34
这两个字节拼成 16 位值:0x1234。因为先收到的0x12是高位,后收到的是低位。
你代码里的拼接必须是:
int16_t accel_x = (data_high << 8) | data_low; // 结果:accel_x = 0x1234这就是大端序解码。如果这是一台小端序机器,你正好在把通信流里的大端序数据转为处理器的小端序变量。
实例二:龙芯 1C102 的内存存储(小端序处理器)
你的龙芯 1C102 是 MIPS 架构,通常是小端序。当你在代码里定义一个 32 位变量uint32_t val = 0x12345678;时,它在内存中的实际存储顺序是:
| 内存地址 (低→高) | 存储的内容 |
|---|---|
| 0x1000 | 0x78(最低位字节) |
| 0x1001 | 0x56 |
| 0x1002 | 0x34 |
| 0x1003 | 0x12(最高位字节) |
这是小端序:低地址存低位字节。
3. 当大端序设备遇上小端序处理器
这就是嵌入式通信里端序冲突的根源。
设备端(MPU6050、RC522、网络数据包)几乎统一用大端序(这叫网络字节序,Network Byte Order)。
处理器端(龙芯 1C102、STM32、x86)大多用小端序(这叫主机字节序,Host Byte Order)。
当你把从 MPU6050 读到的两个字节0x12 0x34,用(high << 8) | low拼成0x1234时,你实际上在执行一次从大端序通信流到小端序处理器变量的隐式转换。因为你的 CPU 认为0x1234在内存里应该是0x34 0x12,但数据拼出来直接存入寄存器或变量时,编译器会帮你处理好,你不必手动翻转字节——只要你拼的顺序是高位在左、低位在右。
所以你的代码(data_high << 8) | data_low是通用的,无论 CPU 是大小端,只要通信协议是先发高字节(大端),就能正确还原成处理器期望的数值。
4. 端序问题的实战陷阱
面试中一个经典的“下马威”问题是这个:
题目:你在一个小端序的 MCU 上,从 I2C 设备读到了一个 16 位值,两个字节分别是0x12(先收到)和0x34(后收到)。你用memcpy直接把它拷贝到一个uint16_t变量里。这个变量的值是多少?
分析:
通信流里,先收到
0x12放在数组低地址buf[0],后收到0x34放在buf[1]。memcpy到uint16_t val,小端序 CPU 把低地址buf[0]的值当成低位。所以
val的内存布局是:低地址0x12(低位),高地址0x34(高位)。结果:
val = 0x3412,和你想表达的0x1234完全颠倒了。
正确做法:永远手动拼接,或用htons()/ntohs()这类字节序转换函数,不要直接 memcpy。