HomeAbout UsContact Us

Cortex-M Cache Maintenance for DMA Coherency

By Jithin Tom
Published in Embedded C/C++
August 19, 2026
3 min read
Cortex-M Cache Maintenance for DMA Coherency

Table Of Contents

01
The Coherency Problem
02
Cache Maintenance Operations
03
Memory Barriers: DSB and ISB
04
Practical Patterns
05
Alignment and Size Gotchas
06
Common Mistakes
07
STM32H7 Specifics
08
Summary
09
Related Reading
10
References
11
Frequently Asked Questions

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).

The Coherency Problem

+----------+ 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.

Cache Maintenance Operations

Cortex-M7 provides three D-Cache maintenance operations through the SCB:

// CMSIS-Core (core_cm7.h)
void SCB_CleanDCache(void); // Clean entire D-Cache
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize); // Clean by address range
void SCB_InvalidateDCache(void); // Invalidate entire D-Cache
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); // Invalidate by address range
void SCB_CleanInvalidateDCache(void); // Clean + Invalidate entire D-Cache
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); // By address range

Operation Semantics

OperationDirectionUse Case
CleanCPU → RAMCPU finished writing buffer, DMA will read
InvalidateRAM → CPUDMA finished writing buffer, CPU will read
Clean+InvalidateBothOwnership 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 boundary
static uint8_t dma_buffer[1024] __attribute__((aligned(32)));
void dma_tx_prepare(void *buf, uint32_t len) {
// CPU wrote data, DMA will read: CLEAN
SCB_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: INVALIDATE
SCB_InvalidateDCache_by_Addr((uint32_t*)buf, len);
__DSB(); __ISB(); // Barriers: ensure invalidate completes before CPU reads
}

Memory Barriers: DSB and ISB

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

Why Both?

Timeline without barriers:
CPU: SCB_CleanDCache_by_Addr() ----> returns immediately
DMA: Starts transfer ----> may see partially cleaned cache
Timeline with barriers:
CPU: SCB_CleanDCache_by_Addr()
CPU: __DSB() -------------------> waits for clean to reach RAM
CPU: __ISB() -------------------> pipeline sees new memory state
DMA: 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).

Practical Patterns

Pattern 1: TX Buffer (CPU → DMA → Peripheral)

// Buffer aligned to cache line
static 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 them
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
__DSB(); __ISB();
// Start DMA transfer
HAL_UART_Transmit_DMA(&huart3, tx_buf, len);
}

Pattern 2: RX Buffer (Peripheral → DMA → CPU)

static uint8_t rx_buf[256] __attribute__((aligned(32)));
void uart_dma_receive(uint16_t len) {
// Invalidate: discard any stale cache lines before DMA writes
SCB_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 writes
SCB_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.

Pattern 3: Bidirectional / Ping-Pong Buffers

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 512
static 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 buffers
active_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 DMA
SCB_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 data
SCB_InvalidateDCache_by_Addr((uint32_t*)active_rx_buf, len);
__DSB(); __ISB();
process_data(active_rx_buf, len);
// Switch and restart RX
active_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 and Size Gotchas

  1. 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.

  2. 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.

  3. 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.

  4. 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
}

Common Mistakes

MistakeSymptomFix
No cache maintenanceIntermittent 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 alignmentAdjacent variables corruptedAlign to 32 bytes, pad between buffers
Invalidate before CPU reads (TX)CPU reads garbageUse Clean for TX, Invalidate for RX
Forgetting size roundingBuffer overrun in cache opsAccount for 32-byte rounding in buffer layout

STM32H7 Specifics

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:

  • D1/D2/D3 domains: DMA in D2 domain accessing D1 SRAM crosses domain boundaries — cache maintenance still required
  • MPU regions: Configure buffers as Device or Normal Non-Cacheable to bypass cache entirely (simpler but slower)
  • SCB_EnableDCache() / SCB_DisableDCache(): Enable early in 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 buffer
region.AccessPermission = MPU_REGION_FULL_ACCESS;
region.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
region.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // Key: non-cacheable
region.IsShareable = MPU_ACCESS_SHAREABLE;
region.Number = MPU_REGION_NUMBER0;
region.TypeExtField = MPU_TEX_LEVEL0;
region.SubRegionDisable = 0;
HAL_MPU_ConfigRegion(&region);
}

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.

