一、为什么D-Cache与DMA会冲突?

STM32H7 的 Cortex-M7 内核带有 16KB D-Cache 和 16KB I-Cache。D-Cache 作为 CPU 与主存之间的高速缓冲,能大幅提升数据访问效率。但 DMA 控制器直接访问物理内存,不经过 Cache。

冲突场景:

  • CPU 写数据到缓冲区(数据暂存在 Cache 中,未写回内存),随后启动 DMA 发送。DMA 从内存读到的是旧数据。
  • DMA 将外设数据写入内存,CPU 读取同一缓冲区时,Cache 中可能仍是旧内容,导致读到脏数据。

因此,必须通过 MPU 配置内存属性 或 手动 clean/invalidate 操作 来维护一致性。

二、MPU 配置:从根源上划分内存区域

MPU(Memory Protection Unit)可将内存划分为不同属性的区域。对于 DMA 缓冲区,推荐配置为 Non-Cacheable 或 Write-Through,避免手动维护。

2.1 关键属性说明

  • TEX=0, C=0, B=0:Non-Cacheable,读写均不经过 Cache,适合 DMA 缓冲区。
  • TEX=0, C=1, B=0:Write-Back, Write-Allocate,性能最高但需手动维护。
  • TEX=0, C=1, B=1:Write-Through,写操作同时更新 Cache 和内存,读仍可缓存。

2.2 MPU 配置步骤(以 Non-Cacheable 为例)

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 关闭 D-Cache 和 I-Cache(配置期间)。
  3. 禁用 MPU:MPU->CTRL = 0;
  4. 配置区域基址、属性、大小。
  5. 使能 MPU 和 Cache。
#include "stm32h7xx.h"

// 定义 DMA 缓冲区所在区域,例如 0x30000000 (D2 SRAM)
#define DMA_BUFFER_ADDR  0x30000000
#define DMA_BUFFER_SIZE  1024  // 1KB

void MPU_Config(void)
{
    // 关闭 D-Cache 和 I-Cache
    SCB_DisableDCache();
    SCB_DisableICache();

    // 禁用 MPU
    MPU->CTRL = 0;

    // 配置 Region 0
    MPU->RNR = 0;  // 选择区域 0

    // 基址:必须对齐到区域大小
    MPU->RBAR = DMA_BUFFER_ADDR & MPU_RBAR_ADDR_Msk;

    // 属性:Non-Cacheable, Non-Shareable, 可读写
    // AP=011 (Full access), TEX=0, C=0, B=0, S=0
    MPU->RASR = (0x3 << MPU_RASR_AP_Pos) |
                (0x0 << MPU_RASR_TEX_Pos) |
                (0x0 << MPU_RASR_C_Pos)   |
                (0x0 << MPU_RASR_B_Pos)   |
                (0x0 << MPU_RASR_S_Pos)   |
                MPU_RASR_ENABLE_Msk;

    // 设置区域大小:1KB -> SIZE=9 (2^(9+1)=1024)
    // 注意:SIZE 字段编码为 log2(size)-1
    MPU->RASR |= (9 << MPU_RASR_SIZE_Pos);

    // 使能 MPU
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;

    // 使能 Cache
    SCB_EnableICache();
    SCB_EnableDCache();
}

注意:区域大小必须为 2 的幂,且基址按大小对齐。若缓冲区跨越多个区域,需分别配置。

三、手动 clean/invalidate 的边界条件

若无法使用 MPU(如缓冲区动态分配),则需手动维护。核心函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中脏数据写回内存。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载。

3.1 正确使用顺序

CPU 写 -> DMA 读(发送):

  1. CPU 填充缓冲区。
  2. SCB_CleanDCache_by_Addr 写回内存。
  3. 启动 DMA。

DMA 写 -> CPU 读(接收):

  1. 启动 DMA 前,SCB_InvalidateDCache_by_Addr 确保 Cache 无旧数据。
  2. DMA 完成中断中,再次 SCB_InvalidateDCache_by_Addr 丢弃可能被预取的旧行。
  3. CPU 读取数据。

3.2 边界条件与陷阱

  • 地址对齐:操作地址必须 32 字节对齐,长度向上取整到 32 字节。否则可能误伤相邻数据。
  • 缓冲区重叠:若两个 DMA 缓冲区共享同一 Cache 行,clean 一个可能影响另一个。建议缓冲区按 32 字节对齐并填充。
  • 中断中调用:clean/invalidate 本身不阻塞,但需确保 DMA 未在传输中。
  • 性能损耗:频繁 clean/invalidate 会降低 Cache 效率,尽量用 MPU 替代。
// 安全封装:处理对齐
void Safe_CleanDCache(void *addr, uint32_t size)
{
    uint32_t start = (uint32_t)addr & ~0x1F;  // 向下对齐
    uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
    SCB_CleanDCache_by_Addr((uint32_t*)start, end - start);
}

void Safe_InvalidateDCache(void *addr, uint32_t size)
{
    uint32_t start = (uint32_t)addr & ~0x1F;
    uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
    SCB_InvalidateDCache_by_Addr((uint32_t*)start, end - start);
}

四、完整实战示例:UART DMA 接收

以 UART 空闲中断 + DMA 接收为例,展示一致性处理。

#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t rxBuffer[RX_BUFFER_SIZE];

void UART_DMA_Init(void)
{
    // 1. 配置 DMA 为循环模式,目标地址 rxBuffer
    // 2. 使能 UART 空闲中断
    // 3. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}

// UART 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    // 丢弃 Cache 中可能存在的旧数据
    Safe_InvalidateDCache(rxBuffer, Size);

    // 此时 rxBuffer 中为最新数据,可安全处理
    ProcessData(rxBuffer, Size);

    // 重新启动接收(注意:需先 invalidate 再启动)
    Safe_InvalidateDCache(rxBuffer, RX_BUFFER_SIZE);
    HAL_UART_Receive_DMA(&huart1, rxBuffer, RX_BUFFER_SIZE);
}

五、注意事项总结

  • 优先使用 MPU:将 DMA 缓冲区设为 Non-Cacheable 是最简单可靠的方式。
  • 对齐是生命线:所有 clean/invalidate 操作必须 32 字节对齐,长度向上取整。
  • 避免缓冲区共享 Cache 行:不同 DMA 缓冲区之间至少间隔 32 字节。
  • DMA 传输中禁止 invalidate:否则可能丢失未写回的数据。
  • 调试时关闭 Cache:若数据异常,可暂时关闭 D-Cache 定位问题。
  • 参考手册:Cortex-M7 的 TRM 和 STM32H7 参考手册中 Cache 章节有详细说明。

掌握这些要点,你就能在 STM32H7 上安全地同时使用 D-Cache 和 DMA,充分发挥芯片性能。