第4章 电机控制相关的STM32外设(4.4)
4.4 ADC
本小节我们将介绍STM32G474的ADC(Analog-to-digital converters,模数转换器)功能。ADC应用于电机控制的多个方面,例如:电源电压采集、电机电流采集、驱动板温度采集等。我们通过四个实验来学习ADC,分别是单通道ADC采集实验、单通道ADC采集(DMA读取)实验、多通道ADC采集(DMA读取)实验和内部温度传感器实验。
本小节主要分为如下几个部分:
4.4.1 ADC简介
4.4.2 单通道ADC采集实验
4.4.3 单通道ADC采集(DMA读取)实验
4.4.4 多通道ADC采集(DMA读取)实验
4.4.5 内部温度传感器实验
4.4.1 ADC简介
ADC即模拟数字转换器,英文详称Analog-to-digital converter,可以将外部的模拟信号转换为数字信号。
STM32G474VET6芯片拥有5个ADC,都可以独立工作,其中ADC1和ADC2以及ADC3和ADC4还可以各自组成双重模式(提高采样率)。这些ADC都是12位逐次逼近型的模拟数字转换器,有19个通道,可测量16个外部信号源、2个内部信号源和Vbat通道的信号。ADC中的各个通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16位数据寄存器中。
STM32G474的ADC主要特性我们可以总结为以下几条:
1、12,10,8,6位可设置分辨率
2、转换结束、注入转换结束和发生模拟看门狗事件时产生中断
3、单次和连续转换模式
4、自校准
5、带内嵌数据一致性的数据对齐
6、采样间隔可以按通道分别编程
7、规则转换和注入转换均有外部触发选项
8、间断模式
9、双重模式(ADC1和ADC2或者ADC3和ADC4)
10、ADC供电要求:2.4V到3.6V
11、ADC输入范围:VREF– ≤ VIN ≤ VREF+
12、规则通道转换期间有DMA请求产生
13、支持增益补偿和偏移补偿
下面来介绍ADC的框图:

图4.4.1.1 ADC框图
图4.4.1.1中,我们按照ADC的配置流程标记了10处位置,分别如下:
① VREF+电压
VREF+ 是正模拟参考电压输入,选择范围是1.62V~3.6V,开发板上我们一般给VREF+接入的电压时3.3V,所以得到开发板上的ADC测量范围是0~3.3V。
② ADC的双时钟域架构
ADC有两种时钟源可以选择,分别是:
(1)adc_hclk(属于同步时钟),来自AHB总线的系统时钟,ADC1和ADC2处在170MHz的 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,得到的频率是42.5MHz,注意数据手册中明确限制ADC时钟频率最高能到60M,切记不可超频,因为超频误差会比较大。接着我们介绍下另外一种时钟源。
(2)adc_ker_ck(属于异步时钟),可以通过RCC_CCIPR寄存器的ADC12SEL [1:0]位来选择不同的时钟源,前提是前面提到的CKMODE[1:0]=00。ADC异步时钟模式下可以选择以下的时钟源:
ADCSEL [1:0]=00,未选择时钟;
ADCSEL [1:0]=01,使用PLL锁相环中“P”输出作为ADC1/2时钟源;
ADCSEL [1:0]=10,使用系统时钟作为ADC1/2时钟源;
ADCSEL [1:0]=11,保留;
实际的例程中我们选择adc_hclk作为ADC时钟源,好处就是绕过了时钟域重新同步,进而使其被触发时没有任何的不确定性。并且可以通过ADCx_CCR寄存器的CKMODE [1:0]位进行分频,可以是1、2、4这3种分频系数。
上面的分析请结合下面的ADC时钟方案图理解:

图4.4.1.2 ADC时钟方案
③ 输入通道
在确定好了ADC输入电压后,如何把外部输入的电压输送到ADC转换器中呢?在这里引入了通道概念,注意STM32G4系列ADC总共有19个输入通道。
ADC连接多路内部模拟输入,分别是:
(1)内部参考电压 (VREFINT ) 连接到ADC1/3/4/5_IN18。
(2)内部温度传感器 (VSENSE ) 连接到ADC1_IN16/ADC5_IN4。
(3)VBAT 监测通道 (VBAT/3) 连接到ADC1/3/5_IN17。
具体的ADC通道表见表4.4.1.1所示:


表4.4.1.1 ADC通道表
④转换序列
可以将转换分为两组:常规转换组和注入转换组。常规转换组最多允许16个通道进行转换。注入转换组最多允许4个通道进行转换。
如何理解常规转换组和注入转换组?常规转换组相当于你正常运行的程序,而注入转换组就相当于中断。在你程序正常执行的时候,中断是可以打断你的执行的,获得优先执行的权利。所以注入转换组可以打断常规转换组的转换,获得优先转换的权利,在注入转换组转换完成后,常规转换组才得以继续转换。
为了便于理解,请看常规转换组和注入转换组的转换优先级对比图,如图4.4.1.2所示:

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

表4.4.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个通道。
注入序列
注入序列和常规序列差不多,决定的是注入转换组的顺序。注入组最大允许4个通道输入,它的注入序列由JSQR寄存器配置。注入序列寄存器JSQR控制关系如表4.4.1.3所示:

表4.4.1.3 注入序列寄存器控制关系汇总表
注入序列的长度写入JL [ 1 : 0 ]位,范围是0~3。注意:写入0表示这个注入序列有一个通道,而不是0个通道。
① 触发源
在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC的触发转换有两种方法:分别是通过软件或外部事件(也就是硬件)触发转换。。
我们先来看看通过软件触发转换的方法,常规通道由ADCx_CR寄存器的ADSTART位触发,注入通道由ADCx_CR寄存器的JADSTART位触发。方法是:通过对ADCx_CR寄存器的ADSTART(JADSTART)位写1开始转换,转换结束由硬件清零该位,这个控制ADC转换的方式非常简单。
另一种就是通过外部事件触发转换的方法,如定时器和输入引脚触发等等,具体请看《STM32G4xx参考手册_V7(英文版).pdf》的623页开始的的表163~表166。外部事件触发转换可分为:常规通道的外部触发和注入通道的外部触发两种。
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]位来选择。
② 转换时间
(1)ADC时钟
在学习转换时间之前,我们先来了解ADC时钟。从标号⑤框出来部分可以看到ADC时钟是要经过ADC预分频器的,那么ADC的时钟源是什么?ADC预分频器的分频系数可以设置的范围又是多少?以及ADC时钟频率的最大值又是多少?下面将为大家解答。
ADC的输入时钟是由APB2经过分频产生,分频系数是由RCC_CFGR寄存器中的PPRE2[2:0]进行设置的,可选择2/4/6/8/16分频。需要注意的是,STM32G474的ADC的输入时钟频率最大值是36MHz,如果超过这个值将会导致ADC的转换结果准确度下降。
一般我们设置APB2为84MHz。为了不超过ADC的最大输入时钟频率36MHz,我们设置ADC的预分频器分频系数为4,就可以得到ADC的输入时钟频率为84MHz/4,即21MHz。例程中,我们也是如此设置的。
(2)转换时间
STM32G474的ADC总转换时间的计算公式如下:
T CONV = 采样时间 + 逐次逼近时间(T SAR)
采样时间可通过ADC_SMPR1和ADC_SMPR2寄存器中的SMP[2:0]位编程,ADC_SMPR1控制的是通道0~9,ADC_SMPR2控制的是通道10~18。每个输入通道都支持通过编程来选择不同的采样时间,采样时间可选的范围如下:
l SMP = 000:2.5个ADC时钟周期
l SMP = 001:6.5个ADC时钟周期
l SMP = 010:12.5个ADC时钟周期
l SMP = 011:24.5个ADC时钟周期
l SMP = 100:47.5个ADC时钟周期
l SMP = 101:92.5个ADC时钟周期
l SMP = 110:247.5个ADC时钟周期
l SMP = 111:640.5个ADC时钟周期
逐次逼近时间(TSAR)是由分辨率决定的,分辨率通过对ADCx_CFGR寄存器的RES[1:0]位进行编程,可将分辨率配置为12位、10位、8位、6位。而逐次逼近时间和分辨率的对应关系如下表所示:

