# STM32H7 使用 MDMA 与 DMA 双缓冲实现 ADC 连续采样零 CPU 干预的时序对齐技巧 ## 引言 在工业自动化、音频处理和高速数据采集等场景中,ADC 连续采样要求极高的吞吐量和低延迟。STM32H7 系列内置了强大的 MDMA(Master DMA)和标准 DMA 控制器,通过巧妙组合,可以实现 ADC 数据从外设到内存的全自动搬运,全程无需 CPU 干预。然而,多 DMA 协同工作时,时序对齐成为关键难点。本文将分享一种基于双缓冲的时序对齐方案,确保数据连续性和一致性。 ## 原理剖析 ### DMA 与 MDMA 的分工 - **DMA1/DMA2**:负责外设(如 ADC)到内存(SRAM)的数据传输,支持循环模式,可配置双缓冲。 - **MDMA**:作为高级 DMA,支持内存到内存、外设到内存等传输,具备链表模式和 64 位带宽,可处理大块数据搬运。 ### 双缓冲机制 双缓冲(Double Buffering)利用两个内存缓冲区交替接收数据。当 DMA 正在填充缓冲区 A 时,CPU 或 MDMA 可处理缓冲区 B 的数据,从而避免数据覆盖。在 STM32H7 中,DMA 的双缓冲模式通过 `DMA_SxCR` 寄存器的 `DBM` 位启用,并利用 `CT` 位指示当前使用的缓冲区。 ### 时序对齐的核心思想 ADC 连续采样产生固定速率的数据流。DMA 以循环模式将数据写入缓冲区,当缓冲区满时触发传输完成中断。此时,MDMA 被触发,将已满的缓冲区数据搬运至更大的存储区(如外部 SDRAM)或进行预处理。关键在于: - **缓冲区大小与采样率匹配**:确保 DMA 填充一个缓冲区的时间足够 MDMA 完成搬运,避免数据丢失。 - **中断优先级与触发时机**:利用 DMA 的传输完成中断(TC)精确触发 MDMA,保证数据在缓冲区切换瞬间被搬走。 ## 配置步骤 ### 1. 硬件资源 - STM32H743 开发板 - ADC1 通道(如 PA3) - 内部 SRAM(缓冲区)和外部 SDRAM(可选) ### 2. 初始化 ADC 配置 ADC 为连续转换模式,采样率设为 1MHz,数据宽度 16 位。使用 HAL 库简化配置: ```c ADC_HandleTypeDef hadc1; void ADC_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_16B; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); } ``` ### 3. 配置 DMA 双缓冲 使用 DMA2 Stream0,配置为循环模式,双缓冲,数据宽度半字(16 位)。定义两个缓冲区: ```c #define BUFFER_SIZE 1024 uint16_t bufferA[BUFFER_SIZE]; uint16_t bufferB[BUFFER_SIZE]; void DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc.Instance = DMA2_Stream0; hdma_adc.Init.Request = DMA_REQUEST_ADC1; hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc.Init.MemInc = DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode = DMA_CIRCULAR; hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; hdma_adc.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_adc); // 关联 DMA 到 ADC __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc); // 配置双缓冲,先使用 bufferA HAL_DMAEx_MultiBufferStart(&hdma_adc, (uint32_t)&ADC1->DR, (uint32_t)bufferA, (uint32_t)bufferB, BUFFER_SIZE); } ``` ### 4. 配置 MDMA MDMA 用于将已满的缓冲区搬运到目标区域(例如另一块内存或 SDRAM)。这里以搬运到 `destBuffer` 为例,并开启传输完成中断: ```c MDMA_HandleTypeDef hmdma; void MDMA_Init(void) { __HAL_RCC_MDMA_CLK_ENABLE(); hmdma.Instance = MDMA_Channel0; hmdma.Init.Request = MDMA_REQUEST_DMA2_Stream0_TC; 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_HALFWORD; hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD; hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE; hmdma.Init.BufferTransferLength = BUFFER_SIZE; HAL_MDMA_Init(&hmdma); // 配置 MDMA 目标地址,源地址在中断中动态设置 HAL_MDMA_Start(&hmdma, (uint32_t)bufferA, (uint32_t)destBuffer, BUFFER_SIZE); } ``` ### 5. 中断处理与时序对齐 关键在于 DMA 的传输完成中断中,根据当前使用的缓冲区(通过 `hdma_adc.Instance->CR & DMA_SxCR_CT` 判断)来设置 MDMA 的源地址,并启动 MDMA。同时,确保 MDMA 在下一个 DMA 缓冲区切换前完成搬运。 ```c void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_adc); } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 判断当前 DMA 使用的缓冲区 uint32_t currentBuffer = (hdma_adc.Instance->CR & DMA_SxCR_CT) ? (uint32_t)bufferB : (uint32_t)bufferA; // 设置 MDMA 源地址为当前已满的缓冲区 HAL_MDMA_Start(&hmdma, currentBuffer, (uint32_t)destBuffer, BUFFER_SIZE); // 可选:启动 MDMA 中断,以便处理搬运完成后的数据 } ``` 注意:`HAL_ADC_ConvCpltCallback` 在 DMA 传输完成一半或全部时调用(取决于 `DMA_IT_HT` 和 `DMA_IT_TC` 配置)。为了时序对齐,应使用传输完成中断(TC),并确保 MDMA 优先级高于 DMA,以便立即响应。 ## 完整代码示例 以下是一个简化但完整的初始化序列,整合了上述步骤: ```c int main(void) { HAL_Init(); SystemClock_Config(); ADC_Init(); DMA_Init(); MDMA_Init(); // 启动 ADC 并开启 DMA 传输 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)bufferA, BUFFER_SIZE); while (1) { // 主循环无需干预,数据自动搬运 } } ``` ## 注意事项 - **缓冲区大小与采样率匹配**:若采样率过高,MDMA 搬运时间可能超过 DMA 填充时间,导致数据覆盖。建议通过计算或实测调整缓冲区大小。 - **中断优先级**:MDMA 中断应设置为最高优先级,确保在 DMA 切换瞬间抢占处理。 - **内存对齐**:MDMA 要求源和目标地址按数据宽度对齐,建议使用 `__attribute__((aligned(4)))` 声明缓冲区。 - **调试技巧**:使用逻辑分析仪或调试器观察缓冲区切换时序,验证是否出现数据撕裂。 - **功耗考虑**:零 CPU 干预意味着 CPU 可进入低功耗模式,但需确保 MDMA 和 DMA 时钟保持开启。 ## 结语 通过 MDMA 与 DMA 双缓冲的巧妙配合,STM32H7 实现了 ADC 连续采样的零 CPU 干预,极大提升了系统吞吐量。本文的时序对齐技巧确保了数据完整性,为高速数据采集系统提供了可靠方案。开发者可根据实际需求扩展至多通道或更复杂的数据处理流程。