STM32H7 显存搬运的 Cache 一致性难题:MDMA 与 DMA2D 协同的三种实战策略
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 高性能平台上,MDMA 与 DMA2D 协同搬运显存可大幅提升图形性能,但 Cache 一致性常成为性能杀手。本文深入剖析 Cortex-M7 的 Cache 架构与一致性问题,并给出三种实战策略:全量失效、按区域 Clean+Invalidate、以及利用 MPU 配置非 Cacheable 区域。每种策略均附原理讲解、配置步骤和完整代码示例,助你彻底告别花屏与数据错乱。
# 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()`。
**代码示例**:
```c
// 假设显存区域: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_Addr` 和 `SCB_InvalidateDCache_by_Addr`,注意地址对齐。
**代码示例**:
```c
// 定义显存区域(假设 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 库):
```c
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 的协同工作,让图形性能真正飞起来!