表4.4.1.4 T SAR 与分辨率的对应关系
举个例子,我们配置SMP = 111,即设置最大采样周期,然后采用12位分辨率,那么得到:
T CONV = 640.5个ADC时钟周期 + 12.5个ADC时钟周期 = 653个ADC时钟周期
表格中,F ADC的频率是30MHZ,我们的例程中ADC的时钟源是170MHZ的hsi_ker_ck经过4分频得到,即42.5MHZ。我们就以FADC的频率为42.5MHZ来举例,可得到:

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

表4.4.1.5 每个ADC的ADC中断
在表4.4.1.5中,前面5个中断都很好理解,我们从模拟看门狗中断介绍。
模拟看门狗中断发生条件:首先通过ADCx_TRx寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。
采样阶段结束:如果位EOSMP被硬件置1,则说明采样阶段结束(仅限常规转换),然后可以通过对EOSMP位写1来清零该位。如果EOSMPIE位置1,可以产生采样阶段结束中断。
上溢:如果常规转换后的数据未在新转换数据可用之前(由CPU或DMA)读取,会由溢出标志(OVR)指示缓冲区溢出事件。如果OVRIE位置1,可以产生一个溢出中断。
此外,我们还要知道常规组和注入组的转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据存储在内存里面,防止读取不及时数据被覆盖。
4.4.2 单通道ADC采集实验
本实验我们来学习单通道ADC采集实验。本实验使用常规转换组单通道的单次转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。下面先带大家来了解本实验要配置的寄存器
4.4.2.1 ADC寄存器
这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
l ADC通用控制寄存器(ADC_ CCR)
ADC通用控制寄存器描述如图4.4.2.1.1所示:

图4.4.2.1.1 ADC_CCR寄存器
该寄存器本章只需要用到CKMODE[1:0]这2个位,用于设置ADC时钟的预分频系数(即对adc_hclk的分频系数)。本章的实验我们都设置adc_hclk(170MHz)作为时钟源。因此:
adc_hclk = 170MHz
又由于ADC的输入时钟频率不能大于60M,所以,我们需要设置CKMODE [1:0]=11,即可得到ADC输入时钟频率为:
adc_hclk / 4 = 170 / 2=42.5MHz
l ADC控制寄存器(ADCx_CR)
ADC控制寄存器描述如图4.4.2.1.2所示:

图4.4.2.1.2 ADC_CR寄存器
该寄存器我们用到多个位,这里就不全部列出来讲解了,而是抽出几个重要的位进行针对性的介绍,详细的介绍请参考《STM32G4xx参考手册_V7(英文版).pdf》第21.7.3节,687页。
ADEN位用于使能ADC转换器。需要设置该位为1,ADC才可以正常工作。
ADSTART位用于启动ADC常规通道的转换序列。当使用硬件触发时(EXTEN[1:0]!=0),设置该位为1,必须在相应的硬件触发事件产生时,才会启动ADC转换。而当不使用硬件触发时(EXTEN[1:0]=0),设置该位为1则可以立即启动ADC转换。
ADCAL位用与控制/读取ADC校准状态。设置该位为1时,可以启动ADC校准,等校准完成以后,硬件会自动清零该位。因此在设置改位为1以后,通过判断该位是否变为0,即可判断校准是否完成。
l ADCx配置寄存器(ADCx_CFGR)
ADCx配置寄存器描述如图4.4.2.1.3所示:

图4.4.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。
ALIGN位用于设置ADC的数据对齐方式,当设置该位为0时,代表数据右对齐,当设置该位为1时,代表数据左对齐。本实验设置该位为0,也就是数据右对齐。
l ADCx常规序列寄存器1(ADCx_SQR1)
ADCx常规序列寄存器1描述如图4.4.2.1.4所示:

图4.4.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寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的常规序列寄存器控制关系汇总表。
l ADCx采样时间寄存器1(ADCx_SMPR1)
ADCx采样时间寄存器1描述如图4.4.2.1.5所示:

图4.4.2.1.5 ADCx_SMPR1寄存器
该寄存器用于设置ADC通道0~9的采样时间,而ADCx_SMPR2设置ADC通道10~18的采样时间。STM32G4的ADC总转换时间的计算方法前面已经介绍过了。建议采样时间尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率,所以大家在实际应用中自行结合自身情况设置。
l ADCx常规数据寄存器(ADCx_ DR)
ADCx常规数据寄存器描述如图4.4.2.1.6所示:

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

