C · 2026-06-16 · embedded · atomic · ISR · concurrency · Cortex-M

Atomic writes & RMW (C)

On a 32-bit core a single aligned word write is already atomic — the bus transaction either happens or it doesn’t. The danger is the read-modify-write: flags |= MASK compiles to load, OR, store, and an interrupt landing between the load and the store works on a stale copy, so your bit is silently lost. The same is true of ++ and of any update that spans more than one word. The two standard fixes:

#include <stdint.h>
#include "cmsis_gcc.h"          /* __LDREXW / __STREXW exclusive access (Cortex-M3+) */

/*******************************************************************************
 * Function Name  : atomic_fetch_or
 * Description    : Atomically OR mask into *addr using LDREX/STREX (no IRQ disable).
 * Input          : addr - word to update; mask - bits to set.
 * Output         : None.
 * Return         : The previous value of *addr.
 *******************************************************************************/
uint32_t atomic_fetch_or(volatile uint32_t *addr, uint32_t mask)
{
  uint32_t oldVal;
  uint32_t newVal;

  do
  {
    oldVal = __LDREXW(addr);     /* take the exclusive reservation */
    newVal = oldVal | mask;
  }
  while (__STREXW(newVal, addr) != 0u);   /* retry if the monitor was lost */

  return oldVal;
}

The exclusive monitor flags the address on LDREX; if anything else writes it (an ISR, a DMA, another core) before the STREX, the store fails, returns non-zero, and the loop retries. No interrupts are disabled, so latency stays bounded — the right tool when the update is a single word.

Portable fallback: a critical section

For a core without LDREX (Cortex-M0), or when the update touches several words that must change together, the portable hammer is to briefly disable interrupts — saving and restoring PRIMASK so the function nests safely inside an already-disabled context.

static volatile uint32_t sharedFlags;

/*******************************************************************************
 * Function Name  : flags_set
 * Description    : Set bits in a shared flags word inside a critical section.
 * Input          : mask - bits to set.
 * Output         : None.
 * Return         : None.
 *******************************************************************************/
void flags_set(uint32_t mask)
{
  uint32_t irqState = __get_PRIMASK();   /* save current interrupt-enable state */

  __disable_irq();
  sharedFlags |= mask;
  __set_PRIMASK(irqState);               /* restore — never blindly __enable_irq() */
}

Restoring the saved PRIMASK instead of unconditionally re-enabling is the part people get wrong: if a caller had already entered a critical section, a blind __enable_irq() would re-open interrupts too early and reintroduce the very race you were guarding against.

What’s already atomic

  • A single read or write of an aligned uint32_t (or smaller) — no protection needed.
  • Everything else: |=, &=, ++, --, and any multi-word or multi-field update — these are read-modify-write and need one of the methods above.
  • On a hosted or multi-core target prefer C11 <stdatomic.h> (atomic_fetch_or) and let the compiler pick the right instructions; the code above is the bare-metal equivalent.