Why and How to Declare Memory‑Mapped Hardware Registers as Volatile in Embedded C
In embedded C, using volatile on memory‑mapped registers forces the compiler to emit real memory accesses. This guide shows a practical UART polling example, explains how volatile works, and highlights common pitfalls and limits.
01 Jun 2026, 18:21 UTC

The Core Problem
In bare‑metal or RTOS‑based embedded systems, peripheral registers live at fixed memory addresses. The compiler treats ordinary C objects as if they are only modified by the executing thread. When you read a register in a tight loop, the optimizer may cache the value in a CPU register and skip subsequent memory accesses. That optimization is harmless for normal variables but disastrous for hardware status bits that can change asynchronously.
Using volatile Correctly
The volatile keyword tells the compiler that the object can change unexpectedly, so every load and store must be emitted as an actual memory operation. It does not make the access atomic, nor does it enforce ordering between multiple volatile objects. It simply disables caching of the value in a register across separate statements.
When mapping a register, the type must match the bus width (e.g., uint32_t for a 32‑bit peripheral). The pointer itself should not be volatile; only the pointed‑to memory should be. A typical declaration looks like:
/* UART status register at address 0x4000_0000 */
#define UART_STATUS_ADDR 0x40000000U
volatile uint32_t * const UART_STATUS = (volatile uint32_t *)UART_STATUS_ADDR;
Notice the const qualifier on the pointer, ensuring the address never changes, while volatile applies to the data at that address.
Worked Example: UART Status Polling
Consider polling the UART’s TX_READY bit (bit 0) before sending a byte:
/* Poll until TX is ready */
while (!(*UART_STATUS & 0x1U)) {
/* Busy wait */
}
/* Write data */
*UART_STATUS = data;
With -O2 or higher, a compiler without volatile would transform the loop into:
uint32_t tmp = *UART_STATUS;
while (!(tmp & 0x1U)) {
/* busy wait */
}
which reads the register once and reuses tmp. The volatile qualifier forces a reload on each iteration.
Verifying the Compiler Behavior
- Compile with optimization:
gcc -O2 -S uart_poll.c -o uart_poll.s - Search the assembly for the address: look for
0x40000000or the symbol nameUART_STATUS. - Check the loop body: you should see a
ldr(load) instruction inside the loop, not a cached register. - Compare without
volatile: remove the qualifier and repeat the steps. The loop should contain a singleldrbefore the loop and none inside.
Additionally, inspect the linker map to confirm that UART_STATUS resolves to the peripheral region and not to RAM.
Limits of volatile
- It does not guarantee atomic read‑modify‑write operations. For bit‑field updates, use device‑specific bit‑banding or disable interrupts.
- It provides no ordering guarantees between multiple volatile accesses. If you need a memory barrier, use platform‑specific intrinsics (e.g.,
__sync_synchronize()on GCC). - Across CPU cores or DMA,
volatilealone does not ensure visibility. Pair it with cache‑flush or memory‑barrier instructions.
Common Mistakes
- Volatile on the pointer, not the data:
volatile uint32_t * const UART_STATUSis wrong; it makes the pointer volatile, not the memory it points to. - Copying into a non‑volatile local:
uint32_t status = *UART_STATUS;followed bywhile (!(status & 0x1U))will not re‑read the register. - Assuming
volatilemakes a read‑modify‑write atomic. Use device‑specific atomic primitives instead. - Using the wrong width type. A 16‑bit peripheral accessed as 32‑bit can cause bus errors on some architectures.
Conclusion
Declaring memory‑mapped registers as volatile is essential to preserve correct hardware interaction. It forces real memory accesses, preventing the optimizer from caching stale values. However, remember that volatile does not replace atomicity or ordering guarantees. Pair it with proper synchronization primitives when needed, and always verify the generated assembly to ensure the compiler behaves as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.