图4.4.2.1.7 ADCx_ ISR寄存器
该寄存器保存了ADC转换时的各种状态。本实验我们通过EOC位的状态来判断ADC转换是否完成,如果查询到EOC位被硬件置1,就可以从ADC_DR寄存器中读取转换结果,否则需要等待转换完成。
至此,本章要用到的ADC相关寄存器全部介绍完毕了,对于未介绍的部分,请大家参考《STM32G4xx参考手册_V7(英文版).pdf》第21章相关内容。
4.4.2.2 硬件设计
1. 例程功能
采集ADC1通道8(PC2)上的电压,在LCD模块上面显示ADC转换值以及换算成电压后的电压值。我们使用杜邦线将PC2引脚接到DC 0~3.3V的电源上(外部电源记得共地),即可采集到ADC数据。ADC采集到的数据和转换后的电压值将在LCD屏中显示。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子1.3寸TFTLCD显示屏
4)ADC1 :通道8 – PC2
3. 原理图
ADC属于STM32G474内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验通过ADC1的通道8(PC2)来采集外部电压值,我们只需要1根杜邦线,一端接到PC2上,另外一端就接在要测试的电压点(如果是外部电源记得共地),即可测量电压。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.2.3 程序设计
4.4.2.3.1 ADC的HAL库驱动
ADC在HAL库中的驱动代码在stm32g4xx_hal_adc.c和stm32g4xx_hal_adc_ex.c文件(及其头文件)中。
1. HAL_ADC_Init函数
ADC的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);
l 函数描述:
用于初始化ADC。
l 函数形参:
形参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_HandleTypeDef;
该结构体定义和其他外设比较类似,我们着重看第二个成员变量Init含义,它是结构体
ADC_InitTypeDef类型,结构体ADC_InitTypeDef定义为:
typedef struct
{
uint32_t ClockPrescaler; /* 设置预分频系数,即PRESC[3:0]位 */
uint32_t Resolution; /* 配置ADC的分辨率 */
uint32_t DataAlign; /* 设置数据的对齐方式 */
uint32_t GainCompensation; /* 增益补偿 */
uint32_t ScanConvMode; /* 扫描模式 */
uint32_t EOCSelection; /* 转换完成标志位 */
FunctionalState LowPowerAutoWait; /* 低功耗自动延时 */
FunctionalState ContinuousConvMode; /* 开启连续转换模式否则单次转换模式 */
uint32_t NbrOfConversion; /* 设置转换通道数目 */
FunctionalState DiscontinuousConvMode; /* 是否使用规则通道组间断模式 */
uint32_t NbrOfDiscConversion; /* 单次转换通道的数目 */
uint32_t ExternalTrigConv; /* ADC外部触发源选择 */
uint32_t ExternalTrigConvEdge; /* ADC外部触发极性*/
uint32_t SamplingMode; /* 常规组的采样模式选择 */
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最大时钟配置为60MHZ。
2) Resolution:配置ADC的分辨率,可选的分辨率有12位、10 位、8 位和 6 位。分辨率越高,转换数据精度越高,转换时间也越长;反之分辨率越低,转换数据精度越低,转换时间也越短。
3) DataAlign:用于设置数据的对齐方式,这里可以选择右对齐或者是左对齐,该参数可选为:ADC_DATAALIGN_RIGHT和ADC_DATAALIGN_LEFT。
4)GainCompensation:指定用于ADC原始转换数据的ADC增益补偿系数。
5) ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。
6) EOCSelection:可选参数为ADC_EOC_SINGLE_CONV和ADC_EOC_SEQ_CONV,指定转换结束时是否产生EOS中断或事件标志。
7)LowPowerAutoWait:配置是否使用低功耗自动延迟等待模式,可选参数为 ENABLE和 DISABLE,当使能时,仅当一组内所有之前的数据已处理完毕时,才开始新的转换,适用于低频应用。该模式仅用于ADC的轮询模式,不可用于DMA以及中断。
8) ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
9) NbrOfConversion:设置常规转换通道数目,范围是:1~16。
10) DiscontinuousConvMode:配置是否使用规则通道组间断模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道1和2,下次触发就是转换通道5和7,这样不连续的转换,依次类推。此参数只有将ScanConvMode使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。
11) NbrOfDiscConversion:配置间断模式的通道个数,禁止规则通道组间断模式后,此参数忽略。
12) ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
13) ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。
14)SamplingMode:常规序列的采样模式选择
15) DMAContinuousRequests:指定DMA请求是否以一次性模式执行(当达到转换次数时,DMA传输停止)或在连续模式下(DMA传输无限制,无论转换的数量)。注:在连续模式下,DMA必须配置为循环模式。否则,当达到DMA缓冲区最大指针时将触发溢出。注意:当常规组和注入组都没有转换时(禁用ADC,或启用ADC,没有连续模式或可以启动转换的外部触发器),必须修改此参数。该参数可设置为“启用”或“禁用”。
16) Overrun:当有新的数据溢出时,可以选择覆盖写入或者是丢弃新的数据。
17) OversamplingMode:是否使用过采样模式。
18) Oversampling:配置过采样模式的参数
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_ADCEx_Calibration_Start函数
ADC的自校准函数,其声明如下:
HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc, uint32_t SingleDiff);
l 函数描述:
首先调用HAL_ADC_Init函数配置了相关的功能后,再调用此函数进行ADC自校准功能。
l 函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是单端或差分模式选择,有以下两种:
1)ADC_SINGLE_ENDED表示单端输入模式。
2)ADC_DIFFERENTIAL_ENDED表示差分输入模式。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_ADC_ConfigChannel函数
ADC通道配置函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc, ADC_ChannelConfTypeDef *sConfig);
l 函数描述:
调用了HAL_ADC_Init函数配置了相关的功能后,就可以调用此函数配置ADC具体通道。
l 函数形参:
形参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~18。
2) Rank:在常规转换中的常规组的转换顺序,可以选择1~16。
3) SamplingTime:ADC的采样周期,最大640.5个ADC时钟周期,要求尽量大以减少误差。
4) SingleDiff:选择通道单端输入还是差分输入。
5) OffsetNumber:选择使用偏移量的通道。
6) Offset:定义要从原始数据减去的偏移量。根据ADC的分辨率不同,支持的最大偏移量也不同,例如分辨率是12bit,,最大的偏移量为0x0FFF。
7)OffsetSign:定义偏移量从原始转换数据中减去还是增加。
8) OffsetSignedSaturation:是否使能ADC采样值的最高位为符号位。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
3. HAL_ADC_Start函数
ADC转换启动函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
l 函数描述:
当配置好ADC的基础的功能后,就调用此函数启动ADC。
l 函数形参:
ADC_HandleTypeDef结构体类型指针变量。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
4. HAL_ADC_PollForConversion函数
等待ADC规则组转换完成函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout);
l 函数描述:
一般先调用HAL_ADC_Start函数启动转换,再调用该函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
l 函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是等待转换的等待时间,单位是毫秒(ms)。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. HAL_ADC_GetValue函数
获取常规组ADC转换值函数,其声明如下:
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc);
l 函数描述:
一般先调用HAL_ADC_Start函数启动转换,再调用HAL_ADC_PollForConversion函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
l 函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
l 函数返回值:
当前的转换值,uint32_t类型数据。
单通道ADC采集配置步骤
1)开启ADCx和通道输入的GPIO时钟,配置该IO口的模拟输入功能
首先开启ADCx的时钟,然后配置GPIO为模拟输入。本实验我们默认用到ADC1通道8,对应IO是PC2,它们的时钟开启方法如下:
__HAL_RCC_ADC12_CLK_ENABLE(); /* 使能ADC1时钟 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOA时钟 */
IO口模拟输入功能是通过函数HAL_GPIO_Init来配置的。
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值。
4.4.2.3.2 程序流程图

图4.4.2.3.2.1 单通道ADC采集实验程序流程图
4.4.2.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。
adc.h文件针对ADC及通道引脚定义了一些宏定义,具体如下:
/* ADC及引脚 定义 */
#define ADC_ADCX_CHY_GPIO_PORT GPIOC
#define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_2
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ADC_ADCX ADC1
#define ADC_ADCX_CHY ADC_CHANNEL_8
#define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
ADC的通道与引脚的对应关系在《STM32G474VCT6.pdf》数据手册可以查到,我们这里使用ADC1的通道8,在数据手册中的表格为:

表4.4.2.3.3.1 ADC通道8对应引脚查看表
下面直接开始介绍adc.c的程序,首先是ADC初始化函数。
/**
* @brief ADC初始化函数
* @note 本函数支持ADC1/ADC2任意通道, 但是不支持ADC3
* 我们使用12位精度, ADC采样时钟=170/4 = 42.5M, 转换时间为: 采样周期 +
12.5个ADC周期
* 设置最大采样周期: 640.5, 则转换时间 = 653 个ADC周期 = 0.15us
* @param 无
* @retval 无
*/
void adc_init(void)
{
g_adc_handle.Instance = ADC_ADCX;
/* 4分频,ADCCLK = SYSCLK/4 = 170/4 = 42.5Mhz */
g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_handle.Init.GainCompensation = 0; /* 不需要增益补偿 */
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 = ENABLE; /* 使能连续转换 */
/* 1个转换在规则序列中 也就是只转换规则序列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;
g_adc_handle.Init.DMAContinuousRequests = DISABLE; /* 关闭DMA请求 */
/* 数据溢出时覆盖ADC数据 */
g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
g_adc_handle.Init.OversamplingMode = DISABLE; /* 禁用过采样功能 */
HAL_ADC_Init(&g_adc_handle); /* 初始化 */
/* 以单端模式运行ADC校准 */
HAL_ADCEx_Calibration_Start(&g_adc_handle, ADC_SINGLE_ENDED);
}
该函数调用HAL_ADC_Init函数配置了选择哪个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(); /* 使能ADCx时钟 */
ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启GPIO时钟 */
/* AD采集引脚模式设置,模拟输入 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
gpio_init_struct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
}
}
可以看到在HAL_ADC_MspInit函数中,我们使能了ADC和通道对应IO时钟、初始化IO为模拟输入模式。
接下来要介绍的函数是adc_channel_set,其定义如下:
/**
* @brief 设置ADC通道采样时间
* @param adcx : adc句柄指针,ADC_HandleTypeDef
* @param ch : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17
* @param stime: 采样时间 0~7, 对应关系为:
* @arg ADC_SAMPLETIME_2CYCLES_5, 2.5个ADC时钟周期
ADC_SAMPLETIME_6CYCLES_5, 6.5个ADC时钟周期
* @arg ADC_SAMPLETIME_12CYCLES_5, 12.5个ADC时钟周期
ADC_SAMPLETIME_24CYCLES_5, 24.5个ADC时钟周期
* @arg ADC_SAMPLETIME_47CYCLES_5, 47.5个ADC时钟周期
ADC_SAMPLETIME_92CYCLES_5, 92.5个ADC时钟周期
* @arg ADC_SAMPLETIME_247CYCLES_5 , 247.5个ADC时钟周期
ADC_SAMPLETIME_640CYCLES_5,640.5个ADC时钟周期
* @param rank: 多通道采集时需要设置的采集编号,
假设你定义channel1的rank=1,channel2的rank=2,
那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就是通道1的转换结果,
AdcDMA[1]就是通道2的转换结果。
单通道设置为 ADC_REGULAR_RANK_1
* @arg 编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16
* @retval 无
*/
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch,
uint32_t rank, uint32_t stime)
{
/* 配置对应ADC通道 */
ADC_ChannelConfTypeDef adc_channel;
adc_channel.Channel = ch; /* 设置ADCX对通道ch */
adc_channel.Rank = rank; /* 设置采样序列 */
adc_channel.SamplingTime = stime; /* 设置采样时间 */
adc_channel.SingleDiff = ADC_SINGLE_ENDED; /* 单端输入模式 */
HAL_ADC_ConfigChannel( adc_handle, &adc_channel );
}
该函数主要是通过HAL_ADC_ConfigChannel函数选择要配置的ADC规则组通道,并设置通道的序列号和采样时间。
下面要介绍的是获得ADC转换后的结果函数,其定义如下:
/**
* @brief 获取ADC值
* @param ch: 通道值 0~18,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_18
* @retval 无
*/
uint32_t adc_get_result(uint32_t ch)
{
/* 设置通道,序列和采样时间 */
adc_channel_set(&g_adc_handle , ch, 1, ADC_SAMPLETIME_640CYCLES_5);
HAL_ADC_Start(&g_adc_handle); /* 开启ADC */
HAL_ADC_PollForConversion(&g_adc_handle, 10); /* 轮询转换 */
/* 返回最近一次ADC1规则组的转换结果 */
return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
}
该函数先是调用我们自己定义的adc_channel_set函数选择ADC通道、设置转换序列号和采样时间等,接着调用HAL_ADC_Start启动转换,然后调用HAL_ADC_PollForConversion函数等待转换完成,最后调用HAL_ADC_GetValue函数获取转换结果。
接下来要介绍的函数是获取ADC某通道多次转换结果平均值函数,函数定义如下:
/**
* @brief 获取通道ch的转换值,取times次,然后平均
* @param ch : 通道号, 0~18
* @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库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_init(); /* 初始化ADC */
lcd_show_string(10, 10, 200, 24, 24, "STM32", RED);
lcd_show_string(10, 40, 200, 24, 24, "ADC TEST", RED);
lcd_show_string(10, 70, 200, 24, 24, "ATOM@ALIENTEK", RED);
lcd_show_string(10, 100, 200, 16, 16, "ADC1_CH3_VAL:", RED);
/* 先在固定位置显示小数点 */
lcd_show_string(10, 130, 200, 16, 16, "ADC1_CH3_VOL:0.000V", RED);
while (1)
{
/* 获取ADC通道的转换值,10次取平均 */
adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
/* 显示ADC采样后的平均值 */
lcd_show_xnum(114, 100, adcx, 5, 16, 0, RED);
/* 获取计算后的带小数的实际电压值,比如3.1111 */
temp = (float)adcx * (3.3 / 4096);
/* 赋值整数部分给adcx变量,因为adcx为u16整形 */
adcx = temp;
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
lcd_show_xnum(114, 130, adcx, 1, 16, 0, RED);
/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111 - 3 = 0.1111 */
temp -= adcx;
/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数 */
temp *= 1000;
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111 */
lcd_show_xnum(130, 130, temp, 3, 16, 0X80, RED);
LED0_TOGGLE();
delay_ms(100);
}
}
main函数中,我们在LCD上显示一些提示信息后,将每隔100ms刷新一次ADC1通道8的值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。ADC值的显示简单介绍一下:首先在LCD固定位置显示了小数点,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就能在LCD上面显示转换结果的整数和小数部分。
4.4.2.4 下载验证
下载代码后,可以看到LCD显示如图4.4.2.4.1所示:

图4.4.2.4.1单通道ADC采集实验测试图
上图中,我们使用1根杜邦线将PC2接到要测试的电压点(如果是外部电源记得共地),即可测出电压值,可测量的电压范围:0~3.3V。LED0闪烁,提示程序运行。
注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.3 单通道ADC采集(DMA读取)实验
本实验我们来学习单通道ADC采集(DMA读取)。本实验使用常规转换组单通道的连续转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
4.4.3.1 ADC&DMA寄存器
本实验我们很多的设置和单通道ADC采集实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集实验不同设置的ADC_CR2寄存器进行介绍,其他的配置基本一样的。另外因为我们用到DMA读取数据,所以还会介绍如何配置相关DMA的寄存器。
l ADCx配置寄存器(ADCx_CFGR)
ADCx配置寄存器描述如图4.4.3.1.1所示:

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

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

图4.4.3.1.3 DMA_CMARx寄存器
该寄存器存放的是DMA读或者写数据的目标存放的地址。同样的,该寄存器也是受写保护,只有当DMA_CCRx的EN位为0时才可以写入。
l DMA通道x数据项数寄存器(DMA_CNDTRx)(x = 1…8)
DMA通道x数据项数寄存器描述如图4.4.3.1.4所示:

图4.4.3.1.4 DMA_CNDTRx寄存器
前面的DMA_CPARx寄存器是传输的源地址,而DMA_CMARx寄存器是传输的目的地址,DMA_CNDTRx寄存器则是要传输的数据项数目(0到65535)。
其他的DMA寄存器我们就不一一介绍了,大家有需要的话可以查阅参考手册。
4.4.3.2 硬件设计
1. 例程功能
采集ADC1通道8(PC2)上的电压,并在LCD上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。我们使用杜邦线将PC2引脚接到DC 0~3.3V的电源上(外部电源记得共地),即可采集到ADC数据。ADC采集到的数据和转换后的电压值将在LCD屏中显示。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 1.3寸TFTLCD模块
4)ADC1 :通道8 – PC2
5)DMA(DMA1通道1)
3. 原理图
ADC属于STM32G474内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验通过ADC1的通道8(PC2)来采集外部电压值,我们只需要1根杜邦线,一端接到PC2上,另外一端就接在要测试的电压点(如果是外部电源记得共地),即可测量电压。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.3.3 程序设计
4.4.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);
l 函数描述:
用于启动DMA传输,并开启相关中断,DMA1和DMA2都是用的这个函数。
l 函数形参:
形参1是DMA_HandleTypeDef结构体类型指针变量。
形参2是DMA传输的源地址。
形参3是DMA传输的目的地址。
形参4是要传输的数据项数目。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_ADC_Start_DMA函数
启动ADC(DMA传输)方式函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t *pData, uint32_t Length);
l 函数描述:
用于启动ADC,转换结果以DMA传输方式读取。
l 函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是ADC采样数据传输的目的地址。
形参3是要传输的数据项数目。
l 函数返回值:
HAL_StatusTypeDef枚举类型的值。
l 注意事项:
HAL_ADC_Start_DMA和HAL_DMA_Start都是配置并启动DMA的函数,区别在于:HAL_ADC_Start_DMA比较局限性,只是用于启动ADC的数据传输。HAL_DMA_Start则适用性较广泛,任何能使用DMA传输的场景都可以用该函数启动。实际应用中看个人的需求选择用哪个函数。在例程中我们使用的是HAL_ADC_Start_DMA函数。如果我们需要使用DMA中断,我们还可以使用HAL_DMA_Start_IT函数,使能了DMA全部的中断。
单通道ADC采集(DMA读取)配置步骤
1)开启ADCx和通道输入的GPIO时钟,配置该IO口的模拟输入功能
首先开启ADCx的时钟,然后配置GPIO为模拟输入。本实验我们默认用到ADC1通道2,对应IO是PC2,它们的时钟开启方法如下:
__HAL_RCC_ADC12_CLK_ENABLE (); /* 使能ADC1时钟 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
IO口模拟输入功能是通过函数HAL_GPIO_Init来配置的。
2)初始化ADCx, 配置其工作参数
通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
注意:该函数会调用:HAL_ADC_MspInit回调函数来存放ADC及GPIO时钟使能、GPIO初始化等代码。
3)配置ADC通道并启动AD转换器
在HAL库中,通过HAL_ADC_ConfigChannel函数来选择要配置ADC的通道,并设置规则序列、采样时间等。
配置好ADC通道之后,通过HAL_ADC_Start_DMA函数启动AD转换器。
4)初始化DMA
通过HAL_DMA_Init函数初始化DMA,包括配置通道,外设地址,存储器地址,传输数据量等。
HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。这个宏定义为__HAL_LINKDMA。
5)使能DMA对应通道中断,配置DMA中断优先级,使能ADC,使能并启动DMA
通过HAL_ADC_Start_DMA函数开启ADC转换,通过DMA传输结果。
通过HAL_DMA_Start_IT函数启动DMA读取,使能DMA中断。
通过HAL_NVIC_EnableIRQ函数使能DMA通道中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
DMA的每个通道都有一个中断服务函数,比如DMA1_Channel1的中断服务函数为DMA1_Channel1_IRQHandler。简单的做法就是,在对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清除该中断标志位,本实验的做法就是如此。
除此之外,我们还可以通过调用HAL库所提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
4.4.3.3.2 程序流程图

图4.4.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图
4.4.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。由于本实验用到DMA,所以在adc.h头文件定义了以下一些宏定义:
/* ADC单通道/多通道 DMA采集 DMA通道相关 定义 * 注意: 这里我们的通道还是使用上面的定义 */ #define ADC_ADCX_DMASx DMA1_Channel1 #define ADC_ADCX_DMASx_REQUEST DMA_REQUEST_ADC1 #define ADC_ADCX_DMASx_IRQn DMA1_Channel1_IRQn #define ADC_ADCX_DMASx_IRQHandler DMA1_Channel1_IRQHandler /* ADC DMA采集 BUF大小, 应等于ADC通道数的整数倍 */ #define ADC_DMA_BUF_SIZE 50 #define ADC_CH_NUM 1 /* ADC 通道数量 */ /* ADC DMA BUF */ extern uint16_t g_adc_dma_buf[ADC_CH_NUM * ADC_DMA_BUF_SIZE]; extern uint16_t g_adc_val[ADC_CH_NUM]; /* ADC平均值存放数组 */ extern ADC_HandleTypeDef g_adc_handle; /* ADC句柄 */
下面给大家介绍adc.c文件里面的函数,首先是ADC DMA读取初始化函数。
/**
* @brief ADC DMA读取 初始化函数
* @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
* @param par : 外设地址
* @param mar : 存储器地址
* @retval 无
*/
void adc_dma_init(void)
{
__HAL_RCC_DMAMUX1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA配置 */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 设置DMA通道 */
g_dma_adc_handle.Init.Request= ADC_ADCX_DMASx_REQUEST; /* 设置DMA请求 */
/* DIR = 1 , 外设到存储器模式 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
/* 外设数据长度:16位 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_CIRCULAR; /* 外设流控模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_adc_handle); /* 初始化DMA */
adc_init(); /* 初始化ADC */
/* 配置对应ADC通道 */
adc_channel_set(&g_adc_handle , ADC_ADCX_CHY, ADC_REGULAR_RANK_1,
ADC_SAMPLETIME_640CYCLES_5);
__HAL_LINKDMA(&g_adc_handle,DMA_Handle,g_dma_adc_handle);
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3); /* 设置DMA中断优先级
为3,子优先级为3 */
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn); /* 使能DMA中断 */
}
adc_dma_init函数包含了输入通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。下面来看看该函数的代码内容。
第一部分,使能ADC、DMA和GPIO的时钟。
第二部分,初始化DMA、ADC,配置ADC时钟预分频系数为4,得到ADC的输入时钟频率是42.5MHz。
第三部分,通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
第四部分,配置ADC通道。
第五部分,配置DMA通道请求中断优先级,并使能该中断。
另外在函数HAL_ADC_MspInit里存放使能时钟、GPIO初始化相关的代码。
接下来给大家介绍使能一次ADC DMA传输函数,其定义如下:
/**
* @brief 使能一次ADC DMA传输
* @param ndtr: DMA传输的次数
* @retval 无
*/
void adc_dma_enable(uint16_t ndtr)
{
HAL_ADC_Stop(&g_adc_handle); /* 先关闭ADC */
__HAL_DMA_DISABLE(&g_dma_adc_handle); /* 关闭DMA传输 */
g_dma_adc_handle.Instance->CNDTR = ndtr; /* 重设DMA传输数据量 */
__HAL_DMA_ENABLE(&g_dma_adc_handle); /* 开启DMA传输 */
HAL_ADC_Start(&g_adc_handle); /* 重新启动ADC */
ADC_ADCX->CR |= 1 << 2; /* 启动规则转换通道 */
}
该函数的某些部分我们使用寄存器来操作,因为用HAL库操作会对adc_dma_init配置好的某些参数修改。HAL_ADC_Start_DMA函数已经配置好了DMA传输的源地址和目标地址,本函数只需要调用g_dma_adc_handle.Instance->CNDTR=cndtr;语句给DMA_CNDTR寄存器写入要传输的数据量,然后启动DMA就可以传输了。
下面介绍的是ADC DMA采集中断服务函数,函数定义如下:
/**
* @brief ADC DMA采集中断服务函数
* @param 无
* @retval 无
*/
void ADC_ADCX_DMASx_IRQHandler(void)
{
HAL_DMA_IRQHandler(&g_dma_adc_handle);
}
/**
* @brief ADC转换完成的回调函数
* @param 无
* @retval 无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
HAL_ADC_Stop_DMA(&g_adc_handle); /* 关闭DMA转换 */
/* ADC数值转换 */
calc_adc_val(g_adc_dma_buf, g_adc_val, ADC_CH_NUM, ADC_DMA_BUF_SIZE);
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t *)&g_adc_dma_buf,
(uint32_t)(ADC_CH_NUM * ADC_DMA_BUF_SIZE)); /* 再启动DMA转换 */
}
}
在DMA通道1的中断服务函数里,调用HAL库的DMA中断公用处理函数HAL_DMA_IRQHandler,然后在其ADC转换完成回调函数HAL_ADC_ConvCpltCallback中做ADC的数据处理,首先关闭ADC的DMA传输,然后调用函数calc_adc_val取平均值存于g_adc_val中,之后在重新开启ADC的DMA传输。
最后在main.c里面编写如下代码:
int main(void)
{
uint8_t tbuf[40] = {0};
float temp;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_dma_init(); /* 初始化ADC DMA采集 */
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t*)&g_adc_dma_buf,
ADC_DMA_BUF_SIZE); /* 开始DMA数据传输 */
lcd_show_string(10, 10, 200, 32, 32, "STM32", RED);
lcd_show_string(10, 42, 200, 24, 24, "ADC DMA TEST", RED);
lcd_show_string(10, 66, 200, 24, 24, "ATOM@ALIENTEK", RED);
while (1)
{
/* 显示结果 */
sprintf((char *)tbuf, "ADC1_CH8_VAL:%d ", g_adc_val[0]);
lcd_show_string(10, 90, 239, 24, 24, (char*)tbuf, BLUE);
temp = (float)g_adc_val[0] * (3.3 / 4096);
sprintf((char *)tbuf, "ADC1_CH8_VOL:%.2fV ",temp);
lcd_show_string(10, 114, 239, 24, 24, (char*)tbuf, BLUE);
LED0_TOGGLE(); /* 每1000ms,翻转一次LED0 */
delay_ms(1000);
}
}
此部分代码,和单通道ADC采集实验十分相似,只是这里使能了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,这里我们对数组的数据取平均值存于g_adc_val中。在LCD屏显示结果的处理和单通道ADC采集实验一样。
4.4.3.4 下载验证
下载代码后,LED0闪烁,提示程序运行,可以看到LCD显示如图4.4.3.4.1所示:

