STM32Hal库平衡车1-1 理解ADC测量原理(对应课程2.1.【电池】使用ADC采集电池电压(1))

0 阅读5分钟

在开始之前,有必要带大家复习一下使用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,这么直接测,难道不会爆炸吗?答案是不会,电路板制造者早就想到了这个问题,于是他们在内部设置了分压电路。

image.png

因此就有了电压换算比这个东西,当我们使用2482*0.8mv 之后,还需要再乘一个分压比的倒数,才能换算成真实的电压。

image.png

至于分压比是多少,我这里直接给出开发套件包的电路板的电阻分压比。

分压比θ=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

感谢大家阅读到这里。