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

STM32H7 的 Cortex-M7 内核带有 16KB 的 D-Cache。开启后,CPU 读写数据会先经过 Cache,而不是直接访问 SRAM。DMA 则绕过 Cache 直接读写 SRAM。这就导致:

  • CPU 写数据到 Cache,但未写回 SRAM:DMA 从 SRAM 读到的还是旧数据。
  • DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据:CPU 看不到更新。

因此,必须在 DMA 传输前后正确维护 Cache 一致性。

二、MPU 配置:为 DMA 缓冲区划定“安全区”

最优雅的方案是利用 MPU 将 DMA 缓冲区配置为 Write-Through, No Write-Allocate 或 Non-Cacheable,从而避免手动维护。

2.1 配置步骤(以 Non-Cacheable 为例)

  1. 使能 MPU:SCB_EnableMPU() 前先 SCB_DisableMPU()。
  2. 定义 MPU 区域:基地址为缓冲区地址,大小需 32 字节对齐。
  3. 设置属性:TEX=1, C=0, B=0, S=1 表示 Non-Cacheable, Shareable。
  4. 使能该区域并开启 MPU。
#include "stm32h7xx.h"

#define DMA_BUF_ADDR  0x30000000  // AXI SRAM 起始地址
#define DMA_BUF_SIZE  1024        // 1KB

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_1KB;
    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_LEVEL1;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

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

注意:MPU 区域大小必须是 2 的幂,且基地址对齐到该大小。

三、Clean 与 Invalidate 的正确时机

若无法使用 MPU(例如缓冲区分散),则必须手动调用 CMSIS 函数:

  • Clean:将 Cache 中已修改的数据写回 SRAM。
  • Invalidate:丢弃 Cache 中的旧数据,强制从 SRAM 重新加载。

3.1 发送方向(CPU → DMA)

  1. CPU 填充缓冲区。
  2. Clean 缓冲区(确保数据到达 SRAM)。
  3. 启动 DMA 发送。
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&periph, len);

3.2 接收方向(DMA → CPU)

  1. Invalidate 缓冲区(丢弃旧 Cache 行,防止 DMA 写入后 CPU 读旧值)。
  2. 启动 DMA 接收。
  3. DMA 完成中断中再次 Invalidate(因为 DMA 可能已写入新数据,但 Cache 中可能残留旧行)。
  4. CPU 读取数据。
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_DMA_Start_IT(&hdma, (uint32_t)&periph, (uint32_t)rx_buf, len);

// DMA 完成回调中
void DMA_Complete_Callback(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
    // 处理数据
}

四、完整实战代码:UART DMA 不定长接收

以 UART 空闲中断 + DMA 接收为例,展示完整流程。

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

void UART_DMA_Init(void)
{
    // 1. 配置 MPU(可选,若使用 Non-Cacheable 则无需手动维护)
    // MPU_Config();

    // 2. 启动 DMA 接收前 Invalidate
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// UART 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart == &huart1) {
        // 3. 数据已到达 SRAM,Invalidate 使 CPU 可见
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);

        // 4. 处理数据
        ProcessData(rx_buf, Size);

        // 5. 重新启动接收
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
        HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
    }
}

五、常见踩坑与注意事项

  • 地址未对齐:SCB_CleanDCache_by_Addr 要求地址 32 字节对齐,否则可能误伤相邻数据。使用 __attribute__((aligned(32))) 修饰缓冲区。
  • Invalidate 前未 Clean:若缓冲区中既有 CPU 写入又有 DMA 写入,先 Invalidate 会丢失 CPU 未写回的数据。正确顺序:先 Clean 再 Invalidate。
  • DMA 传输中访问缓冲区:DMA 工作时 CPU 不应读写该缓冲区,否则 Cache 行可能被意外修改。
  • MPU 配置冲突:多个 MPU 区域重叠时,编号高的优先级高,需仔细规划。
  • 性能陷阱:频繁 Clean/Invalidate 会降低性能,尽量用 MPU 将 DMA 缓冲区设为 Non-Cacheable。
  • 中断中调用:Cache 维护函数执行时间较长,在高速中断中慎用,可考虑延迟处理。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性是嵌入式开发中的经典难题。核心原则:明确数据流向,在正确时机 Clean 或 Invalidate。优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,可一劳永逸。若必须手动维护,牢记“发送前 Clean,接收前 Invalidate,接收后再次 Invalidate”。掌握这些,即可让 H7 的高性能与 DMA 的便利性完美结合。