带内部参考电压(VREFINT)校正的STM32 DMA 内置温度采集

笔者今天来介绍一下STM32ADC内置温度的采集,重点是通过内置参考电压来避免ADC参考电压VDDA对温度ADC采集的影响。
1、STM32ADC简介

  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。
在这里插入图片描述

2、直接读取内部温度ADC

直接读取内部温度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,然后主函数中读取数据。

在这里插入图片描述

3、连续模式读取内部温度ADC

设置连续模式读取后,转换完成并读取后,自动进行下一次转换,然后判断ADC_FLAG_EOC即可再次读取计算。(无需定时启动)

ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;

4、DMA读取内部温度和内部参考电压ADC

  如果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)
在这里插入图片描述

5、内部参考电压校准的内置温度数据

  刚开始提到,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 内置温度采集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章