In embedded systems, every byte matters. Whether you are working with a small 8-bit microcontroller, an ARM Cortex-M device, or a memory-constrained IoT module, understanding how data is stored in memory can help you write more efficient and reliable firmware.
One common source of confusion is the size of struct variables in C. Beginners often expect the size of a structure to be equal to the sum of the sizes of its members. But in many cases, sizeof(struct) returns a larger value than expected.
The reason is padding.
Structure padding is added by the compiler to make memory access faster and properly aligned for the target processor. In embedded systems, this can affect RAM usage, communication protocols, EEPROM layouts, flash storage, and hardware register mapping.
What Is a Structure in C?
A structure, or struct, is a user-defined data type that groups related variables together.
For example:
typedef struct {
uint8_t id;
uint16_t value;
uint8_t status;
} SensorData;
At first glance, you might expect this structure to use:
uint8_t id; // 1 byte
uint16_t value; // 2 bytes
uint8_t status; // 1 byte
Total:
1 + 2 + 1 = 4 bytes
But depending on the compiler and target architecture, this structure may actually occupy 6 bytes instead of 4.
Why? Because the compiler may insert unused bytes between members. These unused bytes are called padding bytes.
What Is Padding?
Padding is extra space inserted by the compiler between structure members or at the end of a structure.
The purpose of padding is to align data members to memory addresses that are efficient, or sometimes required, for the CPU to access.
For example, many 32-bit microcontrollers prefer 16-bit values to be placed at addresses divisible by 2, and 32-bit values to be placed at addresses divisible by 4.
Consider this structure:
typedef struct {
uint8_t a;
uint32_t b;
} Example;
You might expect:
a = 1 byte
b = 4 bytes
Total = 5 bytes
But the compiler may arrange it like this:
Offset 0: a
Offset 1: padding
Offset 2: padding
Offset 3: padding
Offset 4: b byte 0
Offset 5: b byte 1
Offset 6: b byte 2
Offset 7: b byte 3
So the actual size becomes:
8 bytes
The compiler inserted 3 padding bytes so that b starts at offset 4, which is properly aligned for a 32-bit value.
Why Alignment Matters in Embedded Systems
Memory alignment affects how efficiently a processor can read and write data.
On some processors, unaligned access is allowed but slower. On others, unaligned access can cause a fault or exception.
For example, reading a uint32_t from an address divisible by 4 is usually efficient. But reading the same value from an odd address may require multiple memory accesses, or may not be allowed at all.
This is especially important in embedded systems because firmware often interacts directly with:
- Peripheral registers
- Communication buffers
- EEPROM or flash memory
- DMA buffers
- Packed protocol frames
- Bootloaders
- Memory-mapped hardware
A structure that looks correct in C may not have the exact memory layout you expect unless you account for padding.
Example: Padding Changes Structure Size
Consider this structure:
#include <stdint.h>
#include <stdio.h>
typedef struct {
uint8_t id;
uint32_t count;
uint16_t voltage;
} DeviceData;
Expected size:
id = 1 byte
count = 4 bytes
voltage = 2 bytes
Total = 7 bytes
But the actual layout may look like this:
Offset 0: id
Offset 1: padding
Offset 2: padding
Offset 3: padding
Offset 4: count
Offset 8: voltage
Offset 10: padding
Offset 11: padding
Actual size:
12 bytes
That is 5 extra bytes of padding.
On a desktop computer, this may not matter much. But on a microcontroller with only 2 KB of SRAM, wasting several bytes per structure can become a real problem, especially if you create large arrays.
For example:
DeviceData devices[100];
If each structure is 12 bytes instead of 7 bytes, the array uses:
12 × 100 = 1200 bytes
instead of:
7 × 100 = 700 bytes
That is 500 extra bytes of RAM.
Member Order Affects Padding
One simple way to reduce padding is to arrange structure members from largest to smallest.
Instead of this:
typedef struct {
uint8_t id;
uint32_t count;
uint16_t voltage;
} DeviceData;
Use this:
typedef struct {
uint32_t count;
uint16_t voltage;
uint8_t id;
} DeviceDataOptimized;
The new layout may be:
Offset 0: count // 4 bytes
Offset 4: voltage // 2 bytes
Offset 6: id // 1 byte
Offset 7: padding // 1 byte
Actual size:
8 bytes
By simply reordering the members, the structure size is reduced from 12 bytes to 8 bytes.
That may not seem like much for one variable, but it matters when the structure is used in arrays, queues, buffers, logs, and communication packets.
Checking Structure Size with sizeof()
sizeof()The
sizeof() operator tells you the actual memory size used by a structure.
Example:
printf("Size of DeviceData: %u\n", sizeof(DeviceData));
printf("Size of DeviceDataOptimized: %u\n", sizeof(DeviceDataOptimized));
On embedded systems, you may not always have printf() available. In that case, you can inspect the size in the debugger, use a compile-time assertion, or view the map file generated by the compiler.
Example using a compile-time check:
_Static_assert(sizeof(DeviceDataOptimized) == 8, "Unexpected struct size");
This is useful when the structure size must not change, such as when it is used for a communication packet or saved data format.
Padding at the End of a Structure
Padding can also be added at the end of a structure.
For example:
typedef struct {
uint32_t timestamp;
uint8_t status;
} LogEntry;
The members use:
timestamp = 4 bytes
status = 1 byte
Total = 5 bytes
But the structure may still be 8 bytes because the compiler pads the end of the structure to maintain alignment when used in arrays.
Why?
Because in this array:
LogEntry logs[10];
each timestamp in each array element should still be aligned properly.
Without end padding, the second structure might start at an address that causes its timestamp member to become misaligned.
Packed Structures
Sometimes, you need the structure layout to match an exact byte format. This is common in communication protocols, file formats, EEPROM layouts, and binary packet parsing.
In GCC, you can use the packed attribute:
typedef struct __attribute__((packed)) {
uint8_t id;
uint32_t count;
uint16_t voltage;
} PackedDeviceData;
This tells the compiler not to insert padding bytes.
The structure size becomes:
1 + 4 + 2 = 7 bytes
For ARM compilers or other toolchains, the syntax may be different. Some compilers use pragmas such as:
#pragma pack(push, 1)
typedef struct {
uint8_t id;
uint32_t count;
uint16_t voltage;
} PackedDeviceData;
#pragma pack(pop)
However, packed structures should be used carefully.
The Danger of Packed Structures
Packed structures save memory, but they can also create problems.
When a structure is packed, members may be placed at unaligned addresses. Accessing those members directly can be slower or unsafe on some microcontrollers.
For example:
typedef struct __attribute__((packed)) {
uint8_t header;
uint32_t value;
} Packet;
In this structure, value may start at offset 1. That means it is not aligned to a 4-byte boundary.
On some processors, this access may be inefficient. On others, it may cause a hard fault.
A safer approach is to copy the unaligned data into an aligned variable before using it:
uint32_t value;
memcpy(&value, &packet.value, sizeof(value));
This may look unnecessary, but it avoids unaligned memory access problems and is often safer for portable embedded code.
Structures and Communication Protocols
Padding becomes very important when sending structures over UART, SPI, I2C, CAN, BLE, or Ethernet.
For example:
typedef struct {
uint8_t command;
uint16_t length;
uint32_t checksum;
} Message;
If you send this directly:
uart_write((uint8_t *)&msg, sizeof(msg));
you may accidentally send padding bytes too.
The receiver may expect a compact packet, but the transmitted data may contain extra bytes inserted by the compiler. This can break communication between devices, especially if the other side is written in a different language, uses a different compiler, or runs on a different architecture.
For protocol data, it is often better to serialize the packet manually:
buffer[0] = command;
buffer[1] = length & 0xFF;
buffer[2] = (length >> 8) & 0xFF;
buffer[3] = checksum & 0xFF;
buffer[4] = (checksum >> 8) & 0xFF;
buffer[5] = (checksum >> 16) & 0xFF;
buffer[6] = (checksum >> 24) & 0xFF;
Manual serialization gives you full control over byte order, padding, and packet format.
Structures and EEPROM or Flash Storage
Another common mistake is saving a structure directly to EEPROM or flash:
eeprom_write((uint8_t *)&settings, sizeof(settings));
This works only if you are certain the structure layout will never change.
Problems can happen when:
- The compiler changes
- Optimization settings change
- The target architecture changes
- Members are reordered
- New fields are added
- Packing settings change
- Padding bytes contain random values
If the structure is used for persistent storage, consider adding:
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t size;
uint8_t data[32];
uint32_t crc;
} SettingsBlock;
A version number, size field, and CRC make the stored data easier to validate and migrate when the firmware changes.
Structures and Hardware Registers
In embedded systems, structures are often used to represent hardware registers.
Example:
typedef struct {
volatile uint32_t CR;
volatile uint32_t SR;
volatile uint32_t DR;
} UART_TypeDef;
This works because the structure is designed to match the exact register layout described in the microcontroller datasheet.
However, hardware register structures must be written very carefully. If a register is reserved, the structure must include a reserved field to preserve the correct offset.
Example:
typedef struct {
volatile uint32_t CR;
volatile uint32_t SR;
uint32_t RESERVED0;
volatile uint32_t DR;
} UART_TypeDef;
Without the reserved field, DR would appear at the wrong offset, and the firmware would access the wrong register.
This is why vendor header files usually contain many RESERVED fields in peripheral structure definitions.
How to Inspect Member Offsets
The offsetof() macro from <stddef.h> can be used to check where each member is placed inside a structure.
Example:
#include <stddef.h>
printf("id offset: %u\n", offsetof(DeviceData, id));
printf("count offset: %u\n", offsetof(DeviceData, count));
printf("voltage offset: %u\n", offsetof(DeviceData, voltage));
This helps you confirm whether the compiler inserted padding between members.
For embedded debugging, this is useful when checking protocol structures, register maps, or memory layouts.
Best Practices for Embedded Systems
When using structures in embedded firmware, keep these guidelines in mind.
- First, do not assume that the size of a structure is equal to the sum of its members. Always check with sizeof().
- Second, arrange members from largest to smallest when RAM usage matters. This can reduce padding without needing compiler-specific packing attributes.
- Third, avoid sending raw structures directly over communication interfaces unless the layout is explicitly controlled.
- Fourth, be careful with packed structures. They are useful for matching exact byte layouts, but they can cause unaligned memory access.
- Fifth, use fixed-width integer types such as uint8_t, uint16_t, and uint32_t instead of plain int, short, or long when the binary layout matters.
- Sixth, use offsetof() and _Static_assert() to verify structure sizes and member positions.
- Seventh, for EEPROM, flash, or protocol data, consider manual serialization instead of writing the raw structure directly.
- Finally, always check the compiler documentation for packing, alignment, and ABI behavior, especially when moving code between compilers or microcontroller families.
On classic AVR-based Arduino boards, structure padding is usually less noticeable because the CPU is 8-bit and has fewer strict alignment requirements. However, the habit of checking sizeof(), using fixed-width integer types, and avoiding raw struct transfers is still important—especially if the code may later run on ESP32, STM32, SAMD, RP2040, or other 32-bit boards.
Conclusion
Structure padding is one of those C language details that becomes very important in embedded systems. It affects RAM usage, binary compatibility, communication packets, EEPROM layouts, flash storage, DMA buffers, and hardware register definitions.
Padding is not a compiler bug. It is a normal part of how C structures are arranged in memory to satisfy alignment requirements.
For ordinary application-level data, padding is usually harmless. But for embedded systems, where memory layout often matters, you need to be aware of it.
The key rule is simple:
Do not guess the size or layout of a structure. Check it.
Use sizeof(), offsetof(), _Static_assert(), and careful member ordering. Use packed structures only when necessary, and be careful when accessing unaligned members.
Understanding structure padding will help you write embedded firmware that is smaller, safer, more portable, and easier to debug.





