筆者今天來介紹一下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 內置溫度採集