ADC(Analog-Digital Converter)模拟-数字转换器

ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁

12位逐次逼近型ADC,1us转换时间(12位与1微秒,分别表示ADC的两个重要参数,一个是分辨率:多少位就表示0~2的多少次方减1这个范围,也就是量化结果的范围,位数越高,量化越精细,分辨率就越高,第二个是时间,就是转换频率,AD转换需要一定时间,表示从AD转换开始一次道结束,需要1微秒的时间,对应频率就是1MHz,这个是STM32的最快转换频率了,若待转换信号频率比这个数更高,就要考虑自己的转换频率够不够咯。)

输入电压范围:0~3.3V,转换结果范围:0~4095(输入电压以及转换结果是一个线性关系,输入电压乘以一个系数,就会等于转换结果)

18个输入通道,可测量16个外部(IO口,接模拟量就好了)和2个内部信号源(内部温度传感器(可以测CPU温度)以及内部参考电压(1.2V左右的参考电压,不随外部供电电压变化的,如果外部输入的电压不对,可以使用参考电压进行进行校正))

规则组和注入组两个转换单元(普通的ADC都是启动一次转换读一次值,但是STM32比较高级,就是列一个组,连续转换多个值,而这些组分为常规使用的规则组以及突发事件的注入组)

模拟看门狗自动监测输入电压范围(一般测量温度,光强这些值,并且经常使用,在待测量高于或者低于某些值的时候,进入中断执行操作,这个高于还是低于就是用看门口进行判断,有了这个东西就不用再手动读值进行判断了)

STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道(最多18个,我们这个芯片比较少,10个)

image.png

 ADC0809的内部结构,这个并不是STM32的ADC内部结构,只是举例说明。

地址锁存和译码,是通过这个东西控制通道选择开关,控制选择哪一路的输入信号输入。

进来之后,DAC就会输出一个一直编码的电压与输入电压进行比较,它的大小与寄存器SAR有关,这里是8位寄存器,取值范围就是0~255,利用二分法与输入电压进行比较,大于则减小电压再比较,直到与输入电压近似相等。

image.png

二分法的每一次判断的数值其实都是二的每一位权重,二分法的过程其实就是判断寄存器的每一位是0还是1的过程。这就是逐渐逼近型名字的来源。对于8位寄存器,从高位到地位判断八次就能得到输入电压的编码了。

ADC结束之后,DAC的输入数据就是输入电压的编码,然后再三态锁存缓冲器进行输出,几位就几个输出。

EOC是转换结束信号,START给一个输入脉冲转换开始,Clock是时钟信号,因为ADC内部是一步一步进行判断的,需要时钟推动。

Vref是DAC的参考电压,对于这八位的寄存器来说,255对应的是5V还是3.3v,由这个参考电压决定,因为两者是线性关系,同样这个也是ADC的参考电压,因为DAC是检测ADC的,有一个最大值,间接就是ADC的参考电压。

VCC跟GND跟Vref+以及Vref-接在一起。

这个是STM32的ADC结构图

image.png

 IN0~IN15是ADC的十六个GPIO口通道,以及内部两个通道:温度传感器以及Vrefint内部参考电压。模拟多路开关选择想要的通道,在进入模数转换器进行逐次比较,转换结果放在寄存器中,读取数值就知道转换的结果了。对于普通的AD转换,一般只能读取一个通道的值,但是这里比较高级,可以读取多个通道的值,规则通道组可以选择16个通道,注入通道组可以选择4个。

但是规则通道组有个毛病,就是虽然可以同时转换16个通道,但是他的寄存器只能存放一个结果,如果都传送过去会被挤掉,只保留最后一个数值,所以一般配合DMA使用,把结果传送走保留起来。注入组则是可以同时放上去四个数值。

触发ADC有软件触发以及硬件触发两种,软件触发就是输入软件指令进行触发,硬件触发则是根据上图控制位的两个通道触发。

在ADC转换的时候需要一段时间,一般情况使用延迟函数延迟一定时间然后进入中断再读取数值,但是频繁中断不好,占用软件CPU资源,所以一般用定时器设定一定的时间1,然后到达一定时间就产生一个更新事件,然后映射到TRGO即可,然后ADC也选择TRGO输入,这便是通过硬件自动触发ADC。当然还有其他方式。

