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

STM32H7 基于 Cortex-M7 内核,主频可达 480MHz,为了弥补 CPU 与存储器之间的速度差距,引入了 L1 D-Cache。CPU 访问数据时,会先将数据从 SRAM 加载到 Cache 行(通常 32 字节)中,后续访问直接命中 Cache,从而大幅提升性能。

但 DMA 是“独立于 CPU”的搬运工,它直接访问 SRAM,不经过 Cache。这就导致两个问题:

  • CPU 写数据到 Cache,但未写回 SRAM:DMA 从 SRAM 读到的还是旧数据。
  • DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据:CPU 看不到 DMA 的更新。

因此,必须手动维护 Cache 一致性:在 DMA 读取前,将 CPU 写的数据 Clean(写回 SRAM);在 DMA 写入后,将 CPU 中对应的 Cache 行 Invalidate(丢弃,强制从 SRAM 重新加载)。

二、地址对齐:32 字节的硬性要求

Cortex-M7 的 Cache 行大小为 32 字节。Clean 和 Invalidate 操作都是以 Cache 行为单位的。如果 DMA 缓冲区地址或长度没有 32 字节对齐,就会误伤相邻数据。

  • 错误示例uint8_t buf[100] 定义在 0x20000004,长度 100 字节。Clean 时,CPU 会写回包含 0x20000000~0x2000001F 的整行,可能把其他变量也写回,若其他变量被 DMA 修改过,就会覆盖新值。
  • 正确做法:使用 __attribute__((aligned(32))) 将 DMA 缓冲区强制 32 字节对齐,且长度建议为 32 的整数倍。
// 定义 DMA 缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[256];

三、Clean 与 Invalidate 的时序

正确的操作顺序取决于数据流向:

  • CPU 写 → DMA 读(发送):先 Clean,再启动 DMA。
  • DMA 写 → CPU 读(接收):先启动 DMA,等 DMA 完成后再 Invalidate。

绝对禁止:在 DMA 传输过程中 Invalidate,因为此时 Cache 中可能还有未写回的数据,Invalidate 会直接丢弃,导致数据丢失。

四、实测数据损坏复现

下面通过一个简单实验复现问题:使用 DMA 将内存数据搬到另一块内存,CPU 先写入源缓冲区,然后启动 DMA,最后检查目标缓冲区。

4.1 错误代码(无 Cache 维护)

__attribute__((aligned(32))) uint8_t src[256];
__attribute__((aligned(32))) uint8_t dst[256];

void test_dma_no_cache(void) {
    for (int i = 0; i < 256; i++) src[i] = i;  // CPU 写 src
    // 启动 DMA 搬运 src -> dst
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
    HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);
    // 检查 dst
    for (int i = 0; i < 256; i++) {
        if (dst[i] != i) {
            printf("Error at %d: expected %d, got %d\n", i, i, dst[i]);
            break;
        }
    }
}

现象:由于 src 在 Cache 中,DMA 从 SRAM 读到的 src 是旧值(可能全 0),导致 dst 数据错误。

4.2 正确代码(Clean + Invalidate)

__attribute__((aligned(32))) uint8_t src[256];
__attribute__((aligned(32))) uint8_t dst[256];

void test_dma_with_cache(void) {
    for (int i = 0; i < 256; i++) src[i] = i;
    // 1. Clean src,确保 SRAM 中数据最新
    SCB_CleanDCache_by_Addr((uint32_t*)src, 256);
    // 2. 启动 DMA
    HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
    HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);
    // 3. Invalidate dst,丢弃 Cache 中旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)dst, 256);
    // 4. 检查 dst
    for (int i = 0; i < 256; i++) {
        if (dst[i] != i) {
            printf("Error at %d: expected %d, got %d\n", i, i, dst[i]);
            break;
        }
    }
}

注意SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,长度建议为 32 的倍数。

五、配置步骤与注意事项

5.1 配置步骤

  1. 使能 D-Cache:在 SystemInitmain 开头调用 SCB_EnableDCache()
  2. 定义 DMA 缓冲区:使用 __attribute__((aligned(32))) 对齐,长度设为 32 的倍数。
  3. 根据数据流向维护 Cache
    • 发送:先 SCB_CleanDCache_by_Addr,再启动 DMA。
    • 接收:启动 DMA,等待完成,再 SCB_InvalidateDCache_by_Addr
  4. 避免在 DMA 传输中操作 Cache

5.2 注意事项

  • 不要使用 SCB_InvalidateDCache() 全局无效化:会清掉所有 Cache,影响性能,且可能误伤其他数据。
  • DMA 描述符也要对齐:如果使用链表模式,描述符本身也需 32 字节对齐并维护 Cache。
  • MPU 配置:可将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,但会牺牲性能,需权衡。
  • 调试时关闭 Cache:如果问题难以复现,可暂时关闭 D-Cache 对比测试。

六、总结

STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。核心原则:谁写谁 Clean,谁读谁 Invalidate,地址对齐 32 字节,时序不能乱。掌握这些,就能在享受 H7 高性能的同时,保证数据一致性。