问题现象:DMA缓冲区里的通道数据“串位”

在多通道ADC采集中,常见配置是ADC1扫描模式+DMA循环传输,把规则组通道按顺序写入数组。典型现象是:

  • 配置顺序为 CH0, CH1, CH2, CH3,但 adc_buf[0] 读到的是CH2的值,adc_buf[1] 是CH0的值;
  • 每次复位后错乱顺序还不一样;
  • 单通道测试完全正常,一旦开启多通道扫描就出错。

这类问题不是ADC本身“算错了”,而是扫描序列、DMA搬运宽度、HAL初始化时序三者没有对齐。

原理:ADC扫描与DMA的配合机制

STM32的ADC规则组支持最多16个通道的扫描序列,由 SQR1/SQR2/SQR3 寄存器中的 SQx 字段定义转换顺序。每次触发后,ADC按序列依次转换,每完成一个通道就把结果写入 DR 寄存器。

DMA的作用是在 DR 被写入后自动搬运到内存。关键点在于:

  • DMA请求由ADC的EOC(转换结束)事件产生,每个通道转换完成都会触发一次搬运;
  • DMA的外设数据宽度必须与ADC数据对齐方式匹配:ADC配置为12位右对齐时,DR 低16位有效,DMA应配置为半字(16位);
  • DMA的内存地址递增必须开启,否则所有通道数据会覆盖到同一个数组元素;
  • 扫描模式下必须使能 ADC_CR1_SCAN,否则只有第一个通道被转换。

顺序错乱的本质是:DMA搬运次数与ADC转换次数不同步,或搬运宽度不匹配导致地址偏移错位。

排查步骤

1. 检查CubeMX中的DMA配置

在 CubeMX -> ADC1 -> DMA Settings 中确认:

  • Mode 选择 Circular(连续采集)或 Normal(单次);
  • Data Width:Peripheral 与 Memory 都设为 Half Word;
  • Increment Address:Memory 勾选,Peripheral 不勾选;

如果Memory宽度误设为Word,DMA每次搬运4字节,而ADC只提供2字节有效数据,数组会出现“高低半字交错”的错位。

2. 检查ADC扫描序列配置

HAL库中规则组序列通过 ADC_ChannelConfTypeDef 的 Rank 字段指定。常见错误是多个通道使用相同Rank,或Rank不连续。正确做法:

ADC_ChannelConfTypeDef sConfig = {0};
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES;

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

注意:Rank 从1开始,且必须连续。若中间跳号,HAL库不会报错,但扫描序列会异常。

3. 检查HAL_ADC_Start_DMA的调用时机

必须在ADC校准和配置完成后调用,且DMA通道已初始化:

HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM);

如果先启动DMA再配置通道,前几次转换可能使用旧序列,导致缓冲区前几个元素错位。

4. 用固定电平验证通道映射

将各通道分别接已知电压(如CH0接GND、CH1接3.3V、CH2接1.65V),打印缓冲区,确认 adc_buf[i] 与物理通道一一对应。这是最直接的定位手段。

完整修复代码示例

以下代码基于STM32F4 HAL库,配置ADC1的CH0~CH3四通道扫描+DMA循环采样:

#define ADC_CH_NUM 4

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adc_buf[ADC_CH_NUM];

void ADC1_Init(void)
{
    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_DMA2_CLK_ENABLE();

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

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

    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES;

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

    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CH_NUM);
}

关键配置说明:

  • EOCSelection = ADC_EOC_SEQ_CONV:整个序列转换完成才触发EOC,配合DMA可减少中断开销;若使用 ADC_EOC_SINGLE_CONV,每个通道都触发DMA请求,需确保DMA缓冲区长度与通道数一致;
  • DMAContinuousRequests = ENABLE:循环模式下持续请求;
  • NbrOfConversion = ADC_CH_NUM:必须与实际配置的Rank数量一致。

注意事项与避坑清单

  • DMA宽度必须匹配ADC对齐方式:12位右对齐用Half Word;若使用左对齐或过采样,需重新核算;
  • Rank必须从1连续编号,且 NbrOfConversion 等于最大Rank;
  • 不要在DMA传输中途修改通道序列,否则缓冲区映射关系会变化;
  • 循环模式下数组长度等于通道数,否则DMA会越界写;
  • 调试时先关闭连续转换,用单次触发验证顺序,再切回连续模式;
  • 若使用ADC_EOC_SINGLE_CONV,DMA缓冲区长度应为通道数,但HAL库回调触发频率更高,注意CPU负载;
  • 多ADC(如ADC1+ADC2)同步采样时,主从ADC的DMA配置需分别检查,顺序错乱常来自从ADC的Rank配置遗漏。

按以上步骤排查,绝大多数“通道顺序错乱”都能定位到DMA宽度、Rank连续性或启动时序三个点。建议在项目初期就加入固定电平自检代码,把问题挡在联调之前。