Skip to Content
Course content

64: Unions and Bit Fields

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact bug pop up in countless embedded projects. A developer is trying to interface with a piece of hardware—say, a network chip or a sensor—that sends a 16-bit status register. To make the code readable, they use a union to "overlay" a bit field structure on top of a raw integer. It looks elegant on paper, but then the program starts reporting that the system is "Healthy" when the hardware is actually screaming "Critical Error."

#include <stdio.h>
#include <stdint.h>

typedef union {
    uint16_t raw;
    struct {
        uint16_t error : 1;
        uint16_t warning : 1;
        uint16_t ready : 1;
        uint16_t reserved : 13;
    } flags;
} StatusRegister;

int main() {
    StatusRegister reg;
    // Hardware sends 0x8000 (The most significant bit is set)
    // In the manual, the MSB is the 'error' bit.
    reg.raw = 0x8000; 

    if (reg.flags.error) {
        printf("Error detected!\n");
    } else {
        printf("System OK\n");
    }
    return 0;
}

When you run this on a standard x86 machine, it prints "System OK". Wait, what? We set the raw value to 0x8000, and the manual says the error bit is the most significant bit. Why is the error flag coming back as 0?

The Bit-Order Trap

Here is the thing about bit fields: the C standard gives the compiler a huge amount of leeway in how it packs them. On most common compilers (like GCC or Clang on little-endian systems), bit fields are packed from the least significant bit (LSB) to the most significant bit (MSB).

In the code above, you told the compiler that error is the first 1-bit field. The compiler put it at the very bottom (bit 0). But the hardware manual told you the error bit is at the top (bit 15). You've essentially mapped your "Error" variable to the "Reserved" section of the hardware register and your "Reserved" section to the actual error bit. I'll be honest: relying on bit fields for hardware mapping is a gamble unless you've read your compiler's ABI manual cover-to-cover.

Correcting the Layout for Little-Endian

If you absolutely want to use a union and bit fields for readability, you have to define them in the order they appear in memory, not the order they appear in the documentation's diagram. To fix the bug, we need to move the error bit to the end of the struct so it aligns with the MSB of the 16-bit word.

typedef union {
    uint16_t raw;
    struct {
        uint16_t reserved : 13; // Bottom 13 bits
        uint16_t ready : 1;
        uint16_t warning : 1;
        uint16_t error : 1;     // Top bit (MSB)
    } flags;
} StatusRegister;

Now, when reg.raw = 0x8000 is set, the 15th bit is toggled, which now maps directly to flags.error. It works, but you're still tied to a specific endianness and compiler behavior. In professional production code, I usually steer my team toward explicit bitmasking (reg & ERROR_MASK), but unions are incredibly powerful for other tasks where you need to view the same memory in two different ways without casting pointers everywhere.

When Unions Actually Shine

Unions aren't just for bit-hacking. They are most useful when you have a data structure that can be one of several different things, but never more than one at a time. Think of a "Variant" type. If you're building a protocol where a packet could be a Command, a DataPayload, or a KeepAlive, using a union allows you to allocate only enough memory for the largest of the three, rather than summing their sizes.

Just remember: the union doesn't know which member is currently "active." You'll almost always pair a union with an enum inside a larger struct to keep track of what's actually stored there. This is called a "tagged union."

The Danger of Type Punning

You'll often see people use unions to "cheat" the type system—for example, putting a float in a union and reading it back as an int to look at the raw IEEE 754 bits. This is called type punning. While most modern compilers support this, be careful. Strictly speaking, reading from a union member other than the one last written to can trigger undefined behavior in some strict interpretations of the C standard, though in practice, it's the standard way to handle raw bit access in systems programming.




📋 Practical Task

Building a Compact Network Packet Header

You are writing a driver for a legacy network protocol. The protocol uses a 32-bit header. You need to create a PacketHeader union that allows you to set the entire header as a uint32_t, but also allows you to access the following fields individually:

  • Version: 4 bits (Least Significant Bits)
  • Priority: 3 bits
  • PayloadType: 5 bits
  • SequenceNumber: 20 bits (Most Significant Bits)

Your task:

  1. Define a union called PacketHeader containing a uint32_t raw and a struct with the bit fields listed above.
  2. In your main function, create an instance of PacketHeader.
  3. Set the raw value to 0x12345678.
  4. Print the values of the Version, Priority, PayloadType, and SequenceNumber to the console.
  5. Change the Priority to 7 and the Version to 2 using the bit field members, then print the new raw hex value to verify the changes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.