问题现象与背景

在使用STM32 HAL库做多通道ADC采集时,很多开发者会配置ADC为扫描模式(Scan Conversion Mode)并开启DMA连续传输。典型现象是:明明按 IN0, IN1, IN2, IN3 的顺序配置了规则组,读到的DMA缓冲区却出现通道错位,例如 buf[0] 对应的是IN2、buf[1] 对应IN0。数据本身没错,错的是“谁是谁”。

这类问题在嵌入式项目中非常隐蔽,因为电压值看起来都“合理”,只有做通道比对时才会暴露。下面从原理到代码逐层排查。

原理:规则组序列与DMA搬运顺序

STM32的ADC规则组转换顺序由 SQR1/SQR2/SQR3 三个寄存器中的 SQ1~SQ16 字段决定。转换顺序由SQx编号决定,而不是由通道号大小决定。例如:

  • SQ1 = 通道2
  • SQ2 = 通道0
  • SQ3 = 通道3

则DMA缓冲区顺序为 [CH2, CH0, CH3]。

HAL库通过 ADC_ChannelConfTypeDef.Rank 来写这些字段。Rank 是“第几个转换”,不是通道号。很多错乱源于Rank赋值与预期通道顺序不一致,或CubeMX中Rank被自动重排。

另一个关键点是DMA数据宽度。ADC_DR是32位寄存器,若DMA配置为半字(Half Word)传输,而ADC分辨率/对齐方式不匹配,会导致缓冲区元素错位,表现为“隔一个错一个”。

排查步骤

1. 检查Rank与通道的映射

在 MX_ADC1_Init() 中逐条核对:

sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;   // 第1个转换
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = ADC_REGULAR_RANK_2;   // 第2个转换
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

若Rank写成 ADC_REGULAR_RANK_1 重复,后写会覆盖前写,最终只剩一个通道或顺序异常。

2. 检查DMA配置

hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode                = DMA_CIRCULAR;

ADC的 Resolution 若为12位且右对齐,ADC_DR 低16位有效,用半字传输即可;若用字传输而缓冲区定义为 uint16_t,会出现高低半字错位。

3. 检查扫描与连续转换使能

hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 4;   // 必须等于实际通道数

NbrOfConversion 小于实际配置数时,多出的Rank不会转换,缓冲区尾部数据是旧值,容易被误判为错位。

重排方案与完整代码

最稳妥的做法是不依赖Rank的隐式顺序,而是显式建立“缓冲区索引→通道”的映射表,在应用层做重排。这样即使CubeMX重排Rank,逻辑仍然正确。

#include "main.h"

extern ADC_HandleTypeDef hadc1;
extern DMA_HandleTypeDef hdma_adc1;

#define ADC_CH_NUM   4

/* DMA原始缓冲区:顺序由Rank决定 */
static volatile uint16_t adc_raw[ADC_CH_NUM];

/* 逻辑通道顺序:索引i对应物理通道 */
static const uint8_t ch_map[ADC_CH_NUM] = {
    ADC_CHANNEL_0, ADC_CHANNEL_1, ADC_CHANNEL_2, ADC_CHANNEL_3
};

/* 重排后的结果,按ch_map顺序存放 */
static uint16_t adc_sorted[ADC_CH_NUM];

void ADC_DMA_Init(void)
{
    /* 按ch_map顺序显式配置Rank,保证raw顺序与map一致 */
    ADC_ChannelConfTypeDef sConfig = {0};
    for (uint8_t i = 0; i < ADC_CH_NUM; i++) {
        sConfig.Channel      = ch_map[i];
        sConfig.Rank         = i + 1;          /* Rank从1开始 */
        sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5;
        if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
            Error_Handler();
        }
    }

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

/* 在转换完成回调中重排 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        for (uint8_t i = 0; i < ADC_CH_NUM; i++) {
            adc_sorted[i] = adc_raw[i];   /* raw顺序已与map一致 */
        }
    }
}

若历史工程中Rank已被打乱,可在回调中用查表法重排:

/* 假设raw实际顺序为 [CH2, CH0, CH3, CH1] */
static const uint8_t raw_to_logic[ADC_CH_NUM] = {1, 3, 0, 2};

for (uint8_t i = 0; i < ADC_CH_NUM; i++) {
    adc_sorted[raw_to_logic[i]] = adc_raw[i];
}

注意事项

  • Rank必须从1连续递增,不能跳号,否则未配置的Rank行为未定义。
  • NbrOfConversion必须等于实际Rank数量,否则DMA长度与转换数不匹配。
  • DMA缓冲区建议加 volatile 并用 __DSB() 或关闭中断临界区读取,避免编译器优化与DMA并发写冲突。
  • 使用CubeMX时,修改通道后要重新生成代码并复查 MX_ADC1_Init(),因为CubeMX会按界面顺序重写Rank。
  • 若采样率较高,建议用双缓冲(DMA Half/Full回调)分离采集与处理,避免重排时读到半更新数据。
  • 调试时先用固定直流电压逐通道验证映射表,再接入真实信号,可快速定位是硬件通道还是软件顺序问题。

小结

通道错乱的本质是“转换顺序由Rank决定”这一规则被忽视。排查时先核对Rank与NbrOfConversion,再检查DMA数据宽度,最后用显式映射表在应用层重排。把顺序问题从“隐式约定”变成“显式代码”,是嵌入式ADC多通道采集最稳妥的工程实践。