Embedded C

Registers & volatile

A hardware register looks like an ordinary variable in C — an address you read and write — but it isn't one, and the compiler needs to be told so explicitly. This page covers what a register actually is, exactly why volatile exists, and the bit-level manipulation that's the real, daily mechanics underneath every protocol page elsewhere in this notebook.

Quick reference
What it is
A fixed memory address that controls or reports hardware state, accessed exactly like RAM but behaving nothing like it.
Mental model
A normal variable is a note only your code writes on. A register is a note the hardware itself can also change, at any moment, with no announcement — volatile is what tells the compiler not to assume it knows the note's contents.
Key numbers
A single register commonly packs 8, 16, or 32 independent bit-fields — reading/writing "the register" as one number without masking is how you clobber a neighbor's setting.
Reach for it when
Any variable touched by hardware or an interrupt handler — never optional, never a performance-only concern.

What a register actually is

A peripheral register is a specific memory address, documented in the chip's reference manual, where reading or writing doesn't just store a value — it controls or reports the state of actual hardware (a GPIO pin's level, a timer's current count, whether a UART has a byte ready). This is memory-mapped I/O: the CPU accesses hardware using the exact same load/store instructions it uses for ordinary RAM, at an address that happens to be wired to a peripheral instead of a memory chip.

Why volatile exists

Without volatile, the compiler is allowed to assume a variable's value only ever changes through code it can see — a completely valid assumption for ordinary variables, and the basis for real optimizations like caching a value in a CPU register instead of re-reading memory every time. That assumption is simply wrong for a hardware register or a variable an interrupt handler writes asynchronously — the value can change from somewhere the compiler has no visibility into at all, which is precisely the shared-state situation already covered in Interrupts.

/* WITHOUT volatile — a real, classic bug */
while (STATUS_REG == 0) { }
/* the optimizer may read STATUS_REG exactly ONCE, decide the loop
   condition can't change (nothing in the visible code writes it), and
   generate an infinite loop that never looks at the register again */

/* WITH volatile — correct */
volatile uint32_t *status = (volatile uint32_t *)STATUS_REG_ADDR;
while (*status == 0) { }   // re-read from hardware on every iteration, guaranteed

Register access patterns

Two equivalent ways to reach a register show up constantly. Direct address-cast access:

#define GPIO_BASE      0x40020000UL
#define GPIO_ODR_OFFSET 0x14UL

*(volatile uint32_t *)(GPIO_BASE + GPIO_ODR_OFFSET) = 0x1;

And the struct-overlay pattern most vendor SDKs actually generate — a struct whose member layout exactly matches the peripheral's register map, then one pointer cast to the peripheral's base address gives named access to every register in it:

typedef struct {
    volatile uint32_t MODER;   // offset 0x00
    volatile uint32_t OTYPER;  // offset 0x04
    volatile uint32_t ODR;     // offset 0x14
} GPIO_TypeDef;

#define GPIOA  ((GPIO_TypeDef *)0x40020000UL)

GPIOA->ODR = 0x1;   // same effect as the direct-cast version, more readable

Bit manipulation

A register is rarely one logical value — it's a packed set of independent bit-fields, and writing the whole register wholesale would clobber every unrelated bit sharing it. Read-modify-write with bitwise operators touches only the bit(s) intended:

#define BIT(n) (1UL << (n))

reg |=  BIT(3);   // set bit 3, leave every other bit untouched
reg &= ~BIT(3);   // clear bit 3
reg ^=  BIT(3);   // toggle bit 3
if (reg & BIT(3)) { /* bit 3 is set */ }
An 8-bit UART status/control register, each bit position carrying a completely independent meaning — reserved bits, an overrun flag, a parity-error flag, and enable bits 7 6 5 4 3 2 1 0 RSV RSV OVR PERR TXE RXR TXEN RXEN Writing this register as one 8-bit number instead of masking individual bits would silently disable TX/RX or clear flags nothing intended to touch.
Every protocol page elsewhere in this notebook is configuring registers exactly like this one, whether it's spelled out or not — a UART's baud setting, an I2C status flag, an SPI mode bit, all live in registers structured this way.

Real-world applications

Gotchas

Field notes