5.1.33 · D5C Programming

Question bank — volatile keyword — preventing optimization of hardware registers

1,635 words7 min readBack to topic

This bank hunts the misconceptions volatile invites: confusing it with atomicity, mis-parsing the pointer slot, thinking -O0 is a substitute, and forgetting hardware/ISR/DMA can change memory behind the compiler's back.

volatile means re-read from memory every access

Trap 1 confusing it with atomicity

Trap 2 mis-parsing the pointer slot

Trap 3 thinking minus O zero is a substitute

Trap 4 forgetting external writers exist

count plus plus is still load modify store

volatile int star p vs int star volatile p

accidental and fragile breaks at higher levels

hardware ISR and DMA change memory

Two figures below give you the pictures the text leans on: a memory-access timeline (why the loop hangs) and a pointer-slot map (where the word volatile binds).

Figure — volatile keyword — preventing optimization of hardware registers
Figure — volatile keyword — preventing optimization of hardware registers

True or false — justify

Each item: decide true/false, then give the why. The reveal always contains the reasoning, never a bare verdict.

volatile guarantees that count++ is safe against interrupts.
False. count++ is still load–modify–store; an ISR can fire between the load and the store. volatile only forces each access to hit memory — use _Atomic for atomicity.
A volatile access is treated by the C standard as an observable side effect.
True. That is exactly why the compiler is forbidden to delete or fold it — side effects must be preserved in program order.
Compiling at -O0 makes volatile unnecessary for hardware registers.
False. -O0 only accidentally keeps the re-reads; it is fragile, breaks at -O2, and hides intent. volatile is correct at every optimization level.
const and volatile cannot be combined on one variable.
False. const volatile describes a read-only hardware register: your code never writes it (const) but hardware does (volatile), so it is still re-read each access. See const qualifier.
volatile int *p and int *volatile p mean the same thing.
False. The first makes the pointed-to data volatile (re-read *p); the second makes the pointer variable volatile while the data is ordinary. See Pointers and Type Qualifiers and figure s02.
Marking a shared variable volatile inserts a memory barrier between it and your other variables.
False. volatile orders volatile accesses among themselves but gives no ordering guarantee for ordinary variables — that needs explicit barriers.
Two consecutive reads a = *DATA; b = *DATA; on a volatile FIFO pointer both actually execute.
True. Here DATA is a pointer to a FIFO's data register and *DATA reads one word from it; each volatile read is a side effect the compiler may not merge, so without volatile it would fold b = a and lose a FIFO word.
volatile can slow down a tight loop.
True. Every access becomes a real memory load/store, so the value can no longer live in a fast CPU register — that's the price of correctness.
Declaring a variable volatile changes the value the hardware writes into it.
False. volatile changes nothing about the data itself; it only changes whether the compiler re-reads and preserves accesses.

Spot the error

Each line names buggy intent; the reveal explains the actual defect and the fix.

int *volatile p = (int*)0x40000000; used to poll a status register.
The volatile is on the wrong slot: p is volatile but *p is cached, so polling still hangs. Want volatile int *p so the target is re-read (see figure s02).
volatile int flag; while(flag==0){} — but the ISR does flag = 1; on a non-volatile copy declared elsewhere.
The loop reads the volatile flag correctly, but if the ISR updates a different / non-volatile object they aren't the same memory. Both sides must refer to one shared volatile object.
Using volatile on a shared counter and assuming counter += 1 in an ISR and in main won't race.
+= 1 is read–modify–write; a preemption between steps loses an update. Needs atomic ops or interrupt masking, not volatile.
#define REG *(uint32_t*)0x4000C000 (no volatile) then polling while(!(REG&1)){}.
The cast lacks volatile, so the compiler reads REG once and may loop forever. Use *(volatile uint32_t*)0x4000C000.
Trying to memcpy a large volatile struct expecting each field to be re-read.
memcpy takes non-volatile pointers; the volatile-ness is dropped, so per-element ordering/re-reads aren't guaranteed. Copy field-by-field through volatile accesses.
Writing volatile int x; x = compute(); x = compute(); and expecting the first write dropped as dead.
The compiler must keep both writes because volatile stores are side effects — dead-store elimination is disabled here, which is intentional for a write-only register.

Why questions

Why does the compiler cache a variable in a register by default?
The standard lets it assume only visible code changes memory, so re-reading RAM every iteration would be wasteful — caching is a legal speed optimization until you tell it otherwise.
Why is volatile described as "removing an assumption" rather than "adding a feature"?
It lifts the compiler's assumption that nothing external touches the variable; the machinery to hit memory always existed, volatile just forbids the optimizations that skip it.
Why must a UART status poll mask with & 0x01 rather than compare the whole register?
Other bits in the register are unrelated flags or noise; masking isolates the single "data ready" bit so the loop reacts only to that condition.
Why doesn't turning off optimization document your intent the way volatile does?
-O0 is a global build flag with no connection to a specific variable; volatile states at the declaration that this object changes externally, surviving refactors and higher -O levels.
Why is const volatile sensible for a read-only status register?
const stops your code from writing it (catching accidental stores at compile time) while volatile forces re-reads because hardware mutates it — the two qualifiers describe two different actors.
Why can Memory-mapped IO break without volatile even when the syntax looks identical to normal memory?
A memory-mapped address behaves like RAM to the compiler but reads/writes trigger hardware side effects (advancing FIFOs, clearing flags); only volatile tells the compiler those accesses are meaningful and must not be merged or removed.

Edge cases

What does volatile guarantee about a multi-byte read on a 8-bit bus?
Nothing about atomicity — a 32-bit volatile read may split into several bus transactions, so a value can tear if hardware/ISR updates mid-sequence. volatile only guarantees each access is real, not indivisible.
Is a volatile local variable ever useful?
Rarely — only when its address escapes to something external (e.g. setjmp/longjmp or an ISR holding a pointer to it); a purely local volatile just pessimizes without protecting anything.
What happens to volatile across a function call whose body the compiler can inline?
The volatile accesses inside must still occur in order after inlining — inlining may not reorder or drop them, so the observable memory traffic is preserved.
Does volatile on a pointer target survive when you pass *p (a plain value) into a function?
No — once you read *p into an ordinary parameter, the read already happened and the copy is a normal non-volatile value. The qualifier protects the access, not the resulting data.
Zero-degenerate case: what if a volatile variable is never written by any code at all?
Reads still hit memory every time (correct for a hardware-updated register). The compiler cannot assume its value is the initializer, because "no visible write" no longer implies "unchanged".
Limiting case: at maximum optimization with whole-program analysis, can the compiler ever drop a volatile access?
No. Regardless of how much it can prove, volatile accesses are observable side effects the standard requires it to emit in order — optimization power does not override that rule.

Recall One-line summary of the traps

volatile = "re-read this from memory, never cache/drop/reorder its accesses." It is NOT atomicity, NOT a memory barrier for other variables, NOT indivisibility, and its meaning flips with pointer placement.

The block below is a spaced-repetition section: #flashcards/coding tags these Q ::: A lines so the vault's review plugin can surface them on a schedule — they are the two highest-value facts from this whole page, pulled out for daily drilling.

The single promise of volatile.
Every read and write of that object is a real, order-preserved memory access the compiler may not cache away, delete, or reorder among other volatile accesses.
Three things volatile does NOT give you.
Atomicity, a memory barrier across ordinary variables, and indivisibility of multi-byte accesses.