帶內部參考電壓(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 內置溫度採集

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