【正点原子STM32连载】 第三十一章 ADC实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

1)实验平台:正点原子MiniPro H750开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html
4)对正点原子STM32感兴趣的同学可以加群讨论:879133275

第三十一章 ADC实验

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

31.1 ADC简介
STM32H750xx系列有3个ADC,都可以独立工作,其中ADC1和ADC2还可以组成双重模式(提高采样率),ADC3比较独立,我们在下一章会有详细的讲解。
STM32H750的ADC主要特性我们可以总结为以下几条:
1、可配置16位、14位、12位、10位或8位分辨率,降低分辨率可以缩短转换时间,转换时间越短,可以做到的采样率就越高。
2、每个ADC支持多达20个的采集通道,其中有6路快速通道和14路慢速通道,慢速和快速的区别主要是支持的最高采样率不同,慢速通道要比快速通道低。这些通道的A/D转换可以单次、连续、扫描或间断模式执行。
3、ADC的结果可以左对齐或右对齐方式存储在32位数据寄存器中。
4、ADC具有五条专用的内部通道,内部参考电压 (VREFINT ) 连接到ADC3_INP/INN19,内部温度传感器 (VSENSE ) 连接到 ADC3_INP/INN18和VBAT 监测通道 (VBAT /4) 连接到 ADC3_INP/INN17,这三个都是连接到ADC3。另外DAC内部通道1连接到ADC2_INP/INN16,DAC内部通道2连接到ADC2_INP/INN17。
5、支持过采样,最高可以达到26位采样率。
6、每个ADC支持三路模拟看门狗。
7、支持单独输入和差分输入(可按通道进行编程)。
8、ADC输入范围:VREF– ≤ VIN ≤ VREF+。由VREF- 、VREF+ 、VDDA 和VSSA 这四个外部引脚决定。一般我们把VSSA 和VREF- 接地,把 VREF+ 和VDDA接到3.3V,所以得到ADC 的输入电压范围是:0~3.3V。注意不要接超出这个范围的电压进来,否则容易烧坏芯片。
9、自校准(偏移校准和线性度校准)。
10、最多4条注入转换序列,16条常规转换序列。
上面我们列出的一些特性都是ADC重要的特性,其它特性请查看参考手册。
下面来介绍ADC(仅限ADC1或ADC2)的框图:
在这里插入图片描述

图31.1.1 ADC框图
图中,我们标记了11处位置,分别如下:
① VREF+电压
VREF+ 是正模拟参考电压输入,选择范围是1.8V3.6V,开发板上我们一般给VREF+接入的电压时3.3V,所以得到开发板上的ADC测量范围是03.3V。
②ADC的双时钟域架构
ADC有两种时钟源可以选择,分别是:
(1)adc_hclk(属于同步时钟),来自AHB总线的系统时钟,ADC1和ADC2处在240MHz的 AHB1总线时钟。可以通过ADCx_CCR寄存器的CKMODE[1:0]位来选择不同分频的AHB1总线时钟,有以下的四种情况:
CKMODE[1:0]=00,这是异步时钟模式选择的配置,适用于下面要讲的adc_ker_ck时钟。
CKMODE[1:0]=01,adc_hclk/1(同步时钟模式)
CKMODE[1:0]=10,adc_hclk/2(同步时钟模式)
CKMODE[1:0]=11,adc_hclk/4(同步时钟模式)
比如我们选择4分频的adc_hclk,得到的频率是60MHz,但是数据手册限制ADC时钟频率最高是36MHz,说明这样配置就超频了,这是不可行的,因为超频误差会比较大。我们可以降低AHB总线时钟,但是这样会影响我们其它外设的性能,所以为了系统能达到最优的性能,我们一般不会选择adc_hclk作为ADC的时钟源,于是我们选择下面要说的这种时钟源。
(2)adc_ker_ck(属于异步时钟),可以通过RCC_D3CCIPR寄存器的ADCSEL [1:0]位来选择不同的时钟源,前提是前面提到的CKMODE[1:0]=00。ADC异步时钟模式下可以选择以下的时钟源:
ADCSEL [1:0]=00,pll2_p_ck作为ADC时钟源(复位后的默认值)
ADCSEL [1:0]=01,pll3_r_ck作为ADC时钟源
ADCSEL [1:0]=10,per_ck作为ADC时钟源
实际的例程中我们选择per_ck作为ADC时钟源,而per_ck 时钟可为 hse_ck、hsi_ker_ck 或 csi_ker_ck,通过RCC_D1CCIPR寄存器的CKPERSEL位选择,默认选择hsi_ker_ck作为per_ck的时钟源。hsi_ker_ck时钟源就是来自频率为64MHz的高速内部RC振荡器(HSI)。
选择了adc_ker_ck时钟源作为ADC的时钟,则可以通过ADCx_CCR寄存器的PRESC[3:0]位进行分频,可以是1、2、4、6、8、10、12、16、32、64、128、256这12种分频系数。
上面的分析请结合下面的ADC时钟方案图理解。
在这里插入图片描述

图31.1.2 ADC时钟方案
③输入通道
ADC总共有20个输入通道。注意:STM32H7的ADC支持单端/差分转换,由寄存器ADCx_DIFSEL(x=1~3)控制,该寄存器默认是0(单端模式),配置为1(则为差分模式)。因为H750的ADC支持差分通道输入,因此有ADCx_INP[19:0]和ADCx_INN[19:0]两组通道。其中,INP是差分正向输入,INN是差分反向输入。ADC_INP[0:5]和 ADC_INN[0:5]是快速模拟输入。ADC_INP[6:19]和 ADC_INN[6:19]是慢速模拟输入。如果我们使用单端输入,则只有ADCx_INP[19:0]有效,ADCx_INN[19:0]在内部自动接VSSA。
ADC连接5路内部模拟输入,分别是:
(1)内部参考电压 (VREFINT ) 连接到ADC3_INP/INN19。
(2)内部温度传感器 (VSENSE ) 连接到ADC3_INP/INN18。
(3)VBAT 监测通道 (VBAT/4) 连接到ADC3_INP/INN17。
(4)DAC内部通道1,连接到ADC2_INP/INN16。
(5)DAC内部通道2,连接到ADC2_INP/INN17。
为了方便大家查询ADC通道和IO的对应关系,给大家整理了表31.1.1。大多数情况,我们都是使用单端模式。前面也说了单端模式下,ADCx_INN[19:0]在内部自动接VSSA,所以表31.1.1中的通道0~通道19指的是ADCx_INP[19:0]。
在这里插入图片描述

表31.1.1 ADC通道表
由于开发板上使用的主控芯片是STM32H750VBT6,它的封装是LQFP100。该封装是没有表31.1.1中标黄色的IO引脚,所以相关的通道自然是没有引出来。
④转换序列
可以将转换分为两组:常规转换组和注入转换组。常规转换组最多允许16个通道进行转换。注入转换组最多允许4个通道进行转换。
如何理解常规转换组和注入转换组?常规转换组相当于你正常运行的程序,而注入转换组就相当于中断。在你程序正常执行的时候,中断是可以打断你的执行的,获得优先执行的权利。所以注入转换组可以打断常规转换组的转换,获得优先转换的权利,在注入转换组转换完成后,常规转换组才得以继续转换。
为了便于理解,请看常规转换组和注入转换组的转换优先级对比图,如图31.1.3所示:
在这里插入图片描述

图31.1.3 常规转换组和注入转换组的转换优先级对比图
常规转换组最多允许16个通道进行转换,注入转换组最多允许4个通道进行转换,那么转换的顺序怎么设置的?我们把这个转换顺序分别称为常规序列和注入序列。
(1)常规序列
常规序列在ADCx_SQRy寄存器中设置,每个ADC都有4个SQR寄存器,比如ADC1的SQR寄存器有ADC1_SQR1~ ADC1_SQR4。这四个寄存器怎么来设置常规序列的呢?下面通过表31.1.2给大家说明。
在这里插入图片描述

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

