一、为什么 D-Cache 会成为 DMA 的“猪队友”?

STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存)。CPU 访问数据时,若命中缓存则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存(SRAM)。

当两者同时操作同一块缓冲区时,就会出现:

  • CPU 写数据到缓存,但未写回 SRAM → DMA 从 SRAM 读到旧数据。
  • DMA 写入新数据到 SRAM,但 CPU 缓存中仍是旧数据 → CPU 读到旧值。

这就是缓存一致性问题。STM32H7 提供了 SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr() 等函数来手动维护一致性,但误用会导致更隐蔽的偶发错误。

二、典型误用场景与现象

2.1 只 Clean 不 Invalidate(发送方向)

DMA 发送(内存→外设)前,必须将 CPU 写入缓存的数据 Clean(写回 SRAM)。若忘记 Clean,DMA 可能发送旧数据。

2.2 只 Invalidate 不 Clean(接收方向)

DMA 接收(外设→内存)前,若缓冲区已被 CPU 写过,必须先 CleanInvalidate。否则 Invalidate 会丢弃未写回的脏数据,导致数据丢失。

2.3 在 DMA 传输过程中 Invalidate

DMA 正在写 SRAM 时,CPU 执行 Invalidate 会丢弃部分已写入的数据,造成数据错乱。

2.4 对非缓存对齐地址操作

STM32H7 的 Cache 行大小为 32 字节。若操作地址未按 32 字节对齐,Clean/Invalidate 会影响相邻数据,引发随机错误。

三、系统化排查路径

遇到偶发数据错乱时,按以下步骤排查:

  1. 确认是否启用 D-Cache:检查 SCB_EnableDCache() 是否被调用。
  2. 定位共享缓冲区:找出 CPU 与 DMA 共同访问的内存区域。
  3. 检查操作顺序
    • 发送前:是否执行了 Clean?
    • 接收前:是否执行了 Clean + Invalidate?
    • 是否在 DMA 传输中误操作 Cache?
  4. 检查地址对齐:缓冲区起始地址和长度是否 32 字节对齐?
  5. 使用 MPU 配置:将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,可从根本上避免一致性问题。
  6. 调试手段
    • 暂时关闭 D-Cache,若问题消失,则确认为缓存一致性导致。
    • 在 Clean/Invalidate 前后加内存屏障 __DSB()
    • 使用 ITM 或 GPIO 翻转测量时序。

四、正确配置步骤与完整代码

4.1 启用 D-Cache 与 MPU 配置

#include "stm32h7xx.h"

// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];

void Cache_Init(void)
{
    // 启用 D-Cache
    SCB_EnableDCache();
    SCB_EnableICache();

    // 配置 MPU:将 DMA 缓冲区区域设为 Non-Cacheable
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_rx_buf & 0xFFFFFFE0; // 32字节对齐
    MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_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_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.2 DMA 发送(内存→外设)

void DMA_SendData(uint8_t *src, uint16_t len)
{
    // 确保数据已写入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)src, len);
    __DSB(); // 数据同步屏障

    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)&UART->TDR, len);
}

4.3 DMA 接收(外设→内存)

void DMA_ReceiveData(uint8_t *dst, uint16_t len)
{
    // 先 Clean 避免丢弃脏数据,再 Invalidate 确保读取新数据
    SCB_CleanDCache_by_Addr((uint32_t *)dst, len);
    SCB_InvalidateDCache_by_Addr((uint32_t *)dst, len);
    __DSB();

    HAL_DMA_Start(&hdma_uart_rx, (uint32_t)&UART->RDR, (uint32_t)dst, len);
}

// DMA 传输完成中断中,读取数据前再次 Invalidate
void DMA_Complete_Callback(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
    __DSB();
    // 此时可安全读取 dma_rx_buf
}

五、注意事项与最佳实践

  • 优先使用 MPU:将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,可一劳永逸,避免手动维护。
  • 地址对齐:所有 Clean/Invalidate 操作的地址和长度必须按 32 字节对齐,否则会破坏相邻数据。
  • 顺序不能错:接收方向必须先 Clean 再 Invalidate;发送方向只需 Clean。
  • 加内存屏障:在 Cache 操作后、启动 DMA 前插入 __DSB(),确保操作完成。
  • 避免传输中操作:DMA 传输期间严禁对缓冲区执行 Invalidate。
  • 调试技巧:若问题偶发且与数据量相关,优先怀疑 Cache 一致性;可暂时关闭 D-Cache 验证。
  • 使用 SCB_InvalidateDCache_by_Addr 而非全局 Invalidate:全局操作影响范围大,可能误伤其他数据。

掌握以上原理与排查方法,STM32H7 的 D-Cache 与 DMA 一致性问题将不再神秘。正确配置 MPU 并遵循 Clean/Invalidate 规范,可显著提升系统稳定性。