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个)
ADC0809的内部结构,这个并不是STM32的ADC内部结构,只是举例说明。
地址锁存和译码,是通过这个东西控制通道选择开关,控制选择哪一路的输入信号输入。
进来之后,DAC就会输出一个一直编码的电压与输入电压进行比较,它的大小与寄存器SAR有关,这里是8位寄存器,取值范围就是0~255,利用二分法与输入电压进行比较,大于则减小电压再比较,直到与输入电压近似相等。
二分法的每一次判断的数值其实都是二的每一位权重,二分法的过程其实就是判断寄存器的每一位是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结构图
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分频。
DMA数据转运:
对于模拟看门狗,他可以设置一个与之上限以及阈值下限,当看门狗所看的寄存器转换之后的数值高于或者低于这个范围的话,看门狗就会乱叫,在状态寄存器置标志位,然后如果后续使能的话,会走过去,然后再NVIC中申请中断。
注入寄存器以及规则寄存器中,当数据转换完成的时候,都会在转台寄存器中置标志位,读取其中的值就知道完成转换没有,同样使能后也可以到NVIC中生申请中断。
这是ADC的对应引脚表格,注意到ADC1,2都是在同一个引脚,这其实就是ADC的双ADC模式,就是两个ADC一起工作,可以配置成交叉模式,同步模式,交叉模式就是两个ADC对同一个通道采样,相当于你一拳我一拳这样,频率更高效果更好。当然也可以分开使用,分别对不同的引脚采样。
注意表格中只有ADC1有温度传感器以及内部参考电压,ADC3有一些也没有。
这个表格不代表是这款芯片的所有都有,有一些引脚没有的自然也就没有这个通道。
转换模式有四种,转换模式以及扫描模式的互相搭配,四种情况:
这个表是规则组的菜单,模式就是每一次转换都需要触发一次,才能进行,这个非扫描的模式下只有第一个位置有效。在序列1选择我们想要读取的通道,触发转换后,经过一段时间转化完成,对EOC进行标志1,转换完成,就可以在寄存器读取结果啦。要想再一次进行转换,就要再次进行触发,如果想换通道,就在转换之前把通道换成需要的。
单次转换的意思就是每一轮转换都需要手动触发一次,一轮就是要把序列中的通道都读一遍的意思,扫描模式就是要把序列中的通道输入都读一遍,非扫描就是只读第一个。而且序列中的值都是也可以重复的。
有个参数,通道数目,这个值就是扫描前几个序列的意思,扫描完成之后,就会产生EOC信号表示结束。手动触发进行新一轮的转换。
本芯片的ADC的数据是12位的,而寄存器是16位的,存在数据对齐问题,一般用右对齐,因为一读取出来就是数值了,左对齐就是把结果放大了,左边移动以为就相当于把数据乘以2的n次方了。
左对齐的作用就是如果不想要右对齐的时候那么高的分辨率,就用左对齐,然后只要高八位,把后面的都切掉,那就变成八位寄存器数值了,然后
AD转换需要一定时间,在这一段时间内,输入的待检测电压会发生抖动,所以电压不稳定,后续AD转换没有稳定的输入电压转换就不准确,所以在转化之前需要进行采样保持,也就是在输入端接上一个电容,一段时间之后断开连接,然后利用电容的充放电特性给ADC供稳定电压。保持的一段时间成为采样时间。采样时间越大,外部输入毛刺的干扰就越小。而12.5个ADC周期是量化编码花费的时间,12位ADC话费12个周期,至于0.5应该是用来做别的事情了。
上图举例的那里是ADC转换的最快时间。如果把ADCCLK设置比14MHz大,ADC就超频了,虽然可以降低转换的时间,但是却没办法确保稳定性。
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这个参数,想要配置别的序列,只能不断赋值配备这个参数啦
#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_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寄存器的值
}
跟此图的流程是相对的。
但是数值有时候会出现抖动情况,又时候光控灯也会如此,假设光线强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