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”。
三、排查步骤
-
确认DMA宽度:
hdma_adc1.Init.PeriphDataAlignment与MemDataAlignment都必须是DMA_PDATAALIGN_HALFWORD。 -
确认扫描长度:
hadc1.Init.NbrOfConversion等于实际Rank数,且缓冲区元素数≥该值。 -
确认DMA模式:连续采集用
DMA_CIRCULAR,单次用DMA_NORMAL,模式错误会造成只搬一次或覆盖错位。 -
检查Cache:M7平台在DMA启动前对缓冲区做
SCB_InvalidateDCache_by_Addr,或把缓冲区放到非Cache区。 -
用调试器观察:在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错位问题可在十分钟内定位并修复。
暂无评论,来抢沙发~