江协科技STM32学习笔记(第06章 ADC模数转换器)

第06章 ADC模数转换器

6.1 ADC模数转换器

STM32的ADC是12位的,所以AD结果最大值是4095,也就是2^12-1。对于GPIO来说,它只能读取引脚的高低电平,要么是高电平,要么是低电平,只有两个值。而使用了ADC之后,我们就可以对这个高电平和低电平之间的任意电压进行量化,最终用一个变量来表示,读取这个变量,就可以知道引脚的具体电压是多少了。

6.1.1 ADC简介

ADC(Analog-Digital Converter)模拟-数字转换器;

ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁;

反过来DAC就是就数模转换器。

12位逐次逼近型ADC1us转换时间;

从AD转换开始到产生结果,需要花1us的时间,对应AD转换的频率就是1MHz

输入电压范围:0~3.3V,转换结果范围:0~4095;

ADC的输入电压,一般都是要求在芯片供电的负极和正极之间变化的,最低电压就是负极0V,最高电压是正极3.3V,经过ADC转换之后,最小值就是0,最大值是4095,0V对应0,3.3V对应4095,中间都是一一对应的线性关系。

18个输入通道,可测量16个外部和2个内部信号源;

外部信号源就是16个GPIO口,在引脚上直接模拟信号就行了,不需要任何额外的电路,引脚就能直接测电压;2个内部信号源是内部温度传感器和内部参考电压,温度传感器可以测量CPU的温度,比如电脑可以显示一个CPU温度,就可以用ADC读取这个温度传感器来测量。内部参考电压是一个1.2V左右的基准电压,这个基准电压是不随外部供电电压变化而变化的,所以如果芯片的供电不是标准的3.3V,那测量外部引脚的电压可能就不对。这时就可以读取这个基准电压进行校准,这样就能得到正确的电压值了。

规则组和注入组两个转换单元;

这个就是STM32ADC的增强功能了,普通的AD转换流程是,启动一次转换,读一次值,然后再启动,再读值这样的流程,但是STM32的ADC就比较高级,可以列一个组,一次性启动一个组,连续转换多个值;并且有两个组,一个是用于常规使用的规则组,一个是用于突发事件的注入组。

模拟看门狗自动监测输入电压范围;

这个ADC一般可以用于测量光线强度、温度这些值,并且经常会有个需求,就是如果光线高于某个阈值、低于某个阈值;或者温度高于某个阈值、低于某个阈值时,执行一些操作。这个高于某个阈值、低于某个阈值的判断,,就可以用模拟看门狗来自动执行。模拟看门狗可以监测指定的某些通道,当AD值高于它设定的上阈值或者低于下阈值时,它就会申请中断,就可以在中断函数里执行相应的操作,这样就不用不断地手动读值,再用if进行判断了。

STM32F103C8T6 ADC资源:ADC1ADC210个外部输入通道。

也就是最多只能测量10个外设引脚的模拟信号。上面所说的16个外部信号源,这是这个系列最多有16个外部信号源。但是我们这个系列引脚比较少,有很多引脚没有引出来,,所以只有10个外部信号源。如果想要更多的外部通道,可以选择引脚更多的型号,具体有多少个通道,就要参考数据手册了。

6.1.2 逐次逼近型ADC

这个图是ADC0809的内部结构图, 它是一个独立的8位逐次逼近型ADC芯片,在以前的时候,单片机的性能还不是很强,所以需要外挂一个ADC芯片才能进行AD转换,这个ADC0809就是一款比较经典的ADC芯片,现在单片机的性能和集成度都有很大的提升。很多单片机内部就已经继承了ADC外设,这样就不用外挂芯片了,引脚可以直接测电压。

