Skip to content
VLSI Mentor

C Part II · Module 17 · volatile for Hardware Engineers

What volatile Does Not Guarantee

Three times across two modules this curriculum has said volatile does not give you that and moved on. This is where the debts are collected, with a measurement for each: an increment on a volatile object compiling to three separate instructions, and a plain store being deleted and another moved across a volatile one that was supposed to be sitting between them. The third — mutual exclusion — gets no measurement because there is nothing to measure, and that is the point. All three reduce to one sentence about what the qualifier is for, and each has a named replacement.

Expert22 min readCPart IIHardwareCorrectness

Part II · Module 17 · Chapter 17.5 · What volatile Does Not Guarantee

1. The Debts

This chapter has been promised five times:

  • 16.3 §7 — "volatile means preserved, not safe", with the three gaps named and deferred
  • 16.5 §4 — the window between a read and a write, and "volatile does not help"
  • 17.2 §4 — the qualifier in "not reordered with respect to other volatile accesses"
  • 17.2 §5 — "a guarantee about the abstract machine only"
  • 17.4 §6 — the limits of volatile sig_atomic_t, stated and not explained

Three distinct non-guarantees, each believed by people who would tell you they understand volatile, and each with a measurement.

Before the three, the contrast. Chapter 17.2 measured what the qualifier does give: a discarded read is performed, two writes both happen, g + g becomes two loads and an add, and two volatile accesses keep their order. Those are real and they are the whole list.

2. Not Atomic