一般来说Vref+接VDDA,Vref-接VSSA。决定输入电压的范围。

对于ADCCLK,他的时钟来源就是下图时钟树这里,这里有些尴尬,虽然他最大是124MHz,但是它达不到这个值,因为输入最大72MHz,二分频四分频都超过14MHz,所以一般只能用6.8分频。

image.png

DMA数据转运:

对于模拟看门狗,他可以设置一个与之上限以及阈值下限,当看门狗所看的寄存器转换之后的数值高于或者低于这个范围的话,看门狗就会乱叫,在状态寄存器置标志位,然后如果后续使能的话,会走过去,然后再NVIC中申请中断。

注入寄存器以及规则寄存器中,当数据转换完成的时候,都会在转台寄存器中置标志位,读取其中的值就知道完成转换没有,同样使能后也可以到NVIC中生申请中断。

image.png

右下角的开关控制,就是ADC_cmd函数,给ADC模块供电。

image.png

这是ADC的对应引脚表格,注意到ADC1,2都是在同一个引脚,这其实就是ADC的双ADC模式,就是两个ADC一起工作,可以配置成交叉模式,同步模式,交叉模式就是两个ADC对同一个通道采样,相当于你一拳我一拳这样,频率更高效果更好。当然也可以分开使用,分别对不同的引脚采样。

注意表格中只有ADC1有温度传感器以及内部参考电压,ADC3有一些也没有。

这个表格不代表是这款芯片的所有都有,有一些引脚没有的自然也就没有这个通道。

转换模式有四种,转换模式以及扫描模式的互相搭配,四种情况:

image.png

 这个表是规则组的菜单,模式就是每一次转换都需要触发一次,才能进行,这个非扫描的模式下只有第一个位置有效。在序列1选择我们想要读取的通道,触发转换后,经过一段时间转化完成,对EOC进行标志1,转换完成,就可以在寄存器读取结果啦。要想再一次进行转换,就要再次进行触发,如果想换通道,就在转换之前把通道换成需要的。

image.png

 这个之处发一次,然后就会一直在转换,不用查读没读完EOC的值,直接就读寄存器的值就行

image.png

单次转换的意思就是每一轮转换都需要手动触发一次,一轮就是要把序列中的通道都读一遍的意思,扫描模式就是要把序列中的通道输入都读一遍,非扫描就是只读第一个。而且序列中的值都是也可以重复的。

有个参数,通道数目,这个值就是扫描前几个序列的意思,扫描完成之后,就会产生EOC信号表示结束。手动触发进行新一轮的转换。

image.png

 一次转换完成之后,开始下一次转换。

还有一个间断模式,就是在每读几个序列的时候,就开始停止一下,需要再次触发重新读取。

image.png

image.png

 本芯片的ADC的数据是12位的,而寄存器是16位的,存在数据对齐问题,一般用右对齐,因为一读取出来就是数值了,左对齐就是把结果放大了,左边移动以为就相当于把数据乘以2的n次方了。

左对齐的作用就是如果不想要右对齐的时候那么高的分辨率,就用左对齐,然后只要高八位,把后面的都切掉,那就变成八位寄存器数值了,然后

image.png

AD转换需要一定时间,在这一段时间内,输入的待检测电压会发生抖动,所以电压不稳定,后续AD转换没有稳定的输入电压转换就不准确,所以在转化之前需要进行采样保持,也就是在输入端接上一个电容,一段时间之后断开连接,然后利用电容的充放电特性给ADC供稳定电压。保持的一段时间成为采样时间。采样时间越大,外部输入毛刺的干扰就越小。而12.5个ADC周期是量化编码花费的时间,12位ADC话费12个周期,至于0.5应该是用来做别的事情了。

上图举例的那里是ADC转换的最快时间。如果把ADCCLK设置比14MHz大,ADC就超频了,虽然可以降低转换的时间,但是却没办法确保稳定性。

image.png

 这个是校准功能,只需要几行代码就可以实现,不用太过深入。

image.png

 图一徒儿都是分压电路,输入是跟GND连接的那个电阻的电压,第三个图就是当输入大于规定的电压时候,就用这个接法,先把大电压降成小电压,再输入。但是过高电压就不用这个电路啦。用专业的芯片之类的。

