为什么 DMA 和 D-Cache 会“打架”?

STM32H7 的 Cortex-M7 内核主频高达 480MHz,为了匹配 CPU 速度,芯片内部集成了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM。DMA 控制器则绕过 Cache,直接读写 SRAM。

这就导致一个经典问题:

  • CPU 写数据到 SRAM:数据可能只停留在 Cache 中(write-back 模式),SRAM 里还是旧值。此时启动 DMA 发送,DMA 读到的是旧数据。
  • DMA 从外设搬数据到 SRAM:SRAM 已更新,但 CPU 读同一地址时,Cache 命中旧数据,CPU 读到的是旧值。

解决思路只有两个:配置 MPU 将 DMA 缓冲区设为非缓存(Non-cacheable),或者在 DMA 传输前后手动 clean/invalidate Cache

方法一:MPU 配置非缓存区域(推荐)

对于频繁 DMA 的大块缓冲区,最省心的方式是用 MPU 将这块内存配置为 Non-cacheable。这样 CPU 和 DMA 都直接访问 SRAM,无需任何 Cache 维护操作。

配置步骤

  1. 使能 MPU:SCB_EnableMPU() 之前先 SCB_EnableICache()SCB_EnableDCache()
  2. 定义 MPU 区域,设置 Disable 缓存属性。
  3. 注意对齐:MPU 区域基址和大小必须是 32 字节对齐,且大小是 2 的幂。

完整代码示例

#include "stm32h7xx.h"

// 假设 DMA 缓冲区放在 D2 SRAM 中,地址 0x30000000,大小 32KB
#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x8000

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

    HAL_MPU_Disable();

    // 配置 DMA 缓冲区区域为非缓存、非共享
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUF_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    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);
}

注意:MPU 配置必须在使能 D-Cache 之前完成,否则可能触发异常。

方法二:手动 clean / invalidate Cache

如果缓冲区不能设为非缓存(比如需要 CPU 频繁读写),就必须在 DMA 传输前后手动维护 Cache。

核心 API

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回 SRAM。用于 DMA 发送前
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 读取。用于 DMA 接收后
  • SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃,用于双向传输。

正确使用顺序

发送方向(CPU → 外设)

  1. CPU 填充缓冲区数据
  2. SCB_CleanDCache_by_Addr() 写回 SRAM
  3. 启动 DMA 发送

接收方向(外设 → CPU)

  1. 启动 DMA 接收
  2. 等待 DMA 完成
  3. SCB_InvalidateDCache_by_Addr() 丢弃旧 Cache
  4. CPU 读取缓冲区

代码示例

#define BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

// DMA 发送
void DMA_Send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);  // 写回
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE); // 丢弃
    // 此时读取 rx_buf 才是 DMA 写入的新数据
    ProcessData(rx_buf, BUF_SIZE);
}

实战避坑指南

  • 地址和长度必须 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节。如果地址或长度不对齐,clean/invalidate 会误伤相邻数据,导致其他变量被意外写回或丢弃。建议用 __attribute__((aligned(32))) 修饰缓冲区。
  • 不要对同一缓冲区同时 clean 和 invalidate:除非确实需要双向同步,否则分开使用。
  • invalidate 前确保 DMA 已停止:如果 DMA 还在写,invalidate 可能丢弃刚写入的数据。
  • MPU 配置区域大小必须是 2 的幂:32KB、64KB 可以,48KB 不行。
  • 中断中调用 Cache 维护函数要小心:这些函数执行时间较长,可能影响实时性。
  • 使用 SCB_InvalidateDCache_by_Addr 后,CPU 不能再依赖 Cache 中的旧值:所有后续读取都会从 SRAM 重新加载。

总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的高频坑点。优先推荐用 MPU 将 DMA 缓冲区设为 Non-cacheable,简单可靠;若必须使用 Cache,则严格遵循“发送前 clean、接收后 invalidate”的原则,并注意 32 字节对齐。掌握这两招,就能让 H7 的性能与数据正确性兼得。