4、DMA与MDMA核心概念:DMA1/DMA2与MDMA的区别、MDMA特性(双缓冲、链表、突发传输)、MDMA工作模式
在STM32H7系列中,数据搬运的效率直接决定了摄像头帧数据能否在有限的帧间隔内完成传输,并顺利交给神经网络推理引擎。许多开发者初学时容易将DMA与MDMA混为一谈,认为它们只是“快一点”和“慢一点”的区别。实际上,DMA1/DMA2与MDMA在总线架构、传输粒度、描述符管理以及工作模式上存在本质差异。理解这些差异,是设计高性能视觉系统的前提。本章将围绕DMA与MDMA的核心概念展开,重点剖析MDMA的双缓冲、链表和突发传输特性,并梳理其工作模式,为后续的帧数据搬运与推理联动打下坚实基础。
上图展示了DMA1/DMA2与MDMA在系统架构中的位置差异。DMA1/DMA2挂载在AHB/APB总线上,主要服务于低速外设(如ADC、SPI、UART)与SRAM之间的数据搬运;而MDMA则直接连接AXI总线,能够以64位甚至更宽的突发方式访问AXI SRAM、Flash以及外部存储器,并且支持链表和双缓冲等高级特性。下面我们逐一深入解析。
4.1 DMA1/DMA2与MDMA的本质区别
4.1.1 核心原理:总线架构与传输粒度
STM32H7采用多域总线架构,分为D1域(AXI SRAM、Flash)、D2域(SRAM1/2/3)和D3域(备份SRAM)。DMA1/DMA2属于传统DMA控制器,它们通过AHB总线矩阵访问存储器,每次传输的最大数据宽度为32位,且FIFO深度仅4个字。这意味着当需要搬运大量连续数据(例如800×600×2字节的灰度帧)时,DMA1/DMA2需要频繁地仲裁总线,效率较低。
MDMA(Master Direct Memory Access)则是一个独立的总线主设备,拥有两个主端口(一个用于读取,一个用于写入),直接连接到AXI总线矩阵。它支持8/16/32/64位的数据宽度,并且可以发起长达256字节的突发传输。更重要的是,MDMA的每个通道都配备了一组链表描述符(Linked List),可以预先定义多个传输块(Block),由硬件自动依次执行,无需CPU干预。
| 对比维度 | DMA1/DMA2 | MDMA |
|---|---|---|
| 总线接口 | AHB/APB | AXI(主端口×2) |
| 最大数据宽度 | 32位 | 64位 |
| 突发传输 | 不支持(仅单次) | 支持,最长256字节 |
| 描述符 | 无,需软件重装 | 链表描述符,硬件自动加载 |
| 双缓冲 | 需手动切换 | 硬件自动Ping-Pong |
| 适用场景 | 低速外设数据采集 | 高速帧搬运、存储器间拷贝 |
4.1.2 判断方法:如何选择DMA还是MDMA
在实际工程中,判断依据主要有三点:数据源/目的地的位置、数据量大小以及是否需要多段传输。如果数据源是DCMI摄像头接口,且目标地址是AXI SRAM中的帧缓冲区,那么必须使用MDMA,因为DCMI本身不直接连接AXI,需要通过DMA将数据从FIFO搬到SRAM,但DMA的带宽不足以支撑高帧率。如果数据源是SPI Flash中的模型参数,需要加载到SRAM中供推理使用,同样推荐MDMA,因为它可以一次突发读取大量数据。
4.1.3 案例拆解:摄像头帧数据搬运的DMA选择
假设我们使用OV5640摄像头,输出RGB565格式,分辨率640×480,帧率30fps。每帧数据量为640×480×2 = 614,400字节。若使用DMA2(挂在AHB1上)搬运,由于AHB总线时钟为240MHz,理论带宽约960MB/s,但实际可用带宽受仲裁和FIFO限制,通常只能达到理论值的60%左右。而MDMA直接连接AXI,时钟频率可达480MHz,且支持64位突发,实测带宽可达1.2GB/s以上。更重要的是,MDMA的双缓冲模式可以在DCMI采集第一帧时,同时将上一帧数据搬运到推理引擎的输入缓冲区,实现流水线操作。
// MDMA配置示例(简化)
MDMA_InitTypeDef mdma_init = {0};
mdma_init.Request = MDMA_REQUEST_DMA1_TC3; // 由DMA1传输完成触发
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; // 32位
mdma_init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
mdma_init.DataAlignment = MDMA_DATAALIGN_PACK;
mdma_init.BufferTransferLength = 640*480/2; // 每次传输的字数
HAL_MDMA_Init(&mdma_handle, &mdma_init);
4.1.4 常见误区与总结
一个常见误区是认为MDMA可以完全替代DMA。实际上,MDMA不能直接访问APB外设(如UART、I2C),它只能通过DMA1/DMA2作为中介来与外设交互。另一个误区是忽略MDMA的链表描述符初始化,导致传输完成后无法自动加载下一个描述符。正确的做法是使用HAL_MDMA_Start_IT()并配置链表节点,确保每个节点都指向正确的源/目的地址和传输长度。
__ALIGN(8)或__ALIGN(16)声明缓冲区。
4.2 MDMA核心特性:双缓冲、链表与突发传输
4.2.1 双缓冲(Double Buffer)机制
双缓冲是MDMA最实用的特性之一。它允许MDMA在传输当前缓冲区数据的同时,预加载下一个缓冲区的描述符。当当前缓冲区传输完成,硬件自动切换到另一个缓冲区,无需CPU介入。这对于摄像头帧采集至关重要:当DCMI正在写入帧缓冲区A时,MDMA可以将帧缓冲区B中的上一帧数据搬运到推理引擎;当A写满后,两者角色互换。
双缓冲的实现依赖于MDMA的通道配置中的CTR(Current Target Register)位。当CTR=0时,使用缓冲区0;当CTR=1时,使用缓冲区1。传输完成后,硬件自动翻转CTR位,并触发中断。开发者只需在中断服务函数中更新已完成的缓冲区指针即可。
4.2.2 链表描述符(Linked List)
链表描述符是MDMA的另一大杀器。它允许将多个传输块(Block)串联起来,每个块可以有不同的源地址、目的地址和传输长度。MDMA硬件会自动从内存中加载下一个描述符,直到链表结束。这对于处理非连续的内存区域(例如将摄像头帧数据拆分为Y分量和UV分量分别搬运)非常有用。
链表描述符的结构体定义如下:
typedef struct {
uint32_t CTCR; // 传输控制寄存器(长度、地址增量)
uint32_t CBNDT; // 块内传输计数
uint32_t CSAR; // 源地址
uint32_t CDAR; // 目的地址
uint32_t CBRUR; // 块重复次数
uint32_t CLAR; // 下一个描述符地址
uint32_t CBR; // 块重复控制
uint32_t CMDR; // 模式寄存器
} MDMA_LinkNodeTypeDef;
在初始化时,需要将每个节点的CLAR字段指向下一个节点,最后一个节点的CLAR设为0表示链表结束。通过HAL_MDMA_Init()中的LinkedListNode参数传入链表头指针。
4.2.3 突发传输(Burst Transfer)
突发传输是MDMA高带宽的基石。MDMA支持1、2、4、8、16、32、64、128、256字节的突发长度。突发传输意味着MDMA在获得总线控制权后,连续发送多个数据,而不需要每次传输都重新仲裁。这大大减少了总线切换开销,尤其适合从AXI SRAM读取连续数据。
配置突发时需要注意:源突发长度(SBURST)和目的突发长度(DBURST)可以不同,但必须与数据宽度匹配。例如,如果数据宽度为32位,突发长度为4,则一次突发传输16字节。此外,突发传输要求源地址和目的地址必须按突发长度对齐,否则会触发错误。
4.2.4 常见误区与总结
一个常见误区是认为链表描述符越多越好。实际上,每个描述符的加载都需要额外的总线周期,如果链表节点过多,反而会降低效率。建议将连续的内存区域合并为一个节点,只在必要时拆分。另一个误区是忽略双缓冲的同步问题。当使用双缓冲时,必须确保在切换缓冲区之前,CPU或推理引擎已经完成了对旧缓冲区的读取,否则会造成数据覆盖。
4.3 MDMA工作模式详解
4.3.1 工作模式分类
MDMA支持三种主要工作模式:单次传输模式(Single Transfer)、块传输模式(Block Transfer)和重复块传输模式(Repeated Block Transfer)。在单次传输模式下,MDMA完成一个缓冲区传输后即停止,需要软件重新触发。块传输模式则通过链表描述符,可以连续执行多个块,直到链表结束。重复块传输模式则允许对同一个块重复执行多次,适用于需要周期性搬运数据的场景(例如每帧图像搬运两次,一次用于显示,一次用于推理)。
| 模式 | 触发方式 | 适用场景 | 中断产生时机 |
|---|---|---|---|
| 单次传输 | 软件或外设请求 | 小数据量一次性搬运 | 传输完成 |
| 块传输 | 链表节点自动加载 | 多段不连续数据搬运 | 每个块完成或全部完成 |
| 重复块传输 | 硬件定时器或外部事件 | 周期性帧采集 | 每次重复完成 |
4.3.2 操作流程:配置一个完整的MDMA传输
配置MDMA的流程可以归纳为以下步骤:
- 使能时钟:通过
__HAL_RCC_MDMA_CLK_ENABLE()使能MDMA时钟。 - 初始化句柄:填充
MDMA_InitTypeDef结构体,设置请求源、传输模式、数据宽度、突发长度等参数。 - 配置链表(可选):如果使用链表,需要定义并初始化
MDMA_LinkNodeTypeDef数组,并设置每个节点的地址和长度。 - 启动传输:调用
HAL_MDMA_Start_IT()或HAL_MDMA_Start(),传入源地址、目的地址和传输长度。 - 中断处理:在中断回调函数中检查传输完成标志,并更新缓冲区指针或触发推理任务。
// 启动MDMA传输示例
HAL_MDMA_Start_IT(&mdma_handle, (uint32_t)&frame_buffer_A,
(uint32_t)&inference_input, 640*480/2);
4.3.3 案例拆解:帧采集与推理的流水线联动
假设我们设计一个实时目标检测系统,摄像头输出30fps的RGB565图像。我们需要将每一帧数据从DCMI搬运到AXI SRAM,然后转换为推理引擎所需的浮点张量。传统做法是:DMA将帧数据搬到SRAM,CPU进行格式转换,再启动推理。这种方式CPU占用率高,且帧率受限。
使用MDMA后,我们可以构建如下流水线:
- 步骤1:DMA2将DCMI FIFO中的一帧数据搬运到帧缓冲区A(AXI SRAM)。
- 步骤2:DMA2传输完成中断触发MDMA,MDMA通过链表描述符将帧缓冲区A的数据搬运到推理输入缓冲区,同时进行像素格式转换(例如RGB565转RGB888)。
- 步骤3:MDMA传输完成中断触发推理引擎(如NPU或DSP)开始推理,同时DMA2开始搬运下一帧到帧缓冲区B。
在这个过程中,MDMA的双缓冲特性使得帧缓冲区A和B可以交替使用,而链表描述符则允许在一次MDMA传输中完成数据搬运和格式转换(通过设置不同的源/目的数据宽度)。实测表明,这种设计可以将CPU占用率从原来的40%降低到5%以下,帧率从15fps提升到30fps。
DataAlignment为MDMA_DATAALIGN_RIGHT,并确保目的缓冲区有足够的空间(每像素4字节)。
4.3.4 常见误区与总结
一个常见误区是认为MDMA的重复块传输模式可以无限循环,实际上它需要外部触发信号来重新启动。另一个误区是忽略MDMA与DMA之间的同步。当DMA正在写入帧缓冲区时,如果MDMA同时读取该缓冲区,会导致数据不一致。解决方法是使用DMA的传输完成中断来触发MDMA,确保数据完整后再搬运。