ADC代码部分:

image.png

电位器的内部结构图如上图所示,相当于一个滑动变阻器,左边扭动就向左移,右边移动就向右移。

配置ADC步骤:

image.png

1.开启时钟RCC,包括GPIO以及ADC,以及ADCCLK的分频器。

void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);//开启ADCCLK的时钟,此函数在RCC.h库中

void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);//初始化ADC

void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);

2.配置GPIO,把GPIO配置为模拟输入的模式

void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);//开启DMA转运数据所用

void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);//也就是上图的中断输出控制

3.配置多路选择器,把多路选择器的输入传到后续的组中

4.利用库函数配置ADC

5.看门口或者中断(按照选择使用)

6.开关控制,ADC_cmd开启ADC的时钟。void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);

void ADC_ResetCalibration(ADC_TypeDef* ADCx);//复位校准

FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);//获得复位校准状态

void ADC_StartCalibration(ADC_TypeDef* ADCx);//开始校准

FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);//获得开始效准状态

void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//软件触发开始转换,对应的是上图的转换控制

FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);//没啥用

FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//转换完成EOC置1,利用这个判断转换结束与否,而不是用上面的那个函数

void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);//控制间隔多少个进行间断

void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//使能开启间断转换模式

void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);//此函数用来给序列填入相应的通道,第一个参数ADC几,二十第几个通道,三是第几个序列,四是转换时间(快的就选小的,稳定的就选大的)

void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//外部触发转换控制,是否允许外部触发转换

uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);//获取单模式ADC转换之后的寄存器结果

uint32_t ADC_GetDualModeConversionValue(void);//获取双模式ADC转换的结果

void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//这种带有Inject的是注入组的配置函数

void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog);//是否开启看门狗

void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold);//配置看门狗的阈值

void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel);//配置看门狗所看通道

void ADC_TempSensorVrefintCmd(FunctionalState NewState);//温度传感器以及参考电压两个内部通道的配置

FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//获取标志位

void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//清除标志位

ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT);//获取中断状态

void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);//清除中断状态

void AD_Init(void)

{

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);//PCLK2就是APB时钟2的意思

    

    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,这是ADC专有的模式,在这个模式下GPIO口是无效的,防止GPIO的输入输出对模拟电压造成影响,

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    //在ADC1中的序列1中写入通道0这个参数,想要配置别的序列,只能不断赋值配备这个参数啦

image.png

 ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC工作模式,跳转如下:

#define ADC_Mode_Independent                       ((uint32_t)0x00000000)
#define ADC_Mode_RegInjecSimult                    ((uint32_t)0x00010000)
#define ADC_Mode_RegSimult_AlterTrig               ((uint32_t)0x00020000)
#define ADC_Mode_InjecSimult_FastInterl            ((uint32_t)0x00030000)
#define ADC_Mode_InjecSimult_SlowInterl            ((uint32_t)0x00040000)
#define ADC_Mode_InjecSimult                       ((uint32_t)0x00050000)
#define ADC_Mode_RegSimult                         ((uint32_t)0x00060000)
#define ADC_Mode_FastInterl                        ((uint32_t)0x00070000)
#define ADC_Mode_SlowInterl                        ((uint32_t)0x00080000)
#define ADC_Mode_AlterTrig                         ((uint32_t)0x00090000)

除了第一个之外,其余都是双ADC模式。

    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对其:左对齐还是右对齐

    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//外部触发源的选择,None的意思就是不选择外部触发源,此处用软件触发

    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转换与否

    ADC_InitStructure.ADC_ScanConvMode = DISABLE;//扫描模式与否

    ADC_InitStructure.ADC_NbrOfChannel = 1;//通道数目,也就是扫描的通道个数

    ADC_Init(ADC1, &ADC_InitStructure);//

    

    ADC_Cmd(ADC1, ENABLE);

    //下面四个函数是用来进行校准的,第一个是用来复位校准位的,也就是把CR2的RSTCAL位置1,这个是软件置1,用第一个函数,当校准完成之后,会自动硬件变成0,然后用第二个参数检测是否为0,是的话就开始校准,下面最后两个参数也是一样。

    ADC_ResetCalibration(ADC1);

    while (ADC_GetResetCalibrationStatus(ADC1) == SET);

    ADC_StartCalibration(ADC1);

    while (ADC_GetCalibrationStatus(ADC1) == SET);

}

