嵌入式基础知识硬件篇之字节序


16、大小端的介绍

    Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
     Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    数字0x12 34 56 78在内存中的表示形式为:

大端模式:

低地址 -----------------> 高地址
0x12  |  0x34  |  0x56  |  0x78

小端模式:

低地址 ------------------> 高地址
0x78  |  0x56  |  0x34  |  0x12

Big-Endian: 低地址存放高位,如下:
高地址
        ---------------
        buf[3] (0x78) -- 低位
        buf[2] (0x56)
        buf[1] (0x34)
        buf[0] (0x12) -- 高位
        ---------------
        低地址
Little-Endian: 低地址存放低位,如下:
高地址
        ---------------
        buf[3] (0x12) -- 高位
        buf[2] (0x34)
        buf[1] (0x56)
        buf[0] (0x78) -- 低位
        --------------
低地址

C语言实现测试大小端:

 #include <stdio.h>

 int main(int argc, char **argv)

 {

      union

      {

          int a;

          char b;

      }c;

      c.a = 1;

      if(c.b == 1)

      {

          printf("little\n");

      }

      else

          printf("big\n");

      return 0;

  }

    常见CPU的大小端:

 Big Endian : PowerPC、IBM、Sun
    Little Endian : x86、DEC

    常见文件的字节序

 Adobe PS – Big Endian
    BMP – Little Endian
    DXF(AutoCAD) – Variable
    GIF – Little Endian
    JPEG – Big Endian
    MacPaint – Big Endian
    RTF – Little Endian


    Java和所有的网络通讯协议都是使用Big-Endian的编码。

大小端的转换:

16位

#define BigtoLittle16(A)   (( ((uint16)(A) & 0xff00) >> 8)    | \  

                                       (( (uint16)(A) & 0x00ff) << 8))  

32位

#define BigtoLittle32(A)   ((( (uint32)(A) & 0xff000000) >> 24) | \  

                                       (( (uint32)(A) & 0x00ff0000) >> 8)   | \  

                                       (( (uint32)(A) & 0x0000ff00) << 8)   | \  

                                       (( (uint32)(A) & 0x000000ff) << 24))  

    从软件的角度上,不同端模式的处理器进行数据传递时必须要考虑端模式的不同。如进行网络数据传递时,必须要考虑端模式的转换。在Socket接口编程中,以下几个函数用于大小端字节序的转换。

    ntohs(n)     //16位数据类型网络字节顺序到主机字节顺序的转换  

    htons(n)     //16位数据类型主机字节顺序到网络字节顺序的转换  

    ntohl(n)      //32位数据类型网络字节顺序到主机字节顺序的转换  

    htonl(n)      //32位数据类型主机字节顺序到网络字节顺序的转换 

 




很多人讨厌碰到字节序问题,跟它打交道就像走迷宫,每次都要牺牲不少脑细胞。即使这一次似乎搞清楚了,下次碰到还是要重新在大脑里构建和模拟。这里尽量做一个字节序问的完整备忘记录。

主机字节序

多字节数据在内存中的字节排列顺序称为主机字节序,主机字节序基本由CPU硬件决定,某些CPU如X86、Z80等为little-endian;有些如moto6800、sparc等为big-endian;而有些CPU可通过寄存器设置支持不同字节序,如ARM、MIPS等,这类平台上不同OS可能配置了不同字节序。

理解字节序需要知道两个名词:MSB (Most Significant Byte),代表一个数中权值最大的字节;LSB(Least Significant Byte),代表一个数中权值最小的字节。比如十进制数1234,1权值为千位,最大,相当于MSB;4为个位,权值最小,相当于LSB。那么真正以字节为单位考虑, 4字节数0x12345678里那个是MSB/LSB呢?

主机字节序分Big-Endian和Little-Endian两种,定义为:

a. Little-Endian,又称低字节优先,是把LSB放在内存低地址端,MSB在内存高地址端。举例,4字节数0x12345678在little-endian体系的内存中存放(从地址0x1000开始)为:

b. Big-Endian,又称高字节优先,把MSB放在内存低地址端,LSB放在内存高地址端。0x12345678此时存放为:

big-endian沿地址增长方向先放高权位数字的方式恰好符合一般习惯(按照千,百,十,个位来书写数字)。这也是为什么初学者变换endian时总觉得big-endian比较正常。

