本帖最后由 正点原子运营 于 2026-9-2 10:53 编辑
第二十九章 ADC实验
1)实验平台:正点原子Mini STM32H562开发板
2)章节摘自【正点原子】STM32H562开发指南 V1.0
3)购买链接: https://detail.tmall.com/item.htm?id=753606659612
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h562_mini.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32开发板技术交流群:414052738
本章,我们将介绍STM32H562的ADC(Analog-to-digital converters,模数转换器)功能。我们通过四个实验来学习ADC,分别是单通道ADC采集实验、单通道ADC采集(DMA读取)实验、多通道ADC采集(DMA读取)实验和单通道ADC过采样(16位分辨率)实验。
本章分为如下几个小节:
29.1 ADC简介
29.2 单通道ADC采集实验
29.3 单通道ADC采集(DMA读取)实验
29.4 多通道ADC采集(DMA读取)实验
29.5 单通道ADC过采样(16位分辨率)实验
29.1 ADC简介
STM32H562xx系列有2个ADC,都可以独立工作,其中ADC1和ADC2还可以组成双重模式(提高采样率)。
STM32H562的ADC主要特性我们可以总结为以下几条:
1、可配置12位、10位、8位、6位分辨率,降低分辨率可以缩短转换时间,转换时间越短,可以做到的采样率就越高。
2、每个ADC支持多达20个的采集通道,其中18个外部通道和2个内部通道。18个外部通道中:有6路快速通道和12路慢速通道,慢速和快速的区别主要是支持的最高采样率不同,慢速通道要比快速通道低。这些通道的A/D转换可以单次、连续、扫描或间断模式执行。
3、ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。
4、ADC具有四条专用的内部通道,一路用于内部温度传感器(VSENSE)、一路用于内部参考电压、一路用于监控外部VBAT电源引脚、一道用于监控内部VDDCORE电源。
5、支持过采样,过采样比2 ~ 256x可调,可编程数据移位高达8位。
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)的框图:
图29.1.1 ADC框图
图中,我们标记了11处位置,分别如下:
①VREF+电压
VREF+ 是正模拟参考电压输入,选择范围是1.62V~3.6V(参考芯片数据手册),开发板上我们一般给VREF+接入的电压时3.3V,所以得到开发板上的ADC测量范围是0~3.3V。
②ADC的双时钟域架构
ADC有两种时钟源可以选择,分别是:
(1)adc_hclk(属于同步时钟),来自AHB总线的系统时钟,ADC1和ADC2处在250MHz的 AHB2总线上。可以通过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,得到的频率是62.5MHz,例程中我们就是使用该配置。这个频率不是随便设置的,需要查看芯片数据手册,是有限制的,在《STM32H562VIT6.pdf》第133页,根据电压级别不同,取值反正是50~125MHz。用户根据设置的主稳压器输出的电压级别进行选择即可,我们在sys_stm32_clock_init()函数中设置了电压级别为VOS = 0。设置ADC时钟频率不要超频了,因为超频后ADC测量误差会比较大。
(2)adc_ker_ck(属于异步时钟),可以通过RCC_CCIPR5寄存器的ADCDACSEL[2:0]位来选择不同的时钟源,前提是前面提到的CKMODE[1:0]=00。ADC异步时钟模式下可以选择以下的时钟源:
ADCDACSEL[2:0]=000,rcc_hclk作为ADC时钟源(复位后的默认值)
ADCDACSEL[2:0]=001,sys_ck作为ADC时钟源
ADCDACSEL[2:0]=010,pll2_r_ck作为ADC时钟源
ADCDACSEL[2:0]=011,hse_ck作为ADC时钟源
ADCDACSEL[2:0]=100,his_ker_ck作为ADC时钟源
ADCDACSEL[2:0]=101,csi_ker_ck作为ADC时钟源
ADCDACSEL[2:0]=其它,保留
③输入通道
ADC总共有20个输入通道。注意:STM32H5的ADC支持单端/差分转换,由寄存器ADC_DIFSEL控制,该寄存器默认是0(单端模式),配置为1(则为差分模式)。因为H562的ADC支持差分通道输入,因此有ADC_INP[19:0]和ADC_INN[19:0]两组通道。其中,INP是差分正向输入,INN是差分反向输入。ADC_INP[0:5]和 ADC_INN[0:5]是快速模拟输入。ADC_INP[6:19]和 ADC_INN[6:19]是慢速模拟输入。如果我们使用单端输入,则只有ADC_INP[19:0]有效,ADC_INN[19:0]在内部自动接VSSA。
ADC连接的4路内部模拟输入,如下表所示:
表29.1.1 ADC内部通道
ADC的内部通道包括:一路用于内部温度传感器(VSENSE)、一路用于内部参考电压、一路用于监控外部VBAT电源引脚、一道用于监控内部VDDCORE电源。
为了方便大家查询ADC通道和IO的对应关系,给大家整理了表29.1.2。大多数情况,我们都是使用单端模式。前面也说了单端模式下,ADCx_INN[19:0]在内部自动接VSSA,所以表29.1.2中的通道0~通道19指的是ADCx_INP[19:0]。
表29.1.2 ADC通道表
由于开发板上使用的主控芯片是STM32H562VIT6,它的封装是LQFP100。该封装是没有表29.1.2中标黄色的IO引脚,所以相关的通道是没有引出来的。
④转换序列
可以将转换分为两组:常规转换组和注入转换组。常规转换组最多允许16个通道进行转换。注入转换组最多允许4个通道进行转换。
如何理解常规转换组和注入转换组?常规转换组相当于你正常运行的程序,而注入转换组就相当于中断。在你程序正常执行的时候,中断是可以打断你的执行的,获得优先执行的权利。所以注入转换组可以打断常规转换组的转换,获得优先转换的权利,在注入转换组转换完成后,常规转换组才得以继续转换。
为了便于理解,请看常规转换组和注入转换组的转换优先级对比图,如图29.1.2所示:
图29.1.2 常规转换组和注入转换组的转换优先级对比图
常规转换组最多允许16个通道进行转换,注入转换组最多允许4个通道进行转换,那么转换的顺序怎么设置的?我们把这个转换顺序分别称为常规序列和注入序列。
(1)常规序列
常规序列在ADCx_SQRy寄存器中设置,每个ADC都有4个SQR寄存器,比如ADC1的SQR寄存器有ADC1_SQR1~ ADC1_SQR4。这四个寄存器怎么来设置常规序列的呢?下面通过表29.1.3给大家说明。
表29.1.3 常规序列寄存器控制关系汇总表
从上表可以知道,当我们想设置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控制关系如表29.1.4所示:
表29.1.4 注入序列寄存器控制关系汇总表
注入序列的长度写入JL [ 1 : 0 ]位,范围是0~3。注意:写入0表示这个注入序列有一个通道,而不是0个通道。
⑤触发源
ADC的触发转换有两种方法:分别是通过软件或外部事件(也就是硬件)触发转换。
我们先来看看通过软件触发转换的方法,常规通道由ADCx_CR寄存器的ADSTART位触发,注入通道由ADCx_CR寄存器的JADSTART位触发。方法是:通过对ADCx_CR寄存器的ADSTART(JADSTART)位写1开始转换,转换结束由硬件清零该位,这个控制ADC转换的方式非常简单。
另一种就是通过外部事件触发转换的方法,如定时器和输入引脚触发等等,具体请看《STM32H5xx参考手册_V1(英文版).pdf》的1017页到1019页的表242。外部事件触发转换可分为:常规通道的外部触发和注入通道的外部触发两种。
adc_ext_trg[31:0],对应的就是常规通道的外部触发,共有32路。
adc_jext_trg[31:0],对应的就是注入通道的外部触发,共有32路。
如果选择硬件触发,则需要选择相应的硬件触发事件和触发边沿等,然后由外部硬件事件来触发ADC的采集(外部事件触发配置ADSTART位为1)。
硬件触发事件由ADCx_CFGR寄存器的EXTSEL[4:0]和ADCx_JSQR寄存器的 JEXTSEL[4:0]位来选择,分别是常规转换组和注入转换组的触发源选择。而触发边沿是通过ADCx_CFGR寄存器的EXTEN[1:0]和ADCx_JSQR寄存器的JEXTEN[1:0]位来选择。
⑥转换时间
STM32H5的ADC总转换时间的计算公式如下:
TCONV = 采样时间(TSMPL) + 逐次逼近时间(TSAR) 采样时间(TSMPL)可通过ADCx_SMPR1和ADCx_SMPR2寄存器中的SMP[2:0]位编程,ADC_SMPR1控制的是通道0~9,ADC_SMPR2控制的是通道10~19。所有通道都可以通过编程来控制使用不同的采样时间,可选采样时间值如下:
SMP = 000:2.5 个 ADC 时钟周期
SMP = 001:6.5 个 ADC 时钟周期
SMP = 010:12.5 个 ADC 时钟周期
SMP = 011:24.5 个 ADC 时钟周期
SMP = 100:47.5 个 ADC 时钟周期
SMP = 101:92.5 个 ADC 时钟周期
SMP = 110:247.5 个 ADC 时钟周期
SMP = 111:640.5 个 ADC 时钟周期
逐次逼近时间(TSAR)是由分辨率决定的,分辨率通过对ADCx_CFGR寄存器的RES[1:0]位进行编程,可将分辨率配置为12位、10位、8位、6位。而逐次逼近时间和分辨率的对应关系如下表所示:
表29.1.5 TSAR与分辨率的对应关系
举个例子,我们配置SMP = 111,即设置最大采样周期,然后采用12位分辨率,那么得到:
TCONV = 640.5个ADC时钟周期 + 12.5个ADC时钟周期 = 653个ADC时钟周期 表格中,FADC的频率是30MHz,我们的例程中ADC的时钟源是250MHz的HCLK经过4分频得到,即62.5MHz。我们就以FADC的频率为62.5MHz来举例,可得到:
⑦参考电压
选择参考电压,我们可以设置参考电压来自外部的Vref+,也可以设置参考电压来自内部的稳压器。
⑧ADC的核心
ADC的核心是一个12位的逐次逼近型ADC转换器,它根据我们设置好的参考电压、输入通道、启动条件等,执行模数转换。
⑨数据寄存器
常规转换组的转换结果会存放到ADCx_DR寄存器的RDATA[15:0]位中,注入转换组的转换结果会存放到ADCx_JDRy寄存器的JDATA[16:0]位中。如果是使用双重模式,常规通道的数据则是存放在ADC_CDR寄存器。转换结果CPU可以通过AHB总线读取,同时也可以产生相关中断(adc_it)。
常规数据寄存器(ADC_DR)
常规数据寄存器ADC_DR是一个16位的寄存器。因为ADC的最大分辨率是12位,如果使用过采样,则分辨率可达16位,所以允许数据对齐方式。由ADC_CFGR2寄存器的OVSS[3:0]位设置数据对齐方式。
细心的朋友可能发现,常规转换组最多有16个输入通道,而ADC常则数据寄存器只有一个,如果一个常则转换组用到好几个通道,数据怎么读取?如果使用多通道转换,那么这些通道的数据也会存放在DR里面,按照常规转换组的顺序,上一个通道转换的数据,会被下一个通道转换的数据覆盖掉,所以当通道转换完成后要及时把数据取走。比较常用的方法是使用DMA模式。当常规转换组的通道转换结束时,就会产生DMA请求,这样就可以及时把转换的数据搬运到用户指定的目的地址存放。如果没有使用DMA传输,可以通过判断ADC_ISR寄存器相关位来得到当前ADC的转换状态,从而进行控制。
注入数据寄存器(ADCx_JDRy)(x=1~2)(y=1~4)
每个ADC注入数据寄存器有4个,注入转换组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。ADC_JDRx寄存器是16位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC_CFGR2寄存器的OVSS[3:0]位设置数据对齐方式。
通用常则数据寄存器ADC_CDR
常规数据寄存器ADC_DR仅适用于独立模式下,常规转换组的转换数据存储,而通用常规数据寄存器ADC_CDR适用于双重模式下,常规转换组的转换数据存储。在双重模式下,一般会配合DMA来传输数据。
⑩中断
对于每个ADC都可在下列情况下产生中断:
表29.1.6 每个ADC的ADC中断
在表29.1.5中,前面5个中断都很好理解,我们从模拟看门狗中断介绍。
模拟看门狗中断发生条件:首先通过ADCx_LTR和ADCx_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。
采样阶段结束:如果位EOSMP被硬件置1,则说明采样阶段结束(仅限常规转换),然后可以通过对EOSMP位写1来清零该位。如果EOSMPIE位置1,可以产生采样阶段结束中断。
上溢:如果常规转换后的数据未在新转换数据可用之前(由CPU或DMA)读取,会由溢出标志(OVR)指示缓冲区溢出事件。如果OVRIE位置1,可以产生一个溢出中断。
此外,我们还要知道常规组和注入组的转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据存储在内存里面,防止读取不及时数据被覆盖。
⑪单次转换模式和连续转换模式
单次转换模式和连续转换模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
(1)单次转换模式
通过将ADC_CFGR寄存器的CONT位置0选择单次转换模式。该模式下,ADC只执行一次转换。单次转换由ADC_CR寄存器的ADSTART位(只适用于常规转换组)或者JADSTART位(只适用于注入转换组)启动,也可以通过外部事件触发启动(适用于常规转换组或注入转换组),并且触发外部事件之前,ADSTART位或JADSTART位必须置1。
在常规序列中,每次转换完成后:转换数据存储在16位ADCx_DR寄存器中、EOC(常规转换结束)标志置 1、EOCIE位置1时将产生中断。
在注入序列中,每次转换完成后:转换数据存储在四个16位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来启动此模式。
在常规序列中,每次转换完成后:转换数据存储在16位ADCx_DR寄存器中、EOC(转换结束)标志置1、EOCIE 位置1时将产生中断。
转换序列完成后:EOS(序列结束)标志置 1、EOSIE位置1时将产生中断。
随后,会立即重启新序列,ADC会继续重复执行转换序列。
注意:注入通道不能连续转换,唯一例外的是,在连续转换模式下(使用JAUTO位)注入通道配置为在常规通道后的自动转换。
到这里,我们基本上介绍了ADC的大多数基础的知识点,其它知识后面用到会继续补充,如果还有不懂的内容,请参考《STM32H5xx参考手册_V1(英文版).pdf》的第26章。
29.2 单通道ADC采集实验
本实验我们来学习单通道ADC采集实验。本实验使用常规转换组单通道的单次转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。下面先带大家来了解本实验要配置的寄存器。
29.2.1 ADC寄存器
这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
ADCx通用控制寄存器(ADCx_CCR)
ADCx通用控制寄存器描述如图29.2.1.1所示:
图29.2.1.1 ADCx_CCR寄存器
CKMODE [1:0]位用于设置ADC时钟模式。本实验中,设置CKMODE[1:0] = 11,即adc_hclk/4 (同步时钟模式)。因此:
adc_ker_ck= adc_hclk/4=62.5MHz。 ADCx控制寄存器(ADCx_CR)
ADCx控制寄存器描述如图29.2.1.2所示:
图29.2.1.2 ADCx_CR寄存器
该寄存器我们用到多个位,这里就不全部列出来讲解了,而是抽出几个重要的位进行针对性的介绍,详细的介绍请参考《STM32H5xx参考手册_V1(英文版).pdf》第26.6.3节,1098页。
ADEN位用于使能ADC转换器。需要设置该位为1,ADC才可以正常工作。
ADSTART位用于启动ADC常规通道的转换序列。当使用硬件触发时(EXTEN[1:0]!=0),设置该位为1,必须在相应的硬件触发事件产生时,才会启动ADC转换。而当不使用硬件触发时(EXTEN[1:0]=0),设置该位为1则可以立即启动ADC转换。
ADCAL位用与控制/读取ADC校准状态。设置该位为1时,可以启动ADC校准,等校准完成以后,硬件会自动清零该位。因此在设置改位为1以后,通过判断该位是否变为0,即可判断校准是否完成。
ADCx配置寄存器(ADCx_CFGR)
ADCx配置寄存器描述如图29.2.1.3所示:
图29.2.1.3 ADCx_CFGR寄存器
RES[1:0]位用于设置ADC转换的分辨率:0表示12位;1表示10位;2表示8位;3表示6位。本实验使用12位分辨率,因此设置RES[1:0]=0即可。
EXTEN[1:0]位用于设置常规通道的外部触发方式和极性。本实验使用软件触发,因此设置EXTEN[1:0]=00即可,即禁止外部触发。
OVRMOD位用于设置是否使能覆写功能。当设置该位为0时,如果上一次转换的数据未及时读取,新的转换结果将被丢弃;当设置该位为1时,如果上一次转换的数据未及时读取,将会被新的结果覆盖。本实验该位设置为1。
CONT位用于设置转换模式。当CONT=0时,表示单次转换模式;当CONT=1时,表示连续转换模式。本实验该位设置为0。
ADCx常规序列寄存器1(ADCx_SQR1)
ADCx常规序列寄存器1描述如图29.2.1.4所示:
图29.2.1.4 ADCx_SQR1寄存器
L[3:0]位用于存储常规序列的长度,取值范围:0~15,表示常规序列长度为:1~16。本实验只用到1个通道,L[3:0]=0即可。
SQ1[4:0]~SQ4[4:0]同于设置常规序列中第1~4个转换通道,第5~16个转换通道的设置请查看ADCx_SQR2和ADCx_SQR4寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的常规序列寄存器控制关系汇总表。
ADCx采样时间寄存器2(ADCx_SMPR2)
ADCx采样时间寄存器2描述如图29.2.1.5所示:
图29.2.1.5 ADCx_SMPR2寄存器
该寄存器用于设置ADC通道10~19的采样时间,而ADCx_SMPR1设置ADC通道0~9的采样时间。STM32H5的ADC总转换时间的计算方法前面已经介绍过了。建议采样时间尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率,所以大家在实际应用中自行结合自身情况设置。
ADCx常规数据寄存器(ADCx_ DR)
ADCx常规数据寄存器描述如图29.2.1.6所示:
图29.2.1.6 ADCx_ DR寄存器
常规序列中的AD转化结果都将被存在这个寄存器里面,我们读取该寄存器,即可得到ADC转换后的结果。如果是注入通道的转换结果,则被保存在ADCx_JDRy(y=1~4)里面。
ADCx中断和状态寄存器(ADCx_ ISR)
ADCx中断和状态寄存器描述如图29.2.1.7所示:
图29.2.1.7 ADCx_ ISR寄存器
该寄存器保存了ADC转换时的各种状态。本实验我们通过EOC位的状态来判断ADC转换是否完成,如果查询到EOC位被硬件置1,就可以从ADC_DR寄存器中读取转换结果,否则需要等待转换完成。
至此,本实验要用到的ADC相关寄存器介绍完毕了,对于未介绍的部分,请大家参考《STM32H5xx参考手册_V1(英文版).pdf》第26章相关内容。
29.2.2 硬件设计
1. 例程功能
1、采集ADC1通道19(PA5)上面的电压,并在LCD模块上面显示ADC转换值以及换算成电压后的电压值。
2、使用短路帽将P5的ADC和RV1连接,然后调节电位器的旋钮,可以看到TFTLCD显示的电压值产生变化。
3、LED0闪烁指示系统正在运行。
2. 硬件资源
1)LED灯
LED0 - PB4
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1通道19 - PA5
3. 原理图
ADC属于STM32H562内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道19(PA5)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过短路帽将PA5与电位器连接,如下图所示:
图29.2.2.1 PA5与电位器示意图
使用短路帽将P5的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其它地方的电压值,我们只需要1跟杜邦线,一端接到P5的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
29.2.3 程序设计
29.2.3.1 ADC的HAL库驱动
ADC在HAL库中的驱动代码在stm32h5xx_hal_adc.c和stm32h5xx_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 DataAlign; /* 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 SamplingMode; /* ADC组常规转换的采样模式 */
- FunctionalState DMAContinuousRequests; /* DMA请求是单次模式或连续模式执行 */
- uint32_t Overrun; /* 发生溢出时,进行的操作 */
- 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) DataAlign:配置ADC数据对齐方式,左对齐或者右对齐。
4) ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。
5) EOCSelection:可选参数为ADC_EOC_SINGLE_CONV和ADC_EOC_SEQ_CONV,指定转换结束时是否产生EOS中断或事件标志。
6) LowPowerAutoWait:配置是否使用低功耗自动延迟等待模式,可选参数为 ENABLE和 DISABLE,当使能时,仅当一组内所有之前的数据已处理完毕时,才开始新的转换,适用于低频应用。该模式仅用于ADC的轮询模式,不可用于DMA以及中断。
7) ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
8) NbrOfConversion:设置常规转换通道数目,范围是:1~16。
9) DiscontinuousConvMode:配置是否使用不连续的采样模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道 1 与通道 2,下次触发就是转换通道 5 与通道 7,这样不连续的转换,依次类推。此参数只有将 ScanConvMode 使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。
10) NbrOfDiscConversion:不连续采样通道数。
11) ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
12) ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。
13) SamplingMode:指定ADC组常规转换的采样模式。
14) DMAContinuousRequests:指定DMA请求是否以单次模式(DMA)执行(当达到转换数量时,传输停止)或连续模式(DMA传输无限制,无论转换次数多少)。
15) Overrun:当有新的数据溢出时,可以选择覆盖写入或者是丢弃新的数据。
16) OversamplingMode:是否使用过采样模式。
17) Oversampling:配置过采样模式的参数。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_ADCEx_Calibration_Start函数
ADC的自校准函数,其声明如下:
- HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc,
- uint32_t SingleDiff);
复制代码 函数描述:
首先调用HAL_ADC_Init函数配置了相关的功能后,再调用此函数进行ADC自校准功能。
函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是单端或差分模式选择,有以下两种:
1)ADC_SINGLE_ENDED表示单端输入模式。
2)ADC_DIFFERENTIAL_ENDED表示差分输入模式。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_ADC_ConfigChannel函数
ADC通道配置函数,其声明如下:
- HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc,
- const 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; /* 偏移量 */
- uint32_t OffsetSign; /* 定义偏移量符号,是要减去还是要加上 */
- 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) OffsetSign:定义偏移量符号,是要减去还是要加上。
8) OffsetSignedSaturation:是否使能ADC采样值的最高位为符号位。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
4. HAL_ADC_Start函数
ADC转换启动函数,其声明如下:
- HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
复制代码 函数描述:
当配置好ADC的基础的功能后,就调用此函数启动ADC。
函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. 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枚举类型的值。
6. 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_ADC_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值。
29.2.3.2 程序流程图
图29.2.3.2.1 单通道ADC采集实验程序流程图
29.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_ADC_CLK_ENABLE(); }while(0) /* ADC1 & ADC2 时钟使能 */
- /*****************************************************************************/
复制代码 ADC的通道与引脚的对应关系在 《STM32H562VIT6.pdf》 数据手册可以查到,我们这里使用ADC1的通道19,在数据手册中的表格为:
表29.2.3.3.1 ADC1通道5对应引脚查看表
下面直接开始介绍adc.c的程序,首先是ADC初始化函数。
- /**
- * @brief ADC初始化函数
- * [url=home.php?mod=space&uid=60778]@note[/url] 本函数支持ADC1/ADC2任意通道
- * 我们使用12位精度, ADC采样时钟=62.5MHz, 转换时间为:采样周期 + 12.5个ADC周期
- * 设置最大采样周期: 640.5, 则转换时间 = 653个ADC周期 = 10.45us
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void adc_init(void)
- {
- g_adc_handle.Instance = ADC_ADCX; /* 选择哪个ADC */
- /* 输入时钟4分频,即adc_ker_ck=HCLK/4=62.5Mhz */
- g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
- g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
- g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐 */
- g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;/* 非扫描模式,用到一个通道 */
- g_adc_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 转换通道结束标志 */
- g_adc_handle.Init.LowPowerAutoWait = DISABLE; /* 自动低功耗关闭 */
- g_adc_handle.Init.ContinuousConvMode = DISABLE; /* 关闭连续转换模式 */
- /* 赋值范围是1~16,本实验用到1个常规通道 */
- g_adc_handle.Init.NbrOfConversion = 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;
- /* 选择用于ADC组常规转换的采样模式 */
- g_adc_handle.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
- g_adc_handle.Init.DMAContinuousRequests = DISABLE; /* 不使用DMA */
- /* 有新的数据后直接覆盖掉旧数据 */
- g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
- g_adc_handle.Init.OversamplingMode = DISABLE; /* 关闭过采样 */
- HAL_ADC_Init(&g_adc_handle); /* 初始化 */
- HAL_ADCEx_Calibration_Start(&g_adc_handle, 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)
- {
- GPIO_InitTypeDef gpio_init_struct = {0};
- RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
- if(hadc->Instance == ADC_ADCX)
- {
- ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADC1/2时钟 */
- ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启ADC通道IO引脚时钟 */
- /* 设置ADC时钟 */
- /* 要配置ADC时钟源 */
- adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADCDAC;
- /* ADC时钟时钟源为HCLK */
- adc_clk_init.AdcDacClockSelection = RCC_ADCDACCLKSOURCE_HCLK;
- HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 配置ADC时钟源 */
- gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引脚 */
- gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */
- gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
- HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);/* GPIO初始化 */
- }
- }
复制代码 可以看到在HAL_ADC_MspInit函数中,我们除了使能ADC和通道对应IO时钟、初始化IO外,还配置了ADC的时钟源来自于HCLK。
接下来要介绍的是adc_channel_set和adc_get_result函数,其定义如下:
- /**
- * @brief 设置ADC通道采样时间
- * @param adcx : adc句柄指针,ADC_HandleTypeDef
- * @param ch : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_19
- * @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
- * @param stime: 采样时间, 对应关系为:
- * @arg ADC_SAMPLETIME_2CYCLES_5, 2.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_6CYCLES_5, 6.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_12CYCLES_5, 12.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_24CYCLES_5, 24.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_47CYCLES_5, 47.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_92CYCLES_5, 92.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_247CYCLES_5, 247.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_640CYCLES_5, 640.5个ADC时钟周期
- * @arg ADC_SAMPLETIME_3CYCLES_5, 3.5个ADC时钟周期
- * @retval 无
- */
- void adc_channel_set(ADC_HandleTypeDef *adc_handle,
- uint32_t ch, uint32_t rank, uint32_t stime)
- {
- ADC_ChannelConfTypeDef adc_ch_conf = {0};
-
- adc_ch_conf.Channel = ch; /* 通道 */
- adc_ch_conf.Rank = rank; /* 序列 */
- adc_ch_conf.SamplingTime = stime; /* 采样时间 */
- adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED; /* 单边采集 */
- adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE; /* 不使用偏移量的通道 */
- adc_ch_conf.Offset = 0; /* 偏移量为0 */
- HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf); /* 通道配置 */
- }
- /**
- * @brief 获得ADC转换后的结果
- * @param ch: 通道值 0~19,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_19
- * @retval 无
- */
- uint32_t adc_get_result(uint32_t ch)
- {
- adc_channel_set(&g_adc_handle, ch, ADC_REGULAR_RANK_1,
- ADC_SAMPLETIME_640CYCLES_5); /* 设置通道,序列和采样时间 */
- HAL_ADC_Start(&g_adc_handle); /* 开启ADC */
- HAL_ADC_PollForConversion(&g_adc_handle, 10); /* 轮询转换 */
- /* 返回最近一次ADC规则组的转换结果 */
- return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
- }
复制代码 adc_channel_set函数用于设置ADC通道采样时间,通过调用HAL_ADC_ConfigChannel函数选择ADC通道、设置转换序列号和采样时间等。
adc_get_result函数用于获得ADC转换后的结果,通过调用adc_channel_set函数设置通道,然后调用HAL_ADC_Start和HAL_ADC_PollForConversion函数,启动ADC转换和等待转换完成,最后调用HAL_ADC_GetValue函数获取转换结果。
下面介绍的是获取ADC某通道的转换多次后的平均值函数,函数定义如下:
- /**
- * @brief 获取通道ch的转换值,取times次,然后平均
- * @param ch : 通道号, 0~19,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_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;
- HAL_Init(); /* HAL库初始化 */
- HAL_ICACHE_Enable(); /* 使能指令缓存 */
- sys_stm32_clock_init(250, 4, 2, 2); /* 系统时钟初始化, 250Mhz */
- delay_init(250); /* 延时初始化 */
- usart_init(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_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 / 4096);
- 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值的显示简单介绍一下:首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。
29.2.4 下载验证
下载代码后,可以看到LCD显示如图29.2.4.1所示:
图29.2.4.1单通道ADC采集实验测试图
上图中,我们使用短路帽将P5的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
29.3 单通道ADC采集(DMA读取)实验
本实验我们来学习单通道ADC采集(DMA读取)实验。本实验使用常规转换组单通道的连续转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
29.3.1 ADC寄存器
本实验我们很多的设置和单通道ADC采集实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集实验不同设置的ADCx_CFGR寄存器进行介绍,其它的配置基本一样的。
ADCx配置寄存器(ADCx_CFGR)
ADCx配置寄存器描述如图29.3.1.1所示:
图29.3.1.1 ADCx_CFGR寄存器
CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用连续转换模式,所以CONT位置1即可,其它请参考上一个实验的配置。
29.3.2 硬件设计
1. 例程功能
1、采集ADC1通道19(PA5)上面的电压(DMA读取),并在LCD模块上面显示ADC转换值以及换算成电压后的电压值。
2、使用短路帽将P5的ADC和RV1连接,然后调节电位器的旋钮,可以看到TFTLCD显示的电压值产生变化。
3、LED0闪烁指示系统正在运行。
2. 硬件资源
1)LED灯
LED0 - PB4
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1通道19 - PA5
5)DMA(DMA1通道1)
3. 原理图
ADC和DMA属于STM32H562内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道19(PA5)来采集外部电压值,并通过DMA来读取,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过断路帽将PA5与电位器连接,如下图所示:
图29.2.2.1 PA5与电位器示意图
使用短路帽将P5的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其他地方的电压值,我们只需要1跟杜邦线,一端接到P5的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
29.3.3 程序设计
29.3.3.1 ADC & DMA的HAL库驱动
单通道ADC采集实验已经介绍本实验要用到的ADC的HAL库API函数,这里要介绍的是DMA链表相关的API函数。
1. HAL_DMAEx_List_BuildNode函数
构建DMA通道节点函数,其声明如下:
- HAL_StatusTypeDef HAL_DMAEx_List_BuildNode(DMA_NodeConfTypeDef const *const pNodeConfig, DMA_NodeTypeDef *const pNode)
复制代码 函数描述:
根据DMA_NodeConfTypeDef中指定的参数构建DMA通道节点。
函数形参:
形参1是DMA_NodeConfTypeDef结构体类型指针变量。
形参2是DMA_NodeTypeDef结构体类型指针变量。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_DMAEx_List_InsertNode函数
插入新节点函数,其声明如下:
- HAL_StatusTypeDef HAL_DMAEx_List_InsertNode(DMA_QListTypeDef *const pQList,
- DMA_NodeTypeDef *const pPrevNode,
- DMA_NodeTypeDef *const pNewNode)
复制代码 函数描述:
根据选择前一个节点,在链表队列的任意队列位置插入新节点。
函数形参:
形参1是DMA_QListTypeDef结构体类型指针变量,包含队列信息。
形参2是DMA_NodeTypeDef结构体类型指针变量,包含链表先前的节点寄存器配置。
形参3是DMA_NodeTypeDef结构体类型指针变量,包含链表新节点寄存器配置。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_DMAEx_List_SetCircularMode函数
配置循环模式函数,其声明如下:
- HAL_StatusTypeDef HAL_DMAEx_List_SetCircularModeConfig(DMA_QListTypeDef *const pQList, DMA_NodeTypeDef *const pFirstCircularNode)
复制代码 函数描述:
给链表队列设置为循环模式。
函数形参:
形参1是DMA_QListTypeDef结构体类型指针变量,包含队列信息。
形参2是DMA_NodeTypeDef结构体类型指针变量,包含链表第一个循环节点注册配置。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
4. HAL_DMAEx_List_Init函数
以链表方式初始化DMA通道函数,其声明如下:
- HAL_StatusTypeDef HAL_DMAEx_List_Init(DMA_HandleTypeDef *const hdma)
复制代码 函数描述:
以链表方式初始化DMA通道并创建关联句柄。
函数形参:
形参1是DMA_HandleTypeDef结构体类型指针变量。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. HAL_DMAEx_List_LinkQ函数
链接到DMA通道的链表队列函数,其声明如下:
- HAL_StatusTypeDef HAL_DMAEx_List_LinkQ(DMA_HandleTypeDef *const hdma,
- DMA_QListTypeDef *const pQList)
复制代码 函数描述:
链接到DMA通道的链表队列。
函数形参:
形参1是DMA_HandleTypeDef结构体类型指针变量。
形参2是DMA_QListTypeDef结构体类型指针变量,包含队列信息。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
单通道ADC采集(DMA读取)配置步骤
1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道19,对应IO是PA5,它们的时钟开启方法如下:
- __HAL_RCC_ADC_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
通过调用29.3.3.1小节介绍的API函数初始化并使用DMA,具体请参考源码。
5)使能DMA通道,配置DMA中断优先级并开启中断,启动ADC和DMA
通过HAL_ADC_Start_DMA函数启动ADC转换并开启DMA中断,通过DMA传输ADC数据。
通过HAL_NVIC_EnableIRQ函数使能DMA数据流中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
DMA中断对于每个通道都有一个中断服务函数,比如DMA1通道1的中断服务函数为GPDMA1_Channel1_IRQHandler。一般,我们会在中断服务函数里面调用HAL库的DMA中断公共处理函数HAL_DMA_IRQHandler()来处理DMA相关的中断。然后再调用相关的中断回调函数来处理用户的逻辑代码,比如本实验会调用HAL_ADC_ConvCpltCallback回调函数来处理ADC转换完成相关的逻辑代码。
29.3.3.2 程序流程图
图29.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图
29.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。
adc.h头文件和单通道ADC采集实验的内容一样,这里就不列出来了。
下面给大家介绍adc.c文件里面的函数,首先是adc_dma_init和HAL_ADC_MspInit函数,定义如下:
- /**
- * @brief ADC DMA读取 初始化函数
- * @param 无
- * @retval 无
- */
- void adc_dma_init(void)
- {
- ADC_ChannelConfTypeDef adc_ch_conf = {0};
- /* 初始化ADC */
- g_adc_dma_handle.Instance = ADC_ADCX; /* 选择哪个ADC */
- /* 输入时钟4分频,即adc_ker_ck=HCLK/4=62.5Mhz */
- g_adc_dma_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
- g_adc_dma_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
- g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐 */
- g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 非扫描模式 */
- g_adc_dma_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;/* 转换通道结束标志 */
- g_adc_dma_handle.Init.LowPowerAutoWait = DISABLE; /* 自动低功耗关闭 */
- g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能连续转换模式 */
- /* 赋值范围是1~16,本实验用到1个常规通道 */
- g_adc_dma_handle.Init.NbrOfConversion = 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;
- /* 选择用于ADC组常规转换的采样模式 */
- g_adc_dma_handle.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
- g_adc_dma_handle.Init.DMAContinuousRequests = ENABLE; /* 使用DMA */
- /* 有新的数据后直接覆盖掉旧数据 */
- g_adc_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
- g_adc_dma_handle.Init.OversamplingMode = DISABLE; /* 关闭过采样 */
- HAL_ADC_Init(&g_adc_dma_handle); /* 初始化 */
- HAL_ADCEx_Calibration_Start(&g_adc_dma_handle, ADC_SINGLE_ENDED);/*ADC校准*/
- /* 配置ADC通道 */
- adc_ch_conf.Channel = ADC_ADCX_CHY; /* 选择ADC通道 */
- adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 规则组序列号为1 */
- adc_ch_conf.SamplingTime = ADC_SAMPLETIME_640CYCLES_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_dma_handle, &adc_ch_conf);/* 通道配置 */
- }
- /**
- * @brief ADC底层驱动,引脚配置,时钟使能
- 此函数会被HAL_ADC_Init()调用
- * @param hadc:ADC句柄
- * @retval 无
- */
- void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
- {
- RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; /* 定义外设时钟配置结构体变量 */
- GPIO_InitTypeDef gpio_init_struct = {0}; /* 定义GPIO初始化结构体变量 */
- DMA_NodeConfTypeDef node_conf = {0}; /* 定义GPDMA的链表节点配置结构体变量 */
- DMA_NodeTypeDef gpdma1_ch1_node = {0};/* 定义GPDMA1通道1的链表节点结构体变量 */
- DMA_QListTypeDef gpdma1_ch1_qlist = {0};/* 定义GPDMA1通道1链表队列结构体变量 */
-
- if(hadc->Instance == ADC_ADCX)
- {
- /* 打开IO、ADC1和GPDMA相关时钟 */
- ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADC1/2时钟 */
- ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启ADC通道IO引脚时钟 */
- __HAL_RCC_GPDMA1_CLK_ENABLE(); /* GPDMA1时钟使能 */
-
- /* 设置ADC时钟 */
- adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;/*要配置ADC时钟源*/
- /* ADC时钟时钟源为HCLK */
- adc_clk_init.AdcDacClockSelection = RCC_ADCDACCLKSOURCE_HCLK;
- HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 配置ADC时钟源 */
- /* 初始化ADC采集通道对应的IO引脚 */
- gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引脚 */
- gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟 */
- gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
- HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);/* GPIO初始化 */
- /* ADC1 DMA初始化 */
- node_conf.NodeType = DMA_GPDMA_LINEAR_NODE; /* GPDMA线性寻址节点类型 */
- node_conf.Init.Request = GPDMA1_REQUEST_ADC1; /* DMA通道请求 */
- /* 单/突发级别的硬件请求协议 */
- node_conf.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
- /* DMA通道的传输方向:外设到内存 */
- node_conf.Init.Direction = DMA_PERIPH_TO_MEMORY;
- node_conf.Init.SrcInc = DMA_SINC_FIXED; /* 源增量模式:固定模式 单个/突发 */
- /* 目标增量模式:增量模式 单个/突发 */
- node_conf.Init.DestInc = DMA_DINC_INCREMENTED;
- /* 源数据宽度:16位 */
- node_conf.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD;
- /* 目标数据宽度:16位 */
- node_conf.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD;
- node_conf.Init.SrcBurstLength = 1; /* 指定DMA的源突发长度,范围: 1 到 64 */
- node_conf.Init.DestBurstLength = 1; /* 指定的目标突发长度,范围: 1 到 64 */
- node_conf.Init.TransferAllocatedPort =
- DMA_SRC_ALLOCATED_PORT1|DMA_DEST_ALLOCATED_PORT1; /* 指定传输分配的端口 */
- /* 指定DMA通道的传输事件模式 */
- node_conf.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
- node_conf.Init.Mode = DMA_NORMAL; /* DMA模式:正常模传输 */
- /* 设置DMA通道触发极性 */
- node_conf.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
- /* DMA通道数据交换模式:无数据交换 */
- node_conf.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
- /* 如果源数据宽度<目标数据宽度=>右对齐,填充0至目标数据宽度 */
- node_conf.DataHandlingConfig.DataAlignment =
- DMA_DATA_RIGHTALIGN_ZEROPADDED;
- /* 构建DMA通道节点 */
- HAL_DMAEx_List_BuildNode(&node_conf, &gpdma1_ch1_node);
- /* 插入新节点 */
- HAL_DMAEx_List_InsertNode(&gpdma1_ch1_qlist, NULL, &gpdma1_ch1_node);
- /* 设置链表队列的循环模式 */
- HAL_DMAEx_List_SetCircularMode(&gpdma1_ch1_qlist);
- /* 以链表方式初始化DMA通道 */
- /* 选择GPDMA1_Channel1 */
- g_gpdma1_ch1_adc_handle.Instance = GPDMA1_Channel1;
- /* 通道优先级 */
- g_gpdma1_ch1_adc_handle.InitLinkedList.Priority = DMA_HIGH_PRIORITY;
- g_gpdma1_ch1_adc_handle.InitLinkedList.LinkStepMode =
- DMA_LSM_FULL_EXECUTION; /* 执行所有链表的通道 */
- g_gpdma1_ch1_adc_handle.InitLinkedList.LinkAllocatedPort =
- DMA_LINK_ALLOCATED_PORT1; /* 链路分配端口1 */
- g_gpdma1_ch1_adc_handle.InitLinkedList.TransferEventMode =
- DMA_TCEM_BLOCK_TRANSFER; /* 指定DMA通道的传输事件模式 */
- g_gpdma1_ch1_adc_handle.InitLinkedList.LinkedListMode =
- DMA_LINKEDLIST_CIRCULAR; /* 循环链表DMA通道传输 */
- /* 以链表方式初始化DMA通道 */
- HAL_DMAEx_List_Init(&g_gpdma1_ch1_adc_handle);
- /* 链接到DMA通道的链表队列 */
- HAL_DMAEx_List_LinkQ(&g_gpdma1_ch1_adc_handle, &gpdma1_ch1_qlist);
- /* 将DMA句柄与ADC句柄关联起来 */
- __HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_gpdma1_ch1_adc_handle);
- HAL_DMA_ConfigChannelAttributes(&g_gpdma1_ch1_adc_handle,
- DMA_CHANNEL_NPRIV); /* 通道没有特权 */
- HAL_NVIC_SetPriority(GPDMA1_Channel1_IRQn, 2, 0);
- HAL_NVIC_EnableIRQ(GPDMA1_Channel1_IRQn);
- }
- }
复制代码 adc_dma_init函数主要初始化ADC和其通道的工作参数。
HAL_ADC_MspInit函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟源配置、DMA链表初始化等代码。
下面介绍的是GPDMA1通道1的中断服务函数和ADC转换完成回调函数,定义如下:
- /**
- * @brief ADC转换完成回调函数
- * @param hadc:ADC句柄
- * @retval None
- */
- void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
- {
- g_adc_dma_sta = 1;
- }
- /**
- * @brief GPDMA1通道1中断服务函数
- * @param 无
- * @retval 无
- */
- void GPDMA1_Channel1_IRQHandler(void)
- {
- HAL_DMA_IRQHandler(&g_gpdma1_ch1_adc_handle);
- }
复制代码 在GPDMA1通道1中断服务函数中调用HAL库公共处理函数胡HAL_DMA_IRQHandler,然后在HAL_ADC_ConvCpltCallback回调函数中,将g_adc_dma_sta变量置1,方便main文件中处理ADC转换完成相关代码。
最后在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库初始化 */
- HAL_ICACHE_Enable(); /* 使能指令缓存 */
- sys_stm32_clock_init(250, 4, 2, 2); /* 系统时钟初始化, 250Mhz */
- delay_init(250); /* 延时初始化 */
- usart_init(115200); /* 串口初始化 */
- led_init(); /* LED初始化 */
- lcd_init(); /* LCD初始化 */
- adc_dma_init(); /* 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_CH19_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH19_VOL:0.000V", BLUE);
- HAL_ADC_Start_DMA(&g_adc_dma_handle, (uint32_t *)g_adc_dma_buf,
- ADC_DMA_BUF_SIZE);
- 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(142, 110, adcx, 5, 16,0, BLUE);/*显示ADCC采样后的原始值*/
-
- /* 获取计算后的带小数的实际电压值,比如3.1111 */
- temp = (float)adcx * (3.3 / 4096);
- 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采集完成状态标志 */
- HAL_ADC_Start_DMA(&g_adc_dma_handle, (uint32_t *)g_adc_dma_buf,
- ADC_DMA_BUF_SIZE);
- }
- LED0_TOGGLE();
- delay_ms(100);
- }
- }
复制代码 此部分代码,和单通道ADC采集实验十分相似,只是这里使能了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,这里我们对数组的数据取平均值,减少误差。在LCD屏显示结果的处理和单通道ADC采集实验一样。首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。
29.3.4 下载验证
下载代码后,可以看到LCD显示如图29.3.4.1所示:
图29.3.4.1 单通道ADC采集(DMA读取)实验测试图
这里的实验效果和单通道ADC采集实验是一样的,我们使用短路帽将P5的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
29.4 多通道ADC采集(DMA读取)实验
本实验我们来学习多通道ADC采集(DMA读取)实验。本实验使用常规转换组多通道的连续转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
29.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描述如图29.4.1.1所示:
图29.4.1.1 ADCx_SQR1寄存器
L[3:0]位用于设置常规序列的长度,取值范围:0~15,表示常规序列长度为1~16。本实验使用到6个通道,所以设置这几个位的值为5即可。
SQ1[4:0]~SQ4[4:0]位设置常规组序列的第1~4个转换编号,第5~16个转换编号的设置请查看ADCx_SQR2和ADCx_SQR4寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的常规序列寄存器控制关系汇总表。
下面我们来看看本实验是怎么设置的:SQ1[4:0]位赋值为0、SQ2[4:0]位赋值为1、SQ3[4:0]位赋值为14、SQ4[4:0]位赋值为15、SQ5[4:0]位赋值为18、SQ6[4:0]位赋值为19,即常规序列1到6分别对应的通道是0、1、14、15、18、19。其中SQ5[4:0]位和SQ6[4:0]位是在ADCx_SQR2寄存器中配置。
29.4.2 硬件设计
1. 例程功能
使用ADC1采集(DMA读取)通道0\1\14\15\18\19的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 - PB4
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1 :通道0 – PA0、通道1 – PA1、通道14 – PA2、
通道15 – PA3、通道18 –PA4、通道19 –PA5
5)DMA(DMA1通道1)
3. 原理图
ADC和DMA属于STM32H562内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道0\1\14\15\18\19来采集外部电压值,并通过DMA来读取。
29.4.3 程序设计
29.4.3.1 ADC的HAL库驱动
本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍多通道ADC采集(DMA读取)配置步骤。
多通道ADC采集(DMA读取)配置步骤
1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道0、1、14、15、18、19,对应IO是PA0、PA1、PA2、PA3、PA4和PA5,它们的时钟开启方法如下:
- __HAL_RCC_ADC_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
通过调用29.3.3.1小节介绍的API函数初始化并使用DMA,具体请参考源码。
5)使能DMA通道,配置DMA中断优先级并开启中断,启动ADC和DMA
通过HAL_ADC_Start_DMA函数启动ADC转换并开启DMA中断,通过DMA传输ADC数据。
通过HAL_NVIC_EnableIRQ函数使能DMA数据流中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
DMA中断对于每个通道都有一个中断服务函数,比如DMA1通道1的中断服务函数为GPDMA1_Channel1_IRQHandler。一般,我们会在中断服务函数里面调用HAL库的DMA中断公共处理函数HAL_DMA_IRQHandler()来处理DMA相关的中断。然后再调用相关的中断回调函数来处理用户的逻辑代码,比如本实验会调用HAL_ADC_ConvCpltCallback回调函数来处理ADC转换完成相关的逻辑代码。
29.4.3.2 程序流程图
图29.4.3.2.1 多通道ADC采集(DMA读取)实验程序流程图
29.4.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。
adc.h头文件只是函数声明,这里就不列出来了。
下面给大家介绍adc.c文件里面的函数,主要是adc_nch_dma_init和HAL_ADC_MspInit函数,定义如下:
- /**
- * @brief ADC N通道(6通道) DMA读取 初始化函数
- * @note 由于本函数用到了6个通道, 默认使用PA0~PA5这6个通道
- * @param 无
- * @retval 无
- */
- void adc_nch_dma_init(void)
- {
- ADC_ChannelConfTypeDef adc_ch_conf = {0};
- /* 初始化ADC */
- g_adc_nch_dma_handle.Instance = ADC1; /* 选择哪个ADC */
- /* 输入时钟4分频,即adc_ker_ck=HCLK/4=62.5Mhz */
- g_adc_nch_dma_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
- g_adc_nch_dma_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
- 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.EOCSelection=ADC_EOC_SINGLE_CONV;/*转换通道结束标志*/
- 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; /* 采用软件触发的话,此位忽略 */
- /* 选择用于ADC组常规转换的采样模式 */
- g_adc_nch_dma_handle.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
- g_adc_nch_dma_handle.Init.DMAContinuousRequests = ENABLE; /* 使用DMA */
- /* 有新的数据后直接覆盖掉旧数据 */
- g_adc_nch_dma_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
- g_adc_nch_dma_handle.Init.OversamplingMode = DISABLE; /* 关闭过采样 */
- HAL_ADC_Init(&g_adc_nch_dma_handle); /* 初始化 */
- /* ADC校准 */
- HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle, ADC_SINGLE_ENDED);
- /* 配置ADC通道 */
- adc_ch_conf.Channel = ADC_CHANNEL_0; /* 配置使用的ADC通道 */
- adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 采样序列里的第1个 */
- /* 采样周期为640.5个时钟周期 */
- adc_ch_conf.SamplingTime = ADC_SAMPLETIME_640CYCLES_5;
- adc_ch_conf.SingleDiff = ADC_SINGLE_ENDED ; /* 单端输入 */
- adc_ch_conf.OffsetNumber = ADC_OFFSET_NONE; /* 无偏移 */
- adc_ch_conf.Offset = 0; /* 无偏移的情况下,此参数忽略 */
- 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_14; /* 配置使用的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_15; /* 配置使用的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通道 */
- }
- /**
- * @brief ADC底层驱动,引脚配置,时钟使能
- 此函数会被HAL_ADC_Init()调用
- * @param hadc:ADC句柄
- * @retval 无
- */
- void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
- {
- RCC_PeriphCLKInitTypeDef adc_clk_init = {0}; /* 定义外设时钟配置结构体变量 */
- GPIO_InitTypeDef gpio_init_struct = {0}; /* 定义GPIO初始化结构体变量 */
- DMA_NodeConfTypeDef node_conf = {0}; /* 定义GPDMA的链表节点配置结构体变量 */
- DMA_NodeTypeDef gpdma1_ch1_node = {0};/* 定义GPDMA1通道1的链表节点结构体变量 */
- DMA_QListTypeDef gpdma1_ch1_qlist = {0};/* 定义GPDMA1通道1链表队列结构体变量 */
- if(hadc->Instance == ADC1)
- {
- /* 打开IO、ADC1和GPDMA相关时钟 */
- __HAL_RCC_ADC_CLK_ENABLE(); /* 使能ADC1时钟 */
- __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
- __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
- __HAL_RCC_GPDMA1_CLK_ENABLE(); /* GPDMA1时钟使能 */
-
- /* 设置ADC时钟 */
- adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;/*要配置ADC时钟源*/
- /* ADC时钟时钟源为HCLK */
- adc_clk_init.AdcDacClockSelection = RCC_ADCDACCLKSOURCE_HCLK;
- HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 配置ADC时钟源 */
- /* 初始化ADC多通道对应的GPIO
- * PA0-ADC_CHANNEL_0、PA1-ADC_CHANNEL_1、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; /* 模拟 */
- gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
- HAL_GPIO_Init(GPIOA, &gpio_init_struct);
- /* ADC1 DMA初始化 */
- node_conf.NodeType = DMA_GPDMA_LINEAR_NODE; /* GPDMA线性寻址节点类型 */
- node_conf.Init.Request = GPDMA1_REQUEST_ADC1; /* DMA通道请求 */
- /* 单/突发级别的硬件请求协议 */
- node_conf.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
- /* DMA通道的传输方向:外设到内存 */
- node_conf.Init.Direction = DMA_PERIPH_TO_MEMORY;
- node_conf.Init.SrcInc = DMA_SINC_FIXED; /* 源增量模式:固定模式 单个/突发 */
- /* 目标增量模式:增量模式 单个/突发 */
- node_conf.Init.DestInc = DMA_DINC_INCREMENTED;
- /* 源数据宽度:16位 */
- node_conf.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD;
- /* 目标数据宽度:16位 */
- node_conf.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD;
- node_conf.Init.SrcBurstLength = 1; /* 指定DMA的源突发长度,范围: 1 到 64 */
- node_conf.Init.DestBurstLength = 1;/* 指定的目标突发长度,范围: 1 到 64 */
- node_conf.Init.TransferAllocatedPort =
- DMA_SRC_ALLOCATED_PORT1|DMA_DEST_ALLOCATED_PORT1; /* 指定传输分配的端口 */
- /* 指定DMA通道的传输事件模式 */
- node_conf.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
- node_conf.Init.Mode = DMA_NORMAL; /* DMA模式:正常模传输 */
- /* 设置DMA通道触发极性 */
- node_conf.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
- /* DMA通道数据交换模式:无数据交换 */
- node_conf.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
- /* 如果源数据宽度<目标数据宽度=>右对齐,填充0至目标数据宽度 */
- node_conf.DataHandlingConfig.DataAlignment =
- DMA_DATA_RIGHTALIGN_ZEROPADDED;
- HAL_DMAEx_List_BuildNode(&node_conf, &gpdma1_ch1_node);/*构建DMA通道节点*/
- /* 插入新节点 */
- HAL_DMAEx_List_InsertNode(&gpdma1_ch1_qlist, NULL, &gpdma1_ch1_node);
- HAL_DMAEx_List_SetCircularMode(&gpdma1_ch1_qlist);/*设置链表队列的循环模式*/
- /* 以链表方式初始化DMA通道 */
- /* 选择GPDMA1_Channel1 */
- g_gpdma1_ch1_adc_nch_handle.Instance = GPDMA1_Channel1;
- /* 通道优先级 */
- g_gpdma1_ch1_adc_nch_handle.InitLinkedList.Priority = DMA_HIGH_PRIORITY;
- g_gpdma1_ch1_adc_nch_handle.InitLinkedList.LinkStepMode =
- DMA_LSM_FULL_EXECUTION; /* 执行所有链表的通道 */
- g_gpdma1_ch1_adc_nch_handle.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT1; /* 链路分配端口1 */
- g_gpdma1_ch1_adc_nch_handle.InitLinkedList.TransferEventMode =
- DMA_TCEM_BLOCK_TRANSFER; /* 指定DMA通道的传输事件模式 */
- g_gpdma1_ch1_adc_nch_handle.InitLinkedList.LinkedListMode =
- DMA_LINKEDLIST_CIRCULAR; /* 循环链表DMA通道传输 */
- /* 以链表方式初始化DMA通道 */
- HAL_DMAEx_List_Init(&g_gpdma1_ch1_adc_nch_handle);
- /* 链接到DMA通道的链表队列 */
- HAL_DMAEx_List_LinkQ(&g_gpdma1_ch1_adc_nch_handle, &gpdma1_ch1_qlist);
- __HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle,
- g_gpdma1_ch1_adc_nch_handle); /* 将DMA句柄与ADC句柄关联起来 */
- HAL_DMA_ConfigChannelAttributes(&g_gpdma1_ch1_adc_nch_handle,
- DMA_CHANNEL_NPRIV); /* 通道没有特权 */
- HAL_NVIC_SetPriority(GPDMA1_Channel1_IRQn, 2, 0);
- HAL_NVIC_EnableIRQ(GPDMA1_Channel1_IRQn);
- }
- }
复制代码 adc_nch_dma_init函数主要初始化ADC和其通道的工作参数。
HAL_ADC_MspInit函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟源配置、DMA链表初始化等代码。
此外还有两个函数,GPDMA1通道1的中断服务函数和ADC转换完成回调函数,这两个函数和单通道ADC采集(DMA读取)实验基本一样,就不列出来了。
最后在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;
- HAL_Init(); /* HAL库初始化 */
- HAL_ICACHE_Enable(); /* 使能指令缓存 */
- sys_stm32_clock_init(250, 4, 2, 2); /* 系统时钟初始化, 250Mhz */
- delay_init(250); /* 延时初始化 */
- usart_init(115200); /* 串口初始化 */
- led_init(); /* LED初始化 */
- lcd_init(); /* LCD初始化 */
- adc_nch_dma_init(); /* 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, 130, 200, 12, 12, "ADC1_CH0_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 142, 200, 12, 12, "ADC1_CH0_VOL: 0.000V", BLUE);
-
- lcd_show_string(30, 160, 200, 12, 12, "ADC1_CH1_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 172, 200, 12, 12, "ADC1_CH1_VOL: 0.000V", BLUE);
- lcd_show_string(30, 190, 200, 12, 12, "ADC1_CH14_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 202, 200, 12, 12, "ADC1_CH14_VOL:0.000V", BLUE);
- lcd_show_string(30, 220, 200, 12, 12, "ADC1_CH15_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 232, 200, 12, 12, "ADC1_CH15_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);
- HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t *)g_adc_dma_buf,
- ADC_DMA_BUF_SIZE);
- while (1)
- {
- if (g_adc_dma_sta == 1)
- {
- /* 计算DMA 采集到的ADC数据的平均值 */
- sum = 0;
- /* 循环显示通道10~通道15的结果 */
- 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 / 4095);
- 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采集完成状态标志 */
- HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t *)g_adc_dma_buf,
- ADC_DMA_BUF_SIZE);
- }
- 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的转换值和换算成电压后的电压值。
29.4.4 下载验证
下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图29.4.4.1所示:
图29.4.4.1 多通道ADC采集(DMA读取)实验测试图
使用ADC1采集(DMA读取)通道0\1\14\15\18\19的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5到你想测量的电压源(0~3.3V)。
这6个通道对应引出来的引脚PA0\PA1\PA2\PA3\PA4\PA5在开发板上的位置,如下图所示:
图29.4.4.2 PA0\PA1\PA2\PA3\PA4\PA5引脚在开发板位置示意图
这六个通道可以同时测量不同测试点的电压,只需要用杜邦线分别接到不同的电压测试点即可。注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
29.5 单通道ADC过采样(16位分辨率)实验
本实验我们来学习单通道ADC过采样(16位分辨率)实验。本实验使用常规转换组单通道的单次转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。下面先带大家来了解本实验要配置的寄存器。
29.5.1 ADC寄存器
本实验很多配置和单通道ADC采集实验是一样的,下面只介绍ADCx_CFGR2寄存器。
ADCx配置寄存器2(ADCx_CFGR2)
ADCx配置寄存器2描述如图29.5.1.1所示:
图29.5.1.1 ADCx_CFGR2寄存器
OVSR[2:0]位用于设置ADC的过采样率。本实验使用过采样,并且是16位分辨率,所以设置OVSR[2:0] = 111,即256倍过采样。
ROVSM位用于设置常规过采样模式,默认为0即可,即连续模式。
TROVS位用于设置已触发常规过采样,默认为0即可,即会在触发后连续完成某一通道的所有过采样转换。
OVSS[3:0]位用于设置过采样结果右移位数。
ROVSE位是常规过采样使能位,置1使能常规过采样。
29.5.2 硬件设计
1. 例程功能
ADC1过采样(16位分辨率)采集通道19(PA5)上面的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA5到你想测量的电压源(0~3.3V),然后通过TFTLCD显示的电压值。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 - PB4
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1 :通道19 - PA5
3. 原理图
ADC属于STM32H562内部资源,实际上我们只需要软件设置就可以正常工作,另外还需要将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1过采样(16位分辨率)通道19(PA5)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V,可以通过断路帽将PA5与电位器连接,从而测量电位器的电压。
29.5.3 程序设计
29.5.3.1 ADC的HAL库驱动
本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。
单通道ADC过采样(16位分辨率)配置步骤
1)开启ADCx和ADC通道对应的IO时钟,并配置该IO为模拟功能
首先开启ADCx的时钟,然后配置GPIO为模拟模式。本实验我们默认用到ADC1通道19,对应IO是PA5,它们的时钟开启方法如下:
- __HAL_RCC_ADC_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值。
29.5.3.2 程序流程图
图29.5.3.2.1 单通道ADC过采样(16位分辨率)实验程序流程图
29.5.3.3 程序解析
adc.h头文件和单通道ADC采集实验的内容一样,这里就不列出来了。
下面来介绍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)
- {
- g_adc_handle.Instance = ADC_ADCX; /* 选择哪个ADC */
- /* 输入时钟4分频,即adc_ker_ck=HCLK/4=62.5Mhz */
- g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
- g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
- g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐 */
- g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE; /* 非扫描模式 */
- g_adc_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* 转换通道结束标志 */
- 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;
- /* 选择用于ADC组常规转换的采样模式 */
- g_adc_handle.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
- g_adc_handle.Init.DMAContinuousRequests = DISABLE; /* 不使用DMA */
- /* 有新的数据后直接覆盖掉旧数据 */
- g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
- g_adc_handle.Init.OversamplingMode = ENABLE; /* 使用过采样 */
- g_adc_handle.Init.Oversampling.Ratio = osr ; /* 设置osr倍过采样 */
- 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_SINGLE_ENDED); /* ADC校准 */
- }
复制代码 adc_oversample_init函数的功能是设置ADC过采样、初始化ADC,校准ADC等。另外还有几个函数都在单通道ADC采集实验列出来了,请大家参考源码。
最后在main.c里面编写如下代码:
- int main(void)
- {
- uint16_t i,j;
- uint16_t adcx;
- uint32_t sum;
- float temp;
- HAL_Init(); /* HAL库初始化 */
- HAL_ICACHE_Enable(); /* 使能指令缓存 */
- sys_stm32_clock_init(250, 4, 2, 2); /* 系统时钟初始化, 250Mhz */
- delay_init(250); /* 延时初始化 */
- usart_init(115200); /* 串口初始化 */
- led_init(); /* LED初始化 */
- lcd_init(); /* LCD初始化 */
- /* 初始化ADC, 256x过采样, 右移4位 */
- adc_oversample_init(ADC_OVERSAMPLING_RATIO_256, ADC_RIGHTBITSHIFT_4);
- 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, 5, 16, 0, BLUE); /* 显示ADCC采样后的原始值 */
- /* 获取计算后的带小数的实际电压值,比如3.1111 */
- temp = (float)adcx * (3.3 / 65536);
- 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闪烁,以提示程序正在运行。
29.5.4 下载验证
下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图29.5.4.1所示:
图29.5.4.1 单通道ADC过采样(16位分辨率)实验测试图
上图中,我们使用短路帽将P5的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0闪烁,提示程序运行。
大家也可以用杜邦线将ADC排针接到其它待测量的电压点,看看测量到的电压值是否准确?但是要注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。 |