表31.1.3 注入序列寄存器控制关系汇总表
注入序列的长度写入JL [ 1 : 0 ]位,范围是0~3。注意:写入0表示这个注入序列有一个通道,而不是0个通道。
⑤触发源
ADC的触发转换有两种方法:分别是通过软件或外部事件(也就是硬件)触发转换。
我们先来看看通过软件触发转换的方法,常规通道由ADCx_CR寄存器的ADSTART位触发,注入通道由ADCx_CR寄存器的JADSTART位触发。方法是:通过对ADCx_CR寄存器的ADSTART(JADSTART)位写1开始转换,转换结束由硬件清零该位,这个控制ADC转换的方式非常简单。
另一种就是通过外部事件触发转换的方法,如定时器和输入引脚触发等等,具体请看《STM32H7xx参考手册_V3(中文版).pdf》的825页和826页的表192和表193。外部事件触发转换可分为:常规通道的外部触发和注入通道的外部触发两种。
adc_ext_trg[20:0],对应的就是常规通道的外部触发,共有21路。
adc_jext_trg[20:0],对应的就是注入通道的外部触发,共有21路。
如果选择硬件触发,则需要选择相应的硬件触发事件和触发边沿等,然后由外部硬件事件来触发ADC的采集(外部事件触发配置ADSTART位为1)。
硬件触发事件由ADCx_CFGR寄存器的EXTSEL[4:0]和ADCx_JSQR寄存器的 JEXTSEL[4:0]位来选择,分别是常规转换组和注入转换组的触发源选择。而触发边沿是通过ADCx_CFGR寄存器的EXTEN[1:0]和ADCx_JSQR寄存器的JEXTEN[1:0]位来选择。
⑥转换时间
STM32H7的ADC总转换时间的计算公式如下:
TCONV = 采样时间(TSMPL) + 逐次逼近时间(TSAR)
采样时间(TSMPL)可通过ADCx_SMPR1和ADCx_SMPR2寄存器中的SMP[2:0]位编程,ADC_SMPR1控制的是通道09,ADC_SMPR2控制的是通道1019。所有通道都可以通过编程来控制使用不同的采样时间,可选采样时间值如下:
SMP = 000:1.5 个 ADC 时钟周期
SMP = 001:2.5 个 ADC 时钟周期
SMP = 010:8.5 个 ADC 时钟周期
SMP = 011:16.5 个 ADC 时钟周期
SMP = 100:32.5 个 ADC 时钟周期
SMP = 101:64.5 个 ADC 时钟周期
SMP = 110:387.5 个 ADC 时钟周期
SMP = 111:810.5 个 ADC 时钟周期
逐次逼近时间(TSAR)是由分辨率决定的,分辨率通过对ADCx_CFGR寄存器的RES[1:0]位进行编程,可将分辨率配置为16位、14位、12位、10位、8位。而逐次逼近时间和分辨率的对应关系如下表所示:
在这里插入图片描述

表31.1.4 TSAR与分辨率的对应关系
举个例子,我们配置SMP = 111,即设置最大采样周期,然后采用16位分辨率,那么得到:
TCONV = 810.5个ADC时钟周期 + 8.5个ADC时钟周期 = 819个ADC时钟周期
表格中,FADC的频率是24MHZ,我们的例程中ADC的时钟源是64MHZ的hsi_ker_ck经过2分频得到,即32MHZ。我们就以FADC的频率为32MHZ来举例,可得到:
TCONV = 819个ADC时钟周期 = = 25.6us
⑦参考电压
选择参考电压,我们可以设置参考电压来自外部的Vref+,也可以设置参考电压来自内部的稳压器。
⑧ADC的核心
ADC的核心是一个16位的逐次逼近型ADC转换器,它根据我们设置好的参考电压、输入通道、启动条件等,执行模数转换。
⑨数据寄存器
常规转换组的转换结果会存放到ADCx_DR寄存器的RDATA[31:0]位中,注入转换组的转换结果会存放到ADCx_JDRy寄存器的JDATA1~4[31:0]位中。如果是使用双重模式,常规通道的数据则是存放在ADC_CDR寄存器。转换结果CPU可以通过AHB总线读取,同时也可以产生相关中断(adc_it)。
常规数据寄存器(ADCx_DR)(x=1~4)
常规数据寄存器ADC_DR是一个32位的寄存器。因为ADC的最大分辨率是16位,如果使用过采样,则分辨率可达26位,所以允许数据对齐方式。由ADC_CFGR2寄存器的OVSS[3:0]位和LSHIFT[3:0]位设置数据对齐方式。
细心的朋友可能发现,常规转换组最多有16个输入通道,而ADC常则数据寄存器只有一个,如果一个常则转换组用到好几个通道,数据怎么读取?如果使用多通道转换,那么这些通道的数据也会存放在DR里面,按照常规转换组的顺序,上一个通道转换的数据,会被下一个通道转换的数据覆盖掉,所以当通道转换完成后要及时把数据取走。比较常用的方法是使用DMA模式。当常规转换组的通道转换结束时,就会产生DMA请求,这样就可以及时把转换的数据搬运到用户指定的目的地址存放。如果没有使用DMA传输,可以通过判断ADC_ISR寄存器相关位来得到当前ADC的转换状态,从而进行控制。
注入数据寄存器(ADCx_JDRy)(x=13)(y=14)
每个ADC注入数据寄存器有4个,注入转换组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。ADC_JDRx寄存器是32位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC_CFGR2寄存器的OVSS[3:0]位和LSHIFT[3:0]位设置数据对齐方式。
通用常则数据寄存器ADC_CDR
常规数据寄存器ADC_DR仅适用于独立模式下,常规转换组的转换数据存储,而通用常规数据寄存器ADC_CDR适用于双重模式下,常规转换组的转换数据存储。在双重模式下,一般会配合DMA来传输数据。
⑩中断
对于每个ADC都可在下列情况下产生中断:
在这里插入图片描述

表31.1.5 每个ADC的ADC中断
在表31.1.5中,前面5个中断都很好理解,我们从模拟看门狗中断介绍。
模拟看门狗中断发生条件:首先通过ADCx_LTR和ADCx_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。
采样阶段结束:如果位EOSMP被硬件置1,则说明采样阶段结束(仅限常规转换),然后可以通过对EOSMP位写1来清零该位。如果EOSMPIE位置1,可以产生采样阶段结束中断。
上溢:如果常规转换后的数据未在新转换数据可用之前(由CPU或DMA)读取,会由溢出标志(OVR)指示缓冲区溢出事件。如果OVRIE位置1,可以产生一个溢出中断。
此外,我们还要知道常规组和注入组的转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据存储在内存里面,防止读取不及时数据被覆盖。
⑪通道预选控制信号
通道预选控制信号,用于将ADC某个通道连接到对应的GPIO上。PCSEL[19:0]每个位对应一个通道,总共20个通道。这一点和以前的STM32系列不一样,在使用的时候,需要特别注意。
⑫单次转换模式和连续转换模式
单次转换模式和连续转换模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
(1)单次转换模式
通过将ADC_CFGR寄存器的CONT位置0选择单次转换模式。该模式下,ADC只执行一次转换。单次转换由ADC_CR寄存器的ADSTART位(只适用于常规转换组)或者JADSTART位(只适用于注入转换组)启动,也可以通过外部事件触发启动(适用于常规转换组或注入转换组),并且触发外部事件之前,ADSTART位或JADSTART位必须置1。
在常规序列中,每次转换完成后:转换数据存储在32位ADCx_DR寄存器中、EOC(常规转换结束)标志置 1、EOCIE位置1时将产生中断。
在注入序列中,每次转换完成后:转换数据存储在四个32位ADC_JDR1寄存器的其中一个寄存器中、JEOC(注入转换结束)标志置1、JEOCIE 位置1时将产生中断。
常规序列完成后:EOS(常规序列结束)标志置1、EOSIE位置1时将产生中断。
注入序列完成后:JEOS(注入序列结束)标志置1、JEOSIE 位置1时将产生中断。
随后,ADC会停止工作,直至发生新的外部常规或注入触发,或者将ADSTART或JADSTART位再次置1。
(2)连续转换模式
通过将ADC_CFGR寄存器的CONT位置1选择连续转换模式。该模式仅适用于常规转换组。在连续转换模式下,如果发生软件或者硬件常规触发事件,ADC会将常规转换组的所有通道执行一次,随后会自动重启并持续执行序列的每个转换。CONT位为1时,可通过外部触发或将ADCx_CR寄存器中的ADSTART位置1来启动此模式。
在常规序列中,每次转换完成后:转换数据存储在32位ADCx_DR寄存器中、EOC(转换结束)标志置1、EOCIE 位置1时将产生中断。
转换序列完成后:EOS(序列结束)标志置 1、EOSIE位置1时将产生中断。
随后,会立即重启新序列,ADC会继续重复执行转换序列。
注意:注入通道不能连续转换,唯一例外的是,在连续转换模式下(使用JAUTO位)注入通道配置为在常规通道后的自动转换。
到这里,我们基本上介绍了ADC的大多数基础的知识点,其它知识后面用到会继续补充,如果还有不懂的内容,请参考《STM32H7xx参考手册_V3(中文版).pdf》的第25章。
31.2 单通道ADC采集实验
本实验我们来学习单通道ADC采集实验。本实验使用常规转换组单通道的单次转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。下面先带大家来了解本实验要配置的寄存器。
31.2.1 ADC寄存器
这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
ADCx通用控制寄存器(ADCx_CCR)
ADCx通用控制寄存器描述如图31.2.1.1所示:
在这里插入图片描述

