STM32 HAL库ADC多通道DMA采集数据错位:从原理到修复的完整排查指南

一、问题现象

多通道ADC+DMA是STM32最常用的采集方案,但很多开发者在调试时遇到如下现象:

  • 数组adc_buf[0]恒为0,真实数据从adc_buf[1]开始;
  • 通道1的数据出现在通道2的位置,整体错位一位;
  • 数据周期性错乱,通道顺序与预期不符;
  • 单次采集正常,连续采集后数据“漂移”。

这类问题通常不是ADC本身故障,而是DMA传输宽度、ADC扫描序列长度、内存对齐三者不匹配导致的。

二、原理:数据是怎么“错位”的

1. ADC扫描与DMA的配合

ADC在扫描模式下按Rank顺序转换:Rank1→Rank2→…→RankN。每次转换完成产生一次DMA请求,DMA把ADC->DR搬运到内存数组。因此数组下标与Rank顺序严格一一对应:

adc_buf[0] ← Rank1 (通道x)
adc_buf[1] ← Rank2 (通道y)
...

2. 三类典型根因

  • DMA数据宽度不匹配:ADC的DR是16位,若DMA配置为HALFWORD以外的宽度(如BYTE),每次搬运只取低8位或地址步进错误,导致数据错位。
  • 扫描序列长度与缓冲区长度不一致:NbrOfConversion设为3,但缓冲区只定义2个元素,DMA循环覆盖时下标错乱;或CubeMX中Rank数量与实际通道数不符。
  • 内存对齐/缓存问题:在Cortex-M7等带D-Cache的芯片上,DMA写入的内存未做Cache无效化,CPU读到旧值,表现为“错位”或“首元素为0”。

三、排查步骤

  1. 确认DMA宽度:hdma_adc1.Init.PeriphDataAlignment与MemDataAlignment都必须是DMA_PDATAALIGN_HALFWORD。
  2. 确认扫描长度:hadc1.Init.NbrOfConversion等于实际Rank数,且缓冲区元素数≥该值。
  3. 确认DMA模式:连续采集用DMA_CIRCULAR,单次用DMA_NORMAL,模式错误会造成只搬一次或覆盖错位。
  4. 检查Cache:M7平台在DMA启动前对缓冲区做SCB_InvalidateDCache_by_Addr,或把缓冲区放到非Cache区。
  5. 用调试器观察:在DMA完成回调中打印adc_buf,对比已知电压源,定位错位方向。

四、完整修复代码

以下为3通道(PA0/PA1/PA2)循环采集的标准配置:

#define ADC_CH_NUM 3
static uint16_t adc_buf[ADC_CH_NUM] __attribute__((aligned(4)));

void ADC_DMA_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 = ADC_CH_NUM;   // 与Rank数一致
    hadc1.Init.DMAContinuousRequests = ENABLE;
    HAL_ADC_Init(&hadc1);

    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    sConfig.Rank = 1; sConfig.Channel = ADC_CHANNEL_0; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Rank = 2; sConfig.Channel = ADC_CHANNEL_1; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    sConfig.Rank = 3; sConfig.Channel = ADC_CHANNEL_2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    // DMA配置:宽度必须为HALFWORD
    hdma_adc1.Instance = DMA1_Channel1;
    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);

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

// 采集完成后按Rank顺序读取
void ADC_GetValues(uint16_t *out)
{
    out[0] = adc_buf[0];  // Rank1
    out[1] = adc_buf[1];  // Rank2
    out[2] = adc_buf[2];  // Rank3
}

若使用M7内核,在启动DMA前加:

SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));

五、注意事项

  • 缓冲区长度必须≥NbrOfConversion,否则循环DMA会越界覆盖,造成看似“错位”的随机值。
  • 不要在DMA运行中修改Rank配置,需先HAL_ADC_Stop_DMA再重配。
  • 首元素为0常见于DMA尚未搬运完成就读取,应等待HAL_ADC_ConvCpltCallback或在读取前做Cache无效化。
  • CubeMX生成代码中若手动改过Rank顺序,务必同步修改NbrOfConversion,二者不一致是最隐蔽的错位来源。
  • 多通道采集建议给缓冲区加aligned(4),避免非对齐访问在部分内核上触发异常。

按以上顺序排查,90%以上的ADC多通道DMA错位问题可在十分钟内定位并修复。