正点原子STM32连载 第三十章 ADC实验摘要

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第三十章 ADC实验

本章,我们将介绍STM32F103的ADC(Analog-to-digital converters,模数转换器)功能。我们通过四个实验来学习ADC,分别是单通道ADC采集实验、单通道ADC采集(DMA读取)实验、多通道ADC采集(DMA读取)实验和单通道ADC过采样(16位分辨率)实验。
本章分为如下几个小节:
30.1 ADC简介
30.2 单通道ADC采集实验
30.3 单通道ADC采集(DMA读取)实验
30.4 多通道ADC采集(DMA读取)实验
30.5 单通道ADC过采样(16位分辨率)实验

30.1 ADC简介

ADC即模拟数字转换器,英文详称Analog-to-digital converter,可以将外部的模拟信号转换为数字信号。
STM32F103系列芯片拥有3个ADC(C8T6只有2个),这些ADC可以独立使用,其中ADC1和ADC2还可以组成双重模式(提高采样率)。STM32的ADC是12位逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源,其中ADC3根据CPU引脚的不同其通道数也不同,一般有8个外部通道。ADC中的各个通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16位数据寄存器中。
STM32F103的ADC主要特性我们可以总结为以下几条:
1、12位分辨率;
2、转换结束、注入转换结束和发生模拟看门狗事件时产生中断
3、单次和连续转换模式
4、自校准
5、带内嵌数据一致性的数据对齐
6、采样间隔可以按通道分别编程
7、规则转换和注入转换均有外部触发选项
8、间断模式
9、双重模式(带2个或以上ADC的器件)
10、ADC转换时间:时钟为72MHz为1.17us
11、ADC供电要求:2.4V到3.6V
12、ADC输入范围:VREF–≤VIN≤VREF+
13、规则通道转换期间有DMA请求产生
下面来介绍ADC的框图:

图30.1.1 ADC框图
图中,我们按照ADC的配置流程标记了七处位置,分别如下:
① 输入电压
在前面ADC的主要特性也对输入电压有所提及,ADC输入范围VREF–≤VIN≤VREF+,最终还是由VREF–、VREF+、VDDA和VSSA决定的。下面看一下这几个参数的关系,如图30.1.2所示:

图30.1.2 参数关系图
从上图可以知道,VDDA和VREF+接VCC3.3,而VSSA和VREF-是接地,所以ADC的输入范围即0~3.3V。
② 输入通道
在确定好了ADC输入电压后,如何把外部输入的电压输送到ADC转换器中呢,在这里引入了ADC的输入通道,在前面也提及到了ADC1和ADC2都有16个外部通道和2个内部通道;而ADC3就有8个外部通道。外部通道对应的是上图中的ADCx_IN0、ADCx_IN1…ADCx_IN15。ADC1的通道16就是内部通道,连接到芯片内部的温度传感器,通道17连接到Vrefint。而ADC2的通道16和17连接到内部的VSS。ADC3的通道9、14、15、16和17连接到的是内部的VSS。具体的ADC通道表见表30.1.1所示:

表30.1.1 ADC通道表
③ 转换顺序
当ADC的多个通道以任意顺序进行转换就诞生了成组转换,这里有两种成组转换类型:规则组和注入组。规则组就是图中的规则通道,注入组就是图中的注入通道。为了避免大家对输入通道,以及规则通道和注入通道的理解混淆,后面规则通道以规则组来代称,注入通道以注入组来代称。
规则组最多允许16个输入通道进行转换,而注入组最多允许4个输入通道进行转换。这里讲解一下规则组和注入组。
规则组(规则通道)
规则组,按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最多也是规则组。
注入组(注入通道)
注入组,按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。
为了便于理解,下面看一下规则组和注入组的执行优先级对比图,如图30.1.3所示:

图30.1.3 规则组和注入组的执行优先级对比图
了解了规则组和注入组的概念后,下面来看看它们的转换顺序,即转换序列。转换序列可以分为规则序列和注入序列。下面分别来介绍它们。
规则序列
规则组最多允许16个输入通道进行转换,那么就需要设置通道转换的顺序,即规则序列。规则序列寄存器有3个,分别为SQR1、SQR2和SQR3。SQR3控制规则序列中的第1个到第6个转换;SQR2控制规则序列中第7个到第12个转换;SQR1控制规则序列中第13个到第16个转换。规则序列寄存器控制关系汇总如表30.1.2所示:
规则序列寄存器控制关系汇总

表30.1.2 规则序列寄存器控制关系汇总表
从上表可以知道,当我们想设置ADC的某个输入通道在规则序列的第1个转换,只需要把相应的输入通道号的值写入SQR3寄存器中的SQ1[4:0]位即可。例如想让输入通道5先进行转换,那么就可以把5这个数值写入SQ1[4:0]位。如果还想让输入通道8在第2个转换,那么就可以把8这个数值写入SQ2[4:0]位。最后还要设置你的这个规则序列的输入通道个数,只需把输入通道个数写入SQR1的SQL[3:0]位。注意:写入0到SQL[3:0]位,表示这个规则序列有1个输入通道的意思,而不是0个输入通道。
注入序列
注入序列,跟规则序列差不多,决定的是注入组的顺序。注入组最大允许4个通道输入,它的注入序列由JSQR寄存器配置。注入序列寄存器JSQR控制关系如表30.1.3所示:

注入序列有多少个输入通道,只需要把输入通道个数写入到JL [ 1 : 0 ]位,范围是0~3。注意:写入0表示这个注入序列有一个输入通道,而不是0个输入通道。这个内容很简单。编程时容易犯错的是注入序列的转换顺序问题,下面给大家讲解一下。
如果JL[ 1 : 0 ]位的值小于3,即设置注入序列要转换的通道个数小于4,则注入序列的转换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。例如:JL [ 1 : 0 ]=10、JSQ4 [ 4 : 0 ]= 00100、JSQ3 [ 4 : 0 ]= 00011、JSQ2 [ 4 : 0 ]= 00111、JSQ1 [ 4 : 0 ]= 00010,意味着这个注入序列的转换顺序是:7、3、4,而不是2、7、3。如果JL[ 1 : 0 ]=00,那么转换顺序是从JSQ4 [ 4 : 0 ]开始。
④ 触发源
在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC的触发转换有两种方法:分别是通过ADON位或外部事件触发转换。
(1)ADON位触发转换
当ADC_CR2寄存器的ADON位为1时,再独立给ADON位写1(其它位不能一起改变,这是为了防止误触发),这时会启动转换。这种控制ADC启动转换的方式非常简单。
(2)外部触发转换
另一种方法是通过外部事件触发转换,例如定时器捕获、EXTI线和软件触发,可以分为规则组外部触发和注入组外部触发。
规则组外部触发使用方法是将EXTTRIG位置1,并且通过EXTSET[2:0]位选择规则组启动转换的触发源。如果EXTSET[2:0]位设置为111,那么可以通过SWSTART为启动ADC转换,相当于软件触发。
注入组外部触发使用方法是将JEXTTRIG位置1,并且通过JEXTSET[2:0]位选择注入组启动转换的触发源。如果JEXTSET[2:0]位设置为111,那么可以通过JSWSTART为启动ADC转换,相当于软件触发。
ADC1和ADC2的触发源是一样的,ADC3的触发源和ADC1/2有所不同,这个需要注意。
⑤ 转换时间
(1)ADC时钟
在学习转换时间之前,我们先来了解ADC时钟。从标号⑤框出来部分可以看到ADC时钟是要经过ADC预分频器的,那么ADC的时钟源是什么?ADC预分频器的分频系数可以设置的范围又是多少?以及ADC时钟频率的最大值又是多少?下面将为大家解答。
ADC的输入时钟是由PCLK2经过分频产生的,分频系数是由RCC_CFGR寄存器的ADCPRE[1:0]位设置的,可选择2/4/8/16分频。需要注意的是,ADC的输入时钟频率最大值是14MHz,如果超过这个值将会导致ADC的转换结果准确度下降。
一般我们设置PCLK2为72MHz。为了不超过ADC的最大输入时钟频率14MHz,我们设置ADC的预分频器分频系数为6,就可以得到ADC的输入时钟频率为72MHz/6,即12MHz。例程中,我们也是如此设置的。
(2)转换时间
STM32F103的ADC总转换时间的计算公式如下:
TCONV = 采样时间 + 12.5个周期
采样时间可通过ADC_SMPR1和ADC_SMPR2寄存器中的SMPx[2:0]位设置,x=017。ADC_SMPR1控制的是通道09,ADC_SMPR2控制的是通道10~17。每个输入通道都支持通过编程来选择不同的采样时间,采样时间可选的范围如下:
SMP = 000:1.5个ADC时钟周期
SMP = 001:7.5个ADC时钟周期
SMP = 010:13.5个ADC时钟周期
SMP = 011:28.5个ADC时钟周期
SMP = 100:41.5个ADC时钟周期
SMP = 101:55.5个ADC时钟周期
SMP = 110:71.5个ADC时钟周期
SMP = 111:239.5个ADC时钟周期
可以看出,采样时间最小是1.5个时钟周期,设置为这个值,那么我们可以得到最短的转换时间。下面以我们例程的ADC时钟配置为例,来给大家计算一下ADC的最短转换时间,计算过程如下:
TCONV = 1.5个ADC时钟周期 + 12.5个ADC时钟周期 = 14个ADC时钟周期
例程中,PCLK2的时钟是72MHz,经过ADC时钟预分频器的6分频后,ADC时钟频率为12MHz。代入上式可得到:
TCONV = 14个ADC时钟周期 = = 1.17us
⑥数据寄存器
ADC转换完成后的数据输出寄存器。根据转换组的不同,规则组的完成转换的数据输出到ADC_DR寄存器,注入组的完成转换的数据输出到ADC_JDRx寄存器。假如是使用双重模式,规则组的数据也是存放在ADC_DR寄存器。下面给大家简单介绍一下这两个寄存器。
(1)ADC规则数据寄存器(ADC_DR)
ADC规则组数据寄存器ADC_DR是一个32位的寄存器,独立模式时只使用到该寄存器低16位保存ADC1/2/3的规则转换数据。在双ADC模式下,高16位用于保存ADC2转换的数据,低16位用于保存ADC1转换的数据。
因为ADC的精度是12位的,ADC_DR寄存器无论高16位还是低16,存放数据的位宽都是16位的,所以允许选择数据对齐方式。由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。
细心的朋友可能发现,规则组最多有16个输入通道,而ADC规则数据寄存器只有一个,如果一个规则组用到好几个通道,数据怎么读取?如果使用多通道转换,那么这些通道的数据也会存放在DR里面,按照规则组的顺序,上一个通道转换的数据,会被下一个通道转换的数据覆盖掉,所以当通道转换完成后要及时把数据取走。比较常用的方法是使用DMA模式。当规则组的通道转换结束时,就会产生DMA请求,这样就可以及时把转换的数据搬运到用户指定的目的地址存放。注意:只有ADC1和ADC3可以产生DAM请求,而由ADC2转换的数据可以通过双ADC模式,利用ADC1的DMA功能传输。
(2)ADC注入数据寄存器x(ADC_JDRx)(x=1~4)
ADC注入数据寄存器有4个,注入组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。ADC_JDRx寄存器是32位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。
⑦中断
ADC中断可分为三种:规则组转换结束中断、注入组转换结束中断、设置了模拟看门狗状态位中断。它们都有独立的中断使能位,分别由ADC_CR寄存器的EOCIE、JEOCIE、AWDIE位设置,对应的标志位分别是EOC、JEOC、AWD。
模拟看门狗中断
模拟看门狗中断发生条件:首先通过ADC_LTR和ADC_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。
DMA请求
规则组和注入组的转换结束后,除了可以产生中断外,还可以产生DMA请求,我们利用DMA及时把转换好的数据传输到指定的内存里,防止数据被覆盖。
注意:只有ADC1和ADC3可以产生DAM请求,DMA相关的知识请回顾DMA实验。
⑧单次转换模式和连续转换模式
单次转换模式和连续转换模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
(1)单次转换模式
通过将ADC_CR2寄存器的CONT位置0选择单次转换模式。该模式下,ADC只执行一次转换,由ADC_CR2寄存器的ADON位启动(只适用于规则组),也可以通过外部触发启动(适用于规则组或注入组)。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DR寄存器中、EOC(转换结束)标志位被置1、如果设置了EOCIE位,则产生中断,然后ADC停止。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DRJx寄存器中、JEOC(转换结束)标志位被置1、如果设置了JEOCIE位,则产生中断,然后ADC停止。
(2)连续转换模式
通过将ADC_CR2寄存器的CONT位置1选择连续转换模式。该模式下,ADC完成上一个通道的转换后会马上自动地启动下一个通道的转换,由ADC_CR2寄存器的ADON位启动,也可以通过外部触发启动。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DR寄存器中、EOC(转换结束)标志位被置1、如果设置了EOCIE位,则产生中断。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DRJx寄存器中、JEOC(转换结束)标志位被置1、如果设置了JEOCIE位,则产生中断。
⑨扫描模式
扫描模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
可以通过ADC_CR1寄存器的SCAN位配置是否使用扫描模式。如果选择扫描模式,ADC会扫描所有被ADC_SQRx寄存器或ADC_JSQR选中的所有通道,并对规则组或者注入组的每个通道执行单次转换,然后停止转换。但如果还设置了CONT位,即选择连续转换模式,那么转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。
如果设置了DMA位,在每次EOC后, DMA控制器把规则组通道的转换数据传输到SRAM中。而注入通道转换的数据总是存储在ADC_JDRx寄存器中。
到这里,我们基本上介绍了ADC的大多数基础的知识点,其它知识后面用到会继续补充,如果还有不懂的内容,请参考《STM32F10xxx参考手册_V10(中文版).pdf》的第11章。
30.2 单通道ADC采集实验
本实验我们来学习单通道ADC采集实验。本实验使用规则组单通道的单次转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。下面先带大家来了解本实验要配置的寄存器。
30.2.1 ADC寄存器
这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
 ADC控制寄存器1(ADC_CR1)
ADC控制寄存器1描述如图30.2.1.1所示:

图30.2.1.1 ADC_CR1寄存器
SCAN位用于选择是否使用扫描模式。本实验我们使用单通道采集,所以没必要选择扫描模式,该位置0即可。
DUALMOD[3:0]位用来设置ADC的操作模式,我们的例程中ADC相关的实验都是使用独立模式,所以设置为0000即可。
 ADC控制寄存器2(ADC_CR2)
ADC控制寄存器2描述如图30.2.1.2所示:

图30.2.1.2 ADC_CR2存器
该寄存器我们针对性的介绍一些位:ADON位用于打开或关闭AD转换器,还可以用于触发ADC转换。CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用单次转换模式,所以CONT位置0即可。CAL位用于开启AD校准。RSTCAL位用于判断校准寄存器是否已初始化。ALIGN用于设置数据对齐,我们使用右对齐,所以该位置0。EXTSEL[2:0]位用于选择规则组启动转换的外部事件触发源,本实验使用的是软件触发(SWSTART),所以这三个位置为111。EXTTRIG位必须置1,EXTSEL[2:0]位才能选择软件触发(SWSTART),别漏了这步,否则设置软件触发会不成功。SWSTART位用于启动一次规则组通道的转换,即软件触发转换。
 ADC采样事件寄存器1(ADC_SMPR1)
ADC采样事件寄存器1描述如图30.2.1.3所示:

图30.2.1.3 ADC_SMPR1寄存器
 ADC采样事件寄存器2(ADC_SMPR2)
ADC采样事件寄存器2描述如图30.2.1.4所示:

图30.2.1.4 ADC_SMPR2寄存器
ADC采样时间设置需要由两个寄存器设置,ADC_SMPR1和ADC_SMPR1,分别设置通道1017和通道09的采样时间,每个通道用3个位设置。可以看出ADC的每个通道的采样时间是支持单独设置的。
一般每个要转换的通道,采样时间建议尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率,看大家怎么衡量选择了。本实验中,我们设置采样时间是239.5个周期。结合前面介绍过的转换时间公式:
TCONV = 采样时间 + 12.5个周期
以及例程中,PCLK2的时钟是72MHz,经过ADC时钟预分频器的6分频后,ADC时钟频率为12MHz。代入上式可得到:
TCONV = 239.5 + 12.5个ADC时钟周期 = = 21us
由上式可得,ADC的转换时间是21us。
 ADC规则序列寄存器1
ADC规则序列寄存器共有3个,这几个寄存器的功能都差不多,这里我们仅介绍一下ADC规则序列寄存器1(ADC_SQR1),描述如图30.2.1.5所示:

图30.2.1.5 ADC_SQR1寄存器
L[3:0]用于设置规则组序列的长度,取值范围:015,表示规则组的长度是116。本实验只用了1个输入通道,所以L[3:0]位设置为0000即可。
SQ13[4:0]SQ16[4:0]位设置规则组序列的第1316个转换编号,第1~12个转换编号的设置请查看ADC_SQR2和ADC_SQR3寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的规则序列寄存器控制关系汇总表。
本实验我们使用单通道,ADC1通道14,所以规则组序列里只有一个输入通道,我们将ADC_SQR3寄存器的SQ1[4:0]位的值设置为14即可。
 ADC规则数据寄存器(ADC_DR)
ADC规则数据寄存器描述如图30.2.1.6所示:

图30.2.1.6 ADC_DR寄存器
在规则序列中AD转换结果都将被存在这个寄存器里面,而注入通道的转换结果被保存在ADC_JDRx里面。该寄存器的数据可以通过ADC_CR2的ALIGN位设置左对齐还是右对齐。在读取数据的时候要注意。
 ADC状态寄存器(ADC_SR)
