Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 15

Endianness Detection and Byte Order Conversion

Recommended resources

Hello! Welcome to your next lesson in the Embedded Interview Preparation Bootcamp.

In our last session, we mastered the container_of pattern, a technique focused on memory layout and addressing to create efficient, intrusive data structures. We saw how to find the start of a struct from a pointer to one of its members.

Today, we shift our focus from where data is to how it's represented at the byte level. Once we have a pointer to a multi-byte data type like a uint32_t, how are its constituent bytes arranged in memory? This question leads us directly to the concept of endianness.

This lesson addresses the learning outcome: Write code to detect system endianness at runtime and perform conversions between little-endian, big-endian, and network byte order. Understanding this is non-negotiable for roles involving hardware interaction, network protocols, or binary file formats at companies like Qualcomm, AMD, and Nvidia.

1. What is Endianness?

Endianness refers to the order in which bytes of a multi-byte data type (like int, short, long) are stored in computer memory. There are two primary types:

  • Big-Endian: The "big end" (most significant byte) is stored first, at the lowest memory address. This is intuitive, as it's how we read numbers.
  • Little-Endian: The "little end" (least significant byte) is stored first.

Let's look at the 32-bit integer 0x0A0B0C0D.

Little-endian vs. Big-endian Memory Storage
This diagram shows how the 32-bit hex value 0x0A0B0C0D is stored in memory. In little-endian systems (like Intel x86 and most modern ARM), the least significant byte `0x0D` is at the lowest address. In big-endian systems, the most significant byte `0x0A` is at the lowest address.

Most modern CPUs you'll work with, such as those based on x86-64 and ARM, are little-endian. Big-endian was common in older architectures like PowerPC and SPARC. Interestingly, some architectures (like ARMv3 and later) are bi-endian, meaning they can be configured to operate in either mode.


2. Why Endianness Causes Problems

For code running on a single machine, you can often ignore endianness. The compiler and CPU handle it transparently. The problems arise when you move binary data between systems with different endianness. The most common scenarios are:

  1. Network Communication: Sending binary data between two computers.
  2. File I/O: Saving a binary file (like an image or a configuration file) on one machine and reading it on another.
  3. Memory-Mapped I/O: A little-endian CPU communicating with a big-endian hardware peripheral on the same SoC.

Let's watch a practical demonstration of how this can go wrong.

the TRUTH about numbers and network programming (endianess or ELSE)

The following video from the 'Low Level' channel provides an excellent, practical demonstration. It shows a simple C client-server application where sending a number from a little-endian machine to a big-endian machine results in data corruption.

Watch from the beginning up to the 3:19 mark. Pay attention to how the same number, 4, is interpreted completely differently when received on a machine with a mismatched byte order.

As the video shows, the big-endian client interprets the bytes 04 00 00 00 (sent by the little-endian server) as 0x04000000, which is 67,108,864, not 4.


3. Detecting System Endianness: A Classic Interview Question

A very common interview question is, "Write a C function to determine if the host system is little-endian or big-endian." There are two classic approaches to solve this at runtime.

Top 100 Embedded C Interview Questions

The following resource provides concise code for the two most common methods used to detect endianness. Understanding both is essential for interview preparation.

Please study the two methods presented under the question 'Write a C program to check the endianness of the system.' Focus on how both the pointer-casting method (Method 1) and the union method (Method 2) allow you to inspect the first byte of a multi-byte integer.

Let's break down the logic of these two methods.

Method 1: Pointer Casting




#include <stdio.h>

void check_endianness_pointer() {
    unsigned int i = 1;
    char *c = (char*)&i;
    if (*c) {
        printf("System is Little-Endian\n");
    } else {
        printf("System is Big-Endian\n");
    }
}

Explanation:

  1. We declare a multi-byte integer i and initialize it to 1.
  2. In memory, this value will be stored as either 01 00 00 00 (little-endian) or 00 00 00 01 (big-endian).
  3. We create a char pointer c and point it to the address of i. A char* always points to a single byte.
  4. When we dereference c (*c), we read the value of the very first byte at that memory location.
  5. If the system is little-endian, this first byte will be 1. If it's big-endian, it will be 0.

Method 2: Using a Union




#include <stdio.h>

void check_endianness_union() {
    union {
        unsigned int i;
        char c[4];
    } u;

    u.i = 1;
    if (u.c[0] == 1) {
        printf("System is Little-Endian\n");
    } else {
        printf("System is Big-Endian\n");
    }
}

Explanation:

  1. A union allocates enough memory to hold its largest member. All members share the same memory location.
  2. We define a union containing a unsigned int and a char array.
  3. We write 1 to the integer member u.i.
  4. We then read the first byte of that same memory location via the char array, u.c[0].
  5. The logic is the same: if u.c[0] is 1, the system is little-endian.

For performance-critical code where the target is known at compile time, you can also use preprocessor directives, as shown in the support.tools article, often using compiler-defined macros like __BYTE_ORDER__.


