Embedded

The volatile keyword in embedded C

By CourseTron Team ·  8 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    🖨  Download / Print this guide (PDF)

    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.

    What Is the volatile keyword in embedded c?

    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.

    Why It Matters and How It Works

    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.
    • Declared as 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.

    Key fact: volatile controls how the compiler generates loads and stores; it does not make an operation atomic, does not insert CPU memory barriers, and does not create a critical section. On a 32-bit MCU, reading a volatile 64-bit counter can still be torn by an interrupt that fires between the two halves.
    Polling a status register: volatile re-fetches each passwhile (STATUS_REG == 0)loop body is emptyeach pass: fresh loadexits when bit flipsSTATUS_REGvolatile uint32_thardware sets the bitvalue changes any timeloadvalueWithout volatile: one read, then an infinite loopCourseTron · coursetron.com
    With volatile, every iteration issues a real load from STATUS_REG (the moving dot), so the loop exits the instant the hardware flips the bit.

    Where You Actually Use It

    There are three canonical situations in embedded firmware where the volatile keyword in embedded c is mandatory:

    1. Memory-mapped peripheral registers. Hardware registers live at fixed addresses. A UART data register or GPIO input register changes because of the physical world, not because of your code. These are almost always declared through a pointer such as #define UART_DR (*(volatile uint32_t *)0x40011004).
    2. Variables modified by an interrupt service routine (ISR). A flag the main loop polls and an ISR sets — 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.
    3. Variables shared with a different hardware context, such as a DMA engine that writes a buffer the CPU then reads. The pointer to the buffer, or the completion flag, needs volatile so the CPU does not use stale cached values.
    Three contexts that change a variable behind the compilerPeripheral registersfixed address, e.g. UART_DRISR-modified flagvolatile bool data_readyDMA buffer / flagwrites while CPU readsvolatile objectcompiler must emita real load / storeCourseTron · coursetron.com
    Memory-mapped registers, ISR-modified variables and DMA-shared data all need volatile so the CPU never relies on a stale cached copy.

    How It Looks in Practice

    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.

    Non-volatile vs volatile accessC codereads xCPU register (cached)stale value reusedMemory-mapped registerfresh hardware valueHardware / ISRno volatilevolatileCourseTron · coursetron.com
    A normal read may reuse a stale copy held in a CPU register; a volatile read goes to the memory-mapped register to fetch the value the hardware or ISR last wrote.

    Common Pitfalls and How to Fix Them

    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:

    • Omitting volatile on an ISR flag. The code works in debug, hangs in release. Add volatile to the shared variable.
    • Over-using volatile. Marking everything volatile cripples the optimizer and bloats code. Qualify only the specific objects that truly change outside the compiler's view.
    • Assuming volatile means atomic. For multi-byte shared data, pair volatile with a critical section (disable interrupts) or use / the platform's atomic primitives. On multi-core parts you also need memory barriers — volatile alone gives no ordering against ordinary memory.
    • Const volatile confusion. A read-only status register is legitimately const volatile: your code never writes it, but the hardware changes it, so it must still be re-read every time.
    volatile is not atomic: a torn 64-bit read1. read low half32-bit loadInterrupt firescounter changes2. read high half32-bit loadResult: halves from different momentsFix: critical section or atomicsCourseTron · coursetron.com
    Each half is a real load thanks to volatile, but the pair is not atomic; an interrupt between them tears the value, so a critical section or atomic primitive is needed.
    Learn this hands-on

    Real projects and industry tool flows, with certification.

    Explore CourseTron Courses →

    How Industry Tools Handle It

    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.

    volatile keyword in embedded c — Interview FAQ

    Does volatile make a variable thread-safe or atomic?

    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.

    When is const volatile meaningful?

    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.

    Why does removing volatile sometimes still appear to work?

    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.

    Where exactly do you place volatile on a pointer to a register?

    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.

    Advertisement