The most common and the most consequential.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern volatile uint32_t v;
void inc(void) { v++; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_inc:
    ldr  w9, [x8]
    add  w9, w9, #1
    str  w9, [x8]
    ret

Three instructions. A load, an arithmetic operation, and a store — which is precisely Chapter 16.5's read-modify-write sequence, with the qualifier applied.

volatile did its job completely: the load happened, the store happened, neither was elided or merged. And the three steps are as separate as they ever were. An interrupt between the ldr and the str reads the old value, increments it, stores it, and is then overwritten by the str — one increment lost, silently.

This is worth being blunt about because the belief is widespread: there is nothing in the definition of volatile about indivisibility. Chapter 17.2 §1 derived the whole guarantee from one fact — accesses are side effects and therefore observable — and indivisibility does not follow from that and was never claimed.

Chapter 17.4 §4 met the same point from the signals side: sig_atomic_t guarantees that a single access is not torn, and says nothing about ++, which is not a single access.

The replacement has a name. For a counter shared with an interrupt handler or another thread, C11 provides _Atomic types and operations such as atomic_fetch_add, which compile to a genuine atomic read-modify-write instruction sequence. That is outside this module's scope — it is a subject in its own right — but the name is what you need, because "use volatile" is the wrong answer and "be careful" is not an answer.

The difference is visible in one line. atomic_fetch_add(&ac, 1u) on this target compiles to:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ldaddal  w9, w8, [x8]

One instruction, an atomic add with acquire-release semantics, against volatile's three. There is no interleaving to worry about because there is no gap.

3. Not Ordered Against Ordinary Accesses

Chapter 17.2 §4 measured this as part of the definition. It belongs here too, because it is the gap that produces the most serious device bugs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern volatile uint32_t vreg;
extern uint32_t plain;

void mixed(void) {
    plain = 1u;      /* plain    */
    vreg  = 2u;      /* volatile */
    plain = 3u;      /* plain    */
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_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
    ret

plain = 1u is gone — a dead store, eliminated despite a volatile access standing between it and the store that overwrote it. And the ordering the source expressed, plain before volatile, is not present in the output.

The reason is exactly what Chapter 17.2 §2 predicted from the as-if rule: plain accesses are not observable behaviour, so nothing constrains them, and a volatile access is a barrier for other volatile accesses and for nothing else.

Why this is the dangerous one. The canonical device sequence is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dma_buffer[0] = header;        /* ordinary memory */
dma_buffer[1] = payload;       /* ordinary memory */
DMA_CTRL = DMA_START;          /* volatile register write */

The buffer writes are plain stores. Nothing in the C source requires them to reach memory before the DMA_START write reaches the device — and if the engine reads the buffer when it sees the start bit, it may read a buffer that was never written. The volatile on DMA_CTRL does nothing for the two lines above it.

Compiled here, the order happened to be preserved — and the compiler still transformed the buffer writes freely:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stp  w0, w1, [x8]        <- the TWO buffer stores, merged into one instruction
ldr  x8, [x8, _DMA_CTRL@GOTPAGEOFF]
mov  w9, #1
str  w9, [x8]            <- the volatile store

Two source statements became one stp. That they also stayed before the volatile store is this compiler's choice on this day, not a guarantee — and the merge is the evidence that it regards them as freely transformable. Relying on the order is relying on something nobody promised, which is Chapter 16.8 §6's observed, not guaranteed.

Chapter 17.6 is the mechanism that fixes this, and it is not a qualifier.

4. Not Mutual Exclusion

This one gets no measurement, and the absence is the argument.

There is no lock in the definition of volatile. Chapter 17.2 §1 derived the entire guarantee from "accesses are side effects"; nothing in that creates a critical section, acquires anything, or excludes anyone. There is nothing to measure because there is nothing there.

What people actually want when they reach for it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (flag != 0) {        /* read   */
    handle();           /* act    */
    flag = 0;           /* clear  */
}

Three operations on a shared object with a gap between each. If the handler sets flag between the handle() and the flag = 0, the new event is cleared without being handled — which is Chapter 16.5 §2's measured W1C failure in software clothing, and the reason that chapter insisted "unsafe" and "wrong" are different problems.

volatile guarantees that the read reads and the write writes. It has no opinion about the gap.

No race trace is fabricated here, for the reason Chapter 16.5 §4 gave: single-threaded C on this machine cannot produce an interrupt arriving between two instructions, and inventing a transcript would be worth nothing. The argument is from the definition, and the definition is short enough to check.

The replacements have names: disabling interrupts around the sequence (for the handler case on one core), a real lock, or restructuring so only one agent writes the object — Chapter 13.11's ownership habit, which costs nothing at runtime and is the one people skip.

5. The Whole Picture

Six properties, which of them volatile provides, and the measured evidence for each. The access happens at all, rather than being deleted: volatile provides this, measured as a discarded read surviving where the plain version emitted no load. The access happens at the width you wrote: provided, because the access is through a typed pointer. Two volatile accesses stay in the order written: provided, measured as both stores present in source order. Atomicity: NOT provided, measured as v plus plus compiling to a load, an add and a store, three separate operations. Mutual exclusion: NOT provided, because there is no lock anywhere in the definition. Ordering against ordinary non-volatile accesses: NOT provided, measured as a plain store being deleted and another moved across a volatile store.The propertyDoes volatile give it?Measured evidenceThe access happens atallyesdiscarded read survivesplain emitted no loadAt the width you wroteyesthe pointer typedecides16.3 measured strb, strh, strTwo volatile accessesstay orderedyesboth stores, in order1 then 2AtomicityNOv++ is three operationsldr · add · strMutual exclusionNOthere is no lock in itnothing to acquireOrdering vs ordinaryaccessesNOa plain store movedacross itand another was deleted12
Figure 1 — six properties, whether volatile provides each, and the evidence. The split is not arbitrary: everything in the top half is about whether an access happens and in what form, and everything in the bottom half is about what happens between two accesses or inside one. That is the dividing line, and it is the sentence section 6 builds to. Note the second row in particular — the width of an access is guaranteed, but by the pointer type rather than by the qualifier, because C17 6.7.3 explicitly declines to define what an access is and leaves it implementation-defined.

6. One Sentence

The three non-guarantees look unrelated and are not. Read Figure 1's two halves:

The top half is about whether an access happens, and in what form. Is it emitted? At what width? In what order relative to other volatile accesses?

The bottom half is about what happens between two accesses, or inside one. Can something interleave with my increment? Can something run between my read and my write? Can ordinary work be moved across my register access?

That is the whole distinction:

volatile is about whether an access happens. It is not about what happens between two accesses.

Every one of the three non-guarantees is a question about between. Atomicity asks what can happen between the load and the store of a read-modify-write. Mutual exclusion asks what can happen between a read and a later write. Ordering asks whether other work can move across the boundary.

The qualifier has nothing to say about any of them, not because of an oversight but because Chapter 17.2 §1 derived its entire meaning from a rule about side effects being preserved — and preservation is a statement about each access individually.

7. The Misconception: volatile Is the Embedded Programmer's Thread Safety

"In embedded work you use volatile for anything shared — registers, flags, buffers. That is what it is for."

Half right, and the half that is wrong accounts for a large share of intermittent embedded faults.

It is for registers (Modules 16 and 17.3) and for a single flag written asynchronously (17.4 §5). Both are cases where the problem is the access not happening, which is the problem it solves.

It is not a sharing mechanism, and the replacements are specific:

What you wantWhat volatile givesWhat to use
a counter incremented from two contextsthree separate instructions_Atomic and atomic_fetch_add
read-act-clear without interferenceno protection at alldisable interrupts, a lock, or one owner
memory writes visible before a register writenothinga barrier — Chapter 17.6

The reason the folklore persists is that volatile is necessary in all three cases — remove it and you get the deleted-access bugs of Chapter 17.1 on top of everything else — so adding it genuinely does fix some bugs. It is necessary and not sufficient, and "it fixed the last one" is Chapter 13.2's reasoning that this curriculum has spent two parts dismantling.

The accurate position:

Use volatile for every memory-mapped access and for an asynchronously written flag. When two agents interact with an object, volatile is a prerequisite and never the mechanism.

8. What You Should Be Able to Say Now

  1. The three non-guarantees are atomicity, mutual exclusion, and ordering against ordinary accesses — deferred here five times across Modules 16 and 17.
  2. Not atomic. Measured: v++ on a volatile uint32_t is ldr, add, str — three instructions, exactly 16.5's read-modify-write. The replacement is _Atomic / atomic_fetch_add.
  3. Indivisibility was never claimed. 17.2 §1 derived the guarantee from accesses being side effects; nothing about that implies a single instruction.
  4. Not ordered against ordinary accesses. Measured: with a volatile store between two plain ones, one plain store was deleted and the source's ordering was not preserved.
  5. The dangerous shape is the DMA sequence — buffer writes then a start-bit write. The buffer writes are plain stores and nothing orders them.
  6. Not mutual exclusion. No measurement, because there is no lock in the definition. The read-act-clear sequence has gaps volatile has no opinion about.
  7. No race trace is fabricated here or in 16.5 §4 — this machine cannot produce one, and an invented transcript is worth nothing.
  8. The dividing line: volatile is about whether an access happens, not about what happens between two accesses. All three non-guarantees are questions about between.
  9. Necessary, never sufficient. It is a prerequisite wherever two agents share an object, and never the mechanism.

9. Exercises

  1. Compile v++ on a volatile uint32_t at -O2 and confirm the three instructions. Then write out the interleaving that loses an increment.
  2. Reproduce §3's mixed and confirm both effects. Add a fourth statement writing a different plain variable before the volatile store and report whether it survives and where it lands.
  3. Write the DMA sequence from §3 and identify, line by line, which statements the compiler is free to move and which it is not.
  4. For §4's read-act-clear, write out the interleaving that loses an event. Say which chapter measured the hardware version of the same failure.
  5. Fill in Figure 1's three columns for a volatile sig_atomic_t flag written by a signal handler. Which rows change from Chapter 17.4's pattern, and which do not?
  6. For each row of §7's table, write the one-sentence justification for the replacement named.
  7. A colleague's fix for an intermittent fault was to add volatile to a shared counter, and the fault became rarer. Explain, using this chapter and Chapter 13.2, why "rarer" is the expected outcome and why it is not evidence of a fix.

10. Summary

  • Three non-guarantees, deferred five times and collected here: atomicity, mutual exclusion, ordering against ordinary accesses.
  • Not atomic — measured: v++ is ldr · add · str. Use _Atomic / atomic_fetch_add.
  • Not ordered against plain accesses — measured: a plain store deleted and the ordering lost across a volatile store. This is the DMA-buffer bug.
  • Not mutual exclusion — no lock exists in the definition; nothing to measure. Use interrupt disable, a lock, or one owner.
  • No fabricated race traces (same discipline as 16.5 §4).
  • The line: volatile is about whether an access happens, not what happens between accesses. All three gaps are between.
  • Necessary, never sufficient — and "adding it made the fault rarer" is 13.2's reasoning, not evidence.

The third gap is the only one of the three with a mechanism inside this module's scope, and it has a complication the other two do not: there are two separate things that can reorder your accesses — the compiler, and the machine — and they need different mechanisms, one of which emits no instruction at all.

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.