一、为什么 DMA 和 D-Cache 会打架?

STM32H7 主频高达 480MHz,为了匹配 CPU 速度,Cortex-M7 内核配备了 D-Cache(数据缓存)。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 是“绕过 CPU”直接访问 SRAM 的搬运工,它看不到 Cache 里的新数据。

典型场景:

  • CPU 准备好发送缓冲区 → 数据还在 D-Cache → 启动 DMA 发送 → DMA 从 SRAM 读到旧数据。
  • DMA 接收数据写入 SRAM → CPU 读缓冲区 → CPU 从 D-Cache 读到旧数据。

这就是Cache 一致性(Coherency)问题。

二、三种解决思路对比

| 方案 | 原理 | 优点 | 缺点 | |------|------|------|------| | Cache 维护 | 手动 Clean/Invalidate | 灵活,适合小缓冲区 | 易漏、性能损耗 | | MPU 配置 Write-Through | 写操作同时更新 SRAM | 无需手动 Clean | 仍有读缓存问题 | | MPU 配置 Non-Cacheable | 该区域完全绕过 Cache | 一劳永逸 | 降低 CPU 访问速度 |

实际工程中常组合使用:DMA 缓冲区用 Non-Cacheable,其他区域保留 Cache。

三、Cache 维护操作:Clean 与 Invalidate

CMSIS 提供以下函数(需包含 core_cm7.h):

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
  • Clean:把 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 之前。
  • Invalidate:丢弃 Cache 内容,强制下次从 SRAM 重新加载。用于 DMA 写 → CPU 读 之前。
  • CleanInvalidate:两者都做,用于双向传输。

注意:地址必须 32 字节对齐,长度建议向上取整到 32 字节边界。

#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];

// 发送前:清理 D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);

// 接收后:无效化 D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
process_data(dma_rx_buf);

四、MPU 配置:从根源上隔离 DMA 缓冲区

更优雅的方式是用 MPU 将 DMA 缓冲区设为 Non-Cacheable 或 Write-Through。

4.1 配置步骤

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 关闭 MPU:MPU->CTRL = 0;
  3. 配置 Region:基地址、大小、属性。
  4. 使能 Region 和 MPU。

4.2 完整代码示例

#include "stm32h7xx.h"

// 假设 DMA 缓冲区放在 D2 SRAM,地址 0x30000000,大小 32KB
#define DMA_BUFFER_ADDR   0x30000000
#define DMA_BUFFER_SIZE   0x8000   // 32KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // Region 0: DMA 缓冲区设为 Non-Cacheable, Non-Shared
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // Region 1: 其他 SRAM 保持 Write-Back, Write-Allocate
    MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
    MPU_InitStruct.BaseAddress      = 0x24000000; // AXI SRAM
    MPU_InitStruct.Size             = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.3 链接脚本调整

确保 DMA 缓冲区真的落在 Non-Cacheable 区域:

MEMORY
{
  DTCMRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 128K
  AXI_SRAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
  D2_SRAM (xrw)  : ORIGIN = 0x30000000, LENGTH = 288K
}

SECTIONS
{
  .dma_buffer (NOLOAD) :
  {
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
  } > D2_SRAM
}

在代码中指定段:

__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[4096];

五、排查路径与注意事项

5.1 快速排查清单

  • 确认 DMA 缓冲区地址是否在 Non-Cacheable 区域。
  • 若未配 MPU,检查是否在 DMA 启动前后正确调用了 Clean/Invalidate。
  • 检查缓冲区是否 32 字节对齐。
  • 检查 SCB_EnableDCache() 是否已调用(HAL 初始化中默认开启)。
  • 使用 __DSB() 和 __ISB() 确保内存操作完成。

5.2 常见坑点

  • Invalidate 误伤:如果 CPU 刚写过缓冲区,直接 Invalidate 会丢弃未写回的数据。应先 Clean 再 Invalidate。
  • 多缓冲区共享 Cache Line:32 字节 Cache Line 内混有 DMA 和非 DMA 数据,Invalidate 会误丢其他变量。务必对齐并隔离。
  • DMA 描述符:链表式 DMA 的描述符本身也需 Non-Cacheable 或手动 Clean。
  • 中断中操作 Cache:Clean/Invalidate 耗时,避免在高速中断中频繁调用。

5.3 推荐实践

  • 所有 DMA 缓冲区统一放在 MPU 配置的 Non-Cacheable 区域。
  • 若必须用 Cache,封装 dma_send_prepare() 和 dma_recv_complete() 统一处理。
  • 使用 SCB_InvalidateDCache_by_Addr 时,长度参数用 size + 31 & ~31 向上取整。

六、总结

STM32H7 的 D-Cache 是把双刃剑。理解 Clean/Invalidate 的语义,结合 MPU 将 DMA 缓冲区设为 Non-Cacheable,是解决一致性问题的标准路径。建议在项目初期就规划好内存布局,避免后期调试时被“随机数据错乱”折磨。记住:DMA 不认 Cache,只认 SRAM。