这个结构左边是IN0~IN7,是8路输入通道,通过通道选择开关,选择一路,输出后进行转换。下面是地址锁存器和译码,就是想选择哪一路,就把通道号放在这三个引脚上,然后给一个锁存信号,上面对应的通路开关就可以自动拨好了。上面部分就相当于一个可以通过模拟信号的数据选择器。因为ADC转换是一个很快的过程,给一个开始信号,过一个几us就转换完成了,所以说如果想转换多路信号,不必设计多个AD转换器,只需要一个AD转换器,然后加一个多路选择开关,想转换哪一路,先拨一下开关,选中对应通道,然后再开始转换就行了。这就是输入通道选则的部分。这个ADC0809只有8个输入通道,STM32内部有18个输入通道,所以对应这里就是一个18路输入的多路开关。输入通道选好后,怎么知道对应的数据是多少呢,就需要用逐次逼近的方法来一一比较了。首先是一个电压比较器,它可以判断两个输入信号电压的大小关系,输出一个高电平只是谁大谁小,它的两个输入端,一个是待测的电压,另一个是DAC的电压输出端,DAC是数模转换器,给它一个数据,它就可以输出数据对应的电压,DAC内部是使用加权电阻网络来实现的转换。有了一个外部通道输入的,未知编码的电压,和一个DAC输出的,已知编码的电压,它两1同时输入到电压比较器,进行大小判断,如果DAC输出的电压比较大,就调小DAC输出的数据,如果DAC输出的数据比较小,就增大DAC数据。直到DAC输出的电压和外部通道输入的电压近似相等。这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理。这个电压调节的过程就是这个逐次逼近SAR来完成的。为了最快找到未知电压的编码,通常我们会使用二分法来进行寻找。比如这里是8位的ADC,那编码就是从0~255,第一次比较的时候,我们就给DAC输入255的一半进行比较,那就是128,然后看谁大谁小,如果DAC电压大了,那第二次比较的时候吗,再就给128的一半,64,如果还大,第三次比较的时候就给32,如果这次DAC电压小了,那就给32到64中间的值,依次进行下去,就能最快找到未知电压的编码。并且这个过程,如果用二进制来表示的话,就会发现128、64、32这些数据,正好是二进制每一位的位权,这个判断过程就相当于是,对二进制高位到低位依次判断是1还是0的过程。这就是逐次逼近型名字的来源。那对于8位的ADC,从高位到地位依次判断8次就能找到未知电压的编码了。对于12位ADC,就需要依次判断12次,这就是逐次逼近的过程。然后,ADC转换结束后,DAC的输入数据,就是未知电压的编码,往右走进行输出,8位就有8根线,12位就有12根线。EOC是End of Convert,转换结束信号,START是开始转换,给一个输入脉冲,开始转换,CLOCK是ADC时钟,因为ADC内部是一步一步进行判断的,所以需要时钟来推动这个过程。下面VREF+和VREF-是DAC的参考电压,比如给一个数据255,对应5V还是3.3V,就由这个参考电压决定。这个DAC的参考电压也决定了ADC的输入范围,所以它也是ADC参考电压。最后左边是整个芯片的供电,VCC和GND,通常参考电压的正极和VCC是一样的,会接在一起,参考电压的负极和GND也是一样的,也接在一起。所以一般情况下,ADC输入电压的范围和ADC的供电是一样的。

6.1.3 STM32ADC框图

左边是ADC的输入通道,包括16个GPIO口,IN0~IN15 ,和两个内部二点通道,一个是内部温度传感器,另一个是VREFINT(V Reference Internal),内部参考电压,总共是18个输入通道,然后到达模拟多路开关,可以指定我们想要选则的通道,右边是多路开关的输出,进入到模数转换器。这里模数转换器就是执行刚刚上面讲的逐次比较的过程。转换结果会直接放在数据寄存器里,我们读取寄存器就能直到ADC转换的结果了。对于普通的ADC,多路开关一般是只选中1个的,就是选中某个通道、开始转换、等待转换完成、取出结果、这是普通的流程。但是STM32上就比较高级了,它可以同时选中多个,而且在转换的时候,还分成了两个组,规则通道组和注入通道组;其中规则组可以一次性最多选择中16个通道,注入组最多可以选中4个通道。这有什么作用呢?举个例子:就像是去餐厅点菜、普通的ADC是,指定一个菜,老板做完后送给自己,这里就是,指定一个菜单,这个菜单最多可以填16个菜,老板按照菜单的顺序依次做好,一次性端上来,这样就大大提高效率,当然,菜单也可以只写一个菜,就简化成普通的模式了。对于这个菜单也有两种,1种是规则组,可以同时上16个菜,但是有个尴尬的地方,就是规则组只有一个数据寄存器,就是这个桌子比较小,只能放一个菜,如果上16个菜,前15个菜就会被挤掉。所以对于规则组来说,如果使用这个菜单的话,最好配合DMA来实现。DMA是一个数据转运小帮手,它可以在每上一个菜之后,把这个菜挪到其它地方去,防止被覆盖。规则组虽然可以同时转换16个通道,但是数据寄存器只能存一个结果,如果不想之前的结果被覆盖,那就转换完成之后,要尽快把结果拿走。注入组,这个组就比较高级,相当于是餐厅的VIP座位,在这个座位上,一次性最多可以点4个菜,并且数据寄存器有4个,是可以同时上4个菜的,对于注入组而言,就不用担心数据覆盖的问题了。这就是规则组和注入组的介绍。一般情况下,使用规则组就完全足够了,如果要使用规则组的菜单,那就再配合DMA转运数据。

模数转换器外围电路:左下角是触发转换的部分,也就是ADC0809的START。对于STM32的ADC,触发ADC开始转换的信号有两种,一种是软件触发,就是再程序中手动调用一条代码,就可以启动转换了;另一种是硬件触发,就是左下角的触发源。上面是注入组的触发源,下面是规则组的触发源,这些触发源主要是来自定时器,有定时器的各个通道,还有TRGO定时器主模式的输出。之前讲定时器的时候也介绍过,定时器可以通过ADC、DAC这些外设,用于触发转换。那因为ADC经常要过一个固定时间段转换一次,比如每隔1ms转换一次,正常的思路就是,用定时器,每隔1ms申请1次中断,在中断里手动开始一次转换,这样也是可以的。但是频繁进中断对程序是有一定影响的,比如有很多中断都需要频繁进入,肯定会影响主程序的执行,并且不同中断之间,由于优先级的不同,也会导致某些中断不能及时得到响应,如果触发ADC的中断不能及时响应,那我们ADC的转换频率就肯定会产生影响了,所以对于这种需要频繁进中断,并且在中断里只完成了简单工作的情况,一般都会有硬件的支持。比如这里就可以给TIM3定一个1ms的时间,并且把TIM3的更新事件选择位TRGO输出,然后在ADC这里,选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换了,整个过程不需要进中断,节省了中断资源。这就是这里定时器触发的作用。当然这里还可以选择外部中断引脚来触发转换,都可以在程序中配置,这就是触发转换的部分。

