# STM32H7 使用 MDMA 与双缓冲实现 ADC 连续采样零 CPU 干预的配置陷阱 ## 引言 在高速数据采集系统中,ADC 连续采样往往要求 CPU 零干预,以释放算力给核心算法。STM32H7 系列凭借其强大的 MDMA(Master DMA)和双缓冲(Double Buffer)机制,成为实现这一目标的理想平台。然而,MDMA 的灵活性与复杂性并存,配置不当极易引发数据错乱、传输中断甚至系统死锁。本文将深入剖析配置中的关键陷阱,并提供一套经过验证的解决方案。 ## 原理基础 ### MDMA 与 DMA 的区别 STM32H7 拥有两种 DMA:传统 DMA(DMA1/DMA2)和 MDMA。MDMA 支持 128 位带宽、4KB 突发传输和链表模式,可访问所有存储器(包括 AXI SRAM),并能与 DMA 级联。在 ADC 连续采样场景中,我们通常使用 DMA1 将 ADC 数据搬运至 SRAM,再由 MDMA 将数据块搬移至最终缓冲区,实现两级流水线。 ### 双缓冲机制 双缓冲(Double Buffer)通过两个缓冲区交替使用,避免数据覆盖。在 MDMA 中,可通过配置 `MDMA_CTR` 寄存器的 `DBUF` 位启用双缓冲,并设置两个目标地址。当 MDMA 完成一次传输后,自动切换至另一缓冲区,同时触发中断通知 CPU 处理已满缓冲区。 ## 配置陷阱剖析 ### 陷阱一:MDMA 通道选择错误 MDMA 有 16 个通道,但并非所有通道都支持双缓冲。查阅参考手册,只有通道 0-7 支持 `DBUF` 功能。若使用通道 8-15,双缓冲配置将被忽略,导致数据覆盖。 **解决方案**:务必选择通道 0-7 之一,并在初始化时检查 `MDMA_CCR` 的 `DBUF` 位是否置位。 ### 陷阱二:缓冲区地址对齐要求 MDMA 要求源和目的地址必须按传输宽度对齐。若使用 32 位数据,地址需 4 字节对齐;若使用 128 位突发,需 16 字节对齐。未对齐时,MDMA 可能产生总线错误或数据错位。 **解决方案**:使用 `__attribute__((aligned(32)))` 声明缓冲区,或通过 `memalign` 动态分配。 ### 陷阱三:传输结束回调的触发条件 MDMA 的传输完成中断(TCIF)在每次块传输完成后触发,但若未正确配置 `TCIE` 和 `TEIE`,中断可能不产生。此外,双缓冲模式下,中断标志需在回调中手动清除,否则会持续触发。 **解决方案**:在中断服务函数中读取 `MDMA_CISR` 清除标志,并确保 `MDMA_CCR` 的 `TCIE` 置位。 ### 陷阱四:与 DMA 级联时的同步问题 当 DMA1 搬运 ADC 数据至中间缓冲区,MDMA 再搬运至最终缓冲区时,需确保 DMA1 完成一帧后 MDMA 才启动。若两者独立运行,可能产生数据竞争。 **解决方案**:使用 DMA1 的传输完成中断来触发 MDMA 启动,或利用 MDMA 的硬件请求信号(如 `MDMA_REQ`)与 DMA1 联动。 ## 配置步骤 1. **初始化 ADC**:配置 ADC 为连续扫描模式,使能 DMA 请求。 2. **配置 DMA1**:选择 ADC 的 DMA 通道,设置数据宽度为半字(16 位),循环模式,并指定中间缓冲区。 3. **配置 MDMA**:选择支持双缓冲的通道(如通道 0),设置源地址为中间缓冲区,目的地址为双缓冲区的两个地址,启用 `DBUF`,传输宽度为 32 位,突发大小为 4。 4. **使能中断**:在 MDMA 中断服务函数中处理数据并清除标志。 5. **启动流程**:先使能 MDMA,再使能 DMA1,最后启动 ADC。 ## 完整代码示例 以下代码基于 STM32H743,使用 HAL 库,实现 16 位 ADC 连续采样,双缓冲大小为 1024 字节。 ```c #include "stm32h7xx_hal.h" // 缓冲区定义,32 字节对齐 __attribute__((aligned(32))) uint32_t dma_buf[256]; // 中间缓冲区(DMA1 写入) __attribute__((aligned(32))) uint32_t mdma_buf[2][256]; // 双缓冲区(MDMA 写入) volatile uint8_t buf_index = 0; // 当前使用的缓冲区索引 // ADC 句柄和 DMA 句柄 ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; MDMA_HandleTypeDef hmdma_mdma; void MDMA_Init(void) { __HAL_RCC_MDMA_CLK_ENABLE(); hmdma_mdma.Instance = MDMA_Channel0; hmdma_mdma.Init.Request = MDMA_REQUEST_DMA1_ADC1; // 由 DMA1 触发 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_WORD; // 源地址递增,字 hmdma_mdma.Init.DestinationInc = MDMA_DEST_INC_WORD; // 目的地址递增 hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD; // 32 位 hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD; hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACK; hmdma_mdma.Init.BufferTransferLength = 256; // 每次传输 256 字 hmdma_mdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS; hmdma_mdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS; hmdma_mdma.Init.SourceBlockAddressOffset = 0; hmdma_mdma.Init.DestBlockAddressOffset = 0; hmdma_mdma.Init.DoubleBufferMode = MDMA_DOUBLEBUFFER_MODE_ENABLE; // 启用双缓冲 hmdma_mdma.Init.DoubleBufferBaseAddress = (uint32_t)mdma_buf; // 双缓冲基地址 hmdma_mdma.Init.DoubleBufferCurrentBuffer = 0; // 初始使用缓冲区 0 HAL_MDMA_Init(&hmdma_mdma); // 配置中断 HAL_NVIC_SetPriority(MDMA_IRQn, 0, 0); HAL_NVIC_EnableIRQ(MDMA_IRQn); } void MDMA_Start(void) { // 源地址为 DMA1 的中间缓冲区,目的地址为双缓冲基地址 HAL_MDMA_Start(&hmdma_mdma, (uint32_t)dma_buf, (uint32_t)mdma_buf, 256, 1); } void MDMA_IRQHandler(void) { HAL_MDMA_IRQHandler(&hmdma_mdma); } void HAL_MDMA_TCIncCallback(MDMA_HandleTypeDef *hmdma) { // 传输完成,处理当前缓冲区数据 buf_index = hmdma->Init.DoubleBufferCurrentBuffer ^ 1; // 获取已满的缓冲区索引 // 此处可处理 mdma_buf[buf_index] 中的数据 // 注意:需在回调中清除中断标志,HAL 库已自动清除,但若使用非 HAL 需手动 } // 主函数中初始化并启动 int main(void) { HAL_Init(); // 配置时钟、ADC、DMA1 等(略) MDMA_Init(); MDMA_Start(); // 启动 ADC 和 DMA1(略) while (1) { // 主循环无需干预数据搬运 } } ``` ## 注意事项 - **缓冲区大小**:MDMA 的 `BufferTransferLength` 必须与 DMA1 的传输长度匹配,否则可能丢失数据或产生溢出。 - **中断优先级**:MDMA 中断优先级应高于其他非关键中断,但低于实时性要求更高的中断(如系统节拍)。 - **调试技巧**:若数据错乱,先检查地址对齐和 `DBUF` 位;若中断不触发,检查 `TCIE` 和 `TEIE` 是否使能。 - **功耗考虑**:零 CPU 干预虽好,但 MDMA 和 DMA 的持续运行会增加功耗,低功耗场景需权衡。 ## 结语 STM32H7 的 MDMA 与双缓冲组合为 ADC 连续采样提供了强大的零 CPU 干预方案,但配置陷阱众多。通过理解原理、遵循正确的配置步骤,并注意地址对齐、通道选择等细节,即可稳定实现高速数据采集。希望本文能助你在嵌入式开发中少走弯路。