ADC状态寄存器描述如图30.2.1.7所示:

图30.2.1.7 ADC_SR寄存器
该寄存器保存了ADC转换时的各种状态。本实验我们通过EOC位的状态来判断ADC转换是否完成,如果查询到EOC位被硬件置1,就可以从ADC_DR寄存器中读取转换结果,否则需要等待转换完成。
至此,本实验要用到的ADC关键寄存器全部介绍完了,对于未介绍的部分,请大家参考《STM32F10xxx参考手册_V10(中文版).pdf》第11章相关内容。
30.2.2 硬件设计

  1. 例程功能
    采集ADC1通道1(PA1)上面的电压,并在LCD模块上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。使用杜邦线将ADC和RV1排针连接,使得PA1连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)ADC1 :
    通道1 – PA1
  3. 原理图
    ADC属于STM32F103内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道1(PA1)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过杜邦线将PA1与电位器连接,如下图所示:

图30.2.2.1 PA1(对应ADC排针)与电位器示意图
使用杜邦线将ADC和RV1排针连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测量。
有的朋友可能还想测量其它地方的电压值,我们只需要1根杜邦线,一端接到ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
30.2.3 程序设计
30.2.3.1 ADC的HAL库驱动
ADC在HAL库中的驱动代码在stm32f1xx_hal_adc.c和stm32f1xx_hal_adc_ex.c文件(及其头文件)中。

  1. HAL_ADC_Init函数
    ADC的初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);
    函数描述:
    用于初始化ADC。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
  ADC_TypeDef                    *Instance;      	/* ADC寄存器基地址 */ 
  ADC_InitTypeDef               Init;            	/* ADC参数初始化结构体变量 */
  DMA_HandleTypeDef             *DMA_Handle;   	/* DMA配置结构体 */
  HAL_LockTypeDef               Lock;            	/* ADC锁定对象 */
  __IO uint32_t                 State;           	/* ADC工作状态 */
  __IO uint32_t                 ErrorCode;     	/* ADC错误代码 */
}ADC_HandleTypeDef;

该结构体定义和其他外设比较类似,我们着重看第二个成员变量Init含义,它是结构体ADC_InitTypeDef类型,结构体ADC_InitTypeDef定义为:

typedef struct {
uint32_t DataAlign;                     		/* 设置数据的对齐方式 */
uint32_t ScanConvMode;                   	  	/* 扫描模式 */
FunctionalState ContinuousConvMode;     	/* 开启连续转换模式否则就是单次转换模式 */
uint32_t NbrOfConversion;                 	/* 设置转换通道数目 */
FunctionalState DiscontinuousConvMode; 	/* 是否使用规则通道组间断模式 */
uint32_t NbrOfDiscConversion;            	/* 配置间断模式的规则通道个数 */
uint32_t ExternalTrigConv;                	/* ADC外部触发源选择 */
} ADC_InitTypeDef;
  1. DataAlign:用于设置数据的对齐方式,这里可以选择右对齐或者是左对齐,该参数可选为:ADC_DATAALIGN_RIGHT和ADC_DATAALIGN_LEFT。
  2. ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。
  3. ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
  4. NbrOfConversion:指定规则组转换通道数目,范围是:1~16。
  5. DiscontinuousConvMode:配置是否使用规则通道组间断模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道1和2,下次触发就是转换通道5和7,这样不连续的转换,依次类推。此参数只有将ScanConvMode使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。
  6. NbrOfDiscConversion:配置间断模式的通道个数,禁止规则通道组间断模式后,此参数忽略。
  7. ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  1. HAL_ADCEx_Calibration_Start函数
    ADC的自校准函数,其声明如下:
    HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc);
    函数描述:
    首先调用HAL_ADC_Init函数配置了相关的功能后,再调用此函数进行ADC自校准功能。
    函数形参:
    ADC_HandleTypeDef结构体类型指针变量。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  2. HAL_ADC_ConfigChannel函数
    ADC通道配置函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc,
    ADC_ChannelConfTypeDef *sConfig);
    函数描述:
    调用了HAL_ADC_Init函数配置了相关的功能后,就可以调用此函数配置ADC具体通道。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量。
    形参2是ADC_ChannelConfTypeDef结构体类型指针变量,用于配置ADC采样时间,使用的通道号,单端或者差分方式的配置等。该结构体定义如下:
typedef struct {
  uint32_t Channel;						/* ADC转换通道*/
  uint32_t Rank;						/* ADC转换顺序 */
  uint32_t SamplingTime;				/* ADC采样周期 */
} ADC_ChannelConfTypeDef;
  1. Channel:ADC转换通道,范围:0~19。
  2. Rank:在常规转换中的常规组的转换顺序,可以选择1~16。
  3. SamplingTime:ADC的采样周期,最大810.5个ADC时钟周期,要求尽量大以减少误差。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  1. HAL_ADC_Start函数
    ADC转换启动函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
    函数描述:
    当配置好ADC的基础的功能后,就调用此函数启动ADC。
    函数形参:
    ADC_HandleTypeDef结构体类型指针变量。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  2. HAL_ADC_PollForConversion函数
    等待ADC规则组转换完成函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc,
    uint32_t Timeout);
    函数描述:
    一般先调用HAL_ADC_Start函数启动转换,再调用该函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量。
    形参2是等待转换的等待时间,单位是毫秒(ms)。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  3. HAL_ADC_GetValue函数
    获取常规组ADC转换值函数,其声明如下:
    uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef hadc);
    函数描述:
    一般先调用HAL_ADC_Start函数启动转换,再调用HAL_ADC_PollForConversion函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量。
    函数返回值:
    当前的转换值,uint32_t类型数据。
    单通道ADC采集配置步骤
    1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
    首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道1,对应IO是PA1,它们的时钟开启方法如下:
    __HAL_RCC_ADC1_CLK_ENABLE(); /
    使能ADC1时钟 /
    __HAL_RCC_GPIOA_CLK_ENABLE(); /
    开启GPIOA时钟 */
    2)初始化ADCx, 配置其工作参数
    通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
    注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。
    3)配置ADC通道并启动AD转换器
    在HAL库中,通过HAL_ADC_ConfigChannel函数来选择要配置ADC的通道,并设置规则序列、采样时间等。
    配置好ADC通道之后,通过HAL_ADC_Start函数启动AD转换器。
    4)读取ADC值
    这里选择查询方式读取,在读取ADC值之前需要调用HAL_ADC_PollForConversion等待上一次转换结束。然后就可以通过HAL_ADC_GetValue来读取ADC值。
    30.2.3.2 程序流程图

图30.2.3.2.1 单通道ADC采集实验程序流程图
30.2.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。本章有四个实验,每一个实验的代码都是在上一个实验后面追加。
adc.h文件针对ADC及通道引脚定义了一些宏定义,具体如下:

/* ADC及引脚 定义 */
#define ADC_ADCX_CHY_GPIO_PORT            GPIOA
#define ADC_ADCX_CHY_GPIO_PIN             GPIO_PIN_1
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOC_CLK_ENABLE();\
 }while(0)    /* PA口时钟使能 */

#define ADC_ADCX                           	ADC1 
#define ADC_ADCX_CHY                      ADC_CHANNEL_1 /* 通道Y,  0 <= Y <= 16 */ 
/* ADC1时钟使能 */
#define ADC_ADCX_CHY_CLK_ENABLE()      do{ __HAL_RCC_ADC1_CLK_ENABLE();}while(0)

ADC的通道与引脚的对应关系在《STM32F103ZET6.pdf》数据手册可以查到,我们这里使用ADC1的通道1,在数据手册中的表格为:

表30.2.3.3.1 ADC通道1对应引脚查看表
下面直接开始介绍adc.c的程序,首先是ADC初始化函数。

/**
 * @brief      ADC初始化函数
 * @note       本函数支持ADC1/ADC2任意通道, 但是不支持ADC3
 *              我们使用12位精度, ADC采样时钟=12M, 转换时间为: 采样周期 + 12.5个ADC周期
 *              设置最大采样周期: 239.5, 则转换时间 = 252 个ADC周期 = 21us
 * @param      无
 * @retval      无
 */