4. Converting Endianness: The Solution

To prevent the data corruption we saw earlier, all parties in communication must agree on a standard byte order.

4.1. Network Byte Order

For network protocols (TCP/IP), the standard is big-endian, which is referred to as Network Byte Order. The POSIX standard provides a family of functions to convert between your machine's Host Order and the standard Network Order.

Let's see how these functions are used to fix the client-server bug.

the TRUTH about numbers and network programming (endianess or ELSE)

Now, let's return to the 'Low Level' video to see the solution in action. The key is for both the sender and receiver to use conversion functions.

Watch from 3:19 to the end. Observe how htonl() is used before sending and ntohl() is used after receiving to ensure the data is interpreted correctly, regardless of the host systems' native endianness.

The key functions, typically found in <arpa/inet.h> or <winsock2.h>, are:

  • htonl(): Host to Network Long (32-bit)
  • htons(): Host to Network Short (16-bit)
  • ntohl(): Network to Host Long (32-bit)
  • ntohs(): Network to Host Short (16-bit)

On a big-endian machine, these functions might do nothing (they are effectively no-ops). On a little-endian machine, they will perform the necessary byte-swapping. This makes your code portable.

4.2. Manual Byte Swapping

In some cases (e.g., bare-metal programming without standard libraries, or dealing with a non-standard file format), you may need to swap bytes manually. This is another excellent test of C fundamentals.

Top 100 Embedded C Interview Questions

The following resource shows how to implement a byte-swapping function using bitwise operations.

Read the section 'How to Convert little-endian to big-endian vice versa in C?' and analyze the ChangeEndianness function. Trace how it uses bitwise AND (&) and bit shifts (<<, >>) to move each byte to its new position.

The logic of the ChangeEndianness function for a 32-bit value val is as follows:

  1. Isolate the LSB (val & 0x000000FF) and shift it 24 bits left to become the MSB.
  2. Isolate the second byte (val & 0x0000FF00) and shift it 8 bits left.
  3. Isolate the third byte (val & 0x00FF0000) and shift it 8 bits right.
  4. Isolate the MSB (val & 0xFF000000) and shift it 24 bits right to become the LSB.
  5. Combine these four results using bitwise OR (|).

For performance, it's often better to use a compiler intrinsic if available. For GCC/Clang, these are highly optimized built-in functions:

  • __builtin_bswap16(x)
  • __builtin_bswap32(x)
  • __builtin_bswap64(x)

These compile down to a single, efficient assembly instruction (like bswap on x86 or rev on ARM).


5. Advanced Application: Endianness in Memory-Mapped I/O

This is where endianness becomes critically important in semiconductor-related roles. An SoC often contains a CPU core (e.g., ARM, which is little-endian) and various peripherals connected via a bus (e.g., APB, AXI). The hardware designers may have specified that a peripheral's 32-bit registers are big-endian.

If your little-endian CPU writes 0x12345678 directly to a big-endian register, the hardware will see 0x78563412. To prevent this, you must byte-swap the value before writing.




#include <stdint.h>

// Assume this is a big-endian hardware register on our little-endian system
volatile uint32_t *TIMER_CTRL_REG = (uint32_t*)0x40010000;

// Function to enable the timer with a specific mode
void enable_timer(uint32_t mode) {
    uint32_t value_to_write = 0x80000000 | mode; // Set enable bit + mode




#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    // We are on a little-endian system, but the register is big-endian.
    // We must swap the bytes before writing.
    *TIMER_CTRL_REG = __builtin_bswap32(value_to_write);



#else
    // We are on a big-endian system, matching the register. No swap needed.
    *TIMER_CTRL_REG = value_to_write;



#endif
}

This code is robust and efficient. It uses a compile-time check to determine if a byte swap is necessary and then uses an optimized compiler intrinsic to perform it. This is exactly the kind of careful, low-level coding expected in driver and firmware development.


Conclusion

Today we've demystified endianness, a fundamental concept in low-level programming that is a frequent source of bugs and interview questions.

Key Takeaways:

  • Endianness is the byte order of multi-byte data types in memory (Little-Endian vs. Big-Endian).
  • Mismatches cause data corruption during network communication, binary file I/O, and memory-mapped I/O.
  • Detection can be done at runtime (using pointer casting or unions) or at compile time (using preprocessor macros).
  • Conversion for networking uses the standard htonl/ntohl family of functions. For other purposes, manual bit-shifting or compiler intrinsics like __builtin_bswap32 are used.
  • Correctly handling endianness for hardware registers is a critical skill in embedded systems programming.

Preview of the Next Lesson:
Now that we understand how to correctly write a full 32-bit value to a hardware register, considering its endianness, we will zoom in further. In the next lesson, we will develop a set of type-safe, efficient macros for hardware register manipulation, allowing us to read, write, set, and clear individual bits and bit-fields within those registers.