## 为什么需要双缓冲 DMA? 在 ADC 连续采样、UART 高速接收或 SPI 从机接收等场景中,数据以固定速率持续产生。传统单缓冲 DMA 在缓冲区满后必须触发中断,由 CPU 将数据搬移到用户区,期间 DMA 可能因缓冲区未及时清空而停止接收,导致数据丢失。双缓冲 DMA 则利用两个缓冲区(Memory 0 和 Memory 1)交替工作:当 DMA 正在填充缓冲区 A 时,CPU 可以安全地处理缓冲区 B 的数据,反之亦然。硬件自动切换指针,无需 CPU 干预,从而实现零拷贝、无间断的数据流。 ## 双缓冲 DMA 的工作原理 STM32 的 DMA 控制器(如 DMA1/DMA2)支持双缓冲模式,其核心机制如下: - **两个内存缓冲区**:分别由 `DMA_M0AR` 和 `DMA_M1AR` 寄存器指向,大小可不同,但通常设为相同。 - **硬件自动切换**:当 DMA 传输完当前缓冲区(例如 M0)后,硬件自动将内存地址切换到 M1,并继续传输,同时置位传输完成中断标志(TCIF)。 - **CPU 并行处理**:CPU 在中断服务函数中读取当前未使用的缓冲区数据,而 DMA 正在填充另一个缓冲区,两者互不干扰。 - **循环模式支持**:双缓冲通常与循环模式(Circular Mode)结合,实现无限循环传输,适合连续数据流。 关键点:在双缓冲模式下,DMA 的 `NDTR`(传输数量)寄存器会在每次切换后自动重载,无需软件重设。 ## 配置步骤(基于 STM32CubeMX + HAL 库) 以下以 STM32F407 的 ADC1 连续采样为例,演示双缓冲 DMA 的配置。 ### 1. CubeMX 配置 - 开启 ADC1,选择扫描模式(若多通道)和连续转换模式。 - 在 DMA Settings 选项卡中添加 DMA 请求,选择 `ADC1`,模式设为 `Circular`,数据宽度设为 `Half Word`(16 位)。 - 注意:HAL 库中双缓冲模式需要手动启用,CubeMX 不直接提供选项,需在代码中修改。 ### 2. 内存缓冲区定义 在全局定义两个缓冲区,注意对齐到 32 字节(部分 DMA 要求): ```c #define BUFFER_SIZE 1024 __ALIGN_BEGIN uint16_t adc_buffer0[BUFFER_SIZE] __ALIGN_END; __ALIGN_BEGIN uint16_t adc_buffer1[BUFFER_SIZE] __ALIGN_END; ``` ### 3. 启用双缓冲模式 在初始化代码中,调用 `HAL_ADC_Start_DMA()` 后,需要手动设置 DMA 的双缓冲寄存器。推荐使用 `HAL_DMAEx_MultiBufferStart()` 函数(针对支持多缓冲的 DMA),但更通用的方法是直接操作寄存器: ```c DMA_HandleTypeDef hdma_adc; // 假设已初始化 // 启用双缓冲模式 hdma_adc.Init.Mode = DMA_CIRCULAR; // 必须为循环模式 hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; // ... 其他配置 // 设置双缓冲相关寄存器(以 DMA2 Stream0 为例) hdma_adc.Instance->M0AR = (uint32_t)adc_buffer0; hdma_adc.Instance->M1AR = (uint32_t)adc_buffer1; hdma_adc.Instance->CR |= DMA_SxCR_DBM; // 启用双缓冲模式 ``` 更简洁的方式是使用 HAL 库的扩展函数(如果芯片支持): ```c HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart_IT(&hdma_adc, (uint32_t)&ADC1->DR, (uint32_t)adc_buffer0, (uint32_t)adc_buffer1, BUFFER_SIZE); ``` 注意:`HAL_DMAEx_MultiBufferStart_IT` 会自动配置双缓冲和中断,但需要确保 DMA 初始化时模式为循环。 ### 4. 中断处理 在 DMA 中断回调中,判断当前正在使用的缓冲区,并处理另一个缓冲区数据。HAL 库提供了 `HAL_ADC_ConvCpltCallback`,但双缓冲模式下需要区分哪个缓冲区完成。可通过检查 DMA 的 `CR` 寄存器的 `CT` 位(当前目标内存索引)来判断: ```c void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { uint32_t current_buffer_index = (hdma_adc.Instance->CR & DMA_SxCR_CT) ? 1 : 0; uint16_t* current_buffer = current_buffer_index ? adc_buffer1 : adc_buffer0; uint16_t* processed_buffer = current_buffer_index ? adc_buffer0 : adc_buffer1; // 处理 processed_buffer 中的数据(例如计算平均值、存储等) process_data(processed_buffer, BUFFER_SIZE); } ``` 注意:`CT` 位为 0 表示当前使用 M0AR,为 1 表示使用 M1AR。因此,当 DMA 完成 M0 时,CT 变为 1(切换到 M1),此时应处理 M0 的数据。 ## 完整代码示例 以下是一个完整的 ADC 双缓冲 DMA 示例(基于 STM32F4 HAL 库): ```c /* Includes */ #include "main.h" /* 缓冲区定义 */ #define BUFFER_SIZE 1024 __ALIGN_BEGIN uint16_t adc_buffer0[BUFFER_SIZE] __ALIGN_END; __ALIGN_BEGIN uint16_t adc_buffer1[BUFFER_SIZE] __ALIGN_END; /* 全局句柄 */ ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; /* 初始化函数(CubeMX 生成) */ static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; // 单通道 hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; HAL_ADC_Init(&hadc1); } /* DMA 初始化 */ static void MX_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 必须循环 hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc1); // 关联 DMA 到 ADC __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); } /* 启动双缓冲 DMA */ void Start_ADC_DoubleBuffer(void) { // 使用 HAL 扩展函数启动双缓冲 HAL_StatusTypeDef status = HAL_DMAEx_MultiBufferStart_IT(&hdma_adc1, (uint32_t)&ADC1->DR, (uint32_t)adc_buffer0, (uint32_t)adc_buffer1, BUFFER_SIZE); if (status != HAL_OK) { Error_Handler(); } HAL_ADC_Start(&hadc1); } /* 中断回调:处理已满缓冲区 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { uint32_t ct = (hdma_adc1.Instance->CR & DMA_SxCR_CT) ? 1 : 0; uint16_t* processed = ct ? adc_buffer0 : adc_buffer1; // 注意:CT 表示当前使用的缓冲区,所以处理另一个 // 示例:计算平均值 uint32_t sum = 0; for (uint16_t i = 0; i < BUFFER_SIZE; i++) { sum += processed[i]; } float avg = (float)sum / BUFFER_SIZE; // 此处可添加数据处理代码 } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); Start_ADC_DoubleBuffer(); while (1) { // 主循环可执行其他任务,无需干预数据流 } } ``` ## 注意事项与常见陷阱 - **缓冲区大小与对齐**:DMA 要求缓冲区地址按数据宽度对齐(如 16 位数据需 2 字节对齐),建议使用 `__ALIGN_BEGIN` 宏进行 32 字节对齐,以满足部分 DMA 的缓存一致性要求(如带 D-Cache 的芯片)。 - **中断优先级**:双缓冲依赖中断及时处理数据,若中断优先级过低,可能导致缓冲区溢出。建议将 DMA 中断优先级设为最高(如 `NVIC_PRIORITY_GROUP_4` 中的 0)。 - **模式必须为循环**:双缓冲模式仅支持循环模式(`DMA_CIRCULAR`),否则传输一次后停止。 - **区分 `HAL_ADC_ConvCpltCallback` 与 `HAL_DMA_TC_Callback`**:在双缓冲中,`HAL_ADC_ConvCpltCallback` 会在每次缓冲区切换时调用,但需注意 `HAL_DMA_TC_Callback` 可能被多次触发,建议使用 ADC 回调。 - **内存屏障**:在带缓存(如 Cortex-M7)的 MCU 上,处理缓冲区数据前需调用 `SCB_InvalidateDCache_by_Addr` 使缓存失效,避免读到陈旧数据。 - **缓冲区大小可变**:双缓冲允许两个缓冲区大小不同,但切换时 `NDTR` 会重载为各自大小,需确保逻辑正确。 ## 总结 双缓冲 DMA 是 STM32 高性能数据传输的利器,通过硬件自动切换缓冲区,彻底消除了 CPU 搬运数据的瓶颈。本文从原理到实践,给出了完整的配置和代码示例。在实际项目中,开发者应根据外设特性(如 ADC 采样率、UART 波特率)合理选择缓冲区大小,并注意中断优先级和缓存一致性,即可实现高效、可靠的数据采集与传输。