Table of Contents
KEY TAKEAWAYS
- Unions overlay multiple data types at the same memory address for efficient type conversion
- Protocol parsing uses unions to interpret raw byte buffers as structured message fields
- Unions combined with structs enable access to both individual bytes and multi-byte values
- This technique is common in embedded communication stacks for packing/unpacking serial data
What is a Union in C?
A union in C is a user-defined data type that allows you to store different data types in the same memory location. Unlike a structure where each member gets its own memory, all members of a union share the same block of memory. The size of a union equals the size of its largest member.union Data {
int i; // 4 bytes
float f; // 4 bytes
char c; // 1 byte
};
// Size of this union = 4 bytes (size of largest member)This memory sharing property makes unions extremely useful for packing and unpacking data, especially in embedded systems where you frequently work with hardware registers, communication protocols, and raw byte streams.Why Packing and Unpacking Matters
In embedded systems, you often need to:- Send a multi-byte value (like a 32-bit float) over a byte-oriented protocol (UART, SPI, I2C)
- Parse incoming raw bytes into meaningful data types
- Access individual bytes of a hardware register
- Construct protocol frames from different fields
Packing: Breaking a Value into Bytes
Suppose you need to send a 32-bit floating-point temperature reading over UART, which sends one byte at a time. A union makes this straightforward:#include <stdio.h>
union FloatBytes {
float value;
uint8_t bytes[4];
};
void send_float_over_uart(float temperature) {
union FloatBytes data;
data.value = temperature;
// Now we can access individual bytes
for (int i = 0; i < 4; i++) {
uart_send_byte(data.bytes[i]); // Send each byte
printf("Byte %d: 0x%02X\n", i, data.bytes[i]);
}
}
int main() {
send_float_over_uart(23.5f);
return 0;
}Here, writing to data.value fills the 4 bytes of memory, and reading from data.bytes[] gives you access to those exact same bytes individually.Unpacking: Assembling Bytes into a Value
On the receiving end, you get raw bytes and need to reconstruct the original value:float receive_float_from_uart(void) {
union FloatBytes data;
// Receive 4 bytes
for (int i = 0; i < 4; i++) {
data.bytes[i] = uart_receive_byte();
}
// The float value is automatically available
return data.value;
}No bit shifting, no manual byte assembly. The union handles it because both value and bytes[] occupy the same memory.Practical Example: Sensor Data Protocol
Imagine a sensor sends a 10-byte data packet with the following structure:- Byte 0: Sensor ID (uint8_t)
- Bytes 1-4: Temperature (float)
- Bytes 5-8: Pressure (float)
- Byte 9: Checksum (uint8_t)
#include <stdint.h>
typedef struct __attribute__((packed)) {
uint8_t sensor_id;
float temperature;
float pressure;
uint8_t checksum;
} SensorPacket;
typedef union {
SensorPacket packet;
uint8_t raw[sizeof(SensorPacket)];
} SensorFrame;
// Receiving data
void process_sensor_data(void) {
SensorFrame frame;
// Fill raw bytes from communication buffer
for (int i = 0; i < sizeof(SensorFrame); i++) {
frame.raw[i] = receive_byte();
}
// Access parsed fields directly
printf("Sensor ID: %d\n", frame.packet.sensor_id);
printf("Temperature: %.2f Cn", frame.packet.temperature);
printf("Pressure: %.2f hPan", frame.packet.pressure);
printf("Checksum: 0x%02X\n", frame.packet.checksum);
}
// Sending data
void send_sensor_data(uint8_t id, float temp, float pressure) {
SensorFrame frame;
frame.packet.sensor_id = id;
frame.packet.temperature = temp;
frame.packet.pressure = pressure;
frame.packet.checksum = calculate_checksum(frame.raw, 9);
// Send raw bytes
for (int i = 0; i < sizeof(SensorFrame); i++) {
send_byte(frame.raw[i]);
}
}Hardware Register Access
Unions are commonly used to access hardware registers where the same register can be viewed as a whole or as individual bit fields:typedef union {
uint8_t reg; // Access the full 8-bit register
struct {
uint8_t mode : 2; // Bits 0-1: Operating mode
uint8_t enable: 1; // Bit 2: Enable flag
uint8_t irq : 1; // Bit 3: Interrupt flag
uint8_t speed : 3; // Bits 4-6: Speed setting
uint8_t ready : 1; // Bit 7: Ready status
} bits;
} ControlRegister;
void configure_device(void) {
ControlRegister ctrl;
// Write the full register at once
ctrl.reg = 0x00;
// Or set individual fields
ctrl.bits.mode = 2; // Mode 2
ctrl.bits.enable = 1; // Enable the device
ctrl.bits.speed = 5; // Speed setting 5
// Write to hardware
DEVICE_CTRL_REG = ctrl.reg;
// Read back and check a field
ctrl.reg = DEVICE_CTRL_REG;
if (ctrl.bits.ready) {
printf("Device is readyn");
}
}This is much more readable than using bit masks and shifts:// Without union - harder to read uint8_t reg = 0; reg |= (2 << 0); // mode reg |= (1 << 2); // enable reg |= (5 << 4); // speed
Converting Between Data Types
Unions are useful when you need to inspect the internal representation of a value:union IntBytes {
uint32_t value;
uint8_t bytes[4];
};
void print_bytes(uint32_t val) {
union IntBytes data;
data.value = val;
printf("Value: %u (0x%08X)\n", val, val);
printf("Bytes: ");
for (int i = 0; i < 4; i++) {
printf("0x%02X ", data.bytes[i]);
}
printf("n");
}
// Output on little-endian system:
// Value: 305419896 (0x12345678)
// Bytes: 0x78 0x56 0x34 0x12This also reveals the endianness of the system, which matters when communicating between different architectures.Important Considerations
1. Endianness
The byte order depends on the processor architecture. A little-endian system (like ARM Cortex-M, x86) stores the least significant byte first, while a big-endian system stores the most significant byte first. When packing data for communication between different systems, you may need to handle byte order explicitly.2. Structure Padding
Compilers may add padding bytes in structures for alignment. When using unions for protocol parsing, use__attribute__((packed)) (GCC) or #pragma pack(1) to prevent padding.3. Type Punning and Strict Aliasing
Accessing a union member that was not the last one written to is technically implementation-defined in C (though well-defined in C99 and later). In practice, all major compilers for embedded systems support this pattern.4. Only One Member is Valid at a Time
Since all members share memory, writing to one member overwrites the others. This is actually what makes packing and unpacking work, but be aware that you should not expect independent values.Summary
Unions are one of the most practical tools in embedded C programming for handling raw data:- Packing: Write a multi-byte value, then read it as individual bytes for transmission
- Unpacking: Write individual received bytes, then read the assembled multi-byte value
- Register access: View a register as a whole byte or as individual bit fields
- Protocol parsing: Overlay a structure on a raw byte buffer for easy field access

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






