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

STM32H7 的 Cortex-M7 内核最高运行在 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,如果命中 Cache,就直接从 Cache 读写,不会立即同步到 SRAM。

而 DMA 是“绕过 CPU”的搬运工,它直接访问 SRAM 或外设。当 CPU 把数据写入 Cache 但尚未回写到 SRAM 时,DMA 从 SRAM 读到的就是旧数据;反之,DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本,就会“看不到”新数据。这就是 数据一致性问题。

二、Cache 行与地址对齐:一切的基础

  • Cache 行大小:STM32H7 的 D-Cache 行大小为 32 字节。Cache 的维护(Clean/Invalidate)以“行”为单位,而不是以字节为单位。
  • 地址对齐:DMA 缓冲区建议按 32 字节对齐。如果缓冲区跨越两个 Cache 行,操作一行会影响相邻数据,极易误伤。
  • 推荐做法:使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏定义缓冲区,并确保长度是 32 的整数倍(或至少不与其他变量共享 Cache 行)。
// 定义 32 字节对齐的 DMA 缓冲区
ALIGN_32BYTES(uint8_t dma_rx_buf[256]);
ALIGN_32BYTES(uint8_t dma_tx_buf[256]);

三、Clean 与 Invalidate:方向决定操作

  • Clean(清理):将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 的场景(发送方向)。
  • Invalidate(无效化):将 Cache 中的对应行标记为无效,下次 CPU 读取时会从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景(接收方向)。
  • 注意:Invalidate 会丢弃 Cache 中尚未回写的数据!如果 CPU 刚写过该缓冲区,必须先 Clean 再 Invalidate,否则数据丢失。

四、实战配置步骤(以 UART DMA 接收为例)

4.1 初始化阶段

  • 使能 D-Cache(通常在 SystemInit 中完成)。
  • 将 DMA 缓冲区放在 非缓存区域 或使用 MPU 配置 为 Write-Through/Non-Cacheable,可一劳永逸。但本文重点讲手动维护方法。

4.2 发送数据(CPU 写 → DMA 读)

// 1. 填充数据到 dma_tx_buf
memcpy(dma_tx_buf, src, len);

// 2. Clean 操作,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);

// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);

4.3 接收数据(DMA 写 → CPU 读)

// 1. 启动 DMA 接收前,先 Invalidate 缓冲区,避免 Cache 中有旧数据
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));

// 2. 等待 DMA 完成(中断或轮询)
// 3. DMA 完成后,再次 Invalidate,让 CPU 读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));

// 4. 此时 CPU 读取 dma_rx_buf 才是正确的
process_data(dma_rx_buf, len);

五、完整代码示例(基于 HAL 库)

#include "stm32h7xx_hal.h"

ALIGN_32BYTES(uint8_t uart_tx_buf[128]);
ALIGN_32BYTES(uint8_t uart_rx_buf[128]);
volatile uint8_t dma_rx_done = 0;

void uart_dma_send(uint8_t *data, uint16_t len) {
    if (len > sizeof(uart_tx_buf)) return;
    memcpy(uart_tx_buf, data, len);
    // Clean 确保数据写入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)uart_tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, uart_tx_buf, len);
}

void uart_dma_start_receive(void) {
    // 启动前 Invalidate,丢弃 Cache 旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, sizeof(uart_rx_buf));
    HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    // 再次 Invalidate,让 CPU 看到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, sizeof(uart_rx_buf));
    dma_rx_done = 1;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_UART4_Init();

    uart_dma_start_receive();

    while (1) {
        if (dma_rx_done) {
            dma_rx_done = 0;
            // 处理 uart_rx_buf 中的数据
            uart_dma_send(uart_rx_buf, 128);
            uart_dma_start_receive();
        }
    }
}

六、避坑指南与注意事项

  • 缓冲区必须 32 字节对齐,且长度最好是 32 的倍数,避免跨 Cache 行误伤。
  • Clean 和 Invalidate 不要搞反:发送用 Clean,接收用 Invalidate。
  • 接收前也要 Invalidate:否则 Cache 中的旧数据可能被 CPU 误读。
  • DMA 传输期间不要操作缓冲区:否则 Cache 与 SRAM 可能再次不一致。
  • 使用 MPU 配置非缓存区域:如果不想手动维护,可将 DMA 缓冲区所在区域配置为 Non-Cacheable,但会牺牲性能。
  • 注意中断优先级:DMA 完成中断中调用 Invalidate 是安全的,但需确保没有其他 CPU 访问同一缓冲区。
  • 多缓冲区场景:如果使用双缓冲,每个缓冲区都要独立维护,不能只维护一个。

掌握以上要点,你就能在 STM32H7 上放心使用 D-Cache 和 DMA,既享受高性能,又避免数据错乱。