48脚 STM32HAL库ADC 高精度采集电池电量与芯片内部温度方法 (使用内部参考电压方案)

主控芯片:STM32L051C8T6

采用内部1.2V参考电压,解决了测量电池电量小于3.0V以下检测不准的问题。篇幅有点长,还需要各位看官慢慢阅读。

基本上可以做到1.8V~5V之间的精准检测电池电量。

1、原理图

            

2、在这先普及一个概念

在使用STM32进行ADC采集处理的时候,需要设计到参考电压的选取问题,关于模拟部分,牵扯到参考电压的引脚为:
1.100引脚以下的芯片,ADC参考电压引脚VREF+在单片机内部和VDDA引脚连接。
2.100引脚以上的芯片,ADC参考电压引脚VREF+和VDDA是分开的。
所以,在100引脚以上的芯片中,我们可以是采用单独供电,改变ADC的参考电压。但是在100引脚以下的芯片中,就必须是看VDDA的电压值范围。

查看数据手册,可以发现,VDDA为所有的模拟电路部分供电,包括:ADC模块,复位电路,PVD(可编程电压监测器),PLL,上电复位(POR)和掉电复位(PDR)模块,控制VBAT切换的开关等。即使不 使用ADC功能,也需要连接VDDA,强烈建议VDD和VDDA使用同一个电源供电。VDD与VDDA之间的电压差不能超过300mV,VDD与VDDA 应该同时上电或调电。

所以,不管是任何引脚的STM32芯片,我们必须将VDDA和VDD引脚进行连接,所以,也就是说,100引脚以下的STM32的ADC参考电压是3.3V(固定的),没办法改变的。

VREF+在硬件上是必须 大于 2.5V,当此引脚电平值不正确时,STM32单片机也不能正常工作。
今天在调试STM32 的OTG板子的时候,出现的问题就是这个原因。

3、通过查看STM32L051C8T6中文数据手册中301页,第14.10 小节

14.10 温度传感器和内部参考电压

温度传感器可用于测量器件的结温 (TJ)。温度传感器在内部连接到 ADC_IN18 输入通道,该
通道用于将传感器输出电压转换为数字值。温度传感器模拟引脚的采样时间必须大于数据手
册中指定的最小 TS_temp 值。不使用时可将传感器置于掉电模式。
内部参考电压 (VREFINT) 为 ADC 和比较器提供了一个稳定的(带隙基准)电压输出。
VREFINT 内部连接到 ADC_IN17 输入通道。 VREFINT 的精确电压由 ST 在生产测试期间对每
部分单独测量,并存储于系统存储区。访问模式为只读。
图 53 显示的是温度传感器、内部参考电压与 ADC 之间连接的方框图。
必须将 TSEN 位置 1 才能使能 ADC_IN18(温度传感器)的转换,必须将 VREFEN 位置 1
才能使能 ADC_IN17 (VREFINT) 的转换。
温度传感器的输出电压随温度线性变化。由于工艺不同,该线的偏移量取决于各个芯片(芯
片之间的温度变化可达 45 °C)。
未校准的内部温度传感器更适用于对温度变量而非绝对温度进行测量的应用。为提高温度传
感器测量的准确性, ST 在生产过程中将校准值存储在每个器件的系统存储器中。
在制造过程中,会将温度传感器的校准数据和内部参考电压存储在系统存储区。随后,用户
应用可读取这些数据,并使用这些数据提高温度传感器或内部参考的准确性。其他相关信
息,请参见数据手册。

.
.
.

使用内部参考电压计算实际的 VDDA 电压
施加给微控制器的 VDDA 电源电压可能会有变化,或无法获得准确值。在制造过程中由 ADC
在 VDDA = 3 V 的条件下获得的内置内部参考电压 (VREFINT) 及其校准数据可用于评估实际
的 VDDA 电压水平。
以下公式可求得为器件供电的实际的 VDDA 电压:
      VDDA = 3 V x VREFINT_CAL / VREFINT_DATA
其中:
. VREFINT_CAL 是 VREFINT 校准值
. VREFINT_DATA 是由 ADC 转换得到的实际 VREFINT 输出值
将电源相关的 ADC 测量值转换为绝对电压值
ADC 用于提供对应于模拟电源与施加给转换通道的电压之比的数字值。对于大部分应用用
例,需要将该比值转换成与 VDDA 无关的电压。对于 VDDA 已知、 ADC 转换值进行了右对齐
的应用,可使用以下公式得到该绝对值:
      VCHANNELx = VDDA * ADC_DATAx / FULL_SCALE 
