Migration Techniques for Different Endianness
Endianness can be a major migration problem, causing migrated software to not work properly. This article shows techniques which can be useful when migrating from a big-endian to a little-endian CPU.
Endianness
Endianness and endian are terms that describe the order in which a sequence of bytes is stored in memory. Big-endian is an order in which the most significant value in the sequence is stored first. In little-endian systems the least significant value in the sequence is stored first. For example, in a big-endian CPU, the four bytes of data 0x01020304 would be stored 0x01(address+0), 0x02(address+1), 0x03(address+2), 0x04(address+3). In a little-endian CPU, the same bytes would be stored 0x04(address+0), 0x03(address+1), 0x02(address+2), 0x01(address+3).
If your program uses a simple data structure such as "int" and "short", there is little trouble. But if the data structure is similar to the following example, you might run into problems. In this case, the union variable can store "unsigned int" and "unsigned char [4]" which both have 4 bytes of memory space. This kind of union is commonly used in application programs. When input and output data is transferred, 4 bytes access is used. When the data is accessed in the program, one byte access is used, and sometimes also bitfield access.
union {
unsigned int dat;
unsigned char c[4];
}X;
void foo( ) {
int t0;
X.dat = 0x01020304;
t0 = X.c[0];
・・・
}
When this code is compiled and executed in a big-endian CPU, the value of "t0" is 0x01. In a little-endian CPU, the value of "t0" is 0x04.
Techniques for endianness in IAR Embedded Workbench for ARM
I will show you two techniques for handling different endianness:
- The __big_endian keyword
- The intrinsic functions __REV, __REV16, __REVSH, RBIT
The __big_endian keyword
There is an expended compiler keyword: __big_endian. This is how the keyword is described in the IAR Embedded Workbench user guide: The __big_endian keyword is used for accessing a variable that is stored in the big-endian byte order regardless of what byte order the rest of the application uses. The __big_endian keyword is available when you compile for ARMv6 or higher. Note that this keyword cannot be used on pointers. Also, this attribute cannot be used on arrays.
The previous code example can be modified by simply adding the__big_endian keyword:
____big_endian union {
unsigned int dat;
unsigned char c[4];
}X;
void foo( ) {
int t0;
X.dat = 0x01020304;
t0 = X.c[0];
・・・
}
This modified code is compiled and executed in a little-endian CPU, and the variable of "t0" is 0x01. The __big_endian keyword provides a very convenient way to port application program from big-endian to little-endian. There is also a __little_endian keyword, to be used from little-endian to big-endian.
The keyword __big_endian inserts REV instructions for swapping the byte data. The insertion of REV instructions affects code size and execution time.
The keyword __big_endian is not a silver bullet solution. The keyword has a restriction and cannot be applied to complex data structures. Remember this part of its description in the user guide: Note that this keyword cannot be used on pointers. Also, this attribute cannot be used on arrays. For example, this code generates an error:
__big_endian
union {
unsigned long dat;
unsigned char c[4];
struct {
unsigned long a0: 1;
unsigned long a1: 1;
unsigned long a2: 2;
unsigned long a3: 4;
unsigned long a4: 8;
unsigned long a5: 16;
}s;
} f1_dat2;
In this case, a byte manipulation instruction may be used.
Intrinsic functions: __REV, __REV16, __REVSH, RBIT
The difference of endianness between big-endian and little-endian is just the order. So, what we need to do is change the order of bytes. Let’s again use the variable 0x01020304 as an example.
We can implement a swap function as follows:
typedef unsigned long uint32_t;
uint32_t bswap_32(uint32_t x) {
uint32_t t = x;
uint32_t s;
s = ( (((uint32_t)(t) & (uint32_t)0x000000ffUL) << 24) |
(((uint32_t)(t) & (uint32_t)0x0000ff00UL) << 8) |
(((uint32_t)(t) & (uint32_t)0x00ff0000UL) >> 8) |
(((uint32_t)(t) & (uint32_t)0xff000000UL) >> 24) );
return s;
}
This bswap_32 function has four shifts, four bitwise and, and three bitwise or. This will cause overhead time and code size.
Devices based on Cortex-M/R/A which implement the ARMv6 architecture have byte manipulate instructions. For example, REV instructions works the same way as the bswap_32 function.
In the C code, we usually write inline assembler code but IAR Embedded Workbench for ARM has intrinsic functions for byte manipulate instructions. To use intrinsic functions, include the header files intrinsics.h.
- __REV : Reverse the byte order in a word
- __REV16: Reverse the byte order in each half of the word independently
- __REVSH: Reverse the byte order in the bottom half of the word, and sign extend to 32 bits
- __RBIT: Reverse the bit order in a 32b-bit word.
The following examples show data manipulation with REV/REV16 and REVSH instruction.
The code is as follows:
#include <intrinsics.h>
void x1( void ) {
s2 = __REV(s1);
s3 = __REV16(s1);
s4 = __REVSH(s1);
}
Conclusion
This article explains two techniques for porting from big-endian to little-endian. The keyword __big_endian can be used for simple case porting. For complex cases where the __big_endian keyword cannot be used, REV/REV16/REVSH instructions can be used to change the order of the data.