问题现象与背景

在使用STM32 HAL库配置ADC多通道DMA扫描采集时,很多开发者会遇到这样的现象:明明按顺序配置了通道0、1、2、3,但DMA缓冲区中读到的数据却像是“乱序”的,例如通道0的数据出现在索引2的位置,或者通道3的数据出现在索引0的位置。这种错乱并非随机,而是与ADC扫描序列的配置方式、DMA传输对齐以及HAL库内部处理逻辑密切相关。

本文以STM32F4系列为例(其他系列原理类似),从ADC扫描序列原理出发,逐步排查错乱原因,并给出两种实用的重排方法:软件重排与配置修正。

原理讲解:ADC扫描序列与DMA传输

ADC规则组扫描序列

STM32的ADC支持规则组(Regular Group)多通道扫描。每个规则组最多可配置16个转换(取决于系列),每个转换对应一个通道号,并存储在SQRx寄存器中。转换顺序由SQR1、SQR2、SQR3中的SQ1[4:0]到SQ16[4:0]决定,其中SQ1最先转换。

关键点:ADC转换顺序由SQR寄存器中的SQx序列决定,而非通道编号顺序。HAL库的HAL_ADC_ConfigChannel()函数会根据传入的Rank参数将通道写入对应的SQx位置。如果Rank设置错误,转换顺序就会与预期不符。

DMA传输机制

ADC规则组转换完成后,结果存储在DR寄存器中。当使能DMA时,每次转换完成都会触发一次DMA请求,将DR中的值搬运到内存缓冲区。由于DMA按请求顺序搬运,因此缓冲区中的数据顺序严格对应ADC的转换顺序。

HAL库配置陷阱

HAL库中常见的配置错误包括:

  • Rank参数重复或跳跃:多个通道使用相同的Rank,或Rank不连续,导致SQR寄存器写入异常。
  • 未正确设置NbrOfConversion:ADC_ChannelConfTypeDef中的Rank必须从1开始连续递增,且ADC_InitTypeDef中的NbrOfConversion必须等于实际通道数。
  • DMA缓冲区大小与通道数不匹配:缓冲区过小会导致数据覆盖,过大则可能读到未更新的旧值。
  • 扫描模式未使能:多通道必须使能ScanConvMode,否则只转换第一个通道。

排查步骤

步骤1:检查ADC初始化结构体

确认ADC_InitTypeDef中:

  • ScanConvMode = ENABLE
  • NbrOfConversion等于实际通道数
  • ContinuousConvMode根据需求设置(单次或连续)
  • DMAContinuousRequests建议使能,以便DMA循环搬运

步骤2:检查通道配置的Rank

每个通道的Rank必须从1开始连续递增,且与期望的转换顺序一致。例如期望顺序为CH0→CH1→CH2→CH3,则:

ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
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);

步骤3:检查DMA配置

DMA应配置为从ADC->DR到内存缓冲区,方向为外设到内存,数据宽度为半字(16位),模式为循环模式(如果需要连续采集)。

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);

步骤4:验证实际转换顺序

若以上配置均正确但数据仍错乱,可通过以下方法验证实际转换顺序:

  1. 将某个通道接固定电压(如3.3V),其他通道接地,观察缓冲区中哪个索引出现满量程值。
  2. 使用调试器查看ADC->SQR1、SQR2、SQR3寄存器的值,确认SQx序列是否与预期一致。

重排方法

方法一:软件重排(推荐用于已存在错乱的工程)

如果硬件配置已无法修改(例如PCB已固定通道顺序),或HAL库版本存在已知问题,可在DMA传输完成回调中对缓冲区数据进行重排。

假设实际转换顺序为CH2→CH0→CH3→CH1,而期望顺序为CH0→CH1→CH2→CH3,则重排映射为:

#define ADC_CH_NUM 4

uint16_t adc_raw[ADC_CH_NUM];      // DMA目标缓冲区
uint16_t adc_sorted[ADC_CH_NUM];   // 重排后缓冲区