void adc_init(void)
{
    g_adc_handle.Instance = ADC_ADCX;                       /* 选择哪个ADC */
    g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;   /* 数据对齐方式:右对齐 */
    g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;   /* 非扫描模式 */
    g_adc_handle.Init.ContinuousConvMode = DISABLE;       /* 关闭连续转换模式 */
    g_adc_handle.Init.NbrOfConversion = 1;    /* 范围是1~16,这里用到1个规则通道 */
g_adc_handle.Init.DiscontinuousConvMode = DISABLE;  /* 禁止规则通道组间断模式 */
/* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */
    g_adc_handle.Init.NbrOfDiscConversion = 0;               
    g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
    HAL_ADC_Init(&g_adc_handle);                                   /* 初始化 */

    HAL_ADCEx_Calibration_Start(&g_adc_handle);                 /* 校准ADC */
}
该函数主要调用了两个HAL库函数,HAL_ADC_Init函数配置了选择哪个ADC、数据对齐方式、是否使用扫描模式等参数,HAL_ADCEx_Calibration_Start函数用于校准ADC。另外HAL_ADC_Init函数会调用它的MSP回调函数HAL_ADC_MspInit,该函数用来存放使能ADC和通道对应IO的时钟和初始化IO口等代码,其定义如下:
/**
 * @brief       	ADC底层驱动,引脚配置,时钟使能
                 	此函数会被HAL_ADC_Init()调用
 * @param       	hadc:ADC句柄
 * @retval      无
 */
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
    if(hadc->Instance == ADC_ADCX)
    {
        GPIO_InitTypeDef gpio_init_struct;
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
        
        ADC_ADCX_CHY_CLK_ENABLE();                             /* 使能ADCx时钟 */
        ADC_ADCX_CHY_GPIO_CLK_ENABLE();                       /* 开启GPIO时钟 */

        /* 设置ADC时钟 */
        adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;/* ADC外设时钟 */
/* 分频系数为6,所以ADC的时钟为72M/6=12MHz */
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                /* 设置ADC时钟 */

        /* 设置AD采集通道对应IO引脚工作模式 */
        gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;           /* ADC通道IO引脚 */
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;                /* 模拟 */
        HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
    }
}

可以看到在HAL_ADC_MspInit函数中,我们除了使能ADC和通道对应IO时钟、初始化IO外,还配置了ADC的时钟预分频系数。ADC的时钟源是PCLK2(72MHz),经过6分频后,得到ADC的输入时钟是12MHz。
接下来要介绍的函数是adc_channel_set,其定义如下:

/**
 * @brief       设置ADC通道采样时间
 * @param       adcx : adc句柄指针,ADC_HandleTypeDef
 * @param       ch   : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17
 * @param       stime: 采样时间  0~7, 对应关系为:
 * @arg         ADC_SAMPLETIME_1CYCLE_5, 1.5个ADC时钟周 
ADC_SAMPLETIME_7CYCLES_5, 7.5个ADC时钟周期
 *   @arg        ADC_SAMPLETIME_13CYCLES_5, 13.5个ADC时钟周期     ADC_SAMPLETIME_28CYCLES_5, 28.5个ADC时钟周期
 *   @arg        ADC_SAMPLETIME_41CYCLES_5, 41.5个ADC时钟周期        ADC_SAMPLETIME_55CYCLES_5, 55.5个ADC时钟周期
 *   @arg        ADC_SAMPLETIME_71CYCLES_5, 71.5个ADC时钟周期        ADC_SAMPLETIME_239CYCLES_5, 239.5个ADC时钟周期
 *   @param      rank: 多通道采集时需要设置的采集编号,
                  假设你定义channle1的rank=1,channle2 的rank=2,
                  那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结果,AdcDMA[1]就是通道2的转换结果。 
                 单通道DMA设置为 ADC_REGULAR_RANK_1
 *   @arg       编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16
 *   @retval    无
 */
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch,uint32_t rank,  uint32_t stime)
{
    ADC_ChannelConfTypeDef adc_ch_conf;
    
    adc_ch_conf.Channel = ch;                             /* 通道 */
    adc_ch_conf.Rank = rank;                              /* 序列 */
    adc_ch_conf.SamplingTime = stime;                   /* 采样时间 */
    HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf); /* 通道配置 */
}

该函数主要是通过HAL_ADC_ConfigChannel函数选择要配置的ADC规则组通道,并设置通道的序列号和采样时间。
下面要介绍的是获得ADC转换后的结果函数,其定义如下:

/**
 * @brief       获得ADC转换后的结果
 * @param       ch: 通道值 0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17
 * @retval      无
 */
uint32_t adc_get_result(uint32_t ch)
{

    adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1,  ADC_SAMPLETIME_239CYCLES_5);		/* 设置通道/序列和采样时间 */
    HAL_ADC_Start(&g_adc_handle);                      /* 开启ADC */
    HAL_ADC_PollForConversion(&g_adc_handle, 10);   /* 等待转换结束 */
/* 返回最近一次ADC1规则组的转换结果 */
return (uint16_t)HAL_ADC_GetValue(&g_adc_handle); 
}

该函数先是调用我们自己定义的adc_channel_set函数选择ADC通道、设置转换序列号和采样时间等,接着调用HAL_ADC_Start启动转换,然后调用HAL_ADC_PollForConversion函数等待转换完成,最后调用HAL_ADC_GetValue函数获取转换结果。
接下来要介绍的函数是获取ADC某通道多次转换结果平均值函数,函数定义如下:

/**
 * @brief       获取通道ch的转换值,取times次,然后平均
 * @param       ch      : 通道号, 0~17
 * @param       times   : 获取次数
 * @retval      通道ch的times次转换结果平均值
 */
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{
    uint32_t temp_val = 0;
    uint8_t t;

    for (t = 0; t < times; t++) 	/* 获取times次数据 */
    {
        temp_val += adc_get_result(ch);
        delay_ms(5);
    }

    return temp_val / times;    		/* 返回平均值 */
}
该函数用于获取ADC多次转换结果的平均值,从而提高准确度。
最后在main函数里面编写如下代码: 
int main(void)
{
    uint16_t adcx;
float temp;
    sys_stm32_clock_init(RCC_PLL_MUL9);   	/* 设置时钟, 72Mhz */
    delay_init(72);                       		/* 延时初始化 */
    usart_init(115200);                   		/* 串口初始化为115200 */
    led_init();                 				/* 初始化LED */
    lcd_init();                 				/* 初始化LCD */
    adc_init();                 				/* 初始化ADC */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "ADC TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE); 

    while (1)
{
/* 获取通道5的转换值,10次取平均 */
     adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
     lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE); /* 显示ADCC采样后的原始值 */

     temp = (float)adcx * (3.3/4096);/* 获取计算后的带小数的实际电压值,比如3.1111 */
     adcx = temp;             /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
     lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE); 

     temp -= adcx;/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
     temp *= 1000;/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数*/
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
     lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);

     LED0_TOGGLE();
     delay_ms(100);
    }
}

此部分代码,我们在TFTLCD模块上显示一些提示信息后,将每隔100ms读取一次ADC通道1的转换值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。ADC值的显示简单介绍一下:首先在液晶固定位置显示了小数点,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就能在液晶上面显示转换结果的整数和小数部分。
30.2.4 下载验证
下载代码后,可以看到LCD显示如图30.2.4.1所示:

图30.2.4.1单通道ADC采集实验测试图
上图中,我们使用杜邦线将ADC和RV1排针连接,使得PA1连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。

30.3 单通道ADC采集(DMA读取)实验
本实验我们来学习单通道ADC采集(DMA读取)实验。本实验使用规则组单通道的连续转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
30.3.1 ADC & DMA寄存器
本实验我们很多的设置和单通道ADC采集实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集实验不同设置的ADC_CR2寄存器进行介绍,其他的配置基本一样的。另外因为我们用到DMA读取数据,所以还会介绍如何配置相关DMA的寄存器。
 ADC配置寄存器(ADC_CR2)
ADCx配置寄存器描述如图30.3.1.1所示:

图30.3.1.1 ADC_CR2寄存器
ADC_CR2寄存器中我们主要跟前面设置不同的有两个位,分别如下:
DMA位用于配置使用DMA模式,本实验该位置1。在单通道ADC采集实验中,默认设置为0,即不使用DMA模式,规则组转换的结果存储在ADC_DR寄存器,然后通过手动读取ADC_DR寄存器的方式得到转换结果。本实验我们使用ADC的连续转换模式,并通过DMA读取转换结果,这样DMA就会自动在ADC_DR寄存器中读取转换结果。
CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用连续转换模式,所以CONT位置1即可。
这里介绍ADC_CR2寄存器的这两个位,其它请参考上一个实验的配置。下面介绍DMA一些比较重要的寄存器配置。
 DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)
DMA通道x外设地址寄存器描述如图30.3.1.2所示:

图30.3.1.2 DMA_CPARx寄存器
该寄存器存放的是DMA通道x外设地址。本实验,我们需要通过DMA读取ADC转换后存放在ADC规则数据寄存器 (ADC_DR) 的结果数据。所以我们需要给DMA_CPARx寄存器写入ADC_DR寄存器的地址。这样配置后,DMA就会从ADC_DR寄存器的地址读取ADC的转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_CMARx寄存器来设置,比如定义一个变量,把这个变量的地址值写入该寄存器。
注意:DMA_CPARx寄存器受到写保护,只有DMA_CCRx寄存器中的EN为“0”时才可以写入,即先要禁止通道开启才可以写入。
 DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)
DMA通道x存储器地址寄存器描述如图30.3.1.3所示:

图30.3.1.3 DMA_CMARx寄存器
该寄存器存放的是DMA通道x存储器地址。同样的,该寄存器也是受写保护,只有当DMA_CCRx的EN位为0时才可以写入。
 DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
