Preventing Compiler Optimization of Memory-Mapped Registers with volatile
Use volatile-qualified pointers to memory-mapped registers so the compiler emits every read and write exactly as written.
02 Dec 2025, 10:12 UTC

The problem: a register read that never happens
You poll a hardware status register in a loop, waiting for a peripheral to set a flag. The code compiles cleanly, but on the board it hangs forever. The compiler, optimizing for speed, noticed that nothing in your C code changes the register between reads. It loaded the value once, reused that copy, and never went back to the bus. From the compiler’s perspective, the program is correct. From the hardware’s perspective, the register was never read again.
The takeaway: declare memory-mapped registers as pointers to volatile objects, not just ordinary pointers. volatile tells the compiler that an object may change outside the abstract machine—the C standard’s idealized model of program execution—so every read and write must be emitted exactly as written.
Qualify the object, not the pointer
There are two ways to add volatile to a pointer declaration, and they mean different things:
| Declaration | Meaning |
|---|---|
volatile uint32_t *reg; | The data at the address may change unexpectedly. Every access through reg is preserved. |
uint32_t * volatile reg; | The pointer variable itself may change. The register contents are treated as ordinary memory. |
For memory-mapped I/O, you almost always want the first form: volatile uint32_t *reg. The pointer address is fixed by the hardware map; what varies is the register content. Pair it with uint32_t from <stdint.h> (standardized in C99) so the access width matches the peripheral’s bit width on both 32-bit and 64-bit targets.
Worked example: polling a status register
Assume a 32-bit status register at address 0x40001000. Bit 0 is the “ready” flag.
#include <stdint.h>
/* Address is fixed by the silicon memory map. */
volatile uint32_t *const status_reg = (volatile uint32_t *)0x40001000u;
int wait_for_ready(void)
{
while ((*status_reg & 1u) == 0u) {
/* Intentional empty body: spin until hardware sets the flag. */
}
return 0;
}
The const qualifier on the pointer prevents accidental reassignment of the address. The volatile qualifier on uint32_t prevents the compiler from caching the register value in a CPU register.
To verify, compile with optimization and inspect the assembly:
# Run on a host with a cross-compiler or native toolchain targeting your architecture.
# No special permissions are required for compilation.
arm-none-eabi-gcc -O2 -c register_example.c -o register_example.o
arm-none-eabi-objdump -d register_example.o
Expected check: the disassembly should show a separate load instruction inside the loop body on every iteration. If you see the load hoisted outside the loop, volatile is missing or misapplied.
Risk: casting an arbitrary integer to a pointer is implementation-defined. If the address is not aligned for uint32_t, or if it does not map to a real peripheral, a write can trigger a hard fault or bus error at runtime. Validate addresses in bring-up code before deploying.
What volatile does not do
volatile stops unwanted optimization, but it is not a synchronization primitive. It does not guarantee:
- Atomic read-modify-write operations;
- Ordering relative to non-volatile memory accesses;
- Visibility across multiple CPU cores;
- Protection against concurrent interrupt access.
If an interrupt handler and main code share a flag, use volatile sig_atomic_t or proper atomic types from <stdatomic.h> (C11), along with memory barriers where the architecture requires them. Some embedded compilers offer extensions such as __volatile or strong ordering attributes; behavior varies by vendor and optimization level, so check your compiler documentation.
Practical verification on hardware
After compiling, run the code on actual hardware or a cycle-accurate simulator. Use a debugger or logic analyzer to confirm that the bus transactions happen at the expected times and that writes land on the intended address. If the peripheral never sees the write, check the address map and alignment before blaming the compiler.
Closing
Declare hardware registers as volatile uint32_t * (or the appropriate fixed-width type), keep the pointer address constant with const, and verify the generated assembly under optimization. Treat volatile as an optimization barrier, not a concurrency tool, and validate addresses early in bring-up to avoid hard faults.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.