左上角是VREF+、VREF-、VDDA和VSSA,上面两个是ADC的参考电压,决定了ADC输入电压的范围,下面两个是ADC的供电引脚,一般情况下,VREF+要接VDDA、VREF-要接VSSA,在这款芯片上,没有VREF+和VREF-的引脚,它在内部就已经和VDDA和VSSA接在一起了。VDDA和VSSA在引脚定义里就可以看到。VDDA和VSSA是内部模拟部分的电源,比如ADC、RC振荡器、锁相环等等,在这里VDDA接3.3V、VSSA接GND、所以ADC的输入电压范围就是0~3.3V。

右边ADCCLK是ADC的时钟,也就是ADC0809上的CLOCK,是用于驱动内部逐次比较的时钟。这个是来自ADC预分频器,这个ADC预分频器是来源于RCC的。如下图(RCC时钟树框图)所示:APB272MHz通过预分频器进行分频,得到ADCCLK,ADCCLK最大是14MHz,所以这个预分频器就有点尴尬,它可以选择2、4、6、8分频,如果选择2分频,72M/2=36M,超出允许范围了;4分频之后是18M,也超了;所以对于ADC预分频器,只能选择6分频,结果是12M和8分频、结果是9M,这两个值。

DMA请求是用于触发DMA进行数据转运的。 

两个数据寄存器是用于存放转换结果的,上面还有一个模拟看门狗,里面可以存一个阈值高限和阈值低限,如果启动了模拟看门狗,并指定了看门的通道,那这个看门狗就会关注它看门的通道、一旦超过这个阈值范围了,它就会乱叫,就会在上面申请一个模拟看门狗的中断,最后通向NVIC。然后对于规则组和注入组而言,它们转换完成后,也会有一个EOC转换完成的信号,在这里EOC是规则组的完成信号,JEOC是注入组完成的信号,这两个信号会在状态寄存器里置一个标志位,读取这个标志位,就能知道是不是转换结束了,同时这两个标志位也可以去到NVIC,申请中断,如果开启了NVIC对应的通道,它们就会触发中断。

6.1.4 ADC基本结构图

 左边是输入通道,16个GPIO口、外加两个内部的通道;然后进入AD转换器、AD转换器里有两个组、一个是规则组、一个是注入组。规则组最多可以选中16个通道、注入组最多可以选择4个通道,然后转换的结果可以存放在AD数据寄存器里,其中规则组只有一个数据寄存器,注入组有4个,下面有触发控制、提供了开始转换这个START信号,触发控制可以选择软件触发和硬件触发,硬件触发主要来自于定时器,当然也可以选择外部中断的引脚。右边是来自RCC的ADC时钟CLOCK,ADC逐次比较的过程就是由这个时钟推动的。然后上面可以布置一个模拟看门狗用于监测转换结果的范围,如果超出设定的阈值,就通过中断输出控制,向NVIC申请中断,另外、规则组和注入组转换完成后会有个EOC信号,它会置一个标志位,当然也可以通向NVIC。最后右下角还有一个开关控制,在库函数种,就是ADC_Cmd函数,用于给ADC上电的。

6.1.5 输入通道 

通道

ADC1

ADC2

ADC3

通道0

PA0

PA0

PA0

通道1

PA1

PA1

PA1

通道2

PA2

PA2

PA2

通道3

PA3

PA3

PA3

通道4

PA4

PA4

PF6

通道5

PA5

PA5

PF7

通道6

PA6

PA6

PF8

通道7

PA7

PA7

PF9

通道8

PB0

PB0

PF10

通道9

PB1

PB1

通道10

PC0

PC0

PC0

通道11

PC1

PC1

PC1

通道12

PC2

PC2

PC2

通道13

PC3

PC3

PC3

通道14

PC4

PC4

通道15

PC5

PC5

通道16

温度传感器

通道17

内部参考电压

总共有18个通道、通道16对应ADC1的温度传感器,通道17对应ADC1的内部参考电压。我们使用的这款芯片没有ADC3,这颗芯片也没有PC0到PC5。

