stm32学习笔记---ADC模数转换器(代码部分)AD单通道/多通道

目录

第一个代码: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获取软件开始转换状态,从名字上来看,这个函数好像是判断转换是不是正在进行的。我们是不是可以调用这个函数来判断转换是否已经结束?答案是不行的。这个函数就是用来获取CR2SWSTART这一位。

在手册里可以看到这一位的作用是开始转换规则通道由软件设置该位启动转换转换开始后硬件马上清除此位。

因此,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

本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/756754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索 Electron:将 Web 技术带入桌面应用

Electron是一个开源的桌面应用程序开发框架,它允许开发者使用Web技术(如 HTML、CSS 和 JavaScript)构建跨平台的桌面应用程序,它的出现极大地简化了桌面应用程序的开发流程,让更多的开发者能够利用已有的 Web 开发技能…

iOS17系统适配

iOS17 新功能 文章目录 iOS17 新功能iOS17支持哪几款机型Xcode15新特性iOS17-开发适配指南 横屏待机 在iOS 17中,还带来了横屏待机功能,苹果将这个新功能命名为“Standby”模式,为 iPhone 带来了全新的玩法。iPhone启用之后,默认情…

文件加密|电脑文件夹怎么设置密码?5个文件加密软件,新手必看!

电脑文件夹怎么设置密码?您是否希望更好地在电脑上保护您的个人或敏感文件?设置电脑文件夹密码是一种简单而有效的方式来确保你的隐私不被侵犯。通过使用文件加密软件,您可以轻松地为您的文件和文件夹设置密码保护。因此,本文将介…

快速应用开发(RAD):加速软件开发的关键方法

目录 前言1. 快速应用开发的概念1.1 什么是快速应用开发?1.2 RAD与传统开发方法的对比 2. 快速应用开发的实施步骤2.1 需求分析与规划2.2 快速原型开发2.3 用户评估与反馈2.4 迭代开发与改进2.5 最终交付与维护 3. 快速应用开发的优点与应用场景3.1 优点3.2 应用场景…

Python逻辑控制语句 之 判断语句--if elif else 结构(多重判断)

1.if elif else 的介绍 # if elif else 如果 ... 如果 ... 否则 .... # 多个如果之间存在关系 应用场景:在判断条件时, 需要判断 多个条件, 并且对应不同条件要执行 不同的代码 2.if elif else 的语法 if 判断条件1: 判断条件1成立,执行的代码 elif 判…

React@16.x(44)路由v5.x(9)源码(1)- path-to-regexp

目录 1,作用2,实现获取 match 对象2.1,match 对象的内容2.2,注意点2.3,实现 1,作用 之前在介绍 2.3 match 对象 时,提到了 react-router 使用第3方库 path-to-regexp 来匹配路径正则。 我们也…

【漏洞复现】科立讯通信有限公司指挥调度管理平台uploadgps.php存在SQL注入

0x01 产品简介 科立讯通信指挥调度管理平台是一个专门针对通信行业的管理平台。该产品旨在提供高效的指挥调度和管理解决方案,以帮助通信运营商或相关机构实现更好的运营效率和服务质量。该平台提供强大的指挥调度功能,可以实时监控和管理通信网络设备、…

【Android面试八股文】请描述一下Service的生命周期是什么样的?

文章目录 一、Service的生命周期是什么样的?1.1 通过 `startService` 启动的 Service 生命周期:1.1.1 相关方法说明1.1.2 流程1.1.3 总结1.2 通过 bindService 启动的 Service 生命周期1.2.1 相关方法说明1.2.2 流程1.3 生命周期调用1.4 总结一、Service的生命周期是什么样的…

20240629在飞凌开发板OK3588-C上使用Rockchip原厂的SDK跑通I2C扩展GPIO芯片TCA6424ARGJRR

