嵌入式C语言:大小端详解

server/2025/2/4 1:22:29/

目录

一、大小端的概念

1.1. 大端序(Big-endian)

1.2. 小端序(Little-endian)

二、大小端与硬件体系的关系

2.1. 大小端与处理器架构

2.2. 大小端与网络协议

2.3. 大小端对硬件设计的影响

三、判断系统的大小端方式

3.1. 方法一:使用联合体(Union)

3.2. 方法二:使用指针强制类型转换

3.3. 方法三:宏定义法

3.4. 方法四:利用标准库函数(特定平台)

四、大小端转换的方法

4.1. 手动位操作

4.2. 使用联合体(Union)

4.3. 使用指针和数组

4.4. 使用标准库函数(如果可用)

4.5. 位操作宏定义法

五、大小端对嵌入式系统的影响

六、注意事项

6.1. 数据存储和读取

6.2. 数据类型转换

6.3. 结构体和联合体的使用

6.4. 跨平台开发

七、总结

八、参考文献


在嵌入式C语言中,大小端(Endianness)是一个重要的概念,它涉及到多字节数据在内存中的存储顺序。

一、大小端的概念

大小端,又称端序(Endianness),是指多字节数据在内存中的存储顺序。对于单字节数据,不存在存储顺序的问题,因为每个字节都是独立的。但是,对于多字节数据(如int、float、double等),就需要考虑字节的存储顺序了。

1.1. 大端序(Big-endian)

  • 高位在前大端序将数据的最高有效字节(MSB,Most Significant Byte)存储在内存的最低地址处,而最低有效字节(LSB,Least Significant Byte)则存储在最高的地址处
  • 特点:符合人类阅读习惯的表示方法,因为人类通常从左到右(即从高位到低位)阅读数字。
  • 示例:对于int num = 0x12345678,在大端模式下,在内存中的存储顺序为:
| 内存地址(低 -> 高)| 存储内容 |
|----|----|
|0x00|0x12|
|0x01|0x34|
|0x02|0x56|
|0x03|0x78|

1.2. 小端序(Little-endian)

  • 低位在前:数也称小端序或小字节序。与大端相反,数据的低位字节存于低地址,高位字节存于高地址。如同从右到左(低位在前)存储字节。
  • 特点:在某些处理器架构中,小端序可以简化指令集和内存访问操作,因为最低有效字节通常更容易被处理器直接访问。
  • 示例:对于int num = 0x12345678,在小端模式下,在内存中的存储顺序为:
| 内存地址(低 -> 高)| 存储内容 |
|----|----|
|0x00|0x78|
|0x01|0x56|
|0x02|0x34|
|0x03|0x12|

二、大小端与硬件体系的关系

大小端与硬件体系的关系十分密切,不同的处理器和平台可能会采用不同的大小端方式。

2.1. 大小端与处理器架构

  • x86系列处理器:多数x86系列的处理器,如Intel和AMD的处理器,通常采用小端序。意味着在这些处理器上运行的系统和数据存储也倾向于使用小端方式。
  • 其他处理器架构:与x86系列不同,一些其他处理器架构可能采用大端序。例如,PowerPC处理器通常使用大端序。因此,在这些平台上运行的系统和数据存储也相应地遵循大端方式。

2.2. 大小端与网络协议

  • TCP/IP协议网络协议中的字节序与处理器的大小端方式并不总是一致。例如,TCP/IP协议规定使用大端序来解析数据,被称为网络字节序。这种规定是为了确保数据在网络传输过程中的一致性和兼容性。
  • 转换需求:由于网络字节序与某些处理器的大小端方式可能不同,因此在数据通过网络传输时,发送方需要将主机字节序转换为网络字节序(大端序),而接收方在收到数据后则需要将网络字节序转换回自己的主机字节序以便正确解析。

2.3. 大小端对硬件设计的影响

  • 寄存器设计:在硬件设计中,寄存器的位序定义可能与大小端方式有关。例如,在大端模式下,寄存器的最高有效位(MSB)可能定义在较低的地址上,而在小端模式下则相反。

  • 总线设计:数据总线和小端外设的连接也需要考虑大小端方式。例如,当大端处理器访问小端外设时,可能需要进行一定的处理以确保数据的一致性。

  • 软件兼容性:在处理器系统中,如果存在大端和小端模式同时存在的情况,可能会对软件和硬件的设计带来额外的挑战。软件开发者需要编写能够处理不同端模式的代码,而硬件设计师则需要在寄存器、指令集、数据总线等方面进行相应的调整。

