目录
第一个代码:AD单通道
ADC初始化步骤
ADC相关的库函数
RCC_ADCCLKConfig
三个初始化相关函数
ADC_Cmd
ADC_DMACmd
ADC_ITConfig
四个校准相关函数
ADC_SoftwareStartConvCmd
ADC_GetSoftwareStartConvStatus
ADC_GetFlagStatus
ADC_RegularChannelConfig
ADC_ExternalTrigConvCmd
ADC_GetConversionValue
ADC_GetDualModeConversionValue
九个配置ADC注入组的函数
三个模拟看门狗配置的函数
ADC_TempSensorVrefintCmd
四个获取或清除标志位函数
代码实现
AD.c
第一步,开启RCC时钟
第二步,配置GPIO
第三步,配置多路开关
第四步,配置ADC转换器
第五步,开关控制
第六步,校准
AD.h
Main.c
第二个代码:AD多通道
AD.c
AD.h
Main.c
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
本节我们来学习一下AD转换的代码部分。
第一个代码:AD单通道
接线图:
电位器的内部结构是这样的:
左边和右边的两个引脚接的是电阻的两个固定端,中间这个引脚接的是滑动抽头。电位器外边这里有个十字形状的槽,可以拧,往左拧抽头就往左靠,往右拧,抽头就往右靠。所以外围电路这里,我们把左边的固定端接在负极,右边的固定端接在正极,中间就可以输出,从负极到正极可调的电压了,把可调的电压输出接在PA0。
复制工程并改名:
ADC初始化步骤
AD的初始化看这个结构图
ADC初始化的步骤具体的步骤:
第一步,开启RCC时钟,包括ADC和GPIO的时钟。另外这里ADC CLK的分频器也需要配置一下。
第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式。
第三步,配置多路开关
把左边的通道接入到右边的规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单里。
第四步,配置ADC转换器,在库函数里是用结构体来配置的,可以配置这一大块电路的参数。
包括ADC是单次转换还是连续转换,扫描还是非扫描,有几个通道,触发源是什么,数据对齐是左对齐还是右对齐,这一大批参数用一个结构体配置就可以了。
如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的。
如果想开启中断,就在中断输出控制里用ITconfig函数开启对应的中断输出,然后再在NVIC里配置一下优先级,这样就能触发中断了。
不过模拟看门狗中断我们本节暂时不用。
第五步,开关控制,调用一下ADC_Cmd的函数开启ADC。
这样ADC就配置完成了就能正常工作了。
第六步,校准
当然在开启ADC之后,根据手册里的建议,我们还可以对ADC进行一下校准,这样可以减小误差。在ADC工作的时候,如果想要软件触发转换,会有函数可以触发。如果想读取转换结果,也会有函数,可以读取结果。这个等会介绍扩函数的时候就可以看到了。
ADC相关的库函数
首先我们看一下ADC CLK的配置函数,打开这个rcc.h文件,拖到最后。
RCC_ADCCLKConfig
这个函数是用来配置ADC CLK分频器的。它可以对APB2的72MHz时钟选择二、四、六、八分频,输入到ADC CLK,这就是这个函数的作用。
然后我们找一下ADC的库函数,打开adc.h文件,拖到最后。
三个初始化相关函数
这三个函数和其它模块的库函数一样,都是老朋友,不用多讲了。
ADC_Cmd
这个是用于给ADC上电的,也就是这里的开关控制
ADC_DMACmd
这个是用于开启DMA输出信号的。如果使用DMA转运数据,就得调用这个函数。这个我们下节讲DMA的时候再用。
ADC_ITConfig
中断输出控制,也就是这里用于控制某个中断能不能通过NVIC
四个校准相关函数
接下来这里有四个函数
分别是复位校准、获取复位校准状态、开始校准、获取开始校准状态,这就是用于控制校准的函数。我们在ADC初始化完成之后依次调用就行了。
ADC_SoftwareStartConvCmd
ADC软件开始转换控制,这个就是用于软件触发的函数了,调用一下就能软件触发转换了,也就是这里的触发控制,我们目前使用软件触发。
ADC_GetSoftwareStartConvStatus
ADC获取软件开始转换状态,从名字上来看,这个函数好像是判断转换是不是正在进行的。我们是不是可以调用这个函数来判断转换是否已经结束?答案是不行的。这个函数就是用来获取CR2的SWSTART这一位。
在手册里可以看到这一位的作用是开始转换规则通道,由软件设置该位以启动转换,转换开始后硬件马上清除此位。
因此,ADC_SoftwareStartConvCmd这个函数就是给SWSTART位置1,以开始转换的。
而ADC_GetSoftwareStartConvStatus这个函数是返回SWSTART的状态。
由于SWSTART位在转换开始后立刻清零了。所以这个函数的返回值跟转换是否结束毫无关系。
那如何才能知道转换是否结束?
我们需要用到下面这个函数:
ADC_GetFlagStatus
获取标志位状态,然后参数给EOC的标志位,判断EOC标志位是不是置1了。如果转换结束,EOC标志位置1,然后调用这函数判断标志位。这样才是正确的判断转换是否结束的方法。
所以ADC_GetSoftwareStartConvStatus这个函数其实没啥用,我们一般不用,不要被它误导了。
然后下面这两个函数是用来配置间断模式的。
第一个函数是每隔几个通道间断一次。第二个函数是是不是启用间断模式。需要间断模式的话,可以了解一下。
ADC_RegularChannelConfig
ADC规则组通道配置,这个函数比较重要。它的作用就是给序列的每个位置填写指定的通道,就是填写点菜菜单的过程。
第一个参数是ADCx,第二个ADC channel就是理想指定的通道。第三个rank就是序列几的位置。然后第四个sample time就是指定通道的采样时间。
ADC_ExternalTrigConvCmd
ADC外部触发转换控制,就是是否允许外部触发转换。
ADC_GetConversionValue
ADC获取转换值,这个函数也比较重要,就是获取AD转换的数据寄存器,读取转换结果,就要使用这个函数。
ADC_GetDualModeConversionValue
之后,ADC获取双模式转换值,这个是双ADC模式读取转换结果的函数,我们暂时不用。
以上这些函数就是对ADC的一些基本功能和规则组的配置。
九个配置ADC注入组的函数
然后接下来这里有一大批函数,里面都带了一个injected,就是注入组的意思。
这一大批函数都是对ADC注入组进行配置的。
三个模拟看门狗配置的函数
然后下面的这三个函数就是对模拟看门狗进行配置的。
第一个是是否启动模拟看门狗。第二个是配置高低阈值。第三个是配置看门的通道。
ADC_TempSensorVrefintCmd
ADC温度传感器内部参考电压控制,这个是用来开启内部的两个通道的。如果你要用这两个通道,得调用一下这个函数开启一下,要不然是读不到正确的结果的。
四个获取或清除标志位函数
分别是获取标志位状态、清除标志位、获取中断状态、清除中断挂起位,这些函数也是常用函数了,不用多说。
看完这些函数我们来开始写代码。
代码实现
AD.c
第一步,开启RCC时钟
开启ADC和GPIO的时钟
不要忘了还有一个ADC CLK需要配置,我们到rcc.h来复制一下这个函数
参数有四个取值,分别是二、四、六、八分频。
ADC的CLK=PCLK2/2、4、6、8,这个PCLK2,就是APB2时钟的意思。
我们选择这个六分频。
分频之后,ADC CLK=72MHz/6=12MHz
这样ADC CLK就配置好了。
第二步,配置GPIO
下一步配置GPIO,输入模式这个要改一下,这里要选择AIN模拟输入这个模式。
在AIN模式下,GPIO口是无效的。断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰。所以AIN模式就是ADC的专属模式。
这样PA0引脚就初始化为模拟输入。
第三步,配置多路开关
下一步选择规则组的输入通道
我们需要用到这个函数:
参数第一个ADCx给ADC1。第二个参数是指定通道,这个参数可以是下面的一个值:通道0到通道17
我们选择通道0。
第三个参数rank,解释是规则组序列器里的次序,这个参数必须在一到十六之间。对应的就是规则组这里的十六个序列。
目前只有PA0一个通道,使用的是非扫描的模式。所以这里指定的通道就放在第一个序列一的位置。
还有一个参数指定通道的采样时间,下面就是采样时间的参数,
这个就根据你的需求来,需要更快的转换,就选择小的参数,需要更稳定的转换,就选择大的参数。
如果对速度和稳定性都没啥要求,随便选就可以了。这里我们这个项目没啥要求,所以就随便选这个,这时的采样时间就是55.5个ADC CLK的周期。
这样输入通道就选择好了
现在我们的配置是在规则组菜单列表的第一个位置,写入通道零这个通道,在图里表示的话,就是在这个序列一的位置写入通道0。
如果你还想在序列二的位置写入其它的通道,就复制一下这个代码,
把这个序列数改成二,然后指定你想要的通道,比如通道三、通道八、通道十等等。如果还想继续填充菜单,就再复制修改序列和通道,这样就可以了。
另外每个通道也可以设置不同的采样时间,这个在最后一个参数修改就是了。这就是填充菜单列表的方法。
第四步,配置ADC转换器
接下来用结构体初始化ADC
这里我们需要用到ADC_Init的函数,第一个参数给ADC1,第二个参数是结构体,我们依次看一下结构体成员:
第一个ADC_Mode即ADC的工作模式,配置ADC是工作在独立模式还是双ADC模式,取值范围
其中第一个independent是独立模式,就是ADC1和ADC2各转换各的。剩下的就全是双ADC的模式了。
这里我们就选择第一个独立模式。
接着下一个成员是ADC_DataAlign数据对齐,指定ADC数据是左对齐还是右对齐
取值:
第一个是右对齐,第二个是左对齐,这里我们就选择右对齐
下一个外部触发转换选择
ADC_ExternalTrigConv就是触发控制的触发源,用于启动规则组转换的外部触发源,参数取值:
它们对应的是这个结构框图的外部触发源选择。
这里这些参数都是一一对应的,大家可以看一下。然后这里有个外部触发None,就是不使用外部触发,也就是使用内部软件触发的意思。
我们本节代码使用软件触发,所以就选择这个参数。
接着下面三个成员
第一个ADC_ContinuousConvMode连续转换模式,这个可以选择是连续转换还是单次转换。
第二个ADC_ScanConvMode扫描转换模式,这个可以选择是扫描模式还是非扫描模式。
第三个ADC_NbrOfChannel通道数目,这个是指定在扫描模式下,总共会用到几个通道。
对应上节讲的四种转换模式:
单次转换,非扫描模式;
连续转换,非扫描模式;
单次转换,扫描模式;
连续转换,扫描模式。
通道数目的参数就是这里,扫描模式总共需要扫描几个通道。
这样这三个成员怎么配置,应该就有思路了。
ADC_ContinuousConvMode是指定转换是连续模式还是单次模式。这个参数可以是enable或disable enable,就是连续模式。disable就是单次模式。
ADC_ScanConvMode是指定转换式扫描模式多通道还是非扫描模式单通道,这个参数也是enable或disable,enable就是扫描模式,disable就是非扫描模式。
ADC_NbrOfChannel是指定规则组转换列表里通道的数目,这个参数必须在一到十六之间。
那么我们目前使用的是单次转换、非扫描的模式、一个通道,所以ADC_ContinuousConvMod、ADC_ScanConvMode这两个参数都给disable,ADC_NbrOfChannel给1。
ADC_NbrOfChannel其实这个参数仅在扫描模式下才需要用,如果是非扫描的模式,整个列表就只有第一个序列有效。所以在非扫描的模式下,这个参数其实是没有用的。无论写多少数目,最终都只有序列一的位置有效。
到这里,ADC的整体结构就配置完成了。
如果需要中断和模拟看门狗的话,可以继续配置,我们就暂时不用了。
第五步,开关控制
最后我们可以开启ADC的电源了,调用ADC_Cmd函数。第一个参数ADC1,第二个enable开启ADC的电源。
这样ADC就准备就绪了。
在开启电源之后,根据手册的建议,我们还需要对ADC进行校准。
第六步,校准
这四个函数对应校准的四个步骤:
第一步,调用第一个函数复位校准。
第二步,调用第二个函数等待复位校准完成。
第三步,调用第三个函数开始校准。
第四步,调用第四个函数,等待校正完成。
第二步中获取的标志位和是否校准完成是怎样的对应关系?
这个函数返回值说明是ADC复位校准计存器的状态:set或reset。它获取的就是CR2寄存器里的RSTCAL标志位。
之后就需要参考一下手册的CR2寄存器里看看标志位的说明。
该位由软件设置并由硬件清除,在校准寄存器被初始化后,该位将被清除。所以该位的用法就是软件置该位为1,硬件就会开始复位校准。当复位校准完成后,该位就会有硬件自动清零。
所以我们先给把这一位置1。然后获取复位校准状态,就是读取这一位。所以在读取这一位的时候,如果它是一,就需要一直空循环等待。如果它变为零了,就说明复位校准完成,可以跳出等待。所以这里while的条件就是获取标志位是不是等于set。如果等于set, while条件为真,就会一直空循环。一旦标志位被硬件清零了,这个空循环就会自动跳出来,这样就实现了等待复位校准完成的效果。当然等于set这一步也是可以省略的。因为返回值set,直接作为条件和是不是等于等于set作为条件,效果是一样的。
第三个开始函数校准放参数给ADC1,这样就能启动校准了。之后内部电路就会自动进行校准过程,不需要我们管。
最后我们还需要等待校准完成,调用第四个函数获取校准状态。参数还是ADC1。同样我们也用while把它套起来,循环条件是校准标志位是不是等于set,这样就可以等待校准是否完成了。
到这里,ADC的初始化就已经完成了。
这样ADC就处于准备就绪的状态了。
我们想启动转换获取结果,就可以在下面再写一个函数获取AD转换的值。
获取AD转换的值的函数
在这个函数里我们只要按照这个流程来写就行了
首先软件触发转换,然后等待转换完成,也就是等待EOC标志位置1。最后读取ADC数据寄存器就完了。
我们用这个软件触发转换的函数触发转换
第一个参数给ADC1,第二个新的状态给enable,这样就可以触发ADC,就已经开始进行转换了。
转换需要一段时间,所以我们还需要等待一下,我们需要用到这个获取标志位状态的函数
第一个参数给ADC1,第二个参数有五个取值:
第一个AWD模拟看门狗标志位,第二个EOC规则组转换完成标志位,第三个JEOC注入组转换完成标志位,第四个jstart注入组开始转换标志位,第五个start规则组开始转换标志位。
我们需要判断规则组是不是转换完成了,所以就使用第二个规则组转换完成标注位。
同样我们也需要套一个while空循环来实现一个等待的过程。
返回的标志位set, reset和转换是否完成的对应关系是怎样的?
我们还是参考一下手册的寄存器描述,在状态寄存器里,有这个EOC转换结束标志位
我们获取的就是这个EOC标志位,该位由硬件在规则或注入通道组转换结束时设置。也就是说这个EOC是规则组或注入组完成时都会置1。这一位由软件清除或由读取ADC_DR时清除。ADC_DR是数据寄存器,一般EOC标志位置1我们就会来读取数据,所以它就多设计了一个功能,就是这一位可以在读取数据寄存器之后,自动清除,就不需要你再手动清除了,可以省掉代码。当它为0时表示转换未完成,为1表示转换完成。
所以当EOC标志位等于reset时转换未完成,while条件为真,执行空循环,转换完成后,EOC由硬件自动置1,while循环就自动跳出来。
这样就是等待转换完成的代码。
具体会等待多长时间?
我们刚才配置的时候指定这个通道的采样周期是55.5,转换周期是固定的12.5,加在一起就是68个周期。前面我们配置的ADC CLK是72MHz的六分频就是12MHz。12MHz进行68个周期转换才能完成,最终的时间就是1/12M再乘68,结果大概是5.6us。
所以这个while循环大概会等待5.6us,等待完成之后,我们就可以取结果了。
取结果就用这个函数ADC获取转换值:
这个函数它就是直接读取ADC的DR数据寄存器,参数给ADC1,它的返回值就是AD转换的结果。这里我们可以直接把返回值return过去。
这里因为读取DR寄存器会自动清除EOC标志位,所以这之后我们就不需要再手动清除标志位了。
这样启动、等待读取的过程就写好了。
这样运行结果是拧一下定位器,往右拧数据减小,最小值是零,往左拧数据增大,最大是4095。
注:AD值的末尾会有些抖动,这是正常的波动。
如果你想对这个值进行判断,再执行一些操作,比如光线的AD值小于某一域值就开灯,大于某一域值就关灯,可能会存在这样的情况,比如,光线逐渐变暗,AD值逐渐变小,但是由于波动,AD值会在判断阈值附近来回跳变,这会导致输出产生抖动,现象就会来回开灯、关灯、开灯、关灯。
如何避免这种情况?
这个可以使用迟滞比较的方法来完成,设置两个阈值,低于下阈值时,开灯,高于上阈值时,采光,这就可以避免输出抖动的问题题,这和GPIO一节讲的施密特触发器是一个原理。
另外如果觉得数据跳变太厉害,还可以采用滤波的方法让AD值平滑一些,比如均值滤波,就是读取十个或二十个值取平均值,作为滤波的AD值,或者还可以裁剪分辨率,把数据的尾数去掉,这样也可以减少数据波动,这都是可行的方法,大家实际遇到这方面问题的话,可以考虑一下。
如果想显示一下实际的电压值怎么办?
这只需要对这个数据进行一个线性变换就行了。
我们在上面定义一个变量表示电压
然后我们将转换的结果再进行一下运算Voltage = (float)ADValue / 4095 * 3.3;,这样就能得到电压值。
另外这里要注意因为AD value是整数,在除4095之后会舍弃掉小数部分,这样会导致计算错误。所以我们先把AD value类型强转为float,这样再除才不会出问题。
由于目前我们的OLED驱动还没有显示浮点数的函数(这个之后讲OLED的时候再加)。目前这里我们如果想显示浮点数,可以用显示整数的函数来操作。
如果直接用显示整数的函数的话,小数就会舍弃掉,所以我们要用两个显示整数的函数,将第二个显示整数的函数的值再进行一下处理变成小数显示出来,也就是先把书扩大100倍,比如原来是1.23,现在就是123,然后再对100取余,就是23,这样就把1.23的小数部分取出来了。
另外由于浮点数是不能取余的,所以(Voltage * 100)要括起来,然后再进行强制类型转换变成整数,再对它取余。这样就可以显示浮点数了。
这里实际上AD值等于4096时才对应3.3V负,会有一个数的偏差,所以AD值最大的4095实际上对应的应该是比3.3V小一丢丢,没有办法达到满量程3.3V,这个是受限于ADC的结构,具体就不再细说了。总之就是认为4095对应3.3V伏可以,认为,4096对应3.3V也可以,只有一点点偏差,也看不出来差别。
如果就只是进行阈值判断数据记录的话,也可以不进行变换,直接使用原始的AD数据,这样也是可以的。
AD.c
#include "stm32f10x.h" // Device header
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //连续转换,失能,每转换一次规则组序列后停止
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式,失能,只转换规则组的序列1这一个位置
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*ADC使能*/
ADC_Cmd(ADC1, ENABLE); //使能ADC1,ADC开始运行
/*ADC校准*/
ADC_ResetCalibration(ADC1); //复位校准,固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//返回复位校准的状态,如果没有校准完成就在while循环里等待
ADC_StartCalibration(ADC1);//开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//等待校正完成
}
/**
* 函 数:获取AD转换的值
* 参 数:无
* 返 回 值:AD转换的值,范围:0~4095
*/
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待EOC标志位,即等待AD转换结束
return ADC_GetConversionValue(ADC1); //读数据寄存器,得到AD转换的结果
}
AD.h
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(void);
#endif
Main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t ADValue; //定义AD值变量
float Voltage; //定义电压变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "ADValue:");
OLED_ShowString(2, 1, "Voltage:0.00V");
while (1)
{
ADValue = AD_GetValue(); //获取AD转换的值
Voltage = (float)ADValue / 4095 * 3.3; //将AD值线性变换到0~3.3的范围,表示电压
OLED_ShowNum(1, 9, ADValue, 4); //显示AD值
OLED_ShowNum(2, 9, Voltage, 1); //显示电压值的整数部分
OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2); //显示电压值的小数部分
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间,让数据刷新的慢一些
}
}
运行结果:
STM32-AD单通道
目前我们使用的是第一种转换方式:单次转换、非扫描。
我们还可以使用第二种转换方式:连续转换、非扫描。这个模式的好处就是不需要不断的触发,也不需要等待转换完成的。
这种模式只要对程序稍作修改就行。我们要切换为连续转换模式,那么这个参数就要改成enable。
连续转换仅需要在最开始触发一次就行了,所以这里软件触发转换的函数就可以挪到初始化的最后,即在初始化完成之后,触发一次就行了。
这时内部的ADC就会一次次接着一次连续不断的对我们指定的通道进行转换,转换结果放在数据寄存器里。此时,数据寄存器会不断的刷新最新的转换结果。
所以在这里就不需要判断标志位这行代码了,
直接return数据寄存器的值就行了。
这样程序就是单通道连续转换非扫描的模式。
下载程序现象和刚才是一样的,也能实现单通道的AD转换。这就是连续转换非车描的模式。
第二个代码:AD多通道
接线图:
在这里我们使用了四个AD通道,第一个通道还是电位器,接在PA0口。之后上面又接了三个传感器模块,分别是光敏传感器,热敏传感器、反射式红外传感器,它们的vcc和gnd都分别接在面包板的正负极。然后这个AO就是模拟量的输出引脚,三个模块的AO分别接在PA1,PA2和PA3口,加上电位器的PA0,总共是四个输入通道,同样这些GPIO口也是可以在PA0到PB1之间任意选择的。这里就选择前四个。
复制上一个工程并改名
如何实现多通道采集?
我们首先想到的应该是后面这两种扫描模式
利用这个列表把四个通道都填进去,然后触发转换,这样就能实现多通道了。
这样确实是一种不错的方法,但是有个数据覆盖的问题。
如果想要用扫描模式实现多通道,最好要配合DMA来实现。我们下节讲完DMA之后,再来试一下扫描模式。
那我们一个通道转换完成之后,手动把数据转运出来不就行了吗?为啥非要用DMA来转运?
这个方案看似简单,但是实际操作起来会有一些问题。
第一个问题就是在扫描模式下,启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断。你不知道某一个通道是不是转换完了。它只有在整个列表都转换完成之后,才会产生一次EOC标志位,才能触发中断。而这时前面的数据就已经覆盖丢失了。
第二个问题就是AD转换是非常快的,刚才我们也计算过转换一个通道,大概只有几微秒。也就是说,如果你不能在几微秒的时间内把数据转运走,数据就会丢失,这对我们程序手动转移数据要求就比较高了。
所以在扫描模式下,手动转移数据是比较困难的。不过比较困难,也不是说手动转运不可行,我们可以使用间断模式,在扫描的时候,每转换一个通道就暂停一次,等我们手动把数据转运走之后再继续触发,继续下一次转换。这样可以实现手动转移数据的功能。
但是由于单个通道转换完成之后,没有标志位。所以启动转换完成之后,只能通过Delay延时的方式,延时足够长的时间,才能保证转换完成,这种方式既不能让我们省心,也不能提高效率。所以我们暂时不推荐使用。
这些方法都不行,我们本节是不是就不能实现多通道了?答案是能实现,而且非常简单,怎么实现?
我们可以使用单次转换、非扫描的模式来实现多通道。只需要在每次触发转换之前,手动更改一下列表第一个位置的通道就行了。
比如,第一次转换,先写入通道0,之后触发,等待、读值。第二次转换,再把通道0,改成通道1,之后触发,等待、读值。第三次转换,再先改成通道二修改......这样在转换前先指定一下通道,再启动转换,就可以轻松的实现多通道转换的功能了。
那么我们本次的代码就比较简单,只需要做一些简单的修改就行了。
我们可以把这个填充通道的这一句代码剪切,
然后放到触发转换之前
然后我们想指定的通道,可以作为成AD_GetValue函数的参数
然后这里通道0改成参数指定的通道
这样就行了。
这样我们在调用AD_GetValue进行转换时,只需要指定一个转换的通道,返回值就是我们指定通道的结果了。
接下来我们现在要指定的通道是通道0/1/2/3,所以上面这里的GPIO初始化也不要忘了加上这几个引脚。
AD.c
#include "stm32f10x.h" // Device header
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //连续转换,失能,每转换一次规则组序列后停止
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //扫描模式,失能,只转换规则组的序列1这一个位置
ADC_InitStructure.ADC_NbrOfChannel = 1; //通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*ADC使能*/
ADC_Cmd(ADC1, ENABLE); //使能ADC1,ADC开始运行
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
}
/**
* 函 数:获取AD转换的值
* 参 数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
* 返 回 值:AD转换的值,范围:0~4095
*/
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5); //在每次转换前,根据函数形参灵活更改规则组的通道1
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); //等待EOC标志位,即等待AD转换结束
return ADC_GetConversionValue(ADC1); //读数据寄存器,得到AD转换的结果
}
AD.h
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif
Main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t AD0, AD1, AD2, AD3; //定义AD值变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
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); //单次启动ADC,转换通道0
AD1 = AD_GetValue(ADC_Channel_1); //单次启动ADC,转换通道1
AD2 = AD_GetValue(ADC_Channel_2); //单次启动ADC,转换通道2
AD3 = AD_GetValue(ADC_Channel_3); //单次启动ADC,转换通道3
OLED_ShowNum(1, 5, AD0, 4); //显示通道0的转换结果AD0
OLED_ShowNum(2, 5, AD1, 4); //显示通道1的转换结果AD1
OLED_ShowNum(3, 5, AD2, 4); //显示通道2的转换结果AD2
OLED_ShowNum(4, 5, AD3, 4); //显示通道3的转换结果AD3
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
在主函数里
调用AD_GetValue这个函数读取某个通道的结果,指定通道参数是什么?
我们看一下这个函数的参数取值
这些就是可选的通道,
我们选择通道0/1/2/3。
现在是依次启动四次转换,并且在转换之前指定了转换的通道,每次转换完成之后,把结果分别存在四个数据,最后显示一下,这就是使用单次转换非扫描的模式实现AD多通道的方法,也是一个比较简单直观的方法。
运行结果:
STM32-AD多通道
说明:热敏传感器的不太敏感,所以数据变化不明显,代码是没有问题的。
到这里有关AD转换的代码部分就完成了,AD转换的扫描模式和更高级玩法我们下节再继续学习。
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