Hello! Welcome back to our course on embedded driver development.
In our last lesson, we explored how to use structs and unions to create clean, readable, and maintainable interfaces for memory-mapped peripherals. We saw how the C language maps these data structures onto specific memory layouts. Today, we'll continue this exploration of memory layout by focusing on a fundamental concept: how individual multi-byte variables are stored.
This lesson addresses the learning outcome: Write a C program to detect the endianness of the target processor. Understanding endianness is critical for data serialization, network communication, and ensuring portability—and it's a classic topic in embedded software interviews.
1. What is Endianness?
Endianness refers to the order in which a processor stores the bytes of a multi-byte data type (like a uint32_t or uint16_t) in memory. There are two primary formats: Big Endian and Little Endian.
- Big Endian: The "big end" or Most Significant Byte (MSB) is stored first, at the lowest memory address. This is often described as the most "natural" way to store numbers, as it mirrors how we write them.
- Little Endian: The "little end" or Least Significant Byte (LSB) is stored first, at the lowest memory address.
This concept is essential in your field. While many processors you'll encounter (like most ARM Cortex-M/R cores) operate in Little Endian mode, network protocols (like TCP/IP, used in Automotive Ethernet) mandate a Big Endian byte order. This is why you often see functions like htonl() (host-to-network-long) in network stacks.
Let's start by solidifying these definitions.
Little Endian and Big Endian | Full Guide with Examples ...
The article 'Little Endian and Big Endian' from Embetronicx provides a clear and concise introduction to this topic.
Please read the sections 'What is Endianness?', 'Difference between Little Endian and Big Endian', and 'Example'. These will define the concept and walk through a concrete example using the value 0x11223344.
To visualize this, consider storing the 32-bit integer 0x12345678 starting at memory address 0x1000.
- The Most Significant Byte (MSB) is
0x12. - The Least Significant Byte (LSB) is
0x78.
Here’s how it would look in memory:
| Endianness | Address 0x1000 |
Address 0x1001 |
Address 0x1002 |
Address 0x1003 |
|---|---|---|---|---|
| Big Endian | 0x12 |
0x34 |
0x56 |
0x78 |
| Little Endian | 0x78 |
0x56 |
0x34 |
0x12 |
The following image provides another excellent visual representation of this concept.

