引言
在嵌入式 GUI 或摄像头应用中,帧缓冲(Frame Buffer)的搬运效率直接决定系统流畅度。STM32H7 内置 MDMA(Master DMA)和 DMA2D(2D DMA/图形加速器),两者都能访问 AXI SRAM 和 SDRAM,但协同工作时,总线仲裁和缓存一致性(Cache Coherency)问题常被忽视。本文通过实测数据,揭示带宽瓶颈所在,并提供可复用的配置方法。
硬件与测试环境
- MCU: STM32H743VIT6(主频 480MHz,AXI SRAM 1MB)
- 外部存储: 32-bit SDRAM(16MB,时钟 100MHz)
- 测试场景: 将 800x480 RGB565 帧缓冲(约 768KB)从 SDRAM 搬运到 AXI SRAM,再搬运回 SDRAM(模拟双缓冲切换)
- 工具: 内部 DWT 计数器(CYCCNT)计时,串口打印结果
原理:MDMA 与 DMA2D 的差异
- DMA2D: 专为图形设计,支持颜色格式转换、混合、填充等,但只能访问 AXI 总线上的存储器(如 SDRAM、AXI SRAM),且无法访问 TCM 或 Flash。
- MDMA: 通用 DMA,支持任意存储器间搬运(包括 TCM),但无图形处理能力。MDMA 支持 Linked-List 和 4x4 突发传输,适合大数据块。
- 协同方式: 通常用 MDMA 将数据从外设或 TCM 搬到 AXI SRAM,再用 DMA2D 做格式转换或混合。但若两者同时操作同一总线,会竞争带宽。
实测配置与代码
1. 单独使用 DMA2D 搬运
配置 DMA2D 为内存到内存模式,不启用格式转换(直接复制)。
void DMA2D_Copy(uint32_t srcAddr, uint32_t dstAddr, uint32_t size) {
DMA2D->CR = 0; // 复位
DMA2D->FGMAR = srcAddr;
DMA2D->BGMAR = dstAddr;
DMA2D->FGOR = 0;
DMA2D->BGOR = 0;
DMA2D->FGPFCCR = DMA2D_INPUT_RGB565;
DMA2D->BGPFCCR = DMA2D_INPUT_RGB565;
DMA2D->NLR = (size / 2) | (1 << 16); // 每行1像素,总字节数/2
DMA2D->CR = DMA2D_CR_START;
while (DMA2D->CR & DMA2D_CR_START);
}
2. 单独使用 MDMA 搬运
MDMA 配置为单块传输,突发 16 拍,字宽 32 位。
void MDMA_Copy(uint32_t srcAddr, uint32_t dstAddr, uint32_t size) {
MDMA_InitTypeDef mdma_init = {0};
__HAL_RCC_MDMA_CLK_ENABLE();
mdma_init.Request = MDMA_REQUEST_SW;
mdma_init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
mdma_init.SourceInc = MDMA_SRC_INC_WORD;
mdma_init.DestInc = MDMA_DEST_INC_WORD;
mdma_init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
mdma_init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
mdma_init.SourceBurst = MDMA_SOURCE_BURST_16BEATS;
mdma_init.DestBurst = MDMA_DEST_BURST_16BEATS;
HAL_MDMA_Init(&mdma_handle, &mdma_init);
HAL_MDMA_Start(&mdma_handle, srcAddr, dstAddr, size/4, 0, 0);
HAL_MDMA_PollForTransfer(&mdma_handle, HAL_MDMA_FULL_TRANSFER, HAL_MAX_DELAY);
}
3. MDMA 与 DMA2D 协同(串联)
先用 MDMA 将 SDRAM 数据搬到 AXI SRAM 临时区,再用 DMA2D 从临时区搬到目标 SDRAM 地址(模拟格式转换)。
void MDMA_DMA2D_Coop(uint32_t src, uint32_t dst, uint32_t size) {
uint32_t tmp = 0x24000000; // AXI SRAM 临时区
MDMA_Copy(src, tmp, size);
DMA2D_Copy(tmp, dst, size);
}
实测结果与瓶颈分析
| 方法 | 耗时 (ms) | 带宽 (MB/s) | 总线利用率 | |------|----------|------------|-----------| | 仅 DMA2D | 2.15 | 357 | 约 45% | | 仅 MDMA | 2.38 | 322 | 约 40% | | MDMA+DMA2D 串联 | 4.62 | 166 | 约 21% |
关键发现:
- 单独使用 DMA2D 或 MDMA 时,带宽接近 350MB/s,但远低于理论峰值(SDRAM 32-bit @100MHz 理论 400MB/s)。
- 串联时总耗时几乎等于两者之和,且带宽减半,说明瓶颈不在 DMA 引擎本身,而在 总线仲裁 和 缓存一致性。
- 当 DMA2D 和 MDMA 交替访问 SDRAM 时,每次切换都需要重新仲裁,且 SDRAM 的页命中率下降,导致额外延迟。
瓶颈根源:
- SDRAM 行切换开销: 两次搬运间,SDRAM 行缓冲失效,导致预充电和激活延迟。
- AXI 总线仲裁: MDMA 和 DMA2D 共享 AXI 总线矩阵,但串联时无法并行,因为 DMA2D 必须等 MDMA 完成。
- Cache 一致性问题: 若 CPU 启用了 D-Cache,搬运后数据可能被缓存,导致 DMA 看到旧数据。本测试中已关闭 D-Cache 以避免干扰,但实际应用需注意。
优化建议
- 使用 DMA2D 的 P2P(像素到像素)模式: 若只需复制,直接用 DMA2D,避免 MDMA 参与。
- 并行化: 若必须 MDMA+DMA2D,可让 MDMA 搬运下一块数据的同时,DMA2D 处理当前块(流水线),但需双缓冲临时区。
- 调整突发长度: 实测中 MDMA 用 16 拍突发比 4 拍高约 15% 带宽,但超过 16 拍无提升。
- 关闭 D-Cache 或使用 MPU 配置非缓存区域: 对 DMA 缓冲区设置 MPU 为 Non-cacheable,避免一致性开销。
- 考虑使用 LTDC 直接读取 SDRAM: 若目标是显示,LTDC 有专用 FIFO,可减少 CPU/DMA 干预。
完整代码示例(简化)
以下为测试主函数核心部分,包含 DWT 计时和串口输出。
#include "stm32h7xx_hal.h"
uint32_t DWT_GetCycle(void) { return DWT->CYCCNT; }
int main(void) {
HAL_Init();
// 初始化时钟、串口、SDRAM、DMA2D、MDMA...
uint32_t src = 0xD0000000; // SDRAM 地址
uint32_t dst = 0xD0200000;
uint32_t size = 800*480*2;
// 测试 DMA2D
DWT->CYCCNT = 0;
uint32_t t0 = DWT_GetCycle();
DMA2D_Copy(src, dst, size);
uint32_t t1 = DWT_GetCycle();
printf("DMA2D: %f ms\n", (t1-t0)/480.0);
// 测试 MDMA
t0 = DWT_GetCycle();
MDMA_Copy(src, dst, size);
t1 = DWT_GetCycle();
printf("MDMA: %f ms\n", (t1-t0)/480.0);
// 测试协同
t0 = DWT_GetCycle();
MDMA_DMA2D_Coop(src, dst, size);
t1 = DWT_GetCycle();
printf("Coop: %f ms\n", (t1-t0)/480.0);
while(1);
}
注意事项
- MPU 配置: 若启用 D-Cache,务必对 DMA 缓冲区设置 MPU 为 Non-cacheable,否则会出现数据不一致。
- SDRAM 时序: 调整 SDRAM 时序参数(如 CAS 延迟)可能提升带宽,但需确保稳定性。
- DMA2D 的 NLR 寄存器: 当每行像素数大于 65535 时需分块,但本测试中 800 像素安全。
- MDMA 的 Linked-List: 对于多块搬运,使用链表可减少 CPU 干预,但需注意链表本身位于非缓存区域。
总结
STM32H7 的 MDMA 和 DMA2D 协同搬运帧缓冲,实际带宽受限于总线仲裁和 SDRAM 行切换,而非 DMA 引擎本身。单独使用 DMA2D 效率最高,若需格式转换,建议采用流水线设计。合理配置 MPU 和突发长度,可有效减少瓶颈。希望本文的实测数据能帮助你在项目中做出更优选择。