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

    技术2022-07-11  124

    笔者今天来介绍一下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 内置温度采集

    Processed: 0.019, SQL: 9