Summary

  • DMA bypasses cache — explicit maintenance is mandatory on Cortex-M7
  • Clean (CPU→DMA), Invalidate (DMA→CPU), CleanInvalidate (ownership swap)
  • Always pair with __DSB(); __ISB() — barriers ensure completion and visibility
  • Align buffers to 32 bytes — prevents adjacent data corruption
  • Ping-pong buffers for continuous streaming — clean/invalidate per buffer, not global
  • MPU non-cacheable regions are a valid alternative for simpler code at bandwidth cost

The 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.

  • Zero-Copy DMA Patterns on ARM Cortex-M
  • Reducing ARM Cortex-M Interrupt Latency with Tail-Chaining
  • SPI Slave DMA Implementation on STM32

References

  1. ARM Cortex-M7 Processor Technical Reference Manual, “Cache Maintenance Operations” chapter (ARM DDI 0489) — https://support.arm.com/documentation/ddi0489/latest
  2. ARM Cortex-M7 Devices Generic User Guide, “System Control Block” (ARM DUI 0646) — https://support.arm.com/documentation/dui0646/latest
  3. STMicroelectronics, “STM32H7 Series Reference Manual” (RM0433), Section 3.7 “Cache Maintenance Operations” — https://web.archive.org/web/20241113131657/https://www.st.com/resource/en/reference_manual/dm00314099-stm32h742-stm32h743-753-and-stm32h750-value-line-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf
  4. NXP, “i.MX RT1170 Reference Manual” (IMXRT1170RM), Chapter “L1 Cache Controller” — https://mm.digikey.com/Volume0/opasdata/d220001/medias/docus/6465/568_IMXRT1170RM%20manual%20REV3.pdf
  5. Joseph Yiu, “The Definitive Guide to ARM Cortex-M7” (Newnes, 2017), Chapter 12 “Caches and Memory Protection”
  6. FreeRTOS, “Cache Management for Cortex-M7” — https://www.freertos.org/Why-FreeRTOS/FAQs/Memory-usage-boot-times-context/

Frequently Asked Questions

Why does DMA break cache coherency on Cortex-M?

DMA transfers bypass the CPU and its cache entirely. When DMA writes to RAM, the CPU's D-Cache may still hold stale data. When the CPU writes to RAM, DMA may read stale data from RAM because the CPU's dirty cache lines haven't been cleaned to memory.

What is the difference between Clean, Invalidate, and Clean+Invalidate?

Clean writes dirty cache lines to memory (CPU -> RAM). Invalidate discards cache lines so subsequent reads fetch fresh data from memory (RAM -> CPU). Clean+Invalidate does both: writes dirty lines to memory then marks them invalid, used when ownership transfers from CPU to DMA or vice versa.

When do I need memory barriers (DSB/ISB) around cache operations?

Always. Cache maintenance operations (SCB_CleanDCache, SCB_InvalidateDCache) are not guaranteed to complete before subsequent instructions execute. DSB ensures the cache operation completes globally. ISB flushes the pipeline so subsequent instructions see the updated memory state.

How do I handle cache maintenance for a circular DMA buffer?

For circular buffers, you cannot invalidate the entire buffer while DMA is active. Use double-buffering (ping-pong) or split the buffer into regions: clean/invalidate only the region DMA just finished writing, while DMA works on the other region. Align buffer boundaries to cache line size (32 bytes on Cortex-M7).

Does Cortex-M4 need cache maintenance for DMA?

Most Cortex-M4 implementations (e.g., STM32F4) have no data cache, so cache maintenance is a no-op. However, Cortex-M7 (STM32H7, RT1170, etc.) has L1 D-Cache and I-Cache, making cache maintenance mandatory for DMA coherency. Always check your specific MCU's TRM.

Tags

cortex-mcachedmacoherencyarmstm32embedded-c

Share


Previous Article
Cortex-M Floating-Point Unit Lazy Stacking Optimization
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Cortex-M Floating-Point Unit Lazy Stacking Optimization
Cortex-M Floating-Point Unit Lazy Stacking Optimization
August 19, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media