STM32H7: Cache Coherency With DMA — The Errata Chapter Nobody Reads
D-cache on the H7 will silently corrupt your DMA buffers unless you align and flush correctly. Here is exactly where I got burned.

D-cache on the H7 will silently corrupt your DMA buffers unless you align and flush correctly. Here is exactly where I got burned — not in the RM's "cache maintenance" chapter, but in a production ADC pipeline that passed unit tests and failed EMI soak.
Problem
STM32H743, Cortex-M7 with D-cache enabled (default after reset if you turn it on in SystemInit). DMA1 moves SPI RX data into uint32_t rx_buf[4096] in SRAM D2. CPU reads the buffer in the main loop. Occasionally — roughly 1 in 10⁴ samples — words were stale or duplicated.
No HardFault. No DMA error flags. Just wrong data that looked like sensor noise until we correlated it with cache line boundaries.
Why the tutorials skip this
Most STM32 examples disable D-cache or mark all SRAM non-cacheable. That works until you need performance elsewhere and re-enable caching. The H7's split memory map (D1/D2/D3, ITCM/DTCM) adds another layer: where the buffer lives matters as much as whether you flush.
Relevant errata and RM notes (STM32H743/753 Rev V, ES0396): DMA masters see physical memory; CPU with D-cache enabled sees cached copies. Without maintenance, they diverge.
What we had wrong
// .bss in SRAM D2 — linker default
__attribute__((aligned(32))) uint32_t rx_buf[4096];
// D-cache ON, MPU default: cacheable
SCB_EnableDCache();
HAL_SPI_Receive_DMA(&hspi1, (uint8_t*)rx_buf, sizeof(rx_buf));
// main loop reads rx_buf[i] — sometimes stale
Three separate mistakes:
- Alignment: Cortex-M7 cache lines are 32 bytes. Buffer start was aligned, but our consumer stride sometimes read across lines without invalidating.
- No invalidate after DMA RX complete: CPU must
SCB_InvalidateDCache_by_Addr()before reading DMA-written memory. - No clean before DMA TX: opposite direction — CPU writes TX buffer, DMA reads physical RAM before lines write back.
We had only fixed (2) in the TC interrupt, not on HT half-buffer swaps. Ping-pong lost every other half.
Fix options (with tradeoffs)
Option A: MPU non-cacheable region
Map the DMA buffer section as TEX=0, C=0, B=1 (non-cacheable) via MPU:
MPU_InitStruct.BaseAddress = (uint32_t)&rx_buf;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
Pros: Simple mental model; no flush/invalidate in hot path.
Cons: Slower CPU access to that RAM; must carve dedicated linker section; easy to accidentally place stack or other data in the same region.
We use this for all buffers touched by DMA on current projects.
Option B: Cache maintenance by address
ST's CMSIS helpers:
// After DMA writes, before CPU reads:
SCB_InvalidateDCache_by_Addr((void*)rx_buf, sizeof(rx_buf));
// Before DMA reads CPU-prepared TX data:
SCB_CleanDCache_by_Addr((void*)tx_buf, sizeof(tx_buf));
Pros: Keeps cache for non-DMA code paths.
Cons: Address range must be 32-byte aligned and size rounded up to line size; easy to get wrong on HT/TC splits; costs cycles in ISR if not careful.
Related pain documented in STM32H7 DMA double-buffer SPI drift — ISR timing matters when you add maintenance ops.
Option C: DTCM
Place buffers in DTCM at 0x20000000. CPU access is fast; but not all DMA controllers can reach DTCM on all H7 variants. On H743, check DMAMUX routing — SPI RX via DMA1 to DTCM worked; MDMA to certain peripherals did not.
Read the reference manual table for your exact part before betting the product on DTCM placement.
Verification method
- Fill buffer with a known pattern via CPU; clean; DMA TX to loopback SPI; scope MISO/MOSI.
- DMA RX into buffer; invalidate; compare word-by-word.
- Run with D-cache on/off — diff should be zero after fix, non-zero before.
For Rust/no_std projects, the same rules apply — see Rust no_std DMA notes for ownership patterns that force you to think about 'static mut buffer placement in linker scripts.
Errata chapter nobody reads
ES0396 documents additional cases around ART accelerator, OTFDEC, and dual-bank flash interactions. None caused our bug, but one entry flagged bufferable Shareable MPU attributes interacting badly with Ethernet DMA. If you're running lwIP + DMA on H7, read ES0396 section 2.7 before your first field trial.
Production policy we adopted
| Buffer type | Placement | Cache policy |
|---|---|---|
| DMA RX/TX rings | .dma_nc linker section in D2 | MPU non-cacheable |
| Hot CPU working set | DTCM or D1 | Cacheable |
| Display framebuffers | SDRAM via FMC | Non-cacheable or write-through + explicit sync |
Document the linker snippet in the board support package README. Future-you will enable -O3 and forget.
Linker section we ship
.dma_nc (NOLOAD) :
{
. = ALIGN(32);
*(.dma_nc .dma_nc.*)
} > RAM_D2
Application buffers:
__attribute__((section(".dma_nc"), aligned(32)))
uint8_t spi_dma_rx[8192];
MPU region covers .dma_nc start/end from linker symbols __dma_nc_start__ / __dma_nc_end__. One region, all DMA buffers — no per-buffer MPU math errors.
When invalidate beats non-cacheable
Non-cacheable is simpler but costs ~5–15% CPU on tight memcpy loops reading DMA rings. On the radio coprocessor we kept non-cacheable. On the fusion core reading 192 kSPS int32, we switched to cacheable + invalidate on TC only after proving HT path wasn't used — saved 8% CPU at same clock.
Measure before assuming non-cacheable is free.
What I'd do next
Run a CI build target that enables D-cache and runs the invalidate self-test on every PR. The test that passes with cache off only is how this class of bug ships twice.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

