1. ADC 简介
1.1 基本概念
ADC(Analog-Digital Converter)意为模拟到数字转换器,简称模数转换器,或者叫 AD 转换器。
概念:ADC 可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。(ADC 可以将模拟信号转换为数字信号,是模拟电路到数字电路的桥梁)
扩展:数字到模拟的桥梁
- DAC 数字模拟转换器,使用 DAC 就可以将数字变量转换为模拟电压。
- PWM,我们曾经使用 PWM 来控制 LED 的速度、电机的速度,这就是 DAC 的功能。
同时 PWM 只有完全导通和完全断开两种状态,在这两种状态上都没有功率损耗,所以在直流电机调速这种大功率的应用场景下,使用 PWM 来等效模拟量,是比 DAC 更好的选择。并且 PWM 电路更加简单,更加常用。所以可以看出,PWM 还是挤占了 DAC 的很多应用空间的。目前 DAC 的应用主要是在波形生成这些领域,比如信号发生器、音频解码芯片等,这些领域 PWM 还是不好替代的。
DAC 需要大家自行了解了,我们这个型号的 STM32 也没有 DAC 外设。
STM32 主要是数字电路,数字电路只有高低电平,没有 几V电压 的概念,所以如果想读取电压值,就需要借助 ADC 模数转换器来实现了。ADC 读取引脚上的模拟电压,转换为一个数据,存在寄存器里,我们再把这个数据读取到变量里来。就可以进行显示、判断、记录等等操作了。
GPIO 只能读取引脚的高低电平,要么是高电平,要么是低电平,只有俩个值。而使用了 ADC 之后呢,我们就可以对这个高电平和低电平之间的任意电压进行量化,最终用一个变量来表示,读取这个变量,就可以知道引脚的具体电压到底是多少了,所以 ADC 其实就是一个电压表,把引脚的电压值测出来,放在一个变量里,这就是 ADC 的作用。
特性:12位逐次逼近型ADC,1us转换时间。
ADC 的两个关键参数:
- 分辨率:一般用多少位表示,12 位 AD 值,它的表示范围就是 0 ~ 212-1,就是量化结果的范围是 0~4095。位数越高,量化结果就越精细,对应分辨率就越高。
- 转换时间(转换频率):AD 转换是需要花一小段时间的,这里 1us 就表示从 AD 转换开始到产生结果,需要花 1us 的时间,对应 AD 转换的频率就是 1 MHz。这个就是 STM32 ADC 的最快转换频率,如果你需要转换一个频率非常高的信号,那就要考虑一下这个转换频率是不是够用了。如果你的信号频率比较低,那这个最大 1 MHz 的转换频率也完全够用了。
输入电压范围:0~3.3V,转换结果范围:0~4095。
- 这个 ADC 的输入电压一般要求都是要在芯片供电的负极和正极之间变化的,最低电压就是负极 0V,最高电压是正极 3.3V,经过 ADC 转换之后,最小值就是 0,最大值是 4095。0V 对应 0,3.3V 对应 4095,中间都是一一对应的线性关系。计算非常简单,直接乘除一个系数。
- STM32 的 ADC 是 12 位的,所以 AD 结果最大值是 4095,也就是 212 - 1,对应的电压是 3.3V。
18个输入通道,可测量16个外部和2个内部信号源
- 外部信号源就是 16 个 GPIO 口,在引脚上直接接模拟信号就行了,不需要任何额外的电路,引脚直接就能测电压。这个还是非常方便的。
- 2个内部信号源是内部温度传感器和内部参考电压,温度传感器可以测量 CPU 的温度,比如你电脑可以显示一个 CPU 温度,就可以用 ADC 读取这个温度传感器来测量;内部参考电压是一个 1.2V 左右的基准电压,这个基准电压是不随外部供电电压变化而变化的,所以如果你芯片的供电不是标准的 3.3V,那测量外部引脚的电压可能就不对,这时就可以读取这个基准电压进行校准,这样就能得到正确的电压值了。
规则组和注入组两个转换单元
STM32 ADC 的增强功能,普通的 AD 转换流程是,启动一次转换、读一次值,然后再启动,再读值,这样的流程。但是 STM32 的 ADC 就比较高级,可以列一个组,一次性启动一个组,连续转换多个值,并且有两个组,一个是用于常规使用的规则组,一个是用于突发事件的注入组。
模拟看门狗自动监测输入电压范围
这个 ADC,一般可以用于测量光线强度、温度这些值,并且经常会有个需求,就是如果光线高于某个阈值、低于某个阈值,或者温度高于某个阈值、低于某个阈值,执行一些操作。这个高于某个阈值,低于某个阈值的判断,就可以用模拟看门狗来自动执行。模拟看门狗可以监测指定的某些通道,当 AD 值高于它设定的上阈值或者低于下阈值时,它就会申请中断,你就可以在中断函数里执行相应的操作。这样你就不用不断地手动读值,再用 if 进行判断了。
STM32F103C8T6 ADC资源:
ADC1、ADC2 共有两个 ADC 外设。
10个外部输入通道,也就是它最多只能测量 10 个外部引脚的模拟信号。
之前所说的 16 个外部信号源,这是这个系列最多有 16 个外部信号源,但是我们这个芯片引脚比较少,有很多引脚没有引出来,所以就只有 10 个外部信号源,如果你想要更多的外部通道,可以选择引脚更多的型号。具体有多少个通道,那还需要再参考一下数据手册了。
1.2 逐次逼近型 ADC
STM32 的 ADC 原理是逐次逼近型 ADC,但是 STM32 只画了一个框表示 ADC,并没有描述内部结构。
这个图是 ADC0809 的内部结构图,它是一个独立的 8 位逐次逼近型 ADC 芯片。在以前的时候,单片机的性能还不是很强,所以需要外挂一个 ADC 芯片才能进行 AD 转换。这个 ADC 0809 就是一款比较经典的 ADC 芯片;现在单片机的性能和集成度都有很大的提升,很多单片机内部已经集成了 ADC 外设,这样就不用外挂芯片了,引脚可以直接测电压,使用还是非常方便的。
- 输入通道选择部分:首先左边这里 IN0~IN7,是 8 路输入通道,通过通道选择开关,选中一路,输入到这个点进行转换,下面这里是地址锁存和译码,就是你想选中哪个通道,就把通道号放在这三个脚上,然后给一个锁存信号,上面这里对应的通路开关就可以自动拨好了。这部分就相当于一个可以通过模拟信号的数据选择器,因为 ADC 转换是一个很快的过程,你给个开始信号,过个 几us 就转换完成了,所以说如果你想转换多路信号,那不必设计多个 AD 转换器,只需要一个 AD 转换器,然后加一个多路选择开关,想转换哪一路,就先拨一下开关,选中对应通道,然后再开始转换就行了。这个 ADC0809 只有 8 路输入通道;我们 STM32 内部的 ADC 是有 18 个输入通道的,所以对应这里,就是一个 18 路输入的多路开关。
- 输入信号选好后,怎么才能知道这个电压对应的编码数据是多少呢?这就需要我们用逐次逼近的方法来一一比较了。首先这是一个电压比较器,它可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小,它的两个输入端,一个是待测的电压,另一个是 DAC 的电压输出端,DAC 是数模转换器,给它一个数据,它就可以输出数据对应的电压,DAC 内部是使用加权电阻网络来实现的转换。(51 单片机里的 AD/DA 章节)那现在,我们有了一个外部通道输入的,未知编码的电压,和一个 DAC 输出的,已知编码的电压,它俩同时输入到电压比较器,进行大小判断,如果 DAC 输出的电压比较大,那我就调小 DAC 数据;如果 DAC 输出的电压比较小,那我就增大 DAC 数据,直到 DAC 输出的电压和外部通道输入的电压近似相等,这样 DAC 输入的数据就是外部电压的编码数据了,这就是 ADC 的实现原理。这个电压调节的过程就是这个 逐次逼近 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 次,这就是逐次逼近的过程。
- AD 转换结束后,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 的供电是一样的。
1.3 STM32 的 ADC 框图
一般在手册里,每个外设的最前面都有一个整体的结构图,这个结构图还是非常重要的,需要多花点时间看看。
- 左边是 ADC 的输入通道,包括 16 个 GPIO 口,IN0~IN15 和两个内部的通道,一个是内部温度传感器,另一个是 VREFINT(V Reference Internal),内部参考电压,总共是 18 个输入通道。
- 然后到达一个模拟多路开关,可以指定我们想要选择的通道,右边是多路开关的输出,进入到模数转换器,这里模数转换器就是执行我们上节讲过的逐次比较的过程,转换结果会直接放在这个数据寄存器里,我们读取寄存器就能知道 ADC 转换的结果了。
对于普通的 ADC,多路开关一般都是只选中一个。就是选中某个通道,开始转换、等待转换完成、取出结果,这是普通的流程。但是这里就比较高级了,它可以同时选中多个,而且在转换的时候,还分成了两个组,规则通道组和注入通道组;其中规则组可以一次性最多选中 16 个通道,注入组最多可以选中 4 个通道。
(举个例子,这就像是你去餐厅点菜,普通的 ADC 是,你指定一个菜,老板给你做,然后做好了送给你,这里就是,你指定一个菜单,这个菜单最多可以填 16 个菜,然后你直接递个菜单给老板,老板就按照菜单的顺序依次做好,一次性给你端上来,这样的话就可以大大提高效率。当然你的菜单也可以只写一个菜,这样这个菜单就简化成了普通的模式,对于这个菜单,也有两种,一种是规则菜单组,可以同时上16 个菜,但是它有个尴尬的地方,就是这个规则组只有一个数据寄存器,就是这个桌子比较小,最多只能放一个菜,你如果上 16 个菜,那不好意思,前 15 个菜都会被挤掉,你只能得到第 16 个菜。所以对于规则组转换来说,如果使用这个菜单的话,最好配合 DMA 来实现。DMA 是一个数据转运小帮手,它可以在每上一个菜之后,把这个菜挪到其它地方去,防止被覆盖。那现在我们就知道了,这个规则组虽然可以同时转换 16 个通道,但是数据寄存器只能存一个结果,如果不想之前的结果被覆盖,那在转换完成之后,就要尽快的把结果拿走。接着我们看一下注入组,这个组就比较高级了,它相当于餐厅的 VIP 座位,在这个座位上,一次性最多可以点 4 个菜,并且这里数据寄存器有 4 个,是可以同时上 4 个菜的,对于注入组而言,就不用担心数据覆盖的问题了,这就是规则组和注入组的介绍。)
一般情况下我们使用规则组就完全足够了。如果要使用规则组的菜单,那就再配合 DMA 转运数据,这样就不用担心数据覆盖的问题了。所以接下来只讲规则组的操作,注入组涉及的不多,可以参考手册自行了解。
- 外围电路:
- 左下角是触发转换的部分,也就是上节的 START 信号,开始转换。那对于 STM32 的 ADC,触发 ADC 开始转换的信号有两种:一种是软件触发,就是你在程序中手动调用一条代码,就可以启动转换了;另一种是硬件触发,就是图示中的这些触发源,上面是注入组的触发源,下面是规则组的触发源,这些触发源主要是来自于定时器,有定时器的各个通道,还有 TRGO 定时器主模式的输出。
- 左上角这里是 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 的时钟,也就是上一节中的 CLOCK,是用于驱动内部逐次比较的时钟,来自 ADC 预分频器,这个 ADC 预分频器是来源于 RCC 的(APB2 时钟 72 MHz 通过 ADC 预分频器进行分频,得到 ADCCLK,ADCCLK 最大是 14 MHz,所以这个预分频器就有点尴尬,它可以选择 2、4、6、8 分频,如果选择 2 分频,72M / 2 = 36 M,超出允许范围了,4 分频后是 18 M,也超了,所以对于 ADC 预分频器,只能选择 6 分频,结果是 12M,和 8 分频,结果是 9 M,这两个值,这个在程序里要注意下)
- 接着上面是 DMA 请求,这个就是用于触发 DMA 进行数据转运的。两个数据寄存器,用于存放转换结果的。上面还有模拟看门狗,它里面可以存一个阈值高限和阈值低限,如果启动了模拟看门狗,并指定了看门的通道,那这个看门狗就会关注他看门的通道,一旦超出这个阈值范围了,它就会乱叫,就会在上面,申请一个模拟看门狗的中断,最后通向 NVIC。然后对于规则组和注入组而言,它们转换完成之后,也会有一个 EOC 转换完成信号,在这里,EOC 是规则组的完成信号,JEOC 是注入组的完成信号,这两个信号会在状态寄存器里置一个标志位,我们读取这个标志位,就能知道是不是转换结束了,同时这两个标志位就可以去到 NVIC 申请中断,如果开启了 NVIC 对应的通道,它们就会触发中断。
(定时器可以通向 ADC、DAC 这些外设,用于触发转换)因为 ADC 经常需要过一个固定时间段转换一次,比如每个 1ms 转换一次,正常思路就是,用定时器,每隔 1ms 申请一次中断,在中断里手动开始一次转换,这样也是可以的,但是频繁进中断对我们的程序是有一定影响的,比如你有很多中断都需要频繁进入,那肯定会影响主程序的执行,并且不同中断之间,由于优先级的不同,也会导致某些中断不能及时得到响应,如果触发 ADC 的中断不能及时响应,那我们 ADC 的转换频率就肯定会产生影响了。所以对于这种需要频繁进中断,并且在中断里只完成了简单工作的情况,一般都会有硬件的支持。(比如这里,可以给 TIM3 定一个 1ms 的时间,并且把 TIM3 的更新事件选择为 TRGO 输出,然后在 ADC 这里,选择开始触发信号为 TIM3 的 TRGO,这样 TIM3 的更新事件就能通过硬件自动触发 ADC 转换了,整个过程不要进中断,节省了中断资源,这就是这里定时器触发的作用。
1.4 ADC 基本结构图
- 左边是输入通道,16 个 GPIO 口,外加两个内部的通道,然后进入 AD 转换器,AD 转换器里有两个组,一个是规则组,一个是注入组,规则组最多可以选中 16 个通道,注入组最多可以选中 4 个通道,然后转换的结果可以存放在 AD 数据寄存器里,其中规则组只有 1 个数据寄存器,注入组有 4 个。
- 然后下面有触发中断,提供了开始转换这个 START 信号。触发控制可以选择软件触发还是硬件触发,硬件触发主要是来自于定时器,当然也可以选择外部中断的引脚。右边这里是来自于 RCC 的 ADC 时钟 CLOCK,ADC 逐次比较的过程就是由这个时钟推动的。
- 然后上面可以布置一个模拟看门狗用于监测转换结果的范围,如果超出设定的阈值,就会有一个 AWD 信号通过中断输出控制,向 NVIC 申请中断,另外,规则组和注入组转换完成后会有个 EOC/JEOC 信号,它会置一个标志位,当然也可以通向 NVIC。
- 最后右下角这里还有个开关控制,通过设置 ADC_CR2 寄存器中的 ADON 位可以给 ADC 上电。在库函数中就是 ADC_Cmd 函数,用于给 ADC 上电的。当第一次设置 ADON 位时,它将 ADC 从断电状态下唤醒。
GPIO 口上电后默认配置为浮空输入的模式,输出数据寄存器默认输出低电平。寄存器一般上电都默认全为 0。不过也有的不是全为 0。
1.5 输入通道
ADC 通道和引脚复用的关系,这个关系也可以通过引脚定义表看出来(本芯片只有 10 个引脚被复用为 ADC 通道:ADC12_IN0~ADC12_IN9 ,所以这个芯片就只能有 10 个外部输入通道)
ADC12_IN0 意思是 ADC1 和 ADC2 的 IN0 都是在 PA0 上的,然后下面全都是 ADC12,说明 ADC1 和 ADC2 的引脚全都是相同的,既然都相同,那要 ADC2 还有啥用呢?
这就要再说一个 ADC 的高级功能了:就是双 ADC 模式,这个模式比较复杂,只简单介绍一下:双 ADC 模式就是 ADC1 和 ADC2 一起工作,它两可以配合组成同步模式、交叉模式等等模式,比如交叉模式,ADC1 和 ADC2 交叉的对一个通道进行采样,这样就可以进一步提高采样率,就像左右手交叉打拳,那打击的频率肯定比一个拳头打得快对吧。当然还有其他模式,这里就不再细说了。这就是 ADC1 和 ADC2 配合使用的双 ADC 模式,当然 ADC1 和 ADC2 也是可以分开使用的,可以分别对不同的引脚进行采样,这样也是可以的。
通道 | 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 | 内部参考电压 |
- 只有 ADC1 有通道 16 和 17,ADC2 和 ADC3 是没有的。
- ADC1 和 ADC2 的引脚是完全相同的,ADC3 中间会有些变化,不过我们这个芯片也没有 ADC3,不用管的。(由于我们这个芯片没有 PC0~PC5,所以这些通道也就没有了)
1.5 规则组的四种转换模式
在 ADC 初始化的结构体里,会有两个参数:一个是选择单次转换还是连续转换的,另一个是选择扫描模式还是非扫描模式,这两个参数组合起来,就有这四种转换模式:
- 单次转换、非扫描模式
第一个表是规则组里的菜单,有 16 个空位,分别是序列 1 到 序列 16,你可以在这里“点菜”,就是写入你要转换的通道。在非扫描模式下,这个菜单就只有第一个序列 1 的位置有效,这时,菜单同时选中一组的方式就退化为简单地选中一个的方式了。在这里我们可以在序列 1 的位置指定我们想转换的通道,比如通道 2,然后我们就可以触发转换,ADC 就会对这个通道 2 进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给 EOC 标志位置 1,整个转化过程就结束了。我们判断 EOF 标志位,如果转换完成,那我们就可以在数据寄存器里读取结果了。
如果我们想再启动一次转换,那就需要在触发一次,转换结束,置 EOC 标志位,读结果。
如果想换一个通道转换,那在转换之前,把第一个位置通道 2 改成其他通道,然后再启动转换,这样就行了。
没有用到菜单列表,是比较简单的一种模式。
- 连续转换、非扫描模式
非扫描模式:菜单列表只用第一个。
然后它与上一种转换不同的是,它在一次转换结束后不会停止,而是立刻开始下一轮的转换,然后一直持续下去,这样就只需要最开始触发一次,之后就可以一直转换了。
这个模式的好处就是,开始转换之后不需要等待一段时间的,因为它一直都在转换,所以你就不需要手动开始转换了,也不用判断是否结束了,想要读 AD 值的时候,直接从数据寄存器取就是了
- 单次转换、扫描模式
单次转换:每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。
扫描模式:会用到菜单列表,可以在菜单里面点菜,这里每个位置是通道几可以任意指定,并且也是可以重复的,然后初始化结构体里还会有个参数,就是通道数目,因为这 16 个位置你可以不用完,只用前几个,那你就需要再给一个通道数据的参数,告诉它,我有几个通道,比如这里制定通道数据为 7,那它就只看前 7 个位置,然后每次触发之后,他就依次对这前 7 个位置进行 AD 转换,转换结果都放在数据寄存器里。这里为了防止数据被覆盖,就需要用 DMA 及时将数据拷走,那 7 个通道转换完成之后,产生 EOC 信号,转换结束。
然后再触发下一次,就又开始新一轮的转换。
- 连续转换、扫描模式
就是在上一个模式的基础上,一次转换完成后,立刻开始下一次的转换。和上面的非扫描模式的单次和连续是一个套路。
在扫描模式的情况下,还可以有一种模式,叫间断模式。它的作用是,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。了解一下即可,不需要掌握。
1.7 触发控制
这个表就是规则组的触发源,有来自定时器的信号,还有来自引脚或定时器的信号(具体引脚还是定时器,需要用 AFIO 重映射来确定),最后是软件控制位,也就是软件触发。
这些触发源的选择,通过设置右边这个寄存器来完成,当然使用库函数的话,直接给一个参数就行了。
1.8 数据对齐
我们这个 ADC 是 12 位的,它的转换结果就是一个 12 位的数据,但是数据寄存器是 16 位的,所以就存在一个数据对齐的问题。
-
数据右对齐:
12 位的数据向右靠,高位多出来的几位就补 0 -
数据左对齐:
12 位的数据向左靠,低位多出来的几位就补 0
我们一般使用的都是第一种:右对齐,这样读取这个 16 位寄存器,直接就是转换结果。如果选择左对齐,直接读的话,得到的数据会比实际的大,因为数据左对齐实际上就是把数据左移了 4 次。二进制有个特点,就是数据左移一次,就等效于把这个数据乘 2,那这里左移了 4 次,就相当于把结果乘 16 了。所以直接读的话会比实际值大 16 倍。
左对齐用途:如果你不想要这么高的分辨率,你觉得 0~4095 数太大了,我就做个简单的判断,不需要这么高的分辨率,那你就可以选择左对齐,再把这个数据的高 8 位取出来,这样就舍弃掉了后面 4 位的精度,这个 12 位的 ADC 就退化成了 8 位的 ADC 了。
不过我们一般用的时候,选右对齐就可以了,如果需要裁剪一些分辨率,大不了就先把 12 位都取出来,再做处理。这也是可以的,就是多算一步而已。
1.9 转换时间
转换时间这个参数我们一般不太敏感,因为一般 AD 转换都很快。如果不需要非常高速的转换频率,那转换时间就可以忽略了。
AD 转换需要一小段时间的,AD转换的步骤:
-
采样,保持(放在一起,第一大步):因为 AD 转换,就是后面的量化编码,是需要一小段时间的,如果在这一小段时间里,输入的电压还在不断变化,那就没法定位输入电压到底在哪了是吧。所以在量化编码之前,需要设置一个采样开关,先打开采样开关,收集一下外部的电压,比如可以用一个小容量的的电容存储一下这个电压,存储好了之后,断开采样开关,再进行后面的 AD 转换,这样在量化编码期间,电压始终保持不变,这样才能精确的定位未知电压的位置。这就是采样保持电路。那采样保持的过程,需要闭合采样开关,过一段时间再断开,这里就会产生一个采样时间。
-
量化,编码(放在一起,第二大步):ADC 逐次比较的过程,一般位数越多,花的时间越长
STM32 ADC的总转换时间为:
TCONV = 采样时间 + 12.5个ADC周期
采样时间:采样保持花费的时间,可以在程序中进行配置,采样时间越大,越能避免一些毛刺信号的干扰。不过转换时间也会相应延长。
12.5个ADC周期:量化编码花费的时间。因为是 12 位的 ADC,所以需要花费 12 个周期,这里多了半个周期,可能是做其他一些东西花的时间。
ADC 周期:从 RCC 分频过来的 ADCCLK,这个 ADCCLK 最大时 14 MHz。
例如:
最快的转换时间:当ADCCLK=14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
这就是最快 1us 时间的来源。如果你的采样周期再长些,它就达不到 1us 了。另外你也可以把 ADCCLK 的时钟设置超过 14 MHz,这样的话 ADC 就是在超频了,那转换时间可以比 1us 还短,不过这样稳定性就没法保证了。
1.10 校准
ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
建议在每次上电后执行一次校准
启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
这个校准过程是固定的,我们只需要在 ADC 初始化的最后,加几条代码就行了。至于怎么计算、怎么校准的,我们不需要管。
1.11 硬件电路
对于 ADC 的外围电路,我们应该怎么设计呢?
- 电位器产生可调电压的电路:一个电位器产生一个可调的电压,这里电位器的两个固定端,一端接 3.3V,另一端接 GND。这样中间的滑动端就可以输出一个 0~3.3V 可调的电压输出了,我们这里可以接 ADC 的输入通道,比如 PA0 口。当滑动端往上滑时,电压增大,往下滑时,电压减小。
另外注意这个电阻的阻值不要给太小,因为这个电阻两端也是直接跨接在电源正负极的,如果阻值太小,那这个电阻就会比较费电,再小就有可能发热冒烟了。一般至少要接 kΩ 级的电阻,比如这里接的是 10k 的电阻。
-
传感器输出电压的电路:一般来说,像光敏电阻、热敏电阻、红外接收管、麦克风等等,都可以等效为一个可变电阻,那电阻阻值没法直接测量,所以这里就通过和一个固定电阻串联分压,来得到一个反映电阻值电压的电路。那这里,传感器阻值变小时,下拉作用变强,输出端电压就下降;传感器阻值变大时,下拉作用变弱,输出端受上拉电阻作用,电压就会升高,这个固定电阻一般可以选择和传感器阻值相近的电阻,这样可以得到一个位于中间电压区域比较好的输出,当然这里传感器和固定电阻的位置也可以换过来,这样的话,输出电压的极性就反过来了,这就是分压方法来输出传感器阻值的电路。(在传感器模块简介的电路中传感器也是使用分压的方式来进行输出的,我们这一节 AD 转换,需要用到 AO 这个引脚,其他电路是没用的,AO 直接输出,我们直接接线就行了。)
-
电压转换电路:比如想测一个 0~5V 的 VIN 电压,但是 ADC 只能接收 0~3.3V 的电压,那就可以搭建一个这样的简易转换电路,在这里还是使用电阻进行分压,上面阻值 17k,下面阻值 33k,加一起是 50k,根据分压公式,中间的电压就是 VIN / 50k × 33k,最后得到的电压范围就是 0~3.3V,就可以进入 ADC 转换了。如果你想采集 5V、10V 这些电压的话,可以使用这个电路,但是如果你电压再高一些,就不建议使用这个电路了,那可能会比较危险,高电压采集最好使用一些专用的采集芯片,比如隔离放大器等等,做好高低电压的隔离,保证电路的安全。
2. 两个 ADC 模数转换功能案例
2.1 AD 单通道
2.1.1 硬件电路图
- 电位器有 3 个引脚,分别插在这 3 排孔里。电位器内部结构:左边和右边两个引脚接的是电阻的两个固定端,中间这个引脚接的是滑动抽头,电位器外边有个十字形状的槽可以拧,往左拧,抽头就往左靠,往右拧,抽头就往右靠。所以外围电路这里,我们把左边的固定端接在负极,右边的固定端接在正极,中间就可以输出从负极到正极可调的电压了,然后右边这里我们把可调的电压输出,接在 PA0 口上,在这里,根据引脚定义表,PA0 到 PB1 这 10 个引脚是 ADC 的 10 个通道,所以这 10 个引脚你可以任意选,接在哪个都行。但是其他的这些引脚,不是 ADC 的通道,就不能接模拟电压了。
2.1.2 ADC 模数转换模块初始化
- ADC 模数转换模块的库函数(在 adc.h 头文字中)
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);//用来配置 ADCCLK 分频器,它可以对 APB2 的 72MHz 时钟选择 2、4、6、8 分频,输入到 ADCCLK(在 rcc.h 头文件中,是 rcc 库函数里面的)
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
//用于控制校准的函数,在 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);//用于软件触发的函数,调用一下就能软件触发转换。给 SWSTART 位置一,以开始转换
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);//返回 SWSTART 状态,由于 SWSTART 位在转换开始后立刻就清零了,所以这个函数的返回值跟转换是否结束,毫无关系。这个函数其实没啥用,我们一般不用
//用来配置间断模式
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 规则组通道配置,作用是给序列的每个位置填写指定的通道,就是填写菜单的过程(ADCx,想要指定的通道,规则组序列器里的次序(1~16),指定通道的采样时间(更快的转换选择小参数,更稳定的转换选择大参数)
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);//ADC 外部触发转换控制,就是是否允许外部触发转换
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);//获取 AD 转换的数据寄存器,读取转换结果就要使用这个函数
uint32_t ADC_GetDualModeConversionValue(void);//双 ADC 模式读取转换结果的函数
//injected:注入组,以下函数都是都注入组进行配置的。
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);//清除中断挂起位
- 配置 ADC 模数转换模块
- 开启 RCC 时钟,包括 ADC 和 GPIO 时钟,另外 ADCCLK 的分频器也需要配置一下
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置 ADCCLK,PCLK2 就是 APB2时钟 的意思(72M / 6 = 12M)
ADC 都是 APB2 上的设备
- 配置 GPIO,把需要用的 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);//GPIO 配置好默认低电平
AIN 模式是 ADC 的专属模式,在 AIN 模式下,GPIO 口是无效的,断开 GPIO,防止你 GPIO 口的输入输出对我模拟电压的造成干扰
- 配置多路开关,把左边的通道接入到右边的规则组列表里(点菜,把各个通道的菜,列在菜单里)
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
参数
ADC_SampleTime_55Cycles5
表示:采样周期为 55.5 个 ADCCLK 的周期,每个通道可以设置不同的采样时间
- 配置 ADC 转换器,在库函数里,用结构体来配置的(包括ADC转换模式,有几个通道,触发源、数据对齐等等)
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// ADC 工作模式(独立模式/双 ADC 模式)。选择独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐。选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源,定义用于启动规则组转换的外部触发源。不使用外部触发,使用内部软件触发
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//连续转换模式,选择连续转换/单次转换(ENABLE/DISABLE)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//扫描转换模式,选择扫描模式/非扫描模式(ENABLE/DISABLE)
ADC_InitStructure.ADC_NbrOfChannel = 1;//规则组转换列表里通道数目(参数在 1~16),指定在扫描模式下,总共会用到几个通道。
ADC_Init(ADC1, &ADC_InitStructure);
在非扫描的模式下,
ADC_NbrOfChannel
这个参数其实是没有用的,无论写多少数目,最终都是只有序列 1 的位置有效。
- 如果你需要模拟看门狗,那会有几个函数用来配置阈值和监测通道的。
- 如果你想开启中断,那就在中断输出控制里用 ITConfig 函数开启对应的中断输出,然后再在 NVIC 里,配置一下优先级,这样就能触发中断了。
- 开关控制,调用ADC_Cmd 函数,开启 ADC,这样 ADC 就配置完成了,就能正常工作了
ADC_Cmd(ADC1, ENABLE);
- 开启 ADC 之后,根据手册建议,对 ADC 进行一下校准,这样可以减小误差
ADC_ResetCalibration(ADC1);//复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//获取复位校准状态,空循环等待校准完成。
ADC_StartCalibration(ADC1);//开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//获取开始校准状态,空循环等待校准完成。
ADC_GetResetCalibrationStatus
函数获取 CR2 寄存器里的 RSTCAL 标志位,软件置该位为 1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清 0。
- 在 ADC 工作的时候,如果想要软件触发转换,那会有函数可以触发,如果想读取转换结果,那也会有函数可以读取结果。
uint16_t AD_GetValue(void) {
//1.软件触发转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//2.空循环等待完成,也就是等待 EOC 标志位置 1
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
//3.读取 ADC 数据寄存器
return ADC_GetConversionValue(ADC1);//返回值是 AD 转换结果
}
EOC 该位由硬件在(规则/注入)通道组转换结束时置 1,由软件清除或由读取 ADC_DR 时清除。
- 当它为 0 时表示转换未完成,为 1 时表示转换完成。转换完成时间 = 采样周期(55.5) + 转换周期(固定 12.5) = 68 个周期,1 / 12M × 68 = 5.6 微秒
- ADC_DR 是数据寄存器,一般 EOC 标志位置1,我们就会来读取数据,这一位可以在读取数据寄存器之后自动清除,就不需要再手动清除了。
2.1.3 示例代码
AD.h
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(void);
#endif
AD.c
#include "stm32f10x.h" // Device header
void AD_Init(void) {
//1.开启 RCC 时钟,包括 ADC 和 GPIO 时钟,另外 ADCCLK 的分频器也需要配置一下,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//ADC 都是 APB2 上的设备
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置 ADCCLK,PCLK2 就是 APB2时钟 的意思(72M / 6 = 12M)
//2.配置 GPIO,把需要用的 GPIO 配置成模拟输入的模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,AIN 模式是 ADC 的专属模式,在 AIN 模式下,GPIO 口是无效的,断开 GPIO,防止你 GPIO 口的输入输出对我模拟电压的造成干扰
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平
//3.配置多路开关,把左边的通道接入到右边的规则组列表里(点菜,把各个通道的菜,列在菜单里)
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);//采样周期为 55.5 个 ADCCLK 的周期,每个通道可以设置不同的采样时间
//4.配置 ADC 转换器,在库函数里,用结构体来配置的(包括ADC转换模式,有几个通道,触发源、数据对齐等等)
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// ADC 工作模式(独立模式/双 ADC 模式)。选择独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐。选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源,定义用于启动规则组转换的外部触发源。不使用外部触发,使用内部软件触发
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//连续转换模式,选择连续转换/单次转换(ENABLE/DISABLE)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//扫描转换模式,选择扫描模式/非扫描模式(ENABLE/DISABLE)
ADC_InitStructure.ADC_NbrOfChannel = 1;//规则组转换列表里通道数目(参数在 1~16),指定在扫描模式下,总共会用到几个通道。在非扫描的模式下,这个参数其实是没有用的,无论写多少数目,最终都是只有序列1 的位置有效。
ADC_Init(ADC1, &ADC_InitStructure);
//如果你需要模拟看门狗,那会有几个函数用来配置阈值和监测通道的,
//如果你想开启中断,那就在中断输出控制里用 ITConfig 函数开启对应的中断输出,然后再在 NVIC 里,配置一下优先级,这样就能触发中断了。
//5.开关控制,调用ADC_Cmd 函数,开启 ADC,这样 ADC 就配置完成了,就能正常工作了
ADC_Cmd(ADC1, ENABLE);
//6.开启 ADC 之后,根据手册建议,对 ADC 进行一下校准,这样可以减小误差
ADC_ResetCalibration(ADC1);//复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//获取复位校准状态,空循环等待校准完成。
//获取 CR2 寄存器里的 RSTCAL 标志位,软件置该位为 1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清 0。
ADC_StartCalibration(ADC1);//开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//获取开始校准状态,空循环等待校准完成。
}
//在 ADC 工作的时候,如果想要软件触发转换,那会有函数可以触发,如果想读取转换结果,那也会有函数可以读取结果。
uint16_t AD_GetValue(void) {
//1.软件触发转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//2.空循环等待完成,也就是等待 EOC 标志位置 1
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//EOC 该位由硬件在(规则/注入)通道组转换结束时置 1,由软件清除或由读取 ADC_DR 时清除。
//(当它为 0 时表示转换未完成,为 1 时表示转换完成
//转换完成时间 = 采样周期(55.5) + 转换周期(固定 12.5) = 68 个周期
//1 / 12M × 68 = 5.6 微秒
//ADC_DR 是数据寄存器,一般 EOC 标志位置1,我们就会来读取数据,这一位可以在读取数据寄存器之后自动清除,就不需要再手动清除了。
//3.读取 ADC 数据寄存器
return ADC_GetConversionValue(ADC1);//返回值是 AD 转换结果
}
这里我们使用的是第一种转换方式:单次转换非扫描模式,我们还可以使用第二种转换方式:连续转换非扫描。这个模式的好处就是不需要不断地触发,也不需要等待转换完成的。步骤:
- 将配置 ADC 转换器 的结构体参数中的
ADC_ScanConvMode
参数设为 ENABLE。- 连续转换仅需要在最开始触发一次就行了,将软件触发转换函数移到初始化部分的最后,在初始化完成之后,触发一次就行了。这时,内部的 ADC 就会一次接着一次的、连续不断的对我们指定的通道 0 进行转换,转换结果放在数据寄存器里。
- 此时数据寄存器会不断的刷新最新的转换结果,所以就不需要判断标志位了,将空循环等待完成删掉,直接 return 数据寄存器里的值就行了。
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();
AD_Init();
OLED_ShowString(1, 1, "ADValue:");
OLED_ShowString(2, 1, "Voltage:0.00V");
while(1){
ADValue = AD_GetValue();
OLED_ShowNum(1, 9, ADValue, 4);//0~4095
//显示实际的电压值(线性变换即可)
Voltage = (float)ADValue / 4095 * 3.3;/
OLED_ShowNum(2, 9, Voltage, 1);//显示整数部分
OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);//显示小数部分
Delay_ms(100);
}
}
一般的情况下,如果就只是进行阈值判断、数据记录的话,也可以不进行变换,直接使用原始的 AD 数据,这样也是可以的。
在这里 AD 值的末尾会有些抖动,这是正常的波动,如果你想对这个值进行判断,再执行一些操作。比如光线的 AD 值小于某一阈值,就开灯,大于某一阈值,就关灯。那可能出现光线逐渐变暗,AD 值逐渐变小,但是由于波动,AD 值会在判断阈值附近来回跳变,这会导致输出产生抖动,来回开关灯。那如何避免这种情况呢?
- 可以使用迟滞比较的方法来完成,设置两个阈值,低于下阈值时,开灯,高于上阈值时,才关灯,可以避免输出抖动的问题,这跟 施密特传感器是一个原理。
- 另外如果你觉得数据跳变的厉害,还可以采用滤波的方式,让 AD 值平滑一些,比如均值滤波,就是读 10 个值或者 20 个值,取平均值,作为滤波的 AD 值。
- 或者还可以裁剪分辨率,把数据的尾数去掉,这样也可以减少数据波动。
实际上 AD 值等于 4096 才对应 3.3V,会有一个数的偏差,所以 AD 值最大的 4095 实际上对应的应该是 3.3V 小一丢丢,没有办法达到满量程的 3.3V,这个是受限于 ADC 的结构。
总之,你认为 4095 对应 3.3V 可以,认为 4096 对应 3.3V 也可以,只有一点点偏差,也看不出来差别。
2.2 AD 多通道
2.2.1 硬件电路图
在这里我们使用了 4 个 AD 通道:
- 第一个通道还是电位器,接在 PA0 口
- 之后上面又接了三个传感器模块,分别是光敏传感器、热敏传感器、反射式红外传感器,它们的 VCC 和 GND 都分别接在面包板的正负极,然后 AO 就是模拟量的输出引脚,三个模块的 AO 分别接在 PA1、PA2 和 PA3 口,加上电位器的 PA0,总共是 4 个输入通道,同样,这些 GPIO 口也是可以在 PA0~PB1 之间任意选择的,这里选择了前 4 个。
2.2.2 示例代码
多通道我们首先想到的是扫描模式,利用列表,把 4 个通道都填进去,然后触发转换,这样就能实现多通道了。这样确实是一种不错的方法,但是还是存在数据覆盖的问题,如果想要用扫描模式实现多通道,最好要配合 DMA 来实现,我们在讲完 DMA 之后再来试下扫描模式。
我们一个通道转换完成之后,手动把数据转运出来,不就行了,为啥非要用 DMA 来转运呢?
这个方案看似简单,但是实际操作起来会有一些问题:
- 第一个问题:在扫描模式下,你启动列表之后,它里面每个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断,你不知道某一个通道是不是转换完了,它只有在整个列表都转换完成之后,才会产生一次 EOC 标志位,才能触发中断,而这时,前面的数据就已经覆盖丢失了。
- 第二个问题:AD 转换是非常快的,刚才我们也计算过,转换一个通道大概只有 几us,也就是说,如果你不能在 几us 的时间内把数据转运走,那数据就会丢失,这对我们程序手动转运数据,要求就比较高了。
所以在扫描模式下,手动转运数据是比较困难的,不过比较困难也不是说手动转运不可行。
- 我们可以使用间断模式,在扫描的时候,每转换一个通道就暂停一次,等我们手动把数据转运走之后,再继续触发,继续下一次转换,这样可以实现手动转运数据的功能。但是由于单个通道转换完成之后,没有标志位,所以启动转换之后,只能通过 Delay 延时的方式,延迟足够长的时间,才能保证转换完成,这种方式既不能让我们省心,也不能提高效率,所以暂时不推荐使用。
那这些方法都不行,我们目前是不是就不能实现多通道了呢?答案是能实现,而且非常简单。
我们可以使用单次转换非扫描的模式,来实现多通道,只需要在每次触发转换之前,手动更改一下列表第一个位置的通道就行了,比如第一次转换,先写入通道 0,之后触发、等待、读值;第二次转换,再先把通道 0 改成通道 1,之后触发、等待、读值;第三次转换,再先改成通道 2,等等等等。
这样在转换前,先指定一下通道,再启动转换,就可以轻松的实现多通道转换的功能了。这也是一个比较简单直观的方法。
AD.h
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif
AD.c
#include "stm32f10x.h" // Device header
void AD_Init(void) {
//1.开启 RCC 时钟,包括 ADC 和 GPIO 时钟,另外 ADCCLK 的分频器也需要配置一下,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//ADC 都是 APB2 上的设备
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置 ADCCLK,PCLK2 就是 APB2时钟 的意思(72M / 6 = 12M)
//2.配置 GPIO,把需要用的 GPIO 配置成模拟输入的模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,AIN 模式是 ADC 的专属模式,在 AIN 模式下,GPIO 口是无效的,断开 GPIO,防止你 GPIO 口的输入输出对我模拟电压的造成干扰
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);//GPIO 配置好默认低电平
//3.配置 ADC 转换器,在库函数里,用结构体来配置的(包括ADC转换模式,有几个通道,触发源、数据对齐等等)
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// ADC 工作模式(独立模式/双 ADC 模式)。选择独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐。选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源,定义用于启动规则组转换的外部触发源。不使用外部触发,使用内部软件触发
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//连续转换模式,选择连续转换/单次转换(ENABLE/DISABLE)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//扫描转换模式,选择扫描模式/非扫描模式(ENABLE/DISABLE)
ADC_InitStructure.ADC_NbrOfChannel = 1;//规则组转换列表里通道数目(参数在 1~16),指定在扫描模式下,总共会用到几个通道。在非扫描的模式下,这个参数其实是没有用的,无论写多少数目,最终都是只有序列1 的位置有效。
ADC_Init(ADC1, &ADC_InitStructure);
//如果你需要模拟看门狗,那会有几个函数用来配置阈值和监测通道的,
//如果你想开启中断,那就在中断输出控制里用 ITConfig 函数开启对应的中断输出,然后再在 NVIC 里,配置一下优先级,这样就能触发中断了。
//4.开关控制,调用ADC_Cmd 函数,开启 ADC,这样 ADC 就配置完成了,就能正常工作了
ADC_Cmd(ADC1, ENABLE);
//5.开启 ADC 之后,根据手册建议,对 ADC 进行一下校准,这样可以减小误差
ADC_ResetCalibration(ADC1);//复位校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);//获取复位校准状态,空循环等待校准完成。
//获取 CR2 寄存器里的 RSTCAL 标志位,软件置该位为 1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清 0。
ADC_StartCalibration(ADC1);//开始校准
while (ADC_GetCalibrationStatus(ADC1) == SET);//获取开始校准状态,空循环等待校准完成。
}
uint16_t AD_GetValue(uint8_t ADC_Channel) {//只需要指定一个转换的通道,返回值就是我们指定通道的结果了
//1.在转换前指定了转换的通道(点菜,把各个通道的菜,列在菜单里)
ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);//采样周期为 55.5 个 ADCCLK 的周期,每个通道可以设置不同的采样时间
//2.软件触发转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//3.空循环等待完成,也就是等待 EOC 标志位置 1
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//EOC 该位由硬件在(规则/注入)通道组转换结束时置 1,由软件清除或由读取 ADC_DR 时清除。
//4.读取 ADC 数据寄存器
return ADC_GetConversionValue(ADC1);//返回值是 AD 转换结果
}
重新封装了 AD_GetValue 函数,将指定转换通道的库函数从初始化函数转移到了 AD_GetValue 函数中,这样在转换前,可以先指定一下通道,再启动转换,就可以轻松的实现多通道转换的功能了。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t AD0, AD1, AD2, AD3;
int main(void) {
OLED_Init();
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);
}
}