一、为什么 D-Cache 会让 DMA 数据错乱?

STM32H7 的 Cortex-M7 内核带 16KB D-Cache 和 I-Cache。CPU 访问内存时,若命中 Cache 则直接读写 Cache,不会立即同步到物理内存(SRAM/SDRAM)。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存,完全“看不见”Cache。

这就导致两个经典问题:

  • DMA 写入内存后 CPU 读到旧数据:DMA 把新数据写入 SRAM,但 CPU 之前缓存了该地址的旧值,读到的仍是 Cache 里的旧数据。
  • CPU 写入数据后 DMA 发送旧数据:CPU 把数据写入 Cache 但未回写到 SRAM,DMA 从 SRAM 取到的是旧内容。

例如 ADC 连续采样通过 DMA 存入缓冲区,CPU 读取时却始终是第一次的值,或者 UART DMA 发送时发出乱码,都是这个原因。

二、解决思路:Cache 维护 vs MPU 配置

常见方案有两种:

  1. 手动 Cache 维护:在 DMA 传输前后调用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr()。缺点是容易遗漏、地址对齐要求严格,且频繁调用影响性能。
  2. MPU 配置 Non-Cacheable 区域:把 DMA 缓冲区所在的内存区域设为 Non-Cacheable(或 Write-Through),CPU 和 DMA 都直接访问物理内存,从根本上避免一致性问题。

对于固定用于 DMA 的缓冲区,MPU 方案更可靠、更省心。下面重点讲解。

三、MPU 配置步骤

STM32H7 的 MPU 支持 16 个区域,每个区域可独立设置起始地址、大小、访问权限和 Cache 属性。配置流程:

  1. 使能 MPU(HAL_MPU_Enable)。
  2. 禁用 MPU(配置前需先关闭)。
  3. 配置一个区域,属性设为 MPU_ACCESS_NOT_CACHEABLE、MPU_ACCESS_NOT_BUFFERABLE。
  4. 使能该区域。
  5. 重新使能 MPU。

注意:MPU 区域大小必须是 2 的幂,且起始地址按大小对齐。例如 32KB 区域起始地址必须是 32KB 对齐。

四、完整代码示例

假设我们在 0x30000000(D2 SRAM)划出 32KB 作为 DMA 缓冲区,配置为 Non-Cacheable。

#include "stm32h7xx_hal.h"

#define DMA_BUFFER_ADDR   0x30000000UL
#define DMA_BUFFER_SIZE   0x8000UL   /* 32KB */

/* 将 DMA 缓冲区所在区域配置为 Non-Cacheable */
void MPU_Config_NonCacheable(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    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_DISABLE;
    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);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/* DMA 缓冲区定义在该地址,需用链接脚本或绝对地址定位 */
uint8_t dma_buffer[DMA_BUFFER_SIZE] __attribute__((at(DMA_BUFFER_ADDR)));

/* 使用示例:ADC + DMA 采集 */
void ADC_DMA_Start(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)dma_buffer, DMA_BUFFER_SIZE / 2);
}

/* 主循环中直接读取,无需 Cache 维护 */
void Process_Data(void)
{
    for (uint32_t i = 0; i < DMA_BUFFER_SIZE; i++) {
        /* 直接读取 dma_buffer[i],数据始终是最新的 */
    }
}

若使用链接脚本,可在 .ld 文件中定义段:

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

然后变量声明为:

uint8_t dma_buffer[DMA_BUFFER_SIZE] __attribute__((section(".dma_buffer")));

五、注意事项与避坑指南

  • MPU 配置必须在 DMA 初始化之前完成,否则首次传输可能仍走 Cache。
  • 区域大小与对齐:32KB 区域起始地址必须 32KB 对齐,否则 MPU 配置无效。
  • 不要与其它 MPU 区域重叠,重叠时编号高的区域优先,容易导致属性混乱。
  • Non-Cacheable 会降低 CPU 访问速度,仅对 DMA 缓冲区使用,不要全局关闭 Cache。
  • 若使用 SDRAM 做 DMA 缓冲区,需同时配置 FMC 和 MPU,且注意 SDRAM 刷新时序。
  • 调试时若发现数据仍错乱,检查是否遗漏 SCB_CleanInvalidateDCache() 或 MPU 未生效(可读 MPU->TYPE 确认)。
  • 多缓冲区场景:可合并到一个大区域,或使用多个 MPU 区域(H7 支持 16 个)。

六、总结

STM32H7 的 D-Cache 是性能利器,也是 DMA 一致性的“隐形杀手”。对于固定 DMA 缓冲区,用 MPU 配置 Non-Cacheable 区域是最干净、最可靠的方案。记住三句话:DMA 缓冲区单独划区、MPU 设为 Non-Cacheable、配置在 DMA 初始化之前。掌握这一招,从此告别数据错乱。