问题现象与背景
在STM32项目中使用HAL库配置ADC多通道扫描+DMA传输时,开发者常遇到采样数据与通道不对应的情况。例如配置了通道0、1、2、3,但读取数组时发现adc_buf[0]对应通道1的数据,adc_buf[1]对应通道2,而通道0的数据出现在数组末尾。这种错位在DMA循环模式下尤为隐蔽,导致后续数据处理完全错误。
错位根源分析
1. ADC扫描序列与DMA传输顺序
STM32的ADC在扫描模式下,按照Rank顺序依次转换通道。DMA请求在每次转换完成后触发,将ADC_DR寄存器内容搬运到内存。理论上,DMA传输顺序应与Rank顺序一致。但以下因素会导致错位:
- DMA传输未及时启动:若在ADC启动后才配置DMA,首次转换可能丢失或错位。
- DMA循环模式下的缓冲区覆盖:循环模式下DMA持续搬运,若CPU读取速度慢于DMA写入,可能读到半更新数据。
- ADC校准或启动延迟:HAL_ADC_Start_DMA内部先启动DMA再启动ADC,但若ADC未就绪,首次转换可能异常。
- 多通道Rank配置错误:CubeMX中通道顺序与代码中数组索引未对齐。
2. 典型错位场景
假设配置Rank1=通道0,Rank2=通道1,Rank3=通道2,Rank4=通道3,DMA缓冲区uint16_t adc_buf[4]。若DMA传输完成中断触发后读取,发现adc_buf[0]为通道1值,adc_buf[1]为通道2值,adc_buf[2]为通道3值,adc_buf[3]为通道0值。这通常是因为DMA在ADC启动前已使能,首次传输发生在ADC稳定之前,导致第一个数据被丢弃或覆盖。
配置步骤与排查方法
步骤1:检查CubeMX配置
- ADC设置:Scan Conversion Mode = Enabled;Continuous Conversion Mode = Disabled(单次扫描)或Enabled(连续扫描);Discontinuous Conversion Mode = Disabled。
- Rank配置:按实际通道顺序设置Rank,每个Rank对应一个通道,采样时间一致。
- DMA设置:添加DMA请求,模式为Circular(循环)或Normal(单次),数据宽度Half Word(16位),内存地址自增。
步骤2:验证DMA启动顺序
HAL库的HAL_ADC_Start_DMA()函数内部先调用HAL_DMA_Start_IT()启动DMA,再使能ADC。若ADC未校准或未稳定,首次转换可能产生无效数据。建议在启动前执行ADC校准:
if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) {
Error_Handler();
}
步骤3:使用DMA传输完成中断重排
在DMA传输完成回调中,根据Rank顺序重新映射数据。例如:
#define ADC_CH_NUM 4
uint16_t adc_raw[ADC_CH_NUM];
uint16_t adc_data[ADC_CH_NUM]; // 重排后数组,索引0对应通道0
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
// 假设Rank顺序为:通道0,1,2,3
// 但实际DMA缓冲区可能错位,根据实测调整映射
adc_data[0] = adc_raw[3]; // 通道0
adc_data[1] = adc_raw[0]; // 通道1
adc_data[2] = adc_raw[1]; // 通道2
adc_data[3] = adc_raw[2]; // 通道3
}
}
注意:映射关系需通过实验确定。可先固定输入已知电压,观察各数组元素值,建立正确映射表。
步骤4:启用DMA循环模式与双缓冲
若使用循环模式,建议启用DMA双缓冲(若支持)或使用两个缓冲区交替,避免读取时数据被覆盖。HAL库中可通过HAL_ADC_Start_DMA()指定缓冲区地址和长度,在回调中处理。
完整代码示例
以下代码基于STM32F4,ADC1扫描4个通道,DMA循环模式,在回调中重排数据。
#include "main.h"
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
#define ADC_CH_NUM 4
uint16_t adc_raw[ADC_CH_NUM];
uint16_t adc_data[ADC_CH_NUM];
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校准
if (HAL_ADCEx_Calibration_Start(&hadc1) != HAL_OK) {
Error_Handler();
}
// 启动ADC DMA
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw, ADC_CH_NUM) != HAL_OK) {
Error_Handler();
}
while (1) {
// 主循环可读取adc_data,注意临界区保护
__disable_irq();
uint16_t ch0 = adc_data[0];
uint16_t ch1 = adc_data[1];
__enable_irq();
// 处理数据...
HAL_Delay(10);
}
}
// ADC初始化
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 = ADC_CH_NUM;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler();
}
// 配置通道0 (Rank1)
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 通道1 (Rank2)
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 通道2 (Rank3)
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 3;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
// 通道3 (Rank4)
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = 4;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
// DMA初始化
static void MX_DMA_Init(void) {
__HAL_RCC_DMA2_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
// ADC转换完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if (hadc->Instance == ADC1) {
// 实测映射:adc_raw[0]->通道1, [1]->通道2, [2]->通道3, [3]->通道0
adc_data[0] = adc_raw[3];
adc_data[1] = adc_raw[0];
adc_data[2] = adc_raw[1];
adc_data[3] = adc_raw[2];
}
}
// DMA中断处理
void DMA2_Stream0_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_adc1);
}
注意事项
-
校准与启动顺序:务必在
HAL_ADC_Start_DMA前执行校准,否则首次转换可能无效。 - DMA缓冲区对齐:确保缓冲区地址对齐到数据宽度(16位需2字节对齐),避免传输异常。
- 中断优先级:DMA中断优先级应高于或等于ADC中断,防止数据覆盖。
- 循环模式下的数据一致性:在回调中重排时,若主循环同时读取,需使用临界区或双缓冲。
- 映射关系验证:不同STM32系列或CubeMX版本可能导致Rank顺序差异,务必通过实测确定映射表。
- 使用LL库替代:若对时序要求严格,可考虑LL库直接操作寄存器,避免HAL层延迟。
通过以上分析与方案,可有效解决ADC多通道DMA扫描模式下的数据错位问题,确保采样数据与通道正确对应。
暂无评论,来抢沙发~