# STM32H7 零CPU干预:MDMA与DMA双缓冲协同实现ADC连续采样的时序设计 ## 引言 在工业自动化、音频处理或振动监测等场景,ADC需要以MHz级速率连续采样,传统中断方式会频繁打断CPU,导致系统响应迟缓。STM32H7系列凭借其强大的MDMA(Master DMA)和灵活的双缓冲DMA机制,可构建一条从ADC外设到内存的“高速公路”,实现数据采集零CPU干预。本文将带你一步步设计并实现这一方案。 ## 原理剖析 ### 1. DMA双缓冲(Double Buffer) DMA双缓冲模式(Double Buffer Mode)允许DMA在两个内存缓冲区(Buffer A和Buffer B)之间交替传输数据。当DMA正在填充Buffer A时,CPU或MDMA可以安全处理Buffer B中的数据,反之亦然。这种乒乓机制避免了数据覆盖和访问冲突,是连续采样的基石。 在STM32H7中,DMA1/2支持双缓冲,但每个DMA通道仅能管理一个外设请求。对于ADC多通道或高速率,单DMA可能力不从心,此时MDMA登场。 ### 2. MDMA(Master DMA) MDMA是STM32H7新增的高性能DMA控制器,支持存储器到存储器(M2M)传输,可配置为链接模式(Linked List)。它能在DMA完成一个缓冲区填充后,自动将数据搬运到另一块更大的内存区域(如SRAM或SDRAM),而无需CPU介入。MDMA的突发传输和16/32/64位数据宽度,使其非常适合大数据块搬运。 ### 3. 协同工作流程 - **阶段1**:ADC连续采样,触发DMA1通道,数据写入Buffer A(双缓冲)。 - **阶段2**:当Buffer A填满,DMA1产生传输完成中断(但可关闭以省CPU),同时自动切换到Buffer B继续接收。 - **阶段3**:MDMA检测到DMA1的完成事件(通过硬件信号或中断),启动M2M传输,将Buffer A的数据搬运至大容量存储区(如SRAM1或外部SDRAM)。 - **阶段4**:MDMA传输完成后,可触发中断通知CPU处理(可选),或继续等待下一轮。 通过合理配置,CPU仅在需要处理数据时被唤醒,而采样和搬运全程自动进行。 ## 硬件与软件准备 - **硬件**:STM32H743 Nucleo或自定义板,ADC输入信号源。 - **软件**:STM32CubeIDE(或Keil),HAL库。 - **关键外设**:ADC1(12位,采样率1MHz),DMA1(双缓冲),MDMA(M2M)。 ## 配置步骤 ### 1. 配置ADC 使用CubeMX或直接寄存器配置: - 开启ADC1,选择扫描模式(多通道)或单通道。 - 设置采样时间,确保采样率符合需求。 - 使能DMA连续请求(DMA Continuous Requests)。 ### 2. 配置DMA1双缓冲 - 选择DMA1通道,关联ADC1。 - 设置数据宽度(如半字),方向为外设到内存。 - 启用双缓冲模式,指定两个缓冲区地址(需对齐)。 - 关闭DMA中断(或仅开启错误中断),避免CPU负担。 ### 3. 配置MDMA - 选择MDMA通道,模式设为M2M。 - 设置源地址为DMA缓冲区(动态切换),目的地址为大存储区。 - 配置传输大小(与DMA缓冲区大小一致)。 - 启用链接模式,并设置链接列表(可选,用于多缓冲循环)。 - 触发方式:选择硬件触发(如DMA1完成信号)或软件触发。 ### 4. 编写初始化代码 以下为关键代码片段(基于HAL库): ```c // 缓冲区定义(需对齐到32字节) #define BUFFER_SIZE 1024 uint16_t dma_buffer_a[BUFFER_SIZE] __attribute__((aligned(32))); uint16_t dma_buffer_b[BUFFER_SIZE] __attribute__((aligned(32))); uint16_t mdma_dest[2 * BUFFER_SIZE] __attribute__((aligned(32))); // ADC初始化 void ADC_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DMAContinuousRequests = ENABLE; HAL_ADC_Init(&hadc1); } // DMA双缓冲配置 void DMA_Init(void) { hdma_dac.Instance = DMA1_Stream1; hdma_dac.Init.Request = DMA_REQUEST_ADC1; hdma_dac.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_dac.Init.Mode = DMA_CIRCULAR; // 循环模式配合双缓冲 hdma_dac.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_dac); // 配置双缓冲 HAL_DMAEx_MultiBufferStart(&hdma_dac, (uint32_t)&hadc1.Instance->DR, (uint32_t)dma_buffer_a, (uint32_t)dma_buffer_b, BUFFER_SIZE); // 注意:此处使用MultiBufferStart,但需确保DMA模式为循环 } // MDMA配置 void MDMA_Init(void) { hmdma_mdma.Instance = MDMA_Channel0; hmdma_mdma.Init.Request = MDMA_REQUEST_SW; // 软件触发,或硬件触发 hmdma_mdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER; // 块传输 hmdma_mdma.Init.Priority = MDMA_PRIORITY_HIGH; hmdma_mdma.Init.Endianness = MDMA_LITTLE_ENDIAN; hmdma_mdma.Init.SourceInc = MDMA_SRC_INC_HALFWORD; hmdma_mdma.Init.DestInc = MDMA_DEST_INC_HALFWORD; hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD; hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD; hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACKEN; hmdma_mdma.Init.BufferTransferLength = BUFFER_SIZE; HAL_MDMA_Init(&hmdma_mdma); // 设置源地址为dma_buffer_a,目的为mdma_dest HAL_MDMA_Start(&hmdma_mdma, (uint32_t)dma_buffer_a, (uint32_t)mdma_dest, BUFFER_SIZE); } // 启动采集 void Start_Acquisition(void) { HAL_ADC_Start_DMA(&hadc1, (uint32_t*)dma_buffer_a, BUFFER_SIZE); // 启动ADC+DMA // 注意:HAL_ADC_Start_DMA内部会配置DMA,但双缓冲需额外配置 } ``` **注意**:上述代码中,HAL_ADC_Start_DMA通常用于单缓冲,双缓冲需使用HAL_DMAEx_MultiBufferStart并确保ADC的DMA请求已使能。实际应用中,建议直接使用寄存器操作或参考HAL库的DMA双缓冲示例。 ### 5. 时序设计要点 - **缓冲区大小**:需保证DMA填充一个缓冲区的时间大于MDMA搬运该缓冲区的时间,否则数据会丢失。例如,ADC采样率1MHz,每个采样2字节,缓冲区1024字节,填充时间约1ms;MDMA搬运1024字节在STM32H7上约需几十微秒,远小于1ms,故安全。 - **触发同步**:MDMA的触发可配置为DMA1的传输完成事件(通过硬件互连),但STM32H7的MDMA触发源有限,通常使用软件触发或定时器触发。更可靠的方法是:在DMA双缓冲切换时,利用DMA的传输完成中断(即使关闭,也可通过状态标志查询)来启动MDMA,但这样会引入少量CPU开销。为实现完全零干预,可配置MDMA的链接模式,使其自动循环搬运两个缓冲区。 **链接模式示例**: ```c // 定义链接列表项 MDMA_LinkNodeTypeDef link_node1, link_node2; // 初始化节点1:搬运dma_buffer_a到mdma_dest link_node1.SourceAddress = (uint32_t)dma_buffer_a; link_node1.DestAddress = (uint32_t)mdma_dest; link_node1.BufferLength = BUFFER_SIZE; link_node1.NextNode = &link_node2; // 初始化节点2:搬运dma_buffer_b到mdma_dest+BUFFER_SIZE link_node2.SourceAddress = (uint32_t)dma_buffer_b; link_node2.DestAddress = (uint32_t)(mdma_dest + BUFFER_SIZE); link_node2.BufferLength = BUFFER_SIZE; link_node2.NextNode = &link_node1; // 循环 // 配置MDMA使用链接模式 HAL_MDMA_Start_IT(&hmdma_mdma, (uint32_t)&link_node1, NULL, 0); // 启动链接列表 ``` 这样,MDMA会在DMA切换缓冲区时自动搬运对应数据,无需CPU干预。 ## 完整代码示例 以下为整合后的初始化与启动代码(简化版): ```c // 全局句柄 ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_dac; MDMA_HandleTypeDef hmdma_mdma; void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.DMAContinuousRequests = ENABLE; HAL_ADC_Init(&hadc1); } void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_dac.Instance = DMA1_Stream1; hdma_dac.Init.Request = DMA_REQUEST_ADC1; hdma_dac.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_dac.Init.PeriphInc = DMA_PINC_DISABLE; hdma_dac.Init.MemInc = DMA_MINC_ENABLE; hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_dac.Init.Mode = DMA_CIRCULAR; hdma_dac.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_dac); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_dac); } void MX_MDMA_Init(void) { __HAL_RCC_MDMA_CLK_ENABLE(); hmdma_mdma.Instance = MDMA_Channel0; hmdma_mdma.Init.Request = MDMA_REQUEST_SW; hmdma_mdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER; hmdma_mdma.Init.Priority = MDMA_PRIORITY_HIGH; hmdma_mdma.Init.Endianness = MDMA_LITTLE_ENDIAN; hmdma_mdma.Init.SourceInc = MDMA_SRC_INC_HALFWORD; hmdma_mdma.Init.DestInc = MDMA_DEST_INC_HALFWORD; hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD; hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD; hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACKEN; hmdma_mdma.Init.BufferTransferLength = BUFFER_SIZE; HAL_MDMA_Init(&hmdma_mdma); } void Start_ZeroCPU_ADC(void) { // 配置双缓冲 HAL_DMAEx_MultiBufferStart(&hdma_dac, (uint32_t)&hadc1.Instance->DR, (uint32_t)dma_buffer_a, (uint32_t)dma_buffer_b, BUFFER_SIZE); // 启动MDMA链接模式(假设已配置好链接节点) HAL_MDMA_Start_IT(&hmdma_mdma, (uint32_t)&link_node1, NULL, 0); // 启动ADC HAL_ADC_Start_DMA(&hadc1, (uint32_t*)dma_buffer_a, BUFFER_SIZE); } ``` ## 注意事项 - **内存对齐**:DMA和MDMA要求缓冲区地址对齐到数据宽度(半字对齐,建议32字节对齐以优化性能)。 - **缓存一致性**:如果使用带缓存的内存区域(如SRAM),需在CPU访问前调用`SCB_InvalidateDCache`,或在MDMA传输完成后使缓存无效。 - **时序预算**:务必计算DMA填充时间与MDMA搬运时间,确保前者大于后者,否则数据覆盖。可调整缓冲区大小或MDMA突发长度。 - **中断处理**:虽然零CPU干预,但建议保留MDMA完成中断用于数据就绪通知,以便CPU及时处理。 - **链接列表内存**:链接节点必须常驻内存,且不可被优化掉。 - **调试技巧**:使用逻辑分析仪或DMA的传输计数寄存器验证时序。 ## 结语 通过STM32H7的MDMA与DMA双缓冲协同,我们成功实现了ADC连续采样的零CPU干预,释放了CPU资源用于更复杂的处理。该方案可扩展至多通道、多ADC同步等场景,是高性能嵌入式系统的利器。希望本文能助你在项目中游刃有余。