In short: In embedded systems a cache trades a few bytes of fast SRAM for an order-of-magnitude speed-up, but the moment a DMA engine or a second master touches the same memory, firmware must issue explicit clean and invalidate operations to keep the cached copy and DRAM coherent.
A modern embedded SoC runs its CPU at 1 GHz while its external DDR responds in 60–100 ns. Without a cache, every instruction fetch and every load would stall the pipeline for 60–100 cycles. The cache exists to hide that gap, and understanding how it behaves under DMA, interrupts, and multi-core traffic separates firmware that "works on my board" from firmware that ships. This article walks through the instruction/data split, what breaks when a DMA engine bypasses the cache, and the exact maintenance operations you must get right.
Why two caches: the I/D split
Almost every embedded core above the Cortex-M0 level uses a Harvard-style L1 with separate instruction (I-cache) and data (D-cache) ports. The reason is bandwidth: in a single cycle the pipeline wants to fetch the next instruction and complete a load/store. One unified port would serialize those. Splitting them lets the fetch unit and the load/store unit each hit their own single-ported SRAM in parallel.
The two caches have different personalities. The I-cache is effectively read-only during normal execution, so it never needs a write-back policy. The D-cache, however, must decide what to do on a store, and that policy choice is where coherency bugs are born.
On an Arm Cortex-A53, the L1 I-cache and D-cache are each 32 KB, 2-way/4-way set-associative with 64-byte lines. A single line therefore holds 16 consecutive 32-bit words — a fact that matters enormously for DMA alignment.
Write-through vs write-back
A write-through D-cache pushes every store to both the cache and main memory immediately. Memory is always current, which simplifies coherency, but it saturates the write bus. A write-back cache marks the line "dirty" and defers the memory write until the line is evicted. This is far more efficient — a tight loop updating a counter touches DRAM once instead of a million times — but it means DRAM can hold stale data for an unbounded period. Write-back is the default on performance cores precisely because it is faster, and precisely why DMA coherency is a problem.
The DMA coherency problem
Consider a UART or Ethernet peripheral filling a receive buffer via DMA. The sequence that bites every embedded engineer at least once:
- Firmware reads
rx_buf[0]earlier, so a line is cached. - The DMA engine writes fresh packet bytes straight into DRAM, bypassing the D-cache.
- Firmware reads
rx_buf[0]again — the cache still holds the old value and returns stale data.
The reverse hazard exists on the transmit path: firmware builds a packet in a write-back buffer, the bytes sit "dirty" in cache, the DMA engine reads DRAM and sends garbage because the real data never made it out of L1. These are not rare edge cases — they are the default behaviour of a non-coherent system, which describes most microcontrollers and many application processors when peripherals are not on a coherent interconnect.
See our companion piece on DMA in embedded systems for how descriptors and channels are set up; here we focus on keeping that transferred data correct. The related topic of memory barriers and ordering covers the DMB/DSB instructions you often pair with maintenance ops.
Cache maintenance operations
Three primitives solve the problem, each addressing a different direction of staleness:
| Operation | Effect | Use when |
|---|---|---|
| Clean (flush) | Writes dirty lines back to DRAM; line stays valid | Before a DMA reads a buffer the CPU wrote (TX) |
| Invalidate | Discards cached copy; next access re-fetches from DRAM | Before the CPU reads a buffer DMA wrote (RX) |
| Clean & Invalidate | Both, atomically per line | Bidirectional buffers, or safe default |
On Arm, these map to CP15/system-register ops addressed by virtual address and line — for example a driver loops DC CIVAC (clean & invalidate by VA to point of coherency) across the buffer, 64 bytes at a time, then issues a DSB to ensure completion. The RISC-V equivalent uses cbo.clean/cbo.inval/cbo.flush from the Zicbom extension.
Line granularity is a trap: invalidation acts on whole 64-byte lines. If a 20-byte DMA buffer shares a line with an unrelated variable, invalidating the buffer also discards that variable's dirty cache copy — silently corrupting it. Always align and pad DMA buffers to the cache-line size (__attribute__((aligned(64)))).
A buffer in motion
In debugging practice, tools matter. Siemens Questa and Synopsys VCS let you co-simulate the cache-coherent interconnect against the RTL; Arm's cycle-models and Synopsys Platform Architect expose cache hit/miss counters so you can prove a driver actually flushed before the DMA start bit was set. For silicon bring-up, a logic analyzer on the DDR bus plus the core's performance-monitoring unit (PMU) counting L1D_CACHE_REFILL events usually pinpoints a missing maintenance op in minutes.
Hardware coherency and when to skip maintenance
Higher-end SoCs add a coherent interconnect (Arm CCI/CMN, or ACE/CHI-capable masters) so that DMA-capable peripherals snoop the caches directly. On such systems you can map a buffer as "cacheable, coherent" and drop the maintenance calls entirely — the hardware keeps L1, L2, and peripheral views in sync. The catch is that not every master is coherent; a GPU might be while a crypto accelerator is not, so you must read the SoC's interconnect topology rather than assume. When hardware coherency is unavailable, the pragmatic alternative is to allocate DMA buffers as non-cacheable memory (via the MMU/MPU), paying a throughput penalty but removing an entire class of bugs. Teams building production drivers often validate these decisions with a design-services partner such as Avecas VLSI design services before tape-out.
FAQ
Do I need cache maintenance on a Cortex-M4?
The classic M0/M3/M4 cores have no data cache, so no — memory is always coherent with the CPU view. The Cortex-M7 and M55, however, add optional I/D caches, and the moment you enable the M7 D-cache you inherit every DMA coherency concern described above.
Should I clean or invalidate before starting a DMA receive?
Invalidate the buffer region before the DMA writes, and invalidate again after completion before the CPU reads. Cleaning first is also safe and avoids a subtle race where a dirty line is evicted on top of DMA-written data mid-transfer.
Why does my buffer get corrupted even though I invalidated it?
Almost always cache-line aliasing: the buffer is not 64-byte aligned, so invalidation touches a neighbouring variable's line. Align and pad every DMA buffer to the line size.
Is write-through a safe way to avoid all this?
It fixes the CPU-writes-DMA-reads direction because DRAM is always current, but it does nothing for the DMA-writes-CPU-reads direction — you still must invalidate. And it costs significant write bandwidth, so it is rarely the right global policy.
Want to master cache, DMA, and the full memory subsystem with hands-on labs on real SoCs? Explore the CourseTron Embedded Systems & VLSI courses, or sharpen your silicon fundamentals with structured training at ChipXpert.