Publishing MCU Data with Atomics: Pair Release and Acquire Around the Payload
An atomic ready flag does not automatically make an ordinary payload safe to share. The producer must publish completed writes with release semantics, and the consumer must acquire the publication before reading. A reusable slot also needs the reverse handoff: the consumer must finish reading before the producer overwrites it. Qualify both directions, not just the first successful transfer.
This guide uses a C11 single-producer, single-consumer slot with ordinary CPU-accessed memory. It excludes DMA, device registers, multiple producers, multiple consumers and concurrent reset. The executed fixture uses GCC 14.2.0 on an x86-64 host with POSIX threads. MCU interrupt suitability, atomic code generation and cache behavior require separate target verification.
Write the ownership invariant first
Let ready equal zero mean the producer may write the slot, and ready equal one mean the consumer may read it. Only the producer changes zero to one, and only the consumer changes one to zero. Each side must observe permission before touching the ordinary payload. Nobody else may inspect or modify the payload concurrently, including a diagnostic task.
The public WG14 N1570 C11 committee draft, sections 5.1.2.4 and 7.17.3, describes release/acquire synchronization and the data-race rule. An acquire operation must read the relevant release publication for the ordering argument to apply. Merely placing unrelated acquire and release operations somewhere in the program is insufficient.

Use two release/acquire handoffs
The following original C11 fragment is the protocol used in the executed fixture. Its translation unit includes stdatomic.h, stdint.h and stdbool.h. The output destination and input value belong privately to their callers.
struct payload { uint32_t sequence, inverse; };
static struct payload slot;
static _Atomic unsigned ready = 0;
static bool try_publish(struct payload value) {
if (atomic_load_explicit(&ready, memory_order_acquire) != 0)
return false;
slot = value;
atomic_store_explicit(&ready, 1, memory_order_release);
return true;
}
static bool try_consume(struct payload *out) {
if (atomic_load_explicit(&ready, memory_order_acquire) == 0)
return false;
*out = slot;
atomic_store_explicit(&ready, 0, memory_order_release);
return true;
}
The producer’s payload assignment precedes its release store of one. The consumer’s successful acquire observation of that publication precedes its payload copy. In the reverse direction, the consumer’s copy precedes its release store of zero, and the producer’s successful acquire observation of that acknowledgment precedes the next overwrite.
This gives the ordinary payload accesses an ordering relationship despite the payload fields not being atomic individually. It does not create a general lock around the functions. Two producers could both observe zero and write simultaneously, so adding another producer violates the design rather than merely reducing performance.
Identify the tempting incomplete variants
- Relaxed flag everywhere: atomic flag access alone supplies no inter-thread ordering for the ordinary payload. Do not infer publication from the absence of torn flag reads.
- Release publication without acquire consumption: the consumer lacks the matching ordering edge before reading the payload.
- Correct publication but no reuse acknowledgment: the next producer write can overlap the current consumer read.
- Clear ready before copying: this hands the slot back while the consumer still needs its bytes.
- Volatile payload or flag: volatile is not a replacement for the required atomic synchronization protocol.
The GCC 14.2.0 atomic documentation also distinguishes relaxed access from acquire/release ordering and notes that target implementations can use external helpers. Therefore, language-level correctness and an ISR-safe implementation are separate reviews. A correct handoff using an unsuitable runtime helper is still unsuitable for that interrupt path.
Choose a failure policy for the try operations
A false publish result means this attempt did not observe permission to reuse the slot. Treat it as unavailable, and preserve, drop or retry the new value according to the application’s contract. This observation is not a current ownership snapshot: the consumer may release the slot immediately afterward, and an acquire load need not observe the latest state. Do not overwrite anyway. A false consume result means no payload was acquired; the caller must not process an old output buffer as fresh data.
In a hypothetical acquisition system, an ISR publishes a completed CPU-side sample while a thread consumes it. The ISR must not spin waiting for that thread to return the slot: the thread may be unable to run until the ISR exits. A bounded failure path can count the rejected sample or defer work using a separately approved mechanism. This example does not claim that the shown atomic type is lock-free on the chosen MCU.
Initialization and reset belong outside concurrent operation. Resetting ready to zero while a consumer still reads can grant the producer conflicting permission. Reinitializing the atomic object during active access is also invalid. If restart is required, stop both participants and establish quiescence before resetting the protocol state.
Executed host checks
The fixture ran one producer and one consumer for 200,000 transfers per process. Each payload contained a monotonically increasing sequence and its bitwise inverse. The consumer checked both fields and exact sequence order before releasing the slot. Five runs passed with the final slot empty, for one million checked transfers in total.
The build used GCC 14.2.0, C11, -O2, pthreads and warnings treated as errors. Retrying host threads yielded between failed attempts. These tests observed the intended behavior; they are not a formal proof, a data-race-sanitizer result or MCU timing evidence. The proof obligation remains the ownership invariant and the two synchronization edges.
For target qualification, inspect actual atomic lowering, test sustained producer/consumer overlap and exercise full-slot rejection. Verify that no logger or reset path accesses the payload outside the protocol. Finally, measure the target’s service budget separately. Release/acquire orders memory access; it does not make the consumer run sooner or guarantee that a sample will be accepted.