Bit-Banding and Atomic Access on Cortex-M3/M4: Safe Bit Operations

A line such as flags |= (1u << 3); looks like one operation, but the CPU performs three: load the word, OR in the bit, store the word back. If an interrupt fires between the load and the store and changes another bit in the same word, the main code writes back its stale copy and the interrupt's change is lost. This is a read-modify-write race, and it causes rare, unrepeatable bugs. This article shows four ways to make bit operations atomic on ARM, starting with the Cortex-M3 and M4 bit-band feature.

Seeing the Race

Consider a status word shared between main code and a UART interrupt. Main sets bit 0 to request a transmission; the interrupt sets bit 1 when a byte arrives. The compiled code for main typically looks like this:

main: flags |= 1           UART ISR: flags |= 2

ldr  r1, [r0]      ; r1 = 0x00
                   <- interrupt: flags becomes 0x02
orr  r1, r1, #1    ; r1 = 0x01
str  r1, [r0]      ; flags = 0x01, bit 1 from the ISR is lost

The window is only two instructions wide, so the bug may appear once a day. Testing will not find it reliably; it has to be designed out.

How Bit-Banding Works

On Cortex-M3 and M4 parts that implement it, two 1 MB regions are bit-band regions: SRAM from 0x20000000 to 0x200FFFFF and peripherals from 0x40000000 to 0x400FFFFF. Each bit in these regions is mirrored as a full 32-bit word in an alias region. Writing 1 or 0 to the alias word sets or clears exactly that bit, and the bus performs the read-modify-write as one locked operation that cannot be interrupted. Reading the alias word returns 0 or 1.

alias_address = alias_base + (byte_offset x 32) + (bit_number x 4)

SRAM:        region base 0x20000000, alias base 0x22000000
Peripheral:  region base 0x40000000, alias base 0x42000000

Example: bit 3 of the word at 0x20000100
byte_offset = 0x20000100 - 0x20000000 = 0x100
alias = 0x22000000 + 0x100 x 32 + 3 x 4
      = 0x22000000 + 0x2000 + 0xC = 0x2200200C
CoreBit-banding available
Cortex-M0, M0+No
Cortex-M3, M4Optional, included on most parts (LPC17xx, TM4C123, many STM32)
Cortex-M7No
Cortex-M23, M33 (ARMv8-M)No

Code: Bit-Band Macros and the Alternatives

The program shows the same flag operations done four ways. The section name .ahb_ram must be mapped to the AHB SRAM in your linker script, as shown in the memory map tutorial.

atomic_bits.c: bit-banding, LDREX/STREX, PRIMASK and SET/CLR registers on an LPC1768
#include "LPC17xx.h"

/* ---- 1. Bit-band helpers ---- */
#define BITBAND_SRAM(addr, bit) \
    (*(volatile uint32_t *)(0x22000000u + (((uint32_t)(addr) - 0x20000000u) * 32u) + ((bit) * 4u)))

#define BITBAND_PERI(addr, bit) \
    (*(volatile uint32_t *)(0x42000000u + (((uint32_t)(addr) - 0x40000000u) * 32u) + ((bit) * 4u)))

/* Shared flag word placed in AHB SRAM (bit-band capable on LPC17xx) */
__attribute__((section(".ahb_ram")))
volatile uint32_t flags;

#define FLAG_TX_REQ   BITBAND_SRAM(&flags, 0)
#define FLAG_RX_READY BITBAND_SRAM(&flags, 1)

/* ---- 2. LDREX/STREX atomic OR, works on any ARMv7-M core ---- */
static void atomic_or(volatile uint32_t *p, uint32_t mask)
{
    uint32_t v;
    do {
        v = __LDREXW(p);                 /* load and open exclusive monitor */
    } while (__STREXW(v | mask, p));     /* returns 0 on success, retry otherwise */
}

/* ---- 3. Critical section with PRIMASK, works on every Cortex-M ---- */
static void locked_clear(volatile uint32_t *p, uint32_t mask)
{
    uint32_t primask = __get_PRIMASK();  /* remember previous state */
    __disable_irq();
    *p &= ~mask;
    __set_PRIMASK(primask);              /* restore, safe when nested */
}

volatile uint32_t counter;               /* any RAM, no bit-band needed */

void UART0_IRQHandler(void)
{
    (void)LPC_UART0->RBR;                /* read byte, clears the interrupt */
    FLAG_RX_READY = 1;                   /* atomic set of bit 1 */
}

int main(void)
{
    LPC_GPIO1->FIODIR |= (1u << 18);

    FLAG_TX_REQ = 1;                     /* 1. bit-band: atomic set of bit 0 */
    atomic_or(&counter, 0x10u);          /* 2. exclusive access */
    locked_clear(&counter, 0x10u);       /* 3. interrupts briefly off */

    while (1) {
        if (FLAG_RX_READY) {             /* reads 0 or 1 */
            FLAG_RX_READY = 0;           /* atomic clear */
            /* ---- 4. SET/CLR registers are atomic by design ---- */
            LPC_GPIO1->FIOSET = (1u << 18);
        }
    }
}

Choosing the Right Method

MethodCoresCostBest for
Bit-band aliasM3, M4 (when implemented)One store, no retriesSingle-bit flags in SRAM or peripheral registers
LDREX / STREXM3, M4, M7, ARMv8-M MainlineLoop that may retryCounters, multi-bit updates, lock-free queues
PRIMASK critical sectionAll Cortex-MAdds interrupt latencyShort multi-word updates, M0 projects
Peripheral SET/CLR registersDepends on peripheralOne storeGPIO outputs and interrupt enables

The exclusive monitor used by LDREX and STREX is cleared automatically on exception entry and return. If an interrupt runs between the two instructions, STREX fails and returns 1, and the loop simply tries again with a fresh value. This makes it safe without disabling interrupts.

Checking the Alias Address in the Debugger

Before relying on the macro, verify it once. Suppose the linker placed flags at 0x2007C000. Bit 1 then has the alias 0x22000000 + 0x7C000 x 32 + 1 x 4, which is 0x22F80004. Open a memory window at that address, write 1 there, and watch flags change from 0 to 2. Write 0 and it returns to 0. If the value does not change, the variable is not in a bit-band region or the chip does not implement bit-banding, and you should fall back to one of the other methods.

Pitfalls

  • Bit-banding a peripheral register that has write-1-to-clear bits: the hardware read-modify-write writes back all other bits, which may clear status flags you did not intend to touch.
  • Applying the SRAM macro to a variable outside the bit-band region; the alias address points to unrelated memory or causes a bus fault.
  • Calling disable_irq and enable_irq unconditionally inside a function that may already run with interrupts disabled; save and restore PRIMASK instead.
  • Using bit-band code on a Cortex-M7 or M0 port of the project; the alias region does not exist and writes fault.
  • Forgetting volatile on shared variables, so the compiler keeps a stale copy in a register.

Summary

Read-modify-write sequences on shared data can lose updates when an interrupt intervenes. On Cortex-M3 and M4, bit-banding maps each bit of the first 1 MB of SRAM and peripheral space to its own alias word, making single-bit changes atomic. For wider updates use LDREX and STREX, on Cortex-M0 use a short PRIMASK critical section, and for GPIO prefer dedicated SET and CLR registers.