引言

在高速数据采集或通信系统中,外设数据频繁涌入内存,若每次中断都唤醒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

__HAL_RCC_DMA1_CLK_ENABLE();  // LPDMA通常为DMA1
__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
// 配置UART引脚等(略)

3.2 配置LPDMA(以DMA1为例)

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(链表模式)

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接收。
HAL_MDMA_Start_IT(&hmdma, (uint32_t)&node1, NULL, NULL);
HAL_UART_Receive_DMA(&huart1, intermediate_buf, BUF_SIZE);

四、完整代码示例(简化)

// 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性能的关键一步。