一、为什么 Cache 会让 DMA 数据“错乱”?

STM32H7 的 Cortex-M7 内核包含 L1 D-Cache(数据缓存)和 I-Cache。CPU 读写内存时,数据可能暂存在 Cache 中,并未立即写回物理内存(SRAM/SDRAM)。而 DMA 控制器直接访问物理内存,不经过 Cache。

  • CPU 写,DMA 读:CPU 写入的数据还在 Cache 里,DMA 从物理内存读到旧值。
  • DMA 写,CPU 读:DMA 更新了物理内存,但 CPU 读的是 Cache 中的旧数据。

这就是 Cache 一致性(Coherency) 问题。解决思路有两种:

  1. 软件维护:在 DMA 传输前后手动 Clean/Invalidate Cache。
  2. 硬件属性:通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,让 CPU 访问直接穿透到物理内存。

二、MPU 配置:给 DMA 缓冲区“开小灶”

MPU(Memory Protection Unit)可以按地址区域设置内存属性。推荐将 DMA 缓冲区放在 0x30000000(SRAM1/2/3)0x24000000(AXI SRAM),并配置为:

  • Non-Cacheable, Non-Shareable:最简单,CPU 和 DMA 都直接访问物理内存,无需 Clean/Invalidate。
  • Write-Through, No Write Allocate:CPU 写操作同时更新 Cache 和物理内存,读操作可缓存。适合读多写少的 DMA 缓冲区。

以下为 MPU 配置示例(使用 HAL 库):

#include "stm32h7xx_hal.h"

// 假设 DMA 缓冲区位于 0x30000000,大小 32KB
#define DMA_BUF_ADDR   0x30000000
#define DMA_BUF_SIZE   0x8000

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 DMA 缓冲区区域: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_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);

    // 可选:将其他 SRAM 区域配置为 Write-Through
    // ...

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:MPU 区域必须按 2 的幂次对齐,且大小与基地址匹配。修改 MPU 后需调用 SCB_EnableDCache() 等函数重新使能 Cache。

三、Clean 与 Invalidate:软件维护的利器

如果不想用 MPU,或者缓冲区必须可缓存,则需在 DMA 传输前后调用 CMSIS 函数:

  • Clean:将 Cache 中已修改的数据写回物理内存。用于 CPU 写 → DMA 读 之前。
  • Invalidate:丢弃 Cache 中的旧数据,强制从物理内存重新加载。用于 DMA 写 → CPU 读 之前。
  • Clean+Invalidate:两者都做,用于双向传输或不确定方向时。
#include "core_cm7.h"

// 发送前:确保 CPU 写入的数据到达物理内存
void DMA_SendPrepare(uint8_t *buf, uint32_t len)
{
    SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
    // 然后启动 DMA 发送
}

// 接收后:丢弃 Cache 旧数据,让 CPU 读到 DMA 写入的新数据
void DMA_ReceiveComplete(uint8_t *buf, uint32_t len)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
    // 然后 CPU 处理数据
}

关键细节

  • 地址必须 32 字节对齐,长度建议按 32 字节向上取整。
  • Invalidate 前必须确保 DMA 已停止,否则可能丢失数据。
  • 若缓冲区跨越 Cache Line(32 字节),需处理相邻数据被误 Clean/Invalidate 的问题。

四、完整实战:DMA 串口接收 + Cache 维护

以 UART DMA 接收为例,展示完整流程:

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

void UART_DMA_Init(void)
{
    // 1. 配置 MPU 将 rx_buf 所在区域设为 Non-Cacheable(推荐)
    // 或跳过 MPU,使用下面的 Clean/Invalidate 方式

    // 2. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 若未配置 MPU,必须 Invalidate Cache
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);

    // 此时 CPU 读取 rx_buf 才是 DMA 写入的最新数据
    ProcessData(rx_buf, RX_BUF_SIZE);

    // 重新启动接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

五、注意事项与避坑指南

  • 优先用 MPU:将 DMA 缓冲区设为 Non-Cacheable 最省心,避免频繁 Clean/Invalidate 带来的性能开销。
  • 对齐:所有 Cache 操作地址必须 32 字节对齐,长度按 32 字节倍数处理。
  • Invalidate 风险:若缓冲区部分数据在 Cache 中未写回,Invalidate 会丢弃它们。务必先 Clean 再 Invalidate。
  • 多缓冲区:使用多个 DMA 缓冲区时,每个缓冲区独立维护 Cache。
  • 调试:开启 Cache 后,调试器查看变量可能看到旧值,需手动 Clean 或关闭 Cache 调试。
  • 性能权衡:Non-Cacheable 会降低 CPU 访问速度,对大数据量频繁访问的场景,可考虑 Write-Through 模式。

掌握 MPU 配置与 Clean/Invalidate 的配合,你就能在 STM32H7 上放心使用 DMA,不再被“灵异数据”困扰。