1.前言
什么是大小端转换?为什么叫大小端转换?
Jonathan Swift的《格列佛游记》中记载,有两国因为剥鸡蛋的方式不同,即一国要求将熟鸡蛋的较大的一端(大端,big endian)敲碎然后剥壳,另一个国家则强制要求敲碎鸡蛋的小端(little endian)。两国无法达成一致,进而交战多年。
一位名为Danny Cohen的网络协议先驱者,将这两个数据用来描述数据在存储器中的排布方式,进而被广泛使用。例如对于一个四字节数据int data = 0x12345678,将低地址视为敲鸡蛋的地面,则有两种存储分布方式:
图1 大小端字节序示意图
如图1所示,data的高字节为0x12,所以左侧的为大端存储顺序,右侧为小端存储顺序。
CPU的厂家众多,对于数据在存储器中的排布方式,也分为两个派系:
CPU | Endian |
Intel X86 | Little-Endian |
Power-PC/IBM | Big-Endian |
ARM | 默认Little-Endian, 可配置Big-Endian |
大小端模式各有优劣,在小端模式下,指针的强制类型转换不需要调整字节内容,如short * (&data),取的是data低地址的两字节0x56和0x78,对应的也是data较低的16位;大端模式下,数据的符号位固定为第一个字节的最高bit,容易判断符号位,且和人类阅读方式相同,即先写数据的高位,再写数据的低位。
以上讨论的是字节序大小端,事实上,比特(bit)序也存在大小端模式,规则类似,大端的高bit在低地址,小端相反。在定义C语言结构体时,如果存在位段的定义,则需要使各bit与CPU的大小端一致。例如:
typedef struct
{uint32 b2LatValSts:2; //bit 31~30 uint32 b2YawRateValSts:2; //bit 29~28uint32 b3EpbSts:3; //bit 27~25uint32 b1BrakePressSts:1; //bit 24uint32 b2BrakePedlSts:2; //bit 23~22uint32 b2TurnIndicatorSwtichSts:2; //bit 21~20uint32 b3EpsSts:3; //bit 19~17uint32 b1SteerWhlAngDir:1; //bit 16uint32 b1SteerWhlAngSpdDir:1; //bit 15uint32 b15Reserved:15; //bit 0~14
}BIG_ENDIAN_SAMPLE_ST_TYPE;typedef struct
{uint32 b15Reserved:15; //bit 0~14uint32 b1SteerWhlAngSpdDir:1; //bit 15uint32 b1SteerWhlAngDir:1; //bit 16uint32 b3EpsSts:3; //bit 19~17uint32 b2TurnIndicatorSwtichSts:2; //bit 21~20uint32 b2BrakePedlSts:2; //bit 23~22uint32 b1BrakePressSts:1; //bit 24uint32 b3EpbSts:3; //bit 27~25uint32 b2YawRateValSts:2; //bit 29~28uint32 b2LatValSts:2; //bit 31~30
}LITTLE_ENDIAN_SAMPLE_ST_TYPE;
由于Power-PC在网络领域的统治地位,以及其他可能存在的原因,总之网络字节序完成了江湖大一统,统一使用大端字节序。进而,在应用软件开发时,经常需要对网络数据接口进行大小端转换,这个转换主要时针对字节序。那比特序呢?这个通常不需要应用层来做转换,可以姑且理解为某个底层协议栈帮忙做转换了,只需要在应用软件本地按自身CPU的大小端来定义位段即可。
2.大小端转换
2.1 通用的C语言字节序转换方法
#define M_2BYTES_ENDIAN_CONVERT(src) ((src) = ((typeof(src))0xFF00 & ((src) << 8)) | ((typeof(src))0x00FF & ((src) >> 8)))
#define M_4BYTES_ENDIAN_CONVERT(src) ((src) = ((typeof(src))0xFF000000 & ((src) << 24)) | ((typeof(src))0x00FF0000 & ((src) << 8)) |((typeof(src))0x0000FF00 & ((src) >> 8)) | ((typeof(src))0x000000FF & ((src) >> 24)))
这里不多说,具体见上述代码中的两个宏定义。
2.2 CMSIS
Common Microcontroller Software Interface Standard(CMSIS),是ARM封装的Cortex-M架构微控制器的标准软件接口规范。 显然,这里要说的是ARM针对大小端优化所给出的汇编级指令,但还是通过CMSIS标准接口来使用。
2.2.1 大小端转换的相关ARM指令(Cortex-M)
指令 | 解释 |
REV {condition} Rd, Rn | 转换word的字节序,一个word为4字节 |
REV16 {condition} Rd, Rn | 转换half word的字节序,half word即为2字节 |
REVSH {condition} Rd, Rn | 转换低half word的字节序,并符号拓展至32位(4字节) |
RBIT{condition} Rd, Rn | 对一个32位的word(字)进行比特序大小端转换 |
其中,Rd是目标寄存器(destination register), Rn是操作数寄存器(the register holding the operand),conditon是操作条件码。例如:
REV R3, R7 ; Reverse byte order of value in R7 and write it to R3.
REV16 R0, R0 ; Reverse byte order of each 16-bit halfword in R0.
REVSH R0, R5 ; Reverse Signed Halfword of R5 and sing extend to 32 bits and then write to R0
REVHS R3, R7 ; Reverse with Higher or Same condition.
RBIT R7, R8 ; Reverse bit order of value in R8 and write the result to R7
2.2.2 CMSIS接口
例如在cmsis_gcc.h中定义有以下内联函数:
#define __CMSIS_GCC_USE_REG(r) "r" (r)/**\brief Reverse byte order (32 bit)\details Reverses the byte order in unsigned integer value. For example, 0x12345678 becomes 0x78563412.\param [in] value Value to reverse\return Reversed value*/
__STATIC_FORCEINLINE uint32_t __REV(uint32_t value)
{
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)return __builtin_bswap32(value);
#elseuint32_t result;__ASM volatile ("rev %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );return result;
#endif
}/**\brief Reverse byte order (16 bit)\details Reverses the byte order within each halfword of a word. For example, 0x12345678 becomes 0x34127856.\param [in] value Value to reverse\return Reversed value*/
__STATIC_FORCEINLINE uint32_t __REV16(uint32_t value)
{uint32_t result;__ASM volatile ("rev16 %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );return result;
}/**\brief Reverse byte order (16 bit)\details Reverses the byte order in a 16-bit value and returns the signed 16-bit result. For example, 0x0080 becomes 0x8000.\param [in] value Value to reverse\return Reversed value*/
__STATIC_FORCEINLINE int16_t __REVSH(int16_t value)
{
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)return (int16_t)__builtin_bswap16(value);
#elseint16_t result;__ASM volatile ("revsh %0, %1" : __CMSIS_GCC_OUT_REG (result) : __CMSIS_GCC_USE_REG (value) );return result;
#endif
}/**\brief Reverse bit order of value\details Reverses the bit order of the given value.\param [in] value Value to reverse\return Reversed value*/
__STATIC_FORCEINLINE uint32_t __RBIT(uint32_t value)
{uint32_t result;#if ((defined (__ARM_ARCH_7M__ ) && (__ARM_ARCH_7M__ == 1)) || \(defined (__ARM_ARCH_7EM__ ) && (__ARM_ARCH_7EM__ == 1)) || \(defined (__ARM_ARCH_8M_MAIN__ ) && (__ARM_ARCH_8M_MAIN__ == 1)) )__ASM volatile ("rbit %0, %1" : "=r" (result) : "r" (value) );
#elseuint32_t s = (4U /*sizeof(v)*/ * 8U) - 1U; /* extra shift needed at end */result = value; /* r will be reversed bits of v; first get LSB of v */for (value >>= 1U; value != 0U; value >>= 1U){result <<= 1U;result |= value & 1U;s--;}result <<= s; /* shift when v's highest bits are zero */
#endifreturn result;
}
值得注意的是:
①该接口中,包含了使用gcc builtin的转换函数和使用ARM-v7汇编指令两种方式,显然后者效率更高;
②int16_t __REVSH(int16_t value)函数中,result的类型是int16_t,即有符号的16位数据类型,运算过程中将拓展后的32位有符号数据截断并返还低16位数据;
3. 总结
如果当前的CPU是ARM的Cortex-M架构微控制器,建议使用标准的CMSIS接口来进行大小端转换,不仅效率高,软件移植性也会更好。