STM32H7 的 D-Cache 与 DMA 数据一致性:从 Cache 行对齐到 Clean/Invalidate 时序的完整避坑指南

STM32H7 主频高达 480MHz,为了匹配内核与存储器速度,Cortex-M7 引入了 L1 D-Cache。但 DMA 直接访问物理内存,不经过 Cache,若软件不加以干预,就会出现「CPU 写的数据 DMA 读不到」或「DMA 写的数据 CPU 读到旧值」的经典问题。本文从原理到代码,帮你彻底理清。

1. 为什么 D-Cache 会与 DMA 冲突?

  • CPU 访问路径:CPU 读写数据时,若命中 Cache,则直接操作 Cache,不会立即写回 SRAM。
  • DMA 访问路径:DMA 控制器直接读写 SRAM,完全绕过 Cache。
  • 冲突场景
    • CPU 写缓冲区后启动 DMA 发送:数据可能还在 Cache 中(Write-Back 模式),DMA 读到旧数据。
    • DMA 接收数据到缓冲区后 CPU 读取:Cache 中可能缓存了该地址的旧数据,CPU 读到旧值。

因此,必须在 DMA 传输前后对 Cache 进行 Clean(写回)Invalidate(无效化) 操作。

2. Cache 行对齐:一切操作的基础

Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以行为单位,若缓冲区未对齐,会波及相邻数据。

  • 对齐要求:DMA 缓冲区地址必须 32 字节对齐,且长度建议为 32 字节的整数倍。
  • 未对齐的后果:Invalidate 可能丢弃相邻变量尚未写回的数据,导致程序跑飞。
  • 实现方法
    • 使用 __attribute__((aligned(32))) 定义缓冲区。
    • 或使用 ALIGN_32BYTES 宏(STM32 库提供)。
// 发送缓冲区:32 字节对齐,长度 64 字节
__attribute__((aligned(32))) uint8_t txBuf[64];
// 接收缓冲区
__attribute__((aligned(32))) uint8_t rxBuf[64];

3. Clean 与 Invalidate 的语义与时序

  • Clean(清理):将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 的场景(发送)。
  • Invalidate(无效化):将 Cache 中对应行标记为无效,下次 CPU 读取时从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景(接收)。
  • Clean+Invalidate:先写回再无效化,用于双向传输或不确定场景。

关键时序

  • 发送路径:CPU 填充缓冲区 → Clean D-Cache → 启动 DMA 发送。
  • 接收路径:启动 DMA 接收 → 等待 DMA 完成 → Invalidate D-Cache → CPU 读取数据。

注意:Invalidate 必须在 DMA 完成之后、CPU 读取之前执行,否则会丢失 DMA 刚写入的数据。

4. 配置步骤与完整代码示例

4.1 使能 D-Cache

void Enable_DCache(void) {
    SCB_EnableICache();  // 使能指令 Cache
    SCB_EnableDCache();  // 使能数据 Cache
}

4.2 MPU 配置(可选但推荐)

将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,可简化一致性处理。但会牺牲性能,需权衡。

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();
    // 配置 SRAM1 区域为 Non-Cacheable(示例)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    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;
    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_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.3 发送路径:Clean 后启动 DMA

__attribute__((aligned(32))) uint8_t txBuf[64];

void Send_Data(void) {
    // 1. 填充数据
    for (int i = 0; i < 64; i++) txBuf[i] = i;
    // 2. Clean D-Cache,确保数据写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t*)txBuf, sizeof(txBuf));
    // 3. 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, txBuf, sizeof(txBuf));
}

4.4 接收路径:DMA 完成后 Invalidate

__attribute__((aligned(32))) uint8_t rxBuf[64];
volatile uint8_t dma_done = 0;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 1. Invalidate D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuf, sizeof(rxBuf));
    // 2. 标记完成,允许主循环读取
    dma_done = 1;
}

void Process_Data(void) {
    if (dma_done) {
        // 此时 rxBuf 中已是 DMA 写入的最新数据
        for (int i = 0; i < 64; i++) {
            printf("%02X ", rxBuf[i]);
        }
        dma_done = 0;
    }
}

5. 常见陷阱与注意事项

  • 缓冲区未对齐:导致 Clean/Invalidate 误伤相邻数据。务必使用 aligned(32)
  • Invalidate 时机错误:在 DMA 完成前 Invalidate,会丢失 DMA 写入的数据。
  • 多缓冲区共享 Cache 行:若两个变量位于同一 32 字节行,Invalidate 一个会影响另一个。建议将 DMA 缓冲区单独对齐。
  • 栈上变量用于 DMA:栈变量地址不可控,极易未对齐,应使用静态或全局缓冲区。
  • Cache 维护操作耗时:频繁 Clean/Invalidate 会降低性能,可考虑 MPU 配置 Non-Cacheable 区域。
  • 中断中调用 Cache 维护函数SCB_* 函数本身可重入,但需注意时序,建议在 DMA 完成中断中执行 Invalidate。
  • 调试时关闭 Cache:若问题消失,基本可确定是 Cache 一致性问题。

6. 总结

STM32H7 的 D-Cache 与 DMA 共存需要遵循「对齐、Clean、Invalidate、时序」四原则。发送前 Clean,接收后 Invalidate,缓冲区 32 字节对齐,并严格保证操作顺序。对于性能敏感且数据量大的场景,可结合 MPU 将 DMA 区域设为 Non-Cacheable。掌握这些要点,即可避开绝大多数数据一致性陷阱。