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

STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存),它位于 CPU 和 AXI SRAM 之间。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到物理内存。而 DMA 控制器直接访问物理内存,完全绕过 Cache。

当 CPU 写数据到缓冲区后启动 DMA 发送,若数据还在 Cache 中未写回,DMA 读到的是旧数据;反之,DMA 接收数据到内存后,CPU 若从 Cache 读,可能读到旧缓存。这就是数据一致性问题。

二、Cache Line 与对齐的关系

Cortex-M7 的 D-Cache 按 Cache Line 管理,每个 Line 为 32 字节。Cache 维护操作(如 clean、invalidate)以 Line 为单位。

  • 若缓冲区起始地址或大小未按 32 字节对齐,clean/invalidate 会波及相邻数据。
  • 例如:缓冲区 100 字节,起始地址 0x20000010,则首尾 Line 包含其他变量。invalidate 时可能丢弃其他变量的最新值,导致隐蔽 Bug。

因此,DMA 缓冲区必须按 32 字节对齐,且大小最好是 32 的整数倍。

三、配置步骤

3.1 定义对齐的缓冲区

使用 __attribute__((aligned(32))) 强制对齐:

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[BUF_SIZE];

3.2 配置 MPU 将缓冲区设为 Non-Cacheable

更简单的方案:通过 MPU 将 DMA 缓冲区区域设为 Non-Cacheable,彻底避免一致性问题。

void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_tx_buf; // 或整个 SRAM 区域
    MPU_InitStruct.Size = MPU_REGION_SIZE_256B;        // 根据实际大小调整
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.3 使用 Cache 维护函数(若必须 Cacheable)

若缓冲区仍为 Cacheable,则需在 DMA 传输前后手动维护:

  • 发送前:CPU 写数据后,执行 SCB_CleanDCache_by_Addr() 将 Cache 写回内存。
  • 接收后:DMA 写内存后,执行 SCB_InvalidateDCache_by_Addr() 丢弃 Cache 中旧数据。

注意:invalidate 前若 CPU 曾写该缓冲区,必须先 clean,否则丢失数据。

四、完整代码示例(UART DMA 发送)

#include "stm32h7xx.h"

#define TX_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[TX_SIZE];

void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    // 1. 拷贝数据到对齐缓冲区
    memcpy(tx_buf, data, len);

    // 2. 若缓冲区为 Cacheable,clean 确保数据写入内存
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_SIZE);

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

// DMA 接收完成回调中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 4. 接收后 invalidate,确保 CPU 读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
        // 处理 rx_buf ...
    }
}

五、注意事项

  • 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,大小建议为 32 的整数倍。
  • MPU 优先:若性能允许,将 DMA 区域设为 Non-Cacheable 是最省心的方案。
  • 维护范围:clean/invalidate 的地址和长度必须按 32 字节对齐,否则会误伤相邻数据。
  • 避免频繁维护:Cache 维护有开销,批量传输时尽量合并操作。
  • 多缓冲区隔离:不同 DMA 流使用独立对齐缓冲区,防止伪共享。
  • 调试技巧:若数据异常,先检查缓冲区地址是否 32 字节对齐,再确认是否遗漏 clean/invalidate。

掌握以上方法,即可在 STM32H7 上安全高效地使用 DMA 与 D-Cache,充分发挥芯片性能。