问题现象与背景

在使用STM32 HAL库进行ADC多通道DMA采集时,许多开发者会遇到一个典型问题:DMA缓冲区中数据的顺序与预期通道顺序不一致。例如配置了通道0、1、2、3依次转换,但读取到的数组元素却对应了错误的通道,导致温度、电压等物理量解析错误。

该问题在STM32F1/F4/H7等系列中均可能出现,根源往往不在DMA本身,而在于ADC扫描模式、规则组排序与HAL库初始化顺序的配合。

原理讲解:ADC多通道扫描与DMA

STM32的ADC支持规则组多通道扫描。当使能扫描模式(Scan Conversion Mode)后,ADC会按照规则组序列(Rank)中配置的顺序依次转换各通道。每次转换完成产生EOC事件,DMA请求将结果搬运到内存。

关键点:

  • 规则组序列顺序由ADC_ChannelConfTypeDef.Rank决定,Rank值越小越先转换。
  • DMA传输顺序严格跟随ADC转换完成顺序,因此DMA缓冲区索引0对应Rank 1的通道,索引1对应Rank 2,依此类推。
  • HAL库配置陷阱:HAL_ADC_ConfigChannel()会覆盖之前设置的Rank,若多次调用顺序与Rank不匹配,或未正确设置Rank,则实际转换顺序与预期不符。
  • DMA循环模式下,缓冲区长度必须等于规则组通道数,否则会出现数据错位累积。

排查步骤

1. 检查规则组Rank配置

确认每个通道的Rank值唯一且连续(从1开始)。常见错误是全部使用默认Rank或重复Rank,导致ADC按通道号而非配置顺序转换。

2. 验证DMA缓冲区长度

DMA传输长度(hdma_adc.Init.Mode为循环时,BufferLength)必须等于规则组通道总数。若长度大于通道数,多余位置会保留旧值或触发溢出。

3. 检查HAL_ADC_ConfigChannel调用顺序

HAL库中每次调用HAL_ADC_ConfigChannel()都会写入该通道的Rank配置。若调用顺序与Rank值不一致,虽然最终Rank值正确,但部分系列(如F1)的寄存器写入可能受之前配置影响。建议按Rank从小到大依次配置。

4. 确认ADC校准与使能顺序

先执行HAL_ADCEx_Calibration_Start(),再启动DMA,最后HAL_ADC_Start_DMA()。顺序错误可能导致首次转换数据无效或错位。

5. 使用调试器观察ADC->DR与DMA目标地址

在DMA传输完成中断中检查缓冲区内容,同时单步观察ADC规则组序列寄存器(如ADC_SQR1/2/3),确认硬件实际转换顺序。

完整代码示例(以STM32F4为例)

#define ADC_CH_NUM 4
uint16_t adc_dma_buf[ADC_CH_NUM];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_DMA_Init(void)
{
    // 1. ADC时钟与GPIO初始化(略)
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_DMA2_CLK_ENABLE();

    // 2. DMA配置:循环模式,外设到内存,半字
    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);
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

    // 3. ADC基础配置
    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = ENABLE;          // 必须使能扫描
    hadc1.Init.ContinuousConvMode = ENABLE;    // 连续转换
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.NbrOfDiscConversion = 0;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = ADC_CH_NUM;   // 规则组通道数
    hadc1.Init.DMAContinuousRequests = ENABLE;
    hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&hadc1);

    // 4. 按Rank顺序配置通道(关键!)
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;

    sConfig.Channel = ADC_CHANNEL_0;  sConfig.Rank = 1; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_1;  sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_2;  sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_3;  sConfig.Rank = 4; HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // 5. 校准并启动
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_CH_NUM);
}

注意事项

  • Rank必须连续且从1开始:若跳号或重复,ADC行为未定义,HAL库不会报错。
  • DMA缓冲区长度严格等于NbrOfConversion:否则循环模式下数据会错位。
  • 避免在DMA运行中修改Rank:需先停止ADC和DMA,重新配置后再启动。
  • H7系列注意ADC时钟源与DMA请求映射:不同系列DMA请求通道不同,需查阅参考手册。
  • 使用HAL_ADC_Stop_DMA()后再重新配置:防止旧配置残留导致顺序异常。
  • 调试时优先检查ADC_SQRx寄存器:这是验证硬件实际转换顺序的最直接手段。

通过以上排查与修复,可确保ADC多通道DMA采集的数据顺序与配置完全一致,为后续数据处理提供可靠基础。