图4.4.3.4.1 单通道ADC采集(DMA读取)实验测试图
上图中,我们使用1根杜邦线将PC2接到要测试的电压点(如果是外部电源记得共地),即可测出电压值,可测量的电压范围:0~3.3V。LED0闪烁,提示程序运行。
注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.4 多通道ADC采集(DMA读取)实验
本实验我们来学习多通道ADC采集(DMA读取)。本实验使用常规转换组多通道的连续转换模式,并且通过软件触发,即通过对ADCx_CR寄存器的ADSTART位写1启动转换。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
4.4.4.1 ADC寄存器
本实验我们很多的设置和单通道ADC采集(DMA读取)实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集(DMA读取)实验不同设置的ADC_SQRx寄存器进行介绍,其他的配置基本一样的。另外我们用到DMA读取数据,配置上和单通道ADC采集(DMA读取)实验是一样的。
ADC规则序列寄存器有四个(ADC_SQR1~ ADC_SQR4),具体怎么配置,需要看我们用多少个通道,比如本实验我们使用3个通道同时采集ADC数据,具体配置如下:
l ADCx常规序列寄存器1(ADCx_SQR1)
ADCx常规序列寄存器1描述如图4.4.4.1.1所示:

图4.4.4.1.1 ADC_SQR1寄存器
L[3:0]位用于设置规则序列的长度,取值范围:0~15,表示规则序列长度为1~16。本实验使用到3个通道,所以设置这几个位的值为2即可。
SQ1[4:0]~SQ4[4:0]位设置规则组序列的第1~4个转换编号,第5~16个转换编号的设置请查看ADCx_SQR2和ADCx_SQR4寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的常规序列寄存器控制关系汇总表。
下面我们来看看本实验是怎么设置的:SQ1[4:0]位赋值为7、SQ2[4:0]位赋值为8、SQ3[4:0]位赋值为9,即常规序列1到3分别对应的输入通道是7到9。
4.4.4.2 硬件设计
1. 例程功能
使用ADC1采集(DMA读取)通道7\8\9的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PC1\PC2\PC3到你想测量的电压源(0~3.3V),然后通过LCD显示的电压值。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 1.3寸TFTLCD模块
4)ADC1 :通道7 – PC1、通道8 – PC2、通道9– PC3
5)DMA(DMA1通道1)
3. 原理图
ADC和DMA属于STM32G474内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道7\8\9来采集外部电压值,并通过DMA来读取。
4.4.4.3 程序设计
4.4.4.3.1 ADC的HAL库驱动
本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍多通道ADC采集(DMA读取)配置步骤。
多通道ADC采集(DMA读取)配置步骤
1)开启ADCx和通道输入的GPIO时钟,配置该IO口的模拟输入功能
首先开启ADCx的时钟,然后配置GPIO为模拟输入模式。本实验我们默认用到ADC1通道7、8、9,对应IO是PC1、PC2和PC3,它们的时钟开启方法如下:
__HAL_RCC_ADC12_CLK_ENABLE (); /* 使能ADC1时钟 */ __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟 */
IO口模拟输入功能是通过函数HAL_GPIO_Init来配置的。
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_DMA函数启动AD转换器。
4)初始化DMA
通过HAL_DMA_Init函数初始化DMA,包括配置通道,外设地址,存储器地址,传输数据量等。
HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。这个宏定义为__HAL_LINKDMA。
5)使能DMA对应通道中断,配置DMA中断优先级,使能ADC,使能并启动DMA
通过HAL_ADC_Start_DMA函数开启ADC转换。
通过HAL_DMA_Start_IT函数启动DMA读取,使能DMA中断。
通过HAL_NVIC_EnableIRQ函数使能DMA通道中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
DMA的每个通道都有一个中断服务函数,比如DMA1_Channel1的中断服务函数为DMA1_Channel1_IRQHandler。简单的做法就是,在对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清除该中断标志位,本实验的做法就是如此。
除此之外,我们还可以通过调用HAL库所提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后定重新义相关的中断回调处理函数。
4.4.4.3.2 程序流程图

