一、为什么 H7 上 DMA 会“失灵”?

STM32H7 采用 Cortex-M7 内核,带 16KB D-Cache 和 16KB I-Cache。开启 D-Cache 后,CPU 访问 SRAM 会先经过 Cache。而 DMA 是直接访问物理 SRAM 的,它看不见 Cache

这会导致两类问题:

  • DMA 读(外设→内存):DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 里的旧数据。
  • DMA 写(内存→外设):CPU 写的数据还在 Cache 中未回写到 SRAM,DMA 却从 SRAM 读到旧数据。

解决思路只有两个:使用非 Cache 区域(MPU 配置)手动维护 Cache 一致性(Clean/Invalidate)。本文重点讲后者。

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

Cortex-M7 的 Cache Line 为 32 字节。Clean 和 Invalidate 操作都以 Cache Line 为单位,因此 DMA 缓冲区必须 32 字节对齐,且大小最好是 32 的整数倍。

/* 推荐:使用编译器属性强制对齐 */
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];

如果缓冲区未对齐,Invalidate 时可能误伤相邻变量,导致“莫名其妙”的变量被清零——这是最隐蔽的坑之一。

三、Clean 与 Invalidate 的正确顺序

先明确两个操作的含义:

  • Clean:把 Cache 中“脏”数据写回 SRAM。
  • Invalidate:丢弃 Cache 内容,下次读时从 SRAM 重新加载。

3.1 外设 → 内存(DMA 接收)

DMA 写入 SRAM 前,CPU 可能已经读过该缓冲区,Cache 中有旧副本。正确顺序:

  1. Invalidate 缓冲区(丢弃旧 Cache)
  2. 启动 DMA 接收
  3. DMA 完成中断中再次 Invalidate(确保读到最新数据)
/* 启动接收前 */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, sizeof(dma_rx_buf));

/* DMA 完成回调中 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
    /* 此时读取 dma_rx_buf 才是最新数据 */
}

3.2 内存 → 外设(DMA 发送)

CPU 写入缓冲区后数据可能还在 Cache 中,必须 Clean 后才能启动 DMA:

memcpy(dma_tx_buf, src, len);
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, sizeof(dma_tx_buf));
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);

关键原则:Clean 用于“CPU 写、DMA 读”;Invalidate 用于“DMA 写、CPU 读”。顺序错误会导致数据丢失或读到旧值。

四、完整示例:UART DMA 收发

#include "stm32h7xx_hal.h"

#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];

void uart_dma_init(void)
{
    /* 启动接收:先 Invalidate */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

void uart_dma_send(uint8_t *data, uint16_t len)
{
    if (len > BUF_SIZE) return;
    memcpy(tx_buf, data, len);
    /* 发送前 Clean */
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    /* 接收完成:Invalidate 后再处理 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    process_data(rx_buf, BUF_SIZE);
    /* 重新启动接收 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

五、典型踩坑与注意事项

  • 坑 1:缓冲区未 32 字节对齐。Invalidate 会清掉相邻变量,表现为“无关变量被改”。务必用 __attribute__((aligned(32)))
  • 坑 2:Clean 和 Invalidate 顺序颠倒。发送前 Invalidate 会把 CPU 刚写的数据丢弃,DMA 发出全 0 或旧数据。
  • 坑 3:DMA 传输中访问缓冲区。DMA 进行中 CPU 读写同一缓冲区会破坏一致性,应使用双缓冲或等待完成。
  • 坑 4:忘记在中断中再次 Invalidate。接收完成中断里若不 Invalidate,读到的仍是旧 Cache。
  • 坑 5:缓冲区大小不是 32 的倍数SCB_*DCache_by_Addr 内部按 Cache Line 操作,非整数倍可能越界。建议向上取整到 32 字节。
  • 坑 6:多缓冲区共享 Cache Line。两个变量落在同一 32 字节行内,Invalidate 一个会影响另一个。用对齐隔离。

替代方案:若数据量大且频繁,建议用 MPU 将 DMA 缓冲区配置为 Write-Through, No Write-AllocateNon-Cacheable,从根源避免一致性问题,代价是 CPU 访问稍慢。

六、总结

STM32H7 的 D-Cache 是性能利器,但与 DMA 共存时必须谨慎。记住三句话:缓冲区 32 字节对齐;CPU 写后 Clean,DMA 写后 Invalidate;顺序不能反。掌握这些,DMA 数据错乱问题将迎刃而解。