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

STM32H7 的 Cortex-M7 内核包含 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。D-Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读外部 RAM,大幅提升性能。但 DMA 是独立于 CPU 的外设,它直接访问物理内存,不经过 Cache。

这就带来两个经典问题:

  • CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中(Write-Back 模式),尚未写回 RAM。此时启动 DMA 读,DMA 读到的是旧数据。
  • DMA 写、CPU 读:DMA 将新数据写入 RAM,但 CPU 的 Cache 中可能还保留着该地址的旧数据。CPU 读时命中 Cache,读到旧值。

根本原因:Cache 与 RAM 之间可能存在数据副本不一致。

二、Clean 与 Invalidate 的本质

Cortex-M7 提供两条关键维护指令:

  • Clean(清理):将 Cache 中“脏”数据(已修改但未写回)写回 RAM。执行后 Cache 行变为干净,但数据仍在 Cache 中。
  • Invalidate(无效化):将 Cache 行标记为无效。下次 CPU 访问会强制从 RAM 重新加载。

注意:Invalidate 会丢弃 Cache 中未写回的数据!如果 Cache 行是脏的,直接 Invalidate 会导致数据丢失。

三、正确操作顺序(黄金法则)

根据数据流向,操作顺序不同:

3.1 CPU 写 → DMA 读(发送方向)

  1. CPU 填充缓冲区。
  2. Clean 缓冲区对应的 Cache 行(确保数据写回 RAM)。
  3. 启动 DMA 传输。
// 假设 buffer 已填充数据,长度 len
SCB_CleanDCache_by_Addr((uint32_t *)buffer, len);
// 然后启动 DMA
HAL_DMA_Start(&hdma, (uint32_t)buffer, (uint32_t)&periph->DR, len);

3.2 DMA 写 → CPU 读(接收方向)

  1. 启动 DMA 传输(DMA 直接写 RAM)。
  2. 等待 DMA 完成。
  3. Invalidate 缓冲区对应的 Cache 行(丢弃旧 Cache,强制从 RAM 重载)。
  4. CPU 读取数据。
// 启动 DMA 接收
HAL_DMA_Start(&hdma, (uint32_t)&periph->DR, (uint32_t)buffer, len);
// 等待完成...
SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, len);
// 现在 CPU 读取 buffer 是 DMA 写入的新数据

3.3 双向传输(如 SPI 全双工)

先 Clean 发送缓冲区,再启动 DMA,完成后 Invalidate 接收缓冲区。

SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, tx_len);
HAL_DMA_Start(&hdma_tx, ...);
HAL_DMA_Start(&hdma_rx, ...);
// 等待完成
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);

四、实战配置步骤

4.1 使能 Cache

在 main() 初始化阶段使能 D-Cache 和 I-Cache:

SCB_EnableICache();
SCB_EnableDCache();

4.2 配置 MPU(可选但推荐)

对于 DMA 缓冲区,可配置 MPU 为 Write-Through 或 Non-Cacheable,避免手动维护。但注意:Non-Cacheable 会降低 CPU 访问性能。

MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM
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 缓冲区对齐

Cache 操作以 32 字节为单位。缓冲区地址和长度必须 32 字节对齐,否则会误伤相邻数据。

__attribute__((aligned(32))) uint8_t dma_buffer[256];

五、完整代码示例(UART DMA 接收)

#include "stm32h7xx_hal.h"

#define RX_LEN 64
__attribute__((aligned(32))) uint8_t rx_buf[RX_LEN];

void uart_dma_receive(void)
{
    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 关键:无效化 Cache,确保 CPU 读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);
        // 处理数据
        process_data(rx_buf, RX_LEN);
    }
}

六、注意事项与常见坑

  • Invalidate 前必须确保 Cache 行不脏:如果 CPU 曾写过该缓冲区,先 Clean 再 Invalidate,否则数据丢失。
  • 地址和长度必须 32 字节对齐:否则会无效化相邻数据,导致难以调试的随机错误。
  • 不要在 DMA 传输过程中操作 Cache:传输中 Invalidate 可能导致 DMA 写入的数据被丢弃。
  • 多缓冲区场景:每个缓冲区独立维护,不要遗漏。
  • 使用 SCB_InvalidateDCache_by_Addr 而非 SCB_InvalidateDCache:后者会无效化整个 Cache,影响性能且可能误伤其他数据。
  • 调试时关闭 Cache 对比:如果关闭 Cache 后问题消失,基本可确定是 Cache 一致性问题。
  • 注意 D2 SRAM 的默认属性:部分区域默认 Non-Cacheable,需查阅参考手册确认。

七、实战排查思路

  1. 现象:DMA 接收数据偶尔错乱、发送数据部分正确。
  2. 第一步:关闭 D-Cache,看问题是否消失。若消失,锁定 Cache 问题。
  3. 第二步:检查缓冲区是否 32 字节对齐。
  4. 第三步:检查 Clean/Invalidate 调用顺序和位置。
  5. 第四步:用调试器查看 RAM 实际内容与 CPU 读取值是否一致。
  6. 第五步:考虑用 MPU 将 DMA 缓冲区设为 Non-Cacheable,彻底规避。

掌握 Cache 与 DMA 的一致性维护,是 STM32H7 高性能应用开发的必修课。理解原理、遵循顺序、注意对齐,就能避开绝大多数坑。