一、为什么 Cache 与 DMA 会“打架”?

STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。CPU 访问内存时,数据可能暂存在 Cache 中,而 DMA 直接读写物理内存(SRAM/SDRAM)。若 CPU 写数据后未同步到内存,DMA 读到的就是旧数据;若 DMA 写入新数据,CPU 读到的却是 Cache 中的旧副本。这就是Cache 一致性问题。

  • 写通(Write-through):CPU 写操作同时更新 Cache 和内存,但 H7 默认是写回(Write-back),只有 Cache 行被替换时才写回内存。
  • 读分配(Read-allocate):CPU 读未命中时,先从内存加载到 Cache,后续读直接命中 Cache。

因此,DMA 传输前后必须手动维护 Cache:发送前 Clean(清理)接收后 Invalidate(无效化)

二、Clean 与 Invalidate 的准确时机

2.1 发送方向(CPU → 外设)

  1. CPU 填充发送缓冲区(数据可能还在 Cache 中)。
  2. Clean D-Cache:将 Cache 行写回内存,确保 DMA 读到最新数据。
  3. 启动 DMA 发送。
  4. 等待 DMA 完成(此时无需 Invalidate,因为 CPU 不再读该缓冲区)。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));

2.2 接收方向(外设 → CPU)

  1. 启动 DMA 接收(此时缓冲区内容未知,建议先 Invalidate,防止 Cache 中有脏数据)。
  2. 等待 DMA 完成。
  3. Invalidate D-Cache:丢弃 Cache 中的旧副本,强制 CPU 从内存读取 DMA 写入的新数据。
  4. CPU 处理数据。
// 接收前 Invalidate(可选但推荐)
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 等待完成...
// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));

注意SCB_InvalidateDCache_by_Addr 的地址和长度必须按 32 字节对齐(Cache 行大小),否则可能误伤相邻数据。

三、MPU 配置:一劳永逸的非缓存区域

频繁手动 Clean/Invalidate 容易遗漏,且影响性能。更优雅的方案是用 MPU(Memory Protection Unit) 将 DMA 缓冲区所在内存区域配置为 Non-cacheable,CPU 和 DMA 直接访问物理内存,无需任何 Cache 维护。

3.1 MPU 配置步骤(以 SRAM1 为例)

  1. 使能 MPU:HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT)
  2. 配置 Region:基地址、大小、属性(Normal, Non-cacheable, Shareable)。
  3. 使能该 Region。
void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_Init = {0};

    // 假设 DMA 缓冲区位于 0x30000000 (SRAM1),大小 64KB
    MPU_Init.Enable = MPU_REGION_ENABLE;
    MPU_Init.Number = MPU_REGION_NUMBER0;
    MPU_Init.BaseAddress = 0x30000000;
    MPU_Init.Size = MPU_REGION_SIZE_64KB;
    MPU_Init.SubRegionDisable = 0x00;
    MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
    MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_Init.IsShareable = MPU_ACCESS_SHAREABLE;      // 共享,保证一致性
    MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 非缓存
    MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_Init);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.2 链接脚本调整

将 DMA 缓冲区放到非缓存区域,需修改链接脚本(.ld)或使用 __attribute__((section(".dma_buffer"))) 指定段,并在 MPU 中覆盖该段地址。

// 定义非缓存缓冲区
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[1024];

四、完整实战:UART DMA 收发 + MPU 非缓存

#include "stm32h7xx_hal.h"

#define DMA_BUF_SIZE 256
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t tx_buf[DMA_BUF_SIZE];
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t rx_buf[DMA_BUF_SIZE];

void MPU_Config(void) { /* 如上 */ }

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 必须在使能 Cache 前调用
    SCB_EnableICache();
    SCB_EnableDCache();

    MX_GPIO_Init();
    MX_DMA_Init();
    MX_USART1_UART_Init();

    // 填充发送数据
    for (int i = 0; i < DMA_BUF_SIZE; i++) tx_buf[i] = i;

    // 由于 MPU 已配置为非缓存,无需 Clean/Invalidate
    HAL_UART_Transmit_DMA(&huart1, tx_buf, DMA_BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, DMA_BUF_SIZE);

    while (1) {
        // 处理接收数据
    }
}

五、注意事项与避坑指南

  • MPU 配置必须在 SCB_EnableDCache() 之前,否则已缓存的数据可能不一致。
  • Cache 行大小 32 字节:手动维护时地址和长度必须 32 字节对齐,否则用 SCB_CleanInvalidateDCache_by_Addr 并确保不跨越其他数据。
  • DMA 描述符:若使用链表模式,描述符本身也需放在非缓存区或手动 Clean。
  • 性能权衡:非缓存区域会降低 CPU 访问速度,适合仅 DMA 频繁访问的缓冲区;其他内存仍保持 Cache 开启。
  • 多主设备:若以太网、SDMMC 等多个 DMA 同时访问,建议统一规划非缓存区域。
  • 调试时:可临时关闭 D-Cache 验证问题是否由一致性引起。

掌握 Clean/Invalidate 时机与 MPU 配置,就能在 STM32H7 上安全高效地使用 DMA,充分发挥 480MHz 的性能优势。