AD单通道接线图

PA0~7,pb0~1这10个引脚是ADC的10个通道,接在哪个都行,这里我们接的是PA0。
AD初始化步骤

第一步,开启RCC时钟,包括ADC和GPIO时钟。另外ADCCLK的分频器也需要配置一下。
第二步,配置GPIO,把需要用的GPIO配置成模拟输入模式。
第三步,配置多路开关,把左边的通道接入到右边的规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单中。
第四步,配置ADC转换器。在库函数里,是用结构体来配置的,可以配置这一大块电路的参数
。包括ADC是单次转换还是连续转换、扫描还是非扫描、有几个通道、触发源是什么、数据对齐是左对齐还有右对齐这些参数用一个结构体就可以配置了。
第五步,如果你需要模拟看门狗,有几个函数可以用来配置阈值和监测通道。
第六步,如果你想要开启中断,那就在中断输出控制里用ITConfig函数开启对应的中断输出,再在NVIC里配置一下优先级,这样就能触发中断了。
第七步,开关控制。调用一下ADC_Cmd函数,开启ADC。
这样ADC就配置完成了,就能正常工作了,当然在开启ADC之后,根据手册里的建议,我们还可以对ADC进行一下校准,这样就可以减小误差,那在ADC工作的时候,如果想要软件触发转换,那会有函数可以触发。如果想要读取转换结果,那也会有函数可以读取结果。
ADC相关库函数
这个库函数是在RCC中的,void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);
这个函数是用来配置ADCCLK分频器的,他可以对APB2的72MHz时钟选择2、4、6、8分频,输入到ADCCLK。
* @arg RCC_PCLK2_Div2: ADC clock = PCLK2/2
* @arg RCC_PCLK2_Div4: ADC clock = PCLK2/4
* @arg RCC_PCLK2_Div6: ADC clock = PCLK2/6
* @arg RCC_PCLK2_Div8: ADC clock = PCLK2/8
void ADC_DeInit(ADC_TypeDef* ADCx); //恢复缺省配置(默认配置)
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);//Init初始化
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);//结构体初始化。
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);
如果使用DMA转运数据,那就得调用这个函数,这个我们下节讲DAM的时候用。
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);
用于控制某个中断能不能通往NVIC。
void ADC_ResetCalibration(ADC_TypeDef* ADCx);//复位校准
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);//获取复位校准状态
void ADC_StartCalibration(ADC_TypeDef* ADCx);//开启校准
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);//获取开启校准状态
我们在ADC初始化完成之后,依次调用就行了。
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
调用一下,就能软件触发转换了。我们这个代码目前使用软件触发。
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);//ADC获取软件开始转换状态
从名字上来看,这个函数好像是判断转换是不是正在进行的。那我们是不是可以调用这个函数,来判断转换是否已经结束呢?但是不行的。这个函数就是用来获取CR2的SWSTART这一位的,对照手册的寄存器描述,这一位的作用是开始转换规则通道。由软件设置该位以启动转换,转换开始后硬件马上清除此位。
所以ADC_SoftwareStartConvCmd函数就是给SWSTART位置1,以开始转换的,ADC_GetSoftwareStartConvStatus函数是返回SWSTART位的状态。由于SWSTART位在转换开始后立刻清零,所以这个函数的返回值跟转换是否结束毫无关系。
那如何知道转换是否结束呢?我们需要用到下面这个函数。
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);//获取标志位状态。
参数给EOC的标志位,判断EOC标志位是不是置1了。
如果转换结束EOC标志位置1,然后调用这个函数,判断标志位,这样才是正确的判断转换是否结束的方法。
所以简单来说,上面说的“ADC获取软件开始转换状态”函数没啥用,我们一般也不用,所以不要被误导了。
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);
就是获取AD转换的数据寄存器,读取转换结果就要使用这个函数。
uint32_t ADC_GetDualModeConversionValue(void);
ADC获取双模式转换值,这个是双ADC模式读取转换结果的函数。
//是否启动模拟看门狗
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);
ADC单通道代码
void AD_Init()
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//配置ADCCLK,6分频,ADCCLK = 72MHz/6= 12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO,初始化PA0引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
/*在AIN模式下,GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰。
所以AIN模式是ADC的专属模式。*/
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择规则组的输入通道。采样时间是55.5个ADC周期。
//这个函数的意思是,在规则组序列1的位置放入通道0,采样时间是55.5个ADC周期。
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
//配置ADC
ADC_InitTypeDef ADC_InitStruct;
//ADC工作模式,配置独立模式或者双ADC模式。
ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
//数据对齐,选择右对齐。
ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
//外部触发转换选择,就是触发控制的触发源。
//选择不是使用外部触发,就是软件触发的意思。
ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
//连续转换模式,选择连续转换还是单次转换。
ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
//扫描转换模式,选择扫描模式还是非扫描模式。
ADC_InitStruct.ADC_ScanConvMode=DISABLE;
//通道数目,指定在扫描模式下,总共会用几个通道。
/*其实这个参数只在扫描模式下才需要用,
如果是非扫描模式,那整个列表只有第一个序列有用*/
ADC_InitStruct.ADC_NbrOfChannel=1;
ADC_Init(ADC1,&ADC_InitStruct);
//开启ADC电源
ADC_Cmd(ADC1,ENABLE);
//ADC校准
ADC_ResetCalibration(ADC1);//复位校准
/*获取复位校准状态,所以如果要等待复位完成的话,
还需要加个while循环,如果没有校准完成,就在这个while空循环里一直等待。*/
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);//开启校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准是否完成了。
}
通过上面的代码。我们的ADC就处于准备就绪的状态了,那我们想要启动转换,获取结果,就可以在下面再写一个获取结果的函数。
首先,软件触发转换,然后等待转换完成,也就是等待EOC标志位置1,最后读取ADC数据寄存器,就完事了。
uint16_t AD_GetValue()
{
//软件触发的函数,ADC开启进行转换。
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//获取标志位函数查看ADC是否转换完成,转化完成后EOC由硬件自动置1,就跳出循环了。
/*具体要等待多长时间呢?我们刚才配置了采样周期是55.5,转换周期是固定的12.5,
加起来一共是68个ADC周期,ADC频率是12MHz,换算是5.67us*/
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
//转化完成后返回转换值。
return ADC_GetConversionValue(ADC1);
//这里读取数据寄存器,会自动清除EOC的标志位,所以不需要再手动清除标志位了。
}
主函数代码可以这样写:
uint16_t ADvalu;
int main(void){
OLED_Init();
AD_Init();
OLED_ShowString(1,6,"AOOFOO");//字符串
OLED_ShowString(2,1,"ADvalu:");//字符串
while(1)
{
ADvalu = AD_GetValue();
OLED_ShowNum(2,9,ADvalu,4); //十进制无符号数
}
}
下载到STM32中会发现,AD值的末尾会有些抖动,这是正常的波动。
但是如果你想对这个值进行判断,再执行一些操作。比如光线的AD值小于某一阈值,就开灯,大于某一阈值,就关灯,那可能会存在这样的情况,比如光纤逐渐变暗,AD值逐渐变小,但是由于波动,AD值会在判断阈值附近来回跳变,这会导致输出产生抖动,来回开灯关灯。
那如何避免AD值波动呢?
这个可以使用迟滞比较的方法来完成,设置两个阈值,低于下阈值时开灯,高于上阈值时才关灯,这就可以避免输出抖动问题了。这个跟我们GPIO那一节讲的施密特触发器是一个原理。另外,如果你觉的数据跳变太厉害,还可以采用滤波的方法,让AD值平滑一些,比如均值滤波,就是读10个或者20个数,取平均值,作为滤波的AD值。或者还可以裁剪分辨率,把数据的尾数去掉,这样也可以减少数据波动。
如果想要显示实际的电压值怎么办呢?
这只需要对这个数据进行一个线性变换就行了:
uint16_t ADvalu;
float Voltage;
int main(void){
OLED_Init();
AD_Init();
OLED_ShowString(1,6,"AOOFOO");//字符串
OLED_ShowString(2,1,"ADvalu:");//字符串
OLED_ShowString(3,1,"Voltage:0.00V");//字符串
while(1)
{
ADvalu = AD_GetValue();
Voltage = ((float)ADvalu/4095)*3.3;
OLED_ShowNum(2,9,ADvalu,4); //十进制无符号数
OLED_ShowNum(3,9,Voltage,1); //十进制无符号数
OLED_ShowNum(3,11,(uint16_t)(Voltage*100)%100,2); //十进制无符号数
}
}
上面这些代码我们使用的是单次转换非扫描,我们还可以使用连续转换非扫描。这个模式的好处就是不需要不断的触发,就是主函数不用再一直循环调用AD的值,也不需要等待转换完成,下面给大家演示一下:
void AD_Init()
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//配置ADCCLK,6分频,ADCCLK = 72MHz/6= 12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO,初始化PA0引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
/*在AIN模式下,GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰。
所以AIN模式是ADC的专属模式。*/
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择规则组的输入通道。采样时间是55.5个ADC周期。
//这个函数的意思是,在规则组序列1的位置放入通道0,采样时间是55.5个ADC周期。
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
//配置ADC
ADC_InitTypeDef ADC_InitStruct;
//ADC工作模式,配置独立模式或者双ADC模式。
ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
//数据对齐,选择右对齐。
ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
//外部触发转换选择,就是触发控制的触发源。
//选择不是使用外部触发,就是软件触发的意思。
ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
//连续转换模式,选择连续转换还是单次转换。
ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;
//扫描转换模式,选择扫描模式还是非扫描模式。
ADC_InitStruct.ADC_ScanConvMode=DISABLE;
//通道数目,指定在扫描模式下,总共会用几个通道。
/*其实这个参数只在扫描模式下才需要用,
如果是非扫描模式,那整个列表只有第一个序列有用*/
ADC_InitStruct.ADC_NbrOfChannel=1;
ADC_Init(ADC1,&ADC_InitStruct);
//开启ADC电源
ADC_Cmd(ADC1,ENABLE);
//ADC校准
ADC_ResetCalibration(ADC1);//复位校准
/*获取复位校准状态,所以如果要等待复位完成的话,
还需要加个while循环,如果没有校准完成,就在这个while空循环里一直等待。*/
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);//开启校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准是否完成了。
//软件触发的函数,ADC开启进行转换。内部ADC连续不断地对指定通道转换。
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
uint16_t AD_GetValue()
{
return ADC_GetConversionValue(ADC1);
}
ADC多通道代码