上表从引脚定义表也可以看出来,下图所示ADC12_IN0的意思是ADC1和ADC2的IN0都是在PA0上的。而且下面也是全都是ADC12,这说明ADC1和ADC2的引脚全都是相同的,既然都相同,ADC2还有啥用呢?这就是因为ADC的另一个高级功能了,就是双ADC模式,这个模式比较复杂,双ADC模式就是ADC1和ADC2一起工作,它两可以配合组成同步模式、交叉模式等等模式,比如交叉模式、ADC1和ADC2交叉地对一个通道进行采样,这样就可以进一步提高采样率。就像打拳一样,左手一拳右手一拳快速交叉地打拳,打击地频率就比一个拳头快。这就是ADC1和ADC2配合使用的双ADC模式了。ADC1和ADC2也是可以分开使用的,可以分别对不同的引脚进行采样,也是可以的。

6.1.6 规则组的4种转换模式 

在ADC初始化的结构体里,会有两个参数,一个是选择单次转换还是连续转换的,另一个是选择扫描模式还是非扫描模式的。这两个参数组合,就是以下4种转换方式。

6.1.6.1 单次转换、非扫描模式

上图所示列表相当于规则组里的菜单,可以在这里“点菜”,就是写入要转换的通道,在非扫描的模式下,这个菜单就只有第一个序列1的位置有效,这是菜单同时选中1组的方式就退化为简单地选中一个的方式了,在这里我们可以在序列1的位置指定我们想要转换的通道,比如通道2,然后就可以触发转换,ADC就会对这个通道2进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给ROC标志位置1,整个转换过程就结束了。我们判断这个EOC标志位,如果转换完了,那我们就可以在数据寄存器里读取结果了,如果想再启动一次转换,就需要再触发一次,转换结束,置EOC标志位,读结果。如果想换一个通道转换,那在转换之前,把第一个位置的通道2改成其它通道,然后再启动转换就行了。

6.1.6.2 连续转换、非扫描模式

还是非扫描模式,所以菜单列表只选择一个。与单次转换不同的是,在一次转换后不会停止,而是立刻开始下一轮的转换,然后一直持续下去。这样就可以只需要最开始触发一次,之后就可以一直转换了。这个模式的好处就是,开始转换之后不需要等待一段时间的,因为它一直都在转换,所以就不需要手动开始转换了,也不用判断是否结束的,想要读AD值的时候,直接从数据寄存器取就是了。

6.1.6.3 单次转换、扫描模式

这个模式也是单次转换,所以每转换一次,转换结束后,就会停下来, 下次转换就得再触发才能开始,然后它是扫描模式,这就会用到菜单列表了,可以在菜单里点菜,比如第一道是通道2,第二道是通道5等等,这里每个位置是通道几可以任意指定,并且也是可以重复的,然后初始化结构体里还会有个参数,就是通道数目,因为这16个位置可以不用完,只用前几个,那就需要再给一个通道数目的参数,高速它,我有几个通道,比如这里指定通道数目为7,那它就只看前7个位置。然后每次触发之后,就依次对这前7个位置进行AD转换,转换结果都放在数据寄存器里,这里为了放在之数据被覆盖,就需要用DMA及时讲数据挪走,那7个通道转换完成之后,产生EOC信号,转换结束,然后再触发下一次,就又开始新一轮的转换。

6.1.6.4 连续转换、扫描模式 

在扫描模式的情况下,还可以有另一种模式,叫间断模式,它的作用是,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。

6.1.7 触发控制

上图是规则组的触发源, 也就是下图标注部分。有来自定时器的信号,还有来自外部引脚或定时器的信号,这个具体是引脚还是定时器,需要用AFIO重映射来确定,最后是软件控制位,也就是我们之前说的软件触发,这些触发信号的选择,可以通过设置右边所示的寄存器来完成,使用库函数的话直接给一个参数就可以了。

6.1.8 数据对齐 

我们这个ADC是12位的,它的转换结果就是一个12位的数据,但是这个数据寄存器是16位的,所以就存在一个数据对齐的问题。一般使用数据右对齐,这样读取的16位寄存器,直接就是转换结果。选择左对齐得到的结果会比实际的大,因为数据左对齐就是把数据左移了4次。二进制有个特点,就是数据左移一次,就等效于把这个数据乘2,左移4次就相当于把结果乘16了,直接读的话会比实际值大16倍。左对齐的作用就是,如果不想要这么右对齐这么高的分辨率,觉得0~4095数太大了,就做个简单的判断,不需要这么高分辨率,就可以选择左对齐,然后再把这个数据的高8位取出来,这样就舍弃了后面4个精度,这样12位的ADC就退化成了8位的ADC了。

数据右对齐:

数据左对齐:

6.1.9 转换时间

转换时间这个参数,一般不太敏感,因为一般AD转换都很快,若果不需要非常高速的转换频率,那转换时间就可以忽略了。AD转换是需要一小段时间的。

AD转换的步骤:采样,保持,量化,编码

采样保持可以放在一起,量化编码可以放在一起,总共是这两大步。量化编码就是之前讲过的ADC逐次比较的过程,这个是需要花一段时间的,一般位数越多,花的时间就越长;

