STM32 HAL库ADC多通道DMA采集通道顺序错乱的根因与修复

一、问题现象

使用STM32F4/G4系列,配置ADC1规则组扫描4个通道(IN0/IN1/IN4/IN5),开启DMA循环搬运,代码里这样写:

uint16_t adc_buf[4];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 4);

预期adc_buf[0..3]依次对应IN0、IN1、IN4、IN5。实际却出现:

  • 数组元素与通道错位,例如adc_buf[0]读到的是IN4的值;
  • 错位关系每次上电不同,或运行一段时间后“漂移”;
  • 单次转换(非循环)时顺序正确,改循环后出错。

这类问题在电机三相电流采样、多路NTC测温中非常致命,因为软件按固定下标做标定,错位直接导致控制环发散。

二、根因分析

2.1 规则组序列由SQR寄存器决定,不是“按配置顺序”

ADC规则组最多16个转换,顺序由SQR1/SQR2/SQR3中的SQ1..SQ16字段决定。CubeMX生成的MX_ADC1_Init()里通常有:

sConfig.Rank = ADC_REGULAR_RANK_1;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

关键点:HAL_ADC_ConfigChannel内部按Rank写入对应SQx位。如果你在代码里多次调用它但Rank都写成ADC_REGULAR_RANK_1,后一次会覆盖前一次,最终只有最后一个通道生效,或序列被压缩,DMA却仍按Length=4搬运,于是读到的是“残留/重复”数据,表现为顺序错乱。

2.2 DMA长度与序列长度不一致

HAL_ADC_Start_DMA(..., Length)中的Length是传输次数,必须等于规则组实际转换个数(NbrOfConversion)。若NbrOfConversion=4但DMA Length=3,缓冲区最后一个元素永远是旧值;反之Length偏大,DMA会多搬一次,把下一次序列的首通道混进来,形成“整体移位”。

2.3 循环模式下DMA与ADC启动时序

在DMA_CIRCULAR模式下,若先HAL_ADC_Start_DMA再使能ADC(ADEN),或中途调用HAL_ADC_Stop后未重新配置序列,DMA指针与ADC序列可能不同步。G4系列还需注意ADC_CFGR_DMNGT与ADC_CFGR_CONT的配合:连续转换+循环DMA时,DMA请求在每个转换结束产生,若序列长度与缓冲区不整除,会出现半包错位。

2.4 过采样/校准残留

开启过采样(Oversampling)后,每个通道输出被平均,但若Oversampling.Ratio与通道数配置不当,HAL会按“每通道多次”触发DMA,导致缓冲区里一个通道占多个位置,看起来像顺序错乱。

三、修复方案

3.1 方案A:CubeMX正确配置(推荐)

  1. ADC1 → Mode:Scan Conversion Mode = Enable,Continuous Conversion Mode = Enable,DMA Continuous Requests = Enable。
  2. Rank页签:依次添加IN0(Rank1)、IN1(Rank2)、IN4(Rank3)、IN5(Rank4),Number Of Conversion = 4。
  3. DMA Settings:添加ADC1,Mode=Circular,Data Width=Half Word,Number of data = 4(与转换数一致)。
  4. 生成代码后不要再手动改Rank。

3.2 方案B:手写配置(便于理解)

static void ADC1_MultiChannel_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    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.NbrOfConversion = 4;            // 与DMA长度一致
    hadc1.Init.DMAContinuousRequests = ENABLE;
    hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&hadc1);

    /* 关键:Rank必须递增且与缓冲区下标一一对应 */
    sConfig.Channel = ADC_CHANNEL_0;  sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_1;  sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_4;  sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Channel = ADC_CHANNEL_5;  sConfig.Rank = 4; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

启动与校验:

uint16_t adc_buf[4];

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 4);

/* 校验:注入已知电压到IN0,确认adc_buf[0]跟随变化 */
while (1) {
    if (HAL_ADC_GetState(&hadc1) & HAL_ADC_STATE_REG_EOC) {
        // adc_buf[0]=IN0, [1]=IN1, [2]=IN4, [3]=IN5
    }
}

3.3 方案C:用DMA半满/全满回调做双缓冲

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    // 前半区就绪
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    // 后半区就绪
}

双缓冲可避免CPU读取时DMA正在覆盖同一元素,进一步消除“偶发错位”。

四、注意事项

  • Rank必须从1连续递增,跳号会导致序列空洞,DMA长度对不上。
  • NbrOfConversion == DMA Length,这是最常见的错位来源,改一处必须同步改另一处。
  • 采样时间要覆盖信号源阻抗:高阻传感器用ADC_SAMPLETIME_480CYCLES,否则相邻通道串扰,表现为“数值像隔壁通道”。
  • 开启过采样时,DMA长度应设为通道数 × 过采样触发次数,或改用ADC_OVR_DATA_OVERRUN保护。
  • 调试时先关连续转换,用HAL_ADC_Start单次读,确认SQR寄存器(ADC1->SQR1/2/3)内容与预期一致,再开DMA。
  • F1系列无DMA循环+扫描的硬件限制较多,建议改用定时器触发+单次DMA。

五、小结

通道顺序错乱几乎都源于“序列定义”与“DMA搬运长度”不一致,或Rank被重复覆盖。牢记三句话:Rank连续、长度相等、时序对齐,即可在HAL库下稳定实现多通道ADC+DMA采集。