图31.2.1.1 ADCx_CCR寄存器
该寄存器本章只需要用到PRESC[3:0]这四个位,用于设置ADC时钟的预分频系数(即对adc_ker_ck的分频系数),由上图可以得到这四个位域的值的含义可以表示为设置2^PRESC[3:0]分频。
adc_ker_ck的时钟源由RCC_D3CCIPR寄存器的ADCSEL[1:0]位配置。本章的实验我们都设置ADCSEL[1:0]=2,即选择per_ck作为时钟源,而per_ck又由RCC_D1CCIPR寄存器的CKPERSEL[1:0]位选择,默认为0,即选择hsi_ker_ck(64MHz)作为per_ck。因此:
adc_ker_ck=per_ck=hsi_ker_ck=64MHz。
又由于ADC的输入时钟频率不能大于36M,所以,我们需要设置PRESC[3:0]=1,即可得到ADC输入时钟频率为:
adc_ker_ck/2^PRESC[3:0]=64/2=32MHz。
ADCx控制寄存器(ADCx_CR)
ADCx控制寄存器描述如图31.2.1.2所示:
在这里插入图片描述

图31.2.1.2 ADCx_CR寄存器
该寄存器我们用到多个位,这里就不全部列出来讲解了,而是抽出几个重要的位进行针对性的介绍,详细的介绍请参考《STM32H7xx参考手册_V3(中文版).pdf》第25.5.3节,881页。
ADEN位用于使能ADC转换器。需要设置该位为1,ADC才可以正常工作。
ADSTART位用于启动ADC常规通道的转换序列。当使用硬件触发时(EXTEN[1:0]!=0),设置该位为1,必须在相应的硬件触发事件产生时,才会启动ADC转换。而当不使用硬件触发时(EXTEN[1:0]=0),设置该位为1则可以立即启动ADC转换。
BOOST位用于设置是否使用BOOST模式。当BOOST=0时,ADC输入时钟必须小于20MHz;当BOOST=1时,ADC输入时钟必须大于20MHz。因为我们设置的ADC输入时钟频率为32MHz,因此该位必须设置为1。
ADCALLIN位用于设置线性ADC校准。设置该位为1,可以设置ADC的校准模式为线性校准。
ADCAL位用与控制/读取ADC校准状态。设置该位为1时,可以启动ADC校准,等校准完成以后,硬件会自动清零该位。因此在设置改位为1以后,通过判断该位是否变为0,即可判断校准是否完成。
ADCx配置寄存器(ADCx_CFGR)
ADCx配置寄存器描述如图31.2.1.3所示:
在这里插入图片描述

图31.2.1.3 ADCx_CFGR寄存器
RES[2:0]位用于设置ADC转换的分辨率:0表示16位;1表示14位;2表示12位;3表示10位;4表示8位;其它值:保留。本实验使用16位分辨率,因此设置RES[2:0]=0即可。
EXTEN[1:0]位用于设置常规通道的外部触发方式和极性。本实验使用软件触发,因此设置EXTEN[1:0]=00即可,即禁止外部触发。
OVRMOD位用于设置是否使能覆写功能。当设置该位为0时,如果上一次转换的数据未及时读取,新的转换结果将被丢弃;当设置该位为1时,如果上一次转换的数据未及时读取,将会被新的结果覆盖。本实验该位设置为1。
CONT位用于设置转换模式。当CONT=0时,表示单次转换模式;当CONT=1时,表示连续转换模式。本实验该位设置为0。
ADCx配置寄存器2(ADCx_CFGR2)
ADCx配置寄存器2描述如图31.2.1.4所示:
在这里插入图片描述

图31.2.1.4 ADCx_CFGR2寄存器
OSR[9:0]位用于设置ADC的过采样率。OSR[9:0]=01023,表示1x1024x过采样。本实验不使用过采样,设置OSR[9:0]=0即可。
LSHIFT[3:0]位用于设置输出结果的左移位数,015表示左移015位。本实验使用右对齐,因此设置LSHIFT[3:0]=0即可。
ADCx常规序列寄存器1(ADCx_SQR1)
ADCx常规序列寄存器1描述如图31.2.1.5所示:
在这里插入图片描述

图31.2.1.5 ADCx_SQR1寄存器
L[3:0]位用于存储常规序列的长度,取值范围:015,表示常规序列长度为:116。本实验只用到1个通道,L[3:0]=0即可。
SQ1[4:0]SQ4[4:0]同于设置常规序列中第14个转换通道,第5~16个转换通道的设置请查看ADCx_SQR2和ADCx_SQR4寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的常规序列寄存器控制关系汇总表。
ADCx采样时间寄存器2(ADCx_SMPR2)
ADCx采样时间寄存器2描述如图31.2.1.6所示:
在这里插入图片描述

图31.2.1.6 ADCx_SMPR2寄存器
该寄存器用于设置ADC通道1019的采样时间,而ADCx_SMPR1设置ADC通道09的采样时间。STM32H7的ADC总转换时间的计算方法前面已经介绍过了。建议采样时间尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率,所以大家在实际应用中自行结合自身情况设置。
ADCx通道预选寄存器(ADCx_ PCSEL)
ADCx通道预选寄存器描述如图31.2.1.7所示:
在这里插入图片描述

图31.2.1.7 ADCx_ PCSEL寄存器
该寄存器用于控制ADC具体某个输入通道和对应IO的连接,相当于在ADC输入和IO之间,加了一个开关,想要正常使用某个通道,则必须设置对应的PCSELy位为1(y=0~19),否则无法得到对应IO口的正常电压。注意:在STM32H7之前的的其它STM32芯片上面,是没有该寄存器的。该寄存器的存在,有利于ADC和IO的隔离。
举个简单的例子,在STM32H7上面,即便是ADC通道对应的IO口,只要不使用ADC功能(PCSEL不设置为1),那么该IO口就可以兼容5V,但是在STM32H7之前的其它STM32芯片上面,ADC所在的IO口,都不能做5V兼容。
ADCx常规数据寄存器(ADCx_ DR)
ADCx常规数据寄存器描述如图31.2.1.8所示:
在这里插入图片描述

图31.2.1.8 ADCx_ DR寄存器
常规序列中的AD转化结果都将被存在这个寄存器里面,我们读取该寄存器,即可得到ADC转换后的结果,而注入通道的转换结果被保存在ADCx_JDRy(y=1~4)里面。
ADCx中断和状态寄存器(ADCx_ ISR)
ADCx中断和状态寄存器描述如图31.2.1.9所示:
在这里插入图片描述

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

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