如何实现多通道采集呢?
多通道我们首先想到的应该是后面的两种扫描模式(单扫/连扫)对吧?利用列表,把4个通道填进去,然后触发转换,这样就能实现多通道了,这样确实是一种不错的方法,但是还是那个数据覆盖的问题,如果想要用扫描模式实现多通道,最好要配合DAM来实现,我们下一节讲完DMA之后,再来试一下扫描模式。
那你可能会问,我们一个通道转换完成之后,手动把数据转运出来不就行了?为啥非要用DMA来转运呢?这个方案看似简单,但是实际操作起来会有一些问题,第一个问题就是,在扫描模式下,你启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断,你不知道某一个通道是不是转换完了,他只有在整个列表都转换完成之后,才会产生一次EOC标志位,才能触发中断,而这时,前面的数据就已经覆盖丢失了。第二个问题就是,AD转换是非常快的,刚才我们也计算过,转换一个通道大概只有几us,也就是说,如果你不能在几us之内把数据转运走,那数据就会丢失,这对我们程序手动转运数据,要求就比较高了,所以在扫描模式下,手动转运数据是比较困难的。不过比较困难也不是说手动转运不可行,我们可以使用间断模式,在扫描的时候,每转换一个通道就暂停一次,等我们手动把 数据转运走之后,再继续触发,继续下一次转换,这样可以实现手动转运数据的功能。但是由于单个通道转换完成之后没有标志位,所以启动转换之后,只能通过Delay延时的方式,延时足够长的时间才能保证转运完成,这种方式既不能让我们省心,也不能提高效率,所以不推荐使用。
那这些方法都不行,我们本节是不是就不能实现多通道了呢?
答案是能实现,而且非常简单。我们可以使用单次转换,非扫描模式来实现多通道。只需要在每次触发转换之前,手动更改一下列表的第一个位置的通道就行了。比如,第一次转换,先写入通道0,之后触发、等待、读值;第二次转换,再把通道0改成通道1,之后触发、等待、读值……这样,在转换前,先指定一下通道,再启动转换,就可以轻松的实现多通道转换的功能了。
AD初始化以及AD通道值返回函数:
void AD_Init()
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//配置ADCCLK,6分频,ADCCLK = 72MHz/6= 12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO,初始化PA0引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
/*在AIN模式下,GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰。
所以AIN模式是ADC的专属模式。*/
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 |GPIO_Pin_1 |GPIO_Pin_2 |GPIO_Pin_3;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置ADC
ADC_InitTypeDef ADC_InitStruct;
//ADC工作模式,配置独立模式或者双ADC模式。
ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
//数据对齐,选择右对齐。
ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
//外部触发转换选择,就是触发控制的触发源。
//选择不是使用外部触发,就是软件触发的意思。
ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
//连续转换模式,选择连续转换还是单次转换。
ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
//扫描转换模式,选择扫描模式还是非扫描模式。
ADC_InitStruct.ADC_ScanConvMode=DISABLE;
//通道数目,指定在扫描模式下,总共会用几个通道。
/*其实这个参数只在扫描模式下才需要用,
如果是非扫描模式,那整个列表只有第一个序列有用*/
ADC_InitStruct.ADC_NbrOfChannel=1;
ADC_Init(ADC1,&ADC_InitStruct);
//开启ADC电源
ADC_Cmd(ADC1,ENABLE);
//ADC校准
ADC_ResetCalibration(ADC1);//复位校准
/*获取复位校准状态,所以如果要等待复位完成的话,
还需要加个while循环,如果没有校准完成,就在这个while空循环里一直等待。*/
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);//开启校准
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准是否完成了。
}
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
//选择规则组的输入通道。采样时间是55.5个ADC周期。
//这个函数的意思是,在规则组序列1的位置放入通道x,x经过外部传输得知。
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
//软件触发的函数,ADC开启进行转换。
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//获取标志位函数查看ADC是否转换完成,转化完成后EOC由硬件自动置1,就跳出循环了。
/*具体要等待多长时间呢?我们刚才配置了采样周期是55.5,转换周期是固定的12.5,
加起来一共是68个ADC周期,ADC频率是12MHz,换算是5.67us*/
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
//转化完成后返回转换值。
return ADC_GetConversionValue(ADC1);
//这里读取数据寄存器,会自动清除EOC的标志位,所以不需要再手动清除标志位了。
}
AD多通道逻辑函数:
uint16_t AD0,AD1,AD2,AD3;
int main(void){
OLED_Init();
AD_Init();
OLED_ShowString(1,1,"AD0:");//字符串
OLED_ShowString(2,1,"AD1:");//字符串
OLED_ShowString(3,1,"AD2:");//字符串
OLED_ShowString(4,1,"AD3:");//字符串
while(1)
{
AD0 = AD_GetValue(ADC_Channel_0);
AD1 = AD_GetValue(ADC_Channel_1);
AD2 = AD_GetValue(ADC_Channel_2);
AD3 = AD_GetValue(ADC_Channel_3);
OLED_ShowNum(1,5,AD0,4); //十进制无符号数
OLED_ShowNum(2,5,AD1,4); //十进制无符号数
OLED_ShowNum(3,5,AD2,4); //十进制无符号数
OLED_ShowNum(4,5,AD3,4); //十进制无符号数
}
}