一、为什么 STM32H7 上 DMA 会“翻车”

STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存),默认开启。CPU 读写内存时,实际访问的是 Cache,而不是直接访问 SRAM。DMA 控制器则绕过 Cache,直接读写 SRAM。

这就导致两个经典问题:

  • CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 里(Write-Back 策略),SRAM 中还是旧值,DMA 读走旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的却是 Cache 中的旧数据(Cache 命中)。

因此,必须在合适的时机对 Cache 做 Clean(写回) 或 Invalidate(无效化)。

二、Clean 与 Invalidate 到底做什么

  • Clean(清理):把 Cache 中“脏”数据写回 SRAM,保证 SRAM 与 Cache 一致。
  • Invalidate(无效化):把 Cache 行标记为无效,下次 CPU 读取时会从 SRAM 重新加载。

关键点:Invalidate 会丢弃 Cache 中尚未写回的数据。如果顺序错了,就会丢数据。

三、DMA 发送(内存 -> 外设)

数据流向:CPU 写缓冲区 -> DMA 从 SRAM 读到外设。

正确顺序:

  1. CPU 填充发送缓冲区。
  2. Clean 发送缓冲区对应的 Cache 行。
  3. 启动 DMA 发送。
#define TX_LEN 64
__attribute__((aligned(32))) uint8_t tx_buf[TX_LEN];

void uart_dma_send(void)
{
    for (int i = 0; i < TX_LEN; i++) {
        tx_buf[i] = i;
    }

    /* 关键:把 CPU 写入的数据写回 SRAM */
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_LEN);

    HAL_UART_Transmit_DMA(&huart1, tx_buf, TX_LEN);
}

注意:发送前只需要 Clean,不需要 Invalidate。Invalidate 反而可能丢掉刚写的数据。

四、DMA 接收(外设 -> 内存)

数据流向:DMA 把外设数据写入 SRAM -> CPU 读取。

正确顺序:

  1. 准备接收缓冲区(可先 Invalidate,防止 Cache 中有旧数据)。
  2. 启动 DMA 接收。
  3. DMA 完成中断中,Invalidate 接收缓冲区。
  4. CPU 读取数据。
#define RX_LEN 64
__attribute__((aligned(32))) uint8_t rx_buf[RX_LEN];

void uart_dma_start_recv(void)
{
    /* 启动前无效化,避免 Cache 命中旧数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);

    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    /* 接收完成后无效化,让 CPU 读到 DMA 写入的新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);

    /* 此时读取 rx_buf 才是安全的 */
    process_data(rx_buf, RX_LEN);
}

五、最容易踩的坑:顺序与地址对齐

  • 坑 1:接收完成后先读再 Invalidate。CPU 读到的是 Cache 旧值,数据看似“没更新”。
  • 坑 2:发送前做了 Invalidate。可能把尚未写回的数据丢掉,DMA 发出错误内容。
  • 坑 3:地址未按 Cache Line 对齐。Cortex-M7 的 Cache Line 为 32 字节,操作长度也应是 32 的倍数,否则会误伤相邻数据。
  • 坑 4:在中断中频繁 Clean/Invalidate 大块内存。会影响实时性,建议只操作实际使用的缓冲区。

六、更稳妥的替代方案

如果不想手动维护 Cache,可以考虑:

  • 把 DMA 缓冲区放到 非 Cache 区域(如通过 MPU 配置为 Device 或 Non-cacheable 内存)。
  • 使用 MPU 将特定 SRAM 区域设为 Write-Through 或 Non-cacheable。
  • 使用 ST 提供的 SCB_*DCache_by_Addr 函数,并确保 32 字节对齐。

七、总结

记住一句话:

  • CPU 写、DMA 读:先 Clean,再启动 DMA。
  • DMA 写、CPU 读:DMA 完成后先 Invalidate,再读数据。

Cache 与 DMA 一致性问题的本质是“谁先看到数据”。只要顺序正确、地址对齐,STM32H7 的高性能 DMA 就能稳定工作。