图31.2.2.1 PA5与电位器示意图
使用短路帽将P3的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其它地方的电压值,我们只需要1跟杜邦线,一端接到P3的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
31.2.3 程序设计
31.2.3.1 ADC的HAL库驱动
ADC在HAL库中的驱动代码在stm32h7xx_hal_adc.c和stm32h7xx_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_InjectionConfigTypeDef InjectionConfig ; /* ADC注入通道配置结构,用于配置注入通道的转换顺序,数据格式等 */  
}ADC_HandleTypeDef;
该结构体定义和其他外设比较类似,我们着重看第二个成员变量Init含义,它是结构体
ADC_InitTypeDef类型,结构体ADC_InitTypeDef定义为:
typedef struct {
 uint32_t ClockPrescaler;                 	/* 设置预分频系数,即PRESC[3:0]位 */
 uint32_t Resolution;                      	/* 配置ADC的分辨率 */
 uint32_t ScanConvMode;                    	/* 扫描模式 */
 uint32_t EOCSelection;                    	/* 转换完成标志位 */
 FunctionalState LowPowerAutoWait;       	/* 低功耗自动延时 */
 FunctionalState ContinuousConvMode;     	/* 设置单次转换模式还是连续转换模式 */
 uint32_t NbrOfConversion;                 	/* 设置转换通道数目,赋值范围是1~16 */
 FunctionalState DiscontinuousConvMode; 	/* 设置常规转换组不连续模式 */
 uint32_t NbrOfDiscConversion;            	/* 常规转换组不连续模式转换通道的数目 */
 uint32_t ExternalTrigConv;                	/* ADC外部触发源选择*/
 uint32_t ExternalTrigConvEdge;           	/* ADC外部触发极性*/
 uint32_t ConversionDataManagement;      	/* 数据管理 */
 uint32_t Overrun;                          	/* 发生溢出时,进行的操作 */
 uint32_t LeftBitShift;                    	/* 数据左移几位 */
 FunctionalState OversamplingMode;       	/* 过采样模式 */
 ADC_OversamplingTypeDef Oversampling;  	/* 过采样的参数配置 */
} ADC_InitTypeDef;
  1. ClockPrescaler:ADC预分频系数选择,可选的分频系数为 1、2、4、6、8、10、12、16、32、64、128、256。ADC最大时钟配置为36MHZ。
  2. Resolution:配置ADC的分辨率,可选的分辨率有16 位、12 位、10 位和 8 位。分辨率越高,转换数据精度越高,转换时间也越长;反之分辨率越低,转换数据精度越低,转换时间也越短。
  3. ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。
  4. EOCSelection:可选参数为ADC_EOC_SINGLE_CONV和ADC_EOC_SEQ_CONV,指定转换结束时是否产生EOS中断或事件标志。
  5. LowPowerAutoWait:配置是否使用低功耗自动延迟等待模式,可选参数为 ENABLE和 DISABLE,当使能时,仅当一组内所有之前的数据已处理完毕时,才开始新的转换,适用于低频应用。该模式仅用于ADC的轮询模式,不可用于DMA以及中断。
  6. ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
  7. NbrOfConversion:设置常规转换通道数目,范围是:1~16。
  8. DiscontinuousConvMode:配置是否使用不连续的采样模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道 1 与通道 2,下次触发就是转换通道 5 与通道 7,这样不连续的转换,依次类推。此参数只有将 ScanConvMode 使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。
  9. NbrOfDiscConversion:不连续采样通道数。
  10. ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
  11. ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。
  12. ConversionDataManagement: 指定ADC转换后的数据处理方式。可以选择 DMA管理传输数据、数据存储在数据寄存器中或者是传输到DFSDM寄存器中。
  13. Overrun:当有新的数据溢出时,可以选择覆盖写入或者是丢弃新的数据。
  14. LeftBitShift:数据左移位数,最多可支持左移15位。
  15. OversamplingMode:是否使用过采样模式。
  16. Oversampling:配置过采样模式的参数。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  1. HAL_ADCEx_Calibration_Start函数
    ADC的自校准函数,其声明如下:
    HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc,
    uint32_t CalibrationMode, uint32_t SingleDiff);
    函数描述:
    首先调用HAL_ADC_Init函数配置了相关的功能后,再调用此函数进行ADC自校准功能。
    函数形参:
    形参1是ADC_HandleTypeDef结构体类型指针变量。
    形参2是校准模式选择,有以下两种:
    1)ADC_CALIB_OFFSET表示只运行偏移校准而不运行线性度校准。
    2)ADC_CALIB_OFFSET_LINEARITY表示同时运行偏移校准和线性度校准。
    形参3是单端或差分模式选择,有以下两种:
    1)ADC_SINGLE_ENDED表示单端输入模式。
    2)ADC_DIFFERENTIAL_ENDED表示差分输入模式。
    函数返回值:
    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采样周期 */
  uint32_t SingleDiff;                       	/* 输入信号线的类型*/
  uint32_t OffsetNumber;                     	/* 采用偏移量的通道 */
  uint32_t Offset;                         	   	/* 偏移量 */
  FunctionalState OffsetRightShift;        	/* 数据右移位数*/
  FunctionalState OffsetSignedSaturation; 	/* 转换数据格式为有符号位数据 */
} ADC_ChannelConfTypeDef;
  1. Channel:ADC转换通道,范围:0~19。
  2. Rank:在常规转换中的常规组的转换顺序,可以选择1~16。
  3. SamplingTime:ADC的采样周期,最大810.5个ADC时钟周期,要求尽量大以减少误差。
  4. SingleDiff:选择通道单端输入还是差分输入。
  5. OffsetNumber:选择使用偏移量的通道。
  6. Offset:定义要从原始数据减去的偏移量。根据ADC的分辨率不同,支持的最大偏移量也不同,例如分辨率是16bit,,最大的偏移量为0xFFFF。
  7. OffsetRightShift:采样值进行右移的位数。
  8. OffsetSignedSaturation:是否使能ADC采样值的最高位为符号位。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  1. HAL_ADC_Start函数
    ADC转换启动函数,其声明如下:
    HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
    函数描述:
    当配置好ADC的基础的功能后,就调用此函数启动ADC。
    函数形参:
    形参1是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函数来获取当前的转换值。
    函数形参:ADC_HandleTypeDef结构体类型指针变量。
    函数返回值:当前的转换值,uint32_t类型数据。
    单通道ADC采集配置步骤
    1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
    首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道19,对应IO是PA5,它们的时钟开启方法如下:
    __HAL_RCC_ADC12_CLK_ENABLE (); /
    使能ADC1 & ADC2 时钟 /
    __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值。
    31.2.3.2 程序流程图
    在这里插入图片描述

图31.2.3.2.1 单通道ADC采集实验程序流程图
31.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_5
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE()     
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 */

#define ADC_ADCX                           ADC1 
#define ADC_ADCX_CHY                      ADC_CHANNEL_19 /* 通道Y,  0 <= Y <= 19 */ 
#define ADC_ADCX_CHY_CLK_ENABLE()           
do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0)   /* ADC1 & ADC2 时钟使能 */
/*****************************************************************************/

ADC的通道与引脚的对应关系在 《STM32H750VBT6.pdf》 数据手册可以查到,我们这里使用ADC1的通道19,在数据手册中的表格为:
在这里插入图片描述

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

/**
 * @brief      ADC初始化函数
 *   @note     本函数支持ADC1/ADC2任意通道,但是不支持ADC3
 *              我们使用16位精度, ADC采样时钟=32M, 转换时间为:采样周期 + 8.5个ADC周期
 *              设置最大采样周期: 810.5, 则转换时间 = 819个ADC周期 = 25.6us
 * @param      无
 * @retval     无
 */
void adc_init(void)
{
g_adc_handle.Instance = ADC_ADCX;                             /* 选择哪个ADC */
/* 输入时钟2分频,即adc_ker_ck=per_ck/2=32Mhz */
    g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2;    
    g_adc_handle.Init.Resolution = ADC_RESOLUTION_16B;        /* 16位模式  */
    g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;        /* 非扫描模式 */
    g_adc_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;    /* 关闭EOC中断 */
    g_adc_handle.Init.LowPowerAutoWait = 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; /* 软件触发 */
/* 采用软件触发的话,此位忽略 */
g_adc_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;  
/* 常规通道的数据仅仅保存在DR寄存器里面 */
g_adc_handle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR;     
/* 有新的数据后直接覆盖掉旧数据 */
g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;      
/* 设置ADC转换结果的左移位数 */
    g_adc_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; 
    g_adc_handle.Init.OversamplingMode = DISABLE;              /* 关闭过采样 */
    HAL_ADC_Init(&g_adc_handle);                                   /* 初始化 */

HAL_ADCEx_Calibration_Start(&g_adc_handle, ADC_CALIB_OFFSET, 
ADC_SINGLE_ENDED); /* 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;

        ADC_ADCX_CHY_CLK_ENABLE();                       /* 使能ADC1/2时钟 */
        ADC_ADCX_CHY_GPIO_CLK_ENABLE();                 /* 开启ADC通道IO引脚时钟 */

        __HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP); /* ADC外设时钟选择 */

        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的时钟源来自于per_ck。
