一、为什么开启 D-Cache 后 DMA 会“打架”?

STM32H7 的 Cortex-M7 内核带有 16KB 的 D-Cache 和 I-Cache。开启 D-Cache 后,CPU 访问内存时优先命中 Cache,而不是直接读写 SRAM。但 DMA 是“老实人”,它只认物理内存地址,不经过 Cache。

这就导致两个经典问题:

  • CPU 写、DMA 读:CPU 把数据写入 Cache,但尚未写回 SRAM,DMA 从 SRAM 读到的还是旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本,看不到更新。

更隐蔽的是 Cache 行(Cache Line) 问题:Cortex-M7 的 D-Cache 行大小为 32 字节。即使你只修改了 1 个字节,clean 操作也会把整个 32 字节写回;invalidate 操作会丢弃整个 32 字节的 Cache 内容。如果 DMA 缓冲区没有按 32 字节对齐,或者缓冲区大小不是 32 的整数倍,就会误伤相邻数据,造成“数据错乱”。

二、MPU 配置:给 DMA 缓冲区划“特区”

最稳妥的方案是用 MPU(Memory Protection Unit)把 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,让 CPU 和 DMA 看到同一份物理内存,从根上避免一致性问题。

2.1 MPU 配置步骤

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;(可选,用于调试)
  2. 关闭 MPU:MPU->CTRL = 0;
  3. 配置 Region:设置基地址、大小、属性。
  4. 使能 Region 和 MPU:MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
  5. 执行 __DSB() 和 __ISB() 同步。

2.2 完整代码示例

#include "stm32h7xx.h"

#define DMA_BUFFER_SIZE  1024
#define DMA_BUFFER_ADDR  0x30000000  // 例如 D2 SRAM 区域

// 按 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[DMA_BUFFER_SIZE] __attribute__((section(".dma_buffer")));

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {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_64KB; // 根据实际调整
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; // 关键:Non-Cacheable
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 的 Region 大小必须是 2 的幂,且基地址必须按大小对齐。如果缓冲区跨越了多个 Region,需要分别配置。

三、clean/invalidate 的边界条件

如果因为性能原因必须使用 Cacheable 内存,那就必须在 DMA 传输前后手动维护 Cache:

  • CPU 写 → DMA 读:在启动 DMA 前执行 SCB_CleanDCache_by_Addr(),把 Cache 数据写回 SRAM。
  • DMA 写 → CPU 读:在 DMA 完成后执行 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 旧数据,强制 CPU 从 SRAM 重新加载。

3.1 正确用法

// 发送前:clean
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, DMA_BUFFER_SIZE);
HAL_DMA_Start(&hdma, (uint32_t)dma_buffer, (uint32_t)&periph->DR, DMA_BUFFER_SIZE);

// 接收后:invalidate
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000);
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, DMA_BUFFER_SIZE);

3.2 边界条件与陷阱

  • 地址和大小必须 32 字节对齐:SCB_CleanDCache_by_Addr 内部按 Cache Line 操作,如果地址未对齐,会误伤相邻数据。建议缓冲区用 __attribute__((aligned(32))) 声明,大小取 32 的整数倍。
  • 不要对同一区域同时 clean 和 invalidate:先 clean 再 invalidate 会丢弃刚写回的数据,顺序错误导致数据丢失。
  • DMA 传输期间禁止 CPU 访问该缓冲区:否则 Cache 状态不可预测。
  • 中断中调用需谨慎:clean/invalidate 操作耗时,可能影响实时性。
  • 多缓冲区场景:每个缓冲区独立维护,不要跨区域操作。

四、调试与验证

  • 使用 SCB->CCR 确认 D-Cache 已开启。
  • 在 DMA 传输前后打印缓冲区内容,对比 SRAM 实际值。
  • 用逻辑分析仪或示波器抓取 DMA 请求信号,确认传输完成。
  • 如果数据错乱,先检查 MPU 配置是否生效,再检查 clean/invalidate 的调用时机和地址对齐。

五、总结

STM32H7 的 D-Cache 与 DMA 冲突本质是“可见性”问题。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若必须用 Cacheable 内存,则严格遵循“写前 clean、读后 invalidate”的原则,并保证 32 字节对齐。理解 Cache Line 的边界条件,才能避免“莫名其妙”的数据错乱。