Embedded C

Structs, Unions & Bit-Fields

C's Structs & Unions chapter covers the general concept — bundling fields, or sharing memory between interpretations. On real hardware, both get used for something more specific: making a C type's memory layout match something outside the compiler's control exactly — a hardware register map, a wire protocol's byte layout, or a single bit inside a status word — which is where bit-fields come in, a C feature that fundamentals-level coverage skips entirely.

Quick reference
What it is
Structs and unions used to mirror an externally-defined memory layout exactly, plus bit-fields, a struct member syntax for describing individual bits.
Mental model
A normal struct describes memory your program owns; a hardware-mapped struct describes memory your program has to match, byte for byte, to something it doesn't control at all.
Key numbers
A bit-field's width is stated in bits, not bytes (unsigned int flag : 1; is exactly 1 bit); compilers commonly pack several bit-fields into one storage unit.
Reach for it when
Describing a hardware register's individual bits, a network/file format's exact byte layout, or any case where memory layout is dictated by something other than your own code.

Structs as register maps

A microcontroller's peripheral registers often sit at consecutive addresses — a struct whose fields match that layout exactly lets you address them by name instead of raw offsets, building directly on the memory-mapped-I/O idea from Registers & volatile:

typedef struct {
    volatile uint32_t CTRL;    // offset 0x00 — control register
    volatile uint32_t STATUS;  // offset 0x04 — status register
    volatile uint32_t DATA;    // offset 0x08 — data register
} UART_TypeDef;

#define UART1 ((UART_TypeDef *)0x40011000)

UART1->CTRL = 0x01;              // write to the real control register
uint32_t s = UART1->STATUS;      // read the real status register

This only works because the struct's field order and each field's size exactly match the hardware's real memory layout — get either wrong and every field after the mistake reads or writes the wrong address, a bug that compiles cleanly and fails silently.

Packed structs for wire protocols

Alignment & Padding covers why the compiler normally inserts invisible gaps between struct fields for performance. When a struct describes an exact byte layout defined by something external — a network packet, a file format — that padding breaks the match. A compiler extension forces the layout to be exactly as written, with no gaps:

#pragma pack(push, 1)
typedef struct {
    uint8_t  version;
    uint16_t length;
    uint8_t  flags;
} PacketHeader;   // exactly 4 bytes, no padding — matches the wire format exactly
#pragma pack(pop)

Bit-fields

A bit-field declares a struct member with an explicit width in bits, not bytes — the compiler packs several of them together into shared storage:

typedef struct {
    unsigned int power_on   : 1;   // 1 bit
    unsigned int error_flag : 1;   // 1 bit
    unsigned int mode       : 2;   // 2 bits, values 0-3
    unsigned int reserved   : 4;   // unused padding bits
} StatusRegister;   // all 4 fields fit in a single byte

StatusRegister status = {0};
status.power_on = 1;
status.mode = 2;

if (status.error_flag) {
    handle_error();
}
One byte split into bit-fields: bit 0 is power_on, bit 1 is error_flag, bits 2-3 are mode, and bits 4-7 are reserved padding power_on error_flag mode (2 bits) reserved (4 bits, unused) bit 0 bit 1 bits 2-3 bits 4-7
All four fields share one physical byte — the compiler generates the shifting and masking for you every time you read or write one.

Bit-fields vs. manual masking

Everything a bit-field does can also be written by hand with bitwise operators (see C's Variables, Data Types & Operators for the bitwise basics):

// Manual masking — explicit, portable, and predictable
#define POWER_ON_MASK 0x01
status_byte |= POWER_ON_MASK;              // set the bit
int is_on = (status_byte & POWER_ON_MASK) != 0;   // test the bit

// Bit-field — more readable, but with layout caveats (see Gotchas)
status.power_on = 1;
int is_on = status.power_on;

Bit-fields read better, but manual masking gives full, guaranteed control over exactly which physical bit gets set — many embedded codebases deliberately prefer manual masking for anything that has to match an external, fixed specification exactly, for reasons covered next.

Real-world applications

Gotchas

Field notes