Solutions to Endianness Issues in Embedded Programming

The issue of endianness is not new in embedded development. As developers, we have a clear understanding of the endianness problem in embedded development, and here I will provide a simple summary of this issue.

Endianness refers to the order of bytes in memory. There is a wealth of information available online regarding endianness, so I will not elaborate further.

In engineering projects, the need to handle endianness differences mainly arises during data processing, commonly seen in:

1.Packet parsing and assembly

2.Data transmission and parameter passing

  • Packet Parsing and Assembly

The process of packet parsing and assembly can refer to the section on “Designing Protocol Frame Structures” in the article titled “Embedded Hardware Communication Interface Protocol-UART (IV) Designing Application Layer Protocols.” This section discusses how to represent the data of a uint16_t field using two uint8_t types to ensure no discrepancies during data transmission.

However, some interfaces are designed by others, and as users, you can only “comply” with their usage.

In C, you can use type casting to convert data types, but the result of the cast depends on the endianness of the current platform.

The following type casting forcibly converts the data stream in uint8_t type buf to uint16_t type and assigns it to the variable tmp_dat. Observations show that the data stream in buf is “combined” into a uint16_t type data every two bytes. The debug process screenshots are as follows:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

In the code, p1 is a pointer of type uint16_t pointing to the data stream of type uint8_t tmp_stream. The pointer assignment here requires type casting.

Within the for loop, using the p1 pointer as the “starting point” to loop and offset to retrieve data, and each offset is the width of uint16_t type data since p1 is a pointer of uint16_t type.

Observing the tmp_dat variable where the retrieved data is stored, it can be seen that the data is “combined” from left to right (from low address to high address) every two bytes into uint16_t type. For example, 0x11 and 0x12 combine to form 0x1122, and 0x33 and 0x44 combine to form 0x3344… It can be seen that the data at the low address becomes the high byte of uint16_t! This is big-endian mode!

Running the same code on a little-endian ARM platform yields completely different results:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

It is not difficult to find that on a little-endian platform, the data is “combined” into uint16_t type from left to right (from low address to high address) every two bytes, while 0x11 and 0x12 combine to form 0x2211, and 0x33 and 0x44 combine to form 0x4433… It can be seen that the data at the low address becomes the low byte of uint16_t! This is little-endian mode!

Similar issues can also arise during the process of forcibly converting to a structure, and the actual structure obtained may have some members “changed” due to endianness issues!

Using the same data stream, a structure pointer p2 is used to point to this data stream for parsing, comparing the situation of different platform structure members after type casting.

On a big-endian 51 platform:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

On a little-endian ARM platform:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

Comparing the values of structure members, the values of uint16_t and uint32_t members are reversed:

Solutions to Endianness Issues in Embedded Programming

It can be seen that during the type casting process, if the endianness issue is ignored, the conversion result will be inconsistent with expectations, leading to either program processing exceptions or memory overflow causing system crashes!

  • Data Transmission

Data transmission, when using embedded peripheral serial interfaces for communication, is mostly done byte by byte. If a uint16_t or uint32_t type data needs to be transmitted, it is also sent byte by byte, which introduces the issue of endianness.

This is essentially the reverse process of the previous issue of packet parsing and assembly, which is to forcibly convert specific structured data into a byte stream for processing or transmission byte by byte at the interface.

Constructing a structure, assigning values to each member of the structure, and then passing the structure object to the interface for transmission, let’s observe the differences in big-endian and little-endian platforms. The debug process screenshots are as follows:

Solutions to Endianness Issues in Embedded Programming

A uint8_t type pointer p3 points to the starting address of the structure, which is the address of the first member, p3 = &tmp_stru.val_u8;. This statement assigns the address of the structure to p3. When calling the transmission interface, the interface parameter requires a uint8_t type pointer, so p3 can be passed as a parameter to the processing interface trans_port().

Continuing to debug and trace the data stream situation obtained inside the transmission interface:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

Observing the indata_tmp array, the sequentially filled data is 0x10, 0x20, 0x30, 0x40, 0x50,…. It can be seen that uint16_t and uint32_t type data is “split” during processing, with the high byte processed first. For example, the data 0x2030 is processed as 0x20 first and then 0x30, and similarly, the data 0x40506070 is processed as 0x40 first and then 0x50! This is big-endian mode!

Running the same code on a little-endian ARM platform yields completely different results:

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded Programming

On the little-endian ARM platform, processing uint16_t and uint32_t type data is done in reverse order!

In summary, the above validations provide a deep understanding and memory of the endianness differences in embedded platform development. The endianness differences of the platform need to be considered not only in data storage but also during reading, usage, and passing to avoid unnecessary bugs.

  • Common Methods to Handle Differences

