Visual walkthrough — volatile keyword — preventing optimization of hardware registers
We only assume you know that a program is a list of instructions and that a computer has some fast scratch slots (registers) and a big slow store (memory / RAM). Everything else we build.
Step 1 — The two places a value can live
WHAT. A number your program uses lives in one of two places: memory (a numbered mailbox, slow to reach) or a CPU register (a tiny slot right inside the processor, blazing fast). Look at the figure: the mailbox [flag] on the right is memory; the box R on the left is a register.
WHY it matters. Reaching into memory costs many CPU cycles. Reaching a register costs almost nothing. So the compiler loves to copy a memory value into a register once and then reuse the register. That copy is the whole story of this page.
PICTURE.

Step 2 — The compiler's core assumption
WHAT. The C compiler reads your function and builds a mental model: "the only things that touch memory are the reads and writes I can see written here." Under that belief, if nothing in view writes flag, then the value in R stays a perfect copy of [flag] forever.
WHY. This assumption is what permits every speed optimization. If the register copy can never go stale, why ever re-load? The figure shows the compiler drawing a green "still valid" seal over R because it sees no write between the load and the use.
PICTURE.

Step 3 — The optimization fires: read-once
WHAT. Take the classic wait loop:
int flag = 0;
while (flag == 0) { }The compiler applies Step 2: nothing in the loop writes flag, so it loads once, before the loop, and spins on the register.
WHY. Re-loading every iteration would be pure waste under the assumption. So the optimizer hoists the load out. The figure shows the load arrow drawn outside the loop box — it happens exactly once.
PICTURE.

The generated shape:
load R, [flag] ; ONE read, before the loop
L: cmp R, 0 ; compare the CACHED copy
jeq L ; equal-to-zero? jump back
cmp R, 0— compare registerRagainst the number 0.jeq L— "jump if equal" back to labelL. BecauseRnever changes inside the loop,jeqalways jumps. This is correct only if the assumption of Step 2 holds.
Step 4 — A hidden writer breaks the assumption
WHAT. Now a real chip enters. An interrupt service routine runs between your instructions and writes memory:
void timer_isr(void) { flag = 1; } // hardware calls thisThe ISR writes [flag] = 1 in memory. But the loop is spinning on R, which is still 0. The truth in the mailbox changed; the copy in the register did not.
WHY it hangs. The equation from Step 2, , is now false — but the compiler baked in the belief that it can never be false, so no re-load was generated. The figure shows the ISR's coral arrow reaching only the mailbox, never the register. The loop is deaf.
PICTURE.

Step 5 — volatile deletes the assumption
WHAT. Qualify the variable:
volatile int flag = 0;
while (flag == 0) { }volatile tells the compiler: "for this variable, the assumption of Step 2 is FORBIDDEN. Treat every read as if a hidden writer might have just changed memory."
WHY this is the exact cure. The bug was one specific false belief — " stays valid". volatile doesn't add locks, doesn't slow the whole program, doesn't touch other variables. It surgically removes the one permission that produced the bug. Now the load is pulled back inside the loop (figure): every comparison is preceded by a fresh trip to memory.
PICTURE.

L: load R, [flag] ; RE-READ every iteration
cmp R, 0
jeq L
Now when the ISR sets [flag] = 1, the very next load sees 1, cmp fails, jeq falls through, and the loop exits. The chain of custody from hardware → memory → register → your code is unbroken.
Step 6 — Edge case: the dead-store / duplicate-read trap
WHAT. A FIFO (first-in-first-out queue) hardware register: reading it consumes one word and advances to the next.
volatile uint32_t *DATA = (volatile uint32_t *)0x40000010;
uint32_t a = *DATA; // pulls word #1
uint32_t b = *DATA; // pulls word #2WHY volatile is essential here. Without volatile, the compiler sees two reads of the same address with no write between them, concludes they must be identical, and rewrites b = a — deleting the second real read. That is data loss: word #2 is never pulled. The figure shows the FIFO conveyor: without volatile the second cup is skipped; with volatile both cups are taken.
PICTURE.

This is the mirror image of Step 3's bug: there the danger was too few reads of a status flag; here it's too few reads of a data port. volatile fixes both by forbidding the "same address, same value" shortcut.
Step 7 — Degenerate case: the read-only register (const volatile)
WHAT. Some hardware registers you must never write (a status register), yet hardware changes them constantly. That is two facts at once:
const volatile uint32_t *STATUS = (const volatile uint32_t *)0x4000C000;- `const` → your code is forbidden to write it (a compile-time promise).
volatile→ hardware changes it, so re-read every time.
WHY they combine, not clash. const restricts the software's writes; volatile describes the hardware's writes. They speak about different actors, so both can be true. The figure splits the register into two arrows: a blocked (coral) arrow from your code, and a live (mint) arrow from the chip.
PICTURE.

Step 8 — The limit of volatile: it is NOT atomicity
WHAT. volatile int count; does not make count++ safe against interrupts.
WHY. count++ is really three separate machine steps — a load, an add, a store:
volatile guarantees each of those three touches real memory — but an interrupt can strike between step 1 and step 3. The figure shows the ISR sneaking in after the load: it increments count, then your store overwrites it, and one increment is lost.
PICTURE.

For true atomic increments use `_Atomic`; for ordering guarantees across several variables use memory barriers. volatile covers one variable's visibility, nothing more.
The one-picture summary

The whole page is one fork in the road. The compiler always wants to cache. volatile is the single flag that decides whether the cached copy is trusted (bug road) or discarded on every access (correct road).
Recall Feynman retelling — the whole walkthrough in plain words
A number lives in a slow mailbox (memory) and the CPU keeps a fast sticky-note copy (a register). The compiler is lazy: it copies the mailbox to the sticky note once and then only reads the sticky note, because it assumes nobody but the visible code touches the mailbox (Step 1–3). But a hardware chip, an interrupt, or another thread can change the mailbox behind the compiler's back (Step 4) — now the sticky note lies, and a wait-loop hangs forever. volatile is one word that tells the compiler "never trust the sticky note for this box — walk to the mailbox every single time" (Step 5). That also stops it from skipping a second read of a FIFO port where each read pulls new data (Step 6). You can even combine it with const: you promise not to write the box, but the hardware still does, so keep re-reading (Step 7). The one thing volatile does not do is make count++ safe — that's three steps and an interrupt can slip between them (Step 8). Visibility, yes; atomicity, no.