
前言你有没有遇到过类似情况明明是 0x1234解析出来却成了 0x3412网络发包收包整数总是 “倒过来”这不是代码 bug而是计算机底层最基础、最容易被忽略的概念 —— 字节序。今天我们从嵌入式系统的角度出发讲清楚字节序原理。目录一、什么是字节序二、大端与小端2.1 大小端定义2.2 如何判断大小端三、字节序转换四、字节序的一些应用4.1 socket地址结构4.2 BMP图像文件(小端)4.3 I2C协议设备一、什么是字节序字节序又称端序或尾序是指多字节数据在计算机内存中存储的顺序。简单来说就是“低位数据”和“高位数据”谁放在内存的低地址处的问题。对于单字节如char不存在字节序问题。但对于int、short、float等多字节数据不同的 CPU 架构可能有不同的存储习惯。二、大端与小端2.1 大小端定义假设有一个 16 位整数0x34562 个字节在内存中占用 2 个地址。我们假设低地址为0x1000高地址为0x1001。大端高位字节存储在低地址低位字节存储在高地址。如上提到的整数 0x3456 在大端模式下的存储结构如下网络字节序(TCP/IP协议标准)、PowerPC架构一般采用大端模式。小端高位字节存储在高地址低位字节存储在低地址。如上提到的整数 0x3456 在大端模式下的存储结构如下主流的 Intel x86、x64 架构ARM等通常默认运行在小端模式。在网络通信中如果通信双方的主机字节序不同一方是大端一方是小端直接传输数据会导致解析错误。为了解决这个问题TCP/IP 协议规定网络数据流必须采用大端模式。这被称为网络字节序。2.2 如何判断大小端这里推荐一个经典的方法利用联合体原理(所有成员共享同一内存地址)bool is_little_endian_union(void) { union { uint16_t val; // 2字节整体访问 uint8_t bytes[2]; // 逐字节访问 } test; test.val 0x0102; // 十六进制0x01是高字节0x02是低字节 // 小端机bytes[0]0x02, bytes[1]0x01 // 大端机bytes[0]0x01, bytes[1]0x02 return (test.bytes[0] 0x02); }三、字节序转换字节序转换的基本原理是将多字节数据的各个字节按照目标字节序重新排列。转换过程的核心是字节交换即数据的高位字节与低位字节的位置互换。C/C 提供了标准的库函数来处理字节序转换通常需要包含头文件arpa/inet.h标准转换函数1) uint16_t htons(uint16_t hostshort)主机字节序 → 网络字节序16 位用于端口号2) uint32_t htonl(uint32_t hostlong)主机字节序 → 网络字节序32 位用于 IP 地址3) uint16_t ntohs(uint16_t netshort)网络字节序 → 主机字节序16 位4) uint32_t ntohl(uint32_t netlong)网络字节序 → 主机字节序32 位也可自定义函数 进行转换例如16位主机字节序转网络字节序/** * brief 16位主机字节序转网络字节序大端 * param host: 主机字节序16位值 * retval 网络字节序16位值 */ uint16_t htons_custom(uint16_t host) { 移位操作清晰高效 return ((host 0x00FF) 8) | // 低字节移到高8位 ((host 0xFF00) 8); // 高字节移到低8位 }四、字节序的一些应用4.1 socket地址结构#include arpa/inet.h #include netinet/in.h struct sockaddr_in server_addr; // 必须转大端 server_addr.sin_family AF_INET; server_addr.sin_port htons(8080); // 端口号大端 server_addr.sin_addr.s_addr inet_addr(192.168.1.1); // 已是大端 // 错误示例小端主机直接赋值会失败 server_addr.sin_port 8080; // 错误80800x1F90网络看成0x901F368804.2 BMP图像文件(小端)/** * brief BMP文件头Windows格式强制小端 * note 网络用大端Windows文件格式用小端 */ #pragma pack(push, 1) typedef struct { uint16_t type; // BM 0x4D42小端存储 uint32_t size; // 文件大小小端 uint16_t reserved1; uint16_t reserved2; uint32_t offset; // 数据偏移小端 } bmp_file_header_t; typedef struct { uint32_t size; // 信息头大小小端 int32_t width; // 宽度小端有符号 int32_t height; // 高度小端有符号 uint16_t planes; uint16_t bit_count; // 色深小端 uint32_t compression; uint32_t size_image; int32_t x_pels_per_meter; int32_t y_pels_per_meter; uint32_t clr_used; uint32_t clr_important; } bmp_info_header_t; #pragma pack(pop) /** * brief 在小端主机直接写入即可 * note 在大端主机需要逐字节反转 */ void write_bmp_little_endian(FILE *fp, bmp_file_header_t *hdr) { #if defined(__BIG_ENDIAN__) // 大端主机必须转换每个字段 bmp_file_header_t le_hdr; le_hdr.type swap16(hdr-type); le_hdr.size swap32(hdr-size); // ... 所有字段转换 fwrite(le_hdr, sizeof(le_hdr), 1, fp); #else // 小端主机直接写 fwrite(hdr, sizeof(*hdr), 1, fp); #endif }4.3 I2C协议设备/** * brief 传感器数据读取I2C大端格式 * note 多数传感器BMP280、MPU6050等使用大端 */ typedef struct { int16_t temperature; // 大端有符号 uint16_t pressure; // 大端无符号 uint16_t humidity; // 大端无符号 } sensor_data_t; bool sensor_read(int fd, sensor_data_t *data) { uint8_t raw[6]; // I2C读取原始数据大端存储 i2c_read(fd, SENSOR_REG_DATA, raw, 6); // 转换为大端16位 data-temperature (int16_t)((raw[0] 8) | raw[1]); data-pressure (uint16_t)((raw[2] 8) | raw[3]); data-humidity (uint16_t)((raw[4] 8) | raw[5]); return true; }