Regarding the handling of differences, as mentioned earlier, using uint8_t type for design is relatively safe and reliable. However, when the data volume is large and project requirements are complex, it may not suffice, so we still need to confront these hard issues directly.

Here are a few simple conversion processes summarized.

  • 1. Multiple uint8_t to uint16_t/uint32_t

This conversion can be easily implemented:

val_u16 = (val_u8_tmp1<<8) | val_u8_tmp2;

This conversion essentially combines two uint8_t types into uint16_t or uint32_t. In actual project engineering, it is often necessary to convert parts of uint8_t type data streams, so it is directly written as a macro definition for ease of development and portability:

Convert little-endian to uint16_t/uint32_t

Solutions to Endianness Issues in Embedded Programming

Convert big-endian to uint16_t/uint32_t

Solutions to Endianness Issues in Embedded Programming

Analyzing the conversion of little-endian to uint16_t type data, it is essentially: ((uint16_t)*(volatile uint8_t*)((ptr)+1)<<8) | (uint16_t)*(volatile uint8_t*)(ptr) where two data are combined using bitwise OR.

1.The right half (volatile uint8_t*)(ptr) casts the address pointed to by ptr to volatile uint8_t* type;

2.Then, using *(volatile uint8_t*)(ptr) retrieves the byte from memory based on the address;

3.Next, the retrieved byte is forcibly converted to uint16_t type.

Using the same principle,

1.The left half (volatile uint8_t*)((ptr)+1) casts the next memory unit pointed to by ptr to volatile uint8_t* type;

2.Then, using *(volatile uint8_t*)((ptr)+1) retrieves the byte from memory based on the address;

3.Next, the retrieved byte is forcibly converted to uint16_t type and shifted left by 8 bits.

Finally, the two results are combined using bitwise OR, achieving the “combination” and returning a uint16_t type result.

The above process is somewhat verbose, and some of the type casts and volatile modifiers are necessary because for the designed macro to be reliable, it must consider data operations in systems with interrupts and event handling, which require caution!

  • 2. Single uint16_t/uint32_t to uint8_t

Converting a single word/double word to bytes is less common, and the simplest way is to use bitwise AND (&) and shift operations.

Get low byte:

val_u8_tmp = val_u16&0xFF;

Get high byte:

val_u8_tmp =(val_u16>>8)&0xFF;

  • 3. Intra-conversion of uint16_t/uint32_t types

The principle is the same, operating on memory addresses, written as macro definitions for convenience.

Solutions to Endianness Issues in Embedded Programming

The result of this conversion is to reverse the original data into the opposite endianness.

In summary, clarifying the details of project development issues is beneficial for subsequent development work. Moreover, when there is a communication requirement, it is especially necessary to ensure effective communication and coordination of communication protocols, designing a universally applicable protocol structure for both parties, which provides a guideline for subsequent development and can help quickly locate issues when solving bugs!

★★★★★ Recommended Articles

“Embedded Hardware Communication Interface – Using RingBuffer to Process Data (II) Detailed Design Process”

“Embedded Hardware Communication Interface – Using RingBuffer to Process Data (I)”

“Rapid Development of MQTT (I) MQTT from the Perspective of Electronic Engineers”

“Rapid Development of MQTT (II) Introduction to MQTT”

“Building an MQTT Client – The Clearest MQTT Protocol Architecture”

“Building an MQTT Server – The Fastest Way to Validate Your Developed Client”

★★★★★ Similar Articles

“Embedded Hardware Communication Interface Protocol – UART (V) Packet Design and Parsing”

“Embedded Hardware Communication Interface Protocol – UART (IV) Designing Application Layer Protocols”

“Embedded Hardware Communication Interface Protocol – UART (III) Quick Use of Serial Ports and Applications”

“Embedded Hardware Communication Interface Protocol – UART (II) Standards under Different Electrical Specifications”

“Embedded Hardware Communication Interface Protocol – UART (I) Protocol Basics”

“Embedded Hardware Communication Interface Protocol – SPI (I) Protocol Basics”

★★★★★ Further Reading

“【Hardware Circuit】Altium Designer 18 Rule Check Meanings”

“【Hardware Circuit】Basic Principles and Application Cases of N-channel and P-channel MOSFETs”

Solutions to Endianness Issues in Embedded Programming

Long press the QR code to followDigCore

Follow the public account and send a message:

Download & BigLittleEndian

Get the original PDF of this article or download link for source code

Solutions to Endianness Issues in Embedded Programming

Solutions to Endianness Issues in Embedded ProgrammingSolutions to Endianness Issues in Embedded Programming

www.digcore.cn

More technical content awaits you!

Long press the QR code to follow

Solutions to Endianness Issues in Embedded ProgrammingClick the original text for more content!

Leave a Comment