volatile Keyword in C – Why Embedded C Needs It

The volatile keyword in C tells the compiler that a variable’s value can change at any time — from outside the normal flow of the program — and therefore every read and write must go directly to memory, never to a cached register copy. Without it, the C compiler is free to optimise away reads it thinks are redundant, producing code that ignores changes made by hardware, interrupts, or other threads.

The Problem volatile Solves

Consider a hardware status register that becomes non-zero when a device is ready. Without volatile:

/* Without volatile — WRONG for hardware registers */
unsigned int *status = (unsigned int *)0x40000000;
while (*status == 0)   /* wait for device ready */
    ;

The compiler sees that *status is never written inside the loop. It is allowed to load *status once into a CPU register and spin forever comparing the register copy to zero — the actual memory location is never re-read. This is a valid optimisation for ordinary variables but a fatal bug for hardware registers.

With volatile, the compiler must re-read *status from memory on every loop iteration:

/* With volatile — correct for hardware registers */
volatile unsigned int *status = (volatile unsigned int *)0x40000000;
while (*status == 0)   /* polls the actual register every iteration */
    ;

The Three Legitimate Uses of volatile

1. Memory-Mapped Hardware Registers

Hardware registers are mapped to specific memory addresses. Reading them can have side effects (e.g., reading a UART RX register clears the buffer). Writing them sends commands to hardware. The compiler must not reorder, cache, or eliminate these accesses.

/* Typical embedded pattern */
#define UART_DR   (*(volatile unsigned int *)0x40011004)
#define UART_SR   (*(volatile unsigned int *)0x40011000)
#define SR_TXNE   (1u << 7)   /* TX register not empty */

void uart_send(unsigned char ch)
{
    while (!(UART_SR & SR_TXNE))  /* wait for TX buffer ready */
        ;
    UART_DR = ch;                 /* write triggers transmission */
}

2. Variables Modified by Interrupt Service Routines (ISRs)

An ISR runs asynchronously — it can modify a variable at any point between two instructions in the main loop. Without volatile, the compiler may read the variable into a register once and never check memory again.

/* Shared between main loop and ISR */
volatile int timer_tick = 0;

/* Called by hardware timer interrupt — conceptual */
void TIMER_IRQHandler(void)
{
    timer_tick++;   /* volatile: main loop sees every increment */
}

int main(void)
{
    while (1) {
        if (timer_tick >= 100) {
            timer_tick = 0;
            /* do periodic work */
        }
    }
    return 0;
}

3. Variables Modified by signal() Handlers

POSIX signal() handlers behave like ISRs — they can run asynchronously between any two instructions. The C standard explicitly requires volatile sig_atomic_t for variables shared with a signal handler.

#include <signal.h>
#include <stdio.h>

volatile sig_atomic_t caught = 0;

void handler(int sig)
{
    (void)sig;
    caught = 1;   /* must be volatile sig_atomic_t */
}

int main(void)
{
    signal(SIGINT, handler);
    while (!caught)
        ;   /* spin until Ctrl-C */
    printf("Signal caught.\n");
    return 0;
}

How to Compile and Run the Signal Example

gcc -ansi -Wall -Wextra -o signal_demo signal_demo.c
./signal_demo
# Press Ctrl+C to send SIGINT

volatile + const Together

A read-only hardware register — one you read but cannot write — is declared const volatile:

/* Read-only hardware ID register */
const volatile unsigned int *chip_id = (const volatile unsigned int *)0x40020000;

unsigned int id = *chip_id;   /* allowed: read */
*chip_id = 0xDEAD;            /* ERROR: const prevents write */

const is a promise from the programmer that the code won’t write to the variable. volatile is a promise that the compiler won’t cache the read. Both can be true simultaneously.

Pointer Qualifier Combinations

Declaration Pointer is … Pointed-to value is … Use case
volatile int *p Normal pointer volatile (re-read each access) Hardware register pointer
int * volatile p volatile (pointer itself may change) Normal int Pointer updated by ISR
const volatile int *p Normal pointer Read-only, volatile Read-only hardware register
volatile int * const p const (pointer fixed) volatile Fixed-address register pointer

What volatile Does NOT Do

  • volatile is not atomic. A 32-bit write to a volatile int may be split into two 16-bit bus transactions on some architectures. For atomic access, use _Atomic (C11) or platform-specific intrinsics.
  • volatile is not a memory barrier. The compiler still cannot cache the volatile variable, but the CPU may reorder loads and stores relative to other memory operations. Use explicit barriers (__sync_synchronize(), memory fence intrinsics) when ordering between volatile and non-volatile accesses matters.
  • volatile is not a replacement for mutexes. For multi-threaded programs where multiple threads share a variable, volatile alone is insufficient — use _Atomic, pthread_mutex_t, or OS synchronisation primitives.
  • volatile does not prevent all optimisations. The compiler can still reorder volatile accesses relative to non-volatile ones, and it can still inline, unroll, or pipeline surrounding code.

What volatile Actually Generates

To see the effect concretely, compare the generated assembly with and without volatile at -O2:

Without volatile (compiler eliminates the loop entirely):
  ; Compiler sees loop body has no side effects
  ; Generated code: nothing — the while loop is deleted

With volatile (compiler re-reads on every iteration):
.loop:
  ldr  r1, [r0]        ; load *status from memory
  cbz  r1, .loop       ; if zero, loop back

When You Don’t Need volatile

  • Local variables that are never accessed by ISRs, signals, or hardware
  • Function parameters (they are on the stack, not shared)
  • Variables only accessed in a single thread with no ISRs
  • Optimization barriers — use asm volatile("" ::: "memory") instead for compiler fence without changing variable semantics

What This Teaches

  • The compiler’s optimisation model: the C abstract machine assumes only the running thread modifies a variable — volatile is the escape hatch for when that assumption is false
  • The const + volatile pattern: hardware registers that should never be written from software are the canonical use case for const volatile
  • Why embedded C is different: the same -O2 flag that speeds up desktop software can silently break firmware that doesn’t use volatile correctly

Related Topics


As an Amazon Associate we earn from qualifying purchases.

Recommended Book

The C Programming Language by Kernighan & Ritchie is the canonical C reference — it does not cover volatile (added in ANSI C89 after the first edition) but it covers every other aspect of the language. For embedded C specifically, the best companion is Programming Embedded Systems in C and C++. K&R also on Amazon.com.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>