采样保持:这是因为AD转换,就是后面的量化编码,需要一小段时间的,如果在这一小段时间里,输入的电压还在不断变化、那就没办法定位输入电压到底是在哪里了,所以在量化编码之,需要设置一个采样开关,先打开采样开关,收集一下外部的电压,比如可以用一个小容量的电容存储一下这个电压,存储好了之后,断开采样开关,再进行后面的AD转换,这样在量化编码期间,电压始终保持不变,这样才能精确定位未知电压的位置,这就是采样保持电路。采样保持的过程需要闭合采样开关、过一段时间再断开,这里就会产生一个采样时间。

STM32 ADC的总转换时间为:

TCONV = 采样时间 + 12.5ADC周期

采样时间就是采样保持花费的时间,这个时间可以在程序中进行配置,采样时间越大,越能避免一些毛刺信号的干扰,不过转换时间也会相应延长。12.5个ADC周期是量化编码所花费的时间,因为是12位的ADC,所以需要花费12个ADC,这里多半个周期,可能是做一些其它东西花的时间。ADC周期就是从RCC分频过来的ADCCLK,这个ADCCLK最大是14MHz。

例如:当ADCCLK=14MHz,采样时间为1.5ADC周期

  TCONV = 1.5 + 12.5 = 14ADC周期 = 1μs

这就是最快1us时间的来源,如若过采样周期再长一些,它就达不到1us了。 也可以把ADCCLK的时钟设置超过14MHz,这样ADC就是在超频了,那转换时间可以比1us还短,不过这样稳定性就没办法保证了。

6.1.10 校准

ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差。

建议在每次上电后执行一次校准。

启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期。

这个校准过程是固定的,我们只需要在ADC初始化的最后,加几条代码就行了。

6.1.11 硬件电路

如何设计ADC的外围电路:

上图第一个是电位器产生1个可调的电压,这里电位器的两个固定端、一端接3.3V、另一端接GND,这样中间的滑动段就可以输出一个0~3.3V可调的电压输出了。右边可以接ADC的输入通道,比如PA0口,当滑动端往上滑时,电压增大,往下滑时,电压减小。注意电阻的阻值不要给太小,因为这个电阻两端也是直接跨接在电源正负极的,如果阻值太小,这个电阻就会比较费电,再小就有可能发热冒烟了。一般至少要接KΩ级的电阻,比如这里接的10K的电阻。

第二个是传感器产生输出电压的电路,一般来说,像光敏电阻、热敏电阻、红外接收管、麦克风等等。都可以等效为一个可变电阻,电阻值没办法直接测量,所以可以通过和一个固定电阻串联分压,来得到一个反应电阻值电压的电路,这里传感器阻值变小时,下拉作用强,输出电压就下降;传感器阻值变大时,下拉作用变弱,输出端受上拉电阻的作用,电压就会升高。这个固定电阻一般可选择和传感器阻值相近的电阻,这样可以得到一个位于中间电压区域比较好的输出。传感器和固定电阻的位置也可以缓过来,这样输出电压的极性就反过来了。

 第3个电路是一个简单的电压转换电路,比如想要测一个0~5V的VIN电压,但是ADC只能接收0~3.3V的电压,就可以搭建这样的简易转换电路。在这里还是使用电阻进行分压,上面阻值17K,下面阻值33K,加一起50K,中间的电压就是0~3.3V,就可以进入ADC转换了。如果采集的是5V、10V就可以采用这个电路1了。比这个还高就不能使用了,高电压采集最好还是使用一些专用的采集芯片,比如隔离放大器等等,做好高低电压的隔离,保证电路的安全。

6.2 AD单通道 

6.2.1 硬件电路

只有ADC通道能接模拟电压,所以连接时候要注意选择的引脚是否是ADC通道。 

6.2.2 软件部分

(1)复制《OLED显示屏》工程并改名为《AD单通道》

(2)添加驱动文件

(3)ADC的初始化及相关库函数

/*RCC库函数里*/
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);    // 配置ADCCLK分频器的,可以对APB2的72MHz时钟选择2、4、6、8分频,输入到ADCCLK

/*ADC库函数里*/
void ADC_DeInit(ADC_TypeDef* ADCx);        // 恢复缺省配置
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);   // 初始化
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);                // 结构体初始化
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);           // 用于给ADC上电,也就是开关控制
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);        // 用于开启DMA输出信号,若果使用DMA转运数据,就得调用这个函数
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);   // 中断输出控制,用于控制某个中断,能不能通往NVIC
/*下面4个函数是用于获取控制校准的函数、在ADC初始化完成之后,一次调用就可以了*/
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);    
// ADC_软件开始转换控制,这个就是用于软件触发的函数了,调用一下,就能软件触发转换了
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx); 
//ADC获取软件开始转换状态,给SWSTART位置1,以开始转换,这个函数一般不用
/*下面2个函数是用来配置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规则组通道配置,很重要,作用就是给序列的每个位置填写指定的通道
ADC_Channel:要指定的通道;
Rank:序列几的位置;
ADC_SampleTime:指定通道的采样时间。
*/
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
//ADC外部触发转换控制,就是是否允许外部触发转换
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);  // ADC获取转换值,获取ADC转换的数据寄存器,读取转换结果就是要用这个函数
uint32_t ADC_GetDualModeConversionValue(void);//ADC获取双模式转换通道,这个是双ADC模式读取转换结果的函数,暂时不用
/*下面带Injected的函数都是对ADC注入组进行配置的*/
void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv);
void ADC_ExternalTrigInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_SoftwareStartInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
FlagStatus ADC_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef* ADCx);
void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length);
void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset);
uint16_t ADC_GetInjectedConversionValue(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel);
/*下面三个函数是对模拟看门狗进行配置的*/ 
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);//ADC温度传感器、内部参考电压控制,用来开启内部的两个通道的
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);   
//获取标志位状态,参数给EOC标志位,就可以判断EOC标志位是不是置1了,如果转换结束,EOC标志位置1
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);  //清除中断挂起位