DMA通道x传输数量寄存器描述如图30.3.1.4所示:

图30.3.1.4 DMA_CNDTRx
该寄存器控制着DMA通道x的每次传输所要传输的数据量。其设置范围为0~65535。并且该寄存器的值随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成。所以可以通过这个寄存器的值来获取当前DMA传输的进度。
其它的DMA寄存器我们就不一一介绍了,请大家看着寄存器源码对照手册理解,都不难。
30.3.2 硬件设计

  1. 例程功能
    使用ADC采集(DMA读取)通道1(PA1)上面的电压,在LCD模块上面显示ADC转换值以及换算成电压后的电压值。使用短路帽将ADC和RV1排针连接,使得PA1连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)ADC :通道1 – PA1
    5)DMA(DMA1通道1)
  3. 原理图
    ADC属于STM32F103内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道1(PA1)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过杜邦线将PA1与电位器连接,如下图所示:

图30.3.2.1 PA1(对应ADC排针)与电位器示意图
使用杜邦线将ADC和RV1排针连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测量。
有的朋友可能还想测量其它地方的电压值,我们只需要1根杜邦线,一端接到ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
30.3.3 程序设计
30.3.3.1 ADC & DMA的HAL库驱动
单通道ADC采集实验已经介绍了一部分ADC的HAL库API函数,这里要介绍的是HAL_DMA_Start_IT和HAL_ADC_Start_DMA函数。

  1. HAL_DMA_Start_IT函数
    启动DMA传输并开启相关中断函数,其声明如下:
    HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma,
    uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
    函数描述:
    用于启动DMA传输,并开启相关中断,DMA1和DMA2都是用的这个函数。
    函数形参:
    形参1是DMA_HandleTypeDef结构体类型指针变量。
    形参2是DMA传输的源地址。
    形参3是DMA传输的目的地址。
    形参4是要传输的数据项数目。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  2. HAL_ADC_Start_DMA函数
    启动ADC(DMA传输)方式函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,
    uint32_t pData, uint32_t Length);
    函数描述:
    用于启动ADC(DMA传输)方式的函数。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量。
    形参2是ADC 采样数据传输的目的地址。
    形参3是要传输的数据项数目。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
    单通道ADC采集(DMA读取)配置步骤
    1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
    首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道1,对应IO是PA1,它们的时钟开启方法如下:
    __HAL_RCC_ADC1_CLK_ENABLE (); /
    使能ADC1时钟 /
    __HAL_RCC_GPIOA_CLK_ENABLE(); /
    开启GPIOA时钟 */
    2)初始化ADCx, 配置其工作参数
    通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
    注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP回调函数。
    3)配置ADC通道并启动AD转换器
    在HAL库中,通过HAL_ADC_ConfigChannel函数来选择要配置ADC的通道,并设置规则序列、采样时间等。
    4)初始化DMA
    通过HAL_DMA_Init函数初始化DMA,包括配置通道,外设地址,存储器地址,传输数据量等。
    HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。这个宏定义为__HAL_LINKDMA。
    5)使能DMA对应数据流中断,配置DMA中断优先级并开启中断,启动ADC和DMA
    通过HAL_ADC_Start_DMA函数开启ADC转换,通过DMA传输结果。
    通过HAL_DMA_Start_IT函数启动DMA读取,使能DMA中断。
    通过HAL_NVIC_EnableIRQ函数使能DMA数据流中断。
    通过HAL_NVIC_SetPriority函数设置中断优先级。
    6)编写中断服务函数
    DMA的每个数据流几乎都有一个中断服务函数,比如DMA1_Channel1的中断服务函数为DMA1_Channel1_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法就是如此。
    还可以通过调用HAL库提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
    30.3.3.2 程序流程图

