Quick Cap
The volatile and const type qualifiers tell the compiler how a variable may (or may not) change, directly controlling which optimizations are safe. volatile says "this value can change behind your back -- always re-read it from memory," while const says "this value will never be written by the program -- feel free to optimize aggressively." Combining them into volatile const -- a read-only variable that hardware can still change -- is one of the most popular interview questions in embedded C.
Interviewers use these qualifiers to test whether you understand the boundary between your code and the hardware it controls.
Key Facts:
volatileprevents three optimizations: caching a variable in a register, reordering reads/writes, and eliminating "redundant" reads. It does NOT provide atomicity.- Three mandatory uses of
volatile: hardware registers (memory-mapped I/O), variables shared with ISRs, and memory modified by DMA. constenables flash placement: On MCUs,constglobals typically go to.rodatain flash, consuming zero RAM.- Four pointer-const combinations: pointer to data, pointer to const data, const pointer to data, const pointer to const data -- each has a distinct embedded use case.
volatile const: The variable cannot be written by the program but can change due to hardware. Classic example: a status register.- MISRA C Rule 2.7 / 8.13: Require
conston parameters and pointers that are not modified -- enforcing const correctness by policy.
Deep Dive
At a Glance
| Concept | Detail |
|---|---|
volatile effect | Forces every access to go to memory; prevents caching, reordering, and dead-store elimination |
const effect | Prevents program from writing the variable; enables aggressive optimization and flash placement |
volatile const | Read-only from code's perspective, but hardware can change the value at any time |
| Atomicity | volatile does NOT guarantee atomic access -- a 32-bit read on an 8-bit MCU is still multiple instructions |
| Pointer combos | const int *p (data is const), int * const p (pointer is const), const int * const p (both const) |
| MISRA C | Rules 2.7, 8.13 require const qualification; Rule 2.2 flags unused volatile reads |
What volatile Actually Prevents
Without volatile, the compiler is free to optimize memory accesses. Here are the three specific optimizations volatile disables:
1. Register caching -- The compiler may read a variable once, keep the value in a CPU register, and never re-read from memory:
/* Without volatile: compiler may read flag once and loop forever */uint8_t flag = 0; /* set by ISR */while (flag == 0) { } /* compiler: "flag is 0, it never changes in this loop" *//* With volatile: compiler re-reads flag from RAM every iteration */volatile uint8_t flag = 0; /* set by ISR */while (flag == 0) { } /* compiler: "flag is volatile, I must re-read it" */
2. Read/write reordering -- The compiler (and CPU on some architectures) may reorder memory accesses for efficiency. volatile accesses are never reordered relative to other volatile accesses.
3. Dead-store elimination -- If the compiler sees two consecutive writes to the same variable with no read in between, it may eliminate the first write. For a hardware register where each write triggers a side effect, this is catastrophic.
On an 8-bit or 16-bit MCU, a volatile uint32_t still requires multiple load/store instructions. An ISR can fire between them, reading a half-updated value. For shared variables wider than the bus width, you must disable interrupts during access or use an atomic mechanism.
Hardware Register Access
Memory-mapped I/O is the primary reason volatile exists in embedded C. Every peripheral register is a fixed address in the memory map, and reading or writing that address triggers hardware behavior. The compiler must never optimize away, cache, or reorder these accesses.
/* Typical register pointer: volatile pointer-to-volatile data, const address */#define GPIOA_ODR (*(volatile uint32_t *)0x40020014) /* output data reg */#define GPIOA_IDR (*(volatile uint32_t *)0x40020010) /* input data reg */void toggle_led(uint8_t pin) {GPIOA_ODR ^= (1U << pin); /* read-modify-write: must hit hardware */}uint8_t read_button(uint8_t pin) {return (GPIOA_IDR >> pin) & 1U; /* must read hardware, not a cached copy */}
A struct overlay is the cleaner pattern used by vendor HALs. Every field is individually volatile:
typedef struct {volatile uint32_t MODER; /* mode register */volatile uint32_t OTYPER; /* output type register */volatile uint32_t OSPEEDR; /* output speed register */volatile uint32_t PUPDR; /* pull-up/pull-down */volatile uint32_t IDR; /* input data register */volatile uint32_t ODR; /* output data register */} GPIO_TypeDef;#define GPIOA ((GPIO_TypeDef *)0x40020000)
ISR-Shared Variables
Any variable written inside an ISR and read in the main loop (or vice versa) must be volatile. Without it, the compiler has no reason to believe the variable changes outside the visible control flow of main():
volatile uint8_t rx_complete = 0; /* set by UART ISR */void UART_IRQHandler(void) {/* ... read data from DR ... */rx_complete = 1; /* signal main loop */}int main(void) {while (1) {if (rx_complete) { /* compiler must re-read every iteration */rx_complete = 0;process_rx_data();}}}
Volatile Pointer vs Pointer to Volatile
These two are different and both appear in embedded code:
volatile uint32_t *p; /* pointer to volatile data (the DATA is volatile) *//* p itself can be cached in a register */uint32_t * volatile p; /* volatile pointer to non-volatile data (the PTR is volatile) *//* rarely useful -- the pointer itself changes externally */volatile uint32_t * const p = (volatile uint32_t *)0x40020014;/* const pointer to volatile data -- most common for register pointers: *//* the address never changes, but the data at that address can change any time */
The last form -- volatile uint32_t * const p -- is the canonical way to declare a hardware register pointer: the address is fixed at compile time (const), but the register contents are volatile.
const in Embedded Systems
const does more than prevent accidental writes. On MCUs with separate flash and RAM, const globals are placed in .rodata (flash), saving precious RAM:
/* .rodata (flash) -- zero RAM cost, survives power cycles */const uint16_t sin_lut[256] = { 0, 402, 804, /* ... */ };/* .data (RAM, copied from flash at boot) -- costs RAM + boot time */uint16_t mutable_lut[256] = { 0, 402, 804, /* ... */ };
On a Cortex-M with 16 KB RAM, putting a 512-byte lookup table in .rodata instead of .data saves 512 bytes of RAM and eliminates the flash-to-RAM copy at boot.
const in function parameters documents intent and lets the compiler catch mistakes:
/* Caller knows this function will not modify the buffer */void transmit(const uint8_t *data, size_t len);/* Without const, caller cannot be sure their buffer is safe */void transmit(uint8_t *data, size_t len);
The Four Pointer-Const Combinations
This is a guaranteed interview question. Read the declaration right-to-left:
| Declaration | Read right-to-left | What is const? | Embedded use case |
|---|---|---|---|
int *p | "p is a pointer to int" | Nothing | General mutable pointer |
const int *p | "p is a pointer to const int" | The data | Read-only access to a buffer or LUT |
int * const p | "p is a const pointer to int" | The pointer | Fixed-address hardware register (writable) |
const int * const p | "p is a const pointer to const int" | Both | Fixed-address read-only register |
To decode any C declaration with const and volatile, read from the variable name to the left. volatile uint32_t * const reg reads as: "reg is a const pointer to volatile uint32_t." This trick works for every combination and is a great interview move.
volatile const -- The Interview Favorite
A volatile const variable cannot be written by the program (const) but can change at any time due to hardware (volatile). The classic example is a hardware status register:
/* Status register: hardware updates it, firmware only reads it */volatile const uint32_t * const STATUS_REG =(volatile const uint32_t *)0x40020008;uint32_t get_status(void) {return *STATUS_REG; /* always reads hardware, never cached */}/* *STATUS_REG = 0; */ /* compile error -- const prevents writes */
Without volatile, the compiler could read the status register once and reuse the value. Without const, a programmer could accidentally write to a read-only register, potentially causing undefined hardware behavior. Together, they express the exact contract: "hardware owns this value; firmware only observes it."
Another real-world case: a volatile const variable mapped to an external sensor's output register, where the sensor updates the value continuously but the MCU must never write to that address.
Type Qualifiers and MISRA C
MISRA C (widely mandated in automotive, medical, and aerospace) enforces strict use of type qualifiers:
| MISRA C Rule | Requirement |
|---|---|
| Rule 8.13 | A pointer parameter should be const if the function does not modify the pointed-to data |
| Rule 2.7 | Unused function parameters must not silently disappear -- mark them (void)param |
| Rule 2.2 | No dead code -- a volatile read whose result is unused is still NOT dead code (it triggers a side effect) |
| Rule 11.1 | Conversions between pointer-to-function and pointer-to-object require care with qualifiers |
In MISRA-compliant codebases, every pointer that does not modify its target must be declared const. This is enforced by static analysis tools (Polyspace, PC-lint, LDRA). Failing to qualify a read-only pointer is a MISRA violation, even if the code is functionally correct.
Debugging Story: The Sensor That Only Worked in Debug Mode
A team was bringing up an I2C temperature sensor on a Cortex-M4. In debug builds (-O0), the sensor returned correct readings. In release builds (-O2), it always returned the same stale value -- the very first reading from power-on.
The root cause: the sensor's data register was accessed through a plain pointer, not a volatile one. At -O0, the compiler performs no optimization, so every read went to hardware. At -O2, the compiler saw that the pointer target never changed within the visible control flow, cached the first read in a register, and reused it forever.
/* Bug: missing volatile -- works at -O0, fails at -O2 */uint16_t *sensor_data = (uint16_t *)0x40005428;uint16_t temp = *sensor_data; /* compiler caches this read *//* Fix: volatile forces re-read every time */volatile uint16_t *sensor_data = (volatile uint16_t *)0x40005428;uint16_t temp = *sensor_data; /* always hits hardware */
The debugging took three days because the team initially suspected I2C timing, pull-up resistor values, and sensor power sequencing -- all the usual suspects. The lesson: any time a peripheral "works in debug but not release," the first thing to check is missing volatile. This is such a common pattern that experienced embedded engineers check it before anything else.
What interviewers want to hear: You understand that volatile prevents three specific compiler optimizations (register caching, reordering, dead-store elimination) and is mandatory for hardware registers, ISR-shared variables, and DMA buffers -- but does NOT provide atomicity. You can decode all four pointer-const combinations using the right-to-left rule. You know that volatile const means "hardware can change it, but the program cannot write it" and can name the canonical example (a status register). You understand that const enables flash placement on MCUs, saving RAM. You've encountered the "works in debug, fails in release" bug pattern and know it's almost always a missing volatile.
Interview Focus
Classic volatile & const Interview Questions
Q1: "What does the volatile keyword do, and when must you use it?"
Model Answer Starter: "volatile tells the compiler that a variable's value can change outside the visible control flow -- by hardware, an ISR, or DMA. It prevents three specific optimizations: caching the value in a CPU register, reordering reads and writes, and eliminating 'redundant' reads. I use it in three mandatory situations: memory-mapped hardware registers, variables shared between ISRs and the main loop, and buffers modified by DMA. Critically, volatile does NOT guarantee atomicity -- on an 8-bit MCU, a volatile uint32_t still takes multiple instructions to read."
Q2: "Walk me through the four combinations of const and pointers."
Model Answer Starter: "I read declarations right-to-left from the variable name. const int *p -- p points to const data, so I can change p but not *p. int * const p -- p itself is const, so I cannot reassign p but can modify *p. const int * const p -- both are const. And int *p is the baseline with nothing const. In embedded code, the most common pattern is volatile uint32_t * const reg -- a const pointer (the register address never changes) to volatile data (the hardware register content changes)."
Q3: "What is volatile const and why would you use it?"
Model Answer Starter: "volatile const describes a variable that the program is not allowed to write (const) but whose value can change at any time due to hardware (volatile). The classic example is a read-only status register: hardware updates it continuously, but firmware should only read it. Without volatile, the compiler might cache the read; without const, a programmer might accidentally write to it, causing undefined hardware behavior. I've seen this combined as volatile const uint32_t * const STATUS_REG -- a const pointer to volatile const data."
Q4: "A sensor read works in debug mode but returns stale data in release mode. What is wrong?"
Model Answer Starter: "This is almost certainly a missing volatile on the sensor register pointer. At -O0 (debug), the compiler doesn't optimize, so every read hits the actual hardware address. At -O2 (release), the compiler sees that the pointed-to memory is never written within the visible code, caches the first read in a CPU register, and reuses that stale value. The fix is to declare the pointer as volatile. This is such a common embedded bug pattern that it's the first thing I check when anything behaves differently between debug and release builds."
Q5: "Why does const matter for embedded systems beyond preventing accidental writes?"
Model Answer Starter: "const controls where data is physically stored. On an MCU with separate flash and RAM, const globals are placed in .rodata in flash, consuming zero RAM. Without const, the same data goes into .data, which costs both flash (for the initial values) and RAM (for the runtime copy), plus boot time to copy it. On a device with 16 KB of RAM, putting lookup tables and configuration data in const can save kilobytes. MISRA C also requires const qualification on all read-only pointer parameters -- it's enforced by static analysis, not just good practice."
Trap Alerts
- Don't say: "
volatilemakes a variable atomic" -- it only prevents compiler optimizations. Avolatile uint32_ton an 8-bit MCU still requires multiple load instructions, and an ISR can fire between them. - Don't forget: The difference between
volatile uint32_t *p(pointer to volatile data) anduint32_t * volatile p(volatile pointer to data). Interviewers test whether you know which one protects the data and which one protects the pointer. - Don't ignore:
constfor flash placement. Saying "const just prevents writes" misses the critical embedded benefit -- it moves data from RAM to flash, directly reducing RAM consumption.
Follow-up Questions
- "If
volatileprevents reordering of volatile accesses, does it also act as a memory barrier?" - "Can you cast away
constwith(int *)-- and is that undefined behavior?" - "How do you safely read a
volatile uint32_tshared with an ISR on a 16-bit MCU?" - "Why do CMSIS-style register definitions use struct overlays with every field marked
volatile?"
Practice
❓ What three optimizations does the volatile keyword prevent?
❓ What does `volatile const uint32_t *reg` mean?
❓ A peripheral register read works at -O0 but returns stale data at -O2. What is the most likely cause?
❓ Where does a `const uint16_t lookup[256]` at file scope typically reside on a Cortex-M MCU?
❓ Does volatile guarantee atomic access to a shared variable?
Real-World Tie-In
Automotive ECU Register Access -- A powertrain ECU accesses over 200 peripheral registers (ADC, CAN, timers, GPIO) through volatile struct overlays generated from the vendor's SVD file. Every register field is volatile; configuration constants (calibration maps, PID gains) are const to live in flash. MISRA C static analysis enforces const on every pointer parameter that does not modify its target, catching qualifier violations at CI time.
IoT Sensor Node Power Bug -- A battery-powered sensor node's sleep-wake cycle stopped working after a firmware update. The power management IC's status register was read through a non-volatile pointer. The compiler cached the "awake" status, so the firmware never saw the "sleep-ready" flag. Adding volatile to the register pointer and const to the configuration tables (moving them from RAM to flash) fixed the wake logic and reduced RAM usage by 1.2 KB.
Medical Device Certification -- During IEC 62304 review, a static analysis tool flagged 47 MISRA Rule 8.13 violations: pointer parameters in driver functions that never modified the pointed-to data but were not declared const. Fixing these violations was required for certification, and it also caught two latent bugs where a function was accidentally writing through a pointer that should have been read-only.