对于 VDDA 值未知的应用,必须使用内部参考电压, VDDA 可替换为使用内部参考电压计算
实际的 VDDA 电压部分提供的表达式,从而得出以下公式:
      VCHANNELx = 3V * VREFINT_CAL * ADC_DATAx / VREFINT_DATA * FULL_SCALE
其中:
. VREFINT_CAL 是 VREFINT 校准值
. ADC_DATAx 是由 ADC 在通道 x 上测得的值(右对齐)
. VREFINT_DATA 是由 ADC 转换得到的实际 VREFINT 输出值
. full_SCALE 是 ADC 输出的最大数字值。例如,如果分辨率为 12 位,该值为
    212 - 1 = 4095,如果分辨率为 8 位,该值为 28 - 1 = 255。

注: 如果执行 ADC 测量时使用的是输出格式而非 12 位右对齐格式,那么必须先将所有参数转换
为兼容格式,然后再进行计算。

 通过上面讲解得出一个结论,这个参考电压的典型值是1.20V,最小值是1.16V,最大值是1.24V。这个电压基本不随外部供电电压的变化而变化。

4、ADC通道转换模式的理解

STM32的ADC有单次转换和连续转换2种模式,这两种模式又可以选择是否结合扫描模式。

单通道:

(1)CONT=0,SCAN=0   单次转换模式  (CONT为连续转换使能位,SCAN为扫描模式使能位)

举例:ADC1,通常规则通道的顺序为CH0,CH1,CH2,CH3;那么这种模式就是只转换这4个通道其中的一个通道,转换完成后,就停止转换。等待ADC的下一次启动

(2)CONT=1,SCAN=0   单次连续转换模式

举例:ADC1,通常规则通道的顺序为CH0,CH1,CH2,CH3;那么这种模式就是只转换这4个通道其中的一个通道,连续转换扫描这一个通道。

多通道:

(3)CONT=0,SCAN=1   多通道扫描转换模式

举例:ADC1,通常规则通道的顺序为CH0,CH1,CH2,CH3;那么这种模式就是以上的这4个通道,依次从CH0开始转换,转换完成后又开始转换CH1,直到所有的ADC规则通道序列都扫描转换一次,最后就停止转换。等待ADC的下一次启动

(4)CONT=1,SCAN=1   多通道连续扫描转换模式

举例:ADC1,通常规则通道的顺序为CH0,CH1,CH2,CH3;那么这种模式就是以上的这4个通道,依次从CH0开始转换,转换完成后又开始转换CH1,直到所有的ADC规则通道序列都扫描转换一次后,再从第一个CH0通道循环。连续扫描一组

PS:一般多通道采集都会结合DMA来传输数据,不会使用中断来传输数据,以此,来节约CPU资源的占用。

5、STM32CubeMx工具的配置

6、代码

main.c文件 
 /* USER CODE BEGIN 2 */
	
	
	HAL_TIM_Base_Start_IT(&htim2);
	
	HAL_GPIO_WritePin(GPIOB, POWER_ON_Pin, GPIO_PIN_SET);	//开机
	
	HAL_GPIO_WritePin(GPIOB, BAT_EN_Pin, GPIO_PIN_SET);		//使能检测电池电压

	HAL_ADCEx_Calibration_Start(&hadc, ADC_SINGLE_ENDED);
	
	HAL_ADC_Start_DMA(&hadc,(uint32_t*) &adc1_val_buf, (ADC_CHANNEL_CNT*ADC_CHANNEL_FRE));
	
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	
	  getBatVoltage();
	  getChipInTempVal();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

		HAL_Delay(1000);
		HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_2);  	//LED
  }
  /* USER CODE END 3 */
adc.h文件

/* USER CODE BEGIN Includes */

#define ADC_CHANNEL_CNT 3 	//采样通道数
#define ADC_CHANNEL_FRE 100	//单个通道采样次数,用来取平均值


extern uint32_t adc1_val_buf[ADC_CHANNEL_CNT*ADC_CHANNEL_FRE]; //传递给DMA存放多通道采样值的数组
extern uint32_t adc1_aver_val[ADC_CHANNEL_CNT]; //保存多通道的平均采样值的数组
extern uint32_t dma_cnt1;



