一、现象:DMA 数据为何“随机”出错?

在 STM32H7 上开发时,你可能遇到这样的场景:ADC 用 DMA 搬运数据到数组,CPU 读取时数值偶尔跳变;或 UART 发送 DMA 传输字符串,接收端出现乱码。调试时发现,关闭 DCache 后一切正常,开启后问题复现。

根本原因:Cortex-M7 的 DCache 与 DMA 控制器访问内存的路径不同。DMA 直接读写物理内存(SRAM),而 CPU 可能从 Cache 中读取旧数据,或写入的数据还停留在 Cache 未同步到 SRAM。这就是Cache 一致性(Coherency)问题

二、原理:DCache 如何“捣乱”

  • 读操作:CPU 读取某地址时,若 Cache 未命中,会从 SRAM 加载数据到 Cache 行(通常 32 字节)。之后 DMA 更新了 SRAM 中该地址的数据,但 CPU 再次读取时仍命中旧 Cache,得到过时值。
  • 写操作:CPU 写入数据到 Cache(写回策略),并未立即写入 SRAM。若此时启动 DMA 发送该缓冲区,DMA 读到的是 SRAM 中的旧内容。
  • Cache 行共享:一个 Cache 行包含多个变量,DMA 只更新其中一部分,CPU 读取其他变量时可能触发整行回写,覆盖 DMA 刚写入的数据。

三、排查流程:从快速验证到精准配置

步骤 1:关闭 DCache 验证问题

main() 初始化后调用 SCB_DisableDCache(),若问题消失,则确认为 Cache 一致性冲突。

#include "core_cm7.h"
SCB_DisableDCache();  // 仅用于验证,正式产品不建议全局关闭

步骤 2:使用 SCB 维护函数(临时方案)

在 DMA 传输前后手动维护 Cache:

  • DMA 发送前(CPU 写数据):调用 SCB_CleanDCache_by_Addr() 将 Cache 写回 SRAM。
  • DMA 接收后(CPU 读数据):调用 SCB_InvalidateDCache_by_Addr() 丢弃 Cache,强制从 SRAM 重新加载。
#define BUF_SIZE 64
uint8_t dma_buf[BUF_SIZE] __attribute__((aligned(32))); // 32 字节对齐

// 发送前
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_buf, BUF_SIZE);

// 接收后(在 DMA 完成中断中)
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 此时 CPU 读取 dma_buf 才是最新数据

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

步骤 3:MPU 配置——终极解决方案

手动维护 Cache 易遗漏且影响性能。更优雅的方式是用 MPU(内存保护单元)将 DMA 缓冲区所在内存区域配置为 Write-Through, No Write AllocateNon-Cacheable,让该区域数据不经过 DCache,从根本上避免一致性问题。

3.1 确定缓冲区地址与大小

假设使用 0x30000000 开始的 64KB SRAM(D2 域 SRAM1),定义缓冲区:

#define DMA_BUF_ADDR  0x30000000
#define DMA_BUF_SIZE  0x10000  // 64KB

3.2 配置 MPU 区域

#include "stm32h7xx_hal.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;  // 关键:禁用 Cache
    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);
}

调用 MPU_Config() 后,该区域所有访问(CPU 和 DMA)都直接操作 SRAM,无需手动维护 Cache。

3.3 将缓冲区定位到该区域

使用链接脚本或 __attribute__((section(".dma_buffer"))) 将数组放到指定地址。

uint8_t dma_buf[BUF_SIZE] __attribute__((section(".dma_buffer"), aligned(32)));

并在链接脚本中定义:

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

四、完整示例:ADC + DMA + DCache 安全读取

#include "stm32h7xx_hal.h"

#define ADC_BUF_SIZE 128
uint16_t adc_buf[ADC_BUF_SIZE] __attribute__((section(".dma_buffer"), aligned(32)));

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void System_Init(void)
{
    HAL_Init();
    MPU_Config();          // 先配置 MPU
    SCB_EnableDCache();    // 再开启 DCache
    // ... 初始化时钟、ADC、DMA
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
}

// DMA 完成回调中直接读取,无需 Invalidate
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if (hadc->Instance == ADC1) {
        // adc_buf 位于 Non-Cacheable 区域,数据已是最新
        uint32_t sum = 0;
        for (int i = 0; i < ADC_BUF_SIZE; i++) {
            sum += adc_buf[i];
        }
        // 处理 sum...
    }
}

五、注意事项与避坑指南

  • MPU 区域对齐:基地址必须按区域大小对齐(如 64KB 区域基地址需 64KB 对齐)。
  • Cache 维护函数地址对齐SCB_CleanDCache_by_Addr 要求地址 32 字节对齐,长度建议为 32 的倍数。
  • 避免全局关闭 DCache:会严重降低性能,仅用于调试。
  • 多缓冲区场景:若多个 DMA 缓冲区相邻,建议统一放在同一个 Non-Cacheable 区域,避免 Cache 行共享冲突。
  • DMA 描述符:若使用链表式 DMA,描述符本身也需放在 Non-Cacheable 区域。
  • 性能权衡:Non-Cacheable 区域访问速度较慢,仅将 DMA 缓冲区放入,其他代码和数据仍享受 Cache 加速。

通过以上流程,你可以从现象快速定位到根因,并用 MPU 配置一劳永逸地解决 STM32H7 的 DCache 与 DMA 冲突问题。