为什么需要双缓冲 DMA?

在 ADC 连续采样、UART 高速接收或 SPI 从机接收等场景中,数据以固定速率持续产生。传统单缓冲 DMA 在缓冲区满后必须触发中断,由 CPU 将数据搬移到用户区,期间 DMA 可能因缓冲区未及时清空而停止接收,导致数据丢失。双缓冲 DMA 则利用两个缓冲区(Memory 0 和 Memory 1)交替工作:当 DMA 正在填充缓冲区 A 时,CPU 可以安全地处理缓冲区 B 的数据,反之亦然。硬件自动切换指针,无需 CPU 干预,从而实现零拷贝、无间断的数据流。

双缓冲 DMA 的工作原理

STM32 的 DMA 控制器(如 DMA1/DMA2)支持双缓冲模式,其核心机制如下:

  • 两个内存缓冲区:分别由 DMA_M0ARDMA_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 要求):

#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),但更通用的方法是直接操作寄存器:

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 库的扩展函数(如果芯片支持):

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 位(当前目标内存索引)来判断:

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 库):

/* 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_ConvCpltCallbackHAL_DMA_TC_Callback:在双缓冲中,HAL_ADC_ConvCpltCallback 会在每次缓冲区切换时调用,但需注意 HAL_DMA_TC_Callback 可能被多次触发,建议使用 ADC 回调。
  • 内存屏障:在带缓存(如 Cortex-M7)的 MCU 上,处理缓冲区数据前需调用 SCB_InvalidateDCache_by_Addr 使缓存失效,避免读到陈旧数据。
  • 缓冲区大小可变:双缓冲允许两个缓冲区大小不同,但切换时 NDTR 会重载为各自大小,需确保逻辑正确。

总结

双缓冲 DMA 是 STM32 高性能数据传输的利器,通过硬件自动切换缓冲区,彻底消除了 CPU 搬运数据的瓶颈。本文从原理到实践,给出了完整的配置和代码示例。在实际项目中,开发者应根据外设特性(如 ADC 采样率、UART 波特率)合理选择缓冲区大小,并注意中断优先级和缓存一致性,即可实现高效、可靠的数据采集与传输。