STM32H7 显存搬运的 Cache 一致性难题:MDMA 与 DMA2D 协同的三种实战策略

一、问题根源:Cortex-M7 的 Cache 与 DMA 的“盲区”

STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,内置 L1-Cache(I-Cache 和 D-Cache)。D-Cache 通过写回(Write-back)策略缓存内存数据,CPU 写入显存(通常位于 AXI SRAM 或外部 SDRAM)时,数据可能只停留在 Cache 中,尚未同步到物理内存。

而 MDMA(Master DMA)和 DMA2D(2D 图形加速器)作为总线主设备,直接访问物理内存,它们看不到 CPU 的 Cache。当 CPU 通过 Cache 写入数据后,立即启动 MDMA 搬运,MDMA 读到的可能是旧数据;反之,DMA2D 写入显存后,CPU 读取时可能命中过期的 Cache 行,导致花屏或数据错乱。

二、三种实战策略详解

策略一:全量失效(Invalidate)与 Clean——简单粗暴,适合小显存

原理:在 DMA 操作前,将 D-Cache 中对应区域的数据写回内存(Clean);在 DMA 写完后,使 Cache 行失效(Invalidate),强制 CPU 从内存重新读取。

适用场景:显存较小(<16KB),或对性能要求不苛刻的场合。

配置步骤

  1. 开启 D-Cache(默认开启)。
  2. 在 MDMA 搬运前调用 SCB_CleanDCache()SCB_CleanDCache_by_Addr()
  3. 在 DMA2D 写入完成后调用 SCB_InvalidateDCache_by_Addr()

代码示例

// 假设显存区域:uint32_t fb[320*240] __attribute__((section(".ARM.__at_0x24000000")));
#define FB_ADDR  0x24000000
#define FB_SIZE  (320*240*4)  // 320x240 ARGB8888

// MDMA 搬运前:确保 CPU 写入的数据落内存
SCB_CleanDCache_by_Addr((uint32_t*)FB_ADDR, FB_SIZE);

// 启动 MDMA 搬运(从显存到另一缓冲区)
MDMA_Start(&mdma_handle, FB_ADDR, dst_addr, FB_SIZE/4);

// DMA2D 写入显存后:使 Cache 失效,让 CPU 重新读取
SCB_InvalidateDCache_by_Addr((uint32_t*)FB_ADDR, FB_SIZE);

注意事项

  • 地址需 32 字节对齐(Cache line 大小)。
  • 全量操作开销随区域大小线性增长,大显存时性能下降明显。

策略二:按区域 Clean+Invalidate——精准打击,平衡性能

原理:只对 DMA 涉及的具体内存区域执行 Clean 或 Invalidate,避免全量操作。

适用场景:显存较大(如 800x480 的 RGB565 帧缓冲),且频繁更新局部区域。

配置步骤

  1. 定义显存区域和子区域。
  2. 在 MDMA 读取前,对源区域执行 Clean;在 DMA2D 写入后,对目标区域执行 Invalidate。
  3. 使用 SCB_CleanDCache_by_AddrSCB_InvalidateDCache_by_Addr,注意地址对齐。

代码示例

// 定义显存区域(假设 800x480 RGB565,每像素 2 字节)
#define LCD_WIDTH  800
#define LCD_HEIGHT 480
#define LCD_PIXEL  2
#define LCD_FB_SIZE (LCD_WIDTH*LCD_HEIGHT*LCD_PIXEL)
#define LCD_FB_ADDR 0xD0000000  // 外部 SDRAM

// 更新一个矩形区域 (x, y, w, h)
void update_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
    uint32_t offset = (y*LCD_WIDTH + x) * LCD_PIXEL;
    uint32_t size = w * h * LCD_PIXEL;
    uint32_t addr = LCD_FB_ADDR + offset;

    // 1. CPU 绘制到显存(写入 Cache)
    draw_rect(x, y, w, h);

    // 2. 启动 DMA2D 搬运前,Clean 该区域
    SCB_CleanDCache_by_Addr((uint32_t*)addr, size);

    // 3. 启动 DMA2D 搬运(例如从另一缓冲到显存)
    DMA2D_Start(&dma2d_handle, src_addr, addr, w, h);

    // 4. 搬运完成后,Invalidate 该区域,供 CPU 后续读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)addr, size);
}

注意事项

  • 确保区域大小向上取整到 32 字节倍数,否则可能遗漏边界。
  • 多次小区域操作时,可合并相邻区域以减少开销。

策略三:MPU 配置非 Cacheable 区域——釜底抽薪,零维护

原理:利用 Cortex-M7 的 MPU(Memory Protection Unit),将显存所在的地址区域配置为 非 Cacheable(或 Write-through)。这样 CPU 访问该区域时直接读写物理内存,DMA 与 CPU 看到的数据始终一致,无需任何软件维护。

适用场景:显存区域固定,且对性能要求极高(如持续视频流)。

配置步骤

  1. 使能 MPU,配置一个区域覆盖显存地址。
  2. 设置 TEX=1, C=0, B=0(非 Cacheable)或 TEX=0, C=1, B=1(Write-through,但仍有缓存)。推荐非 Cacheable。
  3. 在启动代码或系统初始化中配置。

代码示例(使用 STM32H7 HAL 库):

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

    // 禁用 MPU
    HAL_MPU_Disable();

    // 配置显存区域(例如 SDRAM 0xD0000000,大小 8MB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0xD0000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_8MB;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;  // TEX=1
    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;  // 非 Cacheable
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 在 main 初始化中调用 MPU_Config();

注意事项

  • 非 Cacheable 区域 CPU 访问速度较慢,但 DMA 效率不受影响。
  • 若同时需要 CPU 频繁读写该区域,可考虑 Write-through(TEX=0, C=1, B=1),但仍有缓存一致性问题,需在 DMA 前 Clean。
  • 确保 MPU 区域与显存实际大小匹配,避免覆盖其他关键内存。

三、策略对比与选型建议

| 策略 | 性能开销 | 实现复杂度 | 适用场景 | |------|----------|------------|----------| | 全量失效 | 高(线性增长) | 低 | 小显存、低频操作 | | 按区域 Clean+Invalidate | 中(按需) | 中 | 大显存、局部更新 | | MPU 非 Cacheable | 低(无软件开销) | 中 | 固定显存、高频 DMA |

实战建议

  • 如果显存小于 16KB,直接使用策略一,代码简单。
  • 如果显存较大且更新频繁,优先策略二,配合 DMA2D 的矩形搬运。
  • 如果项目对实时性要求极高,且显存区域固定,果断采用策略三,彻底消除一致性隐患。

四、总结

Cache 一致性是 STM32H7 高性能开发的必经之路。本文的三种策略从“软件维护”到“硬件隔离”层层递进,开发者可根据实际需求灵活选择。记住:没有银弹,只有最合适的方案。建议在项目初期就规划好显存布局和 Cache 策略,避免后期调试的深坑。

希望这篇文章能帮助你彻底驾驭 MDMA 与 DMA2D 的协同工作,让图形性能真正飞起来!