DMA Configuration Bugs and Cache Coherency on Cortex-M7
Cortex-M7 cache incoherence breaks DMA transfers when porting from M4.

DMA controllers and the Cortex-M7's data cache read and write the same physical memory through two completely separate paths, and nothing in the silicon keeps those paths in sync automatically. That single fact explains most of the mystifying DMA bugs that show up when firmware teams port code from a Cortex-M4 project to an M7-based part like the STM32F7 or STM32H7. The bugs look random, showing up as an ADC buffer that never updates, a UART transmission that sends garbage, or a corrupted variable three fields away from the actual DMA buffer. They are the predictable output of a non-coherent memory subsystem meeting code that was written as if coherency were guaranteed. They are the predictable output of a non-coherent memory subsystem meeting code that was written as if coherency were guaranteed.
The Cortex-M4 never had to think about this because it had no data cache to begin with. The M7 is a different animal: a Harvard architecture core with separate instruction and data buses, and an optional L1 cache on both sides, up to 16 Kbytes total across instructions and data on STM32F7 and STM32H7 parts. That cache is fast, and it's genuinely necessary to get the M7's clock speed advantage out of AXI SRAM. But it introduces a second bus master problem the M4 never had: the DMA controller writes and reads SRAM directly, while the CPU reads and writes through cache, and the two views of memory can disagree.
Making it worse, the M7's memory map isn't uniform. There are five distinct interfaces, AXIM, ITCM, DTCM, AHBS, and AHBP, and cacheability on each depends entirely on how the MPU is configured. Some regions can be cached, some can't be reached by DMA at all, and the defaults generated by tools like STM32CubeMX don't necessarily line up with what a given peripheral needs. On top of that, STM32H7 devices ship with caches disabled after reset. Turning them on is often step one in a checklist that nobody reads carefully, and it's also step one toward every bug in this piece.
The RX-path failure: how stale cache lines hide fresh DMA data from the CPU
Set the D-Cache policy to write-back with read and write-allocate (WB-RWA), which is the common default, and have the CPU read a DMA receive buffer at least once before the DMA transfer happens. That read pulls the buffer's addresses into cache lines. Now the DMA controller writes fresh data into SRAM at those same addresses. The CPU comes back to read the buffer again, and the cache, doing what it's designed to do, serves up the old value from its own lines rather than going back out to SRAM. The engineer staring at the debugger sees ADC readings that never change, or UART receive data that's stuck on the first packet.
Microchip's application note TB3295 lays the mechanism out: a transmit buffer sitting in cache holds the string 'ABCDEFGH', while SRAM underneath it has already been overwritten with '12345678'. The DMA wrote the new data correctly. The cache simply was never told to forget the old data, so it keeps answering with the ghost of a previous transfer.
The reason this bug is invisible on Cortex-M4 isn't luck, it's architecture. With no data cache, every CPU read on the M4 goes straight to SRAM, so there's no stale intermediary to consult. Porting the same driver to an M7 and enabling the cache makes the bug appear without anyone having touched the DMA configuration. That's precisely why it blindsides teams: the code didn't change, the memory system did.
The TX-path failure: CPU writes that DMA never sees
The transmit side has a mirror-image problem. Under the same WB-RWA policy, the CPU writes new data into a transmit buffer ahead of a DMA-to-peripheral transfer, whether that's SPI, UART, or something else. Write-back cache doesn't push that write out to SRAM immediately. It marks the cache line dirty and holds onto it, planning to flush it out to memory later, whenever it's convenient for the cache controller. The DMA engine, meanwhile, has no visibility into the cache. It reads straight from SRAM, and SRAM still holds whatever was there before, possibly the previous transfer's payload, possibly uninitialized memory.
A ChibiOS forum discussion on this exact failure states it about as cleanly as it can be stated: writing to RAM does not necessarily update the memory location, just the cache, and the RAM can be written later in time. That's the whole bug in one sentence.
Write-back caching exists to save bus cycles. It's a legitimate performance feature, and disabling it wholesale would waste a real part of the M7's speed advantage. The trouble starts the moment a second bus master, the DMA controller, is allowed to touch the same addresses without being told what the cache is holding back. The cache's laziness is intentional. It just isn't DMA-aware, and nothing in the hardware makes it DMA-aware for you.
The speculative-read hazard that breaks a correctly sequenced invalidation
Adding cache maintenance calls doesn't automatically fix things either. A single invalidation issued after a DMA transfer completes is not enough to guarantee correctness on the RX path, and this is where a lot of otherwise careful engineering still goes wrong.
The Cortex-M7 can issue speculative reads, pulling cache lines into the D-Cache in anticipation of a CPU access that hasn't happened yet. If that speculative fill hits the DMA buffer region while a transfer is actively in progress, the line it loads reflects whatever was in SRAM before the DMA wrote to it, not the incoming data. Invalidating only after the transfer finishes does nothing to prevent that stale, speculatively-loaded line from having been captured mid-transfer.
The fix is a two-step protocol, not a one-step fix: invalidate before the DMA transfer starts, and invalidate again after it completes. The pre-transfer invalidation clears out any dirty lines so the cache can't write stale data back over the buffer during the transfer window. The post-transfer invalidation discards anything the CPU speculatively pulled in while the DMA was running. Skipping either half leaves the RX buffer vulnerable to corruption from speculatively-loaded stale lines captured during the transfer window. The pre-transfer step is the one most commonly omitted from driver implementations.
Cache-line alignment: the silent corruption that taints neighboring variables
The Cortex-M7's D-Cache line size is 32 bytes, and CMSIS exposes that value directly as __SCB_DCACHE_LINE_SIZE. Cache maintenance operations, clean and invalidate alike, don't operate on individual bytes. They operate on whole 32-byte lines. If a DMA buffer starts at an address that isn't a multiple of 32, an invalidate or clean call issued against that buffer reaches backward or forward into the neighboring cache line, and that neighboring line may hold a completely unrelated variable.
TB3195 states this without qualification: even when the DMA itself is configured to move some number of bytes that isn't a clean multiple of 32, the buffer declared in code still needs to be sized to a full multiple of 32 bytes, or the maintenance operation will corrupt whatever sits next to it in memory. A discussion on the ST community forum gives a concrete case: a buffer intended to hold 67 bytes of real data has to be declared as 96 bytes, since 96 is the next multiple of 32 above 67. Those extra 29 bytes are structural padding that keeps the cache maintenance call from reaching into someone else's variable. They're structural padding that keeps the cache maintenance call from reaching into someone else's variable.
This bug survives code review, because the buffer size looks reasonable to anyone not specifically checking it against 32-byte boundaries.
The DTCM placement bug on STM32H7: DMA buffers in memory DMA cannot reach
DMA1 and DMA2 on the STM32H7 sit in the D2 domain, and that domain has no path into ITCM or DTCM RAM, the tightly-coupled memory at 0x20000000. No one can flip this setting. It's a hard constraint of the silicon layout.
The trap is that Buffer declarations can easily end up placed in DTCM through common project configurations and example code. That means a freshly generated project, built exactly as the tool suggests, can be structurally incapable of running its own DMA transfers. The symptom isn't a compile error or an obvious fault. The transfer just fails silently or produces garbage, and because the buffer placement looks like a normal variable declaration, nobody thinks to question it. No amount of cache maintenance fixes this, because the problem isn't coherency, it's reachability: the DMA controller physically cannot address that memory.
The regions that do work on STM32H7 are AXI SRAM at 0x24000000, which every listed peripheral can reach, and SRAM1 through SRAM3, reachable by SPI1 through SPI5, the UARTs, I2C1 through I2C3, ADC12, and SDMMC (SDMMC1 has no direct path into the D2 domain and needs AXI SRAM instead). SRAM4 is reachable by SPI1 through SPI5 and a range of other D1 and D2 peripherals, again excluding SDMMC. ITCM and DTCM, meanwhile, are reachable by no DMA controller. Get the buffer placement wrong, and everything downstream, cache policy, alignment, maintenance calls, is irrelevant.
Why write-through cache policy is not a safe shortcut
Write-through looks like an elegant way to sidestep the whole problem. Every CPU write updates SRAM and the cache line at the same time, so in theory the DMA engine always sees current data without any explicit maintenance call in the driver.
It solves exactly half the problem. Write-through addresses the CPU-writes-to-RAM case, the TX path, and does nothing for the RX case, where the DMA writes to SRAM and the CPU's cache is still holding an old line. Write-through mode only works for CPU writing to RAM, not for DMA writing to RAM. A design that relies on write-through alone still has the exact stale-read bug described at the top of this piece, just on one side of the transfer instead of both.
There's a second reason to avoid leaning on it: documented silicon errata for the Cortex-M7 core covers the write-through cache configuration specifically. That erratum touches STM32H74x and STM32H75x parts. On affected silicon, using write-through can produce failures that look exactly like the coherency bugs it was supposed to eliminate, which makes debugging genuinely harder: the fix and the failure mode become indistinguishable from the outside.
ECC and the initialization hazard: why cache maintenance order at startup matters
Immediately after reset, the cache's internal SRAM holds undefined data, not zeros, not anything predictable. Calling SCB_CleanDCache before the cache has been properly initialized and enabled can trigger an ECC error on that undefined content, and the result is a HardFault before the application has done anything with DMA.
The safe order is to invalidate first: call SCB_InvalidateDCache, which marks every line invalid and removes the undefined content from consideration, and only then enable the cache. Reversing that order, cleaning before invalidating, lets the ECC logic flag an uncorrectable error on data that was never valid to begin with.
Diagnosing this after the fact is unusually hard, because the Cortex-M7 doesn't expose cache ECC errors through a dedicated bit in the Configurable Fault Status Register. An uncorrectable ECC error appears in the log as a generic BusFault entry, with the actual specifics not surfaced directly in the fault status register. A HardFault handler triggered this way gives no immediate indication that cache ECC, rather than some other bus fault, was the cause. This section matters for exactly that reason: a driver that's otherwise correct on every DMA point above can still fail on the very first line of startup code, for a reason that has nothing to do with DMA and everything to do with initialization order, and that failure will derail debugging long before the DMA logic is ever exercised.
Solution path one: MPU non-cacheable regions
One durable fix removes cacheability from the equation. Use the MPU to mark a dedicated RAM region as non-cacheable, place every DMA buffer inside that region, and the stale line has nowhere to occur, because the CPU is now reading and writing SRAM directly for those addresses, same as the DMA controller does.
On STM32F7 and STM32H7 parts, the way to get there is to configure the MPU region's attribute as Shareable. That's the specific attribute that deactivates the D-Cache for the region on these devices, and it's worth being precise about that, since "non-cacheable" isn't a single MPU checkbox on these parts, it's a consequence of the Shareable setting.
The appeal of this route is that it's invisible to application code. No clean or invalidate calls need to appear anywhere in the driver, which makes porting a driver written for a cache-less MCU onto an M7 part far more direct: the buffer just lives somewhere the cache never touches. The implementation pattern is straightforward: define a custom linker section, something like .NoCacheDMA, mapped onto the chosen RAM range; configure an MPU region over that same range with the Shareable attribute; and tag every DMA buffer declaration with __attribute__((section(".NoCacheDMA"))) so the linker actually places it there.
Solution path two: explicit cache maintenance with CMSIS APIs, applied correctly
The alternative keeps the cache active everywhere, including on DMA buffers, and handles coherency explicitly through two CMSIS calls. SCB_CleanDCache_by_Addr(addr, size) flushes dirty lines out to SRAM ahead of a DMA transmit, so the DMA controller reads what the CPU actually wrote rather than stale SRAM content. SCB_InvalidateDCache_by_Addr(addr, size) discards lines that may be stale or speculatively loaded, and per the earlier section on speculative reads, it needs to be called both before and after a DMA receive transfer, not just after.
Both calls carry the same two non-negotiable constraints, tied directly back to the alignment requirement above: addr has to sit on a 32-byte cache-line boundary, and size has to be a multiple of 32 bytes. Violate either one, and the call reaches into a neighboring cache line and corrupts whatever variable happens to live there, exactly as described earlier in this piece.
The complete transmit protocol follows from all of this directly: the CPU populates the transmit buffer, the driver calls SCB_CleanDCache_by_Addr(txBuf, txSize) to push those writes out to SRAM, and only then does the DMA transfer start. Getting the buffer placement, alignment, and call order right turns the coherency bug class described across this piece into a solved, checkable property of the code.
Sources
- Maintaining CPU data cache coherence for DMA buffers | Community
- Managing Cache Coherency on Cortex-M7 Based MCUs
- Handling Cache Coherency Issues at Runtime Using Cache Maintenance Operations on Cortex-M7 MCUs Using MPLAB Harmony v3
- Cortex-M7 platform FatFS Cache coherency - ChibiOS Free Embedded RTOS
- community.st.com
- community.arm.com
- st.com
- community.st.com

