一、问题现象:DMA 数据错乱

最近在 STM32H743 上调试一个 ADC 连续采样 + DMA 搬运到内存缓冲区的功能。代码逻辑很简单:ADC 规则通道连续转换,DMA 循环模式将数据搬到 adc_buffer[256],主循环读取缓冲区做处理。但实际运行时,缓冲区数据经常出现“半新半旧”或完全错乱,而单步调试时又正常。

初步怀疑:DMA 配置错误?ADC 时钟太快?内存对齐?排查一圈后,最终定位到 MPU(Memory Protection Unit)配置错误导致 Cache 与 DMA 不一致

二、原理:Cortex-M7 的 Cache 与 DMA 冲突

STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。Cache 的存在是为了弥补 CPU 与主存之间的速度差,但 DMA 直接访问物理内存,不经过 Cache

  • 写操作:CPU 写数据到缓冲区,数据可能只停留在 Cache 中,未写回主存。此时 DMA 从主存读取,得到的是旧数据。
  • 读操作:DMA 将新数据写入主存,但 CPU 读取时若 Cache 中已有旧数据副本,则读到旧值。

这就是典型的 Cache 一致性(Coherency)问题。解决方法通常有三种:

  1. 将 DMA 缓冲区所在内存区域配置为 Write-Through(写透)Non-Cacheable(不可缓存)
  2. 在 DMA 传输前后手动执行 Cache 清理(Clean)无效化(Invalidate) 操作。
  3. 使用 MPU 将特定区域设为 Non-Cacheable,让 CPU 和 DMA 都直接访问主存。

本例中,我原本想用 MPU 将 0x24000000(AXI SRAM)区域设为 Non-Cacheable,但配置时属性写错,导致该区域仍然是 Cacheable,从而引发数据错乱。

三、排查过程:从现象到根因

  1. 确认 DMA 配置:检查 DMA 流、通道、优先级、数据宽度、循环模式,均正确。
  2. 检查内存对齐:缓冲区 32 字节对齐,符合 Cache 行大小(32 字节)。
  3. 关闭 D-Cache 测试:在 main() 开头调用 SCB_DisableDCache(),数据立即正常。这证实了 Cache 一致性问题是元凶。
  4. 检查 MPU 配置:发现 MPU 区域属性中 TEX=0, C=1, B=0,即 Write-Back, Write-Allocate,这是可缓存的。而我本意是 Non-Cacheable(应设 TEX=1, C=0, B=0)。
  5. 修正 MPU 配置:重新配置区域属性为 Non-Cacheable,问题消失。

四、MPU 配置步骤与完整代码

以下是基于 STM32H7 HAL 库的 MPU 配置示例,将 AXI SRAM 的 0x24000000 起始的 512KB 区域设为 Non-Cacheable。

#include "stm32h7xx_hal.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 设置 AXI SRAM 区域为 Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress = 0x24000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;  // TEX=1
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // C=0
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // B=0

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 必须在使能 Cache 前配置
    SCB_EnableICache();
    SCB_EnableDCache();

    // 后续初始化 ADC、DMA 等
    // ...
    while (1) { }
}

关键点

  • MPU_TEX_LEVEL1 + NOT_CACHEABLE + NOT_BUFFERABLE 组合得到 Non-Cacheable 属性。
  • MPU 配置必须在 SCB_EnableDCache() 之前完成,否则可能不生效。
  • 如果使用 FreeRTOS 等 RTOS,需确保任务堆栈和 DMA 缓冲区不在同一可缓存区域。

五、替代方案:手动 Cache 维护

如果不想将整个区域设为 Non-Cacheable,可以在 DMA 传输前后手动维护 Cache:

// DMA 发送前:将 CPU 写入的数据从 Cache 写回主存
SCB_CleanDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));

// DMA 接收后:无效化 Cache,使 CPU 重新从主存读取
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));

注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度也建议 32 字节对齐,否则可能误伤相邻数据。

六、注意事项与避坑指南

  • MPU 区域属性TEX=1, C=0, B=0 才是 Non-Cacheable;TEX=0, C=1, B=0 是 Write-Back,仍会缓存。
  • Cache 行大小:Cortex-M7 的 Cache 行是 32 字节,DMA 缓冲区地址和长度最好按 32 字节对齐。
  • 多区域重叠:MPU 区域不能重叠,否则行为未定义。
  • DMA 访问权限:确保 MPU 区域允许 DMA 访问(通常 Full Access)。
  • 调试技巧:遇到疑似 Cache 问题,先关闭 D-Cache 验证,再逐步开启并配置 MPU。
  • RTOS 环境:任务切换时可能触发 Cache 维护,需仔细设计内存布局。

七、总结

STM32H7 的 Cache 大幅提升性能,但也给 DMA 带来了数据一致性挑战。通过 MPU 将 DMA 缓冲区设为 Non-Cacheable 是最简单可靠的方案,但必须正确配置 TEX、C、B 位。本次排查历时两天,最终发现是 MPU 属性写错,希望本文能帮你少走弯路。记住:Cache 与 DMA 共存时,一致性永远是第一优先级