Memory-Mapped Registers

Peripherals appear as addresses. Reach them with pointers to fixed addresses and register structs that match the datasheet exactly.

On a microcontroller, peripherals are wired into the address space. Writing to address 0x40020014 on an STM32F4 does not touch RAM — it changes the output pins of GPIO port A. C reaches these addresses by converting an integer to a pointer to volatile.

Reserved gaps and access width

Without volatile the compiler may cache, merge or drop accesses to the register.

struct { volatile uint32_t CR; volatile uint32_t DR; } with the datasheet showing DR at +0x08

Add uint32_t RESERVED0; between them and _Static_assert(offsetof(T, DR) == 0x08)

A missing padding word shifts every later register by 4 bytes — writes land in the wrong register.

What does (*(volatile uint32_t *)0x40020014UL) = 1; do, step by step?

Converts the integer address into a pointer to a volatile 32-bit value, dereferences it, and stores 1 there — a 32-bit bus write to the register at that address.

How can you make the compiler verify your register struct against the datasheet?

With _Static_assert(offsetof(Type, Member) == expected_offset, "...") and _Static_assert(sizeof(Type) == block_size, "...").