一、为什么 DCache 会让 DMA 数据“失灵”?

Cortex-M7 的 L1 DCache 采用写回(write-back)策略。CPU 写数据时,数据先停留在 Cache 中,并未立即写入 SRAM;CPU 读数据时,若 Cache 命中则直接返回旧值,不会去 SRAM 取新值。

而 DMA 是“总线主设备”,它直接访问 SRAM,完全绕过 Cache。于是出现两类经典问题:

  • CPU 写、DMA 读:CPU 写入的数据还在 Cache 里(脏行),DMA 从 SRAM 读到的是旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧副本。

解决思路只有两条:要么让这段内存不经过 Cache(MPU 配置为 Non-Cacheable),要么在恰当时机手动维护 Cache(clean / invalidate)。

二、MPU 配置:最省心的方案

对于频繁与 DMA 交互的缓冲区,推荐用 MPU 将其配置为 Non-Cacheable, Non-Shareable,彻底规避一致性问题。代价是 CPU 访问该区域变慢,但通常可接受。

2.1 关键配置项

  • TEX = 0b001, C = 0, B = 0:Normal memory, Non-cacheable。
  • AP:根据读写权限设置,例如全访问 0b011。
  • XN:若不需要执行代码,设为 1 更安全。

2.2 完整 MPU 配置代码

#include "stm32h7xx.h"

#define DMA_BUF_ADDR   0x30000000UL   /* AXI SRAM 起始 */
#define DMA_BUF_SIZE   0x2000UL       /* 8KB */

void MPU_Config_DMA_Buffer(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    HAL_MPU_Disable();

    /* 配置 Region 0 为 Non-Cacheable */
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_8KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;  /* TEX=1 */
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE; /* C=0 */
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;/* B=0 */

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 配置必须在使能 Cache 之前完成,且 Region 地址与大小必须对齐(大小是 2 的幂,基地址按大小对齐)。

三、clean / invalidate:必须掌握的边界条件

如果缓冲区仍需 Cache(例如 CPU 要频繁读写),就必须手动维护。CMSIS 提供三个函数:

  • SCB_CleanDCache_by_Addr(addr, size):把 Cache 脏行写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,下次读从 SRAM 取。
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。

3.1 使用时机

  • CPU 写 → DMA 读:DMA 启动前执行 Clean。
  • DMA 写 → CPU 读:DMA 完成后执行 Invalidate。
  • CPU 读写混合:DMA 完成后执行 Invalidate(若 CPU 之前写过该区域,需先 Clean 再 Invalidate)。

3.2 边界条件(最容易踩坑)

  • 地址与大小必须 32 字节对齐。Cortex-M7 Cache Line 为 32 字节,若地址不对齐,操作会波及相邻数据,可能误伤其他变量。
  • Invalidate 会丢弃未写回的数据。如果 CPU 刚写过缓冲区且未 Clean,直接 Invalidate 会丢失数据。
  • DMA 传输期间禁止操作 Cache。否则可能读到半新半旧的数据。
  • 多缓冲区共享 Cache Line:若两个变量位于同一 32 字节行,对其中一个 Invalidate 会影响另一个。建议缓冲区按 32 字节对齐并填充。

3.3 对齐的缓冲区定义

/* 强制 32 字节对齐,避免 Cache Line 伪共享 */
__attribute__((aligned(32))) uint8_t dma_tx_buf[1024];
__attribute__((aligned(32))) uint8_t dma_rx_buf[1024];

四、完整示例:UART DMA 收发

以 UART DMA 发送为例,展示 clean 的正确位置。

#include "stm32h7xx.h"

extern UART_HandleTypeDef huart1;
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];

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

    /* 2. 确保 CPU 写入的数据写回 SRAM */
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);

    /* 3. 启动 DMA 传输 */
    HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}

/* DMA 接收完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        /* DMA 已写入 SRAM,使 Cache 失效以读取新数据 */
        SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
        /* 此时可安全处理 dma_rx_buf 中的数据 */
    }
}

提示:SCB_CleanDCache_by_Addr 的 size 参数建议传入实际长度,但函数内部会按 Cache Line 对齐处理,因此缓冲区最好整体对齐。

五、注意事项与最佳实践

  • 优先使用 MPU Non-Cacheable:对 DMA 缓冲区,这是最不易出错的方式。
  • 若必须用 Cache:严格遵循“写后 Clean、读前 Invalidate”的顺序,并保证 32 字节对齐。
  • 避免在中断中频繁操作 Cache:clean/invalidate 有一定开销,高频小数据量传输可考虑 Non-Cacheable。
  • DMA 描述符与缓冲区分离:描述符通常也需 Non-Cacheable 或正确维护。
  • 调试时关闭 Cache 验证:若数据异常,先关 DCache 确认是否为一致性问题。
  • 注意 AXI SRAM 与 DTCM:DTCM 不被 Cache,可直接用于 DMA,但带宽有限;AXI SRAM 需按上述规则处理。

六、总结

STM32H7 的 DCache 与 DMA 一致性没有“银弹”,核心在于理解 Cache 行为并选择合适策略:能不用 Cache 就不用(MPU),必须用时严格对齐并正确 clean/invalidate。掌握这些边界条件,你就能在 H7 上兼顾高性能与数据可靠性。