STM32H7 的 Cache 与 DMA 一致性坑:MPU 配置与 Clean/Invalidate 的实战顺序
STM32H7 系列凭借 Cortex-M7 内核和 L1 Cache(I-Cache/D-Cache)实现了高主频下的优异性能,但 Cache 的引入也让 DMA 传输变得“危机四伏”。许多开发者都遇到过 DMA 发送的数据不对、接收的数据被覆盖等问题,根源往往在于 Cache 与 DMA 的一致性没有处理好。本文将带你理清原理,并给出 MPU 配置与 Clean/Invalidate 的实战顺序。
一、为什么 Cache 会导致 DMA 数据不一致?
Cortex-M7 的 D-Cache 是写回(Write-Back) 且写分配(Write-Allocate) 的。当 CPU 写数据时,数据先写入 Cache,并标记为“脏(Dirty)”,并不会立即写回 SRAM。如果此时启动 DMA 发送,DMA 直接从 SRAM 读取,读到的就是旧数据。
同理,DMA 接收数据时直接写入 SRAM,而 CPU 可能仍从 Cache 中读取旧数据,导致“看不到”新数据。
- CPU 写 → DMA 读:需要将 Cache 中脏数据 Clean(写回) 到 SRAM。
- DMA 写 → CPU 读:需要将 Cache 中对应区域 Invalidate(无效化),强制 CPU 从 SRAM 重新加载。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
最稳妥的做法是利用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,从根源上避免一致性问题。但并非所有场景都适合,例如需要 CPU 频繁访问的大缓冲区,Non-Cacheable 会降低性能。
2.1 MPU 配置步骤(以 Non-Cacheable 为例)
- 使能 MPU:
SCB->CTRL |= SCB_CTRL_MPU_Msk; - 配置 Region 基地址和属性。
- 使能 Region。
- 使能 MPU 和 D-Cache。
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 假设 DMA 缓冲区在 0x30000000 (SRAM1),大小 32KB
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:Non-Cacheable
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在使能 D-Cache 之前完成,且 Region 不能重叠。
三、Clean/Invalidate 的实战顺序
如果缓冲区是 Cacheable 的,就必须在 DMA 传输前后手动维护 Cache。顺序至关重要,弄反会导致数据丢失或性能下降。
3.1 DMA 发送(Memory → Peripheral)
- CPU 填充缓冲区数据。
- Clean 缓冲区对应的 D-Cache 行(写回 SRAM)。
- 启动 DMA 发送。
- 等待 DMA 完成。
#define DMA_BUF_SIZE 1024
uint8_t dma_tx_buf[DMA_BUF_SIZE] __attribute__((aligned(32)));
void DMA_Send_Data(void)
{
// 1. 填充数据
for (int i = 0; i < DMA_BUF_SIZE; i++) {
dma_tx_buf[i] = i & 0xFF;
}
// 2. Clean D-Cache
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, DMA_BUF_SIZE);
// 3. 启动 DMA
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)&UART->TDR, DMA_BUF_SIZE);
}
3.2 DMA 接收(Peripheral → Memory)
- Invalidate 缓冲区对应的 D-Cache 行(丢弃旧数据)。
- 启动 DMA 接收。
- 等待 DMA 完成。
- CPU 读取数据(此时 Cache 未命中,从 SRAM 加载新数据)。
uint8_t dma_rx_buf[DMA_BUF_SIZE] __attribute__((aligned(32)));
void DMA_Receive_Data(void)
{
// 1. Invalidate D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, DMA_BUF_SIZE);
// 2. 启动 DMA 接收
HAL_DMA_Start(&hdma_memtomem, (uint32_t)&UART->RDR, (uint32_t)dma_rx_buf, DMA_BUF_SIZE);
// 3. 等待完成
HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);
// 4. 读取数据
process_data(dma_rx_buf, DMA_BUF_SIZE);
}
3.3 双向传输(如 SPI 全双工)
顺序:先 Invalidate RX 缓冲区,再 Clean TX 缓冲区,然后启动 DMA。
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_SPI_TransmitReceive_DMA(&hspi, tx_buf, rx_buf, len);
四、注意事项与常见坑
-
地址对齐:
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,长度建议为 32 的倍数。使用__attribute__((aligned(32)))修饰缓冲区。 - 不要跨区域操作:如果缓冲区跨越了多个 MPU Region,维护操作可能不完整。
- Invalidate 前必须确保 DMA 已停止:否则可能丢弃 DMA 刚写入但未及时更新的数据。
- 避免频繁 Clean/Invalidate:会严重影响性能,尽量用 Non-Cacheable 区域。
- DMA 描述符:如果使用链表模式,描述符本身也需注意 Cache 一致性。
-
中断中调用:
SCB_CleanDCache_by_Addr等函数执行时间较长,避免在高速中断中频繁调用。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的“必修课”。核心原则:
- 能不用 Cacheable 就不用,优先 MPU 配置为 Non-Cacheable。
- 必须用时,严格遵循:发送前 Clean,接收前 Invalidate。
- 顺序不能错,否则数据丢失或读到旧值。
掌握这些,你就能在 STM32H7 上既享受 Cache 的性能,又保证 DMA 的可靠。