STM32 HAL库ADC多通道DMA扫描模式:采样时序配置与溢出排查实战

一、为什么多通道ADC要配DMA

STM32的ADC在扫描(Scan)模式下会按规则序列依次转换多个通道,每转换完一个通道就产生一次EOC并发出DMA请求。若用中断逐通道读取,CPU需在每次转换后及时搬运DR寄存器,通道多、采样率高时极易丢数据。DMA循环模式可自动把每个通道结果搬到内存数组,CPU零参与,这是多通道采集的标准做法。

但“自动”不等于“无脑”:ADC转换速度与DMA搬运速度之间若出现时序失配,就会触发溢出(OVR),表现为数据错位、数组值跳变。理解时序是排查的前提。

二、采样时序原理

一次ADC转换的总时间由两部分组成:

  • 采样时间(Sampling Time):采样电容充电时间,由SMPx位配置,单位ADC时钟周期。
  • 转换时间(Conversion Time):12位分辨率固定为12.5个ADC时钟周期。

总转换时间 = (采样周期 + 12.5) / f_ADC。例如f_ADC=12MHz、采样周期=84.5时,单通道约8.1µs。

扫描模式下,序列中每个通道依次转换,每个通道转换结束都会产生一次DMA请求。DMA必须在下一个通道转换完成前把DR读走;若DMA被更高优先级外设长时间占用,或ADC时钟过快而DMA通道优先级过低,DR未被及时读取,新结果覆盖旧结果即产生OVR。

关键配置原则:

  • ADC时钟不要超过手册上限(如F1系列14MHz、F4系列36MHz)。
  • DMA通道优先级设为High或VeryHigh,避免被抢占。
  • 采样时间按信号源内阻选取,内阻大则加长采样时间,兼顾精度与吞吐。
  • 使用定时器触发(如TIM_TRGO)而非软件触发,保证采样间隔稳定。

三、CubeMX/HAL配置步骤

  1. ADC配置为Scan Conversion Mode = Enabled,Continuous Conversion Mode按需(定时触发时关闭)。
  2. 规则序列填入通道及各自采样时间,Number Of Conversion设为通道数。
  3. DMA Settings添加ADC请求,Mode选Circular,Data Width选Half Word(12位结果)。
  4. 触发源选定时器TRGO,配置定时器更新频率为期望采样率。
  5. 生成代码后调用HAL_ADC_Start_DMA()启动。

四、完整代码示例

#define ADC_CH_NUM  3
static uint16_t adc_buf[ADC_CH_NUM];

void ADC_DMA_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance = ADC1;
    hadc1.Init.ScanConvMode = ENABLE;
    hadc1.Init.ContinuousConvMode = DISABLE;   /* 定时器触发 */
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = ADC_CH_NUM;
    hadc1.Init.DMAContinuousRequests = ENABLE;
    if (HAL_ADC_Init(&hadc1) != HAL_OK) { Error_Handler(); }

    /* 通道0:采样84.5周期 */
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    /* 通道1:内阻较大,采样加长 */
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank = 2;
    sConfig.SamplingTime = ADC_SAMPLETIME_144CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    /* 通道4 */
    sConfig.Channel = ADC_CHANNEL_4;
    sConfig.Rank = 3;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    /* 启动DMA循环采集 */
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_CH_NUM);
}

/* 定时器2产生触发,频率即采样率 */
void TIM2_Trigger_Init(uint32_t sample_rate_hz)
{
    __HAL_RCC_TIM2_CLK_ENABLE();
    TIM2->PSC = 72 - 1;                 /* 72MHz -> 1MHz */
    TIM2->ARR = (1000000 / sample_rate_hz) - 1;
    TIM2->CR2 |= TIM_CR2_MMS_1;         /* Update -> TRGO */
    TIM2->CR1 |= TIM_CR1_CEN;
}

五、OVR溢出排查清单

出现OVR(HAL_ADC_GetState含ADC_STATE_ERROR,或ADC_ISR_OVR置位)时按以下顺序排查:

  • DMA优先级:确认ADC对应DMA通道优先级不低于其他高频外设。
  • DMA模式:误配为Normal会在搬完一轮后停止,后续转换全部溢出,必须Circular。
  • 采样率与DMA带宽:计算单通道转换时间,确认DMA能在该时间内完成一次搬运。
  • 触发频率:定时器ARR过小导致触发过密,降低采样率验证。
  • 中断抢占:高优先级中断长时间关中断会延迟DMA响应,检查临界区。
  • 数据宽度:DMA配成Word而ADC为12位右对齐,虽不直接致OVR但会错位。
  • 清除标志:溢出后需__HAL_ADC_CLEAR_FLAG(&hadc1, ADC_FLAG_OVR)并重启DMA,否则卡死。

六、注意事项

  • 多通道结果在adc_buf中按Rank顺序排列,与通道号无直接对应,读取时按序列索引。
  • 采样时间过短会导致高内阻信号源采样不足,读数偏小且不稳定,优先加长采样时间而非提高时钟。
  • 若需通道间同步,考虑双ADC交替/同步模式,单ADC扫描本质是串行转换。
  • 调试时可用__HAL_ADC_GET_FLAG在DMA完成回调中检查OVR,尽早暴露时序问题。

掌握“转换时间 vs DMA搬运时间”这条主线,多通道DMA采集的时序配置与溢出问题就能系统化定位,而非盲目试参数。