三、判断系统的大小端方式

在嵌入式C语言中,可以通过编写程序来判断当前系统的大小端方式。以下是一个简单的判断方法。

3.1. 方法一:使用联合体(Union)

联合体是一种特殊的数据结构,它允许在相同的内存位置存储不同类型的数据。利用联合体的这一特性,可以方便地判断系统的大小端方式。

#include <stdio.h>int main() {union {unsigned int i;unsigned char c[sizeof(unsigned int)];} test_union;test_union.i = 0x12345678; // 设定一个已知的整数值if (test_union.c[0] == 0x78) {printf("The system is Little Endian!\n");} else {printf("The system is Big Endian!\n");}return 0;
}

定义了一个联合体test_union,它包含一个无符号整型i和一个字符数组c。我们将一个已知的整数值0x12345678赋给i,然后检查c[0]的值。如果c[0]等于0x78,说明系统的最低有效字节存储在最低的内存地址处,即系统是小端序;否则,系统是大端序。 

3.2. 方法二:使用指针强制类型转换

另一种常用的方法是使用指针强制类型转换。我们可以将一个整数的地址转换为字符指针,然后检查该指针所指向的第一个字节的值。

#include <stdio.h>
#include <stdint.h>int main() {uint32_t x = 0x12345678;char *c = (char*)&x;if (*c == 0x78) {printf("This system is little-endian.\n");} else if (*c == 0x12) {printf("This system is big-endian.\n");} else {printf("Unable to determine endianness.\n");}return 0;
}

 

我们创建了一个32位的无符号整数x,并将其地址转换为一个字符指针c。然后,我们检查*c的值来判断系统的大小端方式。

3.3. 方法三:宏定义法

可以通过预处理器指令来定义一个宏,在编译时根据不同的大小端模式进行不同的操作。示例代码如下:

#include <stdio.h>#if defined(__BIG_ENDIAN__)
#define ENDIAN "大端"
#elif defined(__LITTLE_ENDIAN__)
#define ENDIAN "小端"
#else
#define ENDIAN "未知"
#endifint main() {printf("当前系统是 %s 模式\n", ENDIAN);return 0;
}

3.4. 方法四:利用标准库函数(特定平台)

在某些平台上,可以利用标准库函数来判断系统的大小端方式。例如,在POSIX兼容的系统上,可以使用htons(Host TO Network Short)和ntohl(Network TO Host Long)等函数来检查系统的字节序。然而,这种方法依赖于特定的平台和库,因此并不是所有系统都适用。

四、大小端转换的方法

在嵌入式系统开发、网络编程等场景中,当涉及不同大小端模式设备之间的数据交互时,就需要进行大小端转换。

4.1. 手动位操作

通过手动位操作来交换字节顺序,这是最直接的方法。对于16位、32位和64位的数据类型,可以分别编写转换函数。

#include <stdio.h>
#include <stdint.h>// 16位数据大小端转换
uint16_t swap_endian_16(uint16_t x) {return (x >> 8) | (x << 8);
}// 32位数据大小端转换
uint32_t swap_endian_32(uint32_t x) {return ((x >> 24) & 0x000000FF) |((x >>  8) & 0x0000FF00) |((x <<  8) & 0x00FF0000) |((x << 24) & 0xFF000000);
}// 64位数据大小端转换
uint64_t swap_endian_64(uint64_t x) {return ((x >> 56) & 0x00000000000000FFULL) |((x >> 40) & 0x000000000000FF00ULL) |((x >> 24) & 0x0000000000FF0000ULL) |((x >>  8) & 0x00000000FF000000ULL) |((x <<  8) & 0x000000FF00000000ULL) |((x << 24) & 0x0000FF0000000000ULL) |((x << 40) & 0x00FF000000000000ULL) |((x << 56) & 0xFF00000000000000ULL);
}int main() {// 测试16位数据转换uint16_t num16 = 0x1234;uint16_t swapped16 = swap_endian_16(num16);printf("16位数据: 原始值 0x%04x, 转换后 0x%04x\n", num16, swapped16);// 测试32位数据转换uint32_t num32 = 0x12345678;uint32_t swapped32 = swap_endian_32(num32);printf("32位数据: 原始值 0x%08x, 转换后 0x%08x\n", num32, swapped32);// 测试64位数据转换uint64_t num64 = 0x123456789ABCDEF0ULL;uint64_t swapped64 = swap_endian_64(num64);printf("64位数据: 原始值 0x%016llx, 转换后 0x%016llx\n", num64, swapped64);return 0;
}

 