图4.4.4.3.2.1 多通道ADC采集(DMA读取)实验程序流程图
4.4.4.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。本实验的adc.h头文件只是在单通道ADC采集(DMA读取)实验代码的基础上添加了两个通道的宏定义,具体定义如下:
/* ADC及引脚 定义 */
#define ADC_ADCX_CH7_GPIO_PORT GPIOC
#define ADC_ADCX_CH7_GPIO_PIN GPIO_PIN_1
#define ADC_ADCX_CH7_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ADC_ADCX_CH8_GPIO_PORT GPIOC
#define ADC_ADCX_CH8_GPIO_PIN GPIO_PIN_2
#define ADC_ADCX_CH8_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ADC_ADCX_CH9_GPIO_PORT GPIOC
#define ADC_ADCX_CH9_GPIO_PIN GPIO_PIN_3
#define ADC_ADCX_CH9_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */
#define ADC_ADCX ADC1
#define ADC_ADCX_CH7 ADC_CHANNEL_7
#define ADC_ADCX_CH7_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
#define ADC_ADCX_CH8 ADC_CHANNEL_8
#define ADC_ADCX_CH8_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
#define ADC_ADCX_CH9 ADC_CHANNEL_9
#define ADC_ADCX_CH9_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
/* ADC单通道/多通道 DMA采集 DMA通道相关 定义 */
#define ADC_ADCX_DMASx DMA1_Channel1
#define ADC_ADCX_DMASx_REQUEST DMA_REQUEST_ADC1
#define ADC_ADCX_DMASx_IRQn DMA1_Channel1_IRQn
#define ADC_ADCX_DMASx_IRQHandler DMA1_Channel1_IRQHandler
#define ADC_DMA_BUF_SIZE 50 /* 每个ADC通道的 DMA采集次数 */
#define ADC_CH_NUM 3 /* ADC 通道数量 */
/* ADC DMA 原始ADC数值 */
extern uint16_t g_adc_dma_buf[ADC_CH_NUM * ADC_DMA_BUF_SIZE];
extern uint16_t g_adc_val[ADC_CH_NUM]; /* 滤波后的ADC数值缓存数组 */
extern ADC_HandleTypeDef g_adc_handle; /* ADC句柄 */
在adc.h头文件中,我们添加了ADC1通道7、8相关的宏定义。其中宏ADC_CH_NUM表示ADC采集的通道个数,宏ADC_DMA_BUF_SIZE表示每个通道采集的次数。
下面开始介绍adc.c的函数,首先看ADC的N通道(3通道) DMA读取初始化函数,其定义如下:
/**
* @brief ADC DMA读取 初始化函数
* @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
* @param par : 外设地址
* @param mar : 存储器地址
* @retval 无
*/
void adc_dma_init(void)
{
__HAL_RCC_DMAMUX1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA配置 */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 设置DMA通道 */
g_dma_adc_handle.Init.Request= ADC_ADCX_DMASx_REQUEST; /* 设置DMA请求 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;/* 外设到存储器模式 */
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
/* 外设数据长度:16位 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_CIRCULAR; /* 外设流控模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_adc_handle); /* 初始化DMA */
adc_init(); /* 初始化ADC */
/* 配置对应ADC通道 */
adc_channel_set(&g_adc_handle , ADC_ADCX_CH7, ADC_REGULAR_RANK_1,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle , ADC_ADCX_CH8, ADC_REGULAR_RANK_2,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle , ADC_ADCX_CH9, ADC_REGULAR_RANK_3,
ADC_SAMPLETIME_640CYCLES_5);
__HAL_LINKDMA(&g_adc_handle,DMA_Handle,g_dma_adc_handle);
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn); /* 使能DMA中断 */
}
adc_nch_dma_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。大部分代码和单通道ADC采集(DMA读取)实验一样,下面来看看该函数的代码内容。
第一部分,使能ADC、DMA的时钟。
第二部分,设置ADC采集通道对应IO引脚工作模式,这里用到3个通道。
第三部分,初始化DMA、ADC,配置ADC时钟预分频系数为4,得到ADC的输入时钟频率是42.5MHz。
第四部分,通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
第五部分,配置ADC通道,这里有3个通道需要配置。
第六部分,配置DMA通道请求中断优先级,并使能该中断。
第七部分,启动DMA并开启DMA中断,以及启动ADC并通过DMA传输转换结果。
另外在函数HAL_ADC_MspInit里存放使能时钟、GPIO初始化相关的代码。
接下来我们看ADC采集中断回调函数,其定义如下:
/**
* @brief ADC DMA采集中断服务函数
* @param 无
* @retval 无
*/
void ADC_ADCX_DMASx_IRQHandler(void)
{
HAL_DMA_IRQHandler(&g_dma_adc_handle);
}
/**
* @brief ADC转换完成的回调函数
* @param 无
* @retval 无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
HAL_ADC_Stop_DMA(&g_adc_handle); /* 关闭DMA转换 */
/* ADC数值转换 */
calc_adc_val(g_adc_dma_buf, g_adc_val, ADC_CH_NUM, ADC_DMA_BUF_SIZE);
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t *)&g_adc_dma_buf,
(uint32_t)(ADC_CH_NUM * ADC_DMA_BUF_SIZE)); /* 再启动DMA转换 */
}
}
在中断服务函数里会调用HAL库所提供的DMA中断公用处理函数HAL_DMA_IRQHandler,然后再回调函数HAL_ADC_ConvCpltCallback中,先关闭DMA传输,然后进行3个通道的ADC平均值计算,计算完平均值后再重新开启DMA。ADC平均值计算函数在单通道ADC采集实验已经介绍过,这里不再赘述。
在main.c里面编写如下代码:
int main(void)
{
uint8_t tbuf[40] = {0};
float temp;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_dma_init(); /* 初始化ADC DMA采集 */
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t*)&g_adc_dma_buf,
ADC_DMA_BUF_SIZE); /* 开始DMA数据传输 */
lcd_show_string(10, 10, 200, 32, 32, "STM32", RED);
lcd_show_string(10, 42, 200, 24, 24, "ADC DMA TEST", RED);
lcd_show_string(10, 66, 200, 24, 24, "ATOM@ALIENTEK", RED);
while (1)
{
/* 显示结果 */
sprintf((char *)tbuf, "ADC1_CH7_VAL:%d ", g_adc_val[0]);
lcd_show_string(10, 96, 239, 16, 16, (char*)tbuf, BLUE);
temp = (float)g_adc_val[0] * (3.3 / 4096);
sprintf((char *)tbuf, "ADC1_CH7_VOL:%.2f ",temp);
lcd_show_string(10, 116, 239, 16, 16, (char*)tbuf, BLUE);
sprintf((char *)tbuf, "ADC1_CH8_VAL:%d ", g_adc_val[1]);
lcd_show_string(10, 146, 239, 16, 16, (char*)tbuf, BLUE);
temp = (float)g_adc_val[1] * (3.3 / 4096);
sprintf((char *)tbuf, "ADC1_CH8_VOL:%.2f ",temp);
lcd_show_string(10, 166, 239, 16, 16, (char*)tbuf, BLUE);
sprintf((char *)tbuf, "ADC1_CH9_VAL:%d ", g_adc_val[2]);
lcd_show_string(10, 196, 239, 16, 16, (char*)tbuf, BLUE);
temp = (float)g_adc_val[2] * (3.3 / 4096);
sprintf((char *)tbuf, "ADC1_CH9_VOL:%.2f ",temp);
lcd_show_string(10, 216, 239, 16, 16, (char*)tbuf, BLUE);
LED0_TOGGLE(); /* 每1000ms,翻转一次LED0 */
delay_ms(1000);
}
}
main函数中,我们初始化了ADC和DMA,以及开启DMA的传输,然后在LCD模块上显示一些提示信息后,每隔100ms刷新一次ADC1通道7、8、9的值,并显示读到的ADC值(数字量),以及它们转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。
4.4.4.4 下载验证
下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图4.4.4.4.1所示:

图4.4.4.4.1 多通道ADC采集(DMA读取)实验测试图
使用ADC1采集(DMA读取)通道7\8\9的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PC1\PC2\PC3到你想测量的电压源(0~3.3V),如果测量外部电源需要共地。
这3个通道可以同时测量不同测试点的电压,只需要用杜邦线分别接到不同的电压测试点即可。注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.5 内部温度传感器实验
本实验我们来学习STM32G474的内部温度传感器并使用它来读取温度值,然后在LCD模块上显示出来。下面先带大家来了解一下STM32的内部温度传感器。
4.4.5.1 内部温度传感器简介
STM32有一个内部的温度传感器,可以用来测量CPU及周围的温度(内部温度传感器更适合于检测温度的变化,需要测量精确温度的情况下,建议使用外置传感器)。对于STM32G474来说,该温度传感器在内部和ADC1_IN16输入通道相连接,此通道把传感器输出的电压转换成数字值,温度传感器模拟输入采样时间不能小于10us。STM32内部温度传感器支持的温度范围为:-40~125度。精度为±1.5℃左右。
STM32内部温度传感器的使用很简单,只要设置一下内部ADC,并激活其内部温度传感器通道就差不多了。关于ADC的设置,我们在上一章已经进行了详细的介绍,这里就不再多说。接下来我们介绍一下和温度传感器设置相关的两个地方。
第一个地方,我们要使用STM32的内部温度传感器,必须先激活ADC的内部通道,通过ADC1_CCR的VSENSESEL位设置。设置该位为1则启用内部温度传感器。
第二个地方,STM32G4的内部温度传感器连接在ADC1的通道16上,所以,我们在设置好ADC1之后只要读取通道16的值,就是温度传感器返回来的电压值了。根据这个值,我们就可以计算出当前温度。计算公式如下:

