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),或对性能要求不苛刻的场合。
配置步骤:
- 开启 D-Cache(默认开启)。
- 在 MDMA 搬运前调用
SCB_CleanDCache()或SCB_CleanDCache_by_Addr()。 - 在 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 帧缓冲),且频繁更新局部区域。
配置步骤:
- 定义显存区域和子区域。
- 在 MDMA 读取前,对源区域执行 Clean;在 DMA2D 写入后,对目标区域执行 Invalidate。
- 使用
SCB_CleanDCache_by_Addr和SCB_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 看到的数据始终一致,无需任何软件维护。
适用场景:显存区域固定,且对性能要求极高(如持续视频流)。
配置步骤:
- 使能 MPU,配置一个区域覆盖显存地址。
- 设置 TEX=1, C=0, B=0(非 Cacheable)或 TEX=0, C=1, B=1(Write-through,但仍有缓存)。推荐非 Cacheable。
- 在启动代码或系统初始化中配置。
代码示例(使用 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 的协同工作,让图形性能真正飞起来!