Table of Contents
KEY TAKEAWAYS
- The
volatilekeyword tells the compiler that a variable can change unexpectedly, preventing optimization - Use
volatilefor hardware registers, shared variables in ISRs, and memory-mapped I/O volatiledoes not guarantee atomicity — you still need mutexes or critical sections for thread safety- Without
volatile, the compiler may cache register values and miss external changes
The volatile keyword in C prevents compiler optimization that can break embedded code when variables change unexpectedly through interrupts or hardware.
The proper use of C’s volatile keyword is poorly understood by many programmers. This is not surprising, as most C texts dismiss volatile in a sentence or two. This article will teach you the proper way to use volatile.
Have you ever faced this issue, when you are testing your code independently or without any compiler optimization, it works perfectly fine but as soon as you integrate it with system code or perform compiler optimization, the things start to crash and no longer provides the same output?
Have you experienced any of the following in your C or C++ embedded code?
- Code that works fine–until you enable compiler optimizations
- Code that works fine–until interrupts are enabled
- Flaky hardware drivers
- RTOS tasks that work fine in isolation–until some other task is spawned
If you answered yes to any of the above, it’s likely that you didn’t use the C keyword volatile. You aren’t alone: the use of volatile is poorly understood by too many programmers. It’s not so straight forward to have a sample working C program which can easily showcase the exact effect of “volatile” keyword. You probably won’t be able to use a variable which is qualified as “volatile“ unless you’re doing some low-level hardware programming in C.
What is a Volatile Variable?
Many a times, when you compile a C/C++ code, the compiler optimizes the code to reduce unnecessary processing. Take the following code for example:
In the above code, we are receiving data in a variable called “receivedData.” Inside the main() function, we are forwarding this data if the data is greater than 20. All looks fine with this code. However, if the compiler optimization is switched on, the compiler will assume that the value of “receivedData” is not changing. Hence, it creates a copy of the variable and uses the copy for execution. Now, whenever we receive new data, our main() function is not seeing the updated value because it is using a copy. This can be a serious problem.
The variable “receivedData” is volatile as it can change at any time unexpectedly. So, it is always a good idea to tell it to the compiler that it is a volatile variable and to not optimize it.
A volatile variable is one that can change unexpectedly, it means the compiler cannot make any assumption about its value.
Volatile keyword is intended to prevent the compiler to perform any optimization on that object, that can change in ways which cannot be determined by compiler.
The object can be a memory, register, SFR.
What is a Volatile Keyword in C?
C’s volatile keyword is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time–without any action being taken by the code the compiler finds nearby. The implications of this can be quite serious, and sometimes software experts testify in regard to product failures. But before we examine the implications, let’s take a look at the syntax.
Syntax of C’s volatile Keyword
To declare a variable volatile, include the keyword volatile before or after the data type in the variable definition. For instance both of these declarations will declare an unsigned 16-bit integer variable to be a volatile integer:
Volatile Integer in C/C++
volatile uint16_t x; uint16_t volatile y;
Pointer to Volatile Integer in C/C++
Now, pointers to volatile variables are very common, especially with memory-mapped I/O registers. Both of these declarations declare p_reg to be a pointer to a volatile unsigned 8-bit integer:
volatile uint8_t * p_reg;uint8_t volatile * p_reg;
In case you do not know how to use pointers, or are not very confident, do not worry. Pointers in C/C++ are very easy. Click on the link to read.
Pointers in C/C++
Wrong use of Pointer to Volatile Integer in C/C++
The volatile keyword in CPP and C programming is a crucial qualifier that embedded developers must understand to write reliable system-level code.
//volatile integer variable volatile int num; //integer pointer int* ptr = #
Correct use of Pointer to Volatile Integer in C/C++
//volatile integer variable volatile int num; //integer pointer volatile int* ptr = #
And just for completeness, if you really must have a volatile pointer to a volatile variable, you’d write:
uint16_t volatile * volatile p_y;
Volatile Structure or Union in C/C++
Finally, if you apply volatile to a struct or union, the entire contents of the struct or union are volatile. If you don’t want this behavior, you can apply the volatile qualifier to the individual members of the struct or union.
Proper Use of C’s volatile Keyword
A variable should be declared volatile whenever its value could change unexpectedly. In practice, only three types of variables could change:
1. Memory-mapped peripheral registers
2. Global variables modified by an interrupt service routine
3. Global variables accessed by multiple tasks within a multi-threaded application
Consequences of over usage of volatile
Volatile variable forces compiler not to keep a copy and always get the fresh value from memory and register which takes more clock cycles. For real-time application, you should always perform timing analysis on a volatile variable to check it fulfills the timing requirement.
How the Compiler Optimizes Away Reads Without volatile
To understand why volatile matters, you need to understand what the compiler does without it. Modern compilers (GCC, Clang, MSVC) aggressively optimize memory accesses. If the compiler sees that a variable was already read and no visible code path modifies it, it reuses the cached value from a register instead of re-reading from memory.
// Without volatile — compiler may optimize this loop away
uint32_t *status_reg = (uint32_t *)0x40021000;void wait_for_ready(void) { while ((*status_reg & 0x01) == 0) { // Compiler sees: status_reg never changes in this loop // Optimization: reads *status_reg once, loops forever on cached value } } “`
The compiler is technically correct — from the C abstract machine’s perspective, nothing in the loop modifies *status_reg. But in embedded systems, hardware peripherals change register values independently of the CPU. The volatile keyword tells the compiler: “this value can change at any time, outside the program’s control — always re-read it from memory.”
// With volatile — compiler generates a load instruction every iteration
volatile uint32_t *status_reg = (volatile uint32_t *)0x40021000;void wait_for_ready(void) { while ((*status_reg & 0x01) == 0) { // Compiler must re-read *status_reg from memory each iteration // Correct behavior: loop exits when hardware sets bit 0 } } “`
You can verify this by examining the generated assembly. Without volatile, the compiler generates one LDR instruction before the loop. With volatile, there’s an LDR inside the loop body.
volatile with Interrupt Service Routines (ISRs)
The most common use of volatile in embedded C is for variables shared between an ISR and the main loop (or an RTOS task). Without volatile, the main loop may never see the ISR’s updates.
volatile uint8_t data_ready = 0; // Must be volatile — modified by ISR
volatile uint8_t rx_data;void USART1_IRQHandler(void) { rx_data = USART1->DR; // Read received byte data_ready = 1; // Signal main loop }
int main(void) { while (1) { if (data_ready) { // Without volatile, compiler may optimize this check away process_data(rx_data); data_ready = 0; } } } “`
Without volatile on data_ready, the compiler sees that main() never sets data_ready to a nonzero value (it doesn’t know about the ISR), so it may optimize the if check to always-false and remove the entire block.
Important: volatile does NOT provide atomicity. If rx_data is a 32-bit value on an 8-bit MCU, the ISR could update it mid-read by the main loop. For multi-byte shared variables, you also need to disable interrupts or use a mutex.
volatile with Memory-Mapped Peripheral Registers
Every microcontroller’s peripheral registers are memory-mapped — they appear at specific addresses in the address space, but their values change based on hardware events, not software writes.
// STM32 GPIO registers — all must be accessed through volatile pointers
#define GPIOA_BASE 0x40020000
#define GPIOA_IDR (*(volatile uint32_t *)(GPIOA_BASE + 0x10)) // Input Data Register
#define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE + 0x14)) // Output Data Register
#define GPIOA_BSRR (*(volatile uint32_t *)(GPIOA_BASE + 0x18)) // Bit Set/Reset Registervoid toggle_led(void) { if (GPIOA_IDR & (1 << 5)) { // Read pin state — volatile ensures actual hardware read GPIOA_BSRR = (1 << 21); // Reset bit 5 — volatile ensures actual hardware write } else { GPIOA_BSRR = (1 << 5); // Set bit 5 } } “`
MCU vendor headers (like STM32’s stm32f4xx.h) define all peripheral register structures with volatile. This is why you can write GPIOA->ODR = value and it works correctly — the structure definition already includes the volatile qualifier.
// From STM32 CMSIS header (simplified)
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 register
volatile uint32_t IDR; // Input data register
volatile uint32_t ODR; // Output data register
volatile uint32_t BSRR; // Bit set/reset register
volatile uint32_t LCKR; // Lock register
} GPIO_TypeDef;volatile Does Not Mean Thread-Safe
A common misconception: volatile makes a variable thread-safe. It does not. volatile only prevents compiler optimizations — it says nothing about hardware-level atomicity or memory ordering.
volatile uint32_t shared_counter = 0;// Task A (or ISR) shared_counter++; // This is NOT atomic — it’s: load, increment, store
// Task B if (shared_counter > 10) { // shared_counter could change between the comparison and here reset_counter(); } “`
For true thread safety, you need:
- Atomic operations — hardware-level read-modify-write (
__atomic_add_fetchon ARM Cortex-M3+) - Disabling interrupts — prevents ISR from preempting during the critical operation
- Mutexes — in RTOS environments, for protecting multi-step operations
// Correct: volatile + interrupt disable for ISR-shared variable
volatile uint32_t event_count = 0;void increment_event_count(void) { __disable_irq(); event_count++; __enable_irq(); }
uint32_t read_event_count(void) { __disable_irq(); uint32_t val = event_count; __enable_irq(); return val; } “`
On ARM Cortex-M, single 32-bit reads and writes ARE atomic (single LDR/STR instruction), so a volatile uint32_t flag shared between ISR and main loop is safe for simple flag-based signaling. But any read-modify-write (++, |=, &=) is not atomic.
volatile Qualifiers with Pointers
The placement of volatile with pointers matters — there are three distinct meanings:
volatile int *ptr; // Pointer to volatile int — the DATA is volatile
int *volatile ptr; // Volatile pointer to int — the POINTER is volatile
volatile int *volatile ptr; // Both the pointer AND the data are volatilePointer to volatile data (volatile int *ptr) is the most common in embedded systems. It means: “the data at this address can change unexpectedly, so always re-read it.” Use this for peripheral registers and shared variables.
Volatile pointer (int *volatile ptr) is rare. It means: “the pointer itself can change unexpectedly.” This might be used if an ISR changes which buffer a pointer points to.
Both volatile (volatile int *volatile ptr) means both the pointer and the pointed-to data can change unexpectedly.
// Common embedded pattern: pointer to volatile hardware register
volatile uint32_t * const UART_DR = (volatile uint32_t *)0x40011004;
// - volatile: data at address can change (hardware register)
// - const: pointer itself never changes (register address is fixed)void uart_send(uint8_t byte) { while (!(*UART_SR & TX_EMPTY)); // Wait for TX buffer empty *UART_DR = byte; // Write to hardware register } “`
When NOT to Use volatile
Overusing volatile hurts performance because the compiler cannot optimize memory accesses. Only use it when the value can genuinely change outside the program’s visible control flow.
Do NOT use volatile for:
- Variables accessed only within a single thread/task with no ISR interaction
- Local variables (they can’t be accessed by ISRs or other tasks)
- Variables protected by a mutex (the mutex API includes memory barriers that make
volatileunnecessary) - RTOS queue data (the RTOS handles synchronization internally)
// WRONG: volatile is unnecessary here
volatile int total = 0; // Only used in calculate()
int calculate(int a, int b) {
total = a + b; // No ISR or other task accesses total
return total * 2; // Compiler can't optimize, even though it should
}// RIGHT: no volatile needed — variable is local to this function’s logic int total = 0; int calculate(int a, int b) { total = a + b; return total * 2; // Compiler can optimize freely } “`
In RTOS code with FreeRTOS, if you pass data through queues or protect it with mutexes, those APIs include the necessary memory barriers. Adding volatile on top of that is redundant and prevents the compiler from optimizing the code between synchronization points.
📖 Related: How to use volatile qualifier with structure?
Related on this site
- The canonical
volatileuse case is memory-mapped IO — see introduction to memory mapping for the broader context. - Any variable shared between an ISR and the main loop also needs
volatile— see polling vs interrupts for the interrupt-handling patterns. - Microcontroller register access usually goes through
volatile-qualified pointers — see registers in microcontrollers for the canonical pattern.

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