图30.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图
30.3.3.3 程序解析
由于本实验用到DMA,所以在adc.h头文件定义了以下一些宏定义:
/* ADC单通道/多通道 DMA采集 DMA及通道 定义

  • 注意: ADC1的DMA通道只能是: DMA1_Channel1, 因此只要是ADC1, 这里是不能改动的
  •  *       ADC2不支持DMA采集
     *       ADC3的DMA通道只能是: DMA2_Channel5, 因此如果使用 ADC3 则需要修改
     */
    #define ADC_ADCX_DMACx                    	DMA1_Channel1
    #define ADC_ADCX_DMACx_IRQn              	DMA1_Channel1_IRQn
    #define ADC_ADCX_DMACx_IRQHandler       	DMA1_Channel1_IRQHandler
    
    /*判断DMA1_Channel1传输完成标志, 是一个假函数形式, 不能当函数使用, 只能用在if等语句里面*/
    #define ADC_ADCX_DMACx_IS_TC()              ( DMA1->ISR & (1 << 1) )    
    /* 清除 DMA1_Channel1 传输完成标志 */
    #define ADC_ADCX_DMACx_CLR_TC()             do{ DMA1->IFCR |= 1 << 1; }while(0) 
    下面给大家介绍adc.c文件里面的函数,首先是ADC DMA读取初始化函数。
    /**
     * @brief       ADC DMA读取 初始化函数
     * @param       mar         : 存储器地址
     * @retval      无
     */
    void adc_dma_init(uint32_t mar)
    {
        GPIO_InitTypeDef gpio_init_struct;
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
        ADC_ChannelConfTypeDef adc_ch_conf = {0};
    
        ADC_ADCX_CHY_CLK_ENABLE();                                  /* 使能ADCx时钟 */
        ADC_ADCX_CHY_GPIO_CLK_ENABLE();                            /* 开启GPIO时钟 */
    
    /* 大于DMA1_Channel7, 则为DMA2的通道了 */
        if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                
        {
            __HAL_RCC_DMA2_CLK_ENABLE();                            /* DMA2时钟使能 */
        }
        else
        {
            __HAL_RCC_DMA1_CLK_ENABLE();                            /* DMA1时钟使能 */
        }
    
        /* 设置ADC时钟 */
    adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外设时钟 */
    /* 分频因子6时钟为72M/6=12MHz */
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;     
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                 /* 设置ADC时钟 */
    
        /* 设置AD采集通道对应IO引脚工作模式 */
        gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;      /* ADC通道对应的IO引脚 */
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;           /* 模拟 */
        HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
    
        /* 初始化DMA */
        g_dma_adc_handle.Instance = ADC_ADCX_DMACx;              /* 设置DMA通道 */
        g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;/* 从外设到存储器模式 */
        g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;     /* 外设非增量模式 */
    g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;          /* 存储器增量模式 */
    /* 外设数据长度:16位 */
    g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;   
    /* 存储器数据长度:16位 */
        g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;      
        g_dma_adc_handle.Init.Mode = DMA_NORMAL;                  /* 外设流控模式 */
        g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;   /* 中等优先级 */
    HAL_DMA_Init(&g_dma_adc_handle);
    
    /* 将DMA与adc联系起来 */
        __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);        
    
        g_adc_dma_handle.Instance = ADC_ADCX;                     /* 选择哪个ADC */
        g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;/* 数据对齐方式:右对齐 */
        g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;/* 非扫描模式 */
        g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;     /* 使能连续转换模式 */
        g_adc_dma_handle.Init.NbrOfConversion = 1;/* 范围是1~16,这里用到1个规则序列 */
    g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;/* 禁止规则组间断模式 */
    /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */
        g_adc_dma_handle.Init.NbrOfDiscConversion = 0;                         
        g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
        HAL_ADC_Init(&g_adc_dma_handle);                                   /* 初始化 */
    
        HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);                 /* 校准ADC */
    
        /* 配置ADC通道 */
        adc_ch_conf.Channel = ADC_ADCX_CHY;                               /* 通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                            /* 序列 */
    /* 采样时间,设置最大采样周期:239.5个ADC周期 */
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
        HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);        /* 通道配置 */
    
        /* 配置DMA数据流请求中断优先级 */
        HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
        HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);
    /* 启动DMA,并开启中断 */
        HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
        HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0); /* 开启ADC,通过DMA传输结果 */
    }
    

    adc_dma_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。下面来看看该函数的代码内容。
    第一部分使能ADC、DMA和GPIO的时钟。
    第二部分配置ADC时钟预分频系数为6,得到ADC的输入时钟频率是12MHz。
    第三部分是设置ADC采集通道对应IO引脚工作模式。
    第四部分初始化DMA,并通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
    第五部分是初始化ADC,并校准ADC。
    第六部分是配置ADC通道。
    第七部分是配置DMA数据流请求中断优先级,并使能该中断。
    第八部分是启动DMA并开启DMA中断,以及启动ADC并通过DMA传输转换结果。
    为了方便代码的管理和移植性等,这里就没有使用HAL_ADC_MspInit这个函数来存放使能时钟、GPIO、NVIC相关的代码,而是全部存放在adc_dma_init函数中。
    接下来给大家介绍使能一次ADC DMA传输函数,其定义如下:

    /**
     * @brief     使能一次ADC DMA传输
     * @note      该函数用寄存器来操作,防止用HAL库操作对其他参数有修改,也为了兼容性
     * @param     ndtr: DMA传输的次数
     * @retval    无
     */
    void adc_dma_enable(uint16_t cndtr)
    {
        ADC_ADCX->CR2 &= ~(1 << 0);                /* 先关闭ADC */
    
        ADC_ADCX_DMACx->CCR &= ~(1 << 0);         /* 关闭DMA传输 */
        while (ADC_ADCX_DMACx->CCR & (1 << 0));  /* 确保DMA可以被设置 */
        ADC_ADCX_DMACx->CNDTR = cndtr;             /* DMA传输数据量 */
        ADC_ADCX_DMACx->CCR |= 1 << 0;             /* 开启DMA传输 */
    
        ADC_ADCX->CR2 |= 1 << 0;                    /* 重新启动ADC */
        ADC_ADCX->CR2 |= 1 << 22;                   /* 启动规则转换通道 */
    }
    

    该函数我们使用寄存器来操作,防止用HAL库相关宏操作会对其它参数进行修改,同时也是为了兼容后面的实验。HAL_DMA_Start_IT函数已经配置好了DMA传输的源地址和目标地址,本函数只需要调用ADC_ADCX_DMACx->CNDTR = cndtr;语句给DMA_CNDTRx寄存器写入要传输的数据量,然后启动DMA就可以传输了。
    下面介绍的是ADC DMA采集中断服务函数,函数定义如下:

    /**
     * @brief       ADC DMA采集中断服务函数
     * @param       无 
     * @retval      无
     */
    void ADC_ADCX_DMACx_IRQHandler(void)
    {
        if (ADC_ADCX_DMACx_IS_TC())
        {
            g_adc_dma_sta = 1;           /* 标记DMA传输完成 */
            ADC_ADCX_DMACx_CLR_TC();    /* 清除DMA1 通道1 传输完成中断 */
        }
    }
    

    在该函数里,通过判断DMA传输完成标志位是否是1,是1就给g_adc_dma_sta 变量赋值为1,标记DMA传输完成,最后清除DMA的传输完成标志位。
    最后在main.c里面编写如下代码:

    #define ADC_DMA_BUF_SIZE        100          	/* ADC DMA采集 BUF大小 */
    uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE];	/* ADC DMA BUF */
    extern uint8_t g_adc_dma_sta;               	/* DMA传输状态标志,0,未完成;1,已完成 */
    
    int main(void)
    {
        uint16_t i;
        uint16_t adcx;
        uint32_t sum;
        float temp;
    
        HAL_Init();                             		/*初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
        delay_init(72);                         	/* 延时初始化 */
        usart_init(115200);                     	/* 串口初始化为115200 */
        led_init();                             		/* 初始化LED */
        lcd_init();                             		/* 初始化LCD */
        adc_dma_init((uint32_t)&g_adc_dma_buf);	/* 初始化ADC DMA采集 */
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "ADC DMA TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE); 
        adc_dma_enable(ADC_DMA_BUF_SIZE);   		/* 启动ADC DMA采集 */
    
        while (1)
        {
            if (g_adc_dma_sta == 1)
            {
              /* 计算DMA 采集到的ADC数据的平均值 */
              sum = 0;
              for (i = 0; i < ADC_DMA_BUF_SIZE; i++)	/* 累加 */
              {
                 sum += g_adc_dma_buf[i];
              }
              adcx = sum / ADC_DMA_BUF_SIZE;    		/* 取平均值 */
              /* 显示结果 */
              lcd_show_xnum(134, 110, adcx, 4, 16, 0,BLUE); /*显示ADCC采样后的原始值*/
              temp=(float)adcx*(3.3/4096); /*获取计算后的带小数的实际电压值,比如3.1111*/
              adcx = temp;                /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
    /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
              lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);  
    
              temp -= adcx;/*把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111*/
              temp*=1000;/*小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数*/
    /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
              lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
              g_adc_dma_sta = 0;                  		/* 清除DMA采集完成状态标志 */
              adc_dma_enable(ADC_DMA_BUF_SIZE); 		/* 启动下一次ADC DMA采集 */
            }
            LED0_TOGGLE();
            delay_ms(100);
        }
    }
    

    此部分代码,和单通道ADC采集实验十分相似,只是这里使能了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,这里我们对数组的数据取平均值,减少误差。在LCD屏显示结果的处理和单通道ADC采集实验一样。首先在液晶固定位置显示了小数点,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就能在液晶上面显示转换结果的整数和小数部分。
    30.3.4 下载验证
    下载代码后,可以看到LCD显示如图30.3.4.1所示:

    图30.3.4.1 单通道ADC采集(DMA读取)实验测试图
    上图中,我们使用短路帽将ADC和RV1排针连接,使得PA1连接到电位器上,测试电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
    大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。

    30.4 多通道ADC采集(DMA读取)实验
    本实验我们来学习多通道ADC采集(DMA读取)实验。本实验使用规则组多通道的连续转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
    30.4.1 ADC寄存器
    本实验我们很多的设置和单通道ADC采集(DMA读取)实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集(DMA读取)实验不同设置的ADC_SQRx寄存器进行介绍,其他的配置基本一样的。另外我们用到DMA读取数据,配置上和单通道ADC采集(DMA读取)实验是一样的。
    ADC规则序列寄存器有四个(ADC_SQR1~ ADC_SQR3),具体怎么配置,需要看我们用多少个通道,比如本实验我们使用6个通道同时采集ADC数据,具体配置如下:
     ADC规则序列寄存器1(ADC_SQR1)
    ADC规则序列寄存器1描述如图30.4.1.1所示:

    图30.4.1.1 ADC_SQR1寄存器
    L[3:0]位用于设置规则序列的长度,取值范围:015,表示规则序列长度为116。本实验使用到6个通道,所以设置这几个位的值为5即可。
    SQ13[4:0]SQ16[4:0]位设置规则组序列的第1316个转换编号,第1~12个转换编号的设置请查看ADC_SQR2和ADC_SQR3寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的规则序列寄存器控制关系汇总表。
    下面我们来看看本实验是怎么设置的:SQ1[4:0]位赋值为0、SQ2[4:0]位赋值为1、SQ3[4:0]位赋值为2、SQ4[4:0]位赋值为3、SQ5[4:0]位赋值为4、SQ6[4:0]位赋值为5,即规则序列1到6分别对应的输入通道是0到5。SQ1~SQ6位都是在ADC_SQR3寄存器中配置。
    30.4.2 硬件设计

    1. 例程功能
      使用ADC1采集(DMA读取)通道1\2\3\4\5\6的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
    2. 硬件资源
      1)LED灯
      LED0 – PE5
      2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
      3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
      4)ADC1 : 通道1–PA0、通道2–PA1、通道3–PA2、
      通道4–PA3、通道5–PA4、通道6–PA5
      5)DMA(DMA1通道1)
    3. 原理图
      ADC和DMA属于STM32F103内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道1\2\3\4\5\6来采集外部电压值,并通过DMA来读取。
      30.4.3 程序设计
      30.4.3.1 ADC的HAL库驱动
      本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍多通道ADC采集(DMA读取)配置步骤。
      多通道ADC采集(DMA读取)配置步骤
      1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
      首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道0、1、2、3、4、5,对应IO是PA0、PA1、PA2、PA3、PA4和PA5,它们的时钟开启方法如下:
      __HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟 /
      __HAL_RCC_GPIOA_CLK_ENABLE(); /
      开启GPIOA时钟 */
      2)初始化ADCx, 配置其工作参数
      通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
      注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP回调函数。
      3)配置ADC通道并启动AD转换器
      在HAL库中,通过HAL_ADC_ConfigChannel函数来选择要配置ADC的通道,并设置规则序列、采样时间等。
      配置好ADC通道之后,通过HAL_ADC_Start函数启动AD转换器。
      4)初始化DMA
      通过HAL_DMA_Init函数初始化DMA,包括配置通道,外设地址,存储器地址,传输数据量等。
      HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。这个宏定义为__HAL_LINKDMA。
      5)使能DMA对应数据流中断,配置DMA中断优先级,使能ADC,使能并启动DMA
      通过HAL_ADC_Start_DMA函数开启ADC转换,通过DMA传输结果。
      通过HAL_DMA_Start_IT函数启动DMA读取,使能DMA中断。
      通过HAL_NVIC_EnableIRQ函数使能DMA数据流中断。
      通过HAL_NVIC_SetPriority函数设置中断优先级。
      6)编写中断服务函数
      DMA的每个数据流几乎都有一个中断服务函数,比如DMA1_Channel1的中断服务函数为DMA1_Channel1_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法就是如此。
      还可以通过调用HAL库提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
      30.4.3.2 程序流程图
      图30.4.3.2.1 多通道ADC采集(DMA读取)实验程序流程图
      30.4.3.3 程序解析
      在本实验中adc.h头文件只是添加了一些函数声明,下面开始介绍adc.c的函数,本实验只增加了一个函数,ADC的N通道(6通道) DMA读取初始化函数,其定义如下:
    /**
     * @brief      ADC N通道(6通道) DMA读取 初始化函数
     * @note       由于本函数用到了6个通道, 宏定义会比较多内容, 
    *              因此,本函数就不采用宏定义方式来修改通道了,
     *              直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道.
     *
     *              注意: 本函数还是使用 ADC_ADCX(默认=ADC1)ADC_ADCX_DMACx(DMA1_Channel1) 及其相关定义。不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用.
     * @param      mar         : 存储器地址 
     * @retval     无
     */
    void adc_nch_dma_init(uint32_t mar)
    {
        GPIO_InitTypeDef gpio_init_struct;
        RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
        ADC_ChannelConfTypeDef adc_ch_conf = {0};
    
        ADC_ADCX_CHY_CLK_ENABLE();								/* 使能ADCx时钟 */
        __HAL_RCC_GPIOA_CLK_ENABLE();                               /* 开启GPIOA时钟 */
    
        /* 大于DMA1_Channel7, 则为DMA2的通道了 */
        if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)      
        {
            __HAL_RCC_DMA2_CLK_ENABLE();                       		/* DMA2时钟使能 */
        }
        else
        {
            __HAL_RCC_DMA1_CLK_ENABLE();                       		/* DMA1时钟使能 */
        }
    
        /* 设置ADC时钟 */
    adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外设时钟 */
    /* 分频因子6时钟为72M/6=12MHz */
        adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;  
        HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                 /* 设置ADC时钟 */
    
        /* 
            设置ADC1通道0~5对应的IO口模拟输入
            AD采集引脚模式设置,模拟输入
            PA0对应 ADC1_IN0
            PA1对应 ADC1_IN1
            PA2对应 ADC1_IN2
            PA3对应 ADC1_IN3
            PA4对应 ADC1_IN4
            PA5对应 ADC1_IN5
        */
    gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
    |GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */
        gpio_init_struct.Mode = GPIO_MODE_ANALOG;        /* 模拟 */
        HAL_GPIO_Init(GPIOA, &gpio_init_struct);
    
        /* 初始化DMA */
    g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx; /* 设置DMA通道 */
    /* 从外设到存储器模式 */
        g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
        g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;  /* 外设非增量模式 */
    g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE;       /* 存储器增量模式 */
    /* 外设数据长度:16位 */
    g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    /* 存储器数据长度:16位 */
        g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
        g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL;                /* 外设流控模式 */
        g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;/* 中等优先级 */
        HAL_DMA_Init(&g_dma_nch_adc_handle);
    
    /* 将DMA与adc联系起来 */
        __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);   
    
        /* 初始化ADC */
        g_adc_nch_dma_handle.Instance = ADC_ADCX;               		/* 选择哪个ADC */
        g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;	/* 数据右对齐 */
        g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;  /* 使能扫描模式 */
    g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;    	/* 使能连续转换 */
    /* 赋值范围是1~16,本实验用到6个规则通道序列 */
    g_adc_nch_dma_handle.Init.NbrOfConversion = 6;
    /* 禁止规则组间断模式 */
    g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE; 
    /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */
        g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;                       
        g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;/* 软件触发 */
        HAL_ADC_Init(&g_adc_nch_dma_handle);                   /* 初始化 */
    
        HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle); /* 校准ADC */
    
        /* 配置ADC通道 */
        adc_ch_conf.Channel = ADC_CHANNEL_0;                   /* 配置使用的ADC通道 */
    adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                 /* 采样序列里的第1个 */
    /* 采样时间,设置最大采样周期:239.5个ADC周期 */
        adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                 
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
        
        adc_ch_conf.Channel = ADC_CHANNEL_1;            /* 配置使用的ADC通道 */
        adc_ch_conf.Rank = ADC_REGULAR_RANK_2;          /* 采样序列里的第2个 */
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
    
        adc_ch_conf.Channel = ADC_CHANNEL_2;            /* 配置使用的ADC通道 */
        adc_ch_conf.Rank = ADC_REGULAR_RANK_3;          /* 采样序列里的第3个 */
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
    
        adc_ch_conf.Channel = ADC_CHANNEL_3;            /* 配置使用的ADC通道 */
        adc_ch_conf.Rank = ADC_REGULAR_RANK_4;          /* 采样序列里的第4个 */
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
    
        adc_ch_conf.Channel = ADC_CHANNEL_4;            /* 配置使用的ADC通道 */
        adc_ch_conf.Rank = ADC_REGULAR_RANK_5;         /* 采样序列里的第5个 */
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道 */
    
        adc_ch_conf.Channel = ADC_CHANNEL_5;            /* 配置使用的ADC通道 */
        adc_ch_conf.Rank = ADC_REGULAR_RANK_6;          /* 采样序列里的第6个 */
        HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道 */
    
        /* 配置DMA数据流请求中断优先级 */
        HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
        HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);
    
    /* 启动DMA,并开启中断 */
    HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);    
    /* 开启ADC,通过DMA传输结果 */
        HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);
    }
    

    adc_nch_dma_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。大部分代码和单通道ADC采集(DMA读取)实验一样,下面来看看该函数的代码内容。
    第一部分使能ADC、DMA和GPIO的时钟。
    第二部分配置ADC时钟预分频系数为6,得到ADC的输入时钟频率是12MHz。
    第三部分是设置ADC采集通道对应IO引脚工作模式,这里用到6个通道。
    第四部分初始化DMA,并通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
    第五部分是初始化ADC,并校准ADC。
    第六部分是配置ADC通道,这里有6个通道需要配置。
    第七部分是配置DMA数据流请求中断优先级,并使能该中断。
    第八部分是启动DMA并开启DMA中断,以及启动ADC并通过DMA传输转换结果。
    为了方便代码的管理和移植性等,这里就没有使用HAL_ADC_MspInit这个函数来存放使能时钟、GPIO、NVIC相关的代码,而是全部存放在adc_nch_dma_init函数中。
    最后在main.c里面编写如下代码:
    #define ADC_DMA_BUF_SIZE 50 * 6 /* ADC DMA采集BUF大小, 应等于ADC通道数的整数倍 */

    uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
    extern uint8_t g_adc_dma_sta;               /* DMA传输状态标志, 0,未完成; 1, 已完成 */
    
    int main(void)
    {
        uint16_t i,j;
        uint16_t adcx;
        uint32_t sum;
        float temp;
    
        sys_stm32_clock_init(9);		/* 设置时钟, 72Mhz */
        delay_init(72);             		/* 延时初始化 */
        usart_init(72, 115200);     		/* 串口初始化为115200 */
        led_init();                 		/* 初始化LED */
        lcd_init();                 		/* 初始化LCD */
    
        adc_nch_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "ADC 6CH DMA TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    
    lcd_show_string(30, 110, 200, 12, 12, "ADC1_CH0_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 122, 200, 12, 12, "ADC1_CH0_VOL:0.000V", BLUE);
        
    lcd_show_string(30, 140, 200, 12, 12, "ADC1_CH1_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 152, 200, 12, 12, "ADC1_CH1_VOL:0.000V", BLUE);
    
    lcd_show_string(30, 170, 200, 12, 12, "ADC1_CH2_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 182, 200, 12, 12, "ADC1_CH2_VOL:0.000V", BLUE); 
    
    lcd_show_string(30, 200, 200, 12, 12, "ADC1_CH3_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 212, 200, 12, 12, "ADC1_CH3_VOL:0.000V", BLUE); 
    
    lcd_show_string(30, 230, 200, 12, 12, "ADC1_CH4_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
        lcd_show_string(30, 242, 200, 12, 12, "ADC1_CH4_VOL:0.000V", BLUE); 
    
    lcd_show_string(30, 260, 200, 12, 12, "ADC1_CH5_VAL:", BLUE);
    /* 先在固定位置显示小数点 */
    lcd_show_string(30, 272, 200, 12, 12, "ADC1_CH5_VOL:0.000V", BLUE); 
    
        adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    
        while (1)
        {
            if (g_adc_dma_sta == 1)
            {
                /* 循环显示通道0~通道5的结果 */
                for(j = 0; j < 6; j++)  /* 遍历6个通道 */
                {
                    sum = 0; /* 清零 */
                    for (i = 0; i < ADC_DMA_BUF_SIZE / 6; i++)  
                    {/* 每个通道采集了10次数据,进行10次累加 */
                        sum += g_adc_dma_buf[(6 * i) + j];  /* 相同通道的转换数据累加 */
                    }
                    adcx = sum / (ADC_DMA_BUF_SIZE / 6);    /* 取平均值 */
                    
                    /* 显示结果 */
    /* 显示ADCC采样后的原始值 */
                    lcd_show_xnum(108, 120 + (j * 30), adcx, 4, 12, 0, BLUE);   
    /* 获取计算后的带小数的实际电压值,比如3.1111 */
                    temp = (float)adcx * (3.3 / 4096); 
                    adcx = temp;    /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
    /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
                    lcd_show_xnum(108, 122 + (j * 30), adcx, 1, 12, 0, BLUE);   
    
    /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
                    temp -= adcx;   
    /* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */
                    temp *= 1000;   
    /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
                    lcd_show_xnum(120, 122 + (j * 30), temp, 3, 12, 0X80, BLUE);
                }
                 g_adc_dma_sta = 0;  /* 清除DMA采集完成状态标志 */
                adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动下一次ADC DMA采集 */
            }
            LED0_TOGGLE();
            delay_ms(100);
        }
    }
    

    这里使用了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,该数组的大小是50 * 6。本实验用到6个通道,每个通道使用50个uint16_t大小的空间存放ADC的结果。输入通道0的转换数据存放在g_adc_dma_buf[0]到g_adc_dma_buf[49],输入通道1的转换数据存放在g_adc_dma_buf[50]到g_adc_dma_buf[99],后面的以此类推。然后对数组的每个通道的数据取平均值,减少误差。最后在LCD屏上显示ADC的转换值和换算成电压后的电压值。
    30.4.4 下载验证
    下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图30.4.4.1所示:

    图30.4.4.1 多通道ADC采集(DMA读取)实验测试图
    使用ADC1采集(DMA读取)通道0\1\2\3\4\5的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5到你想测量的电压源(0~3.3V)。
    这6个通道对应引出来的引脚PA0\PA1\PA2\PA3\PA4\PA5在开发板上的位置,如下图所示:

    图30.4.4.2 ADC1的通道0\1\2\3\4\5引脚在开发板位置示意图
    这六个通道可以同时测量不同测试点的电压,只需要用杜邦线分别接到不同的电压测试点即可。注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。

    30.5 单通道ADC过采样(16位分辨率)实验
    本实验我们来学习单通道ADC过采样(16位分辨率)实验。STM32F103自带的ADC分辨率只有12位,虽然可以满足一般的应用,但是有些场合可能需要更高的分辨率,怎么办呢?可以使用外部专用的ADC,或者换一个带更高分辨率ADC的主控芯片。这样做往往会增加额外的成本,那么有没有其它办法呢?答案是有的,可以通过引入过采样技术来实现。
    ADC过采样技术,是利用ADC多次采集的方式,来提高ADC分辨率。下面,简单介绍一下怎么提高ADC测量的分辨率?
    下面直接给大家介绍一个方程,根据要增加分辨率计算过采样频率方程,方程如下:
    fos = 4w ⋅ fs
    其中,w是希望增加的分辨率位数,fs是初始采样频率要求,fos是过采样频率。
    方程的推导过程比较复杂,这里就不带大家去推导,感兴趣的朋友可以通过下面这个链接自行学习:https://max.book118.com/html/2018/0506/165038217.shtm。
    由该方程可以知道,采样速度每提高4倍,分辨率位数可以提高1位。结合ADC的实际情况,换个思路来说,分辨率位数每提高1位,如果采样频率不变的情况下,那么采样速度就会降低4倍。本实验要求得到16位分辨率,即需要增加4位分辨率,那么采样速度就会降低256倍,即需要采集256次才能得出1次数据,相当于ADC速度慢了256倍。
    理论上只要ADC足够快,我们可以无限提高ADC精度,但实际上ADC并不是无限快的,而且由于ADC性能限制,并不是位数无限提高,结果就越好,需要根据自己的实际需求和ADC的实际性能来权衡的。
    下面来看一下我们怎么实现单通道ADC过采样(16位分辨率)实验的?。
    30.5.1 ADC寄存器
    本实验我们很多的设置和单通道ADC采集(DMA读取)实验是一样的,代码实现也是基于该实验实现的,寄存器的介绍请参考前面的ADC实验。
    30.5.2 硬件设计

    1. 例程功能
      使用ADC1通道1(PA1),通过软件方式实现16位分辨率采集外部电压,并在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA1到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
    2. 硬件资源
      1)LED灯
      LED0 – PB5
      2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
      3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
      4)ADC1 :通道1 – PA1
    3. 原理图
      ADC属于STM32F103内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1通道1(PA1)来采集外部电压值。开发板有一个电位器,可调节的电压范围是:0~3.3V,可以通过断路帽将PA1与电位器连接,从而测量电位器的电压。
      30.5.3 程序设计
      30.5.3.1 ADC的HAL库驱动
      本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。
      30.5.3.2 程序流程图

    图30.5.3.2.1 单通道ADC过采样(16位分辨率)实验程序流程图
    30.5.3.3 程序解析
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。本实验沿用前面实验中的函数,并没有改动。
    下面介绍一下main.c里面编写的代码:
    /* ADC过采样次数, 这里提高4bit分辨率, 需要256倍采样 */

    #define ADC_OVERSAMPLE_TIMES    256
    /* ADC DMA采集 BUF大小, 应等于过采样次数的整数倍 */
    #define ADC_DMA_BUF_SIZE        ADC_OVERSAMPLE_TIMES * 10  
    
    uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
    
    extern uint8_t g_adc_dma_sta;               	/* DMA传输状态标志,0,未完成; 1,已完成 */
    extern ADC_HandleTypeDef g_adc_dma_handle;/* ADC(DMA读取)句柄 */
    
    int main(void)
    {
        uint16_t i;
        uint32_t adcx;
        uint32_t sum;
        float temp;
    
        HAL_Init();                                 		/* 初始化HAL库 */
        sys_stm32_clock_init(RCC_PLL_MUL9);    		/* 设置时钟, 72Mhz */
        delay_init(72);                            		/* 延时初始化 */
        usart_init(115200);                       		/* 串口初始化为115200 */
        led_init();                                 		/* 初始化LED */
        lcd_init();                                 		/* 初始化LCD */
    
    adc_dma_init((uint32_t)&g_adc_dma_buf);   	/* 初始化ADC DMA采集 */
    /* 设置ADCX对应通道采样时间为1.5个时钟周期, 已达到最高的采集速度 */
    adc_channel_set(&g_adc_handle, ADC_ADCX_CHY, ADC_REGULAR_RANK_1,
    ADC_SAMPLETIME_1CYCLE_5); 
    
        lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30,  70, 200, 16, 16, "ADC OverSample TEST", RED);
        lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);.
    /* 先在固定位置显示小数点 */
    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE);
    
        adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    
       while (1)
       {
         if (g_adc_dma_sta == 1)
          {
           /* 计算DMA 采集到的ADC数据的平均值 */
           sum = 0;
    
           for (i = 0; i < ADC_DMA_BUF_SIZE; i++)   /* 累加 */
           {
             sum += g_adc_dma_buf[i];
           }
    
           adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES); /* 取平均值 */
    /* 除以2^4倍, 得到12+4位 ADC精度值, 注意: 提高 N bit精度, 需要 >> N */
           adcx >>= 4; 
    
           /* 显示ADCC采样后的原始值 */
           lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE);
    /* 获取计算后的带小数的实际电压值,比如3.1111 */
           temp = (float)adcx * (3.3 / 65536);
           adcx = temp;           /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
    /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
          lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
    
           temp -= adcx;/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
           temp *= 1000;/*小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数*/
    /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
           lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
           g_adc_dma_sta = 0;                  /* 清除DMA采集完成状态标志 */
           adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动下一次ADC DMA采集 */
          }
    
            LED0_TOGGLE();
            delay_ms(100);
       }
    }
    

    上面的代码中,ADC_OVERSAMPLE_TIMES宏定义表示为了提高4位分辨率,ADC需要进行256次采样才能得的一次16位分辨率的数据。为了减少误差,ADC_DMA_BUF_SIZE宏定义是ADC_OVERSAMPLE_TIMES的10倍,为了后期取16位转换结果平均值的。g_adc_dma_buf数组是uint16_t类型的,用于存放转换结果。
    为了提高ADC的采样速度,调用adc_channel_set函数将采样时间调整为1.5个ADC时钟周期,以得到最高的采样速度。
    adcx = sum / (ADC_DMA_BUF_SIZE / ADC_OVERSAMPLE_TIMES);语句可以得到ADC采样10次的16位分辨率转换结果的平均值。adcx >>= 4;语句对该平均值右移4位,这个过程通常被称为抽取。这样就可以得到16位有用的数据,该数据的取值范围是0~65535,这个操作被称为累加和转储。
    接下来的代码就是在LCD屏显示转换值和换算的电压值,以及让LED0闪烁,提示系统正在运行。
    30.5.4 下载验证
    下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图30.5.4.1所示:

    图30.5.4.1 单通道ADC过采样(16位分辨率)实验测试图
    上图中,我们使用短路帽将ADC和RV1排针连接,使得PA1连接到电位器上,测试电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
    大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。

    物联沃分享整理
    物联沃-IOTWORD物联网 » 正点原子STM32连载 第三十章 ADC实验摘要

    发表评论