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

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

  • 发送 DMA 传输完成,但目标内存中的数据部分正确、部分为旧值。
  • 接收 DMA 完成后,CPU 读到的数据与示波器/逻辑分析仪看到的不一致。
  • 关闭 D-Cache 后一切正常,开启后随机出错。
  • 调试时单步执行正常,全速运行出错。

这些现象高度指向同一个根因:D-Cache 与 DMA 的一致性冲突。

二、原理:为什么 Cache 会“骗”了 DMA?

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存)。CPU 访问内存时,数据可能被缓存在 Cache 中,而不是立即写回物理内存(SRAM)。

  • 写操作:CPU 写数据到 Cache,若未配置写穿透(Write-Through),数据可能停留在 Cache 中,物理内存仍是旧值。此时 DMA 从物理内存读取,得到的是旧数据。
  • 读操作:DMA 将新数据写入物理内存,但 CPU 之前已缓存了该地址的旧数据,CPU 再次读取时命中 Cache,读到旧值。

DMA 是“外设”,它直接访问物理内存,不经过 Cache。因此,CPU 与 DMA 对同一块内存的访问必须保证一致性。

三、排查流程:从现象到定位

步骤 1:确认是否由 Cache 引起

临时关闭 D-Cache,观察问题是否消失:

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

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

步骤 2:检查 DMA 缓冲区地址与属性

  • 确认 DMA 缓冲区是否位于可 Cache 的内存区域(如 AXI SRAM、DTCM)。
  • 注意:DTCM 默认不经过 Cache,但 DMA 通常无法访问 DTCM(部分 DMA 控制器不支持)。因此常用 AXI SRAM 作为 DMA 缓冲区,而 AXI SRAM 默认是可 Cache 的。

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

最优雅的方案是用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through。这样 CPU 访问该区域时绕过 Cache,DMA 与 CPU 看到的内存始终一致。

四、MPU 配置实战

以下代码将 AXI SRAM 的 0x24000000 起始的 32KB 区域配置为 Non-Cacheable,供 DMA 使用。

#include "stm32h7xx.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.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x24000000;
    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);

    // 区域 1:其余内存保持默认 Cache 属性(可选)
    // ...

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

在 main() 中,必须在使能 Cache 之前调用 MPU_Config():

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 先配置 MPU
    SCB_EnableICache();
    SCB_EnableDCache();    // 再使能 Cache
    // ...
}

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

若不想划分 MPU 区域,可在 DMA 传输前后手动操作 Cache:

  • DMA 发送前(CPU 写数据后):调用 SCB_CleanDCache_by_Addr() 将 Cache 数据写回内存。
  • DMA 接收后(CPU 读数据前):调用 SCB_InvalidateDCache_by_Addr() 使 Cache 失效,强制从内存重新加载。
// 发送前清理
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
HAL_DMA_Start(&hdma, (uint32_t)tx_buffer, (uint32_t)&UART->TDR, len);

// 接收后无效化
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 100);
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

注意:SCB_InvalidateDCache_by_Addr 的地址和长度必须按 Cache Line(32 字节)对齐,否则可能误伤相邻数据。

六、注意事项与避坑指南

  • MPU 配置必须在使能 Cache 前完成,否则不生效。
  • DMA 缓冲区地址要 32 字节对齐,避免 Cache Line 跨界问题。
  • 不要将 DTCM 用作 DMA 缓冲区,除非确认 DMA 控制器支持访问 DTCM(多数不支持)。
  • 使用 SCB_InvalidateDCache_by_Addr 时,确保没有其他变量与缓冲区共享同一 Cache Line,否则会丢失未写回的数据。
  • 若使用 Ethernet、USB 等自带 DMA 的外设,同样需要处理 Cache 一致性,方法相同。
  • 调试时若发现“单步正常、全速出错”,优先怀疑 Cache 与 DMA 冲突。

七、总结

STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。排查时先关闭 Cache 验证,再通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,或手动维护 Cache 一致性。推荐优先使用 MPU 方案,一劳永逸且不影响其他内存区域的性能。掌握这一流程,可显著提升 H7 系列开发的稳定性与效率。