An interesting point for ARM architecture is that it is often bi-endian, meaning it can be configured (typically at boot time) to operate in either mode. However, in practice, the vast majority of ARM-based systems you'll work on will be configured as Little Endian.
2. Runtime Endianness Detection in C
Now for the practical part: writing a C program to determine the endianness of the processor it's running on. There are two common and reliable runtime methods to achieve this.
Method 1: Pointer Type-Casting
This is the most common technique you'll see. The logic is as follows:
- Declare a multi-byte integer, for example,
unsigned int x = 1;. - In memory, this value
1will be represented as0x00000001(on a 32-bit system). - Create a
charpointer that points to the address of this integer. Acharpointer will only look at one byte at a time. - Dereference the
charpointer to read the first byte of the integer in memory.- On a Little Endian system, the LSB (
0x01) is at the lowest address, so thecharpointer will read1. - On a Big Endian system, the MSB (
0x00) is at the lowest address, so thecharpointer will read0.
- On a Little Endian system, the LSB (
Here is a function that implements this logic:
#include <stdio.h>
int is_little_endian() {
unsigned int test_value = 1;
// Create a char pointer to the first byte of our integer
char *byte_ptr = (char *)&test_value;
// If the first byte is 1, it's Little Endian.
return (*byte_ptr == 1);
}
int main() {
if (is_little_endian()) {
printf("This system is Little Endian.\n");
} else {
printf("This system is Big Endian.\n");
}
return 0;
}
Method 2: Using a Union
This method builds directly on what we learned in the last lesson. A union allows multiple members to share the same memory location. We can leverage this to create different "views" of the same data.
- Define a
unioncontaining both anunsigned intand achar. - Assign the value
1to theunsigned intmember. - Read the value of the
charmember. Since they share the same starting memory address, thecharmember will contain the value of the first byte of the integer.- If the
charvalue is1, the system is Little Endian. - If the
charvalue is0, the system is Big Endian.
- If the
Here's the implementation:
#include <stdio.h>
int is_little_endian_union() {
union {
unsigned int i;
char c;
} endian_check;
endian_check.i = 1;
return (endian_check.c == 1);
}
int main() {
if (is_little_endian_union()) {
printf("This system is Little Endian.\n");
} else {
printf("This system is Big Endian.\n");
}
return 0;
}
Both of these methods are valid and widely used. The pointer-casting method is perhaps more common, but the union method is arguably a slightly cleaner expression of intent in C.
Little Endian and Big Endian / Detecting Endianness
The Embetronicx article and a Stack Overflow discussion both provide excellent code examples for these methods. Reviewing them will reinforce your understanding.
In the Embetronicx article, review the code under 'Example 1' and 'Example 2' in the 'Find the Endianness' section. These correspond directly to the pointer-casting and union methods we just discussed.
Test your understanding!
Imagine you use the pointer-casting method, but instead of unsigned int test_value = 1;, you use unsigned int test_value = 0xAABBCCDD;.
What value would *byte_ptr hold on a Little Endian system? What about on a Big Endian system?
Show answer
On a Little Endian system, the LSB is stored first. The LSB of 0xAABBCCDD is 0xDD. Therefore, *byte_ptr would hold the value 0xDD.
On a Big Endian system, the MSB is stored first. The MSB of 0xAABBCCDD is 0xAA. Therefore, *byte_ptr would hold the value 0xAA.
3. The Challenge of Compile-Time Detection
A natural question is: "Why do this at runtime? Can't we detect this at compile time and use preprocessor directives (#if/#else)?" This would avoid any (admittedly minuscule) runtime cost and could allow for different function declarations based on endianness.
The answer is yes, but it's not as simple as it seems because the C language standard does not define any universal macros for endianness.
- Many compilers provide their own, such as
__BYTE_ORDER__in GCC and Clang. - Different operating systems or C libraries might define their own in header files (e.g.,
<sys/param.h>on some Unix-like systems).
This leads to a fragile situation where code that relies on one compiler's macro will fail to build with another. Creating a truly portable, compile-time check requires a complex web of preprocessor directives to check for many different known macros across various compilers and platforms.
Detecting Endianness (Stack Overflow)
This Stack Overflow thread and a specific header file within it perfectly illustrate the challenge. You don't need to memorize the specifics, but appreciate the complexity involved in creating a robust, portable compile-time solution.
First, read the original question (section 0) to understand the motivation for compile-time detection. Then, look at the header file in the answer (section 3). Skim through the large block of '#if'/'#elif' directives starting around line 33. This demonstrates the patchwork of checks needed to make compile-time detection work across many environments.
Because of this complexity, the simple and reliable runtime check is often preferred unless compile-time switching is an absolute architectural requirement.
Interview Preparation
This is a favorite topic for interviewers to gauge your understanding of data representation.
-
Question: "What is endianness, and what is the difference between Big Endian and Little Endian?"
- Your Answer: "Endianness describes the byte order of multi-byte data types in computer memory. Big Endian stores the most significant byte (MSB) at the lowest memory address, which is common in networking protocols—often called network byte order. Little Endian stores the least significant byte (LSB) at the lowest memory address, which is used by most modern CPUs, including x86 and the ARM cores I typically work with."
-
Question: "How would you write a C function to determine the endianness of a system? Can you show me two different ways?"
- Your Answer: "Yes. The first method is to use a pointer cast. I'd initialize an
unsigned intto 1, cast its address to achar*, and then dereference it. If the value is 1, the system is Little Endian. The second method uses aunioncontaining both anintand achar. By writing 1 to theintand reading thechar, I can check the value of the first byte to determine the endianness."
- Your Answer: "Yes. The first method is to use a pointer cast. I'd initialize an
-
Question: "You're debugging a system where a value from a CAN message appears garbled in your software. The raw CAN frame data looks correct on a bus analyzer. What could be a potential cause?"
- Your Answer: "A common cause for this is an endianness mismatch. The CAN bus itself is endian-agnostic, but the protocol specification (e.g., CANopen or a proprietary one) defines the byte order for multi-byte signals within a message payload. If the sending ECU packs the data as Big Endian and my receiving software interprets it as Little Endian without conversion, the bytes will be swapped, leading to a garbled value. I would need to verify the protocol's endianness and implement the correct byte-swapping logic if a mismatch exists."
Conclusion
In this lesson, we've demystified the concept of endianness and learned how to programmatically detect it. This knowledge is not just academic; it has direct, practical implications in automotive systems, especially when dealing with communication protocols, data logging, and cross-platform compatibility.
Key Takeaways:
- Big Endian stores the MSB first (at the lowest address).
- Little Endian stores the LSB first (at the lowest address).
- You can reliably detect endianness at runtime using either pointer-casting or a union.
- Compile-time detection is complex and non-portable due to a lack of standard C preprocessor macros.
- Endianness is a critical consideration in networking (SOME/IP, TCP/IP), data serialization, and communication protocols like CAN.
Next Lesson Preview:
We have now established a solid foundation in how memory is structured and accessed, both for peripheral registers and for data types. In our next lesson, we will shift our focus from memory layout to processor behavior. We'll dive into the ARM exception model, exploring concepts like Reset, Faults, and Interrupt Requests (IRQs). This is the gateway to understanding how an embedded system reacts to the world around it.