In short: const is a compile-time promise to the compiler that your code will not write a variable, while volatile is a runtime warning that a value may change behind the compiler's back — so hardware registers almost always need both.
On a desktop, forgetting a type qualifier costs you a warning. On a microcontroller driving a motor or polling a sensor, the same slip produces a device that works in debug builds and mysteriously hangs at -O2. Few topics separate a hobbyist from an embedded engineer as cleanly as understanding const and volatile — which is why they appear in nearly every firmware interview.
Two qualifiers, two completely different jobs
The confusion starts because both words sound like restrictions. They are not the same kind of restriction. const talks to the compiler's type checker; volatile talks to the compiler's optimizer. One is about who is allowed to write a value in source code; the other is about whether the compiler is allowed to assume a value stays put between two reads.
const— "I promise not to modify this object through this identifier." The compiler enforces the promise and may place truly const data in flash/ROM.volatile— "This object may change at any time, by something you cannot see." The compiler must emit a real load/store for every access and may not cache the value in a register or reorder it.
Mnemonic that survives interviews: const = read-only (the programmer won't write it); volatile = do-not-optimize (the compiler mustn't assume it). They answer different questions, so they compose cleanly.
Why volatile exists: the optimizer is not your friend here
Consider polling a status flag that a peripheral sets when a conversion completes:
uint8_t status = 0;
while (status == 0) { } /* wait for hardware */
Because nothing inside the loop writes status, an optimizing compiler (GCC or Arm Compiler at -O2) reasons: "the value can never change, so read it once." It loads status into a register, tests it, and if zero generates an unconditional infinite branch — while(1). The hardware updates RAM, but the CPU never looks again. The fix is one word:
volatile uint8_t status = 0;
while (status == 0) { } /* now re-read every iteration */
With volatile, the compiler must emit a fresh load on each iteration. This is the single most common cause of "it works at -O0 but breaks at -O2" bug reports.
Three cases that always need volatile: (1) memory-mapped peripheral registers, (2) global variables shared with an ISR, and (3) variables touched by a second thread or core. Miss any one and the optimizer can silently delete the access.
The register case: why const volatile is correct, not a contradiction
It feels paradoxical — "read-only but keeps changing?" — yet it is the exact description of a hardware status register. A UART line-status register changes on its own (the hardware sets the data-ready bit), so it is volatile. But your firmware must never write to it, so you want the type system to forbid writes: const. Combine them:
#define UART_LSR (*(const volatile uint32_t *)0x4000C014u)
if (UART_LSR & 0x01u) { /* read-only, re-read each time */ }
The qualifiers point at the pointed-to object, so const volatile uint32_t * means "a pointer to a uint32 that I may not write and that may change on its own." A write-only control register flips it: a plain volatile uint32_t * (no const), or even write-only semantics you enforce by convention. For a full treatment of how these addresses map to silicon, see our companion piece on memory-mapped I/O.
| Declaration | Programmer may write? | Optimizer may cache? | Typical use |
|---|---|---|---|
uint32_t x | Yes | Yes | Ordinary local/global |
const uint32_t x | No | Yes | Calibration constant in flash |
volatile uint32_t x | Yes | No | ISR-shared flag, control register |
const volatile uint32_t x | No | No | Status / ID register (read-only HW) |
volatile forces the re-poll; const forbids a stray write.Common traps that break real firmware
Pointer placement
const char *p (pointer to const char) and char * const p (const pointer to char) are not the same. Read declarations right-to-left: char * const is "const pointer to char" — you may change the pointed-to byte but not the pointer. For a register you nearly always want the qualifier on the pointee.
volatile is not atomic and not a memory barrier
A frequent interview trap: does volatile make access thread-safe? No. On a 32-bit Cortex-M, a volatile uint64_t still takes two bus transactions, and an interrupt can land between them. volatile only guarantees the access happens and is not reordered by the compiler relative to other volatiles — it says nothing about CPU reordering, caches, or atomicity. For shared counters use or disable interrupts around the critical section.
const does not mean "in ROM"
Whether a const object actually lands in flash depends on storage duration and the linker script, not the keyword alone. A const local is still on the stack. And casting away const to write the object is undefined behavior if the object was genuinely const — even if it "works" today.
How the toolchain sees it
You can watch the difference directly. Compile the polling loop with arm-none-eabi-gcc -O2 -S and inspect the assembly: without volatile you will see a single ldr hoisted out of the loop; with it, the ldr sits inside the loop body. Static analyzers in professional flows — the MISRA C checkers built into Synopsys Coverity, or the rule sets in commercial embedded suites — flag Rule 2.2 (dead code) and the accesses that a missing volatile would otherwise optimize away. Teams building verified SoCs at houses like Avecas, a VLSI design-services firm, treat correct register qualification as a sign-off gate, because a dropped volatile in a driver can mask a silicon bug during bring-up.
Want to move from "it compiles" to "it ships"? Our Embedded C & firmware track on CourseTron takes you from register maps to interrupt-safe drivers with hands-on labs on real Cortex-M boards.
Quick decision checklist
- Value set by hardware or an ISR, read by main code →
volatile. - Hardware status/ID register you must never write →
const volatile. - Calibration table or lookup you want in flash and never modify →
const. - Shared multi-byte or RMW value →
volatileis not enough; add atomics or a critical section.
Mastering this pairs naturally with understanding interrupt service routines, where almost every shared flag is volatile. If you want structured training beyond the fundamentals, ChipXpert's VLSI and embedded courses cover the same ground with mentor support.
FAQ
Can a variable be both const and volatile at the same time?
Yes, and it is the correct type for a read-only hardware register. const stops your code from writing it; volatile stops the compiler from assuming its value is stable. They operate on different compiler stages, so there is no contradiction.
Does volatile make my code thread-safe?
No. volatile guarantees each access is actually performed and not reordered against other volatile accesses by the compiler. It provides no atomicity and no CPU memory barrier. For concurrency use C11 atomics or disable interrupts around the critical region.
Why does my code work at -O0 but hang at -O2?
Almost always a missing volatile. At -O0 the compiler re-reads most variables from memory anyway; at -O2 it caches them in registers and can turn a polling loop into an infinite loop. Add volatile to any variable that hardware or an ISR can change.
Does const guarantee the data is stored in flash/ROM?
No. Placement in flash depends on storage duration and your linker script. const only enforces the read-only promise in the type system; a const local still lives on the stack.