上式中:
TS_CAL1 是温度传感器在30℃时的校准值,固定保存在芯片内部的:0X1FFF 75A8 ~ 0X1FFF 75A9这两个地址(16位)。
TS_CAL2 是温度传感器在130℃时的校准值,固定保存在芯片内部的:0X1FFF 75CA ~ 0X1FFF 75CB这两个地址(16位)。
TS_DATA:ADC1通道16读取到的当前温度传感器转换值,但是需要注意上面的公式的TS_CAL1和TS_CAL2值是在VREF+为3.0V时所测试的,但是在我们的硬件上我们是将VREF+连接至3.3V,所以需要必须对TS_DATA进行转换,即TS_DATA = TS_DATA*3.3/3.0。
所以在本实验中,最终的温度计算公式如下:

利用以上公式,我们就可以方便的计算出当前温度传感器的温度了。
4.4.5.2 硬件设计
1. 例程功能
通过ADC的通道16读取STM32G474内部温度传感器的电压值,并将其转换为温度值,显示在LCD屏上。LED0闪烁用于提示程序正在运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子1.3寸TFTLCD模块
4)ADC1 通道16
5)内部温度传感器
3. 原理图
ADC和内部温度传感器都属于STM32G474内部资源,实际上我们只需要软件设置就可以正常工作,我们需要用到TFTLCD模块显示结果。
4.4.5.3 程序设计
4.4.5.3.1 ADC的HAL库驱动
本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍读取内部温度传感器ADC值的配置步骤。
读取STM32内部温度传感器ADC值的配置步骤
1)开启ADC时钟
通过__HAL_RCC_ADC12_CLK_ENABLE函数开启ADC1的时钟。
2)设置ADC,开启内部温度传感器
调用HAL_ADC_Init函数来设置ADC1时钟分频系数、分辨率、模式、扫描方式等信息。
注意:该函数会调用:HAL_ADC_MspInit回调函数来完成对ADC底层的初始化,包括:ADC1时钟使能、ADC1时钟源的选择等。
3)配置ADC通道并启动AD转换器
调用HAL_ADC_ConfigChannel()函数配置ADC1通道16,根据需求设置通道、规则序列、采样时间等。然后通过HAL_ADC_Start函数启动AD转换器。
4)读取ADC值,计算温度。
这里选择查询方式读取,在读取ADC值之前需要调用HAL_ADC_PollForConversion等待上一次转换结束。然后就可以通过HAL_ADC_GetValue来读取ADC值。最后根据上面介绍的公式计算出温度传感器的温度值。
4.4.5.3.2 程序流程图

图4.4.5.3.2.1 内部温度传感器实验程序流程图
4.4.5.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。内部温度传感器驱动源码包括两个文件:adc.c和adc.h。首先看adc.h头文件,其定义如下:
/* ADC及引脚 定义 */
#define ADC_ADCX ADC1
/* 内部温度传感器专用通道 */
#define ADC_TEMPSENSOR_CHX ADC_CHANNEL_TEMPSENSOR_ADC1
#define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
ADC_CHANNEL_TEMPSENSOR_ADC1就是内部温度传感器的通道宏定义,它实际上连接到了ADC1的通道16。
下面我们直接介绍与内部温度传感器相关的adc.c的代码,首先是ADC内部温度传感器初始化函数,其定义如下:
/**
* @brief ADC内部温度传感器 初始化函数
* @param 无
* @retval 无
*/
void adc_temperature_init(void)
{
ADC_ADCX_CHY_CLK_ENABLE();
g_adc_handle.Instance = ADC_ADCX;
/* 4分频,ADCCLK = SYSCLK/4 = 170/4 = 42.5Mhz */
g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_handle.Init.GainCompensation = 0; /* 不需要增益补偿 */
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个转换在规则序列中 也就是只转换规则序列1 */
g_adc_handle.Init.NbrOfConversion = 1;
g_adc_handle.Init.DiscontinuousConvMode = DISABLE; /* 禁止不连续采样模式*/
g_adc_handle.Init.NbrOfDiscConversion = 0; /* 不连续采样通道数0*/
g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
g_adc_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
g_adc_handle.Init.DMAContinuousRequests = DISABLE; /* 关闭DMA请求 */
/* 数据溢出时覆盖ADC数据 */
g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
g_adc_handle.Init.OversamplingMode = DISABLE; /* 禁用过采样功能 */
HAL_ADC_Init(&g_adc_handle); /* 初始化 */
/* 以单端模式运行ADC校准 */
HAL_ADCEx_Calibration_Start(&g_adc_handle, ADC_SINGLE_ENDED);
}
内部温度传感器相关的ADC初始化步骤与普通ADC类似,只是采集通道换成了内部温度传感器专用的通道,这里不再赘述。
下面讲解一下获取内部温度传感器温度值函数,其定义如下:
/**
* @brief 获取内部温度传感器温度值
* @param 无
* @retval 温度值(扩大了100倍,单位:℃)
*/
short adc_get_temperature(void)
{
uint32_t adcx;
short result;
double temperature;
float temp = 0;
uint16_t ts_cal1, ts_cal2;
ts_cal1 = *(volatile uint16_t *)(0X1FFF75A8); /* 获取TS_CAL1 */
ts_cal2 = *(volatile uint16_t *)(0X1FFF75CA); /* 获取TS_CAL2 */
temp = (float)((130.0 - 30.0) / (ts_cal2 - ts_cal1)); /* 获取比例因子 */
/* 读取内部温度传感器通道,20次取平均 */
adcx = adc_get_result_average(ADC_TEMPSENSOR_CHX, 20);
/* ts_cal1和ts_cal2是在VREF+为3.0V测试的,所以需要将adcx进行转换 */
adcx *= (3.3 / 3.0);
temperature = (float)(temp * (adcx - ts_cal1) + 30); /* 转化为电压值 */
result = temperature *= 100; /* 扩大100倍 */
return result;
}
该函数先是调用前面ADC实验章节写好的adc_get_result_average函数取获取通道ch的转换值,然后通过温度转换公式计算出当前的温度值。
在main.c里面编写如下代码:
int main(void)
{
short temp;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_temperature_init(); /* 初始化ADC内部温度传感器采集 */
lcd_show_string(10, 10, 200, 24, 24, "STM32", RED);
lcd_show_string(10, 40, 200, 24, 24, "Temperature TEST", RED);
lcd_show_string(10, 70, 200, 24, 24, "ATOM@ALIENTEK", RED);
lcd_show_string(10, 100, 200, 16, 16, "TEMPERATE: 00.00C", BLUE);
while (1)
{
temp = adc_get_temperature(); /* 得到温度值 */
if (temp < 0)
{
temp = -temp;
/* 显示负号 */
lcd_show_string(10 + 10 * 8, 100, 16, 16, 16, "-", BLUE);
}
else
{
/* 无符号 */
lcd_show_string(10 + 10 * 8, 100, 16, 16, 16, " ", BLUE);
}
/* 显示整数部分 */
lcd_show_xnum(10 + 11 * 8, 100, (temp / 100), 2, 16, 0, BLUE);
/* 显示小数部分 */
lcd_show_xnum(10 + 14 * 8, 100, (temp % 100), 2, 16, 0X80, BLUE);
LED0_TOGGLE(); /* LED0闪烁,提示程序运行 */
delay_ms(250);
}
}
该部分的代码逻辑很简单,先是得到温度值,再根据温度值判断正负值,来显示温度符号,再显示整数和小数部分。
4.4 . 5.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图4.4.5.4.1所示:

图4.4.5.4.1 内部温度传感器实验测试图
大家可以看看显示温度值与实际是否相符合(因为芯片会发热,所以一般会比实际温度偏高)。