在开始之前,有必要带大家复习一下使用ADC采集电池电压的原理和流程。
STM32F108C8T6这款芯片属于数字芯片,要想它们能够读取外界的数据,并转换成数字语言,必然要经过从真实数据-->数字语言的转换,这就需要用到单片机的ADC模块(Analog to Digital Converter,模数转换器)。
那么在开始之前,我们有必要了解一下ADC是怎么工作的。C8T6这款芯片的ADC是逐次逼近型,没错,就和我们高中数学使用的二分法有异曲同工之妙,或者说,就是和我们学C语言最开始创造的简单小游戏“猜数字”差不多。当外界向单片机输入一个电压信号,需要使用ADC将这个数据转换成数字语言,让我们得芯片能大概读出这个电压的数据。
比如,当我们向单片机发送一个2.0V的电压信号,但是单片机不知道2,0V的电压是什么概念,于是ADC就派用场了。ADC具有一个参考电压,和我们单片机上面的VREF+引脚电压相同,也就是3.3V。ADC的逐次逼近型操作会把数据分割成12份,比如000000000000(此时将这个信号转换成0,发送给单片机。)由于将3.3V的标准电压数据分割成12位,所以ADC会进行12次逐次逼近,每进行一次就在一位空格上面输入1或者0 ,最后通过二进制转换成十进制。因此,我们的测量范围就是0~4095,一共4096个数字。为什么是4096,因为一共十二位,2^12=4096 。一份就是约莫0.8mv。
现在简要阐述原理。比如ADC内部定义当真实数据比它猜的数据大,就写1,否则写0 。在这个原理之上,我们模拟一下ADC转换电压的流程。SAR寄存器负责猜数字。 SAR:1.8V 经过比较之后,2.0V>1.8V,写1
于是SAR往大的猜 SAR:2.4V 比较之后,2.0V<2.4V,写0
一直这样重复12次,数据逐次逼近2.0V
于是诞生了一串二进制代码。 比如:100110110010 = 2482 然后ADC把数据发送给单片机。2482就是ADC模块经过逐次逼近得到的最靠近2.0V的数据。
那么,当单片机读取到2482这个数据,该怎么转换成电压呢? 我们需要进行数学运算。 由于一份大概是0.8mv,我们前面分了4096份,现在软件读取到一个2482的数值,该怎么转换成电压呢?很简单,用2482*0.8mv是不是就得到了正确的电压数据……吗?
我们要重新思考一下,我们实验测试的电源来自铁头山羊的平衡车开发套件,两节电池最大电压大概是8.4V,远超过单片机引脚最大电压3.3V,这么直接测,难道不会爆炸吗?答案是不会,电路板制造者早就想到了这个问题,于是他们在内部设置了分压电路。
因此就有了电压换算比这个东西,当我们使用2482*0.8mv 之后,还需要再乘一个分压比的倒数,才能换算成真实的电压。
至于分压比是多少,我这里直接给出开发套件包的电路板的电阻分压比。
分压比θ=0.393
进而我们可以得到最终的公式 V(Bat)=V(ADC)1/θ 展开后也就是 V(Bat)= 24820.81/0.393 为了更加严谨,我们把分成4096份的过程也写上去,也就是用3.3/4095替换掉原式中的0.8。因为并不能整除。 V(Bat)= 2482(3.3/4095)*(1/0.393) 很显然,这和铁头山羊的源码是一样的,只不过根据数据格式,需要在浮点型数据后面加上f。
vbat = jdr1 / 4095.0f *3.3 * 8.4f / 3.3;
//代码中的用8.4/3.3,其实是通过分压原理,V之比等于R之比的原理,用电压最大值和3.3V之比表示分压比的倒数而已。
//代码中的jdr1,是用来存储Vadc的数据的。
下面是app_bat.c和app_bat.h的Hal库源码,感兴趣的可以自行阅读
#include "app_bat.h"
extern ADC_HandleTypeDef hadc1;
extern TIM_HandleTypeDef htim2;
static volatile float vbat = 0.0f;
static uint32_t lastTime = 0;
static uint8_t stage = 0;
void App_Bat_Init(void)
{
HAL_TIM_Base_Start_IT(&htim2);
HAL_ADCEx_InjectedStart_IT(&hadc1);
}
float App_Bat_Get(void)
{
return vbat;
}
void App_Bat_Proc(void)
{
if (vbat > 7.9f)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_SET);
}
else if (vbat > 7.4f)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 | GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET);
}
else if (vbat > 7.0f)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_RESET);
}
else if (vbat > 6.5f)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_RESET);
}
else
{
uint32_t now = HAL_GetTick();
if (now - lastTime > 100)
{
if (stage == 0)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_SET);
stage = 1;
}
else
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6, GPIO_PIN_RESET);
stage = 0;
}
lastTime = now;
}
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
HAL_ADCEx_InjectedStart_IT(&hadc1);
}
}
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
uint16_t jdr1 = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1);
vbat = jdr1 / 4095.0f * 3.3f / 0.248f;
}
}
#ifndef APP_BAT_H
#define APP_BAT_H
#include "stm32f1xx_hal.h"
void App_Bat_Init(void);
float App_Bat_Get(void);
void App_Bat_Proc(void);
#endif
感谢大家阅读到这里。