Fundamentals 11 min read

Why Adding a printf Fixes Your Embedded Bug: The volatile Keyword Explained

This article explains how missing volatile causes infinite loops when waiting for interrupt-set flags, demonstrates compiler optimization caching variables in registers, and clarifies volatile's limits in multi-core systems and atomicity guarantees.

IT Services Circle
IT Services Circle
IT Services Circle
Why Adding a printf Fixes Your Embedded Bug: The volatile Keyword Explained

Debugging embedded code often reveals a puzzling phenomenon: the program hangs, but adding a printf to trace execution makes it work. Remove the print, and it breaks again. This article explains why this happens and how the volatile keyword fixes it.

A Typical Scene: Interrupt Sets Flag, Main Loop Waits Forever

Consider a bare-metal or RTOS pattern: an interrupt sets a shared flag, and the main loop polls it.

int flag = 0; // shared between interrupt and main loop

void irq_handler(void) // interrupt
{
    flag = 1;
}

int main(void) // main loop
{
    while (flag == 0)
        ; // wait for interrupt to set flag
    /* continue work */
}

The logic looks correct. The interrupt sets flag = 1, so the main loop should exit. However, with compiler optimizations enabled (e.g., -O2), the loop may never exit. The interrupt fires, flag becomes 1 in memory, but the main loop keeps spinning.

What the Compiler Does

The compiler sees while (flag == 0) with an empty body. Since nothing inside the loop modifies flag, it assumes the value never changes during the loop. It caches the first read of flag into a register and reuses that register value for every iteration. The generated assembly becomes an infinite loop testing a register that never updates.

The interrupt genuinely writes 1 to memory, but the main loop reads the stale register copy. They operate on different storage locations.

Adding printf "fixes" it because printf is a function call. The compiler cannot assume it leaves memory untouched, so it reloads flag from memory after the call. The fix is accidental — printf merely breaks the optimization assumption.

Inspecting the assembly with gcc -O2 -S shows the difference: without volatile, the loop has no memory load instruction; with volatile, each iteration emits a load from memory.

What volatile Actually Prevents

volatile

tells the compiler: "This variable may change in ways you cannot see. Do not cache it; read from memory every time." volatile int flag = 0; With this qualifier, the compiler emits a memory load on every access. The main loop now sees the interrupt's update. volatile is a "prevent compiler optimization" switch for variables modified outside the normal execution flow — interrupt handlers, hardware registers, other tasks.

Another Common Scene: Polling Hardware Registers

Hardware status registers change autonomously. For example, waiting for a UART transmission-complete bit:

while (!(UART->SR & UART_SR_TC))
    ;

If UART->SR lacks volatile, the compiler may cache the register value, creating another infinite loop. Hardware ignores compiler assumptions. Proper register definitions always include volatile:

#define UART_SR (*(volatile uint32_t *)0x40004400)

Boundary 1: Multi-Core SMP — volatile Is Not Enough

The interrupt/main-loop scenario works on single-core bare metal or RTOS because interrupt and main loop share the same physical memory view. volatile forces memory reads, which suffices.

On multi-core SMP, even one-way notification (core A writes, core B reads) fails with only volatile. volatile constrains the compiler, not the CPU. Core A's write may sit in its store buffer, not yet visible to core B. Core B may read a stale cache line. The missing piece is a memory barrier or atomic operation that orders CPU memory operations.

volatile constrains the compiler; memory barriers/atomics constrain the CPU. Single-core needs only compiler protection; multi-core needs both.

In Linux kernel drivers, cross-core flags require READ_ONCE / WRITE_ONCE, atomic operations, or locks — not plain volatile. Do not transplant bare-metal habits to SMP.

Boundary 2: No Atomicity, No Ordering Guarantees

Many mistakenly treat volatile as "thread-safe" or a lock substitute. It guarantees neither atomicity nor ordering.

No atomicity: counter++ compiles to three instructions (read, increment, write). volatile does not prevent interruption between them. Two tasks incrementing concurrently can lose updates.

No ordering: volatile only protects its own variable from optimization. It does not stop the compiler or CPU from reordering other memory accesses around it. The sequence "write data, then set flag" is not enforced.

For concurrent read-modify-write, use atomic operations, critical sections, or mutexes. volatile cannot replace them.

Three Self-Check Questions

To decide if a variable needs volatile, ask:

Can this variable's value be changed by an interrupt, hardware, or another task?

Am I reading it in a loop waiting for it to change?

Could the compiler optimize the memory read into a register cache?

If any answer is yes, add volatile. Conversely, if only your code touches the variable, volatile is unnecessary overhead.

Bonus note: In POSIX signal handlers, only volatile sig_atomic_t is guaranteed safe for shared variables — a detail that signals standards awareness in interviews or code reviews.

Next time you encounter "adding a printf fixed it" or "works at -O0 but fails at -O2," check whether a shared variable missed volatile, letting the compiler optimize away the memory read.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyvolatilememory modelcompiler optimizationmulti-coreinterrupt handlingembedded Chardware registers
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.