STM32H7 双缓冲图形加速:MDMA 与 DMA2D 协同配置深度解析
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 高性能 MCU 上,图形界面(GUI)的流畅度往往受限于内存带宽和 CPU 占用。本文深入剖析如何利用 MDMA(主 DMA)与 DMA2D(图形 DMA)协同工作,实现双缓冲机制下的零拷贝帧传输与图形加速。文章涵盖硬件架构原理、关键寄存器配置、内存屏障与缓存一致性处理,并提供完整的初始化与中断协作代码示例,帮助开发者突破图形性能瓶颈。
# STM32H7 双缓冲图形加速:MDMA 与 DMA2D 协同配置深度解析
## 一、为什么需要 MDMA + DMA2D 协同?
在 STM32H7 系列(如 H743/H750)中,CPU 主频高达 480MHz,但图形渲染(如 LVGL、TouchGFX)若直接由 CPU 操作显存,会占用大量内核周期。DMA2D 专为 2D 图形加速设计,可执行颜色填充、位块传输(BLT)、像素格式转换等操作,但 DMA2D 本身无法直接访问外部 SDRAM(除非通过总线矩阵),且其传输完成中断会打断 CPU。
MDMA(Master DMA)是 STM32H7 新增的高性能 DMA 控制器,支持内存到内存、外设到内存的突发传输,可链接列表(Linked List)方式描述复杂传输任务。通过 MDMA 将数据从后台缓冲(如 SDRAM)搬运到 DMA2D 的输入 FIFO,或直接搬运到 LTDC(LCD 控制器)的帧缓冲,可彻底解放 CPU。
**协同优势**:
- 双缓冲:后台缓冲(渲染)与前台缓冲(显示)切换,避免撕裂。
- 零拷贝:MDMA 直接搬运像素数据,无需 CPU 介入。
- 流水线:DMA2D 处理像素格式转换(如 RGB565→ARGB8888),MDMA 负责大块数据搬运,两者并行。
## 二、硬件架构与内存映射
STM32H7 的总线矩阵支持多个主设备(CPU、MDMA、DMA2D、LTDC)并发访问内存。关键点:
- MDMA 可访问所有内存空间(包括 SDRAM、内部 SRAM、Flash)。
- DMA2D 只能通过 AHB 主接口访问,但可配置为从 MDMA 的 FIFO 读取数据(需通过 DMA2D 的外设接口)。
- 双缓冲通常使用两个帧缓冲:Buffer0 和 Buffer1,位于 SDRAM 中(如 0xC0000000)。
## 三、关键配置步骤
### 1. 时钟与 GPIO 初始化
确保 MDMA、DMA2D、LTDC 时钟使能,并配置 SDRAM 控制器(FMC)。
```c
__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_DMA2D_CLK_ENABLE();
__HAL_RCC_LTDC_CLK_ENABLE();
// FMC SDRAM 初始化略,需确保时序正确
```
### 2. DMA2D 配置(以像素格式转换为例)
配置 DMA2D 为内存到内存模式,输入 RGB565,输出 ARGB8888。
```c
DMA2D_HandleTypeDef hdma2d;
hdma2d.Instance = DMA2D;
hdma2d.Init.Mode = DMA2D_M2M; // 内存到内存
hdma2d.Init.ColorMode = DMA2D_ARGB8888; // 输出格式
hdma2d.Init.OutputOffset = 0; // 行偏移
hdma2d.Init.AlphaInverted = DISABLE;
hdma2d.Init.RedBlueSwap = DISABLE;
HAL_DMA2D_Init(&hdma2d);
// 配置输入层(前景层)
DMA2D_FG_InitTypeDef fg;
fg.ColorMode = DMA2D_RGB565;
fg.AlphaMode = DMA2D_NO_MODIF_ALPHA;
fg.InputOffset = 0;
fg.RedBlueSwap = DISABLE;
fg.Alpha = 0xFF; // 不透明
HAL_DMA2D_ConfigLayer(&hdma2d, 0, &fg);
```
### 3. MDMA 配置(链接列表模式)
MDMA 支持链接列表,可描述多个传输块。这里配置一个简单的内存到内存传输,将后台缓冲数据搬运到 DMA2D 的输入地址(即 DMA2D 的 FG 基地址)。
```c
MDMA_HandleTypeDef hmdma;
MDMA_LinkNodeTypeDef node;
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_MEM2MEM;
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
hmdma.Init.SourceInc = MDMA_SRC_INC_WORD; // 源地址递增
hmdma.Init.DestInc = MDMA_DEST_INC_WORD; // 目的地址递增
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = 1024*600; // 总字节数(需为块大小倍数)
HAL_MDMA_Init(&hmdma);
// 配置链接节点
node.SourceAddress = (uint32_t)background_buffer; // 源地址
node.DestAddress = (uint32_t)&DMA2D->FGCOLR; // 注意:这里应指向 DMA2D 的输入地址(如 FG 的基地址寄存器,但实际需通过外设接口)
// 实际中,DMA2D 的输入地址由 DMA2D 的 FG 基地址寄存器(FG0AR)决定,MDMA 应写入该寄存器?不,MDMA 直接搬运数据到 DMA2D 的 FIFO,需配置 DMA2D 为外设模式。
// 简化:MDMA 将数据从 SDRAM 搬运到内部 SRAM 的中间缓冲,DMA2D 再从该缓冲读取。
// 此处为示例,实际需根据具体应用调整。
node.BlockSize = 1024*600; // 块大小
node.SourceInc = MDMA_SRC_INC_WORD;
node.DestInc = MDMA_DEST_INC_WORD;
node.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
node.DestDataSize = MDMA_DEST_DATASIZE_WORD;
node.NextNodeAddress = 0; // 单节点
HAL_MDMA_ConfigLinkNode(&node);
HAL_MDMA_Start(&hmdma, (uint32_t)background_buffer, (uint32_t)intermediate_buffer, 1024*600);
```
注意:MDMA 与 DMA2D 协同的典型模式是:MDMA 将数据从 SDRAM 搬运到内部 SRAM(DMA2D 可访问),然后 DMA2D 处理格式转换并输出到 LTDC 帧缓冲。或者,若 DMA2D 支持外设模式,可配置 MDMA 直接写入 DMA2D 的 FIFO,但需仔细查阅参考手册。
### 4. 双缓冲切换与同步
使用 LTDC 的影式寄存器(Shadow Registers)实现缓冲切换,配合 MDMA 和 DMA2D 完成中断。
```c
// 在 DMA2D 传输完成中断中,切换 LTDC 帧缓冲
void DMA2D_IRQHandler(void) {
if (__HAL_DMA2D_GET_FLAG(&hdma2d, DMA2D_FLAG_TC)) {
__HAL_DMA2D_CLEAR_FLAG(&hdma2d, DMA2D_FLAG_TC);
// 切换前台缓冲
LTDC->Layer1->WHPCR = (uint32_t)&front_buffer;
// 等待垂直消隐期(可选)
while ((LTDC->CDSR & LTDC_CDSR_VSYNCS) == 0);
// 交换指针
uint32_t *tmp = front_buffer;
front_buffer = back_buffer;
back_buffer = tmp;
}
}
```
### 5. 缓存一致性处理
STM32H7 的 D-Cache 可能导致 DMA 访问的数据不一致。必须对 MDMA 和 DMA2D 涉及的缓冲区执行 Cache 维护操作。
```c
// 在 MDMA 启动前,将源缓冲区(后台缓冲)的 Cache 行回写
SCB_CleanDCache_by_Addr((uint32_t*)background_buffer, buffer_size);
// 在 MDMA 完成后,使目的缓冲区(中间缓冲)的 Cache 失效
SCB_InvalidateDCache_by_Addr((uint32_t*)intermediate_buffer, buffer_size);
// 在 DMA2D 启动前,同样处理输入缓冲区
SCB_CleanDCache_by_Addr((uint32_t*)intermediate_buffer, buffer_size);
// 在 DMA2D 完成后,使输出缓冲区(帧缓冲)失效
SCB_InvalidateDCache_by_Addr((uint32_t*)front_buffer, buffer_size);
```
## 四、完整代码示例(简化)
以下是一个初始化函数,整合了 MDMA 和 DMA2D 的配置,并启动一次传输。
```c
void Graphics_Accelerated_Transfer(uint32_t *src, uint32_t *dst, uint32_t size) {
// 1. 清理源缓存
SCB_CleanDCache_by_Addr((uint32_t*)src, size);
// 2. 配置 DMA2D 为 M2M 模式,输入 RGB565,输出 ARGB8888(假设)
// 这里省略具体配置,使用之前初始化的 hdma2d
// 3. 配置 MDMA 将 src 搬运到中间缓冲(内部 SRAM)
HAL_MDMA_Start(&hmdma, (uint32_t)src, (uint32_t)intermediate, size);
// 等待 MDMA 完成(可中断)
HAL_MDMA_PollForTransfer(&hmdma, HAL_MAX_DELAY);
// 4. 使中间缓冲缓存失效,供 DMA2D 读取
SCB_InvalidateDCache_by_Addr((uint32_t*)intermediate, size);
// 5. 启动 DMA2D 传输,从中间缓冲到帧缓冲
HAL_DMA2D_Start(&hdma2d, (uint32_t)intermediate, (uint32_t)dst, width, height);
// 等待完成(或中断)
HAL_DMA2D_PollForTransfer(&hdma2d, HAL_MAX_DELAY);
// 6. 使帧缓冲缓存失效,供 LTDC 读取
SCB_InvalidateDCache_by_Addr((uint32_t*)dst, size);
}
```
## 五、注意事项与常见陷阱
- **内存对齐**:MDMA 和 DMA2D 要求源/目的地址按 4 字节对齐,否则可能触发总线错误。建议使用 `__attribute__((aligned(4)))`。
- **传输长度**:MDMA 的 `BufferTransferLength` 必须是块大小的整数倍,且块大小需匹配 DMA2D 的行宽。
- **中断优先级**:MDMA 和 DMA2D 中断应设置为高于 LTDC 垂直消隐中断,避免帧撕裂。
- **链接列表**:复杂场景下,使用 MDMA 的链接列表可避免 CPU 多次启动传输,但需注意节点内存的 Cache 一致性。
- **DMA2D 外设模式**:若使用 DMA2D 的外设模式(如从 MDMA 读取),需配置 `DMA2D->CR` 的 `MODE` 位,并确保 MDMA 的目的地址指向 DMA2D 的 FIFO 地址(如 `DMA2D_FG0AR` 寄存器映射的地址)。
- **功耗与带宽**:MDMA 和 DMA2D 同时工作会增加总线负载,需合理配置 AHB 总线矩阵的 QoS 优先级。
## 六、总结
MDMA 与 DMA2D 的协同,将图形数据搬运与像素处理分离,充分利用 STM32H7 的总线架构,显著降低 CPU 占用。通过双缓冲机制,配合缓存一致性维护,可实现高帧率、无撕裂的 GUI 显示。开发者需深入理解 MDMA 的链接列表和 DMA2D 的层配置,并根据实际内存布局调整传输策略。