第一次用STM32做多通道ADC采集时,我对着数据手册发呆了半小时——规则通道、注入通道、扫描模式这些概念像天书一样。后来在工控项目里被逼着啃透了这块,才发现其实掌握几个关键点就能玩转多通道采集。
电压输入范围就像ADC的"胃口",STM32的ADC默认能吃下0-3.3V的电压信号。这个范围由VREF+和VREF-引脚决定,常规接法是VREF-接地,VREF+接3.3V。有次我偷懒没接VREF+,结果采集的数据全飘了,血的教训告诉我们这两个引脚必须接好。
通道选择比想象中复杂得多。STM32F103的ADC1有16个外部通道加2个内部通道(温度传感器和内部参考电压)。这些通道分为规则组和注入组,规则组像排队买奶茶的队伍,注入组就像VIP通道可以插队。我在电机控制项目中就用注入通道来紧急采集过流信号,确实能及时打断常规采集流程。
最让人头大的转换顺序配置,其实就靠三个寄存器搞定:
比如要按顺序采集通道5、11、8,配置代码如下:
c复制ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 3, ADC_SampleTime_55Cycles5);
扫描模式是多通道采集的灵魂。开启后ADC会自动按顺序扫描所有设置的通道,配合连续转换模式就能实现循环采集。不过要注意DMA和中断的区别——我最早用中断处理多通道数据,结果漏数据严重,后来改用DMA才稳定。
ADC中断就像外卖小哥的电话,数据准备好了就通知你来取。但处理不当就会像我被投诉那样——要么漏接电话,要么取餐太慢。
中断类型有三种需要分清:
在电机监控项目中,我是这样配置中断优先级的:
c复制NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = ADC1_2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
中断服务函数里最容易踩的坑是忘记清标志位。有次我写的代码采集几次后就卡死了,debug发现是没清EOC标志。正确的处理流程应该是:
c复制void ADC1_2_IRQHandler(void)
{
if(ADC_GetITStatus(ADC1, ADC_IT_EOC) == SET)
{
adc_values[channel_index++] = ADC_GetConversionValue(ADC1);
if(channel_index >= CHANNEL_NUM) channel_index = 0;
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
}
}
数据对齐问题也坑过我。STM32的ADC是12位精度,但数据寄存器是16位的。右对齐时数据在低12位,左对齐时在高12位。有次我忘记设置对齐方式,采集的值总是差4倍,后来发现是左对齐时没右移4位。
当通道数超过3个时,DMA就是必备技能了。它像个小秘书,自动把ADC数据搬到指定数组,解放CPU去干更重要的事。
DMA配置的关键参数:
我的典型配置代码:
c复制DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = CHANNEL_NUM;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
双缓冲技巧能避免数据竞争。我开辟两个数组,DMA写A数组时程序读B数组,下次中断后交换。这招在音频采集时特别管用:
c复制volatile uint16_t adc_buf1[CHANNEL_NUM];
volatile uint16_t adc_buf2[CHANNEL_NUM];
volatile uint8_t current_buf = 0;
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1))
{
current_buf = !current_buf;
DMA_ClearITPendingBit(DMA1_IT_TC1);
}
}
时钟配置直接影响采样率。我的经验公式:
code复制ADC时钟 = PCLK2 / 分频系数(通常取6分频)
单通道采样时间 = (采样周期 + 12.5) / ADC时钟
多通道总时间 = 各通道采样时间之和 + 转换间隔
比如72MHz主频、6分频、3通道各55.5周期采样,理论最高采样率约200kHz。
调ADC就像老中医把脉,要望闻问切。这里分享几个典型病例和药方。
数据跳变可能是硬件问题。先用示波器看输入信号是否稳定,再检查参考电压纹波。有次我用开关电源给VREF+供电,结果ADC值末位总在跳,换成LDO后立马稳定。
通道串扰的解决办法:
低功耗优化技巧:
校准数据千万别忽略。STM32出厂时带有校准值,上电后要先执行:
c复制ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
有次我偷懒没校准,结果在3V量程下有近50mV的误差,校准后误差小于5mV。
软件滤波算法推荐: