笔者今天来介绍一下STM32ADC内置温度的采集,重点是通过内置参考电压来避免ADC参考电压VDDA对温度ADC采集的影响。
stm32F4系列ADC,逐次趋近型AD、12位、多达19个通道,16个外部通道、2个内部源通道和1个Vbat通道。
了解ADC原理的都知道,ADC需要一个参考电压,而STM23的参考电压是VDDA。
VDDA有可能随着供电电压发生改变(比如其他器件瞬时电流较大,导致供电电压拉低),那么会导致测到ADC值有误。所以这就有可能导致采集到的温度有误。
但是STM32内部还存在一个内部参考电压(Internal reference voltage),1.2V左右,基本不会随着供电电压发生变化,因此在测量内部温度的时候,可以通过多读取一个通道内部参考电压的ADC值来进行VDDA的校正,从而避免内部温度读取错误。
当然这里还有一个方法(避免电压参考值对采集的影响)就是:使用外部的电压参考值,只要外部参考电压稳定就可以。
注意一点的是:64脚的Vref没有引出,其Vref接到了VDDA上面,100,144,176引脚的引出了Vref。
直接读取内部温度ADC比较简单,首先需要使能内部温度传感器。
ADC_TempSensorVrefintCmd(ENABLE);
设置转换序列为1,只有一个16通道
ADC_InitStructure.ADC_NbrOfConversion = 1;
设置规则通道顺序
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles);
使能ADC,并开始转换
ADC_Cmd(ADC1, ENABLE);
ADC_SoftwareStartConv(ADC1);
这样需要在读完ADC值后,再次调用转换,然后再去读取。转换完成之后,可以通过ADC_FLAG_EOC标志位判断是否读取完成,然后将ADC值取出。
笔者这里通过开启一个定时器(1000HZ),定时转换。
void TIM5_IRQHandler(void)
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM5, TIM_IT_Update);
StartCovADCTempture(); //启动ADC转换
}
}
然后在主函数中读取温度ADC值然后转换成对应的温度值,这里有一个关于温度值的计算的参数手册。其中Vsense是采集到的温度电压值。
所以计算公式为:Tempture = (ADC*3.3/4095 - 0.76)/0.025 + 25
完整的代码如下:
void Adc_Init(void)
{
ADC_CommonInitTypeDef ADC_CommonInitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能ADC1时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,ENABLE); //ADC1复位
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,DISABLE); //复位结束
ADC_TempSensorVrefintCmd(ENABLE);//使能内部温度传感器
ADC_VBATCmd(ENABLE);
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_15Cycles;
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; //DMA失能
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; //ADCCLK=PCLK2/4=84/4=21Mhz,ADC时钟最好不要超过36Mhz
ADC_CommonInit(&ADC_CommonInitStructure);
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;//12位模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //非扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;//禁止触发检测,使用软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStructure.ADC_NbrOfConversion = 1; //1个转换在规则序列中 也就是只转换规则序列1
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles); //ADC16,ADC通道,480个周期,提高采样时间可以提高精确度
ADC_Cmd(ADC1, ENABLE);//开启AD转换器
ADC_SoftwareStartConv(ADC1); //使能ADC的软件转换启动功能
}
笔者这里获得温度之后,通过两次求均值减少温度波动。
void GetTempertureAverage()
{
u16 TemptureADC;
u16 TemptureADC2;
if(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)) //等待转换结束
{
TemptureBuff[TemptureCount++] = ADC_GetConversionValue(ADC1);
if(TemptureCount == 100)
{
TemptureCount = 0;
TemptureADC = CalAverageValueT(TemptureBuff,0,100,18); //100个数据,去头去尾各18个,留下64个
TemptureBuff2[TemptureCount2++] = TemptureADC;
if(TemptureCount2 == 20)
{
TemptureCount2 =0;
TemptureADC2 = CalAverageValueT(TemptureBuff2,0,20,2);
Tempture = (u16)((float)((float)TemptureADC2*(0.000806) - 0.76)*400 + 25) - 10; // 默认读出37-38摄氏度 -10是进行人为的修正。
rt_kprintf("%d C\r\n",Tempture);
}
}
}
}
void StartCovADCTempture() //定时器中执行、
{
ADC_RegularChannelConfig(ADC1,ADC_Channel_16,1,ADC_SampleTime_480Cycles); //ADC1,ADC通道,480个周期,提高采样时间可以提高精确度
ADC_SoftwareStartConv(ADC1); //使能指定的ADC1的软件转换启动功能
}
uint16_t CalAverageValueT(uint16_t *dealbuff,uint8_t StartPos,uint8_t AverageCount,uint8_t MaxMinCount)
{
u16 si;
uint32_t totlevalue = 0;
DataLineT(&dealbuff[StartPos], AverageCount);
for(si=0;si<(AverageCount - MaxMinCount*2);si++)
{
totlevalue = totlevalue + dealbuff[si + MaxMinCount];
}
totlevalue = totlevalue/(AverageCount - MaxMinCount*2);
return totlevalue;
}
void DataLineT(uint16_t *Din, uint8_t len)
{
uint8_t i, j;
int16_t head, part;
for(j=0;j<=len-2;j++)
{
head=Din[j];
for(i=0;i<len-j-1;i++)
{
if(head>Din[i+j+1])
{
part=head;
head=Din[i+j+1];
Din[i+j+1]=part;
}
}
Din[j]=head;
}
}
实际的结果如下:定时转换AD,然后主函数中读取数据。
设置连续模式读取后,转换完成并读取后,自动进行下一次转换,然后判断ADC_FLAG_EOC即可再次读取计算。(无需定时启动)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
如果ADC的多个通道进行采集数据,在设置完转换顺序、连续模式读取和扫描模式读取,可以通过判断标志位ADC_FLAG_EOC进行读取当前规则转换通道的数据,
但是如果MCU在其他地方比较耗时,还没来的及读取ADC数据,就被下一个通道的数据覆盖,这个时候就会产生一个溢出的错误,导致数据丢失。
而DMA是对于多个通道的读取是一个好方法,在转换完成后,自动从ADC的外设传输到内存当中,全部序列转换完成之后,传输完成标志位置1,这个时候可以去读取数据。
DMA的配置如下,采用DMA2的数据流0的0通道,采用DMA循环模式。
void ADCDMAInit()
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
DMA_DeInit(DMA2_Stream0);
DMA_InitStructure.DMA_Channel = DMA_Channel_0;
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&ADC1->DR);
DMA_InitStructure.DMA_Memory0BaseAddr = (unsigned long)&(ADCBuffer[0]);
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_BufferSize = 3;
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_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
DMA_Cmd(DMA2_Stream0, ENABLE);
}
ADC的配置,笔者这里使用DMA读取3个通道的ADC值,分别是内部温度ADC、内部参考电压ADC和VBAtADC。
ADC读取采用扫描模式和连续模式。
其中一个非常重要的函数要配置:使能DMA的持续请求。否则只会DMA传输一组数据,而不会传输下一组数据。
ADC_DMARequestAfterLastTransferCmd(ADC1,ENABLE);
void Adc_Init(void)
{
ADCDMAInit();
ADC_CommonInitTypeDef ADC_CommonInitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能ADC1时钟
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,ENABLE); //ADC1复位
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,DISABLE); //复位结束
ADC_TempSensorVrefintCmd(ENABLE);//使能内部温度传感器
ADC_VBATCmd(ENABLE);
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_15Cycles;
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; //DMA失能
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; //ADCCLK=PCLK2/4=84/4=21Mhz,ADC时钟最好不要超过36Mhz
ADC_CommonInit(&ADC_CommonInitStructure);
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;//12位模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;//禁止触发检测,使用软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//右对齐
ADC_InitStructure.ADC_NbrOfConversion =3; //1个转换在规则序列中 也就是只转换规则序列1
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles); //ADC16,ADC通道,480个周期,提高采样时间可以提高精确度
ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_480Cycles); //ADC17,ADC通道,480个周期,提高采样时间可以提高精确度
ADC_RegularChannelConfig(ADC1, ADC_Channel_18, 3, ADC_SampleTime_480Cycles); //ADC18,ADC通道,480个周期,提高采样时间可以提高精确度
ADC_DMARequestAfterLastTransferCmd(ADC1,ENABLE); //在DMA传输一次完成后,运行下一次的DMA请求
ADC_DMACmd(ADC1,ENABLE);
ADC_Cmd(ADC1, ENABLE);//开启AD转换器
ADC_SoftwareStartConv(ADC1); //使能ADC的软件转换启动功能
}
在传输完成后,通过判断DMA通道传输是否传输完成来获取数据并进行计算。
void GetMultiADCValue()
{
u16 TemptureADC;
u16 TemptureADC2;
u16 VolRefADC;
u16 VolRefADC2;
u16 VBatADC;
u16 VBatADC2;
if(DMA_GetFlagStatus(DMA2_Stream0,DMA_FLAG_TCIF0) == SET)
{
DMA_ClearFlag(DMA2_Stream0,DMA_FLAG_TCIF0);
//.........完成数据计算
}
ADCCheckOVRError();
}
当然这里也可以采用中断的方式进行读取数据(DMA传输完成中断)。
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn; //外部中断2
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //配置
void DMA2_Stream0_IRQHandler()
{
if(DMA_GetITStatus(DMA2_Stream0,DMA_IT_TCIF0) == SET)
{
ADCDMAFlag = 1;
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0);
}
}
if(ADCDMAFlag == 1)
{
ADCDMAFlag = 0;
//.........完成数据计算
}
这里在多说一句,通过设置函数规则序列传输完成标志位来进行数据处理,读者可以自行去尝试处理。
ADC_EOCOnEachRegularChannelCmd(ADC1, DISABLE);
实际读取并计算到的数据。(数据1为:温度,数据2为:内部参考电压的ADC值 ADC_Vrefint)
刚开始提到,ADC的参考电压VDDA可能受到供电电压的影响而导致测到的ADC值有所误差,从而导致计算的温度偏离实际较大,因此需要校准VDDA。
下图即为拔电电源后,数据发生突变,温度值瞬间发生较大变化。芯片有电池供电,所以程序继续运行。(数据1为:温度 C为℃的意思,数据2为:内部参考电压的ADC值 ADC_Vrefint)
通过查询手册发现VREFINT内部参考电压不会随着供电电压而发生变化,而STM32也可以进行参考电压VREFINT的ADC值采集,
我怀疑官方也是知道VDDA会受到影响而干扰ADC值的采集,特意给出内部基准电压去校正。
通过等式我们可以来校正:
公式一:ADC_Vrefint/4095 = 1.2/VDDA 内部参考电压的计算
公式二:ADC_Vtempture/4095 = Vtempture / VDDA 内置温度的计算
通过消除VDDA ,可以得到 Vtempture的公式:
公式三:Vtempture = ADC_Vtempture*1.2/ADC_Vrefint;
通过上式计算出的温度电压值,然后再代入到上面的公式,计算出的温度会稳定很多, 不会受到供电电压的干扰。
void GetMultiADCValue()
{
u16 TemptureADC;
u16 TemptureADC2;
u16 VolRefADC;
u16 VolRefADC2;
u16 VBatADC;
u16 VBatADC2;
if(DMA_GetFlagStatus(DMA2_Stream0,DMA_FLAG_TCIF0) == SET)
{
DMA_ClearFlag(DMA2_Stream0,DMA_FLAG_TCIF0);
TemptureBuff[TemptureCount] = ADCBuffer[0];
VolRefBuff[TemptureCount] = ADCBuffer[1];
VBatBuff[TemptureCount] = ADCBuffer[2];
TemptureCount++;
if(TemptureCount == 100)
{
TemptureCount = 0;
TemptureADC = CalAverageValueT(TemptureBuff,0,100,18); //100个数据,去头去尾各18个,留下64个
VolRefADC = CalAverageValueT(VolRefBuff,0,100,18);
VBatADC = CalAverageValueT(VBatBuff,0,100,18);
TemptureBuff2[TemptureCount2] = TemptureADC;
VolRefBuff2[TemptureCount2] = VolRefADC;
VBatBuff2[TemptureCount2] = VBatADC;
TemptureCount2++;
if(TemptureCount2 == 20)
{
TemptureCount2 =0;
TemptureADC2 = CalAverageValueT(TemptureBuff2,0,20,2);
VolRefADC2 = CalAverageValueT(VolRefBuff2,0,20,2);
VBatADC2 = CalAverageValueT(VBatBuff2,0,20,2);
Tempture = (u16)((float)((float)TemptureADC2*(1.21)/VolRefADC2 - 0.76)*400 + 25); // 默认读出37-38摄氏度 -10是进行人为的修正。
rt_kprintf("%d C %d\r\n",Tempture,VolRefADC2);
}
}
}
ADCCheckOVRError();
}
以下图片是经过校正的内部温度值,内部参考电压的ADC发生变化,但是温度值没有发生变化。
注意是因为VDDA发生变化,所以ADC参考电压VDDA发生变化,而内部参考电压Vrefint本身没有改变。
完整的工程下载地址:带内部参考电压(VREFINT)校正的STM32 DMA 内置温度采集