# 引言 在高速数据采集或通信系统中,外设数据频繁涌入内存,若每次中断都唤醒CPU搬运,不仅浪费算力,还可能导致实时性下降。STM32H7内置的MDMA(主DMA)与LPDMA(低功耗DMA)提供了多级DMA链式传输能力,可实现外设→LPDMA→MDMA→RAM的零拷贝流水线,让CPU彻底从数据搬运中解放。 # 一、原理剖析:双DMA如何协同 ## 1.1 总线架构与DMA角色 STM32H7采用AXI总线矩阵,MDMA连接在AXI主接口上,可访问全部存储器与外设;LPDMA则挂在APB总线域,适合低速外设(如UART、SPI)。两者通过硬件握手信号(如MDMA的请求信号)可级联工作: - **LPDMA**:负责从外设接收数据块,存入中间缓冲区(SRAM)。 - **MDMA**:检测到LPDMA传输完成事件后,自动将中间缓冲区数据搬运至目标RAM(如D1域SRAM或外部SDRAM)。 这种设计避免了CPU参与每一字节的搬运,且MDMA的突发传输能力(支持8/16/32字节突发)极大提升了总线利用率。 ## 1.2 零拷贝的关键:双缓冲与硬件握手 零拷贝意味着数据从外设到最终RAM不经过CPU寄存器或额外复制。实现要点: - 使用LPDMA的循环模式或双缓冲模式,确保外设数据连续写入。 - 配置MDMA的Linked List(链表)模式,使MDMA能自动处理多个传输描述符,无需CPU干预。 - 通过LPDMA的传输完成中断触发MDMA启动,或直接使用硬件事件链接(如LPDMA的通道事件输出连接至MDMA的请求输入)。 # 二、硬件与软件准备 - 硬件:STM32H743开发板(或类似),外设如UART或ADC。 - 软件:STM32CubeIDE + HAL库(或LL库)。 - 本文以UART接收为例,演示LPDMA接收数据,MDMA搬运至大缓冲区。 # 三、配置步骤 ## 3.1 使能时钟与GPIO ```c __HAL_RCC_DMA1_CLK_ENABLE(); // LPDMA通常为DMA1 __HAL_RCC_MDMA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); // 配置UART引脚等(略) ``` ## 3.2 配置LPDMA(以DMA1为例) ```c DMA_HandleTypeDef hdma_uart; hdma_uart.Instance = DMA1_Stream0; hdma_uart.Init.Request = DMA_REQUEST_USART1_RX; hdma_uart.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_uart.Init.PeriphInc = DMA_PINC_DISABLE; hdma_uart.Init.MemInc = DMA_MINC_ENABLE; hdma_uart.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_uart.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_uart.Init.Mode = DMA_CIRCULAR; // 循环模式 hdma_uart.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_uart); __HAL_LINKDMA(&huart1, hdmarx, hdma_uart); HAL_UART_Receive_DMA(&huart1, intermediate_buf, BUF_SIZE); ``` ## 3.3 配置MDMA(链表模式) ```c MDMA_HandleTypeDef hmdma; MDMA_LinkNodeTypeDef node1, node2; // 初始化MDMA hmdma.Instance = MDMA_Channel0; hmdma.Init.Request = MDMA_REQUEST_DMA1_TC; // 由DMA1传输完成触发 hmdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER; hmdma.Init.Priority = MDMA_PRIORITY_HIGH; hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS; hmdma.Init.SourceInc = MDMA_SRC_INC_WORD; hmdma.Init.DestinationInc = 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 = BUF_SIZE/4; // 以字为单位 HAL_MDMA_Init(&hmdma); // 配置链表节点(双缓冲交替) node1.SourceAddress = (uint32_t)intermediate_buf; node1.DestinationAddress = (uint32_t)final_buf; node1.BlockDataLength = BUF_SIZE/4; node1.LinkAddress = (uint32_t)&node2; node2.SourceAddress = (uint32_t)intermediate_buf + BUF_SIZE/2; node2.DestinationAddress = (uint32_t)final_buf + BUF_SIZE/2; node2.BlockDataLength = BUF_SIZE/4; node2.LinkAddress = 0; // 结束 HAL_MDMA_Start_IT(&hmdma, (uint32_t)&node1, NULL, NULL); ``` ## 3.4 启动流水线 - 先启动MDMA,等待LPDMA事件。 - 再启动UART的DMA接收。 ```c HAL_MDMA_Start_IT(&hmdma, (uint32_t)&node1, NULL, NULL); HAL_UART_Receive_DMA(&huart1, intermediate_buf, BUF_SIZE); ``` # 四、完整代码示例(简化) ```c // main.c 片段 #define BUF_SIZE 1024 uint8_t intermediate_buf[BUF_SIZE]; uint8_t final_buf[BUF_SIZE]; void MDMA_Init(void) { // 如上配置,略 } void LPDMA_Init(void) { // 如上配置,略 } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); LPDMA_Init(); MDMA_Init(); // 启动流水线 HAL_MDMA_Start_IT(&hmdma, (uint32_t)&node1, NULL, NULL); HAL_UART_Receive_DMA(&huart1, intermediate_buf, BUF_SIZE); while (1) { // 主循环可处理其他任务,数据搬运由DMA完成 // 当MDMA完成中断触发时,可处理final_buf中的数据 } } void MDMA_IRQHandler(void) { HAL_MDMA_IRQHandler(&hmdma); } void DMA1_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_uart); } ``` # 五、注意事项 - **缓冲区对齐**:MDMA要求源和目标地址按字对齐(4字节),否则可能触发总线错误。建议使用`__attribute__((aligned(4)))`。 - **缓存一致性**:若目标RAM在D2域(如SRAM1),需注意CPU缓存与DMA的一致性。可使用`SCB_CleanDCache()`或配置MPU为非缓存区域。 - **中断优先级**:MDMA和LPDMA中断应设置为较高优先级,避免数据覆盖。 - **链表节点内存**:链表节点必须常驻内存,且不能被编译器优化掉,建议定义为全局变量。 - **调试技巧**:使用逻辑分析仪或串口打印MDMA完成标志,验证流水线时序。 # 六、总结 通过MDMA与LPDMA的级联,STM32H7实现了外设到RAM的零拷贝流水线,CPU仅需在最终数据就绪时处理业务逻辑。这种设计特别适合高速通信、音频处理等场景。掌握双DMA协同,是挖掘H7性能的关键一步。