接下来要介绍的是adc_get_result函数,其定义如下:

/**
 * @brief       获得ADC转换后的结果 
 * @param       ch: 通道值 0~19,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_19
 * @retval      返回值:转换结果
 */
uint32_t adc_get_result(uint32_t ch)   
{
    ADC_ChannelConfTypeDef adc_ch_conf;

    adc_ch_conf.Channel = ch;                                  	/* 通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                  	/* 1个序列 */
/* 采样时间,设置最大采样周期: 810.5个ADC周期 */
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_810CYCLES_5; 
    adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED;             	/* 单边采集 */
    adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE;           	/* 不使用偏移量的通道 */
    adc_ch_conf.Offset = 0;                                   	/* 偏移量为0 */
    HAL_ADC_ConfigChannel(&g_adc_handle ,& adc_ch_conf); 	/* 通道配置 */

    HAL_ADC_Start(&g_adc_handle);                            	/* 开启ADC */
    HAL_ADC_PollForConversion(&g_adc_handle, 10);         	/* 轮询转换 */
    return HAL_ADC_GetValue(&g_adc_handle);  /* 返回最近一次ADC1常规组的转换结果 */
}

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

/**
 * @brief       获取通道ch的转换值,取times次,然后平均
 * @param       ch      : 通道号, 0~19
 * @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_cache_enable();                 	 	/* 打开L1-Cache */
    HAL_Init();                        		 	/* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4); 	/* 设置时钟, 480Mhz */
    delay_init(480);                    		/* 延时初始化 */
usart_init(115200);                		/* 串口初始化为115200 */
    mpu_memory_protection();              	/* 保护相关存储区域 */
    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_CH19_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH19_VOL:0.000V", BLUE); 
    while (1)
{
/* 获取通道5的转换值,10次取平均 */
        adcx = adc_get_result_average(ADC_ADCX_CHY, 10); 
        lcd_show_xnum(142, 110, adcx, 5, 16, 0, BLUE);/* 显示ADCC采样后的原始值 */
/* 获取计算后的带小数的实际电压值,比如3.1111 */ 
        temp = (float)adcx * (3.3 / 65536); 
        adcx = temp;          /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
        lcd_show_xnum(142, 130, adcx, 1, 16, 0, BLUE);   

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

        LED0_TOGGLE();
        delay_ms(100);
    }
}

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

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

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

图31.3.1.1 ADCx_CFGR寄存器
ADCx_CFGR寄存器中我们主要跟前面设置不同的有两个位,分别如下:
CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用连续转换模式,所以CONT位置1即可。
DMNGT[1:0]位用于数据管理配置。单通道ADC采集实验我们是默认设置为00:常规转换数据仅存储在DR中,然后通过软件去DR数据寄存器读取。本实验我们要设置为01:选择 DMA单次模式,这样启动一次DMA传输,DMA就会自动读取一次数据。
这里介绍ADCx_CFGR寄存器的这两个位,其它请参考上一个实验的配置。下面介绍DMA一些比较重要的寄存器配置。
DMA数据流x外设地址寄存器(DMA_SxPAR)
DMA数据流x外设地址寄存器描述如图31.3.1.2所示:
在这里插入图片描述

图31.3.1.2 DMA_SxPAR寄存器
该寄存器用于存放数据流x的外设地址。本实验,我们需要通过DMA读取ADC1转换后存放在ADC1常规数据寄存器 (ADC1_DR) 的结果数据。所以我们需要给DMA_SxPAR寄存器写入ADC1_DR寄存器的地址。这样配置后,DMA就会从ADC1_DR寄存器的地址读取ADC的转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_SxM0AR寄存器来设置,比如定义一个变量,把这个变量的地址值写入该寄存器。
注意:DMA_SxPAR寄存器受到写保护,只有DMA_SxCR寄存器中的EN为“0”时才可以写入,即先要禁止数据流传输才可以写入。
DMA数据流x存储器0地址寄存器(DMA_SxM0AR)
DMA数据流x存储器0地址寄存器描述如图31.3.1.3所示:
在这里插入图片描述

图31.3.1.3 DMA_SxM0AR寄存器
该寄存器存放的是DMA数据流x存储器0地址。如果用到双缓冲区模式我们还需要用到DMA_SxM1AR寄存器,本实验我们是用不到的。
DMA数据流x数据项数寄存器(DMA_SxNDTR)
DMA数据流x数据项数寄存器描述如图31.3.1.4所示:
在这里插入图片描述

图31.3.1.4 DMA_SxNDTR
该寄存器控制DMA数据流x的每次传输所要传输的数据量。其设置范围为0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前DMA传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为16位,那么传输一次(一个项)就是2个字节。
其它的DMA寄存器我们就不一一介绍了,请大家看着寄存器源码对照手册理解,都不难。
31.3.2 硬件设计

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

图31.2.2.1 PA5与电位器示意图
使用短路帽将P3的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其他地方的电压值,我们只需要1跟杜邦线,一端接到P3的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
31.3.3 程序设计
31.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和BDMA都是用的这个函数。
    函数形参:
    形参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通道19,对应IO是PA5,它们的时钟开启方法如下:
    __HAL_RCC_ADC12_CLK_ENABLE (); /
    使能ADC1 & ADC2 时钟 /
    __HAL_RCC_GPIOA_CLK_ENABLE(); /
    开启GPIOA时钟 */
    2)初始化ADCx, 配置其工作参数
    通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
    注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP回调函数。
    3)配置ADC通道
    在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_Stream7的中断服务函数为DMA1_Stream7_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法就是如此。
    还可以通过调用HAL库提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
    31.3.3.2 程序流程图
    在这里插入图片描述

图31.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图
31.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。
由于本实验用到DMA,所以在adc.h头文件定义了以下一些宏定义:
/*****************************************************************************/
/* ADC单通道/多通道 DMA采集 DMA数据流相关 定义

  • 注意: 这里我们的通道还是使用上面的定义 */
#define ADC_ADCX_DMASx                      DMA1_Stream7 
#define ADC_ADCX_DMASx_REQ                 DMA_REQUEST_ADC1    /* ADC1_DMA请求源 */
#define ADC_ADCX_DMASx_IRQn                DMA1_Stream7_IRQn 
#define ADC_ADCX_DMASx_IRQHandler         DMA1_Stream7_IRQHandler 

/*判断DMA1 Stream7传输完成标志, 这是一个假函数形式, 不能当函数使用, 只能用在if等语句里*/
#define ADC_ADCX_DMASx_IS_TC()            
 ( __HAL_DMA_GET_FLAG(&g_dma_adc_handle, DMA_FLAG_TCIF3_7) )  

/* 清除DMA1 Stream7传输完成标志 */
#define ADC_ADCX_DMASx_CLR_TC()             
do{ __HAL_DMA_CLEAR_FLAG(&g_dma_adc_handle, DMA_FLAG_TCIF3_7); }while(0)   
/*****************************************************************************/
下面给大家介绍adc.c文件里面的函数,首先是ADC DMA读取初始化函数。
/**
 * @brief       ADC DMA读取 初始化函数
 * @param       par         : 外设地址
 * @param       mar         : 存储器地址
 * @retval      无
 */
