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.