In short: Memory barriers (DMB, DSB, ISB on Arm) force an ordering guarantee that neither the compiler nor the CPU's out-of-order pipeline will otherwise give you — use them whenever the correctness of your code depends on one memory access actually completing, and being observed, before another.
Every embedded engineer eventually hits a bug that cannot be reproduced under the debugger, vanishes when optimisation is turned down, and reappears the moment real traffic hits the device. A large share of these are ordering bugs: the program wrote the code in one order, but the hardware executed or observed it in another. Memory barriers are the tool that closes that gap. This article explains the two independent reordering sources, the three Arm barrier instructions, and the concrete situations where you genuinely need them.
Two reorderings, not one
There are two distinct actors that can rearrange your memory accesses, and they are easy to confuse:
- The compiler reorders at build time. If it cannot see a dependency, it will freely move loads and stores, cache values in registers, or delete "redundant" accesses. This is what
volatileand compiler memory clobbers address — see our deep dive on the volatile keyword in embedded C. - The CPU reorders at run time. A Cortex-M7, Cortex-A53 or any modern core issues loads and stores speculatively, buffers writes, and allows accesses to complete out of program order.
volatiledoes nothing here — it is purely a compiler construct.
Key fact: volatile stops the compiler from reordering, but it emits no barrier instruction. On a Cortex-A class core with a write buffer, two volatile stores can still reach memory in the opposite order. You need a real barrier for that.
The three Arm barriers
| Instruction | Guarantees | Typical use |
|---|---|---|
DMB (Data Memory Barrier) | All memory accesses before it are observed before any after it. It does not wait for them to complete. | Ordering two data accesses, e.g. payload then flag. |
DSB (Data Synchronization Barrier) | Stronger: execution stalls until all prior memory accesses have actually completed. | Before enabling an MPU region, after a cache maintenance op, before WFI. |
ISB (Instruction Synchronization Barrier) | Flushes the pipeline so instructions after it are re-fetched; context-changing effects take hold. | After writing a system control register (e.g. SCB, VTOR) or self-modifying code. |
A useful mental model: DMB orders the appearance of data, DSB orders the completion of data, and ISB orders the fetching of instructions. The CMSIS intrinsics __DMB(), __DSB() and __ISB() are the portable way to emit them from C without inline assembly.
When you actually need a barrier
- Lock-free producer/consumer handoff. Write the buffer,
DMB, then set the ready flag. The reader checks the flag,DMB, then reads the buffer. Skipping either barrier lets the flag overtake the data. - Memory-mapped peripheral sequencing. Clearing an interrupt in a peripheral then returning from the ISR: issue
DSBso the clear reaches the device before theWFIor the pipeline re-enables interrupts — otherwise a spurious re-entry fires. - System register changes. After relocating the vector table via
SCB->VTOR, or changing the MPU/MMU configuration, anISBis required for the change to take effect before subsequent instructions. - Cache + DMA coherency. On a Cortex-M7 with D-cache, clean the cache (
DSB) before starting a DMA read, and invalidate before a CPU read of DMA-written data.
Key fact: On single-core Cortex-M0/M0+/M3 with the default memory map, Device and Strongly-Ordered regions are already ordered by the architecture, so peripheral register sequences often work without DMB. The barrier becomes mandatory on cores with write buffers, caches, or multiple observers (A-class, M7, SMP).
Barriers vs atomics vs volatile
These three solve overlapping but different problems. volatile forces the compiler to actually perform each access. Atomics (C11 ) give you both compiler and CPU ordering through acquire/release semantics, compiling down to the right DMB automatically. Raw barriers are the lowest level and are what you reach for in driver code, RTOS context switches, and bare-metal MMIO where you want explicit control. Static analysis flows from Synopsys Coverity or Siemens Polyspace can flag some missing-ordering patterns, but ordering correctness ultimately needs human reasoning about the hardware model.
A worked handoff
Producer: sensor_buf[i] = sample; then __DMB(); then data_ready = 1;. Consumer: if (data_ready) { __DMB(); process(sensor_buf); }. The two __DMB() calls form a release/acquire pair. Remove the producer's barrier and, on an M7, the write buffer can flush data_ready first; the consumer then reads a half-written sample. The bug surfaces maybe once per million iterations — exactly the kind of field-only failure that costs weeks. If you are building production firmware and want this reviewed by engineers who do it daily, Avecas design services runs embedded and SoC validation flows end to end.
Want to master concurrency, caches and bare-metal driver design hands-on? Explore the CourseTron Embedded Systems track — with lab exercises built around real Cortex-M and Cortex-A targets.
FAQ
Is volatile enough for multithreaded flags?
No. volatile only controls the compiler. On any core that reorders memory or buffers writes you must add a barrier or use C11 atomics; otherwise the flag and its payload can be observed out of order.
What is the difference between DMB and DSB?
DMB only guarantees the observed order of memory accesses and lets the pipeline continue. DSB actually stalls the core until prior accesses complete, which is what you need before cache operations, MPU changes, or low-power entry.
Do I need ISB after every system register write?
Only when a subsequent instruction depends on the new state — vector table relocation, MPU/MMU reconfiguration, or enabling the FPU. For most peripheral register writes an ISB is unnecessary.
Can I just use C11 atomics instead of raw barriers?
Often yes. Acquire/release atomics compile to the correct barriers and are more portable. Raw __DMB()/__DSB()/__ISB() remain preferable for MMIO, cache maintenance, and hand-tuned RTOS code where you want explicit control.
For a related compiler-side companion to this topic, also read cache coherency and DMA on the Cortex-M7.