(4)AD.c

①单次转换,非扫描模式

#include "stm32f10x.h"                  // Device header

/*初始化函数:
第1步:开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK的分频器,也需要配置一下;
第2步:配置GPIO,把需要用的GPIO配置成模拟输入的模式;
第3步:配置多路开关,把最左边的通道接入右边的规则组列表里;
第4步:配置ADC转换器,库函数里使用结构体配置,包括ADC是单次转换还是连续转换、
扫描还是非扫描、有几个通道、触发源是什么、数据对齐是左对齐还是右对齐

如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的;如果想开启中断,那就在中断输出控制
里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置中断优先级,这样就能触发中断了。
第5步:开关控制,调用一下ADC_Cmd函数,开启ADC,这样ADC就配置完成了。开启ADC之后,
还可以对ADC进行校准、这样可以减小误差。

在ADC工作的时候,如果想要触发转换,会有函数可以触发;如果想读取结果,也会有函数可以读取结果。
*/
void AD_Init(void)
{
	/*第1步:开启RCC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);           //开启ADC1的时钟控制
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);          //要使用PA0口采样
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);                             //配置APB2六分频,分频后ADCCLK=72MHz/6=12MHz
	/*第2步:配置GPIO*/
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	/*配置GPIO口是模拟输入模式,AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰;
	所以AIN模式是ADC的专属模式*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_SetBits(GPIOA,GPIO_Pin_0);     
	/*第3步:配置多路开关*/
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	/*ADC_Channel_0:通道0,1:规则组序列里的次序,ADC_SampleTime_55Cycles5:采样时间55.5个ADCCLK的周期*/
	/*第4步:配置ADC转换器*/
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; //配置ADC工作在独立模式
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;  //配置ADC数据对齐为右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //配置不使用外部触发,使用内部软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;     //配置转换模式为单次转换
	ADC_InitStruct.ADC_ScanConvMode = DISABLE; //配置扫描模式为非扫描
	ADC_InitStruct.ADC_NbrOfChannel = 1;    //目前是1个通道,给1,这个参数仅在扫描模式下可用,非扫描模式,整个列表只有第一个序列有用
	ADC_Init(ADC1,&ADC_InitStruct);
	/*第5步:开关控制,开启ADC的电源*/
	ADC_Cmd(ADC1,ENABLE);
	ADC_ResetCalibration(ADC1);           //复位校准
	ADC_GetResetCalibrationStatus(ADC1);  //返回复位校准的状态
	while(ADC_GetResetCalibrationStatus(ADC1) == SET); //读取这一位,如果它是1,那就需要一直空循环等待;如果它变为0,那就说明复位校准完成,可以跳出等待了	
	ADC_StartCalibration(ADC1);           //启动校准,之后内部电路会自动校准
	ADC_GetCalibrationStatus(ADC1);       //获取校准状态
	while(ADC_GetCalibrationStatus(ADC1) == SET); //判断校准状态是否完成
}
	

uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);     //软件触发转换,启动
	while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);   //获取标志位状态并等待完成,ADC_FLAG_EOC:规则组转换完成标志位,自动清除标志位,等待
	/*
	ADC_FLAG_AWD:模拟看门狗标志位
	ADC_FLAG_EOC:规则组转换完成标志位
	ADC_FLAG_JEOC: 注入组转换完成标志位
    ADC_FLAG_JSTRT: 注入组开始转换标志位
	ADC_FLAG_STRT: 规则组开始转换标志位
	*/
	/*具体等待时间:采样周期:55.5、转换周期12.5,12MHz进行68个周期转换完成*/
	return ADC_GetConversionValue(ADC1);     //返回转换值
}

②连续转换,非扫描模式

#include "stm32f10x.h"                  // Device header

