引言

在嵌入式 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 的页命中率下降,导致额外延迟。

瓶颈根源:

  1. SDRAM 行切换开销: 两次搬运间,SDRAM 行缓冲失效,导致预充电和激活延迟。
  2. AXI 总线仲裁: MDMA 和 DMA2D 共享 AXI 总线矩阵,但串联时无法并行,因为 DMA2D 必须等 MDMA 完成。
  3. 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 和突发长度,可有效减少瓶颈。希望本文的实测数据能帮助你在项目中做出更优选择。