STM32H7 的 D-Cache 与 DMA 一致性:Cache Clean/Invalidate 的实操与踩坑

1. 为什么 DMA 和 D-Cache 会打架?

STM32H7 的 Cortex-M7 主频高达 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM。

DMA 则是一个“独立搬运工”,它直接访问 SRAM,不经过 Cache。于是问题来了:

  • CPU 写数据到 SRAM 后,数据可能还在 Cache 里,没写回 SRAM。此时启动 DMA 发送,DMA 读到的就是旧数据。
  • DMA 从外设接收数据写入 SRAM 后,CPU 读该地址时,可能读到 Cache 里的旧数据,而不是 DMA 刚写入的新数据。

这就是 Cache 一致性(Coherency) 问题。解决思路有两种:

  • 硬件层面:使用 MPU 将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable。
  • 软件层面:在 DMA 传输前后,手动执行 Cache Clean(写回)Cache Invalidate(无效化)

本文重点讲软件层面的实操,因为它在不改动 MPU 全局配置时最灵活。

2. Cache Clean 与 Invalidate 到底做了什么?

  • Clean(清理/写回):把 Cache 中“脏”的数据写回 SRAM。用于 CPU 写、DMA 读 的场景。
  • Invalidate(无效化):把 Cache 中的对应行标记为无效。下次 CPU 读时会强制从 SRAM 重新加载。用于 DMA 写、CPU 读 的场景。

注意:Invalidate 不会把数据写回 SRAM,它只是丢弃 Cache 内容。如果 Cache 中有未写回的数据,Invalidate 会导致数据丢失!

3. 典型场景与操作顺序

场景一:CPU 填充缓冲区 → DMA 发送

// 1. CPU 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
    tx_buf[i] = i;
}

// 2. Clean:确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);

// 3. 启动 DMA 发送
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, BUF_SIZE);

场景二:DMA 接收数据 → CPU 读取

// 1. 启动 DMA 接收
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)rx_buf, BUF_SIZE);

// 2. 等待 DMA 完成(中断或轮询)
while (!dma_done);

// 3. Invalidate:丢弃 Cache 旧数据,强制从 SRAM 读
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);

// 4. CPU 读取数据
process_data(rx_buf);

场景三:双向 DMA(如 SPI 全双工)

先 Clean 发送缓冲区,再 Invalidate 接收缓冲区,顺序不能错。

4. 完整代码示例(基于 HAL 库)

#include "stm32h7xx_hal.h"

#define BUF_SIZE 64

// 注意:缓冲区必须 32 字节对齐!
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

DMA_HandleTypeDef hdma_tx;
DMA_HandleTypeDef hdma_rx;

void dma_send_with_cache(void)
{
    // 填充数据
    for (int i = 0; i < BUF_SIZE; i++) {
        tx_buf[i] = i;
    }

    // Clean D-Cache:将 tx_buf 写回 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);

    // 启动 DMA 发送
    HAL_DMA_Start(&hdma_tx, (uint32_t)tx_buf, (uint32_t)&UART4->TDR, BUF_SIZE);
}

void dma_receive_with_cache(void)
{
    // 启动 DMA 接收
    HAL_DMA_Start(&hdma_rx, (uint32_t)&UART4->RDR, (uint32_t)rx_buf, BUF_SIZE);

    // 等待完成(实际项目建议用中断)
    while (HAL_DMA_PollForTransfer(&hdma_rx, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);

    // Invalidate D-Cache:丢弃 rx_buf 的 Cache 行
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);

    // 此时 CPU 读取 rx_buf 才是 DMA 写入的新数据
    for (int i = 0; i < BUF_SIZE; i++) {
        printf("%02X ", rx_buf[i]);
    }
}

5. 踩坑与注意事项

  • 地址必须 32 字节对齐:Cortex-M7 的 Cache 行大小是 32 字节。SCB_CleanDCache_by_Addr 内部会按行操作,如果地址不对齐,可能误伤相邻数据。建议用 __attribute__((aligned(32))) 修饰缓冲区。
  • 长度也要按 32 字节对齐:如果长度不是 32 的倍数,函数会向上取整到 Cache 行边界,可能清理/无效化到缓冲区之外的数据。最好让 BUF_SIZE 是 32 的倍数。
  • Invalidate 前必须确保 Cache 中没有脏数据:如果 CPU 刚写过该缓冲区,直接 Invalidate 会丢失未写回的数据。正确做法是先 Clean 再 Invalidate,或者用 SCB_CleanInvalidateDCache_by_Addr
  • DMA 传输期间不要操作缓冲区:DMA 工作时 CPU 若访问同一缓冲区,可能触发 Cache 行填充,导致数据错乱。
  • MPU 配置是更彻底的方案:如果不想每次手动维护 Cache,可以用 MPU 将 DMA 缓冲区设为 Non-Cacheable 或 Write-Through。但注意 Non-Cacheable 会降低 CPU 访问性能。
  • 中断中调用 Cache 维护函数要小心SCB_CleanDCache_by_Addr 等函数执行时间较长,在高速中断中可能影响实时性。

6. 总结

STM32H7 的 D-Cache 是性能利器,但和 DMA 配合时必须处理好一致性。记住一个口诀:CPU 写、DMA 读 → Clean;DMA 写、CPU 读 → Invalidate。同时注意 32 字节对齐和操作顺序,就能避开大多数坑。如果项目对性能要求极高,建议结合 MPU 配置,从硬件层面减少软件维护开销。