/* USER CODE END Includes */


/* USER CODE BEGIN Prototypes */

uint8_t getBatVoltage(void);
uint16_t getChipInTempVal(void);
void get_ADC_Channel_Val(void);

/* USER CODE END Prototypes */
adc.c文件

/* Includes ------------------------------------------------------------------*/
#include "adc.h"

/* USER CODE BEGIN 0 */
#include "stdio.h"
#include "math.h"
#include "tim.h"

#ifdef 	ADC_MULTICHANNEL_DMA

//对于12位的ADC,3.3V的ADC值为0xfff,温度为25度时对应的电压值为1.43V即0x6EE
#define V25  0x6EE
//斜率 每摄氏度4.3mV 对应每摄氏度0x05
#define AVG_SLOPE 0x05

uint32_t adc1_val_buf[ADC_CHANNEL_CNT*ADC_CHANNEL_FRE] = {0}; //传递给DMA存放多通道采样值的数组
uint32_t adc1_aver_val[ADC_CHANNEL_CNT] = {0}; //保存多通道的平均采样值的数组
uint8_t dma_cnt1 = 0;

#endif


#ifdef 	ADC_SINGLE_CHANNEL

#endif


/* USER CODE END 0 */

ADC_HandleTypeDef hadc;
DMA_HandleTypeDef hdma_adc;

/* ADC init function */
void MX_ADC_Init(void)
{
  ADC_ChannelConfTypeDef sConfig = {0};

  /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) 
  */
  hadc.Instance = ADC1;
  hadc.Init.OversamplingMode = DISABLE;
  hadc.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2;
  hadc.Init.Resolution = ADC_RESOLUTION_12B;
  hadc.Init.SamplingTime = ADC_SAMPLETIME_160CYCLES_5;
  hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD;
  hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc.Init.ContinuousConvMode = ENABLE;
  hadc.Init.DiscontinuousConvMode = DISABLE;
  hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc.Init.DMAContinuousRequests = ENABLE;
  hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc.Init.LowPowerAutoWait = DISABLE;
  hadc.Init.LowPowerFrequencyMode = DISABLE;
  hadc.Init.LowPowerAutoPowerOff = DISABLE;
  if (HAL_ADC_Init(&hadc) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure for the selected ADC regular channel to be converted. 
  */
  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure for the selected ADC regular channel to be converted. 
  */
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure for the selected ADC regular channel to be converted. 
  */
  sConfig.Channel = ADC_CHANNEL_VREFINT;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

}

