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

news2024/12/25 8:59:12

目录

第一个代码: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.coloradmin.cn/o/1877083.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flask之电子邮件

前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除 目录 一、使用Flask-Mail发送电子邮件 1.1、配置Flask-Mail 1.2、构建邮件数据 1.3、发送邮件 二、使用事务邮件服务SendGrid 2.1、注册SendGr…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] LYA的字符串拼接游戏(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 &#x1f…

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-21多输入多输出通道

21多输入多输出通道 import torch from d2l import torch as d2ldef corr2d(X, K):"""计算二维互相关运算"""h, w K.shapeY torch.zeros((X.shape[0] - h 1, X.shape[1] - w 1))for i in range(Y.shape[0]):for j in range(Y.shape[1]):Y[i,…

【操作系统期末速成】 EP02 | 学习笔记(基于五道口一只鸭)

文章目录 一、前言🚀🚀🚀二、正文:☀️☀️☀️2.1 考点二:操作系统的功能及接口2.2 考点三:操作系统的发展及分类2.3 考点四:操作系统的运行环境(重要) 一、前言&#x…

私域流量的深度解析与电商应用

一、私域流量的核心价值 在当今数字化时代,流量成为了企业发展的重要资源。与公域流量相比,私域流量以其独有的私有性和可复用性,为企业提供了与用户建立深度联系的机会。私域流量不仅有助于企业精准触达目标用户,还能通过数据分…

小白学webgl合集-WebGL中给图片添加背景

一.实现效果 二.逻辑 为了在WebGL中给图片添加背景&#xff0c;主要的逻辑步骤包括初始化WebGL上下文、编写和编译着色器、创建和绑定缓冲区、加载和配置纹理以及绘制场景。以下是代码逻辑的详细说明&#xff1a; 1. 获取WebGL上下文 首先&#xff0c;通过获取<canvas>…

Qt信号槽的坑

1、重载的信号&#xff08;以QSpinBox为例&#xff09; 像是点击按钮之类的信号槽很好连接&#xff0c;这是因为它的信号没有重载&#xff0c;如果像SpinBox那样有重载信号的话&#xff08;Qt5.12的见下图&#xff0c;不过Qt5.15LTS开始就不再重载而是换信号名了&#xff09;&…

【融合ChatGPT等AI模型】Python-GEE遥感云大数据分析、管理与可视化及多领域案例应用

随着航空、航天、近地空间遥感平台的持续发展&#xff0c;遥感技术近年来取得显著进步。遥感数据的空间、时间、光谱分辨率及数据量均大幅提升&#xff0c;呈现出大数据特征。这为相关研究带来了新机遇&#xff0c;但同时也带来巨大挑战。传统的工作站和服务器已无法满足大区域…

微服务实战系列之云原生

前言 话说博主的微服务实战系列从去年走到今天&#xff0c;已过去了半年多了。本系列&#xff0c;博主主要围绕微服务实践过程中的主要组件或工具展开介绍。其中基本覆盖了我们项目或产品研发过程中&#xff0c;经常使用的中间件或第三方工具。至此&#xff0c;该系列也该朝着…

web刷题记录(7)

[HDCTF 2023]SearchMaster 打开环境&#xff0c;首先的提示信息就是告诉我们&#xff0c;可以用post传参的方式来传入参数data 首先考虑的还是rce&#xff0c;但是这里发现&#xff0c;不管输入那种命令&#xff0c;它都会直接显示在中间的那一小行里面&#xff0c;而实际的命令…

ProPainter – AI视频去水印工具,可以去除视频中的静态水印、动态物体/人物等 本地一键整合包下载

ProPainter是一个基于E2FGVI实现的AI视频编辑工具&#xff0c;它可以一键移除视频内的移动物体和水印。这个开源项目提供了一个简单而强大的解决方案&#xff0c;帮助用户轻松编辑和改善视频内容。 项目地址&#xff1a;https://github.com/sczhou/ProPainter 一键包下载&…

Debug 调试代码

我们使用 debug 的目的, 认为就是查看代码的执行过程的。 步骤&#xff1a; 1. 打断点 断点的意义是, debug 运⾏的时候, 代码会在断点处停下来不执行如果是想要查看代码的执行过程, 建议将断点放在第⼀行在代码 和 行号之间 点击,出现的红色圆点 就是断点, 再次点击可以取消 …

最逼真的简易交通灯设计

最逼真的简易交通灯设计 需要资料的请在文章末尾获取&#xff08;有问题可以私信我哦~~&#xff09; 01 资料内容 Proteus仿真文件程序源码实物制作&#xff0c;代码修改&#xff0c;功能定制&#xff08;需额外收费&#xff0c;价格实惠&#xff0c;欢迎咨询&#xff09; …

【ChatBI】text2sql-不需要访问数据表-超轻量Python库Vanna快速上手,对接oneapi

oneapi 准备 首先确保你有oneapi &#xff0c;然后申请 kimi的api 需要去Moonshot AI - 开放平台 然后添加一个api key 然后打开oneapi的渠道界面&#xff0c;添加kimi。 然后点击 测试&#xff0c; 如果能生成响应时间&#xff0c;就是配置正确。 然后创建令牌 http:…

动手学深度学习(Pytorch版)代码实践 -计算机视觉-46语义分割和数据集

46语义分割和数据集 # 图像分割和实例分割 """ 图像分割将图像划分为若干组成区域&#xff0c;这类问题的方法通常利用图像中像素之间的相关性。 它在训练时不需要有关图像像素的标签信息&#xff0c;在预测时也无法保证分割出的区域具有我们希望得到的语义。 图…

知识库在AI大模型中的使用流程

大模型知识库的使用流程通常包括以下关键步骤&#xff0c;大模型知识库的使用流程需要跨学科的知识和技能&#xff0c;包括自然语言处理、数据库管理、软件工程等。同时&#xff0c;也需要关注用户体验、性能优化、安全保护等方面&#xff0c;以提供高质量的知识服务。北京木奇…

# linux 系统中,使用 “ ll “ 命令报错 “ bash ll command not found “ 解决方法:

linux 系统中&#xff0c;使用 " ll " 命令报错 " bash ll command not found " 解决方法&#xff1a; 一、错误描述&#xff1a; 报错原因&#xff1a; 1、这个错误表明你尝试在 bash shell 中执行 ll 命令&#xff0c;但是系统找不到这个命令。ll 通常…

Flask之数据库

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、数据库的分类 1.1、SQL 1.2、NoSQL 1.3、如何选择&#xff1f; 二、ORM魔法 三、使用Flask-SQLALchemy管理数据库 3.1、连接数据库服…

基于自组织长短期记忆神经网络的时间序列预测(MATLAB)

LSTM是为了解决RNN 的梯度消失问题而诞生的特殊循环神经网络。该网络开发了一种异于普通神经元的节点结构&#xff0c;引入了3 个控制门的概念。该节点称为LSTM 单元。LSTM 神经网络避免了梯度消失的情况&#xff0c;能够记忆更长久的历史信息&#xff0c;更能有效地拟合长期时…

STM32学习和实践笔记(38):RTC实时时钟实验

1.STM32F1 RTC介绍 STM32 的实时时钟&#xff08; RTC&#xff09;是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器&#xff0c;在相应软件配置下&#xff0c;可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。 RTC模块和时钟配置…