Embedded Systems · Microcontrollers

Bit-Banding: Atomic Single-Bit Access Through an Alias Address Region

How the Cortex-M3 and M4 alias one bit of memory to a whole word, what address arithmetic maps bit to word, and why the feature disappeared from later cores.

Setting a single bit in a shared word is not an atomic operation on any ordinary processor. It takes a read, a modify, and a write, and between the read and the write another context can change a different bit in the same word. The usual answers are a critical section, an atomic instruction such as compare-and-swap, or hardware that performs the read-modify-write without the core’s involvement. Bit-banding is the third answer, and it works by giving each bit of a region its own address.

The alias mapping

The Cortex-M3 and Cortex-M4 implement two 1 MiB bit-band regions: one at 0x2000_0000 covering the low 1 MiB of SRAM, and one at 0x4000_0000 covering the low 1 MiB of peripheral space. Each is shadowed by a 32 MiB alias region — 0x2200_0000 for the SRAM region and 0x4200_0000 for the peripheral region — in which every thirty-two-bit word corresponds to a single bit of the original.

The ratio is thirty-two to one because each bit needs a byte-aligned, word-sized alias. One byte of the source region holds eight bits, which need 8 × 4 = 32 bytes of alias. One megabyte of source therefore needs thirty-two megabytes of alias, and the alias region’s size is a direct consequence of the ratio.

The address arithmetic is:

alias_address = alias_base
              + (byte_offset_from_bitband_base * 32)
              + (bit_number * 4)

The multiplication by thirty-two converts a byte offset into a word offset, and the multiplication by four converts a bit index into a byte offset within the alias. Both are shifts: (offset << 5) + (bit << 2).

A word at the alias address reads as 0x0000_0001 or 0x0000_0000, reflecting the single bit’s state. Only bit 0 of the written value reaches the target: a write whose bit 0 is set sets the bit, and a write whose bit 0 is clear clears it, so writing 0x0000_00FF has the same effect as writing 0x0000_0001 and 0x0000_000E has the same effect as 0x0000_0000. Bits 31:1 are discarded. The alias is not a normal memory location that stores what it is given; it is a command to the bus.

What makes it atomic

The write to the alias address is performed by the bus interface, not by the core. The core issues a single store of a word, and the memory system resolves it into a read-modify-write on the underlying word in the bit-band region, with the modification confined to the addressed bit.

Because the core issues one store, there is no window in which a second context can observe an intermediate value. An interrupt that arrives between two instructions cannot intervene, because there are not two instructions. A higher-priority handler that touches a different bit of the same word cannot lose an update, because the underlying read-modify-write happens in the bus and is not composed of the core’s separate accesses.

The atomicity is with respect to other accesses that also go through the same memory system, which on a single-core part means all of them. It is not atomicity against a DMA engine that writes the underlying word directly: a DMA transfer to the bit-band region’s original address bypasses the alias and can race with the bus’s read-modify-write. The hardware guarantees single-bit atomicity only for accesses that use the alias, and a design that mixes both forms on the same word has a race that no amount of instruction-level reasoning will fix.

There is a second, subtler limit. The alias store is atomic with respect to the word it modifies, not with respect to a group of bits. Two bits in the same word must be set by two separate alias stores, and between them the word is in an intermediate state. Code that must change two bits together — a pair of configuration bits that are only valid in combination — cannot use bit-banding for the pair. It needs a critical section or a single store to the original word.

The generated address as a constant

The alias address is computed from a compile-time-known bit position, so the compiler folds it. A macro or an inline function taking the original address and the bit index produces a constant expression, and the resulting store is a single STR with a literal address or a small immediate offset.

#include <stdint.h>

#define BITBAND_SRAM_BASE   0x22000000u  /* alias of 0x20000000 */
#define SRAM_BASE           0x20000000u

static volatile uint32_t flags;

static inline volatile uint32_t *bit_alias(volatile uint32_t *addr, unsigned bit)
{
    uintptr_t offset = (uintptr_t)addr - SRAM_BASE;
    return (volatile uint32_t *)(BITBAND_SRAM_BASE + (offset << 5) + (bit << 2));
}

void set_flag(void)
{
    /* One store; the read-modify-write happens in the bus, not in software. */
    *bit_alias(&flags, 5) = 1u;
}

The inline function compiles to address arithmetic that the optimiser resolves when the arguments are constants, which they are in practice: the address of the flag word and the bit number are both known at compile time. The generated code is one store instruction with an immediate address, in place of a load, a bit-field insert or ORR, and a store, plus the critical section that the read-modify-write would otherwise require. The saving is in instructions and in the absence of a critical section, not in bus time: the store is converted into a read-modify-write by the bus, so a bit-band write occupies the bus for two cycles and stalls subsequent accesses until it completes, where a plain word store is buffered and retires in one.

