Search topics...
Interrupts & PrioritiesShared Data Protectionfoundational

How do you safely share data between an ISR and the main loop?

0 upvotes
Practice with AISoon
Study the fundamentals first — Interrupts & Priorities topic page

Sharing data between an ISR and the main loop is one of the most error-prone areas in bare-metal firmware. The fundamental problem is that the ISR can fire at any point during the main loop's execution, creating a race condition if both contexts access the same variable. The first and most critical requirement is the volatile keyword: any shared variable must be declared volatile to prevent the compiler from caching its value in a register. Without volatile, the main loop might read the variable once into a register and never re-read it from memory, missing all subsequent ISR updates. This bug is invisible in debug builds (where optimizations are off) and only appears in release builds.

Beyond volatile, you must ensure atomicity. On Cortex-M3/M4/M7, a single aligned 32-bit read or write is atomic — the bus guarantees it completes in one cycle and cannot be interrupted mid-operation. So a volatile uint32_t flag can be safely set in the ISR and read in the main loop. But a 64-bit variable, a struct, or even two related 32-bit variables that must be consistent with each other (like a timestamp and a sensor value) are not atomic. If the ISR updates both fields and the main loop reads between the two writes, it gets a half-old, half-new snapshot — a torn read.

For non-atomic shared data, the standard solutions are: (1) Critical sections — disable interrupts around the main-loop read (__disable_irq() / __enable_irq()), guaranteeing the ISR cannot fire mid-read. Keep these sections as short as possible. (2) Double buffering or sequence counters — the ISR writes to one buffer and flips an index; the main loop reads from the other buffer. No critical section needed, but the logic is more complex. (3) Lock-free ring buffers — ideal for streaming data (UART bytes, ADC samples). The ISR writes to the head, the main loop reads from the tail, and as long as both pointers are updated atomically and the buffer does not overflow, no locking is needed. This is the gold-standard pattern for ISR-to-main-loop data transfer in production firmware.

Source: Interrupts & Priorities Q&A