第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位逐次逼近型ADC,1us转换时间;
从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资源:ADC1、ADC2,10个外部输入通道。
也就是最多只能测量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.5个ADC周期
采样时间就是采样保持花费的时间,这个时间可以在程序中进行配置,采样时间越大,越能避免一些毛刺信号的干扰,不过转换时间也会相应延长。12.5个ADC周期是量化编码所花费的时间,因为是12位的ADC,所以需要花费12个ADC,这里多半个周期,可能是做一些其它东西花的时间。ADC周期就是从RCC分频过来的ADCCLK,这个ADCCLK最大是14MHz。
例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 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);
}
}