/*初始化函数:
第1步:开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK的分频器,也需要配置一下;
第2步:配置GPIO,把需要用的GPIO配置成模拟输入的模式;
第3步:配置多路开关,把最左边的通道接入右边的规则组列表里;
第4步:配置ADC转换器,库函数里使用结构体配置,包括ADC是单次转换还是连续转换、
扫描还是非扫描、有几个通道、触发源是什么、数据对齐是左对齐还是右对齐

如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的;如果想开启中断,那就在中断输出控制
里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置中断优先级,这样就能触发中断了。
第5步:开关控制,调用一下ADC_Cmd函数,开启ADC,这样ADC就配置完成了。开启ADC之后,
还可以对ADC进行校准、这样可以减小误差。

在ADC工作的时候,如果想要触发转换,会有函数可以触发;如果想读取结果,也会有函数可以读取结果。
*/
void AD_Init(void)
{
	/*第1步:开启RCC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);           //开启ADC1的时钟控制
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);          //要使用PA0口采样
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);                             //配置APB2六分频,分频后ADCCLK=72MHz/6=12MHz
	/*第2步:配置GPIO*/
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	/*配置GPIO口是模拟输入模式,AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰;
	所以AIN模式是ADC的专属模式*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_SetBits(GPIOA,GPIO_Pin_0);     
	/*第3步:配置多路开关*/
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	/*ADC_Channel_0:通道0,1:规则组序列里的次序,ADC_SampleTime_55Cycles5:采样时间55.5个ADCCLK的周期*/
	/*第4步:配置ADC转换器*/
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; //配置ADC工作在独立模式
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;  //配置ADC数据对齐为右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //配置不使用外部触发,使用内部软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;     //配置转换模式为连续转换
	ADC_InitStruct.ADC_ScanConvMode = DISABLE; //配置扫描模式为非扫描
	ADC_InitStruct.ADC_NbrOfChannel = 1;    //目前是1个通道,给1,这个参数仅在扫描模式下可用,非扫描模式,整个列表只有第一个序列有用
	ADC_Init(ADC1,&ADC_InitStruct);
	/*第5步:开关控制,开启ADC的电源*/
	ADC_Cmd(ADC1,ENABLE);
	ADC_ResetCalibration(ADC1);           //复位校准
	ADC_GetResetCalibrationStatus(ADC1);  //返回复位校准的状态
	while(ADC_GetResetCalibrationStatus(ADC1) == SET); //读取这一位,如果它是1,那就需要一直空循环等待;如果它变为0,那就说明复位校准完成,可以跳出等待了	
	ADC_StartCalibration(ADC1);           //启动校准,之后内部电路会自动校准
	ADC_GetCalibrationStatus(ADC1);       //获取校准状态
	while(ADC_GetCalibrationStatus(ADC1) == SET); //判断校准状态是否完成
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);     //软件触发转换,启动,使用连续转换的时候放到初始化函数里就可以了
}
	

uint16_t AD_GetValue(void)
{
//	ADC_SoftwareStartConvCmd(ADC1,ENABLE);     //软件触发转换,启动
//	while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);   //获取标志位状态并等待完成,ADC_FLAG_EOC:规则组转换完成标志位,自动清除标志位,等待
	/*
	ADC_FLAG_AWD:模拟看门狗标志位
	ADC_FLAG_EOC:规则组转换完成标志位
	ADC_FLAG_JEOC: 注入组转换完成标志位
    ADC_FLAG_JSTRT: 注入组开始转换标志位
	ADC_FLAG_STRT: 规则组开始转换标志位
	*/
	/*具体等待时间:采样周期:55.5、转换周期12.5,12MHz进行68个周期转换完成*/
	//连续转换时,就不需要配置状态标志位了
	return ADC_GetConversionValue(ADC1);     //返回转换值
}

(5)AD.h

#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(void);
#endif

(6)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "AD.h" 

uint16_t ADValue;
float Voltage;

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	AD_Init();
	OLED_ShowString(1,1,"ADValue:");
	OLED_ShowString(2,1,"Voltage: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);   //当前OLED驱动程序不能直接显示浮点数,因此处理一下
		Delay_ms(100);
	}
}

AD值得末尾数据会有一些抖动,这是正常的波动, 如果想对这个值进行判断,再执行一些操作,比如光线得AD值小于某一阈值,就开灯,大于某一阈值,就关灯。可能会存在这样的情况,比如光线逐渐变暗,AD值逐渐减小,但是由于波动,AD值会在判断阈值附近来回跳变,这会导致输出产生抖动,来回开灯关灯开灯关灯。为了避免这种情况,可以使用迟滞比较的方法来完成,设置两个阈值,低于下阈值时,开灯,高于上阈值时,才关灯,这样就可以避免输出抖动的问题了。和GPIO那一节讲的施密特触发器是一样的原理。如果觉得数据跳变太厉害,还可以采取滤波的方法,让AD值平滑一些,比如均值滤波,就是读取10个或20个AD值,取平均值,作为滤波的AD值,或者还可以裁剪分辨率,把数据的尾数去掉,这样也可以减少数据波动。

6.3 AD多通道

6.3.1 硬件电路

6.3.2 软件部分

(1)复制《AD单通道》工程改名为《AD多通道》

(2)转换模式选择