void adc_dma_init(uint32_t par, uint32_t mar)
{
    GPIO_InitTypeDef gpio_init_struct;
    ADC_ChannelConfTypeDef adc_ch_conf = {0};
    
    ADC_ADCX_CHY_GPIO_CLK_ENABLE();                     /* 开启ADC通道IO引脚时钟 */
    ADC_ADCX_CHY_CLK_ENABLE();                           /* 使能ADC1/2时钟 */
    
/* 得到当前stream是属于DMA2还是DMA1 */
    if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)
    {
        __HAL_RCC_DMA2_CLK_ENABLE();                     /* DMA2时钟使能 */
    }
    else
    {
        __HAL_RCC_DMA1_CLK_ENABLE();                     /* DMA1时钟使能 */
    }

    __HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP);      /* ADC外设时钟选择 */

    /* 初始化ADC采集通道对应的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_DMASx;           /* 使用DMA1 Stream7 */
    g_dma_adc_handle.Init.Request = ADC_ADCX_DMASx_REQ; /* 请求选择*/
    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;         /* 存储器增量模式 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;    
/* 外设数据长度:16位 */
/* 存储器数据长度: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;   /* 中等优先级 */
    g_dma_adc_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;  /* 禁止FIFO*/
HAL_DMA_Init(&g_dma_adc_handle);                             /* 初始化DMA */

/* 将DMA句柄与ADC句柄关联起来 */
    __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle); 

    /* 初始化ADC */
g_adc_dma_handle.Instance = ADC_ADCX;                     /* 选择哪个ADC */
/* 输入时钟2分频,即adc_ker_ck=per_ck/2=32Mhz */
    g_adc_dma_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2;      
    g_adc_dma_handle.Init.Resolution = ADC_RESOLUTION_16B;  /* 16位模式  */
    g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;  /* 非扫描模式 */
    g_adc_dma_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 关闭EOC中断 */
    g_adc_dma_handle.Init.LowPowerAutoWait = 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;/* 采用软件触发 */
/* 采用软件触发的话,此位忽略 */
    g_adc_dma_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
g_adc_dma_handle.Init.ConversionDataManagement = 
ADC_CONVERSIONDATA_DMA_ONESHOT;    /* DMA单次传输ADC数据 */
/* 有新的数据后直接覆盖掉旧数据 */
g_adc_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;    
/* 设置ADC转换结果的左移位数 */
    g_adc_dma_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; 
    g_adc_dma_handle.Init.OversamplingMode = DISABLE;   /* 过采样关闭 */
    HAL_ADC_Init(&g_adc_dma_handle);                       /* 初始化 */
    
HAL_ADCEx_Calibration_Start(&g_adc_dma_handle, ADC_CALIB_OFFSET, 
ADC_SINGLE_ENDED); /* ADC校准 */

    /* 配置ADC通道 */
    adc_ch_conf.Channel = ADC_ADCX_CHY;               /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1;           /* 采样序列里的第1个 */
/* 采样周期为810.5个时钟周期 */
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_810CYCLES_5;  
    adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED ;     /* 单端输入 */
    adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE;     /* 无偏移 */
    adc_ch_conf.Offset = 0;                             /* 无偏移的情况下,此参数忽略 */
    adc_ch_conf.OffsetRightShift = DISABLE;          /* 禁止右移 */
    adc_ch_conf.OffsetSignedSaturation = DISABLE;  /* 禁止有符号饱和 */
    HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);  /* 配置ADC通道 */

    /* 配置DMA数据流请求中断优先级 */
    HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
    HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);

    HAL_DMA_Start_IT(&g_dma_adc_handle, par, mar, 0); /* 启动DMA,并开启中断 */
    HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);  /* 开启ADC,通过DMA传输结果 */
}

adc_dma_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。下面来看看该函数的代码内容。
第一部分使能ADC、DMA和GPIO的时钟。
第二部分选择ADC的时钟源为per_ck,per_ck默认选择hsi_ker_ck作为时钟源,即ADC的时钟源为64MHZ的hsi_ker_ck。
第三部分是设置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 ndtr)
{
    ADC_ADCX->CR &= ~(1 << 0);         	/* 先关闭ADC */

    ADC_ADCX_DMASx->CR &= ~(1 << 0);   	/* 关闭DMA传输 */
    while (ADC_ADCX_DMASx->CR & 0X1);  	/* 确保DMA可以被设置 */
    ADC_ADCX_DMASx->NDTR = ndtr;        	/* 要传输的数据项数目 */
    ADC_ADCX_DMASx->CR |= 1 << 0;       	/* 开启DMA传输 */
    ADC_ADCX->CR |= 1 << 0;              	/* 重新启动ADC */
    ADC_ADCX->CR |= 1 << 2;              	/* 启动常规转换通道 */
}

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

/**
 * @brief       ADC DMA采集中断服务函数
 * @param       无 
 * @retval      无
 */
void ADC_ADCX_DMASx_IRQHandler(void)
{
    if (ADC_ADCX_DMASx_IS_TC())
    {
        g_adc_dma_sta = 1;          	/* 标记DMA传输完成 */
        ADC_ADCX_DMASx_CLR_TC();    	/* 清除DMA1 数据流7 传输完成中断 */
    }
}

在该函数里,通过判断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;

    sys_cache_enable();                    	/* 打开L1-Cache */
    HAL_Init();                              	/* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4); 	/* 设置时钟, 480Mhz */
    delay_init(480);                        	/* 延时初始化 */
    usart_init(115200);                    	/* 串口初始化为115200 */
    mpu_memory_protection();              	/* 保护相关存储区域 */
    led_init();                             		/* 初始化LED */
    lcd_init();                             		/* 初始化LCD */
/* 初始化ADC DMA采集 */
adc_dma_init((uint32_t)&ADC1->DR, (uint32_t)&g_adc_dma_buf); 
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_CH19_VAL:”, BLUE);
/* 先在固定位置显示小数点 /
lcd_show_string(30, 130, 200, 16, 16, “ADC1_CH19_VOL:0.000V”, BLUE);
adc_dma_enable(ADC_DMA_BUF_SIZE); /
启动ADC DMA采集

*/
    while (1)
    {
        if (g_adc_dma_sta == 1)
        { 
            /* 清D Cache */
            SCB_InvalidateDCache();
            /* 计算DMA 采集到的ADC数据的平均值 */
            sum = 0;
            for (i=0; i<100; i++)   /* 累加 */
            {
                sum += g_adc_dma_buf[i];
            }
            adcx= sum / ADC_DMA_BUF_SIZE;   /* 取平均值 */
            
            /* 显示结果 */ 
            lcd_show_xnum(142, 110, adcx, 5, 16, 0, BLUE);/* 显示ADCC采样的原始值*/
            /* 获取计算后的带小数的实际电压值,比如3.1111 */
            temp = (float)adcx * (3.3 / 65536);
            adcx = temp;  /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
            lcd_show_xnum(142, 130, adcx, 1, 16, 0, BLUE);   
/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
            temp -= adcx;     
/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数。 */                              
            temp *= 1000; 
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
            lcd_show_xnum(158, 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采集实验一样。首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。
31.3.4 下载验证
下载代码后,可以看到LCD显示如图31.3.4.1所示:
在这里插入图片描述

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

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

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

  1. 例程功能
    使用ADC1采集(DMA读取)通道14\15\16\17\18\19的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
  2. 硬件资源
    1)RGB灯
    RED : LED0 - PB4
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)ADC1 :通道14 - PA2、通道15 - PA3、通道16 - PA0、
    通道17 - PA1、通道18 - PA4、通道19 - PA5
    5)DMA(DMA1 数据流7 DMA请求源9)
  3. 原理图
    ADC和DMA属于STM32H750内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道14\15\16\17\18\19来采集外部电压值,并通过DMA来读取。
    31.4.3 程序设计
    31.4.3.1 ADC的HAL库驱动
    本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍多通道ADC采集(DMA读取)配置步骤。
    多通道ADC采集(DMA读取)配置步骤
    1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
    首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道14、15、16、17、18、19,对应IO是PA2、PA3、PA0、PA1、PA4和PA5,它们的时钟开启方法如下:
    __HAL_RCC_ADC12_CLK_ENABLE(); /* 使能ADC1 & ADC2 时钟 /
    __HAL_RCC_GPIOA_CLK_ENABLE(); /
    开启GPIOA时钟 */
    2)初始化ADCx, 配置其工作参数
    通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
    注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP回调函数。
    3)配置ADC通道
    在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_Stream7的中断服务函数为DMA1_Stream7_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法就是如此。
    还可以通过调用HAL库提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
    31.4.3.2 程序流程图
    在这里插入图片描述

