In short: the volatile keyword in embedded c tells the compiler that a variable can change outside the normal flow of the program, so every access must go to real memory instead of a cached register. You use it for memory-mapped hardware registers, variables shared with interrupt service routines, and flags touched by multiple execution contexts.
volatile is a type qualifier you place on a variable to warn the compiler that its value may change at any moment, through means the compiler cannot see. When a variable is declared volatile int x;, the compiler must generate a fresh load from memory every time the code reads x, and a real store every time it writes x. It is forbidden from assuming the value stayed the same between two statements.
This matters because modern C compilers are aggressive optimizers. To make code faster, they keep hot variables in CPU registers, reorder independent operations, and delete reads and writes they believe have no observable effect. Those optimizations are perfectly correct for ordinary program variables. They are catastrophic for a variable that represents a hardware pin, a timer count, or a flag set by an interrupt, because the compiler has no way of knowing that something outside the C abstract machine is watching or changing that location.
Consider polling a status register that signals when a peripheral is ready:
while (STATUS_REG == 0) { } — without volatile, the compiler reads STATUS_REG once into a register, sees nothing in the loop changes it, and turns this into an infinite loop that never re-reads the hardware.volatile uint32_t STATUS_REG, the compiler re-fetches the register on every iteration, and the loop exits the instant the hardware flips the bit.The qualifier establishes a contract about observable behavior. The C standard guarantees that accesses to volatile objects happen exactly as written in the source, in program order relative to other volatile accesses, and are never elided. What volatile does not promise is atomicity or memory ordering against non-volatile accesses on multi-core systems — a common and dangerous misconception we address below.
There are three canonical situations in embedded firmware where the volatile keyword in embedded c is mandatory:
#define UART_DR (*(volatile uint32_t *)0x40011004).volatile bool data_ready; — must be volatile, or the optimizer may cache the flag in the main loop and never see the ISR's write.Placement matters with pointers. volatile uint8_t *p means the pointed-to data is volatile; uint8_t * volatile p means the pointer itself is volatile. For a register you nearly always want the former. For structured register maps, engineers write a struct of volatile fields and overlay it on the peripheral base address, giving named, type-safe access while preserving the volatile semantics on every member.
The optimization bugs that volatile fixes are among the hardest to diagnose because they often disappear at low optimization levels and reappear only at -O2 or in a production build. Typical traps:
/ the platform's atomic primitives. On multi-core parts you also need memory barriers — volatile alone gives no ordering against ordinary memory.const volatile: your code never writes it, but the hardware changes it, so it must still be re-read every time.Real projects and industry tool flows, with certification.
Explore CourseTron Courses →Production toolchains treat volatile precisely because safety and correctness depend on it. Compilers such as GCC and Clang (Arm GNU Toolchain, IAR, Keil MDK's Arm Compiler) guarantee that volatile accesses survive every optimization pass; disassembly shows a literal load/store for each access. Static analyzers like MISRA C checkers flag shared variables that are missing volatile, and flag the opposite smell — volatile objects used where atomicity was actually required. Debuggers mark memory-mapped register views as volatile so that simply inspecting a register in a watch window does not get optimized or cached incorrectly. On Arm Cortex-M, firmware pairs volatile with the DMB/DSB barrier intrinsics when ordering against DMA or multiple masters matters.
No. volatile only forces each access to hit memory and preserves the order of volatile accesses relative to one another. It provides no mutual exclusion and no atomicity. For data shared across interrupts or cores you must add a critical section, a lock, or C11 atomics, and often a memory barrier as well.
For a hardware register your program only reads, such as a read-only status or input register. const stops your code from writing it, while volatile still forces a fresh read on every access because the hardware changes the value independently.
At low optimization levels the compiler may already generate real loads and stores, so the bug stays hidden. Raising optimization to -O2, enabling link-time optimization, or changing compilers can expose the missing volatile as a hang or a stale read. Relying on this accidental behavior is unsafe.
Use volatile uint32_t *reg so the data the pointer points at is volatile — that is what you want for a peripheral register. Writing uint32_t * volatile reg instead makes only the pointer variable volatile, which is rarely the intent.
Related: interrupts in embedded systems · the VLSI Glossary · all courses.