The cost that remains is code size and address space. Each alias address is a distinct thirty-two-bit literal, and a driver that manipulates twenty bits across several registers needs twenty literals. The alias regions consume thirty-two megabytes of the 4 GiB space each, which is not a problem on a part with a fixed map but is a reason the feature is confined to the two 1 MiB windows.

Why the feature did not survive

Bit-banding is implemented on the Cortex-M3 and Cortex-M4 and is not implemented on the Cortex-M0 or M0+, on the Cortex-M7, or on the ARMv8-M cores. The reasons are architectural rather than accidental.

The first is that the alias needs a dedicated address decoder and a read-modify-write path in the bus fabric for each bit-band region. That is silicon area and a timing path in the memory system, and it exists to serve a use case that can be met in other ways. The Cortex-M0 family omits it as part of a design aimed at minimum gate count.

The second is that ordinary load-store instructions can cover the same need without the address-space cost. A read-modify-write can be made safe with an exclusive-access pair that detects interference, and the bit-field insert and clear instructions in the Thumb-2 set make the modify step a single operation on a register. That form of atomicity is at the instruction level rather than the address level, which composes better: it works on any memory the core can reach, not just two fixed windows.

The third is portability. Code written for bit-banding does not compile for a part without it, and a driver that uses the alias addresses has to be rewritten, not merely recompiled, when the part changes. Peripheral libraries that support a family spanning bit-banded and non-bit-banded parts therefore provide a bit-set/bit-reset abstraction, which every part implements somehow — through the alias on the M3 and M4, through a dedicated set/reset register on most vendors’ peripherals, or through a critical section on the parts that have neither.

The set-and-clear registers that replaced it

Most microcontroller peripherals provide their own atomic bit access through write-one-to-set and write-one-to-clear registers, the same arrangement that keeps a GPIO write from needing a read-modify-write in the GPIO configuration article. The GPIO output register, for example, is paired with a set register and a clear register, and a write to either affects only the bits whose mask bits are one.

The mechanism is cheaper than bit-banding because it is local to the peripheral. There is no alias address space, no thirty-two-to-one address expansion, and no read-modify-write in the bus fabric; the peripheral’s register file simply implements the write as a masked set or clear. The atomicity is the same, and the code is a single store to an address the peripheral already occupies.

The limitation is coverage. The set/clear registers exist where a vendor chose to add them, which is usually the GPIO and a few configuration registers, and not for arbitrary words in SRAM. A flag word shared between an interrupt handler and the main loop has no set/clear register, so it needs either a critical section or bit-banding on a part that has it. The pattern that works everywhere is to give each flag its own byte, so that a plain store is atomic and no read-modify-write is needed at all — at the cost of memory, which on a part with a few kilobytes of SRAM is a real cost.

Using the alias for peripheral registers

The peripheral bit-band region covers 0x4000_0000 to 0x400F_FFFF, the low megabyte of the peripheral space. Vendor peripherals are laid out from 0x4000_0000 upward, and most parts place their first peripherals within that window, but a peripheral mapped at or above 0x4010_0000 is outside the bit-band region and cannot be reached through the alias.

This is a boundary that catches people who assume the whole peripheral space is bit-banded. The alias region is 0x4200_0000 to 0x43FF_FFFF, and it covers exactly the low megabyte of the source region; a register at 0x400F_0000 has an alias, and a register at 0x4010_0000 does not. The part’s memory map diagram shows where the bit-band region ends, and the driver’s macro has to respect it.

The other boundary is alignment. The bit-band source address must be word-aligned; the alias arithmetic assumes the byte offset is a multiple of four. A byte-wide peripheral register inside the bit-band region cannot be bit-banded, because the mapping is defined for words. Registers that are bytes are typically status or data registers where a single-bit atomic write is not needed, so the restriction is rarely felt, but it is absolute.

Reading through the alias is occasionally useful and usually not. The read returns 0 or 1, which is convenient for testing a flag without masking, but it costs a bus access to the alias region rather than to the peripheral’s own register, and on a part where the alias path has extra latency the read is slower than a load of the original word followed by a shift. The write is where the value lies, because the write is the operation that would otherwise need three instructions and a critical section.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm Limited
  2. Cortex-M3 Technical Reference Manual (ARM DDI 0337) — Arm Limited
  3. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — Joseph Yiu, Newnes