//这部分就是获取寄存器之中的转换值了。

uint16_t AD_GetValue(void)

{

如果是设定为连续转换的话,上面的 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE改为ENABLE,下面的这个函数触发就不用一直启动,可以移动到外面,也不用一直判断EOC了,所以while那条也可以删掉。

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发转换,ADC转换开始

    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//判断EOC是否为1,也就是转换是否完成

跳转如下:具体的描述在ADC状态寄存器中,寄存器中的相应的位会置1或者0

 @arg ADC_FLAG_AWD: Analog watchdog flag//模拟看门狗标志位

  *     @arg ADC_FLAG_EOC: End of conversion flag//规则组以及注入组转换结束都会置1转换标志位,ADC_DR的意思就是数据寄存器,当ADC_DR被读取的时候,这里就会自动置0.

  *     @arg ADC_FLAG_JEOC: End of injected group conversion flag//注入组转换标志位

  *     @arg ADC_FLAG_JSTRT: Start of injected group conversion flag//注入组开始转换标志位

  *     @arg ADC_FLAG_STRT: Start of regular group conversion flag//规则组转换开始标志位

    return ADC_GetConversionValue(ADC1);返回转换之后的数值,内部就是ADC直接读取DR寄存器的值

}

image.png

跟此图的流程是相对的。

但是数值有时候会出现抖动情况,又时候光控灯也会如此,假设光线强AD大,AD大于某个阈值的时候,关灯,低于就开灯,这时候假设在这个阈值上下抖动的话,就会出现很尴尬的灯不断开关的情况,这时候解决的办法就是通过设置两个阈值,一个上阈值,一个下阈值,这样就会有一段空间来避免这类情况。

如果数值跳变很厉害,还可以使用滤波的方法,让AD值更加稳定:比如均值滤波,就是读取很多个AD的值,然后进行求取平均值。后者裁剪分辨率,把后面的数值的尾数去掉,这样也可以避免抖动。

如果想得到相应的电压值,就把AD值除以2的n次方(多少位),然后乘上参考电压,如下:

 Voltage = (float)ADValue / 4095 * 3.3;

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
 
uint16_t ADValue;
float Voltage;
 
int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Volatge:0.00V");
	
	while (1)
	{
		ADValue = AD_GetValue();
		Voltage = (float)ADValue / 4095 * 3.3;
		
		OLED_ShowNum(1, 9, ADValue, 4);
		OLED_ShowNum(2, 9, Voltage, 1);
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
		
		Delay_ms(100);
	}
}

主函数中如此,其中计算电压的时候,因为是利用逐次逼近的方法判断每一位寄存器如何如何,对于12位寄存器,最大的就是4095,4095对应的就是3.3V,所以就这样列。

OLED显示的时候不能直接显示小数,就先显示浮点数的小数点前一位,然后乘以100取余再显示小数点后两位。

在进行多通道扫描输入的时候,为什么要用DMA转运而不是手动传入一个就转运一个呢?

1.在扫描模式下,启动列表之后,每一个通道转换完成之后,不会发生标志位,也没有任何的中断,不知道某个通道是不是转换完了,只有在整个列表转换完成之后,才会EOC标志,然后触发中断。但是这时候前面通道的数据就已经被覆盖了。

2.AD转换非常快,转换一次大概就几微秒,如果不能在这个时间之内把数据转运走,数据就会丢失,对于用程序手动转运要求比较高,但是也并非不可行,可以利用间断模式,在一个通道转换完成的时候,就暂停一次,暂停足够长的时间,把数据转运走了,在进行下一通道传输。而延长这段时间的,就只能通过延时函数进行。因为在一次转换完成的时候,并没有标志位以及进入中断。

但是还有一种不用DMA的方法进行多通道传输,就是用单次转换非扫描模式。只要在每次触发之前,手动更改序列一的通道即可

uint16_t AD_GetValue(uint8_t ADC_Channel)

{

    ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);

    return ADC_GetConversionValue(ADC1);

}

以参数的方式更改,注意还要更改GPIO的相应值。

————————————————

版权声明:本文为CSDN博主「笔下觅封侯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/m0_63148816/article/details/130464799