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 intmay 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
-O2flag that speeds up desktop software can silently break firmware that doesn’t use volatile correctly
Related Topics
- Bit Manipulation in C – Set, Clear, Toggle, and Test Bits
- extern Keyword in C – Multi-File Programs Explained
- C Aptitude Questions and Answers
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.