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

STM32H7 的 Cortex-M7 内核带有 16KB 指令 Cache 和 16KB 数据 Cache(L1 Cache)。当 CPU 访问内存时,数据会被缓存到 L1 中;后续访问若命中 Cache,则直接读写 Cache,不再访问实际内存。

而 DMA 控制器是独立于 CPU 的总线主设备,它直接读写物理内存(SRAM、SDRAM 等),不经过 Cache。这就导致两个典型问题:

  • CPU 写,DMA 读:CPU 将数据写入 Cache 但尚未回写到内存,DMA 从内存读到旧数据。
  • DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读的是 Cache 中的旧数据。

例如,用 DMA 接收串口数据到缓冲区,CPU 去读缓冲区时可能读到全 0 或旧内容,这就是数据一致性问题。

二、三种解决方案对比

| 方案 | 原理 | 优点 | 缺点 | |------|------|------|------| | 禁用 Cache | 关闭 D-Cache | 简单 | 性能损失巨大,H7 失去意义 | | 手动维护 | 调用 SCB_CleanDCache/InvalidateDCache | 灵活 | 易漏、易错,中断中调用有风险 | | MPU 配置 | 将 DMA 缓冲区设为 Write-Through/Non-Cacheable | 一劳永逸,零维护 | 需正确配置 MPU 区域 |

推荐方案:MPU 配置。将 DMA 使用的内存区域配置为 Non-Cacheable 或 Write-Through,CPU 和 DMA 看到的数据始终一致,无需手动 Clean/Invalidate。

三、MPU 配置实战

3.1 确定 DMA 缓冲区地址

假设我们使用 0x30000000 开始的 64KB 区域(例如 SDRAM 或 AXI SRAM)作为 DMA 缓冲区。

#define DMA_BUFFER_ADDR   0x30000000
#define DMA_BUFFER_SIZE   0x10000   // 64KB

3.2 配置 MPU 区域

STM32H7 的 MPU 有 16 个区域,每个区域可独立设置属性。我们配置 Region 0 覆盖 DMA 缓冲区。

#include "stm32h7xx.h"

void MPU_Config(void)
{
    HAL_MPU_Disable();  // 配置前必须关闭 MPU

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 区域 0:DMA 缓冲区,Non-Cacheable, Non-Shareable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    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);

    // 可选:配置其他区域,如 Flash、普通 SRAM 为 Cacheable
    // ...

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

关键点:IsCacheable = MPU_ACCESS_NOT_CACHEABLE 使该区域绕过 D-Cache,CPU 读写直接访问物理内存,与 DMA 天然一致。

3.3 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();          // 必须在使能 Cache 前配置
    SCB_EnableICache();
    SCB_EnableDCache();
    // 后续初始化 DMA、外设...
}

四、完整示例:DMA 串口接收 + MPU 保护

#define RX_BUFFER_SIZE 256
__attribute__((section(".dma_buffer")))  // 链接脚本中定位到 0x30000000
uint8_t rx_buffer[RX_BUFFER_SIZE];

void UART_DMA_Init(void)
{
    // 假设 huart1 已初始化
    HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}

void process_data(void)
{
    // 直接读取 rx_buffer,无需 Clean/Invalidate
    for (int i = 0; i < RX_BUFFER_SIZE; i++) {
        if (rx_buffer[i] != 0) {
            // 处理数据
        }
    }
}

链接脚本片段(GCC):

MEMORY
{
  DMA_RAM (xrw) : ORIGIN = 0x30000000, LENGTH = 64K
}

SECTIONS
{
  .dma_buffer :
  {
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
  } > DMA_RAM
}

五、注意事项与避坑指南

  • MPU 配置必须在使能 Cache 之前,否则可能触发异常或配置不生效。
  • 区域大小必须与对齐匹配:基地址需按区域大小对齐(如 64KB 区域,基地址低 16 位为 0)。
  • 不要将整个 SRAM 设为 Non-Cacheable,否则性能严重下降。只保护 DMA 缓冲区。
  • 若使用 Write-Through 模式:IsCacheable = MPU_ACCESS_CACHEABLE + IsBufferable = MPU_ACCESS_NOT_BUFFERABLE,可兼顾性能与一致性,但 CPU 写操作会立即写穿到内存,适合读多写少的 DMA 缓冲区。
  • 中断中避免手动 Clean/Invalidate:若采用手动维护方案,在中断中操作 Cache 可能因流水线导致数据损坏,MPU 方案无此问题。
  • 多缓冲区场景:每个 DMA 缓冲区都应放入 MPU 保护的同一区域,或分别配置多个 MPU 区域。
  • 调试时确认 MPU 生效:可通过读取 MPU->RBAR 和 MPU->RASR 验证配置。

六、总结

STM32H7 的 L1 Cache 与 DMA 冲突是高性能嵌入式开发的常见陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从硬件层面根除一致性问题,代码简洁且无需在数据传输前后插入维护指令。建议在项目初期就规划好内存布局,将 DMA 缓冲区集中放置,并正确配置 MPU 区域,从而充分发挥 H7 的性能优势,同时保证数据可靠性。