STM32H7 的 DCache 与 DMA 数据一致性排查:从 Cache 维护到 MPU 配置的完整路径

1. 问题现象与根因

在 STM32H7 上启用 DCache 后,DMA 传输经常出现以下诡异现象:

  • DMA 发送缓冲区数据部分正确、部分为旧值
  • DMA 接收完成后,CPU 读到的数据仍是传输前的旧内容
  • 偶发 HardFault,地址看似合法

根因在于 Cortex-M7 的 DCache 与 DMA 控制器访问内存的路径不同:

  • CPU 读写可能命中 Cache,不直接落到 SRAM
  • DMA 直接访问 SRAM,绕过 Cache

因此当 CPU 写数据到 Cache 但未回写 SRAM 时,DMA 读到的是旧数据;当 DMA 写入新数据到 SRAM,而 CPU 读的是 Cache 中的旧副本时,CPU 读到旧数据。

一句话:Cache 与 DMA 看到的内存不是同一份。

2. 三种解决思路对比

| 方案 | 原理 | 适用场景 | 性能影响 | |------|------|----------|----------| | 手动维护 | 传输前后 Clean/Invalidate | 临时缓冲区、小数据量 | 每次传输有开销 | | MPU Write-Through | 写操作同时更新 SRAM | DMA 发送缓冲区 | 写性能略降 | | MPU Non-Cacheable | 该区域完全绕过 Cache | DMA 收发缓冲区 | 读写均变慢 |

推荐做法:为 DMA 缓冲区单独划分 MPU Non-Cacheable 区域,一劳永逸。

3. 方案一:手动 Cache 维护

3.1 关键 API

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

3.2 使用规则

  • DMA 发送前:CPU 写完数据 → Clean,把 Cache 刷到 SRAM
  • DMA 接收后:Invalidate,丢弃 Cache 旧副本,强制从 SRAM 重读
  • DMA 接收前:若缓冲区可能被 Cache 缓存,先 Invalidate

3.3 完整示例(UART DMA 发送)

#define TX_BUF_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[TX_BUF_SIZE];

void uart_dma_send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    /* 关键:发送前 Clean,确保 SRAM 数据最新 */
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

3.4 注意事项

  • 地址必须 32 字节对齐,长度建议按 32 字节向上取整
  • 不要对同一缓冲区同时 Clean 和 Invalidate 造成竞争
  • 传输中不要修改缓冲区,否则 Cache 与 DMA 再次不一致

4. 方案二:MPU 配置 Non-Cacheable 区域

4.1 原理

通过 MPU 将某段 SRAM 标记为 Non-Cacheable,CPU 访问该区域时直接读写 SRAM,与 DMA 完全一致,无需手动维护。

4.2 配置步骤

  1. 在链接脚本中预留专用段,例如 0x30000000 起 32KB
  2. 使能 MPU 并配置 Region
  3. 将该段变量放入指定 section

4.3 完整代码

#include "stm32h7xx_hal.h"

/* 定义 DMA 专用段,放在 SRAM 的 0x30000000 */
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[1024];

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_Init = {0};
    MPU_Init.Enable           = MPU_REGION_ENABLE;
    MPU_Init.Number           = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress      = 0x30000000;
    MPU_Init.Size             = MPU_REGION_SIZE_32KB;
    MPU_Init.SubRegionDisable = 0x00;
    MPU_Init.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_Init.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_Init.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_Init.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;  /* 关键 */
    MPU_Init.IsBufferable     = MPU_ACCESS_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_Init);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

链接脚本片段(GCC):

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

4.4 注意事项

  • MPU 配置必须在 main() 早期、任何 DMA 使用前完成
  • 区域大小与基地址必须对齐(32KB 区域基地址需 32KB 对齐)
  • 若使用 D2 SRAM,注意时钟使能 __HAL_RCC_D2SRAM1_CLK_ENABLE()

5. 方案三:Write-Through 折中

若 DMA 缓冲区读写频繁,可配置为 Write-Through:

MPU_Init.IsCacheable  = MPU_ACCESS_CACHEABLE;
MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;  /* Write-Through */

写操作同时更新 Cache 和 SRAM,DMA 发送安全;但 DMA 接收仍需 Invalidate。

6. 排查清单

遇到 DMA 数据异常时,按顺序检查:

  • [ ] DCache 是否已使能(SCB_EnableDCache())
  • [ ] DMA 缓冲区是否 32 字节对齐
  • [ ] 发送前是否 Clean,接收后是否 Invalidate
  • [ ] MPU 区域是否覆盖缓冲区地址
  • [ ] 链接脚本段是否真正映射到目标 SRAM
  • [ ] 是否在 DMA 传输中修改了缓冲区

7. 总结

STM32H7 的 DCache 是性能利器,但必须与 DMA 协同。小数据量用手动 Clean/Invalidate,大数据流用 MPU Non-Cacheable 区域,是工程上最稳妥的组合。理解“Cache 与 DMA 看到的内存不是同一份”这一本质,所有现象都能迎刃而解。