Using volatile correctly for memory‑mapped registers in Embedded C
Learn why and how to use volatile for memory‑mapped registers in Embedded C, with a concrete LED‑toggle example, trade‑offs, and verification steps.
13 Jul 2025, 14:32 UTC

The problem: optimizer‑induced register access bugs
When you write to a peripheral register in bare‑metal code, you expect each C statement to translate into a distinct bus transaction. Without the proper qualifier, the compiler may treat repeated reads or writes as redundant and eliminate them, leaving the hardware unchanged.
Why volatile is required
The volatile qualifier tells the compiler that an object’s value can change at any moment, independent of the surrounding code. This prevents two common optimizations:
- Eliminating a load or store because the compiler thinks the value hasn’t changed.
- Reordering memory accesses across other operations, which can break timing‑sensitive protocols.
In practice, this means each dereference of a volatile pointer generates a separate load or store instruction in the emitted assembly.
Correct declaration patterns
For a memory‑mapped register you typically need a pointer that is both volatile and const (the pointer itself does not change, but the pointed‑to value may). The width of the pointer must match the register size.
/* 32‑bit register at address 0x40001000 */
volatile uint32_t * const UART_DR = (volatile uint32_t *)0x40001000;
/* 16‑bit register at address 0x40020000 */
volatile uint16_t * const GPIO_OUT = (volatile uint16_t *)0x40020000;
Note the placement: volatile applies to the pointed‑to type, while const after the asterisk makes the pointer immutable.
Worked example: toggling an LED
Assume an LED is connected to bit 5 of a 32‑bit GPIO output register at 0x40020000. The following loop toggles the LED and inserts a software delay.
volatile uint32_t * const GPIO_OUT = (volatile uint32_t *)0x40020000;
int main(void)
{
while (1) {
*GPIO_OUT ^= (1U << 5); /* toggle bit 5 */
for (volatile int i = 0; i < 100000; ++i) {
/* volatile delay loop prevents optimization away */
}
}
}
When compiled with -O2 or higher, the disassembly shows a load, an XOR, and a store for each iteration, followed by a separate loop for the delay. If the volatile qualifier is removed from GPIO_OUT, the optimizer may recognize that the value written to *GPIO_OUT is never read and collapse the entire while(1) block into a no‑op, leaving the LED static.
Trade‑offs and limitations
Applying volatile to ordinary RAM variables that are not subject to asynchronous change defeats useful optimizations, increasing code size and reducing performance without any safety benefit. Therefore, limit volatile to:
- Pointers to peripheral registers.
- Variables modified by interrupt service routines or hardware.
- Variables accessed via DMA or other external agents.
Another pitfall is mismatched width: casting a 16‑bit register to a 32‑bit pointer can cause the compiler to generate 32‑bit accesses, potentially triggering undesired side effects or bus errors. Always match the pointer type to the register’s natural width.
Practical verification steps
- Build the firmware with optimization enabled (
-O2or-O3). - Inspect the disassembly (e.g.,
objdump -d firmware.elf) and confirm that each access to the volatile pointer appears as a distinct load or store. - Run the code on the target hardware or a cycle‑accurate simulator.
- Use an oscilloscope or logic analyzer to observe the LED toggling at the expected rate.
- Repeat the test after removing
volatilefrom the pointer; the toggling should cease or become erratic, confirming the optimizer’s impact. - Optionally run a static analyzer such as
clang‑tidywith checks likebugprone-suspect-memory-argumentto catch missing qualifiers.
Closing
The volatile qualifier is a small annotation with a large effect on the correctness of register access in Embedded C. By declaring hardware pointers as volatile‑qualified objects of the correct width, you guarantee that every read or write reaches the peripheral as intended, while avoiding unnecessary performance penalties elsewhere. Verify the generated assembly and observe the hardware behavior to ensure the qualifier is doing its job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.