Search topics...
Driver DesignHAL vs Bare Metalfoundational

How do you access a hardware register in C? What is the role of volatile?

1 upvote
Practice with AISoon
Study the fundamentals first — Driver Design topic page

Hardware peripherals on ARM Cortex-M are memory-mapped — each peripheral register occupies a fixed address in the processor's memory space. To access a register in C, you cast its address to a pointer and dereference it. The standard pattern uses a struct that mirrors the register layout, cast from the peripheral's base address:

c
// Typical CMSIS-style register access
#define GPIOA_BASE 0x40020000UL
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
GPIOA->ODR |= (1 << 5); // Set pin 5 high

The volatile qualifier is mandatory for all hardware register pointers. Without it, the compiler is free to optimize away accesses that appear redundant from a pure C perspective. Three specific optimizations break hardware interaction: (1) read elimination — if you read a status register twice in a loop, the compiler may cache the first read and never re-read, so you never see the flag change; (2) write elimination — writing the same value twice to a control register (e.g., to generate a pulse) may be collapsed into one write; (3) reordering — the compiler may move register accesses relative to other operations if it determines they are independent, but hardware often requires specific ordering (e.g., write control register before data register).

volatile tells the compiler: every read must actually go to memory, every write must actually be emitted, and the order in the source code must be preserved. Note that volatile does not prevent the CPU's hardware reordering — on Cortex-M7, you may still need memory barriers (__DSB(), __DMB()) for that. But for most Cortex-M0/M3/M4 code, the processor's strongly-ordered peripheral memory region combined with volatile is sufficient to guarantee correct hardware register access.

Source: Driver Design Q&A