Skip to content
volatile is not a mutex: sharing data between an ISR and your main loop
article3 min read163 viewsSep 9, 2026

volatile is not a mutex: sharing data between an ISR and your main loop

Diego Ramírez@diego_firmware

September 9, 2026 · 3 min read

#1 in Bare Metal

163 views3 comments

Every few months a junior in my team come with the same bug: a counter updated in an interrupt, read in the main loop, and sometimes the value is garbage. And the first answer is always "but I put volatile". So here is the explanation I give them, maybe is useful for someone else.

What volatile does

volatile tells the compiler: this variable can change outside of the normal program flow, so read it from memory every time and don't optimize the access away. Without it, the compiler can keep the value in a register and your main loop never see the update from the ISR. So yes, you need it. But that is all it does.

What volatile does NOT do

It does not make the access atomic. Imagine a 32-bit counter on an 8-bit or 16-bit microcontroller. Reading it takes several instructions. If the interrupt fires in the middle of the read, you get half old value and half new value:

volatile uint32_t ticks;

void TIMER_IRQHandler(void) {
    ticks++;            // read, modify, write
}

uint32_t elapsed(void) {
    return ticks;       // on an 8-bit MCU this is 4 separate byte reads
}

Is the same problem if you do ticks++ from both sides, because increment is read, modify, write. Even on a 32-bit Cortex-M, where a single aligned 32-bit read is atomic, a ++ in the main loop is not.

What to use instead

  1. Short critical section. Disable interrupts, copy the value, restore. Keep it very short, only the copy.
    uint32_t elapsed(void) {
        uint32_t primask = __get_PRIMASK();
        __disable_irq();
        uint32_t copy = ticks;
        __set_PRIMASK(primask);   // restore, don't blindly enable
        return copy;
    }
    Notice we restore the previous state and don't just enable, because maybe the caller already had interrupts disabled.
  2. Single writer design. For a ring buffer from ISR to main loop, the ISR writes only the head and the main loop writes only the tail. Each index has one writer, and if the index fit in a type the CPU reads in one instruction, you don't need to disable anything. This is my favourite.
  3. C11 atomics (stdatomic.h) if your toolchain support them well. On Cortex-M3 and up they compile to LDREX/STREX. On Cortex-M0 there is no LDREX/STREX, so you get a library call that disables interrupts anyway. Check what you get.

Also, if you use an RTOS, don't call the normal queue functions from the ISR. FreeRTOS has the FromISR versions, like xQueueSendFromISR, exactly for this.

volatile = visibility. Atomicity is your job. If you remember only one line, remember this one.

Written by

Diego Ramírez

Firmware engineer at automotive supplier in Guadalajara. C, FreeRTOS, CAN bus, oscilloscopes. Mechanical keyboard collector (too many).

Get the best of #bare_metal weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

3

Sign in to join the conversation.

Sign in to comment
Jake Morrison@jakemakesgames#4 in Bare Metal
gamedev here and this is the same lesson as "marking it static doesnt make it thread safe" in unity jobs. massive respect to anyone who debugs with an oscilloscope tho
0 net votes
Tomasz Kowalski@tomasz_k#3 in Bare Metal
Same in Java world. People think volatile makes count++ safe. It does not. Use AtomicInteger. Twenty years and I still find this in code review.
2 net votes
Yuki Tanaka@yuki_ios#4 in Bare Metal
In Swift we have a similar discussion with actors now. Very clear explanation, thank you ^^
0 net votes