本次我们来讲一下ADC(Analog-to-Digital Converter)不是游戏里的AD Carry,我们将实现电池电压的读取。
ADC简介
ADC的全称为Analog-to-Digital Converter(模拟/数字转换器)。
在单片机中传输的信号均为数字信号,通过离散的高低电平表示数字逻辑的 1 和 0,但是在现实的物理世界中只存在模拟信号,即连续变化的信号。将这些连续变化的信号——比如热,光,声音,速度通过各种传感器转化成连续的电信号,再通过 ADC 功能将连续的模拟信号转化成离散的数字信号给单片机进行处理。 常见的ADC类型有并联比较型、逐次逼近型。它们的特点如下
ADC有两个重要参数,分辨率和转换速率。
分辨率:每一个ADC模块都会明确他的分辨率,通过bit来表示,一般的是8bit,10bit,12bit,16bit,24bit.bit越大,说明分辨率越高。
采样率:采样率就是ADC 采样的速率。它是指在规定的时间内可以采集的次数,采样率越高,采集到的点数就越多,那么对原始信号的还原率就越高。采样率的单位是SPS(sample per sencond),每秒采样次数。这个值越大,采样速度越快。
下面简单的讲一下这两种ADC的工作原理
并联比较型
这个也是比较容易理解的一种。下图为并联比较型ADC的工作原理图。
它分为分压部分、比较部分、编码部分。
一开始的一排电阻均为等阻值的电阻,它们将VREF(参考电压)进行均分,也就代表这个该ADC的精度,电阻分压后的电压(Vr)我们将其接到比较器的。图中V1为我们的待测电压,当V1Vr时比较器输出1。比如我们这个假设VREF为15V,V1为1.2V。那么在最下面的比较器中Vr为1V,V1>Vr,比较器输出1,第二个比较器中Vr为3V,V1
之后我们在每个比较器后加上D触发器,当我们把C(控制端)至1后,我们给D输入1,则Q输入1,输入0,则Q输出0。这里我们先给C1,此时比较器输出的结果就被存储到锁存器中。然后再置0,那么无论我们的输入电压V1怎样变,Q输出的值都不变,也就保存了输出值。这里我们改变控制C的速度很大程度决定了最大采样率,当然最大采样率还受比较器等器件最大频率的限制。
但是这里输出的还不是二进制数,因此我们需要后面加一个编码器进行编码。上面就是并联比较型原理的简单描述。
这里我们也可以看出,并联比较型的缺点就是精度想要提升就必须增加相应的电阻、比较器、触发器。这样会增加物料成本与体积。但是优点就是速度很快。
逐级比较型
下图为逐级比较型ADC的工作示意图,我们使用的
STM32F4也是使用的这种ADC。
它可以分为控制
电路、数码寄存器、D/A转换器、电压比较器四个部分
首先第一步在数码寄存器进行编码,将高位D2编码置1,其他位置0,之后进入D/A转换器,将编码出来的数字量转变为模拟量V0。之后V0和输入电压Vx到比较器进行比较。如果Vx大于V0,比较器输出1,那么控制电路就会将高位D2锁定不变,之后将D1置1进行第二轮比较。如果此时Vx还是大于V0我们也就说明了我们V0的值更加接近Vx。我们将D0置1继续进行第三轮比较,比如Vx还是比V0大那么说明Vx大于了ADC的量程,如果Vx
这里给出另一个图方便大家理解
与 1/2Vref 进行比较,Vin 大于 1/2Vref,则将第一位标记为 1
与 3/4Vref 进行比较,Vin 小于 3/4Vref,则将第二位标记为 0
与 5/8Vref 进行比较,Vin 小于 5/8Vref,则将第三位标记为 0。
图中的 Vin 通过这个三位的 ADC 后输出的结果为 100,转换的结果为 1/2Vref。
这里我们看出由于它需要逐次逼近因此它的转换速度是比较慢的。而且它的分辨率和采样速度相互矛盾,分辨率越高,采样速率就越低。
关于ADC相关知识好像在信号与系统课上会更深的了解,作者是物联网专业的,没学过这类课程,就不展开介绍了
CubeMX配置
虽然这里bsp文件已经将我们需要的ADC1,ADC3开启了,但是这里还是带着大家学习一遍,便于大家使用其他STM32板子时快速配置。
首先还是看原理图
这里可以看到
电源ADC引脚为PF10,使用ADC3的通道8。
我们需要开启ADC1和ADC3,ADC1在此处作用为读取STM32内部的1.2V校准电压Vrefint,这个操作在芯片内部实现没有对应引脚。
ADC1的具体配置如下
下表简单描述一下CuebMX中ADC设置中的功能。
ADC3开启通道8,具体配置如ADC1一样。
这里我们为什么需要使用ADC1的VREFINT呢?
VREFINT是ADC的内部参照电压1.2V,一般来说在STM32我们会使用Vcc作为Vref,但是实际情况中Vcc可能存在较大波动导致Vref不稳定最终使得ADC采样值不准确,因此我们使用已有的1.2V内部参考电压先行进行多次采样,计算平均值。将其与ADC采样得出的值进行对比,计算出偏移的比例,得到单位数字电压对应的模拟电压值。
程序编写
首先在RT-Thread Set
tings组件中打开ADC设备驱动程序
之后在硬件中打开ADC1与ADC3
新建一个app_adc.c文件
在使能设备之后有几点需要我们注意
我们需要采样内部参考电压1.2V200次,取平均值并计算单位数字电压对应的模拟电压值
/* 采样内部参考电压200次 */
for(int i=0;i<200;i++)
{
totalvalue+=rt_adc_read(adc_ref,ADC_REF_CHANNEL);
}
/* 计算单位数字电压对应的模拟电压值 */
voltage_vrefint_proportion=200*1.2f/totalvalue;
之后我们对ADC3通道8进行采样,这里有两个点需要注意
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
rt_kprintf("the value is :%d n", value);
/* 转换为对应电压值 */
vol=(double)value*voltage_vrefint_proportion*1009.0909090909090909090909090909f;
rt_kprintf("the voltage is :%d.%02d n", vol / 100, vol % 100);
我们重新看一下原理图,我们这里采样的是分压电路分压之后的值,因此我们需要计算出分压之前的值才为我们所需的电池电压。通过如下计算我们需要乘以10.09
并且RT-Thread中rt_kprintf是不能打印出浮点数的,如果是浮点数的话就不会打印出来,这个我之前也踩过坑,有两个办法,一是修改rt_kprintf函数实现,但是printf作为一个可重入的函数,打印浮点数是不安全的。因此我这里选择方法二,我们想要保留小数点后两位,那么我们就将值*100,之后在打印时再将值/100作为整数部分,值%100作为小数部分,如下所示。
rt_kprintf("the voltage is :%d.%02d n", vol / 100, vol % 100);
完整代码如下,实现功能为输入adc_vol_sample,输出实际读取到的转换的原始数据和经过计算后的实际电压值。
/*
Copyright (c) 2006-2021, RT-Thread Development Team
SPDX-License-Identifier: Apache-2.0
Change Logs:
Date Author Notes
2023-01-09 Goldengrandpa the first version
/
#include
#include
#define ADC_REF_NAME "adc1" /* ADC 内部参考电压设备名称 */
#define ADC_REF_CHANNEL 17
#define ADC_DEV_NAME "adc3" /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 8 /* ADC 通道 */
static int adc_vol_sample(int argc, char *argv[])
{
rt_adc_device_t adc_ref;
rt_adc_device_t adc_dev;
rt_uint32_t totalvalue;
rt_uint32_t value, vol;
double voltage_vrefint_proportion;
rt_err_t ret = RT_EOK;
/* 查找设备 */
adc_ref = (rt_adc_device_t) rt_device_find(ADC_REF_NAME);
if (adc_ref == RT_NULL)
{
rt_kprintf("adc sample run failed! can't find %s device!n", ADC_REF_NAME);
return RT_ERROR;
}
adc_dev = (rt_adc_device_t) rt_device_find(ADC_DEV_NAME);
if (adc_dev == RT_NULL)
{
rt_kprintf("adc sample run failed! can't find %s device!n", ADC_DEV_NAME);
return RT_ERROR;
}
/* 使能设备 */
ret = rt_adc_enable(adc_ref, ADC_REF_CHANNEL);
ret = rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);
/* 采样内部参考电压200次 */
for(int i=0;i<200;i++)
{
totalvalue+=rt_adc_read(adc_ref,ADC_REF_CHANNEL);
}
/* 计算单位数字电压对应的模拟电压值 */
voltage_vrefint_proportion=200*1.2f/totalvalue;
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
rt_kprintf("the value is :%d n", value);
/* 转换为对应电压值 */
vol=(double)value*voltage_vrefint_proportion*1009.0909090909090909090909090909f;
rt_kprintf("the voltage is :%d.%02d n", vol / 100, vol % 100);
/* 关闭通道 */
ret = rt_adc_disable(adc_dev, ADC_DEV_CHANNEL);
return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(adc_vol_sample, adc voltage convert sample);
硬件连接如下,这里使用的是大疆的6s电池,输出电压24V。
由于电池基本要没电了因此计算出来的电压为22.39V偏小属于正常现象。
原作者:goldengrandpa