// 实际转换顺序对应的通道号(根据SQR寄存器或实测确定)
const uint8_t actual_order[ADC_CH_NUM] = {2, 0, 3, 1};

void ADC_DMA_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        for (uint8_t i = 0; i < ADC_CH_NUM; i++)
        {
            // actual_order[i]表示第i个转换对应的通道号
            // 将原始数据放到该通道号对应的位置
            adc_sorted[actual_order[i]] = adc_raw[i];
        }
        // 此时adc_sorted[0]对应CH0,adc_sorted[1]对应CH1,依此类推
    }
}

在main()中启动采集:

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw, ADC_CH_NUM);

并在stm32f4xx_hal_conf.h中使能DMA回调,或在HAL_ADC_ConvCpltCallback中调用上述重排逻辑。

方法二:修正配置实现自然顺序

若可修改配置,最彻底的方法是确保Rank与期望顺序一致。使用CubeMX时,在ADC配置界面按顺序添加通道,并检查生成的代码中Rank是否连续。手动代码中务必保证:

  • NbrOfConversion = 通道数
  • 每个通道的Rank从1开始连续递增
  • 调用HAL_ADC_ConfigChannel()的顺序不影响结果,但Rank必须正确

修正后,DMA缓冲区将直接按Rank顺序存放数据,无需软件重排。

完整代码示例

以下代码展示一个完整的ADC多通道DMA采集与软件重排实现(以STM32F4 HAL库为例):

#include "stm32f4xx_hal.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

#define ADC_CH_NUM 4
uint16_t adc_raw[ADC_CH_NUM];
uint16_t adc_sorted[ADC_CH_NUM];

// 假设实际转换顺序为CH2, CH0, CH3, CH1
const uint8_t actual_order[ADC_CH_NUM] = {2, 0, 3, 1};

void ADC_Init(void)
{
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_DMA2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // GPIO配置为模拟输入(PA0-PA3对应CH0-CH3)
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;
    gpio.Mode = GPIO_MODE_ANALOG;
    gpio.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &gpio);

    // 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);

    // 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.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
    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);

    // 通道配置(Rank按实际顺序设置,此处故意设置为错乱顺序以演示重排)
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;

    sConfig.Channel = ADC_CHANNEL_2;
    sConfig.Rank = 1;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = 2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    sConfig.Channel = ADC_CHANNEL_3;
    sConfig.Rank = 3;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = 4;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        for (uint8_t i = 0; i < ADC_CH_NUM; i++)
        {
            adc_sorted[actual_order[i]] = adc_raw[i];
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    ADC_Init();

    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw, ADC_CH_NUM);

    while (1)
    {
        // adc_sorted中数据已按通道号顺序排列
        HAL_Delay(100);
    }
}

注意事项

  • Rank必须连续且从1开始:HAL库不会自动纠正Rank,重复或跳跃的Rank会导致SQR寄存器写入异常,进而引起转换顺序错乱。
  • DMA缓冲区大小必须等于通道数:若缓冲区大于通道数,循环模式下会覆盖;若小于,则数据丢失。
  • 回调函数中避免耗时操作:重排循环应尽量简短,避免影响下一次DMA传输。
  • 不同系列差异:STM32F1、F4、H7等系列的ADC结构略有不同,但扫描序列与DMA搬运原理一致,重排方法通用。
  • 使用CubeMX时检查生成代码:CubeMX可能因通道添加顺序不同而生成不同的Rank,务必在生成后核对MX_ADC1_Init()中的Rank值。
  • 调试技巧:若无法确定实际转换顺序,可将各通道接不同电压,通过串口打印原始缓冲区数据,反推顺序。

通过以上排查与重排方法,可彻底解决STM32 HAL库下ADC多通道DMA扫描的通道顺序错乱问题,确保采集数据与物理通道一一对应。