Skip to content
Home » Embedded Systems » Embedded C » How Unions Help in Packing and Unpacking Data

How Unions Help in Packing and Unpacking Data

Unions in Packing and Unpacking Data featured image with dark blue background, C FOUNDATIONS badge, U curly braces icon in teal circle, and Byte Conversion and Protocol Parsing subtitle
Embedded Systems Learning Path
Part 26 of 129View Full Path →

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
Without unions, you would need to use bit shifting and masking, which works but can be hard to read. Unions provide a cleaner, more intuitive approach.

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)
You can define this with a union and a packed structure:
#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 0x12
This 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
Compared to manual bit shifting and masking, unions make your code cleaner, more maintainable, and less error-prone. Just remember to handle endianness and structure padding when working across different platforms.

Leave a Reply

Your email address will not be published. Required fields are marked *