一、现象:DMA 数据为何“时好时坏”?

在 STM32H7 上使用 DMA 搬运 ADC 采样、串口数据或外设缓冲区时,常遇到:

  • CPU 读取的缓冲区数据是旧值,DMA 明明已经写入新数据。
  • DMA 发送的数据部分正确、部分乱码,且随编译优化等级变化。
  • 调试时单步执行正常,全速运行就出错。
  • 关闭 D-Cache 后一切正常,打开就异常。

这些现象高度指向 Cache 与 DMA 的数据一致性问题。

二、原理:为什么 Cache 会“捣乱”?

Cortex-M7 的 D-Cache 位于 CPU 与总线矩阵之间。CPU 访问内存时,若命中 Cache 则直接读写 Cache,不会立即同步到物理内存(SRAM)。而 DMA 是独立于 CPU 的总线主设备,它直接访问物理内存,看不到 Cache 中的最新数据。

  • CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中(写回策略),DMA 读到的是旧内存值。
  • DMA 写、CPU 读:DMA 将新数据写入物理内存,但 CPU 可能从 Cache 中读到旧数据(若之前已缓存该区域)。

STM32H7 的 D-Cache 默认策略是 Write-Back, Write-Allocate,即写操作只更新 Cache,不立即写回内存,进一步加剧不一致。

三、排查路径:从现象到寄存器

3.1 确认是否启用 D-Cache

在 SystemInit 或启动文件中,检查 SCB_EnableDCache() 是否被调用。若未启用,问题不在此;若启用,继续。

3.2 定位 DMA 缓冲区地址

  • 查看链接脚本(.ld)或 __attribute__((at(...))),确认缓冲区是否位于 可缓存区域(如 AXI SRAM 0x24000000)。
  • STM32H7 默认将 0x24000000 开始的 512KB AXI SRAM 配置为 Write-Back 可缓存。

3.3 检查 MPU 配置

若未配置 MPU,则所有内存按默认属性访问。此时 DMA 缓冲区若在可缓存区,必然出问题。

四、解决方案:三种武器

4.1 方案一:将 DMA 缓冲区放在非缓存区

最简单:把缓冲区放到 DTCM(0x20000000)或配置为 Non-Cacheable 的 SRAM 区域。DTCM 紧耦合,CPU 访问快,且默认不经过 Cache。

// 将缓冲区定义到 DTCM,需修改链接脚本或使用 section
__attribute__((section(".dtcm_data"))) uint8_t dma_buf[1024];

4.2 方案二:手动维护 Cache 一致性

在 DMA 传输前后调用 CMSIS 提供的函数:

  • DMA 读(CPU 写,DMA 读):传输前 SCB_CleanDCache_by_Addr() 将 Cache 写回内存。
  • DMA 写(DMA 写,CPU 读):传输后 SCB_InvalidateDCache_by_Addr() 使 Cache 失效,强制 CPU 从内存重新加载。
// 发送前清理 Cache
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));
HAL_DMA_Start(&hdma, (uint32_t)dma_buf, (uint32_t)&periph, len);

// 接收后无效化 Cache
HAL_DMA_Start(&hdma, (uint32_t)&periph, (uint32_t)dma_buf, len);
// 等待传输完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));

注意:Invalidate 操作会丢弃未写回的数据,若 CPU 在 DMA 写期间修改了同一 Cache 行,会丢失修改。因此必须确保 DMA 缓冲区独占 Cache 行(32 字节对齐,且大小是 32 的整数倍)。

4.3 方案三:MPU 配置非缓存区域(推荐)

通过 MPU 将特定内存区域设为 Non-Cacheable,一劳永逸。以 STM32H7 为例,将 AXI SRAM 的某一段(如 0x24000000 + 256KB)配置为非缓存,专门给 DMA 使用。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 设置 AXI SRAM 后半部分为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24040000; // 256KB 偏移
    MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
    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_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

之后将 DMA 缓冲区定义到该区域:

__attribute__((section(".dma_buffer"))) uint8_t dma_buf[1024];

并在链接脚本中把 .dma_buffer 段放到 0x24040000。

五、完整示例:ADC + DMA + Cache 安全

#include "stm32h7xx_hal.h"

#define ADC_BUF_LEN 256
__attribute__((section(".dma_buffer"))) uint16_t adc_buf[ADC_BUF_LEN];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_DMA_Init(void)
{
    // ADC 和 DMA 初始化略,重点在缓冲区与 MPU
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_LEN);
}

// 处理数据前,若 MPU 未配置非缓存,需手动无效化
void Process_ADC_Data(void)
{
    // 若已配置 MPU 非缓存,此行可省略
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));

    for (int i = 0; i < ADC_BUF_LEN; i++) {
        // 使用 adc_buf[i]
    }
}

六、注意事项与避坑指南

  • 对齐:手动维护 Cache 时,缓冲区地址和长度必须 32 字节对齐,否则可能误伤相邻数据。
  • DMA 突发:若 DMA 使用突发传输,确保缓冲区大小是 Cache 行的整数倍。
  • 多缓冲区:使用双缓冲时,每个缓冲区都要独立维护一致性。
  • 性能权衡:非缓存区域会降低 CPU 访问速度,仅将 DMA 缓冲区设为非缓存,其余保持缓存。
  • 调试技巧:可临时关闭 D-Cache 验证问题,但正式产品不建议关闭,会损失性能。
  • H7 的 AXI SRAM:默认全部可缓存,务必通过 MPU 划分区域。

七、总结

STM32H7 的 Cache 与 DMA 一致性问题并非玄学,而是架构使然。排查时遵循“确认 Cache 状态 → 定位缓冲区 → 检查 MPU → 选择方案”的路径,即可快速定位。推荐使用 MPU 配置非缓存区域 作为最终方案,兼顾性能与可靠性。掌握这一技能,才能在高性能嵌入式开发中游刃有余。