字节序问题有时会被扩大化,有人一碰到奇怪问题就怀疑字节序。其实它只存在于多字节数据在内存中的解析,注意:1)多字节数据;2)内存中的排列。

a. 多字节数据是指诸如long int short等需要多个字节存储的数据类型,而象char等用一个字节表示的类型永远不会有字节序问题。

b. CPU通用寄存器都是整体操作,不存在单字节地址以及地址增长方向的概念,因此也没有MSB/LSB在前或在后的问题,只有内存中的多字节数据存储才会有这两种差异。从硬件角度看,CPU是直接通过数据总线连接内存和CPU寄存器,这中间没有额外字节序转换的硬件开销,只是不同总线连接方式导致了不同字节序,如下图(from wiki):

信息交换中的字节序

当不同类型平台通过某种媒介交换信息时需要考虑字节序问题,常见介质主要指文件和网络。通过文件或网络读取来自其它主机的多字节数据时,要警惕字节序问题。如x86和moto6800主机通过文件交换信息时,如果不转换,数据处理就不匹配。在x86 VC下运行如下代码:

void main( )

{

FILE* fp;

short a = 0x3132; //为ASIIC码’12’

fp = fopen ("c:test.txt", "wb")

fwrite(&a, sizeof(short), 1, fp);

fclose(fp);

}

代码中a的值0x3132即'12’,由于x86为little endian体系,内存中多字节LSB(这里即0x32)在前,所以运行后打开test.txt文件,内容是'21'。把test.txt文件复制到moto6800机器上,再用fread把文件内容读到short变量里,得到的就不是0x3132而是0x3231了,数据就此被颠倒,后续处理完全错误。

假设我们制定了介质层字节序标准(如big-endian),要通过软件屏蔽不同endian体系的差异,需要两步操作,一判断endian类型:

typedef union {

u16 a;

u8 b[2];

}ENDIAN;

/* judge cpu is big endian(0) or small endian(1) */

bool judge_endian(void)

{

ENDIAN t;

t.a = 0x0001U;

return (bool)(t.b[0] == 1);

}

想想指针怎么做?

判断完两端主机的endian之后,要根据情况做相应endian交换,即发送和接收任何一方只要不符合媒介层标准(big-endian),就要通过移位和位操作,在主机序和媒介序之间转换。一方发送数据前要先将内存数据由主机字节序转换为传输介质层的字节序,再发送出去,接收方收到数据后,要转换为本地的主机字节序,再做后续处理。

比如TCP/IP协议的socket通信,基于socket通讯的双方一般选择Big-Endian为标准,又称网络字节序。socket定义了一组转换函数,用于多字节数在网络字节序和主机字节序之间的转换。htonl,htons用于主机序转换到网络序(如主机本身big endian,函数实际啥也不做);ntohl,ntohs把网络序转换到本机序(同样)。因此傻瓜式做法,无论发送方主机字节序是什么,发送前都用htons或htonl将多字节数据转换为网络字节序;同样无论接收方主机字节序是什么,都用ntohs或ntohl把接收的网络数据转换为本地主机字节序。这样有了网络字节序标准,socket通信时,只需套用这几个函数就能方便地抹平主机间的字节序差异。

问题在于如果双方主机都与网络字节序相反,本来是可以直接通信的,却用htonl/ntohl等转过去又转回来,有点自找麻烦。这就要看软件的预期运行环境,以及怎样平衡软件效率和多平台通用性。

补充SOC内部的字节序

soc中除主CPU外,一些外设中(如硬件媒体编解码器)还包含内部MCU,它们之间一般通过高速总线共享外部DDR,通过低速总线共享外设寄存器。这时如果双方的endian属性不匹配,对host cpu驱动及MCU上的固件编写会造成一定困扰。

总结

真正理解大小端的本质可能要从硬件系统角度,涉及CPU指令集、寄存器、总线以及硬件外设等。但对程序员来说,能够了解字节序问题的存在范围以及如何实现移植性更高的代码,这就足够了。

Tip:对于ARM等可配置的CPU,其编译工具中会有big/little endian的选项,程序移植时也需要注意,如果跟系统设置不匹配,编译得到的目标程序就无法运行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值