STM32 HAL库ADC多通道DMA采集通道顺序错乱的根因与修复
一、问题现象
使用STM32F4/G4系列,配置ADC1规则组扫描4个通道(IN0/IN1/IN4/IN5),开启DMA循环搬运,代码里这样写:
uint16_t adc_buf[4];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 4);
预期adc_buf[0..3]依次对应IN0、IN1、IN4、IN5。实际却出现:
- 数组元素与通道错位,例如
adc_buf[0]读到的是IN4的值; - 错位关系每次上电不同,或运行一段时间后“漂移”;
- 单次转换(非循环)时顺序正确,改循环后出错。
这类问题在电机三相电流采样、多路NTC测温中非常致命,因为软件按固定下标做标定,错位直接导致控制环发散。
二、根因分析
2.1 规则组序列由SQR寄存器决定,不是“按配置顺序”
ADC规则组最多16个转换,顺序由SQR1/SQR2/SQR3中的SQ1..SQ16字段决定。CubeMX生成的MX_ADC1_Init()里通常有:
sConfig.Rank = ADC_REGULAR_RANK_1;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
关键点:HAL_ADC_ConfigChannel内部按Rank写入对应SQx位。如果你在代码里多次调用它但Rank都写成ADC_REGULAR_RANK_1,后一次会覆盖前一次,最终只有最后一个通道生效,或序列被压缩,DMA却仍按Length=4搬运,于是读到的是“残留/重复”数据,表现为顺序错乱。
2.2 DMA长度与序列长度不一致
HAL_ADC_Start_DMA(..., Length)中的Length是传输次数,必须等于规则组实际转换个数(NbrOfConversion)。若NbrOfConversion=4但DMA Length=3,缓冲区最后一个元素永远是旧值;反之Length偏大,DMA会多搬一次,把下一次序列的首通道混进来,形成“整体移位”。
2.3 循环模式下DMA与ADC启动时序
在DMA_CIRCULAR模式下,若先HAL_ADC_Start_DMA再使能ADC(ADEN),或中途调用HAL_ADC_Stop后未重新配置序列,DMA指针与ADC序列可能不同步。G4系列还需注意ADC_CFGR_DMNGT与ADC_CFGR_CONT的配合:连续转换+循环DMA时,DMA请求在每个转换结束产生,若序列长度与缓冲区不整除,会出现半包错位。
2.4 过采样/校准残留
开启过采样(Oversampling)后,每个通道输出被平均,但若Oversampling.Ratio与通道数配置不当,HAL会按“每通道多次”触发DMA,导致缓冲区里一个通道占多个位置,看起来像顺序错乱。
三、修复方案
3.1 方案A:CubeMX正确配置(推荐)
- ADC1 → Mode:
Scan Conversion Mode = Enable,Continuous Conversion Mode = Enable,DMA Continuous Requests = Enable。 - Rank页签:依次添加IN0(Rank1)、IN1(Rank2)、IN4(Rank3)、IN5(Rank4),Number Of Conversion = 4。
- DMA Settings:添加ADC1,Mode=
Circular,Data Width=Half Word,Number of data = 4(与转换数一致)。 - 生成代码后不要再手动改Rank。
3.2 方案B:手写配置(便于理解)
static void ADC1_MultiChannel_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 = 4; // 与DMA长度一致
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
HAL_ADC_Init(&hadc1);
/* 关键:Rank必须递增且与缓冲区下标一一对应 */
sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_5; sConfig.Rank = 4; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
启动与校验:
uint16_t adc_buf[4];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 4);
/* 校验:注入已知电压到IN0,确认adc_buf[0]跟随变化 */
while (1) {
if (HAL_ADC_GetState(&hadc1) & HAL_ADC_STATE_REG_EOC) {
// adc_buf[0]=IN0, [1]=IN1, [2]=IN4, [3]=IN5
}
}
3.3 方案C:用DMA半满/全满回调做双缓冲
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
// 前半区就绪
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 后半区就绪
}
双缓冲可避免CPU读取时DMA正在覆盖同一元素,进一步消除“偶发错位”。
四、注意事项
- Rank必须从1连续递增,跳号会导致序列空洞,DMA长度对不上。
- NbrOfConversion == DMA Length,这是最常见的错位来源,改一处必须同步改另一处。
- 采样时间要覆盖信号源阻抗:高阻传感器用
ADC_SAMPLETIME_480CYCLES,否则相邻通道串扰,表现为“数值像隔壁通道”。 - 开启过采样时,DMA长度应设为
通道数 × 过采样触发次数,或改用ADC_OVR_DATA_OVERRUN保护。 - 调试时先关连续转换,用
HAL_ADC_Start单次读,确认SQR寄存器(ADC1->SQR1/2/3)内容与预期一致,再开DMA。 - F1系列无DMA循环+扫描的硬件限制较多,建议改用定时器触发+单次DMA。
五、小结
通道顺序错乱几乎都源于“序列定义”与“DMA搬运长度”不一致,或Rank被重复覆盖。牢记三句话:Rank连续、长度相等、时序对齐,即可在HAL库下稳定实现多通道ADC+DMA采集。
暂无评论,来抢沙发~