图31.4.3.2.1 多通道ADC采集(DMA读取)实验程序流程图
31.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_DMASx
(默认=DMA1_Stream7) 及其相关定义不要乱修改adc.h里面的这两部分内容, 必须在
理解原理的基础上进行修改, 否则可能导致无法正常使用.
* @param   par         : 外设地址
 * @param   mar         : 存储器地址 
 * @retval  无
 */
void adc_nch_dma_init(uint32_t par, uint32_t mar)
{
    GPIO_InitTypeDef gpio_init_struct;
    ADC_ChannelConfTypeDef adc_ch_conf = {0};

    __HAL_RCC_GPIOA_CLK_ENABLE();                         /* 开启GPIOA引脚时钟 */
    ADC_ADCX_CHY_CLK_ENABLE();                            /* 使能ADC1/2时钟 */

    /* 得到当前stream是属于DMA2还是DMA1 */
    if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)      
    {
        __HAL_RCC_DMA2_CLK_ENABLE();                     /* DMA2时钟使能 */
    }
    else
    {
        __HAL_RCC_DMA1_CLK_ENABLE();                    /* DMA1时钟使能 */
    }

    __HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP);    /* ADC外设时钟选择 */

    /* 初始化ADC多通道对应的GPIO
     * PA0-ADC_CHANNEL_16、PA1-ADC_CHANNEL_17、PA2-ADC_CHANNEL_14
     * PA3-ADC_CHANNEL_15、PA4-ADC_CHANNEL_18、PA5-ADC_CHANNEL_19
     */
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_DMASx;     /* 使用DMA1 Stream7 */
/* 请求选择DMA_REQUEST_ADC1 */
g_dma_nch_adc_handle.Init.Request = ADC_ADCX_DMASx_REQ;  
/* 传外设到存储器模式 */
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;  /* 中等优先级 */
    g_dma_nch_adc_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 禁止FIFO*/
    HAL_DMA_Init(&g_dma_nch_adc_handle);                           /* 初始化DMA */

    /* 将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 */
/* 输入时钟2分频,即adc_ker_ck=per_ck/2=32Mhz */
    g_adc_nch_dma_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2;  
    g_adc_nch_dma_handle.Init.Resolution = ADC_RESOLUTION_16B;   /* 16位模式  */
    g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;    /* 扫描模式 */
    g_adc_nch_dma_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;/*关闭EOC中断 */
    g_adc_nch_dma_handle.Init.LowPowerAutoWait = DISABLE;   /* 自动低功耗关闭 */
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;
g_adc_nch_dma_handle.Init.ExternalTrigConvEdge = 
ADC_EXTERNALTRIGCONVEDGE_NONE;    /* 采用软件触发的话,此位忽略 */
g_adc_nch_dma_handle.Init.ConversionDataManagement = 
ADC_CONVERSIONDATA_DMA_ONESHOT;   /* DMA单次传输ADC数据 */
/* 有新的数据后直接覆盖掉旧数据 */
g_adc_nch_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;   
/* 设置ADC转换结果的左移位数 */
    g_adc_nch_dma_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; 
    g_adc_nch_dma_handle.Init.OversamplingMode = DISABLE;   /* 过采样关闭 */
    HAL_ADC_Init(&g_adc_nch_dma_handle);                       /* 初始化 */

HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle, ADC_CALIB_OFFSET, 
ADC_SINGLE_ENDED);           /* ADC校准 */
    
    /* 配置ADC通道 */
    adc_ch_conf.Channel = ADC_CHANNEL_14;            /* 配置使用的ADC通道 */
adc_ch_conf.Rank = ADC_REGULAR_RANK_1;           /* 采样序列里的第1个 */
/* 采样周期为810.5个时钟周期 */
    adc_ch_conf.SamplingTime = ADC_SAMPLETIME_810CYCLES_5;     
    adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED ;    /* 单端输入 */
    adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE;    /* 无偏移 */
    adc_ch_conf.Offset = 0;                            /* 无偏移的情况下,此参数忽略 */
    adc_ch_conf.OffsetRightShift = DISABLE;         /* 禁止右移 */
    adc_ch_conf.OffsetSignedSaturation = DISABLE;  /* 禁止有符号饱和 */
    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道 */

    adc_ch_conf.Channel = ADC_CHANNEL_15;           /* 配置使用的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_16;           /* 配置使用的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_17;           /* 配置使用的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_18;          /* 配置使用的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_19;          /* 配置使用的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_DMASx_IRQn, 3, 3);
    HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);

HAL_DMA_Start_IT(&g_dma_nch_adc_handle, par, mar, 0); /* 启动DMA,并开启中断 */
/* 开启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的时钟源为per_ck,per_ck默认选择hsi_ker_ck作为时钟源,即ADC的时钟源为64MHZ的hsi_ker_ck。
第三部分是设置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_cache_enable();                    	/* 打开L1-Cache */
    HAL_Init();                              	/* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4); 	/* 设置时钟, 480Mhz */
    delay_init(480);                        	/* 延时初始化 */
    usart_init(115200);                    	/* 串口初始化为115200 */
    mpu_memory_protection();              	/* 保护相关存储区域 */
    led_init();                             		/* 初始化LED */
    lcd_init();                             		/* 初始化LCD */
/* 初始化ADC DMA采集 */
    adc_nch_dma_init((uint32_t)&ADC1->DR, (uint32_t)&g_adc_dma_buf);    

    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, 130, 200, 12, 12, "ADC1_CH14_VAL:", BLUE);
   /* 先在固定位置显示小数点 */
   lcd_show_string(30, 142, 200, 12, 12, "ADC1_CH14_VOL:0.000V", BLUE);
    
