一、引言

在嵌入式开发中,使用STM32的ADC采集多路模拟信号是常见需求。为了高效、不占用CPU,通常会启用ADC的扫描模式(Scan Mode)配合DMA进行多通道数据搬运。然而,很多开发者在使用HAL库时,会遇到数据顺序错乱、通道与数据不对应、甚至数据完全错误的问题。这往往源于对ADC扫描序列、DMA传输对齐以及数据宽度匹配的理解不够深入。本文将系统讲解这些陷阱,并提供完整的解决方案。

二、ADC扫描模式与采样顺序原理

2.1 规则组序列

STM32的ADC规则组支持最多16个通道的序列转换。通过ADC_RegularChannelConfig()(HAL库中为HAL_ADC_ConfigChannel())可以配置每个序列位置对应的通道和采样时间。序列的转换顺序由ADC_SQRx寄存器中的SQ1[4:0]到SQ16[4:0]决定,SQ1最先转换。

关键点:DMA搬运的数据顺序严格遵循规则组的转换顺序,即第一个数据对应SQ1配置的通道,第二个对应SQ2,依此类推。

2.2 扫描模式与连续转换

  • 扫描模式(Scan Mode):使能后,ADC依次转换规则组中的所有通道。
  • 连续转换(Continuous Conversion):使能后,ADC在完成一轮扫描后自动开始下一轮,无需软件触发。
  • DMA循环模式(Circular):使能后,DMA在传输完指定数据量后自动重新开始,适合连续采样。

三、数据对齐陷阱

3.1 数据对齐方式

ADC转换结果可以左对齐或右对齐(12位分辨率下)。HAL库中通过ADC_InitTypeDef的DataAlign成员配置:

  • ADC_DATAALIGN_RIGHT:右对齐,低12位有效,高4位为0。
  • ADC_DATAALIGN_LEFT:左对齐,高12位有效,低4位为0。

陷阱:若使用DMA搬运16位数据(uint16_t),右对齐时直接读取即可;但若配置为左对齐,读取的值需要右移4位才能得到真实电压值。许多开发者忘记移位,导致数据看似“放大”了16倍。

3.2 DMA数据宽度匹配

DMA外设数据宽度(Peripheral Data Size)必须与ADC数据寄存器宽度(16位)一致,通常设置为DMA_PDATAALIGN_HALFWORD。内存数据宽度(Memory Data Size)也应为半字(DMA_MDATAALIGN_HALFWORD),且目标数组类型应为uint16_t。若内存宽度设置为字节(BYTE),则每个16位ADC结果会被拆分成两个字节,导致数据错位。

陷阱:DMA传输完成中断中,若数组定义为uint8_t,数据将无法正确解析。

四、配置步骤与完整代码示例

4.1 硬件与软件环境

  • STM32F407ZGT6
  • STM32CubeMX + HAL库
  • ADC1,通道0(PA0)、通道1(PA1)、通道2(PA2)
  • DMA2 Stream0 Channel0

4.2 CubeMX配置要点

  1. 使能ADC1,勾选Scan Mode、Continuous Conversion Mode、DMA Continuous Requests。
  2. 在规则组中按顺序添加通道0、1、2,设置采样时间。
  3. 使能DMA,模式为Circular,数据宽度均为Half Word。
  4. 生成代码。

4.3 代码实现

#include "main.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

#define ADC_CHANNELS 3
uint16_t adc_buffer[ADC_CHANNELS]; // 必须为uint16_t

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();

  // 校准ADC
  HAL_ADCEx_Calibration_Start(&hadc1);

  // 启动ADC DMA传输
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_CHANNELS);

  while (1)
  {
    // 主循环可处理其他任务,DMA自动搬运数据
    HAL_Delay(100);
  }
}

static void MX_ADC1_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.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐
  hadc1.Init.NbrOfConversion = 3;            // 3个转换
  hadc1.Init.DMAContinuousRequests = ENABLE; // DMA连续请求
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; // 序列转换完成标志
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  // 配置通道0为序列1
  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = 1;
  sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  // 配置通道1为序列2
  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = 2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  // 配置通道2为序列3
  sConfig.Channel = ADC_CHANNEL_2;
  sConfig.Rank = 3;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_DMA_Init(void)
{
  __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;
  if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
  {
    Error_Handler();
  }

  __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}

4.4 数据读取与验证

在adc_buffer中,索引0对应通道0,索引1对应通道1,索引2对应通道2。可通过串口打印验证:

printf("CH0: %d, CH1: %d, CH2: %d\n", adc_buffer[0], adc_buffer[1], adc_buffer[2]);

五、常见陷阱与注意事项

  • 陷阱1:DMA内存地址递增未使能。若MemInc为DISABLE,所有通道数据会覆盖同一地址,导致只有最后一个通道数据。务必使能DMA_MINC_ENABLE。
  • 陷阱2:数据宽度不匹配。ADC数据寄存器为16位,DMA内存宽度必须为半字,目标数组必须为uint16_t。若使用uint8_t数组,数据将错位。
  • 陷阱3:采样顺序与数组索引不对应。务必确保规则组序列配置顺序与代码中数组索引的预期一致。建议使用宏定义或枚举明确通道顺序。
  • 陷阱4:未校准ADC。启动前调用HAL_ADCEx_Calibration_Start()可提高精度。
  • 陷阱5:DMA传输完成中断处理不当。若使用中断,注意在回调函数中及时处理数据,避免覆盖。
  • 陷阱6:连续转换模式下EOC标志选择。EOCSelection应设置为ADC_EOC_SEQ_CONV(序列转换完成),否则DMA可能提前触发。
  • 陷阱7:左对齐数据未移位。若配置为左对齐,读取值需右移4位(12位分辨率)才能得到正确结果。

六、总结

多通道ADC+DMA扫描模式是STM32嵌入式开发中的高效方案,但采样顺序与数据对齐的细节极易出错。掌握规则组序列的配置逻辑、DMA数据宽度匹配、对齐方式的影响,并遵循本文的配置步骤与注意事项,可以避免绝大多数隐蔽bug。建议在调试时先用单通道验证,再逐步增加通道,并利用调试器观察adc_buffer内容,确保数据与通道一一对应。