ADC模数转换器
1、ADC的简介
1.1、ADC测量电压原理
与其说ADC是模数转换器,不如说ADC是一个采样器+数据比较器。如上图:ADC的某个采样引脚去采样模拟信号的某个时间点的数据(3.3v),然后通过电容的充放电将3.3v的数据输入到数据比较器里面。然后ADC不断的向结果寄存器里面填加数据(二进制数),然后数据通过电压发生器(DAC(数模转换器))输入到数据比较器里面。在比较器里面对比进行如下操作:
①若ADC的结果寄存器里面的数据 < 模拟信号,则增加ADC的结果寄存器里面的数据。
②若ADC的结果寄存器里面的数据 > 模拟信号,则减小ADC的结果寄存器里面的数据。
③若ADC的结果寄存器里面的数据 = 模拟信号,则代表成功测量出模拟信号。
【注】增加ADC的数值是从高位依次往低位增加。这就是逐次逼近ADC
1.2、电压分辨率
综上:结果寄存器里面的数据 < 模拟信号,则增加ADC的结果寄存器里面的数据。那结果寄存器里面数值+1(电压分辨率),电压发生器输入到比较器里面的电压加多少喃?
答案:这与ADC的参考电压和结果寄存器的位数有关。
例如:若参考电压为3.3v,结果寄存器为2位。那结果寄存器里面数值+1(电压分辨率)是多少喃?如下图所示:
结果寄存器为4位/8位。那结果寄存器里面数值+1(电压分辨率)是多少喃?如下图所示:
总结:若结果寄存器的位数越多,则分辨率越小。则测量到的结果就越准确。而在STM32F103C8T6这款芯片中ADC的参考电压为VREF+~VREF-。因为VREF连接到VDD供电引脚,所以VREF = 3.3V。结果寄存器的位数为12位。而有些单片机的VREF没有连接到供电引脚VDD,可以通过外部供电来提供参考电压。
1.3、采样转换演示
我们先来了解到ADC的工作原理。如图:当闭合采样开关,模拟信号输入,对电容充电。当充满电后,断开采样开关,比较器通过比较来给结果寄存器增加数据/减少数据,直到和模拟信号的值相差到规定的数值以内,然后将数据保存。
如图:采样到的模拟信号为2.21v,参考电压为3.3v,结果寄存器为4位,则电压分辨率为0.22v。
①将结果寄存器的高位置1,进入比较器比进行比较:8 * 0.22 = 1.76 < 2.21v,保留寄存器中最高位数据1。
②将结果寄存器次高位置1,进入比较器比进行比较:8 * 0.22 + 4 * 0.22 = 2.64 > 2.21v,清除寄存器中次高位数据1。此位置0
③将结果寄存器次后一位置1,进入比较器比进行比较:8 * 0.22 + 2 * 0.22 = 2.20 < 2.21v,则保留寄存器中此位数据1。
④将结果寄存器次最后位置1,进入比较器比进行比较:8 * 0.22 + 2 * 0.22 + 1 * 0.22 = 2.42 > 2.21v,清除寄存器中此位数据1。此位置0
综上:最终得出结果寄存器的数据最后为1010。由于寄存器的位数不够大,导致测量的精度不准。
2、采样时间和转换时间
采样时间:就是对电容充满电所需要的时间
转换时间:就是通过比较,确定结果寄存器里面的数值的时间。
STM32F103C8的ADC挂载在APB2总线上,而ADC的特性规定,ADC的时钟频率<14MHz。所以APB2时钟频率信号输出进入ADC之前要进行分频处理。
-
转换时间 = 12.5的时钟周期。
将结果寄存器里面置1/置0进行输出比较所需的时间为1个时钟周期。而结果寄存器是12位,所以需要12个时钟周期。外加0.5个时钟周期进行等待数据的处理完毕。 -
采样时间 = 电容充电的时间
电容充电的时间由电路中的电阻和电容本身所决定的,电阻越大,电流越小,充电时间越长,采样时间越长。即电容充满电时间格式如下图所示:
当电阻为400Ω时,电容充满电所需时间 = 0.11us,当电阻为10kΩ时,电容充满电所需时间 = 0.85us
而等待电容充电的时间越长,则采样到数据就越精确,误差就越小。若等待的时间超过电容充满电的时间,那么采样到的数据就是模拟电压。
但是ADC的精度本来就是有限的,所以只要采样的误差<1/4*ADC分辨率即可。
若ADC的时钟源频率为14MHz,则将采样时钟转换为时间周期如下图所示:
电阻为400Ω时:采样时间t = 0.11us,而时间T = 1s/(14MHz) = 1/14(us),则t = 1.54T,即采样时间是1.54个时钟周期。
但是在编程中采样时间被规定了8个挡位的,所以我们只能选择和采样时间相近的那个挡位。挡位>采样时间,代表等待电容充电时间>电容充满电的时间,即数据越精确。挡位<采样时间,代表等待电容充电时间<电容充满电的时间,数据不精确,都是采样时间变快。
问:最理想的情况下ADC每砂最多执行多少次转换?
答:最理想的情况是:采样的电阻为0Ω,ADC工作频率为14MHz。采样电阻为0,则采样时间t = 0.0776us = 1.0864T,即为1.0864个时钟周期,而挡位最接近的为1.5个时钟周期。所以总花费的时间 = 1.5T+12.5T = 14T = 14 * 1s/(14MHz) = 1us。所以:所以:理想情况下1us采样转换1次数据。而1s则采样转换10M次数据。
3、STM32中ADC模块
STM32F103C8T6单片机中有2个ADC模块:ADC1、ADC2,结果寄存器都为12位,参考电压为3.3v,也就是将3.3v的电压分为了4095份。且每个ADC片上外设都有10外部测量通道和2个内部信号源。我们可以通过外部通道引脚(PA0~PA7,PB0,PB1)测量外部模块的电压
这么多的通道是怎样管理的喃?
答:通过规则组(常规序列)和注入组(注入序列)进行管理
如上图所示:规则组和注入组像是一个盒子(用来存放需要测量的引脚)。
①注入组:注入组里面最多能放4个采集转换通道(4个序列盒子),给一个触发信号,ADC就开始采集转换,将结果保存到对应的4个结构寄存器里面。
②规则组:规则组里面最多能放入16个采集转换通道(16个序列盒子),给触发控制一个触发信号,ADC就按照规则组里面序列盒子的存放通道顺序进行采集转换。而存放规则组采集转换的数据结果寄存器只有1个。所以使用规则组时要及时将第一个通道测量到的数据进行处理,否则第二个通道的数据会将其覆盖。所以常常搭配DMA进行使用
③ECO:为转换完成标志位,可以通过此为进行中断触发请求。
④模拟看门狗:可以监测结果寄存器里面的数值,设定一个数据阈值,当监测到结果寄存器里面的值大于阈值时,也可以触发一个中断请求。
【注意】注入组给优先级高于规则组,当有注入组里面有通道进行测量,则ADC会停止规则组的测量,去进行注入组的测量。
③触发控制:让ADC开始采集转换的信号,如下图所示,有定时器启动和软件启动。而软件启动就是就是给寄存器置1
4、转换模式
规则组的转换模式:
4.1、单次转换非扫描模式
单次:给一个触发信号,测量转换一次,测量转换完成后,停止工作。
非扫描:序列1盒子里面有测量通道,其他的序列盒子没有。
在此模式下,规则组只有第一个盒子里面的通道才有效(即序列1),我们可以在序列1的盒子里面放入我们需要转换的通道,然后给规则组一个触发信号,开始采集转换,转换完成后数据保存在数据寄存器里面,同时ECO标志位置1。需要进行第二次转换,那么需要在启动一次触发信号,开启转换。
4.2、连续转换非扫描模式
连续:给一个触发信号,ADC就会一直进行测量转换,第一次完成后,开始进行第二次测量转换,一直循环下去。
和单次转换非扫描模式不同的是,给规则组一个触发信号,在第一次转换完成后立马进入第二次转换,不断的进行下去。单片机需要获取数据,只需要读取数据寄存器里面的数据即可。
4.3、单次转换扫描模式
扫描模式:除了序列1盒子里面有测量通道,其他的盒子里面也有。
在规则组里面的多个序列盒子里面放入我们需要转换的通道,然后告诉ADC有多少个盒子被使用。然后给规则组一个触发信号,ADC就按照盒子序列的顺序开始转换,序列1的通道转换完成后数据放入结果寄存器里面,然后开始序列2的通道转换。当全部的通道转换完成时ECO标志位置1,需要第二次转换则需要启动触发信号。
【注意】一般情况下扫描模式搭配DMA使用,这样才会避免数据的覆盖丢失。
4.4、连续转换扫描模式
数据对齐:
ADC转换后的数据是12位的,而数据寄存器是16位的,所以数据寄存器有4个空出来的位置补零。一般的情况下使用右对齐即可
数据校准
校准都是固定的,只需要在初始化后加上校准代码即可,不用理解。
5、编程案列
与之相关的标准库编程接口:
5.1、AD单通道连续转换
①ADC.c文件的代码如下;
/*
ADC单通道实验,通过通道1(PA0)对自身输出电压进行采集,通过电位器改变电压变化,最终通过串口打印出来
*/
#include "ADC.h"
/**
* ADC1初始化函数
*/
void ADC1_Init(void)
{
/* 1、使能GPIO和ADC时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能ADC1时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//对ADC时钟源分频
/* 2、对PA0(ADC1的通道0引脚)进行引脚配置 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 3、配置ADC1工作模式 */
ADC_InitTypeDef ADC_InitStructure;
ADC_DeInit(ADC1); //将外设ADC1的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //非扫描(单通道)模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC1数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //转换的ADC通道的数目,只有PA0
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
/* 4、配置规则组*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //ADC1,ADC通道0(对应PA0)
//盒子序列1,采样时间为55.5周期
//此步就是将通道0放入盒子序列1中
ADC_Cmd(ADC1, ENABLE); //使能ADC1
/* 5、ADC校准 */
ADC_ResetCalibration(ADC1); //使能复位校准
while (ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while (ADC_GetCalibrationStatus(ADC1)); //等待校准结束
/* 6、软件触发 */
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //启动ADC转换
}
/**
* 获取ADC中的结果寄存器的值
*/
uint16_t Get_ADCResult(void)
{
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待转换完成标志位置1
return ADC_GetConversionValue(ADC1); //获取ADCx->DR里面的数据,ECO自动清除
//【注意】返回的数据是12位的二进制数
}
②主函数main.c文件的代码如下:
#include "stm32f10x.h"
#include "Delay.h"
#include "UART.h"
#include "ADC.h"
int main(void)
{
uint16_t Data = 0;
float ADC_Value = 0;
UART1_Init();
ADC1_Init();
printf("ADC电压测量\r\n");
while(1)
{
Data = Get_ADCResult();
printf("结果寄存器数值为:%d\r\n",Data);
ADC_Value = Data * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(1000);
}
}
5.2、AD单通道模拟多通道
我们还没有学习DMA,如果不使用DMA对多通道转换的数据进行挪动的话,那么会出现数据的覆盖。我们通过单次转换非扫描模式模拟单次扫描模式。就是在第一次转换完成后,在获取数据的同时将第1序列的盒子里面的通道号通过手动修改。
ADC.c文件的代码如下:
#include "ADC.h"
/**
* ADC1初始化函数
*/
void ADC1_Init(void)
{
/* 1、使能GPIO和ADC时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能ADC1时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//对ADC时钟源分频
// /* 2、对PA0(ADC1的通道0引脚)进行引脚配置 */
// GPIO_InitTypeDef GPIO_InitStructure;
// GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0 作为模拟通道输入引脚
// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
// GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 2.对通道0(PA0),通道1(PA1),通道2(PA2),通道3(PA3)进行配置 */
GPIO_InitTypeDef GPIOInitStruct;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,ADC的专属模式
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
GPIO_Init(GPIOA,&GPIOInitStruct);
/* 3、配置ADC1工作模式 */
ADC_InitTypeDef ADC_InitStructure;
ADC_DeInit(ADC1); //将外设ADC1的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //非扫描(单通道)模式
// ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //非连续模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC1数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //转换的ADC通道的数目,只有PA0
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
/* 4、配置规则组*/
// ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //ADC1,ADC通道,盒子序列1,采样时间为55.5周期
ADC_Cmd(ADC1, ENABLE); //使能ADC1
/* 5、ADC校准 */
ADC_ResetCalibration(ADC1); //使能复位校准
while (ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while (ADC_GetCalibrationStatus(ADC1)); //等待校准结束
/* 6、软件触发 */
// ADC_SoftwareStartConvCmd(ADC1, ENABLE); //启动ADC转换
}
/**
* 获取ADC中的结果寄存器的值
*/
uint16_t Get_ADCResult(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);//ADC1,ADC通道,盒子序列1,采样时间为55.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //启动ADC转换
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待转换完成标志位置1
return ADC_GetConversionValue(ADC1); //获取ADCx->DR里面的数据,ECO自动清除
//【注意】返回的数据是12位的二进制数
}
主程序文件的代码如下:
/*
ADC单通道模拟多通道的简单使用
*/
#include "stm32f10x.h"
#include "Delay.h"
#include "UART.h"
#include "ADC.h"
int main(void)
{
uint16_t Data0,Data1,Data2,Data3;
float ADC_Value = 0;
UART1_Init();
ADC1_Init();
printf("ADC电压测量\r\n");
while(1)
{
/* 获取通道的测量值 */
Data0 = Get_ADCResult(ADC_Channel_0);//修改通道并获取数据
Data1 = Get_ADCResult(ADC_Channel_1);
Data2 = Get_ADCResult(ADC_Channel_2);
Data3 = Get_ADCResult(ADC_Channel_3);
/* 将测量值计数处理 */
printf("通道0的结果寄存器数值为:%d\r\n",Data0);
ADC_Value = Data0 * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(100);
printf("通道1的结果寄存器数值为:%d\r\n",Data1);
ADC_Value = Data1 * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(100);
printf("通道2的结果寄存器数值为:%d\r\n",Data2);
ADC_Value = Data2 * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(100);
printf("通道3的结果寄存器数值为:%d\r\n",Data3);
ADC_Value = Data3 * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(100);
}
}
5.3、ADC单通道中断
规则组和注入组转换结束时能产生中断,当模拟看门狗状态位被设置时也能产生中断。它们都有独立的中断使能位。
如图:规则组转换完成会将EOC置1,注入组转换完成会将JEOC置1。
【注意】ADC1和ADC2的中断映射在同一个中断向量上,而ADC3的中断有自己的中断向量。
上面的代码是通过阻塞式获取规则组转换完成的数据,接下来是通过中断的方式获取规则组转换完成的数据。
①ADC.c文件的代码如下:
#include "ADC.h"
/**
* ADC1初始化函数
*/
void ADC1_Init(void)
{
/* 1、使能GPIO和ADC时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能ADC1时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//对ADC时钟源分频
/* 2、对PA0(ADC1的通道0引脚)进行引脚配置 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 3、配置ADC1工作模式 */
ADC_InitTypeDef ADC_InitStructure;
ADC_DeInit(ADC1); //将外设ADC1的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //非扫描(单通道)模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC1数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //转换的ADC通道的数目,只有PA0
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
/* 4、配置规则组*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //ADC1,ADC通道,盒子序列1,采样时间为55.5周期
ADC_Cmd(ADC1, ENABLE); //使能ADC1
/* 5、使能EOC中断,NVIC的配置 */
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = ADC1_2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
/* 5、ADC校准 */
ADC_ResetCalibration(ADC1); //使能复位校准
while (ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while (ADC_GetCalibrationStatus(ADC1)); //等待校准结束
/* 6、软件触发 */
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //启动ADC转换
}
/**
* 获取ADC中的结果寄存器的值
*/
//uint16_t Get_ADCResult(void)
//{
// while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待转换完成标志位置1
// return ADC_GetConversionValue(ADC1); //获取ADCx->DR里面的数据,ECO自动清除
// //【注意】返回的数据是12位的二进制数
//}
/**
* ADC单通道转换完成的中断范围函数
*/
uint16_t Data = 0;
void ADC1_2_IRQHandler (void)
{
if(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == SET)//转换完成标志位EOC置1
{
ADC_ClearFlag(ADC1, ADC_FLAG_EOC);//清除标志位EOC
Data = ADC_GetConversionValue(ADC1);//获取结果寄存器中的数据
}
}
②主函数main.c文件的代码如下:
#include "stm32f10x.h"
#include "Delay.h"
#include "UART.h"
#include "ADC.h"
int main(void)
{
float ADC_Value = 0;
UART1_Init();
ADC1_Init();
printf("ADC电压测量\r\n");
while(1)
{
printf("结果寄存器数值为:%d\r\n",Data);
ADC_Value = Data * 3.3 / 4095; //将二进制计数为电压电压值
printf("测量到的电压为:%0.2f\r\n",ADC_Value);
Delay_ms(1000);
}
}
【注意】使用的是单通道连续转换模式,即给个触发信号后会不断的进行转换下去,完成一次转换就会进入一次中断。这样频繁的进入ADC中断可能会影响主函数的运行。所以可以通过定时器中断来解决这个问题。定一段时间,在定时中断里面使能触发信号(ADC配置为非连续模式),ADC开始转换,当转换完成后进入ADC中断,在ADC中断获取数据。注:ADC的中断优先级配置要大于定时器中断优先级。