
The volatile keyword is one of the most misunderstood yet critical qualifiers in embedded C programming. While it appears simple on the surface, improper use of volatile can lead to subtle bugs that are notoriously difficult to debug, especially in real-time systems where timing and deterministic behavior are paramount.
In C, volatile is a type qualifier that tells the compiler that a variable’s value can change at any time—without any action being taken by the code the compiler finds nearby. This has three important implications:
Without volatile, the compiler assumes it knows when a variable’s value changes and may optimize code in ways that break embedded systems interacting with hardware.
The most common use of volatile is for memory-mapped hardware registers:
#define UART_DR (*(volatile uint32_t*)0x40011000) // Data register#define UART_SR (*(volatile uint32_t*)0x40011004) // Status registervoid uart_send(char c) {// Wait until transmit buffer is emptywhile (!(UART_SR & 0x00000080)) ; // Must read SR each time// Send characterUART_DR = c; // Must write to DR each time}
Without volatile, the compiler might:
Variables shared between ISRs and main code must be declared volatile:
volatile uint16_t encoder_count = 0;void TIM2_IRQHandler(void) {if (TIM2->SR & TIM_SR_UIF) {encoder_count++; // Modified in ISRTIM2->SR &= ~TIM_SR_UIF;}}int main(void) {// ... initialization ...while (1) {uint16_t count = encoder_count; // Must read fresh value each time// Process count...}}
Without volatile on encoder_count, the compiler might:
volatile only prevents optimization on that specific variable. It does not:
For proper synchronization, you still need memory barriers or atomic operations:
#include <stdatomic.h>volatile atomic_uint shared_flag = 0;void isr_handler(void) {atomic_store_explicit(&shared_flag, 1, memory_order_release);}void main_loop(void) {if (atomic_load_explicit(&shared_flag, memory_order_acquire)) {// Process flagatomic_store_explicit(&shared_flag, 0, memory_order_relaxed);}}
volatile uint32_t* reg_ptr; // Pointer to volatile uint32_t (correct for hardware)uint32_t volatile* reg_ptr2; // Same as aboveuint32_t* volatile reg_ptr3; // Volatile pointer to non-volatile int (rarely what you want)
For hardware registers, you almost always want “pointer to volatile”.
When dealing with structures or arrays containing hardware registers, you need to apply volatile correctly:
// Correct: Each register is volatiletypedef struct {volatile uint32_t CR; // Control registervolatile uint32_t SR; // Status registervolatile uint32_t DR; // Data register} UART_TypeDef;// Incorrect: Only the struct instance is volatiletypedef struct {uint32_t CR;uint32_t SR;uint32_t DR;} UART_TypeDef;volatile UART_TypeDef* UART1 = (volatile UART_TypeDef*)0x40011000; // Still wrong!
In the incorrect example, while the pointer prevents reordering of the struct access, individual register accesses inside the struct can still be optimized away.
While volatile prevents problematic optimizations, it does come with a cost:
In performance-critical code, minimize the scope of volatile accesses:
// Less optimal - repeated volatile accesseswhile ((volatile uint32_t*)0x40011000) & 0x01) {// Do work}// Better - minimize volatile readsvolatile uint32_t* const status_reg = (volatile uint32_t*)0x40011000;while (*status_reg & 0x01) {// Do work}
The volatile keyword is essential for correct embedded C programming when dealing with hardware and asynchronous events. Understanding what it does (and doesn’t do) is crucial for writing reliable firmware. When used correctly, it ensures that your code interacts with hardware exactly as intended, preventing entire classes of subtle, timing-dependent bugs.
Remember: volatile tells the compiler “I know better than you when this value might change”—use it wisely, and always verify your assumptions about hardware behavior through careful reading of datasheets and reference manuals.
Quick Links
Legal Stuff