多通道可以使用扫描模式,但是会有数据覆盖的问题,用这种方式需要配合DMA来实现。一个通道转换完成之后,手动把数据转运出来这种方案看似简单,实际会出现一些问题。第一个问题就是在扫描模式下,启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断,不知道某一个通道是不是转换完了,它只有在两个列表都转换完成之后,才会产生一次EOC标志位,才能触发中断,而这时候前面的数据已经覆盖丢失了。第二个问题就是AD转换是非常快的,转换一个通道只有几us,也就是说,如果不能在几us的时间内把数据转运走,拿数据就会丢失,这对我们手动转运程序,要求就比较高了,所以在扫描模式下,手动转运数据是比较困难的。不过手动转运也不是不可行,我们可以使用间断模式,在扫描的时候,每转换一个通道就暂停一次,等我们手动把数据转运走之后,再继续触发,继续下一次转换,这样可以实现手动转运数据的功能,但是由于单个通道转换完成之后,没有标志位,所以启动完成之后,只能通过Delay延时的方式,延时足够长的时间,才能保证转换完成。这种方式既不省心,也不能提高效率,所以不推荐使用。

我们可以使用单次转换,非扫描的模式来实现多通道,只需要在每次触发转换之前,手动更改一下列表第一个位置的通道就可以了。比如第一次转换,先写入通道0,之后触发、等待、读值;第二次转换,再把通道0改成通道1、之后触发、等待、读值;第三次转换,先改成通道2,等等等。

(3)AD.c

#include "stm32f10x.h"                  // Device header

/*初始化函数:
第1步:开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK的分频器,也需要配置一下;
第2步:配置GPIO,把需要用的GPIO配置成模拟输入的模式;
第3步:配置多路开关,把最左边的通道接入右边的规则组列表里,使用多个通道,再触发转换函数里实现;
第4步:配置ADC转换器,库函数里使用结构体配置,包括ADC是单次转换还是连续转换、
扫描还是非扫描、有几个通道、触发源是什么、数据对齐是左对齐还是右对齐

如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的;如果想开启中断,那就在中断输出控制
里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置中断优先级,这样就能触发中断了。
第5步:开关控制,调用一下ADC_Cmd函数,开启ADC,这样ADC就配置完成了。开启ADC之后,
还可以对ADC进行校准、这样可以减小误差。

在ADC工作的时候,如果想要触发转换,会有函数可以触发;如果想读取结果,也会有函数可以读取结果。
*/
void AD_Init(void)
{
	/*第1步:开启RCC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);           //开启ADC1的时钟控制
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);          //要使用PA0口采样
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);                             //配置APB2六分频,分频后ADCCLK=72MHz/6=12MHz
	/*第2步:配置GPIO*/
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	/*配置GPIO口是模拟输入模式,AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰;
	所以AIN模式是ADC的专属模式*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);     
	/*ADC_Channel_0:通道0,1:规则组序列里的次序,ADC_SampleTime_55Cycles5:采样时间55.5个ADCCLK的周期*/
	/*第4步:配置ADC转换器*/
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; //配置ADC工作在独立模式
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;  //配置ADC数据对齐为右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //配置不使用外部触发,使用内部软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;     //配置转换模式为单次转换
	ADC_InitStruct.ADC_ScanConvMode = DISABLE; //配置扫描模式为非扫描
	ADC_InitStruct.ADC_NbrOfChannel = 1;    //目前是1个通道,给1,这个参数仅在扫描模式下可用,非扫描模式,整个列表只有第一个序列有用
	ADC_Init(ADC1,&ADC_InitStruct);
	/*第5步:开关控制,开启ADC的电源*/
	ADC_Cmd(ADC1,ENABLE);
	ADC_ResetCalibration(ADC1);           //复位校准
	ADC_GetResetCalibrationStatus(ADC1);  //返回复位校准的状态
	while(ADC_GetResetCalibrationStatus(ADC1) == SET); //读取这一位,如果它是1,那就需要一直空循环等待;如果它变为0,那就说明复位校准完成,可以跳出等待了	
	ADC_StartCalibration(ADC1);           //启动校准,之后内部电路会自动校准
	ADC_GetCalibrationStatus(ADC1);       //获取校准状态
	while(ADC_GetCalibrationStatus(ADC1) == SET); //判断校准状态是否完成
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);     //软件触发转换,启动,使用连续转换的时候放到初始化函数里就可以了
}
	

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);   //获取标志位状态并等待完成,ADC_FLAG_EOC:规则组转换完成标志位,自动清除标志位,等待
	/*
	ADC_FLAG_AWD:模拟看门狗标志位
	ADC_FLAG_EOC:规则组转换完成标志位
	ADC_FLAG_JEOC: 注入组转换完成标志位
    ADC_FLAG_JSTRT: 注入组开始转换标志位
	ADC_FLAG_STRT: 规则组开始转换标志位
	*/
	/*具体等待时间:采样周期:55.5、转换周期12.5,12MHz进行68个周期转换完成*/
	//连续转换时,就不需要配置状态标志位了
	return ADC_GetConversionValue(ADC1);     //返回转换值
}

(4)AD.h

#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif

(5)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "AD.h" 

uint16_t AD0,AD1,AD2,AD3;
float Voltage;

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	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);
		Delay_ms(100);
	}
}


  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值