STM32H7 的 D-Cache 与 DMA 冲突排查:从数据错乱到 MPU 配置的完整定位路径

一、现象:数据错乱的典型表现

在 STM32H7 上使用 DMA 传输时,你可能会遇到:

  • 串口 DMA 接收的数据偶尔出现前几个字节正确、后续全为 0 或乱码;
  • ADC 连续采样 DMA 搬运到数组,但读取到的值跳变剧烈,与真实电压不符;
  • SDMMC 读写文件时数据校验失败,但降低主频或关闭 D-Cache 后正常;
  • 以太网 DMA 描述符被 CPU 修改后,DMA 却读到旧值。

这些现象的共同点是:CPU 访问的内存与 DMA 访问的内存不一致。根源在于 STM32H7 的 Cortex-M7 内核带有 D-Cache,而 DMA 直接访问物理内存,不经过 Cache。

二、原理:D-Cache 与 DMA 为何冲突

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 写数据时,先写入 Cache,标记为脏(Dirty),并不立即写回 SRAM;CPU 读数据时,若 Cache 未命中,则从 SRAM 加载到 Cache。

DMA 控制器直接读写 SRAM,完全绕过 D-Cache。因此:

  • CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中(脏行),DMA 从 SRAM 读到的是旧数据。
  • DMA 写、CPU 读:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着旧数据(干净行或脏行),CPU 读到旧值。

此外,Cache 行大小为 32 字节。若 DMA 缓冲区未按 32 字节对齐,Cache 维护操作可能影响相邻变量,导致更难排查的随机错误。

三、排查路径:从现象到根因

步骤 1:确认是否与 Cache 相关

临时关闭 D-Cache 测试:

// 在 main 初始化前调用
SCB_DisableDCache();

若问题消失,则基本确定是 Cache 一致性问题。注意:关闭 D-Cache 会大幅降低性能,仅用于验证。

步骤 2:检查 DMA 缓冲区地址与对齐

  • 缓冲区是否位于 DTCM RAM?DTCM 默认不经过 Cache,但部分 DMA 无法访问 DTCM(如 MDMA、部分外设 DMA)。
  • 缓冲区是否 32 字节对齐?使用 __attribute__((aligned(32)))。
  • 是否与其他变量共享 Cache 行?避免将 DMA 缓冲区与普通变量放在同一 32 字节区域。

步骤 3:选择正确的 Cache 维护策略

根据数据传输方向:

  • CPU 写 → DMA 读:在启动 DMA 前执行 SCB_CleanDCache_by_Addr(),将脏行写回 SRAM。
  • DMA 写 → CPU 读:在 DMA 完成后执行 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧行,强制从 SRAM 重新加载。
  • 双向:先 Clean 再 Invalidate,或使用 SCB_CleanInvalidateDCache_by_Addr()。

注意:Invalidate 操作若缓冲区有未写回的脏数据,会丢失数据。因此顺序很重要。

步骤 4:使用 MPU 配置内存属性

更优雅的方案是利用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,从而无需手动维护 Cache。

推荐配置:

  • 将 DMA 缓冲区集中放在一个专用区域(如 0x30000000 开始的 SRAM 区域);
  • 用 MPU 将该区域设为 Normal, Non-Cacheable 或 Normal, Write-Through, No Write Allocate;
  • 确保该区域不与其他 Cacheable 数据重叠。

四、完整代码示例

以下示例展示 ADC + DMA 连续采样,使用 MPU 将缓冲区设为 Non-Cacheable。

#include "stm32h7xx.h"

#define ADC_DMA_BUFFER_SIZE  256
// 将缓冲区放在 D2 SRAM 区域,并 32 字节对齐
__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];

// MPU 配置:将 .dma_buffer 区域设为 Non-Cacheable
void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 获取缓冲区地址,按 32 字节对齐向下取整
    uint32_t base_addr = (uint32_t)adc_dma_buffer & ~0x1F;
    // 区域大小至少覆盖缓冲区,设为 4KB 对齐
    uint32_t region_size = 4096;

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = base_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);
}

// 链接脚本中定义 .dma_buffer 段到 D2 SRAM
// 例如:
// .dma_buffer (NOLOAD) : ALIGN(32) { *(.dma_buffer) } > RAM_D2

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();
    SCB_EnableICache();
    SCB_EnableDCache();

    // 初始化 ADC 和 DMA,目标地址为 adc_dma_buffer
    // ...

    while (1) {
        // 由于 MPU 已设为 Non-Cacheable,CPU 直接读取 SRAM,无需手动维护 Cache
        uint16_t value = adc_dma_buffer[0];
        // 处理数据...
    }
}

若不想用 MPU,也可手动维护 Cache:

// DMA 传输前,CPU 写数据到缓冲区后
SCB_CleanDCache_by_Addr((uint32_t*)adc_dma_buffer, sizeof(adc_dma_buffer));

// DMA 传输完成后,CPU 读数据前
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buffer, sizeof(adc_dma_buffer));

五、注意事项

  • 对齐:DMA 缓冲区必须 32 字节对齐,否则 Cache 维护会波及相邻数据。
  • 区域隔离:不要将 DMA 缓冲区与普通变量放在同一 32 字节 Cache 行内。
  • DTCM 限制:DTCM 虽不经过 Cache,但部分 DMA(如 MDMA、以太网 DMA)无法访问 DTCM,需查阅参考手册。
  • Invalidate 风险:若缓冲区有未写回的脏数据,Invalidate 会丢弃它们,务必先 Clean。
  • 多缓冲区:若使用多个 DMA 缓冲区,确保每个都按 32 字节对齐且不重叠。
  • 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,适合 DMA 频繁访问的缓冲区;若 CPU 也频繁读取,可考虑 Write-Through 模式。
  • 调试技巧:使用 SCB_GetDCacheSize() 等函数确认 Cache 状态;在调试器中查看内存时,注意 Cache 可能未写回,可手动 Clean 后再查看。

通过以上路径,你可以系统性地定位并解决 STM32H7 上 D-Cache 与 DMA 的冲突问题,让高性能与数据一致性兼得。