Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way an interrupt or a task hands data to the rest of the program

A volatile flag, a critical section, a ring buffer, a FreeRTOS queue, semaphore or mutex, or a spinlock across cores. Who writes, who reads, and how much data decide which.

Ask who produces the data and who consumes it (an interrupt handler, a task, loop(), the other core), how big it is, and whether events can arrive faster than they are handled.

  • One flag or one small value from an interrupt to loop() → a volatile variable the handler sets and loop() reads. volatile makes every access happen; it does not make a multi-byte read atomic.
  • A multi-byte value or several related variables → read them inside a short critical section (noInterrupts() … interrupts()), so the handler can't change them halfway.
  • A stream of events from an interrupt → a single-producer, single-consumer ring buffer, storing before publishing the index, sized for the longest time loop() can be away.
  • Between FreeRTOS tasks → a queue for data, a binary semaphore to signal from an interrupt (xSemaphoreGiveFromISR), a mutex to guard a shared resource, taken and given on every path.
  • Across the ESP32-S3's two cores → noInterrupts() only stops this core; protect shared data with a portMUX spinlock or an RTOS primitive.
  • Large blocks from a peripheral → let DMA copy them, with double buffering so the processor works on one buffer while the other fills.

In every case keep the handler short: record and flag in the handler, and do the slow work (printing, maths) outside it.

Interrupt to loop()

Short handlers that record and flag, volatile shared variables, and critical sections for multi-byte reads.

A C++ keyword telling the compiler every read and write of a variable must really happen.
A short stretch of code during which interrupts (or other tasks) are kept out.
Reading a value while it is half-updated, getting a mix of old and new.

A stream of events

Section

Ring buffers

N slots hold at most N − 1 values; one writer, one reader, store before publishing.

A fixed-size buffer whose end wraps around to its start, used as a queue.

Example

A 16-slot ring buffer holds at most 15 values; head == tail means empty, (head + 1) % 16 == tail means full.
A queue with exactly one writer and one reader, which can be made safe without locks.

Between tasks

A queue for data, a binary semaphore to signal from an interrupt, a mutex to guard a resource.

What goes wrong when the sharing is unprotected, or locked in the wrong order.

A first-in, first-out channel for passing items between tasks.
A lock that lets only one task use a shared resource at a time.
A signalling object: one task or ISR gives it, another takes it.

Across two cores

Where loop() and the Wi-Fi tasks run, and why two cores make shared data harder to protect.

A lock that waits by looping, used between two CPU cores.

Big blocks without the processor

A hardware copier between a peripheral and memory, and double buffering that overruns if the consumer is too slow.

Using two buffers: one fills while the other is processed, then they swap.