swap_endian_16swap_endian_32 和 swap_endian_64 三个函数分别用于对 16 位、32 位和 64 位无符号整数进行大小端转换。

使用 %04x%08x 和 %016llx 格式化输出 16 位、32 位和 64 位的十六进制数,确保输出的结果长度符合预期。 

4.2. 使用联合体(Union)

联合体允许在同一内存位置存储不同类型的数据。通过联合体,可以方便地访问和修改数据的不同字节。

#include <stdint.h>
#include <stdio.h>// 使用联合体实现32位数据的大小端转换
uint32_t swap_endian_32_union(uint32_t x) {union {uint32_t i;uint8_t c[4];} u;u.i = x;uint32_t swapped = (u.c[0] << 24) | (u.c[1] << 16) | (u.c[2] << 8) | u.c[3];return swapped;
}int main() {// 初始化一个32位无符号整数用于测试uint32_t num = 0x12345678;// 调用大小端转换函数uint32_t swapped_num = swap_endian_32_union(num);// 输出原始值和转换后的值printf("原始的32位数据: 0x%08x\n", num);printf("转换后的32位数据: 0x%08x\n", swapped_num);return 0;
}

 

swap_endian_32_union 函数:定义了一个匿名联合体,该联合体包含两个成员:一个 32 位无符号整数 i 和一个包含 4 个 8 位无符号整数的数组 c。由于联合体的所有成员共享同一块内存空间,所以可以通过不同的方式来访问这块内存。

  • 将输入的 32 位无符号整数 x 赋值给联合体的 i 成员。
  • 通过位运算和移位操作,将数组 c 中的每个字节重新组合成一个新的 32 位无符号整数 swapped,实现了大小端的转换。

4.3. 使用指针和数组

通过指针和数组也可以实现大小端转换,这种方法与联合体类似,但更加直接地操作内存。

#include <stdint.h>
#include <stdio.h>// 使用指针实现32位数据的大小端转换
uint32_t swap_endian_32_pointer(uint32_t x) {uint8_t *bytes = (uint8_t *)&x;uint32_t swapped = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];return swapped;
}int main() {// 定义一个32位无符号整数用于测试uint32_t original_num = 0x12345678;// 调用大小端转换函数uint32_t swapped_num = swap_endian_32_pointer(original_num);// 输出原始值和转换后的值printf("原始32位数据: 0x%08x\n", original_num);printf("转换后32位数据: 0x%08x\n", swapped_num);return 0;
}

 

swap_endian_32_pointer 函数:接收一个 uint32_t 类型的参数 x,表示需要进行大小端转换的 32 位无符号整数。

  • 通过将 x 的地址强制转换为 uint8_t * 类型的指针 bytes,这样就可以将 32 位整数按字节访问。
  • 利用位运算和移位操作,将 bytes 数组中的每个字节重新组合成一个新的 32 位无符号整数 swapped,实现了大小端的转换。 

4.4. 使用标准库函数(如果可用)

在某些编译器和系统库中,提供了专门用于大小端转换的函数。例如,在 GCC 编译器中,有htonshtonlntohsntohl等函数,主要用于网络编程中主机字节序和网络字节序(大端序)之间的转换:

  • htons:将 16 位无符号整数从主机字节序转换为网络字节序。
  • htonl:将 32 位无符号整数从主机字节序转换为网络字节序。
  • ntohs:将 16 位无符号整数从网络字节序转换为主机字节序。
  • ntohl:将 32 位无符号整数从网络字节序转换为主机字节序。
#include <stdio.h>
#include <arpa/inet.h>int main() {uint16_t num16 = 0x1234;uint32_t num32 = 0x12345678;uint16_t swapped16 = htons(num16);uint32_t swapped32 = htonl(num32);printf("16位原始数据: 0x%x, 转换后: 0x%x\n", num16, swapped16);printf("32位原始数据: 0x%x, 转换后: 0x%x\n", num32, swapped32);return 0;
}

4.5. 位操作宏定义法

使用宏定义可以更方便地实现大小端转换,尤其是在需要频繁进行转换的代码中。以下是 32 位数据转换的宏定义示例:

#include <stdio.h>
#include <stdint.h>#define SWAP32(x) \((((x) & 0xFF000000) >> 24) | \(((x) & 0x00FF0000) >> 8) | \(((x) & 0x0000FF00) << 8) | \(((x) & 0x000000FF) << 24))int main() {uint32_t num = 0x12345678;uint32_t swapped = SWAP32(num);printf("原始数据: 0x%x, 转换后: 0x%x\n", num, swapped);return 0;
}

 

