C Part II · Module 17 · volatile for Hardware Engineers
What volatile Actually Means
The guarantee fits in one sentence, and three of its four clauses are the ones people skip. Accesses to a volatile object are part of the program's observable behaviour — so they are not elided, and not reordered with respect to other volatile accesses, and that is a statement about the abstract machine and about nothing else. This chapter states the rule as the standard states it, with the paragraph numbers, and measures each clause including the two limits built into it: a plain store moving across a volatile one, and the fact that the standard declines to define what an access even is.
Advanced22 min readCPart IIHardwareCorrectness
Part II · Module 17 · Chapter 17.2 · What volatile Actually Means
1. The Sentence
Accessing a
volatileobject is a side effect. Side effects are part of the program's observable behaviour. An implementation must preserve observable behaviour.
Everything volatile does follows from those three lines, and so does everything it does not do.
The first is C17 §5.1.2.3 ¶2, which lists what counts as a side effect: modifying an object, modifying a file, accessing a volatile object, or calling a function that does any of these. The second and third are the as-if rule, which Chapter 13.2 used to explain undefined behaviour from the other direction.
So volatile is not a special mechanism bolted onto the language. It works by adding to the list of things that count as observable, and the ordinary as-if rule then does the rest. That is worth holding onto, because it predicts both the guarantee and its limits without any further rules.
2. The As-If Rule, and Where volatile Enters It
A C implementation is allowed to do anything whatsoever to your program, provided the observable behaviour of the abstract machine is preserved. It may reorder, merge, delete, vectorise, unroll and inline freely — Chapter 17.1 measured four examples — as long as what is observable comes out the same.
Observable behaviour is a short list: I/O, the final state of files, and — once the qualifier is present — volatile accesses.
That is the whole mechanism. Qualify an object volatile and its accesses join the list of things that must survive. The compiler's model from Chapter 17.1 §2 is unchanged; what changes is that eliding a load is now visible to the as-if rule, and therefore forbidden.
Two consequences follow immediately, and both are measured in §3 and §4.
It cannot delete an access, because deleting it changes observable behaviour.
It cannot reorder two volatile accesses, because the abstract machine performs them in a defined order and reordering them changes what is observed.
And one non-consequence, which is the entire subject of Chapter 17.5: everything not on that list is still freely reorderable, including ordinary memory accesses, including ones that sit between two volatile accesses in your source.
3. Clause One: Accesses Are Not Elided
Chapter 17.1 §3 measured this as four transformations withdrawn. Restated as the rule:
(void)g; plain: nothing emitted volatile: the load happens
g = 1u; g = 2u; plain: one store volatile: two stores
return g + g; plain: one load, a shift volatile: two loads, an add
while (g == 0u) { } plain: the loop is gone volatile: it reloadsThe g + g pair is the clearest statement of what the rule actually says. With a plain object the compiler reasons "both reads give the same value" and emits lsl — a doubling. With volatile it may not assume that the second access yields the same value as the first, because each access is an observable event in its own right and the standard says nothing about what a volatile object's value will be. So it emits two loads and an add.
The loop case is the one the standard handles separately, and Chapter 17.1 §4 cited it: C17 §6.8.5 ¶6 permits assuming a side-effect-free loop terminates, and lists "does not access volatile objects" as one of its conditions. The forward-progress rule names the qualifier explicitly.
4. Clause Two: Two Volatile Accesses Keep Their Order — and Nothing Else Does
This is the clause that is routinely remembered without its qualifier, and the qualifier is the important half.
Two volatile accesses stay in order. Measured:
void two_volatile(void) { vreg = 1u; vreg = 2u; }mov w9, #1
str w9, [x8]
mov w9, #2
str w9, [x8]Both present, in source order.
A volatile access does not order anything else. Same compiler, same options:
extern volatile uint32_t vreg;
extern uint32_t plain;
void mixed(void) {
plain = 1u; /* plain store */
vreg = 2u; /* volatile store */
plain = 3u; /* plain store */
}_mixed:
ldr x8, [x8, _vreg@GOTPAGEOFF]
mov w9, #2
str w9, [x8] <- the VOLATILE store, FIRST
ldr x8, [x8, _plain@GOTPAGEOFF]
mov w9, #3
str w9, [x8] <- the surviving plain store, AFTER
retTwo things happened to the plain stores, and neither is prevented by the volatile store sitting between them in the source:
plain = 1uwas deleted entirely. It is a dead store — overwritten byplain = 3u— and the volatile access between them did nothing to protect it.plain = 3umoved. In the source it is aftervreg = 2u; in the output it is still after, butplain = 1u's removal means the net effect is that the only surviving plain store sits on the far side of the volatile one, and the ordering the source expressed betweenplain = 1uandvreg = 2uis simply not there any more.
This is exactly what §2 predicted. The plain stores are not observable behaviour, so the as-if rule does not constrain them, and a volatile access is not a barrier for anything except other volatile accesses.
If you wrote a device sequence as "set up a buffer in memory, then write the GO bit", the buffer writes are the plain stores. Chapter 17.6 is where that gets fixed, and it is fixed by a different mechanism entirely.
5. Clause Three: About the Abstract Machine, and Nothing Else
The last clause of the rule is the one that disappoints people, and it is worth stating bluntly because every misuse of volatile traces back to ignoring it.
volatile constrains the compiler. It says nothing about the processor, the store buffer, the cache, the interconnect, or any other core.
The abstract machine is a model of a single sequential executor. When the standard says accesses are not reordered, it means the implementation may not generate code that reorders them — it does not and cannot say anything about what the hardware does with the instructions once generated. Chapter 17.6 measures that distinction directly.
There is a second limit, in the same section of the standard, that is almost always omitted when volatile is explained. C17 §6.7.3 defines the qualifier and then says:
What constitutes an access to an object that has volatile-qualified type is implementation-defined.
The standard guarantees that accesses are preserved and declines to define what an access is. Whether reading a 32-bit volatile object produces one 32-bit load or two 16-bit loads is Chapter 13.3's middle category — the implementation must choose and document it.
That is not a loophole; it is why Chapter 16.3 §2's rule exists. The width of a register access is settled by the pointer type and the implementation's documented behaviour, not by volatile. The qualifier guarantees the access happens; the type decides what it is.
6. The Misconception: volatile Means the Value Is Fresh
"
volatilemeans the variable is always read fresh from memory, so the value is always current and correct."
Two different errors, and separating them is useful.
"Read fresh from memory" is approximately right and the wrong level of description. What the standard says is that the access is performed. On this machine that is a ldr, and what a ldr returns depends on the cache, the store buffer, and the device — none of which volatile has any authority over. For a memory-mapped register on a typical embedded target the region is configured as device memory and is not cached, so the load does reach the device — but that is a property of the MMU configuration, not of the qualifier. volatile on a cached ordinary object reads whatever the cache holds.
"The value is always current" is simply false, and it is false in a way that matters. Consider:
if ((vreg & READY) != 0u) { /* access 1: it was ready */
send_byte(); /* by now it may not be */
}The access happened and returned a true value at the instant it happened. One instruction later the device may have changed state. volatile guarantees you looked; it does not freeze the thing you looked at, and it cannot.
This is the same shape as Chapter 16.4 §7's observation that a register accessor cannot be pure. A volatile read is a sample, not a subscription.
The corrected formulation:
volatileis a statement about code generation: the access you wrote is the access that happens. It says nothing about what the access returns, when, or whether it is still true afterwards.
7. What You Should Be Able to Say Now
- The mechanism is one line: accessing a
volatileobject is a side effect (C17 §5.1.2.3 ¶2), side effects are observable behaviour, and the as-if rule requires preserving observable behaviour. volatileadds to the list of what counts as observable. It is not a special-purpose mechanism; the ordinary as-if rule does the rest.- Accesses are not elided. Measured: a discarded read is performed, two writes both happen,
g + gbecomes two loads and an add rather than one load and a shift. - The loop rule names it: §6.8.5 ¶6's forward-progress exemption lists "does not access volatile objects".
- Two volatile accesses keep their order — measured, both stores present and in sequence.
- Nothing else is ordered by them. Measured: with a volatile store between two plain ones, one plain store was deleted and the ordering the source expressed was not preserved.
- The guarantee is about the abstract machine only — the compiler. Not the processor, the store buffer, the cache, the interconnect, or another core.
- §6.7.3 declines to define what an access is, calling it implementation-defined — which is why the access width is settled by the pointer type and the implementation, not by the qualifier.
- A
volatileread is a sample, not a subscription. It guarantees you looked; it does not keep the answer true.
8. Exercises
- Write the four plain/volatile pairs from §3 and confirm all eight outputs. For
g + g, state in one sentence why the plain version is entitled to use a shift. - Compile §4's
mixedand confirm the two things that happened to the plain stores. Then add a third plain store before the volatile one with a different variable, and say whether it survives and why. - Find C17 §5.1.2.3 ¶2 (or a draft) and list everything it calls a side effect. How many of them does a typical driver perform?
- Find §6.7.3's sentence about what constitutes an access. Write one sentence explaining why that clause, rather than any defect, is the reason Chapter 16.3 insists you choose the pointer type deliberately.
- Write
if ((vreg & READY) != 0u) send_byte();and describe precisely what thevolatilequalifier guarantees about it, and what it does not. - A colleague says "
volatilemakes it read from memory instead of a register". Give a more accurate one-sentence replacement, and name one case where their version would mislead. - Using only §2's framing —
volatileadds to the observable list — predict whether a compiler may reorder two accesses to different volatile objects. Then check the standard and report whether you were right.
9. Summary
- The mechanism: accessing a
volatileobject is a side effect (§5.1.2.3 ¶2); side effects are observable; the as-if rule preserves observable behaviour. Everything else is a deduction. - Not elided — measured across four transformations, including
g + gbecoming two loads and an add. - §6.8.5 ¶6's forward-progress rule names
volatileas its exemption. - Two volatile accesses keep their order — measured.
- Nothing else does. Measured: a plain store deleted and the source ordering not preserved, with a volatile store sitting between.
- The abstract machine only — the compiler, not the processor, cache, store buffer or interconnect.
- §6.7.3: what constitutes an access is implementation-defined. The qualifier preserves accesses; the type decides what one is.
- A
volatileread is a sample, not a subscription.
The rule is now stated and its two limits are on the table. Applying it to a memory-mapped register is mostly a matter of putting the qualifier in the right place in a declaration — which sounds trivial, and which Chapter 16.3 already measured one plausible spelling getting exactly backwards.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
