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

STM32H7 主频高达 480MHz,为了匹配 CPU 速度,芯片内部集成了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的搬运工,它直接访问物理内存(SRAM),完全不知道 Cache 的存在。

这就导致两个典型问题:

  • DMA 写入的数据,CPU 读不到:DMA 把新数据写入 SRAM,但 CPU 之前读过该地址,旧数据还在 Cache 里,CPU 再次读取时命中 Cache,拿到的是旧值。
  • CPU 写入的数据,DMA 读不到:CPU 修改了变量,但只写入了 Cache(写回策略),SRAM 中还是旧数据,DMA 从 SRAM 搬运,发出去的是错误内容。

因此,任何被 DMA 访问的内存区域,都必须保证 Cache 与 SRAM 的一致性。

二、一致性维护的三种手段

  1. 禁用 D-Cache:简单粗暴,但牺牲性能,不推荐。
  2. 软件维护 Cache:在 DMA 传输前后调用 SCB_CleanDCache_by_Addr() 或 SCB_InvalidateDCache_by_Addr()。
  3. MPU 配置内存属性:将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,从硬件层面避免一致性问题。

实际项目中,推荐 MPU 配置 + 必要时软件维护 的组合方案。

三、地址对齐:最容易被忽视的坑

Cortex-M7 的 Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr() 操作的是整个 Cache 行,而不是单个字节。

如果 DMA 缓冲区起始地址或长度不是 32 字节对齐,维护操作会波及相邻数据,导致其他变量被意外清除或写回。

避坑规则:

  • DMA 缓冲区必须 32 字节对齐(推荐用 __attribute__((aligned(32))))。
  • 缓冲区大小最好是 32 的整数倍。
  • 若无法对齐,需手动扩展维护范围到 Cache 行边界,并确保相邻数据不被误伤。
// 推荐:强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];

四、MPU 配置实战

以 STM32H743 为例,将 SRAM 中某块区域(如 0x30000000 起始的 64KB)配置为 Non-Cacheable,专供 DMA 使用。

#include "stm32h7xx_hal.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0x30000000;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;
    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;  // 关键
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

配置后,该区域所有访问都绕过 D-Cache,DMA 与 CPU 看到的内存完全一致,无需再手动维护 Cache。

五、完整示例:DMA 串口接收 + Cache 维护

若不想用 MPU,也可在 DMA 传输前后手动维护 Cache。以下以 UART DMA 接收为例。

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

void UART_DMA_Init(void)
{
    // 启动 DMA 接收前,先无效化 Cache,确保 CPU 读到 SRAM 新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 处理前再次无效化,保证读到 DMA 写入的最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
        ProcessData(rx_buf, RX_BUF_SIZE);
    }
}

// DMA 发送前,先清理 Cache,把 CPU 写入的数据同步到 SRAM
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

注意:SCB_InvalidateDCache_by_Addr 会丢弃 Cache 中未写回的数据,因此只能用于 DMA 写入方向(CPU 只读)。对于 CPU 会写入的缓冲区,应先 Clean 再 Invalidate,或直接使用 MPU Non-Cacheable 方案。

六、排查清单与注意事项

遇到 DMA 数据异常时,按以下顺序排查:

  • 确认缓冲区地址是否 32 字节对齐,长度是否为 32 的倍数。
  • 确认是否在 DMA 传输前后正确维护 Cache,方向是否搞反。
  • 检查 MPU 配置是否覆盖了 DMA 缓冲区,属性是否为 Non-Cacheable 或 Write-Through。
  • 注意多缓冲区场景:多个 DMA 通道共用 Cache 行时,维护一个缓冲区可能影响另一个。
  • 中断优先级:Cache 维护操作应在 DMA 传输完成中断中尽早执行,避免 CPU 先读取旧数据。
  • 调试技巧:可临时禁用 D-Cache 验证问题是否由 Cache 引起,确认后再针对性优化。

七、总结

STM32H7 的 D-Cache 与 DMA 冲突本质是 缓存一致性 问题。核心解决思路有三条:地址对齐是基础,软件维护是补救,MPU 配置是根治。对于高频 DMA 传输,强烈建议用 MPU 将 DMA 缓冲区设为 Non-Cacheable,既保证数据正确,又避免频繁维护带来的性能开销。掌握这些要点,就能在 H7 上放心使用 DMA,不再被“玄学”数据错乱困扰。