STM32 DMA多路ADC采样防错位指南:乒乓缓冲的精确切换与实现

引言

在嵌入式系统中,多通道ADC采样是常见的需求,例如采集多路传感器信号。使用DMA(直接内存访问)可以大幅减轻CPU负担,实现高效的数据传输。然而,当DMA与多路ADC结合时,数据错位问题频发,尤其是在使用乒乓缓冲(双缓冲)进行连续采样时。本文将深入探讨错位产生的根本原因,并提供一套完整的乒乓缓冲精确切换方案,确保数据始终与通道对应。

数据错位的根源

1. DMA传输的连续性与通道顺序

STM32的ADC在扫描模式下,会按照配置的通道顺序(从低到高)依次转换。DMA在每次转换完成后触发传输,将结果存入内存。如果DMA配置为连续模式(Circular),它会持续填充缓冲区。问题在于:DMA传输与ADC转换是异步的,但通常DMA在ADC转换完成中断中触发,这保证了顺序性。然而,当使用乒乓缓冲时,若切换时机不当,可能导致DMA写入新缓冲区时,ADC仍在转换上一轮数据,从而产生数据错位。

2. 缓冲切换的竞态条件

乒乓缓冲的核心是:当DMA填满一个缓冲区时,切换到另一个缓冲区,同时处理已填满的缓冲区。若切换操作(如修改DMA目标地址)发生在DMA传输过程中,可能造成数据写入地址混乱,导致错位。此外,若CPU读取数据与DMA写入同时进行,也会引发数据不一致。

乒乓缓冲的精确切换方案

方案概述

采用双缓冲(乒乓)机制,配合DMA传输完成中断,实现无缝切换。具体步骤:

  1. 配置两个缓冲区(Buffer A和Buffer B),大小等于ADC通道数 × 采样次数。
  2. 使用DMA的循环模式,但通过中断手动切换目标地址。
  3. 在DMA传输完成中断中,切换DMA的存储器地址(M0AR或M1AR),并处理已满缓冲区。

硬件配置(以STM32F4为例)

  • ADC1:扫描模式,4个通道(例如CH0-CH3),连续转换。
  • DMA2 Stream0:通道0(ADC1),数据宽度半字(16位),内存地址递增,外设地址固定。
  • 双缓冲区:使用DMA的双缓冲模式(DBM),硬件自动切换,但需配合中断。

代码实现

1. 初始化ADC和DMA

// 缓冲区定义
#define ADC_CH_NUM 4
#define SAMPLE_NUM 100
uint16_t adc_buf[2][ADC_CH_NUM * SAMPLE_NUM];

// DMA初始化
void DMA_Config(void) {
    DMA_InitTypeDef DMA_InitStructure;
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_DeInit(DMA2_Stream0);
    DMA_InitStructure.DMA_Channel = DMA_Channel_0;  // ADC1
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)adc_buf[0];
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = ADC_CH_NUM * SAMPLE_NUM;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_Init(DMA2_Stream0, &DMA_InitStructure);

    // 使能双缓冲模式(DBM)
    DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)adc_buf[1], DMA_Memory_0);
    DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE);

    // 使能传输完成中断
    DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE);

    // 配置NVIC
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    DMA_Cmd(DMA2_Stream0, ENABLE);
}

// ADC初始化
void ADC_Config(void) {
    ADC_InitTypeDef ADC_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

    // 配置PA0-PA3为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;  // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = ADC_CH_NUM;
    ADC_Init(ADC1, &ADC_InitStructure);

    // 配置通道顺序
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_84Cycles);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_84Cycles);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_84Cycles);

    ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);  // 最后一次传输后产生DMA请求
    ADC_DMACmd(ADC1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);
}

2. DMA中断处理与乒乓切换

volatile uint8_t current_buf = 0;  // 当前使用缓冲区索引
volatile uint8_t buf_ready = 0;    // 缓冲区就绪标志

void DMA2_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TC)) {
        DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TC);

        // 切换缓冲区:DMA硬件自动切换,但我们需要记录当前缓冲区
        current_buf = DMA_GetCurrentMemoryTarget(DMA2_Stream0);  // 0或1
        // 注意:current_buf是DMA正在使用的缓冲区,另一个缓冲区已填满
        uint8_t full_buf = current_buf ^ 1;  // 已填满的缓冲区

        // 处理已填满的数据(例如,拷贝或处理)
        ProcessADCData(adc_buf[full_buf]);

        // 设置标志,表示有数据可处理(如果不在中断中处理)
        buf_ready = 1;
    }
}

// 数据处理函数(示例)
void ProcessADCData(uint16_t *data) {
    // 数据顺序:CH0, CH1, CH2, CH3, CH0, CH1, ...
    // 注意:由于是循环模式,数据是连续的,但每次中断时,缓冲区填满一次
    // 可以按通道提取数据,例如计算平均值
    uint32_t sum[ADC_CH_NUM] = {0};
    for (int i = 0; i < SAMPLE_NUM; i++) {
        for (int ch = 0; ch < ADC_CH_NUM; ch++) {
            sum[ch] += data[i * ADC_CH_NUM + ch];
        }
    }
    // 计算平均值并存储
    for (int ch = 0; ch < ADC_CH_NUM; ch++) {
        avg_values[ch] = sum[ch] / SAMPLE_NUM;
    }
}

关键点解析

  • 双缓冲模式(DBM):STM32的DMA支持硬件双缓冲,通过DMA_DoubleBufferModeConfig配置两个内存地址。当DMA传输完一个缓冲区后,自动切换到另一个,无需软件干预,从而避免了切换时的竞态条件。
  • 中断时机:DMA传输完成中断在缓冲区切换后触发,此时DMA_GetCurrentMemoryTarget返回的是新缓冲区索引,而另一个缓冲区是满的,可以安全处理。
  • 数据对齐:由于ADC扫描模式按顺序转换,DMA按顺序存储,只要缓冲区大小是通道数的整数倍,数据就不会错位。但需注意,如果采样次数不是整数倍,则可能产生错位,因此必须保证DMA_BufferSize为通道数的倍数。

注意事项

  1. 缓冲区大小:必须为通道数的整数倍,否则数据错位。例如,4通道,每次采样100组,则缓冲区大小为400。
  2. 中断优先级:DMA中断应设置为较高优先级,避免被其他中断延迟,导致数据覆盖。
  3. 数据一致性:在中断中处理数据时,确保CPU访问与DMA写入不冲突。使用双缓冲后,处理满缓冲区时,DMA正在写另一个缓冲区,因此安全。
  4. ADC校准:在初始化后,建议执行ADC校准(ADC_GetCalibrationFactor),以提高精度。
  5. DMA模式:使用循环模式(Circular)时,双缓冲自动切换,但需注意,循环模式会持续运行,若不需要连续采样,可在处理完数据后停止DMA。
  6. 内存对齐:对于某些STM32型号,DMA要求内存地址对齐(如4字节对齐),建议将缓冲区定义为__align(4)或使用uint32_t数组。

总结

通过使用STM32的DMA双缓冲模式,可以高效地实现多路ADC连续采样,同时避免数据错位。关键在于理解DMA的传输机制和双缓冲的切换逻辑,确保缓冲区大小与通道数匹配,并在中断中正确处理数据。本文提供的代码示例可直接应用于实际项目,为嵌入式数据采集系统提供稳定可靠的解决方案。