一、为什么 Cache 和 DMA 会打架?

STM32H7 基于 Cortex-M7 内核,带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。开启 D-Cache 后,CPU 访问内存时可能直接命中 Cache,而不会立即写回主存(SRAM)。DMA 则直接访问主存,不经过 Cache。

这就导致两个经典问题:

  • DMA 读取时数据陈旧:CPU 修改了缓冲区数据,但只更新了 Cache,未写回 SRAM。DMA 从 SRAM 读取,拿到的是旧数据。
  • DMA 写入后 CPU 读到旧数据:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中仍保留着该地址的旧内容。CPU 读取时命中 Cache,得到旧数据。

此外,Cache 的写回操作可能以 Cache Line(32 字节) 为单位,若 DMA 缓冲区未按 Cache Line 对齐,Clean/Invalidate 操作可能误伤相邻数据,引发更难排查的错误。

二、解决思路:MPU 配置 + 手动维护

2.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable

通过 MPU(Memory Protection Unit)将 DMA 使用的内存区域设置为 Non-Cacheable, Non-Shareable,这样 CPU 访问该区域时绕过 Cache,从根本上避免一致性问题。

优点:一劳永逸,无需手动 Clean/Invalidate。
缺点:CPU 访问该区域速度下降,适合对性能不敏感的数据缓冲区。

2.2 方案二:保持 Cacheable,手动维护一致性

在 DMA 传输前后调用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr():

  • 发送前(CPU 写,DMA 读):Clean,将 Cache 数据写回 SRAM。
  • 接收后(DMA 写,CPU 读):Invalidate,丢弃 Cache 中的旧数据,强制从 SRAM 重新加载。

优点:CPU 访问缓冲区仍享受 Cache 加速。
缺点:需精确控制,且缓冲区必须按 Cache Line 对齐。

三、MPU 配置步骤与代码

以将 0x30000000 开始的 64KB 区域(D2 SRAM)配置为 Non-Cacheable 为例。

3.1 使能 MPU 并配置 Region

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 配置 Region 0:D2 SRAM 64KB 为 Non-Cacheable */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:IsCacheable = NOT_CACHEABLE 且 IsBufferable = NOT_BUFFERABLE 时,该区域为 Strongly Ordered 或 Device 内存,具体取决于 TEX 等位。对于普通 SRAM,推荐 TEX=0, C=0, B=0,即 Non-Cacheable Normal Memory。

3.2 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 必须在使能 Cache 前配置
    SCB_EnableICache();
    SCB_EnableDCache();
    // ... 其他初始化
}

四、手动 Clean/Invalidate 的正确姿势

若选择方案二,需注意以下要点:

4.1 缓冲区对齐

DMA 缓冲区必须按 32 字节(Cache Line 大小) 对齐,且长度最好是 32 的整数倍。

#define BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];

4.2 发送前 Clean

// CPU 填充数据后,DMA 发送前
memcpy(dma_buf, src, BUF_SIZE);
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_buf, BUF_SIZE);

4.3 接收后 Invalidate

// DMA 接收完成回调中
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
    // 此时 CPU 读取 dma_buf 才能拿到 DMA 写入的新数据
    process_data(dma_buf, BUF_SIZE);
}

4.4 关键陷阱:Invalidate 前先 Clean

如果缓冲区在 DMA 接收期间被 CPU 修改过(例如部分写入),直接 Invalidate 会丢弃这些修改。正确做法是先 Clean 再 Invalidate,或确保 CPU 不碰该区域。

SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);

五、注意事项与常见坑

  • MPU 配置必须在使能 Cache 之前完成,否则配置不生效。
  • DMA 描述符、链表等结构体若被 DMA 访问,也应放在 Non-Cacheable 区域或手动维护。
  • 多缓冲区(Ping-Pong) 时,每个缓冲区都要独立 Clean/Invalidate,不能只操作首地址。
  • Cache Line 对齐:若缓冲区未对齐,Clean/Invalidate 可能影响相邻变量,导致数据损坏。
  • 使用 SCB_InvalidateDCache_by_Addr 时地址必须 32 字节对齐,否则行为未定义。
  • 调试时:若发现 DMA 数据错乱,首先检查 Cache 配置和 Clean/Invalidate 调用时机。
  • 性能权衡:Non-Cacheable 区域访问速度约为 Cacheable 的 1/3~1/2,对高速数据流建议用方案二。

六、总结

STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典难题。核心原则:要么让 DMA 缓冲区绕过 Cache(MPU 配置),要么在 DMA 传输前后正确 Clean/Invalidate。无论哪种方案,都要保证缓冲区按 Cache Line 对齐,并注意操作顺序。掌握这些姿势,你就能在享受 H7 高性能的同时,避开数据一致性的坑。