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

STM32H7 的 Cortex-M7 内核主频高达 480MHz,为了匹配内核速度,芯片内部集成了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存(SRAM)。

当 CPU 和 DMA 同时操作同一块内存时,就会出现两种典型问题:

  • CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 中(Write-Back 模式),DMA 从 SRAM 读到的仍是旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧副本,导致读到脏数据。

这类问题隐蔽性强,往往表现为“偶尔数据错位”“DMA 传输完成但数据不对”,调试时极易怀疑人生。

二、Cache 维护 API:什么时候用,怎么用?

CMSIS 提供了几个关键函数用于维护 Cache 一致性:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回 SRAM(Clean)。
  • SCB_InvalidateDCache_by_Addr(addr, size):将 Cache 中的对应行标记为无效,下次读取时从 SRAM 重新加载(Invalidate)。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate,用于需要完全同步的场景。

使用原则:

  • CPU 写 → DMA 读:在启动 DMA 传输前,对源缓冲区执行 SCB_CleanDCache_by_Addr
  • DMA 写 → CPU 读:在 DMA 传输完成后,对目标缓冲区执行 SCB_InvalidateDCache_by_Addr
  • 双向都有:使用 SCB_CleanInvalidateDCache_by_Addr

注意: 这些函数的地址和大小必须按 32 字节对齐(Cache Line 大小),否则会误伤相邻数据。

三、MPU 配置:从根源上规避一致性问题的利器

频繁调用 Cache 维护 API 会增加 CPU 开销,且容易遗漏。更优雅的方案是利用 MPU(内存保护单元)将 DMA 缓冲区配置为 Non-CacheableWrite-Through 模式。

推荐配置:

  • 将 DMA 使用的 SRAM 区域(如 0x30000000 开始的 AXI SRAM)配置为 Normal, Non-Cacheable。
  • 或者配置为 Write-Through,但 Write-Through 仍可能读到旧数据,非首选。

这样 CPU 和 DMA 都直接访问 SRAM,无需任何维护操作,从根本上消除一致性问题。

四、完整代码示例

以下示例基于 STM32H743,使用 DMA2 将 ADC 数据搬运到缓冲区,并演示 MPU 配置与 Cache 维护。

4.1 MPU 配置(Non-Cacheable 区域)

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR  0x30000000
#define DMA_BUFFER_SIZE  0x1000  // 4KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_4KB;
    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 传输与 Cache 维护(若未用 MPU)

#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"

#define BUF_SIZE  256
alignas(32) uint16_t adc_buf[BUF_SIZE];  // 32 字节对齐

void DMA_Transfer_With_Cache_Maintenance(void)
{
    // 1. 启动 DMA 前,清理源缓冲区(CPU 写 → DMA 读)
    SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));

    // 2. 启动 DMA 传输(假设已配置好)
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);

    // 3. 等待传输完成(实际应用用中断或轮询)
    while (HAL_ADC_GetState(&hadc1) != HAL_ADC_STATE_READY) {}

    // 4. DMA 写 → CPU 读,无效化 Cache
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));

    // 5. 此时 CPU 读取 adc_buf 才能拿到最新数据
    for (int i = 0; i < BUF_SIZE; i++) {
        process(adc_buf[i]);
    }
}

五、排查路径与注意事项

遇到 DMA 数据异常时,按以下路径排查:

  • 确认缓冲区地址是否在 Cacheable 区域:查看链接脚本,确认变量位于 DTCM、AXI SRAM 等区域。DTCM 默认不经过 Cache,但 AXI SRAM 默认 Cacheable。
  • 检查地址与大小对齐:Cache 维护 API 要求 32 字节对齐,否则可能无效或破坏相邻数据。
  • 确认调用时机:Clean 必须在 DMA 启动前,Invalidate 必须在 DMA 完成后。顺序反了等于白做。
  • 使用 MPU 配置 Non-Cacheable:一劳永逸,但注意 MPU 区域不能与其它配置冲突。
  • 避免在中断中频繁调用 Cache 维护:开销较大,可考虑双缓冲 + MPU 方案。

额外提醒:

  • STM32H7 的 DTCM 紧耦合内存默认不经过 Cache,适合放 DMA 缓冲区,但容量有限(通常 128KB)。
  • 若使用 SCB_InvalidateDCache 全局无效化,会清掉所有 Cache,影响性能,不推荐。
  • 调试时可在 DMA 完成中断里翻转 GPIO,用示波器观察时序,辅助判断。

掌握以上原理与配置,你就能在 STM32H7 上驯服 D-Cache 与 DMA,让高性能与数据一致性兼得。