ADC使用
# ADC
ADC(Analog to Digital Converter)是模数转换功能。在单片机中传输的信号均为数字信号,通过离散的高低电平表示数字逻辑的1和0,但是在现实的物理世界中只存在模拟信号,即连续变化的信号。将这些连续变化的信号——比如热,光,声音,速度通过各种传感器转化成连续的电信号,再通过ADC功能将连续的模拟信号转化成离散的数字信号给单片机进行处理。
本节将使用 ADC 测量 STM32 的电源电压与温度
# 原理介绍
ADC 的工作流程主要有三步:
- 采样:是指对某一时刻的模拟电压进行采集
- 比较:是指将采样的电压在比较电路中进行比较
- 转换:是指将比较电路中结果转换成数字量
# 比较与转换
采样是比较简单的一步,而比较与转换的方法,STM32 采用的是逐次逼近法,在STM32F4
中是12位逐次逼近型ADC (SAR-ADC) ,下面以一个信号在3位逼近法中的比较过程为例讲解比较过程
首先你要知道的是,在3位逼近法中,可以认为ADC在未转换之前的值是一个3位二进制数,这3位二进制数字存储的数值取决于这3位比较的出来的值:
采样到模拟信号的值之后:
- 首先与内部参考电压 Vref 的 1/2 进行比较。发现大于其值,则将第一个标志位记为1;反之为0
- 由于大于 1/2 Vref 值,所以下一个比较的值为 1/2 + 1/4 = 3/4 Vref
- 然后与Verf 的 3/4 进行比较。发现小于其值,则将第二个标志位记为0
- 由于小于 3/4 Vref 值,所以下一个比较的值为 3/4 - 1/8 = 5/8 Vref
- 然后与Vref 的 5/8 进行比较。发现小于其值,则将第三个标志位记为0
所以输出的的结果为100
,其对照的结果为 1/2 Vref。如果是12位逼近的方法,这样的过程需要经过12次,输出一串12位的二进制数,然后转化为数值,其完整流程如下:
一般 ADC 的位数越多则转换精度越高,但与此同时转换的速度也会变慢。此外,STM32 内部有一个校准电压VREFINT ,电压为1.2 V,当供电电压不为 3.3 V,可以使用内部的 VREFINT 通道采集1.2 V电压作为 Vref,以提高精度
明确两个概念
VREFINT 是 ADC 的内部参考电压,而 Vref 是在 ADC 进行读取的时候采取的标准电压值
一般来说STM32的ADC采用 Vcc 作为 Vref,但为了防止 Vcc 存在波动较大导致 Vref 不稳定,进而导致采样值的比较结果不准确,STM32 可以通过内部已有的参照电压 VREFINT 来进行校准,接着以 VREFINT 为参照来比较 ADC 的采样值,从而获得比较高的精度,VREFINT的电压为1.2 V
至于如何校准,请见下面的算法
# 电路结构
在开发板中有一个用于读取电池电压使用的电阻分压电路。由于电池提供的电源是24 V的高电压,而单片机引脚的耐压只有0~3.3 V,所以需要通过分压电路进行处理,并使用滤波和二极管限幅电路进行保护
该电路使用一个200 kΩ的电阻与一个22 kΩ的电阻进行分压,由图可见,ADC 采样接口的点位与两个电阻中间的电位相同,由于一点接地,得到其电位大概为:
可以得到分压后的电压大约2.38 V,然后将该电压送至次级电路,在次级电路中,首先通过一个100 nF的电容进行滤波,使输出的电压更加稳定,接着用二极管保护电路将电压限制在3.3 V和0 V之间,当电压大于3.3 V时,二极管正向导通,电压被限制在3.3 V,当产生负压(电压小于0 V)时,二极管正向导通,输出点接地电压被限制在0 V
# 实践配置
# CubeMX
首先配置打开 ADC1 的读取 Vref 与温度的两个通道与 ADC3 读取电池电压的通道8即可:
随后将两个 ADC 的设置均调为:将Vrefint Channel勾选,用于读取内部参考电压。ADC 在 CubeMX 中的设置如图采样频率设置为 PCLK2/4,采样位数为12位,数据设置为右对齐,其余均保持默认。随后由于用于读取电压的 ADC3 通道8需要一个引脚,所以对应的 PF10 亮起,而 ADC1 的两个通道均在内部,不需要引脚
这几项 ADC 设置的功能如下:
名称 | 功能 |
---|---|
Clock Prescaler | 设置采样时钟频率 |
Resolution | 设置采样精度 |
Data Alignment | 设置数据对齐方式 |
Scan Conversion Mode | 扫描转换模式开启/关闭 |
Continuous Conversion Mode | 连续转换模式开启/关闭 |
Discontinuous Conversion Mode | 非连续转换模式开启/关闭 |
DMA Continuous Requests | DMA连续启动开启/关闭 |
End of Conversion Selection | 每个通道转换结束后发送EOC标志/所有通道转换结束后发送EOC标志 |
# 用到的库函数
第一个是HAL_ADC_ConfigChannel
,它设置 ADC 通道的各个属性值(包括转换通道,序列排序,采样时间等),相当于初始化。它的参数2是一个结构体,会比较复杂,详细请见这里;且在实例中发现每次使用 ADC 进行采样时,最好将这个函数重新初始化并设置参数,接收到返回值为HAL_OK
正常时即可正常使用
第二个是HAL_ADC_Start
,它的作用是开启 ADC 的采样。它的参数是 ADC 的句柄指针,也为接收到返回值为HAL_OK
时正常工作,详细信息请见这里
第三个是HAL_ADC_PollForConversion
,它的作用是等待ADC转换结束。它的两个参数就是等待回传的 ADC 句柄指针与最大等待时间,返回值亦是HAL_OK
时正常工作,详细信息请见这里
第四个是HAL_ADC_GetValue
,它的作用是获取ADC值。它的参数是要取值的 ADC 的句柄指针,虽然一个 ADC 有很多通道,但是在使用此函数之前的HAL_ADC_ConfigChannel
函数已经确定了要取的是哪一个端口的值,其详细信息请见这里
# 算法
首先介绍一下,如何通过 VREFINT 对 Vcc 的电压校准:
通过一个函数对比标准的1.2 V的电压进行多次采样,并计算其平均值,接着将其与 ADC 采出的数据值做对比,得到(采集到的电压的)单位数字电压对应的模拟电压值voltage_vrefint_proportion
,其计算公式如下,设采样得到的数字值为average_adc
这一步其实类似于消除测量误差的过程,ADC 测量标准的电压出现误差,则将总共的标准值除以总共的测量值得到的将会是一个测量到的1 V相当于消除测量误差后的多少伏特
接着通过 ADC 对经过了分压电路的电池电压值进行采样,将该采样结果与voltage_vrefint_proportion
相乘,就得到了取值范围在 0-3.3 V 之间的 ADC 采样值,由于这个采样值是分压后的结果,需要反向计算出电压的值。分压的电阻值为 200 kΩ 和 22 kΩ,由于
# 流程
我们定义了几个函数他们的关系如下:
首先是init_vrefint_reciprocal
,就是之前提到的消除误差的作用的函数,他要使用在while(1)
之前来为整个程序提供校准值voltage_vrefint_proportion
这个全局变量
void init_vrefint_reciprocal(void)
{
uint8_t i = 0;
uint32_t total_adc = 0;
for(i = 0; i < 200; i++)
{
total_adc += adcx_get_chx_value(&hadc1, ADC_CHANNEL_VREFINT);
}
voltage_vrefint_proportion = 200 * 1.2f / total_adc;
}
2
3
4
5
6
7
8
9
10
11
然后是get_battery_voltage
,它的作用就是测量电压、计算精确值的主函数,它通过调用adcx_get_chx_value
来取得 ADC3 的测量值,随后与voltage_vrefint_proportion
相乘,得到精确值
fp32 get_battery_voltage(void)
{
fp32 voltage;
uint16_t adcx = 0;
adcx = adcx_get_chx_value(&hadc3, ADC_CHANNEL_8);
//(22K Ω + 200K Ω) / 22K Ω = 10.090909090909090909090909090909
voltage = (fp32)adcx * voltage_vrefint_proportion * 10.090909090909090909090909090909f;
return voltage;
}
2
3
4
5
6
7
8
9
10
11
而adcx_get_chx_value
就是取得 ADC3 的测量值的函数,他调用了前面的几个库函数来读取 ADC3 的值,关于sConfig
这个结构体应该怎样填写,请前往其定义查看参数的注释
static uint16_t adcx_get_chx_value(ADC_HandleTypeDef *ADCx, uint32_t ch)
{
static ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ch; // ch就是传进来的ADC的某个端口名字(的端口号)
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES; //ADC_SAMPLETIME_3CYCLES;
if (HAL_ADC_ConfigChannel(ADCx, &sConfig) != HAL_OK)
{
Error_Handler();
}
HAL_ADC_Start(ADCx);
HAL_ADC_PollForConversion(ADCx, 10);
return (uint16_t)HAL_ADC_GetValue(ADCx);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
顺带一提,这是一个static
类型的函数,它具体作用请见此处,为什么这么用呢,因为这几个自己编写函数是在一个bsp_adc.c
的自写源文件中,我们只希望这个源文件中的函数调用它,而在main.c
中不能直接调用
至于温度的测量则是大同小异,与电压测量几乎相同,希望你可以认真的阅读示例代码 (opens new window),把这个方法学会!