void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(adcHandle->Instance==ADC1)
  {
  /* USER CODE BEGIN ADC1_MspInit 0 */
		
  /* USER CODE END ADC1_MspInit 0 */
    /* ADC1 clock enable */
    __HAL_RCC_ADC1_CLK_ENABLE();
  
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**ADC GPIO Configuration    
    PA1     ------> ADC_IN1 
    */
    GPIO_InitStruct.Pin = BAT_ADC_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(BAT_ADC_GPIO_Port, &GPIO_InitStruct);

    /* ADC1 DMA Init */
    /* ADC Init */
    hdma_adc.Instance = DMA1_Channel1;
    hdma_adc.Init.Request = DMA_REQUEST_0;
    hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_adc.Init.Mode = DMA_CIRCULAR;
    hdma_adc.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_adc) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc);

    /* ADC1 interrupt Init */
    HAL_NVIC_SetPriority(ADC1_COMP_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_COMP_IRQn);
  /* USER CODE BEGIN ADC1_MspInit 1 */
		
		
  /* USER CODE END ADC1_MspInit 1 */
  }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{

  if(adcHandle->Instance==ADC1)
  {
  /* USER CODE BEGIN ADC1_MspDeInit 0 */

  /* USER CODE END ADC1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_ADC1_CLK_DISABLE();
  
    /**ADC GPIO Configuration    
    PA1     ------> ADC_IN1 
    */
    HAL_GPIO_DeInit(BAT_ADC_GPIO_Port, BAT_ADC_Pin);

    /* ADC1 DMA DeInit */
    HAL_DMA_DeInit(adcHandle->DMA_Handle);

    /* ADC1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(ADC1_COMP_IRQn);
  /* USER CODE BEGIN ADC1_MspDeInit 1 */
		
  /* USER CODE END ADC1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */

uint16_t getChipInTempVal(void)		//获取芯片内部温度
{
	uint16_t tempVal = 0;
	tempVal = (V25-adc1_aver_val[2])/AVG_SLOPE+25;
	
	printf("adc1_aver_val[2]: %d, Temp:%3d ℃ \r\n",adc1_aver_val[2],tempVal);
	return tempVal;
}


uint16_t getBatVoltage(void)				
{
	uint8_t i = 0;
	uint16_t BATVAL = 0;
	float VDDA = 0;
	float VCHANNELx = 0;	//实际测量的电池电量值
	__IO uint16_t VREFINT_CAL = 0;  
	VREFINT_CAL = *(__IO uint16_t *)(0x1FF80078); //得到一个16进制的校准值
	
	printf("\n\r ***** START PRINTF ADC INFO ******** \r\n\n");
	for(i=0;i<ADC_CHANNEL_CNT;i++)
	{
		printf("ADC1[%02d] Sampling voltage = %1.3f V Sampling value = %04d\r\n",i,adc1_aver_val[i]*3.0f/4095,adc1_aver_val[i]);
	}
	
	/*
	 * 
	 * 检测实际电池电量公式:
	 * VCHANNELx = 3V * VREFINT_CAL * ADC_DATAx / VREFINT_DATA * FULL_SCALE
	 * ADC_DATAx : 就是读ADC分压电阻的值,对应通道adc1_aver_val[0]
	 * VREFINT_DATA :是 ADC_CHANNEL_17 的值  adc1_aver_val[1]
	 * FULL_SCALE :4095
	 */
	VDDA = 3*VREFINT_CAL*1000/adc1_aver_val[1]; //扩大1000倍
	printf("VDDA:%lf \r\n",VDDA);	
	printf("VREFINT_CAL:%d\r\n",VREFINT_CAL);
	printf("VCHANNELx: %0.4f \r\n",(float)(VDDA * adc1_aver_val[0] / 4095));	
	
	VCHANNELx = (float)((300 * VREFINT_CAL * adc1_aver_val[0]) / (adc1_aver_val[1] * 4095))/100; 
	//printf("VCHANNELx: %0.4f \r\n",VCHANNELx);
	BATVAL=(VCHANNELx*3.0+0.03)*100;	//3.0是分压电阻检测ADC的系数, 0.03是跟实际少的差值100是放大一百倍
//	printf("BATVAL: %d \r\n",BATVAL);	
	printf("g_BatVoltage: %d \r\n", BATVAL);
	return BATVAL; 
	
}

void get_ADC_Channel_Val(void)
{
	uint8_t i = 0;
	//1ms进入中断
	/* 清除adc采样平均值变量 */
	for(i=0;i<ADC_CHANNEL_CNT;i++)
	{
		adc1_aver_val[i] = 0;
	}
	/* 在采样值数组中分别取出每个通道的采样值并求和 */
	for(i=0;i<ADC_CHANNEL_FRE;i++)
	{
		adc1_aver_val[0] +=  adc1_val_buf[i*3+0];
		adc1_aver_val[1] +=  adc1_val_buf[i*3+1];
		adc1_aver_val[2] +=  adc1_val_buf[i*3+2];
	}
	/* 依次对每个通道采样值求平均值 */
	for(i=0;i<ADC_CHANNEL_CNT;i++)
	{
		adc1_aver_val[i] /= ADC_CHANNEL_FRE;
	}
}

/* USER CODE END 1 */

 

这两个通道,可以使用工具goto进去看看,结果发现就是我们要使用的通道17,上面的代码基本使用STM32CubeMx工具生成的,只需添加自己逻辑与功能实现代码即可。

 远行结果:

大于3.3V以上检测的电压值

 小于3.0V以下检测的电压值

 

 这里我只测试了4.4V的电压值,不敢往上测试更高的电压值,怕烧坏产品中的LDO管子,在这种情况下,只要是带电池的设备基本都能满足。

 

源码链接

参考文章:https://blog.csdn.net/u010307522/article/details/56681013

                  https://blog.csdn.net/qq_34160496/article/details/89259793

在这分享一些HAL其他外设操作的例程文章:http://www.waveshare.net/study/article-646-1.html

发布了41 篇原创文章 · 获赞 63 · 访问量 57万+

猜你喜欢

转载自blog.csdn.net/qq_36075612/article/details/102953750
今日推荐