MCU型号:
STM32L071KBU
SDK:HAL库
工具:CubeMX + MDK
下面为STM32F1/4系列的ADC硬件框架(STM32L0系列ADC框架相对比较简单)
12位ADC是一种逐次逼近型模拟数字转换器(successive approximation analog-to-digital converter),它有多达18个通道,可测量16个外部和2个内部信号源。(L0系列有19个通道,其中3个内部信号源),各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。
STM32F10x系列芯片ADC通道和引脚对应关系 :
有16个复用通道,可以将转换分为两组:规则通道组和注入通道组。规则通道至多16个,注入通道至多4个。
规则通道组:相当于正常运行的程序
注入通道组:注入通道可以打断规则通道,如果在规则通道转换过程中,有注入通道进行转换,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。(类似中断打断正常运行的程序)
单次转换模式:ADC只执行一次转换(适用于规则通道和注入通道)
连续转换模式:当前面的ADC转换结束后,立马启动下一次转换
扫描模式(多通道使用):ADC扫描所有选中的规则通道和注入通道,在每个组的每个通道上执行单次转换。在每个转换结束时,这一组的下一个通道被自动转换。如果设置了CONT
位(开启了连续转换模式),转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。(所有通道转换完毕才会产生相应中断,可通过DMA读取)
间断(不连续)模式:每触发一次,转换一个通道。在所选转换通道循环,由触发信号启动新一轮的转换,直到转换完成为止。
扫描模式是一次对所有选中的通道进行转换,比如开了ch0,ch1,ch4,ch5。 ch0转换完以后就会自动转换通道1,4,5直到转换完这个过程不能被打断。如果开启了连续转换模式,则会在转换完ch5之后开始新一轮的转换。
间断模式是对扫描模式的一种补充,它可以把0,1,4,5这四个通道进行分组。可以分成0,1一组,4,5一组。也可以每个通道单独配置为一组。这样每一组转换之前都需要先触发一次。
ADC单通道:
只进行一次ADC转换:配置为“单次转换模式”,扫描模式关闭。ADC通道转换一次后,就停止转换。等待再次使能后才会重新转换
进行连续ADC转换:配置为“连续转换模式”,扫描模式关闭。ADC通道转换一次后,接着进行下一次转换,不断连续。
ADC多通道:
只进行一次ADC转换:配置为“单次转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,就停止转换。等待再次使能后才会重新转换
进行连续ADC转换:配置为“连续转换模式”,扫描模式使能。ADC的多个通道,按照配置的顺序依次转换一次后,接着进行下一次转换,不断连续。
因此,多通道必须使能扫描模式。
ADC一般用于采集小电压,其输入值不能超过VDDA,即ADC输入范围:VREF- ≤ VIN ≤ VREF+
。
一般把VSSA和VREF- 接地, VREF+ 和 VDDA接3V3,那么ADC的输入范围是0~3.3V。
从ADCx_INT0-ADCx_INT15
对应三个ADC的16个外部通道,进行模拟信号转换。此外,还有两个内部通道:温度检测或者内部电压检测
参看【通道选择】。
STM32L0系列ADC时钟为HSI时钟(16M),STM32F1/4系列时钟为经可编程预分频器分频的 APB2 时钟, 分频因子由RCC_CFGR的ADCPRE[1:0]配置,可配置2/4/6/8分频,对F1/4ADC的时钟最好不超过14M。
ADC采样时间的计算公式:T = 采样时间 + 12.5个周期,其中1周期为1/ADCCLK
例如,ADC_CLK = 16 MHz 且采用时间为 1.5 ADC时钟周期:采样时间= 1.5 + 12.5 = 14 ADC 时钟周期 = 0.875 μs
ADC 转换可以由ADC 控制寄存器2: ADC_CR2
的ADON
位来控制,写1 的时候开始转换,写0 的时候停止转换。(L0系列由ADC_CR
的ADSTART
位控制)
除了ADC_CR2
寄存器的ADON
位控制转换的开始与停止,还可以支持外部事件触发转换(比如定时器捕捉、EXTI线),包括内部定时器触发和外部IO触发。具体的触发源由ADC_CR2
的EXTSEL[2:0]
位(规则通道触发源 )和 JEXTSEL[2:0]
位(注入通道触发源)控制。
STM32F1:
STM32F4:
STM32L0:
分别为:校准结束、ADC准备就绪、转换结束、转换序列结束、模拟看门狗被置位、采样结束、溢出
当被ADC转换的模拟电压值低于低阈值或高于高阈值时,便会产生中断。阈值的高低值由ADC_LTR
和ADC_HTR
寄存器配置,防止读取到的电压值超量程或者低于量程。
规则和注入通道转换结束后会产生DMA请求,用于将转换好的数据传输到内存。
DMA与单次转换模式:在这种模式下,每当有新的转换数据字可用时,ADC都会生成一个DMA传输请求,并且一旦DMA传输到最后一个数据时,ADC就会停止生成DMA请求,即使转换已经再次启动。当DMA传输完成时:
ADC数据寄存器内容被冻结
任何正在进行的转换将被中止,其部分结果将被丢弃
没有向DMA发送新的请求,若仍启动了转换,这将避免溢出错误(overrun error)
扫描序列停止和复位
DMA复位
DMA与连续转换模式:在这种模式下,每当数据寄存器中有一个新的转换数据字可用时,ADC就会生成一个DMA传输请求,即使DMA已经到达了最后一次DMA传输。 这允许DMA以循环模式配置,以处理连续模拟输入数据流。
注: 只有ADC1和ADC3拥有DMA功能。由ADC2转化的数据可以通过双ADC模式,利用ADC1的DMA功能传输。(L0系列只有ADC1)
MCU:
STM32L071KB
// 轮询方式开启ADC (单次转换模式每次读完都需要重新开启)
HAL_ADC_Start(ADC_HandleTypeDef* hadc);
// 轮询方式关闭ADC
HAL_ADC_Stop(ADC_HandleTypeDef* hadc);
// 轮询方式等待规则组转换结束
HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);
// 轮询方式等待外部事件触发
HAL_ADC_PollForEvent(ADC_HandleTypeDef* hadc, uint32_t EventType, uint32_t Timeout);
ADC数值读取(适用于轮询和中断)
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc);
ADC校准函数
AL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc, uint32_t SingleDiff)
SingleDiff:单端或差分输入选择;可选参数只有1个:ADC_SINGLE_ENDED
单端
Clock Prescaler
时钟分频系数:同步时钟 / 2Resolution
分辨率:12位Data Alignment
数据对齐方式:右对齐Scan Direction
扫描方向:向前Continuous Conversion Mode
连续转换模式:不使用Discontinuous Conversion Mode
间断(不连续)模式:不使用DMA Continuous Requests
DMA连续请求:不使用End Of Conversion Selection
结束转换选择:结束单次转换Overrun behaviour
溢出行为:保留溢出数据Low Power Auto Wait
低电压自动等待:不使用Low Frequency Mode
低频率模式:不使用Auto off
自动关闭:不使用Oversampling Mode
过采样模式:不使用ADC规则转换模式(ADC_Regular_ConversionMode
)
Sampling Time
采样时间:1.5个周期External Trigge rConversion Source
外部触发转换源:由软件启动定时器转换External Trigge rConversion Edge
外部转换信号触发边沿:无WatchDog
模拟看门狗
Enable Analog WatchDog Mode
使能模拟看门狗:不使能uint16_t adc_read(ADC_HandleTypeDef* hadc)
{
uint32_t adc_value = 0;
for (int i = 0; i < 8; i++)
{
HAL_ADC_Start(hadc);
HAL_ADC_PollForConversion(hadc, 50);
adc_value += HAL_ADC_GetValue(hadc);
}
return adc_value / 8;
}
单次转换模式,每次启动ADC转换一次数据,启动转换后等待转换完成,完成后读取数值,ADC转换会自动停止,下次采集需再次手动开启。
hadc.Init.ContinuousConvMode = ENABLE;
uint16_t adc_read(ADC_HandleTypeDef* hadc)
{
uint32_t adc_value = 0;
HAL_ADC_Start(hadc);
for (int i = 0; i < 8; i++)
{
HAL_ADC_PollForConversion(hadc, 50);
adc_value += HAL_ADC_GetValue(hadc);
}
HAL_ADC_Stop(hadc);
return adc_value / 8;
}
连续转换模式,只要启动ADC后就会不停的工作,读取完数值可采用HAL_ADC_Stop
函数手动停止ADC。(当然也可以不停止,不过这不满足低功耗设计要求)
多通道扫描模式是默认开启的,同时使用连续转换。在扫描模式下,会先采集CH8,然后采集CH9(前向扫描);又因为使能了连续转换模式,所以在本轮序列采集完后,会自动从CH8继续开始采集。
// PB0-RP2 ------> ADC_IN8
// PB1-RP1 ------> ADC_IN9
uint16_t adc_value[2];
void adc_read(ADC_HandleTypeDef* hadc)
{
HAL_ADC_Start(hadc);
if(HAL_ADC_PollForConversion(hadc, 20) == HAL_OK)
adc_value[0] = HAL_ADC_GetValue(hadc); // ADC_IN8
if(HAL_ADC_PollForConversion(hadc, 20) == HAL_OK)
adc_value[1] = HAL_ADC_GetValue(hadc); // ADC_IN9
HAL_ADC_Stop(hadc);
}
但这是有问题的,最后OLED显示结果(两数值均为RP1采集值):
RP2 = adc_value[0] * 3.3f / 4095.f;
RP1 = adc_value[1] * 3.3f / 4095.f;
下面分析源码:
HAL_ADC_PollForConversion
在没有使能DMA情况下(CFGR1
的DMAEN
位没置位),则tmp_Flag_EOC = (ADC_FLAG_EOC | ADC_FLAG_EOS)
,其EOC为单次转换结束标志,EOS为转换序列结束标志,即需要等所有通道转换结束后才会结束等待,返回HAL_OK
。
HAL_ADC_GetValue
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc)
{
return hadc->Instance->DR;
}
DR
寄存器为最后一个转换通道的转换结果。即此处ADC_CH9通道的数值,所以最后采集的数值均为RP1电位器的ADC转换数值。
【结论】STM32L0系列HAL中ADC轮询方式无法实现多通道采集。
那么直接操作寄存器吧:
void adc_read(ADC_HandleTypeDef* hadc)
{
hadc->Instance->CR |= ADC_CR_ADSTART;
while ((hadc->Instance->ISR & ADC_FLAG_EOC) == 0);
adc_value[0] = hadc->Instance->DR; // ADC_IN8
while ((hadc->Instance->ISR & ADC_FLAG_EOS) == 0);
adc_value[1] = hadc->Instance->DR; // ADC_IN9
hadc->Instance->CR |= ADC_CR_ADSTP; // 仅在ADC_CR_ADSTART=1且ADDIS=0时写入有效
}
虽然可以正常读取,但有时RP1的数值会跳变为RP2的数值,原因就是ADC转换太快了,可能执行到adc_value[0] = hadc->Instance->DR
时,ADC单次扫描已结束了,DR寄存器为ADC_IN9的值。
后面采用DMA+连续扫描模式解决。
// 中断方式开启ADC
HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
// 中断方式关闭ADC
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);
ADC中断和DMA方式触发中断的回调函数:
// 转换完成中断触发/DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc);
int main(void){
[...]
HAL_ADC_Start_IT(&hadc);
while (1){
[...]
}
}
ADC转换完成回调函数
ADC_HandleTypeDef* hadc1 = &hadc; // 全局变量与局部变量同名...
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc == hadc1)
{
adc_value[0] = HAL_ADC_GetValue(hadc);
}
}
但最后发现板子卡死了,在ADC回调函数和ADC中断来回跳转,原因是使能了连续模式,单次转换完成后,又继续开始转换,导致ADC中断不断触发、回调,而ADC单次转换周期us级别,CPU主频也才30M,根本来不及跳转到main中执行,造成了“卡死”现象。
关闭连续模式后,也不能在回调函数中再次启动ADC转换:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc == hadc1)
{
adc_value[0] = HAL_ADC_GetValue(hadc);
HAL_ADC_Start_IT(hadc);
}
}
这样跟连续模式没区别,刚触发又开始转换,一样“卡死”。
【解决方法】
uint8_t adc_flag = false;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc == hadc1)
{
adc_value[0] = HAL_ADC_GetValue(hadc);
adc_flag = true;
}
}
ADC回调函数中将标志置位后,在后台系统(main函数)中运行,根据系统tick时间每50ms判断一次:
void adc_test(void)
{
float adc1_vol, adc2_vol;
if (HAL_GetTick() % 50 == 0 && adc_flag)
{
adc_flag = false;
adc2_vol = adc_value[0] * 3.3f / 4095.f;
adc1_vol = adc_value[1] * 3.3f / 4095.f;
snprintf(oled_buf, sizeof(oled_buf), "RP1: %.2fV ", adc1_vol);
OLED_ShowString(0, 0, (uint8_t *)oled_buf, 16);
snprintf(oled_buf, sizeof(oled_buf), "RP2: %.2fV ", adc2_vol);
OLED_ShowString(0, 2, (uint8_t *)oled_buf, 16);
HAL_ADC_Start_IT(&hadc);
}
}
adc_test
在main中调用。
中断方式多通道读取问题与轮询方式一样,但是每次读取的是CH8通道的值(RP2),应该是扫描时CH8转换完成后,置位了EOC标志,触发了中断。这次换个方法解决吧。
利用ADC_CHSELR
ADC通道选择寄存器**(只有当ADSTART=0
才能写入**,该标志每次转换完成硬件会清除):
uint8_t adc_flag = false;
uint8_t channel_switch_flag = false;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc == hadc1)
{
if (channel_switch_flag)
{
adc_value[1] = HAL_ADC_GetValue(hadc); // ADC_IN9
hadc->Instance->CHSELR = 1 << 8;
}
else
{
adc_value[0] = HAL_ADC_GetValue(hadc); // ADC_IN8
hadc->Instance->CHSELR = 1 << 9;
}
channel_switch_flag = ~channel_switch_flag;
adc_flag = true;
}
}
注意:同样需要关闭连续转换模式,不然会一直触发中断,呈现“卡死”现象。
// DMA方式开启ADC和DMA传输
HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
// DMA方式关闭ADC和DMA传输
HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);
pData
指向buffer缓冲区Length
为总采集次数(对于多通道,每个通道计一次采集次数)DMA方式触发中断的回调函数:
// 转换完成中断触发/DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc);
// DMA半传输完成回调
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc);
配置DMA为普通模式、存储器自增、半字传输(12bit分辨率ADC):
首先启动DMA传输:
int main(void){
[...]
HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 1);
while (1){
[...]
}
}
DMA传输完成回调函数:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc == hadc1)
{
adc_flag = 1;
}
}
标志置位后需要再次启动DMA:
void adc_test(void)
{
float adc1_vol, adc2_vol;
if (adc_flag)
{
adc_flag = false;
adc2_vol = adc_value[0] * 3.3f / 4095.f;
adc1_vol = adc_value[1] * 3.3f / 4095.f;
[...]
HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 1);
}
}
注意:单次转换length参数只能为1,否则会导致DMA没有传输完成,不会进入回调函数。
HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);
均值滤波:
uint16_t get_adc_average(uint16_t *pbuf, uint16_t num)
{
uint32_t sum = 0;
for (int i = 0; i < num; i++)
sum += pbuf[i];
return sum / num;
}
转换成电压值:
void adc_test(void)
{
float adc1_vol, adc2_vol;
if (adc_flag)
{
adc_flag = false;
adc2_vol = get_adc_average(adc_value, 10) * 3.3f / 4095.f;
[...]
HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);
}
}
扫描模式是默认开启的,同时必须开启连续模式,否则采集一轮结束后不会继续采集,这样就不会触发DMA中断进入回调函数了。连续扫描模式下,多通道会根据length数量循环采集各通道,如ch8和ch9两通道,length为10的话,则adc_value[0][2][4][6][8]存放的就是ch8通道的采样值,adc_value[1][3][5][7][9]数组位置存放的是ch9的值:
修改一下滤波函数:
void get_adc_average(uint16_t *pbuf, uint16_t num)
{
uint32_t odd_sum = 0, even_sum = 0;
for (int i = 0; i < num; i++)
{
if (i & 0x01) // 奇数
odd_sum += pbuf[i];
else
even_sum += pbuf[i];
}
adc1_val = 2 * odd_sum / num;
adc2_val = 2 * even_sum / num;
}
转换成电压值:
void adc_test(void)
{
float adc1_vol, adc2_vol;
if (adc_flag)
{
adc_flag = false;
adc1_vol = adc1_val * 3.3f / 4095.f;
adc2_vol = adc2_val * 3.3f / 4095.f;
[...]
HAL_ADC_Start_DMA(&hadc, (uint32_t *)adc_value, 10);
}
}
实验发现用DMA方式采集ch8和ch9通道,两通道会相互干扰,比如ch8在0~3.3V变化,则ch9会随之在0~0.26V间浮动。
查询资料发现:采样周期越长通道间的相互干扰就越小,反之则越大。
因此,将采样周期改为最大:
最后经过测试,ch8在0~3.3V变化时,ch9不再受影响,成功解决该问题。
HAL_ADC_Start
函数开始转换,而对于连续转换,只要初始化调用一次HAL_ADC_Start
函数,之后就会自动转换;对于多通道,HAL库要等待转换序列结束标志EOS置位,因此每次读取的都是最后一个通道的数值,最后直接使用寄存器编程解决了此问题,但是数值有跳变现象。HAL_ADC_Start_IT(&hadc);
,导致ADC中断不断触发,并且触发周期是us级别的,产生“卡死”假象,最后只能选择使用单次转换模式,同时在回调函数中设置转换完成标志,在后台系统main中再次开启ADC中断转换;对于多通道采集与轮询模式问题一致,但每次读取的都是第一个通道的数据,最后在ADC回调函数中用来回切换通道的方式解决了该问题,且ADC采集数值比较稳定,也不存在两通道干扰的现象。参考:
END