C & C++ DISCUSSION

Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

Started by kimanzi volatile keywordinterrupt service routineshared variablesatomic accesscompiler optimisation
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

kimanzi C & C++ Forum
#1

On an STM32 I set a flag in a timer interrupt with flag = 1; and the main loop waits with while (!flag) { }. Built with -O0 it works. With -O2 the loop never exits, although the debugger shows the interrupt running and writing the flag. Declaring the flag volatile fixes it.

Why does the optimiser break the original code? I also have a 32-bit tick counter written only by the ISR, and an event counter that the ISR increments and the main loop decrements. Is volatile enough to make those two safe?

Community replies 5

Re: Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

#2

Without volatile the compiler may assume that a variable only changes through code it can see in the current flow of control. Nothing inside while (!flag) { } writes to flag, so at -O2 it loads the value once, keeps it in a register and effectively compiles if (!flag) for (;;) { }. It has no idea that an interrupt handler can run in the middle of the loop.

volatile tells the compiler that every read and write in the source must really be performed, in order, with nothing cached or merged. That is exactly what a variable shared with an ISR or a hardware register needs.

Re: Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

#3

volatile guarantees that each access happens; it does not make a sequence of accesses atomic. count-- in the main loop is a load, a subtract and a store. If the interrupt fires between the load and the store and does count++, main then writes back its stale result and the increment is lost. That is true even for a 32-bit variable on a 32-bit core, because it is three instructions.

The usual fix is a short critical section around the read-modify-write in main. On a Cortex-M, save PRIMASK, call __disable_irq(), update the counter and restore the saved state, so the code still behaves when it is called with interrupts already disabled.

Re: Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

#4

The tick counter is a different case: one writer (the ISR) and code that only reads. Whether a plain read is safe depends on the variable width against the CPU word size. A Cortex-M loads an aligned uint32_t with one instruction, so main sees either the old or the new value.

On an 8-bit AVR the same read takes four byte loads. If the tick goes from 0x000000FF to 0x00000100 after the low byte was read, main assembles 0x000001FF, which is 255 ticks in the future. There, wrap the read in ATOMIC_BLOCK(ATOMIC_RESTORESTATE) from avr-libc's util/atomic.h, or read twice and retry until both reads match.

Re: Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

#5

C11 offers a portable alternative in <stdatomic.h>. Declare the event counter as atomic_int and use atomic_fetch_add(&count, 1) and atomic_fetch_sub(&count, 1); these are true atomic read-modify-write operations, and atomic_flag covers the simple flag case.

How they are implemented depends on the core. Cortex-M3 and above have LDREX/STREX, so the compiler emits a short retry loop. Cortex-M0 has no such instructions, and there the compiler may call library helper functions that your toolchain has to provide. Check the generated code, or keep the interrupt-masking approach on those parts.

Re: Why does my ISR flag need volatile in C, and is volatile enough for a shared counter?

#6

Two practical points. For memory-mapped registers the qualifier belongs to the pointed-to object: volatile uint32_t *reg is a pointer to a volatile register, while uint32_t * volatile reg is a volatile pointer to ordinary memory, which is rarely what you want.

Also, do not mark everything volatile "to be safe". Each use becomes a real load, so an expression that mentions a volatile variable three times reads it three times and may see three different values. Copy it once, uint32_t now = ticks;, and work on the copy. To verify the fix, look at the disassembly of the wait loop at -O2: there should be a load instruction inside the loop.

TEP COMMUNITY