lcd_show_string(30, 160, 200, 12, 12, "ADC1_CH15_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 172, 200, 12, 12, "ADC1_CH15_VOL:0.000V", BLUE); 

lcd_show_string(30, 190, 200, 12, 12, "ADC1_CH16_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 202, 200, 12, 12, "ADC1_CH16_VOL:0.000V", BLUE); 

lcd_show_string(30, 220, 200, 12, 12, "ADC1_CH17_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 232, 200, 12, 12, "ADC1_CH17_VOL:0.000V", BLUE); 

lcd_show_string(30, 250, 200, 12, 12, "ADC1_CH18_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 262, 200, 12, 12, "ADC1_CH18_VOL:0.000V", BLUE); 

lcd_show_string(30, 280, 200, 12, 12, "ADC1_CH19_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 292, 200, 12, 12, "ADC1_CH19_VOL:0.000V", BLUE); 

    adc_dma_enable(ADC_DMA_BUF_SIZE);   /* 启动ADC DMA采集 */
    while (1)
    {
        if (g_adc_dma_sta == 1)
        {
            /* 清除D Cache数据 */
            SCB_InvalidateDCache();
            /* 循环显示通道14~通道19的结果 */
            for(j = 0; j < 6; j++)  /* 遍历6个通道 */
            {
                sum = 0; /* 清零 */
/* 每个通道采集了50次数据,进行50次累加 */
                for (i = 0; i < ADC_DMA_BUF_SIZE / 6; i++)  
                {
                    sum += g_adc_dma_buf[(6 * i) + j];  /* 相同通道的转换数据累加 */
                }
                adcx = sum / (ADC_DMA_BUF_SIZE / 6);    /* 取平均值 */
                
                /* 显示结果 */
/* 显示ADCC采样后的原始值 */
                lcd_show_xnum(114, 130 + (j * 30), adcx, 5, 12, 0, BLUE);   
/* 获取计算后的带小数的实际电压值,比如3.1111 */
                temp = (float)adcx * (3.3 / 65536); 
                adcx = temp;    /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
                lcd_show_xnum(114, 142 + (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(126, 142 + (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的结果。
输入通道14的转换数据存放在g_adc_dma_buf[0]到g_adc_dma_buf[49],输入通道15的转换数据存放在g_adc_dma_buf[50]到g_adc_dma_buf[99],后面的以此类推。然后对数组的每个通道的数据取平均值,减少误差。最后在LCD屏上显示ADC的转换值和换算成电压后的电压值。
31.4.4 下载验证
下载代码后,LED0闪烁,提示程序运行在这里插入图片描述
。可以看到LCD显示如图31.4.4.1所示:

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

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

31.5 单通道ADC过采样(26位分辨率)实验
本实验我们来学习单通道ADC过采样(26位分辨率)实验。本实验使用常规转换组单通道的单次转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。下面先带大家来了解本实验要配置的寄存器。
31.5.1 ADC寄存器
本实验很多配置和单通道ADC采集实验是一样的,下面只介绍ADCx_CFGR2寄存器。
ADCx配置寄存器2(ADCx_CFGR2)
ADCx配置寄存器2描述如图31.5.1.1所示:
在这里插入图片描述

图31.5.1.1 ADCx_CFGR2寄存器
OSR[9:0]位用于设置ADC的过采样率。OSR[9:0]=01023,表示1x1024x过采样。本实验使用过采样,并且是26位分辨率,所以设置OSR[9:0] = 1023。
ROVSM位用于设置常规过采样模式,默认为0即可,即连续模式。
TROVS位用于设置已触发常规过采样,默认为0即可,即会在触发后连续完成某一通道的所有过采样转换。
OVSS[3:0]位用于设置过采样结果右移位数。
ROVSE位是常规过采样使能位,置1使能常规过采样。
31.5.2 硬件设计

  1. 例程功能
    ADC1过采样(26位分辨率)采集通道19(PA5)上面的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
  2. 硬件资源
    1)RGB灯
    RED : LED0 - PB4
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)ADC1 :通道19 - PA5
  3. 原理图
    ADC属于STM32H750内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1过采样(26位分辨率)通道19(PA5)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V,可以通过断路帽将PA5与电位器连接,从而测量电位器的电压。
    31.5.3 程序设计
    31.5.3.1 ADC的HAL库驱动
    本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。
    单通道ADC过采样(26位分辨率)配置步骤
    1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
    首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道19,对应IO是PA5,它们的时钟开启方法如下:
    __HAL_RCC_ADC12_CLK_ENABLE (); /* 使能ADC1 & ADC2 时钟 /
    __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)读取ADC值
    这里选择查询方式读取,在读取ADC值之前需要调用HAL_ADC_PollForConversion等待上一次转换结束。然后就可以通过HAL_ADC_GetValue来读取ADC值。
    31.5.3.2 程序流程图
    在这里插入图片描述

图31.5.3.2.1 单通道ADC过采样(26位分辨率)实验程序流程图
31.5.3.3 程序解析
adc.h文件只是添加了函数声明,下面来介绍adc.c文件的adc_oversample_init函数。

/**
 * @brief       ADC 过采样 初始化函数
 *   @note      本函数可以控制ADC过采样范围从1x ~ 1024x, 得到最高26位分辨率的AD转换结果
 * @param       osr : 过采样倍率, 0 ~ 1023, 表示:1x ~ 1024x过采样倍率
 * @param       ovss: 过采样右移位数, 0~11, 表示右移0位~11位.
 *   @note      过采样后, ADC的转换时间相应的会慢 osr倍.
 * @retval      无
 */
void adc_oversample_init(uint32_t osr, uint32_t ovss)
{
    GPIO_InitTypeDef gpio_init_struct;

    ADC_ADCX_CHY_GPIO_CLK_ENABLE();                  /* 开启ADC通道IO引脚时钟 */
    ADC_ADCX_CHY_CLK_ENABLE();                        /* 使能ADC1/2时钟 */
    __HAL_RCC_ADC_CONFIG(RCC_ADCCLKSOURCE_CLKP);  /* ADC外设时钟选择 */

    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);

g_adc_handle.Instance = ADC_ADCX;               /* 选择哪个ADC */
/* 输入时钟2分频,即adc_ker_ck=per_ck/2=32Mhz */
    g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2;  
    g_adc_handle.Init.Resolution = ADC_RESOLUTION_16B;    /* 16位模式  */
    g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;    /* 非扫描模式 */
    g_adc_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;/* 关闭EOC中断 */
    g_adc_handle.Init.LowPowerAutoWait = 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;    /* 软件触发 */
/* 采用软件触发的话,此位忽略 */
g_adc_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;  
/* 常规通道的数据仅仅保存在DR寄存器里面 */
g_adc_handle.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR;   
/* 有新的数据后直接覆盖掉旧数据 */
g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
/* 设置ADC转换结果的左移位数 */
    g_adc_handle.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE;    
    g_adc_handle.Init.OversamplingMode = ENABLE;            /* 开启过采样 */
    g_adc_handle.Init.Oversampling.Ratio = osr;             /* 设置osr+1倍过采样 */
g_adc_handle.Init.Oversampling.RightBitShift = ovss;  /* 数据右移ovss  bit */
g_adc_handle.Init.Oversampling.TriggeredMode = 
ADC_TRIGGEREDMODE_SINGLE_TRIGGER;  /* 会在触发后连续完成通道的所有过采样转换 */ 
g_adc_handle.Init.Oversampling.OversamplingStopReset = 
ADC_REGOVERSAMPLING_CONTINUED_MODE;/* ROVSE=1, 使能常规过采样 */
    HAL_ADC_Init(&g_adc_handle);            /* 初始化 */

HAL_ADCEx_Calibration_Start(&g_adc_handle, ADC_CALIB_OFFSET, 
ADC_SINGLE_ENDED);  /* ADC校准 */
}

adc_oversample_init函数大概可以分为下面几个部分的功能:
第一部分使能ADC和GPIO的时钟。
第二部分选择ADC的时钟源为per_ck,per_ck默认选择hsi_ker_ck作为时钟源,即ADC的时钟源为64MHZ的hsi_ker_ck。
第三部分是设置ADC采集通道对应IO引脚工作模式。
第四部分初始化ADC,并校准ADC。在这里,开启过采样、还配置过采样率等。
最后在main.c里面编写如下代码:

int main(void)
{
    uint32_t adcx;
    float temp;

    sys_cache_enable();                    	/* 打开L1-Cache */
    HAL_Init();                              	/* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4); 	/* 设置时钟, 480Mhz */
    delay_init(480);                        	/* 延时初始化 */
    usart_init(115200);                    	/* 串口初始化为115200 */
    mpu_memory_protection();              	/* 保护相关存储区域 */
    led_init();                             		/* 初始化LED */
    lcd_init();                             		/* 初始化LCD */

    /* 初始化ADC, 1024x过采样, 不移位 
     * 26位ADC分辨率最大值为:67108864, 实际上由于分辨率太高 ,低位值已经不准确
     * 一般我们可以设置 ovss=4, 缩小16倍, 即22位分辨率, 低位值会相对稳定一些.
     * 这里我们为了演示26位过采样ADC转换效果, 把分辨率调到最大, 26位,并且不移位. 
     */
    adc_oversample_init(1024 - 1, ADC_RIGHTBITSHIFT_NONE);

    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_CH19_VAL:", BLUE);
/* 先在固定位置显示小数点 */
    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH19_VOL:0.000V", BLUE); 

    while (1)
{ 

/* 获取通道5的转换值,10次取平均 /
adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
lcd_show_xnum(142, 110, adcx, 8, 16, 0, BLUE); /
显示ADCC采样后的原始值 /
/
获取计算后的带小数的实际电压值,比如3.1111 /
temp = (float)adcx * (3.3 / 67108864);
adcx = temp; /
赋值整数部分给adcx变量,因为adcx为整形 /
/
显示电压值的整数部分,3.1111的话,这里就是显示3 */
lcd_show_xnum(142, 130, adcx, 1, 16, 0, BLUE);

    temp -= adcx; /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
    temp *= 1000; /*小数部分乘以1000,如:0.1111就转换为111.1,相当于保留三位小数*/

/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
lcd_show_xnum(158, 130, temp, 3, 16, 0X80, BLUE);

    LED0_TOGGLE();
    delay_ms(100);
}

}
此部分代码,我们在TFTLCD模块上显示一些提示信息后,将每隔100ms读取一次ADC通道5的值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。
31.5.4 下载验证
下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图31.5.4.1所示:
在这里插入图片描述

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

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值