20240629在飞凌开发板OK3588-C上使用Rockchip原厂的SDK跑通I2C扩展GPIO芯片TCA6424ARGJRR 2024/6/29 18:02 1、替换DTS了: Z:\repo_RK3588_Buildroot20240508\kernel\arch\arm64\boot\dts\rockchip viewproviewpro-ThinkBook-16-G5-IRH:~/repo_RK3588_Buildroot2024…

Unity WebGL项目问题记录

一、资源优化 可通过转换工具配套提供的资源优化工具,将游戏内纹理资源针对webgl导出做优化。 工具入口: 工具介绍 Texture 搜索规则介绍 已开启MipMap: 搜索已开启了MipMap的纹理。 NPOT: 搜索非POT图片。 isReadable: 搜索已开启readable纹理。 …

【机器学习】大模型训练的深入探讨——Fine-tuning技术阐述与Dify平台介绍

目录 引言 Fine-tuning技术的原理阐 预训练模型 迁移学习 模型初始化 模型微调 超参数调整 任务设计 数学模型公式 Dify平台介绍 Dify部署 创建AI 接入大模型api 选择知识库 个人主页链接:东洛的克莱斯韦克-CSDN博客 引言 Fine-tuning技术允许用户根…

Day 48 消息队列集群RabbitMQ

消息队列集群-RabbitMQ 一、消息中间件 中间件 tomcat java web中间件 web容器 mysql php php mysql uwsgi python mysql mycat 数据库中间件 rabbitMQ 消息中间件 1、简介 MQ 全称为(Message Queue消息队列)。是一种应用程序对应用程序的通信方…

Python之父推荐!Star 60k!这本 CPython 书把内部实现全讲透了!

都说 Python 是人工智能的“天选”语言,为什么呢? 可能很多读者都知道,Python 的解释器是用 C 语言写的,所以其实我们在谈论 “Python” 的时候,99.9% 的情况说的就是 “CPython”! CPython 是目前最流行的…

OpenAI推出自我改进AI- CriticGPT

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…

学习gateway网关路由时遇到的问题

遇到这个问题先别慌,我们首先要检查是哪里出问题了,从报错信息中我们可以看到,他说 Unable to find GatewayFilterFactory with name -AddRequestHeader 找不到这个路由过滤器,所以导致网关设置失败,从这条信息上我…

myCrayon个人博客项目基于springBoot+Vue全栈开发

目录 项目介绍 简介 项目架构 项目模块组成 数据库设计 项目展示 首页 用户登录与注册 个人信息模块 商城展示 博客模块 博客浏览 博客发布与编辑 博客搜索 社区模块 新闻模块 后台管理系统 部署方式 结语 项目介绍 简介 项目类似于CSDN,支持所…

【反者道之动,弱者道之用】统计学中的哲理——回归均值 Regression to the mean

💡💡在统计学中,回归均值(Regression toward the Mean/Regression to the Mean) 指的是如果变量在其第一次测量时是极端的,则在第二次测量时会趋向于接近平均值的现象。   在金融学中, 回归均值是指股票价格无论高于…

基于Java毕业生生活用品出售网站的设计和实现(源码+LW+调试文档+讲解等)

💗博主介绍:✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者,博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 🌟文末获取源码数据库🌟 感兴趣的可以先收藏起来,…

个人搭建cppreference网站

近日,由于购买的腾讯云服务器要过期了,之前在服务器搭建的cppreference也要重新搭建,故写下此文章 cppreference的访问速度也慢,故自己WSL子系统简单搭键一下是个不错的选择 环境准备 首先,自己先安装Nginx,在网上找安装教程即可下载cppreference网站资源包:https://pan.baidu…

24/06/24(12.1117)指针进阶 ,冒泡和快排 习题为依托巩固概念(strlen,sizeof,字符串,数组,指针大小的区别)

回调函数 回过头来调用的函数 #include <stdio.h> #include <stdlib.h> int Find_Max(int arr[], int n){ int max_value arr[0]; for (int i 1; i < n; i){ if (max_value < arr[i]) max_value arr[i]; } return…