五、大小端对嵌入式系统的影响

在嵌入式系统中,大小端的影响主要体现在以下几个方面:

  • 数据通信:当嵌入式系统与外部设备或网络进行通信时,需要确保数据的大小端方式与通信协议一致。如果不一致,可能需要进行大小端转换。
  • 文件存储:当嵌入式系统将数据存储在文件中时,需要考虑文件的大小端方式。如果文件将在不同大小端方式的系统上读取,可能需要进行转换。
  • 跨平台开发:在跨平台开发中,不同平台可能采用不同的大小端方式。因此,开发者需要注意数据的大小端问题,并确保在不同平台上都能正确处理和存储数据。

六、注意事项

6.1. 数据存储和读取

  • 明确数据存储方式:要清楚所使用的硬件平台是大端还是小端模式。比如,PowerPC 架构通常是大端模式,而 x86 架构一般是小端模式。不同的模式下,数据在内存中的存储顺序不同。
  • 避免数据错误:在对多字节数据(如 intfloat 等)进行存储和读取时,要考虑大小端的影响。例如,在小端模式下,int num = 0x12345678,实际在内存中存储的顺序是 0x780x560x340x12。如果按照大端模式的思维去读取,就会得到错误的数据。

6.2. 数据类型转换

  • 注意字节顺序:当进行不同数据类型之间的转换时,特别是涉及到跨平台或与外部设备交互时,要确保大小端的一致性。例如,将一个 int 类型转换为 char 数组时,在小端模式下,低字节先存储,转换后数组中的元素顺序与大端模式下是相反的。
  • 使用合适的转换函数:对于一些需要进行大小端转换的情况,要使用正确的转换函数。比如,网络编程中,常需要在主机字节序和网络字节序(大端)之间进行转换,可以使用 htonlhtonsntohlntohs 等函数,以确保数据在不同环境下的正确传输和处理。

6.3. 结构体和联合体的使用

  • 结构体成员对齐:在结构体中,成员的存储顺序和对齐方式可能会受到大小端的影响。编译器会根据目标平台的大小端模式和结构体成员的类型来进行内存对齐,以提高访问效率。但这可能导致在不同大小端平台上结构体的内存布局不同。例如:
struct {char a;int b;short c;
} myStruct;

在小端模式下,假设 char 占 1 字节,short 占 2 字节,int 占 4 字节,且内存对齐为 4 字节,那么 myStruct 的成员在内存中的布局可能是 a 占 1 字节,然后填充 3 字节达到 4 字节对齐,接着是 b 的 4 字节,最后是 c 的 2 字节。而在大端模式下,布局可能不同。

  • 联合体的特性:联合体可以利用其所有成员共享同一块内存的特性来判断当前平台的大小端模式。例如: 
union {uint32_t i;uint8_t c[4];
} u;u.i = 0x12345678;
if (u.c[0] == 0x78) {printf("小端模式\n");
} else {printf("大端模式\n");
}

6.4. 跨平台开发

  • 编写可移植代码:在进行跨平台开发时,要编写能够适应不同大小端模式的代码。可以通过条件编译等方式,根据不同的平台定义来选择合适的大小端处理方式。例如:
#if defined(__BIG_ENDIAN__)
// 大端模式下的代码
#elif defined(__LITTLE_ENDIAN__)
// 小端模式下的代码
#else
#error "未知的字节序"
#endif
  • 数据一致性:确保在不同大小端平台之间传输和处理的数据具有一致性。在进行数据交换时,要进行必要的大小端转换,以保证数据的正确解读。

七、总结

综上所述,大小端是嵌入式C语言中一个重要的概念,它涉及到多字节数据在内存中的存储顺序。了解并掌握大小端的概念和判断方法对于嵌入式系统的开发和调试具有重要意义。

