
Cache coherency between CPU and DMA is one of the most subtle sources of data corruption in Cortex-M7 systems. The CPU sees the world through its L1 cache; DMA sees physical memory directly. When both access the same buffer without explicit maintenance, one of them operates on stale data. This article covers the exact operations, barriers, and patterns you need for correct DMA on Cortex-M7 (STM32H7, NXP RT1170, similar cores).
+----------+ Load/Store +----------+ Bus Master +----------+| CORTEX-M7 | <----------------> | L1 D-CACHE | <----------------> | DMA || CPU CORE | (cached) | (32B lines) | (uncached) | CONTROLLER|+----------+ +----------+ +----------+^ ^| || PHYSICAL RAM |+---------------------------+------------------------------+|Single physical memory
When the CPU writes to a buffer, data lands in D-Cache (write-back policy). DMA reads physical RAM — it sees old data. When DMA writes to a buffer, CPU reads from D-Cache — it sees old data. The fix is explicit cache maintenance via the SCB (System Control Block) registers.
Cortex-M7 provides three D-Cache maintenance operations through the SCB:
// CMSIS-Core (core_cm7.h)void SCB_CleanDCache(void); // Clean entire D-Cachevoid SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize); // Clean by address rangevoid SCB_InvalidateDCache(void); // Invalidate entire D-Cachevoid SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); // Invalidate by address rangevoid SCB_CleanInvalidateDCache(void); // Clean + Invalidate entire D-Cachevoid SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); // By address range
| Operation | Direction | Use Case |
|---|---|---|
| Clean | CPU → RAM | CPU finished writing buffer, DMA will read |
| Invalidate | RAM → CPU | DMA finished writing buffer, CPU will read |
| Clean+Invalidate | Both | Ownership transfer, or buffer reused for opposite direction |
Critical: The *_by_Addr variants operate on address ranges. The size parameter dsize is in bytes, and the address must be 32-byte aligned (cache line size on Cortex-M7). The hardware rounds the range up to full cache lines.
#define CACHE_LINE_SIZE 32U// Align buffer to cache line boundarystatic uint8_t dma_buffer[1024] __attribute__((aligned(32)));void dma_tx_prepare(void *buf, uint32_t len) {// CPU wrote data, DMA will read: CLEANSCB_CleanDCache_by_Addr((uint32_t*)buf, len);__DSB(); __ISB(); // Barriers: ensure clean completes before DMA starts}void dma_rx_complete(void *buf, uint32_t len) {// DMA wrote data, CPU will read: INVALIDATESCB_InvalidateDCache_by_Addr((uint32_t*)buf, len);__DSB(); __ISB(); // Barriers: ensure invalidate completes before CPU reads}
Cache maintenance operations are not memory barriers. They initiate cache maintenance but return before it completes globally. You need:
// After cache maintenance, before DMA starts / CPU reads__DSB(); // Data Synchronization Barrier: completes all explicit memory accesses__ISB(); // Instruction Synchronization Barrier: flushes pipeline, refetches instructions
Timeline without barriers:CPU: SCB_CleanDCache_by_Addr() ----> returns immediatelyDMA: Starts transfer ----> may see partially cleaned cacheTimeline with barriers:CPU: SCB_CleanDCache_by_Addr()CPU: __DSB() -------------------> waits for clean to reach RAMCPU: __ISB() -------------------> pipeline sees new memory stateDMA: Starts transfer ----> sees fully cleaned data
On Cortex-M7, __DSB() ensures the cache maintenance operation has completed to the point of coherency (L2 or main memory). __ISB() ensures subsequent instruction fetches see the updated memory (relevant if code is in the same region, or for self-modifying code scenarios).
// Buffer aligned to cache linestatic uint8_t tx_buf[256] __attribute__((aligned(32)));void uart_dma_send(const uint8_t *data, uint16_t len) {memcpy(tx_buf, data, len);// Clean: push CPU writes to RAM so DMA sees themSCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);__DSB(); __ISB();// Start DMA transferHAL_UART_Transmit_DMA(&huart3, tx_buf, len);}
static uint8_t rx_buf[256] __attribute__((aligned(32)));void uart_dma_receive(uint16_t len) {// Invalidate: discard any stale cache lines before DMA writesSCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);__DSB(); __ISB();HAL_UART_Receive_DMA(&huart3, rx_buf, len);}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {// DMA done, invalidate again to ensure CPU sees DMA's writesSCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, huart->RxXferSize);__DSB(); __ISB();process_data(rx_buf, huart->RxXferSize);}
Note: Invalidate before DMA starts is defensive — it ensures no stale cache lines from prior CPU reads. Invalidate after DMA completes is mandatory.
For continuous streaming, use two buffers and alternate:
+------------------+ DMA active +------------------+| BUFFER A | <----------------- | BUFFER B || (CPU fills) | | (DMA drains) |+------------------+ +------------------+^ ^| Clean + DSB/ISB | Invalidate + DSB/ISB| |CPU writes DMA writes
#define BUF_SIZE 512static uint8_t ping_buf[BUF_SIZE] __attribute__((aligned(32)));static uint8_t pong_buf[BUF_SIZE] __attribute__((aligned(32)));volatile uint8_t *active_tx_buf = ping_buf;volatile uint8_t *active_rx_buf = pong_buf;void dma_tx_complete_callback(void) {// Switch buffersactive_tx_buf = (active_tx_buf == ping_buf) ? pong_buf : ping_buf;// Prepare next buffer (CPU fills it)fill_buffer(active_tx_buf, BUF_SIZE);// Clean and start next DMASCB_CleanDCache_by_Addr((uint32_t*)active_tx_buf, BUF_SIZE);__DSB(); __ISB();start_dma_tx(active_tx_buf, BUF_SIZE);}void dma_rx_complete_callback(uint16_t len) {// Invalidate so CPU sees DMA's dataSCB_InvalidateDCache_by_Addr((uint32_t*)active_rx_buf, len);__DSB(); __ISB();process_data(active_rx_buf, len);// Switch and restart RXactive_rx_buf = (active_rx_buf == ping_buf) ? pong_buf : ping_buf;SCB_InvalidateDCache_by_Addr((uint32_t*)active_rx_buf, BUF_SIZE);__DSB(); __ISB();start_dma_rx(active_rx_buf, BUF_SIZE);}
Alignment: Buffers must be 32-byte aligned. Use __attribute__((aligned(32))) or alignas(32) (C++11). Unaligned addresses cause the hardware to clean/invalidate adjacent cache lines — potentially corrupting unrelated data.
Size rounding: SCB_CleanDCache_by_Addr(addr, size) rounds size up to the next cache line boundary. If your buffer is 100 bytes, it cleans 128 bytes (4 lines). Ensure adjacent data isn’t corrupted by padding or separation.
Stack buffers: Never use stack buffers for DMA without cache maintenance. Stack is cached. If you must, clean/invalidate the exact stack range — but prefer static/global aligned buffers.
Cache line size: Hardcoded 32 bytes for Cortex-M7. Read CTR_EL0 (Cache Type Register) at runtime for portability:
static inline uint32_t get_dcache_line_size(void) {uint32_t ctr = __get_CTR();// CTR[15:0] = DminLine (log2 of line size in words)return 4U << ((ctr & 0xF) - 2); // Convert to bytes}
| Mistake | Symptom | Fix |
|---|---|---|
| No cache maintenance | Intermittent data corruption, works in debug (cache disabled) | Add Clean/Invalidate + DSB/ISB |
Only __DSB() without __ISB() | CPU reads stale instruction cache (if code in same region) | Always pair DSB + ISB |
| Wrong alignment | Adjacent variables corrupted | Align to 32 bytes, pad between buffers |
| Invalidate before CPU reads (TX) | CPU reads garbage | Use Clean for TX, Invalidate for RX |
| Forgetting size rounding | Buffer overrun in cache ops | Account for 32-byte rounding in buffer layout |
On STM32H7, the Cortex-M7 core has 32 KB D-Cache and 32 KB I-Cache. The MDMA and DMA2D peripherals also access memory. Key points:
SystemInit(), disable only for debugging// Alternative: MPU non-cacheable region (no cache maintenance needed)static void mpu_config_noncacheable(uint32_t addr, uint32_t size) {MPU_Region_InitTypeDef region = {0};region.Enable = MPU_REGION_ENABLE;region.BaseAddress = addr;region.Size = MPU_REGION_SIZE_256B; // Adjust to cover bufferregion.AccessPermission = MPU_REGION_FULL_ACCESS;region.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;region.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // Key: non-cacheableregion.IsShareable = MPU_ACCESS_SHAREABLE;region.Number = MPU_REGION_NUMBER0;region.TypeExtField = MPU_TEX_LEVEL0;region.SubRegionDisable = 0;HAL_MPU_ConfigRegion(®ion);}
Using MPU non-cacheable regions eliminates cache maintenance overhead but costs ~2-3x memory bandwidth. For high-throughput DMA (Ethernet, SDIO, DCMI), explicit cache maintenance with write-back cache is faster.
__DSB(); __ISB() — barriers ensure completion and visibilityThe cost of cache maintenance is negligible compared to the debugging hours saved. Make it a habit: every DMA buffer gets its maintenance calls, every time.
Quick Links
Legal Stuff