八、参考文献

  • 《嵌入式系统原理及应用教程》详细讲解了嵌入式系统的基础原理和应用,其中包含对大小端模式的阐述,结合实际的嵌入式硬件架构,介绍大小端在不同平台下的表现和影响。
  • 《C Primer Plus》经典的 C 语言入门书籍,虽然不是专门针对嵌入式领域,但对 C 语言的基础概念和数据存储方式有深入讲解。
  • 《深入理解计算机系统》这本书从计算机系统的底层原理出发,深入探讨了数据的表示和存储,包括大小端模式的原理、产生原因以及在不同体系结构中的应用。
  • ARM Architecture Reference Manual:如果使用的是 ARM 架构的嵌入式处理器,这份文档是必不可少的参考资料。它详细介绍了 ARM 处理器的体系结构和指令集,其中包含了关于 ARM 处理器大小端模式的设置和操作方法,以及在不同模式下数据的存储和处理规则。
  • Intel® 64 and IA - 32 Architectures Software Developer’s Manual:对于基于 Intel 架构的嵌入式系统,该手册提供了详细的技术信息。它涵盖了 Intel 处理器的各种特性,包括大小端模式的相关内容,能帮助你了解在 Intel 平台上如何处理大小端问题,以及如何编写与大小端相关的 C 语言代码。
  • Stack Overflow:一个知名的技术问答社区,有大量关于嵌入式 C 语言大小端的讨论和解决方案。
  • GitHub:全球最大的开源代码托管平台,上面有许多嵌入式 C 语言项目。
  • 电子发烧友网、开源中国等技术论坛:这些论坛上有很多嵌入式开发相关的板块,其中包含了关于大小端的讨论和经验分享。

http://www.ppmy.cn/server/164761.html

相关文章

基于SpringBoot电脑组装系统平台系统功能实现六

一、前言介绍&#xff1a; 1.1 项目摘要 随着科技的进步&#xff0c;计算机硬件技术日新月异&#xff0c;包括处理器&#xff08;CPU&#xff09;、主板、内存、显卡等关键部件的性能不断提升&#xff0c;为电脑组装提供了更多的选择和可能性。不同的硬件组合可以构建出不同类…

【机器学习】深入无监督学习分裂型层次聚类的原理、算法结构与数学基础全方位解读,深度揭示其如何在数据空间中构建层次化聚类结构

&#x1f31f;个人主页&#xff1a;落叶 &#x1f31f;当前专栏: 机器学习专栏 目录 引言 分裂型层次聚类&#xff08;Divisive Hierarchical Clustering&#xff09; 1. 基本原理 2. 分裂型层次聚类的算法步骤 Step 1: 初始化 Step 2: 选择分裂的簇 Step 3: 执行分裂操作…

深入 Rollup:从入门到精通(三)Rollup CLI命令行实战

准备阶段&#xff1a;初始化项目 初始化项目&#xff0c;这里使用的是pnpm&#xff0c;也可以使用yarn或者npm # npm npm init -y # yarn yarn init -y # pnpm pnpm init安装rollup # npm npm install rollup -D # yarn yarn add rollup -D # pnpm pnpm install rollup -D在…

Android --- handler详解

handler 理解 handler 是一套Android 消息传递机制&#xff0c;主要用于线程间通信。 tips&#xff1a; binder/socket 用于进程间通信。 参考&#xff1a; Android 进程间通信-CSDN博客 handler 就是主线程在起了一个子线程&#xff0c;子线程运行并生成message &#xff0c;l…

吴恩达深度学习——机器学习的策略

本文来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 正交化单一数字评估指标满足指标和优化指标训练、开发&#xff08;验证&#xff09;、测试集划分更改训练、开发、测试集和指标两种错误率贝叶斯错误率人类水平错误率 总结 正交…

4 [危机13小时追踪一场GitHub投毒事件]

事件概要 自北京时间 2024.12.4 晚间6点起&#xff0c; GitHub 上不断出现“幽灵仓库”&#xff0c;仓库中没有任何代码&#xff0c;只有诱导性的病毒文件。当天&#xff0c;他们成为了 GitHub 上 star 增速最快的仓库。超过 180 个虚假僵尸账户正在传播病毒&#xff0c;等待不…

全志 视频输入组件的使用

1.启动MPP和Glog库 示例代码&#xff1a; log_init(argv[0], &stGLogConfig);MPP_SYS_CONF_S stSysConf; memset(&stSysConf, 0, sizeof(MPP_SYS_CONF_S)); stSysConf.nAlignWidth 32; AW_MPI_SYS_SetConf(&stSysConf); ret AW_MPI_SYS_Init();2.获取配置文件信…

DeepSeek 使用的核心技术预测

最近DeepSeek 这个词算是火遍了整个AI圈&#xff0c;这个影响力迅速超过ChatGPT 的产品&#xff0c;都会使用哪些技术来做支撑呢。我这里简单做了一下梳理&#xff0c;结果不一定会完全准确&#xff0c;但是对这类产品的技术架构有个大概的认识。 以下是我对可能涉及的技术架构…