第4章 电机控制相关的STM32外设(下)
4.4 ADC
本小节我们将介绍STM32F407的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,可以将外部的模拟信号转换为数字信号。
STM32F407IGT6芯片拥有3个ADC,都可以独立工作,其中ADC1和ADC2还可以组成双重模式(提高采样率)。这些ADC都是12位逐次逼近型的模拟数字转换器,有19个通道,可测量16个外部信号源、2个内部信号源和Vbat通道的信号。ADC中的各个通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16位数据寄存器中。
STM32F407的ADC主要特性我们可以总结为以下几条:
1、12位分辨率
2、转换结束、注入转换结束和发生模拟看门狗事件时产生中断
3、单次和连续转换模式
4、自校准
5、带内嵌数据一致性的数据对齐
6、采样间隔可以按通道分别编程
7、规则转换和注入转换均有外部触发选项
8、间断模式
9、双重模式(带2个或以上ADC的器件)
10、ADC供电要求:2.4V到3.6V
11、ADC输入范围:VREF–VINVREF+
12、规则通道转换期间有DMA请求产生
下面来介绍ADC的框图:
图4.4.1.1 ADC框图
图4.4.1.1中,我们按照ADC的配置流程标记了七处位置,分别如下:
- 输入电压 在前面ADC的主要特性也对输入电压有所提及,ADC输入范围VREF–≤VIN≤VREF+,由VREF–、VREF+、VDDA和VSSA决定的。下面看一下这几个参数的关系,如图4.4.1.2所示:
-
图4.4.1.2 参数关系图
从上图可以知道,VDDA和VREF+接VCC3.3,而VSSA和VREF-是接地,所以ADC的输入范围即0~3.3V。 - 输入通道
在确定好了ADC输入电压后,如何把外部输入的电压输送到ADC转换器中呢?在这里引入了通道概念,在前面也提及到了ADC1和ADC2都有16个外部通道和3个内部通道。外部通道对应的是上图中的ADCx_IN0、ADCx_IN1…ADCx_IN15。ADC1的通道16就是内部通道,连接到芯片内部的温度传感器,通道17连接到Vrefint,通道18连接到VBAT,而ADC2和ADC3的通道16~18都是连接到内部的VSS。具体的ADC通道表见表4.4.1.1所示:
ADC1 | IO | ADC2 | IO | ADC3 | IO |
通道0 | PA0 | 通道0 | PA0 | 通道0 | PA0 |
通道1 | PA1 | 通道1 | PA1 | 通道1 | PA1 |
通道2 | PA2 | 通道2 | PA2 | 通道2 | PA2 |
通道3 | PA3 | 通道3 | PA3 | 通道3 | PA3 |
通道4 | PA4 | 通道4 | PA4 | 通道4 | PF6 |
通道5 | PA5 | 通道5 | PA5 | 通道5 | PF7 |
通道6 | PA6 | 通道6 | PA6 | 通道6 | PF8 |
通道7 | PA7 | 通道7 | PA7 | 通道7 | PF9 |
通道8 | PB0 | 通道8 | PB0 | 通道8 | PF10 |
通道9 | PB1 | 通道9 | PB1 | 通道9 | PF3 |
通道10 | PC0 | 通道10 | PC0 | 通道10 | PC0 |
通道11 | PC1 | 通道11 | PC1 | 通道11 | PC1 |
通道12 | PC2 | 通道12 | PC2 | 通道12 | PC2 |
通道13 | PC3 | 通道13 | PC3 | 通道13 | PC3 |
通道14 | PC4 | 通道14 | PC4 | 通道14 | PF4 |
通道15 | PC5 | 通道15 | PC5 | 通道15 | PF5 |
通道16 | 连接内部温度传感器 | 通道16 | 连接内部VSS | 通道16 | 连接内部VSS |
通道17 | 连接内部Vrefint | 通道17 | 连接内部VSS | 通道17 | 连接内部VSS |
通道18 | 连接内部VBAT | 通道18 | 连接内部VSS | 通道18 | 连接内部VSS |
表4.4.1.1 ADC通道表
③ 转换顺序
当ADC的多个通道以任意顺序进行转换就诞生了成组转换,这里有两种成组转换类型:规则组和注入组。规则组就是图中的规则通道,注入组就是图中的注入通道。为了避免大家对输入通道,以及规则通道和注入通道的理解混淆,后面规则通道以规则组来代称,注入通道以注入组来代称。
规则组最多允许16个输入通道进行转换,而注入组最多允许4个输入通道进行转换。这里讲解一下规则组和注入组。
规则组(规则通道)
规则组,按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最多也是规则组。
注入组(注入通道)
注入组,按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。
为了便于理解,下面看一下规则组和注入组的执行优先级对比图,如图4.4.1.3所示:
图4.4.1.3 规则组和注入组的执行优先级对比图
了解了规则组和注入组的概念后,下面来看看它们的转换顺序,即转换序列。转换序列可以分为规则序列和注入序列。下面分别来介绍它们。
规则序列
规则组最多允许16个输入通道进行转换,那么就需要设置通道转换的顺序,即规则序列。规则序列寄存器有3个,分别为SQR1、SQR2和SQR3。SQR3控制规则序列中的第1个到第6个转换;SQR2控制规则序列中第7个到第12个转换;SQR1控制规则序列中第13个到第16个转换。规则序列寄存器控制关系汇总如表4.4.1.2所示:
规则序列寄存器控制关系汇总 | |||
寄存器 | 寄存器位 | 功能 | 取值 |
SQR3 | SQ1 [ 4 : 0 ] | 设置第1个转换的通道 | 输入通道0~18 |
SQ2 [ 4 : 0 ] | 设置第2个转换的通道 | 输入通道0~18 | |
SQ3 [ 4 : 0 ] | 设置第3个转换的通道 | 输入通道0~18 | |
SQ4 [ 4 : 0 ] | 设置第4个转换的通道 | 输入通道0~18 | |
SQ5 [ 4 : 0 ] | 设置第5个转换的通道 | 输入通道0~18 | |
SQ6 [ 4 : 0 ] | 设置第6个转换的通道 | 输入通道0~18 | |
SQR2 | SQ7 [ 4 : 0 ] | 设置第7个转换的通道 | 输入通道0~18 |
SQ8 [ 4 : 0 ] | 设置第8个转换的通道 | 输入通道0~18 | |
SQ9 [ 4 : 0 ] | 设置第9个转换的通道 | 输入通道0~18 | |
SQ10 [ 4 : 0 ] | 设置第10个转换的通道 | 输入通道0~18 | |
SQ11 [ 4 : 0 ] | 设置第11个转换的通道 | 输入通道0~18 | |
SQ12 [ 4 : 0 ] | 设置第12个转换的通道 | 输入通道0~18 | |
SQR1 | SQ13 [ 4 : 0 ] | 设置第13个转换的通道 | 输入通道0~18 |
SQ14 [ 4 : 0 ] | 设置第14个转换的通道 | 输入通道0~18 | |
SQ15 [ 4 : 0 ] | 设置第15个转换的通道 | 输入通道0~18 | |
SQ16 [ 4 : 0 ] | 设置第16个转换的通道 | 输入通道0~18 | |
SQL [ 3 : 0 ] | 设置规则序列要转换的通道数 | 1~16 |
表4.4.1.2 规则序列寄存器控制关系汇总表
从上表可以知道,当我们想设置ADC的某个输入通道在规则序列的第1个转换,只需要把相应的输入通道号的值写入SQR3寄存器中的SQ1[4:0]位即可。例如想让输入通道5先进行转换,那么就可以把5这个数值写入SQ1[4:0]位。如果还想让输入通道8在第2个转换,那么就可以把8这个数值写入SQ2[4:0]位。最后还要设置你的这个规则序列的输入通道个数,只需把输入通道个数写入SQR1的SQL[3:0]位。注意:写入0到SQL[3:0]位,表示这个规则序列有1个输入通道的意思,而不是0个输入通道。
注入序列
注入序列,跟规则序列差不多,决定的是注入组的顺序。注入组最大允许4个通道输入,它的注入序列由JSQR寄存器配置。注入序列寄存器JSQR控制关系如表4.4.1.3所示:
注入序列寄存器控制关系汇总 | |||
寄存器 | 寄存器位 | 功能 | 取值 |
JSQR | JSQ1 [ 4 : 0 ] | 设置第1个转换的通道 | 通道1~4 |
JSQ2 [ 4 : 0 ] | 设置第2个转换的通道 | 通道1~4 | |
JSQ3 [ 4 : 0 ] | 设置第3个转换的通道 | 通道1~4 | |
JSQ4 [ 4 : 0 ] | 设置第4个转换的通道 | 通道1~4 | |
JL [ 1 : 0 ] | 需要转换多少个通道 | 1~4 |
表4.4.1.3 注入序列寄存器控制关系汇总表
注入序列有多少个输入通道,只需要把输入通道个数写入到JL [ 1 : 0 ]位,范围是0~3。注意:写入0表示这个注入序列有一个输入通道,而不是0个输入通道。这个内容很简单。编程时容易犯错的是注入序列的转换顺序问题,下面给大家讲解一下。
如果JL[ 1 : 0 ]位的值小于3,即设置注入序列要转换的通道个数小于4,则注入序列的转换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。例如:JL [ 1 : 0 ]=10、JSQ4 [ 4 : 0 ]= 00100、JSQ3 [ 4 : 0 ]=、JSQ2 [ 4 : 0 ]= 00111、JSQ1 [ 4 : 0 ]= 00010,意味着这个注入序列的转换顺序是:7、3、4,而不是2、7、3。如果JL[ 1 : 0 ]=00,那么转换顺序是从JSQ4 [ 4 : 0 ]开始。
④ 触发源
在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC的触发转换有两种方法:分别是通过ADON位或外部事件触发转换。
(1)ADON位触发转换
当ADC_CR2寄存器的ADON位为1时,再独立给ADON位写1(其它位不能一起改变,这是为了防止误触发),这时会启动转换。这种控制ADC启动转换的方式非常简单。
(2)外部触发转换
另一种方法是通过外部事件触发转换,如定时器捕获、EXTI线和软件触发,可以分为规则组外部触发和注入组外部触发。
规则组外部触发使用方法是:通过EXTEN[1:0]位使能规则通道的外部触发,并且通过EXTSEL[3:0]位选择规则组启动转换的触发源。
注入组外部触发使用方法是:通过JEXTEN [1:0]位使能注入通道的外部触发并且通过JEXTSEL[3:0]位选择注入组启动转换的触发源。
ADC1和ADC2的触发源是一样的,ADC3的触发源和ADC1/2有所不同,这个需要注意。
⑤ 转换时间
(1)ADC时钟
在学习转换时间之前,我们先来了解ADC时钟。从标号⑤框出来部分可以看到ADC时钟是要经过ADC预分频器的,那么ADC的时钟源是什么?ADC预分频器的分频系数可以设置的范围又是多少?以及ADC时钟频率的最大值又是多少?下面将为大家解答。
ADC的输入时钟是由APB2经过分频产生,分频系数是由RCC_CFGR寄存器中的PPRE2[2:0]进行设置的,可选择2/4/6/8/16分频。需要注意的是,STM32F407的ADC的输入时钟频率最大值是36MHz,如果超过这个值将会导致ADC的转换结果准确度下降。
一般我们设置APB2为84MHz。为了不超过ADC的最大输入时钟频率36MHz,我们设置ADC的预分频器分频系数为4,就可以得到ADC的输入时钟频率为84MHz/4,即21MHz。例程中,我们也是如此设置的。
(2)转换时间
STM32F407的ADC总转换时间的计算公式如下:
TCONV = 采样时间 + 逐次逼近时间(TSAR)
采样时间可通过ADC_SMPR1和ADC_SMPR2寄存器中的SMP[2:0]位编程,ADC_SMPR1控制的是通道10~18,ADC_SMPR2控制的是通道0~9。每个输入通道都支持通过编程来选择不同的采样时间,采样时间可选的范围如下:
- SMP = 000:3个ADC时钟周期
- SMP = 001:15个ADC时钟周期
- SMP = 010:28个ADC时钟周期
- SMP = 011:56个ADC时钟周期
- SMP = 100:84个ADC时钟周期
- SMP = 101:112个ADC时钟周期
- SMP = 110:144个ADC时钟周期
- SMP = 111:480个ADC时钟周期
逐次逼近时间(TSAR)是由分辨率决定的,分辨率通过对ADCx_CR1寄存器的RES[1:0]位进行编程,可将分辨率配置为12位、10位、8位、6位。逐次逼近时间和分辨率的对应关系如下所示:
● 12 位: 12 个ADC时钟周期
● 10 位: 10个ADC时钟周期
● 8 位 : 8个ADC时钟周期
● 6 位 : 6个ADC时钟周期
可以看出,采样时间最小是3个时钟周期,逐次逼近时间最小是6个时钟周期。下面以我们例程的ADC时钟配置为例,来给大家计算一下ADC的最短转换时间,计算过程如下:
TCONV =3个ADC时钟周期 +12个ADC时钟周期 =15个ADC时钟周期
例程中,APB2的时钟是84MHz,经过ADC时钟预分频器的4分频后,ADC时钟频率为21MHz。代入上式可得到:
⑥ 数据寄存器
ADC转换完成后的数据输出寄存器。根据转换组的不同,规则组的完成转换的数据输出到ADC_DR寄存器,注入组的完成转换的数据输出到ADC_JDRx寄存器。假如是使用双重或三重模式,规则组的数据会存放在ADC_CDR寄存器。下面给大家简单介绍一下这三个寄存器。
(1)ADC规则数据寄存器(ADC_DR)
ADC规则组数据寄存器ADC_DR是一个32位的寄存器,独立模式时只使用到该寄存器低16位保存ADC1/2/3的规则转换数据。
因为ADC的精度是12位的,ADC_DR寄存器无论高16位还是低16,存放数据的位宽都是16位的,所以需要选择数据对齐方式。由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。
细心的朋友可能发现,规则组最多有16个输入通道,而ADC规则数据寄存器只有一个,如果一个规则组用到好几个通道,数据怎么读取?如果使用多通道转换,那么这些通道的数据也会存放在DR里面,按照规则组的顺序,上一个通道转换的数据,会被下一个通道转换的数据覆盖掉,所以当通道转换完成后要及时把数据取走。比较常用的方法是使用DMA模式。当规则组的通道转换结束时,就会产生DMA请求,这样就可以及时把转换的数据搬运到用户指定的目的地址存放。注意:只有ADC1和ADC3可以产生DAM请求,而由ADC2转换的数据可以通过双ADC模式,利用ADC1的DMA功能传输。
(2)双重和三重模式ADC规则数据寄存器(ADC_CDR)
双重和三重模式ADC规则组数据寄存器ADC_CDR是一个32位的寄存器,该寄存器的高16位用于保存ADC2转换的数据,低16位用于保存ADC1转换的数据。
(3)ADC注入数据寄存器x(ADC_JDRx)(x=1~4)
ADC注入数据寄存器有4个,注入组最多有4个输入通道,刚好每个通道都有自己对应的数据寄存器。ADC_JDRx寄存器是32位的,低16位有效,高16位保留,数据也同样需要选择对齐方式。也是由ADC_CR2寄存器的ALIGN位设置数据对齐方式,可选择:右对齐或者左对齐。
⑦ 中断
ADC中断可分为三种:规则组转换结束中断、注入组转换结束中断、设置了模拟看门狗状态位中断。它们都有独立的中断使能位,分别由ADC_CR寄存器的EOCIE、JEOCIE、AWDIE位设置,对应的标志位分别是EOC、JEOC、AWD。
模拟看门狗中断
模拟看门狗中断发生条件:首先通过ADC_LTR和ADC_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。
DMA请求
规则组和注入组的转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据存储在内存里面,防止读取不及时数据被覆盖。
⑧ 单次转换模式和连续转换模式
单次转换模式和连续转换模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
(1)单次转换模式
通过将ADC_CR2寄存器的CONT位置0选择单次转换模式。该模式下,ADC只执行一次转换,由ADC_CR2寄存器的ADON位启动(只适用于规则组),也可以通过外部触发启动(适用于规则组或注入组)。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DR寄存器中、EOC(转换结束)标志位被置1、如果设置了EOCIE位,则产生中断,然后ADC停止。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DRJx寄存器中、JEOC(转换结束)标志位被置1、如果设置了JEOCIE位,则产生中断,然后ADC停止。
(2)连续转换模式
通过将ADC_CR2寄存器的CONT位置1选择连续转换模式。该模式下,ADC完成上一个通道的转换后会马上自动地启动下一个通道的转换,由ADC_CR2寄存器的ADON位启动,也可以通过外部触发启动。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DR寄存器中、EOC(转换结束)标志位被置1、如果设置了EOCIE位,则产生中断。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16位ADC_DRJx寄存器中、JEOC(转换结束)标志位被置1、如果设置了JEOCIE位,则产生中断。
⑨ 扫描模式
扫描模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
可以通过ADC_CR1寄存器的SCAN位配置是否使用扫描模式。如果选择扫描模式,ADC会扫描所有被ADC_SQRx寄存器或ADC_JSQR选中的所有通道,并对规则组或者注入组的每个通道执行单次转换,然后停止转换。但如果还设置了CONT位,即选择连续转换模式,那么转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。
如果设置了DMA位,在每次EOC后, DMA控制器把规则组通道的转换数据传输到SRAM中。而注入通道转换的数据总是存储在ADC_JDRx寄存器中。
到这里我们基本上介绍了ADC的大多数基础的知识点,其它知识后面用到会继续补充,如果还有不懂的内容,请参考《STM32F4xx参考手册(中文版).pdf》的第11章。
4.4.2单通道ADC采集实验
本实验我们来学习单通道ADC采集。本实验使用规则组单通道的单次转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。下面先带大家来了解本实验要配置的寄存器。
4.4.2.1 ADC寄存器
这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
- ADC控制寄存器1(ADC_CR1)ADC控制寄存器1描述如图4.4.2.1.1所示:
-
图4.4.2.1.1 ADC_CR1寄存器
SCAN位用于选择是否使用扫描模式。本实验我们使用单通道采集,所以没必要选择扫描模式,该位置0即可。 - ADC控制寄存器2(ADC_CR2)ADC控制寄存器2描述如图4.4.2.1.2所示:
-
图4.4.2.1.2 ADC_CR2寄存器
该寄存器我们针对性的介绍一些位:ADON位用于打开或关闭AD转换器,还可以用于触发ADC转换。CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用单次转换模式,所以CONT位置0即可。ALIGN用于设置数据对齐,我们使用右对齐,所以该位设置为0。EXTEN[1:0]位用于设置外部触发边沿,本实验使用软件触发,所以设置为00即可。SWSTART位用于开始规则通道的转换,即软件触发转换。 - ADC采样事件寄存器1(ADC_SMPR1)ADC采样事件寄存器1描述如图4.4.2.1.3所示:
-
图4.4.2.1.3 ADC_SMPR1寄存器 - ADC采样事件寄存器2(ADC_SMPR2)ADC采样事件寄存器2描述如图4.4.2.1.4所示:
-
图4.4.2.1.4 ADC_SMPR2寄存器
ADC采样时间设置需要由两个寄存器设置,ADC_SMPR1和ADC_SMPR2,分别设置通道10~18和通道0~9的采样时间,每个通道用3个位设置。可以看出ADC的每个通道的采样时间是支持单独设置的。
一般每个要转换的通道,采样时间建议尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率,看大家怎么衡量选择了。本实验中,我们设置采样时间是480个周期,ADC精度是12位。结合前面介绍过的转换时间公式:
TCONV = 采样时间 + 12个周期
以及例程中,APB2的时钟是84MHz,经过ADC时钟预分频器的4分频后,ADC时钟频率为21MHz。代入上式可得到:-
由上式可得,ADC的转换时间大约是23.4us。- ADC规则序列寄存器1(ADC_SQR1)ADC规则序列寄存器共有3个,这几个寄存器的功能都差不多,这里我们仅介绍一下ADC规则序列寄存器1(ADC_SQR1),描述如图4.4.2.1.5所示:
-
图4.4.2.1.5 ADC_SQR1寄存器
L[3:0]用于设置规则组序列的长度,取值范围:0~15,表示规则组的长度是1~16。本实验只用了1个输入通道,所以L[3:0]位设置为0000即可。
SQ13[4:0]~SQ16[4:0]位设置规则组序列的第13~16个转换编号,第1~12个转换编号的设置请查看ADC_SQR2和ADC_SQR3寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的规则序列寄存器控制关系汇总表。
本实验我们使用单通道,ADC1通道3,所以规则组序列里只有一个输入通道,我们将ADC_SQR3寄存器的SQ1[4:0]位的值设置为3即可。 - ADC规则数据寄存器(ADC_DR)ADC规则数据寄存器描述如图4.4.2.1.6所示:
-
图4.4.2.1.6 ADC_DR寄存器
在规则序列中AD转换结果都将被存在这个寄存器里面,而注入通道的转换结果被保存在ADC_JDRx里面。该寄存器的数据可以通过ADC_CR2的ALIGN位设置左对齐还是右对齐。在读取数据的时候要注意。 - ADC状态寄存器(ADC_SR)ADC状态寄存器描述如图4.4.2.1.7所示:
- 图4.4.2.1.7ADC_SR寄存器
该寄存器保存了ADC转换时的各种状态。本实验我们通过EOC位的状态来判断ADC转换是否完成,如果查询到EOC位被硬件置1,就可以从ADC_DR寄存器中读取转换结果,否则需要等待转换完成。
至此,本章要用到的ADC相关寄存器全部介绍完毕了,对于未介绍的部分,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》第11章相关内容。
4.4.2.2硬件设计
1. 例程功能
采集ADC1通道3(PA3)上的电压,并在LCD模块上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。我们使用杜邦线将PA3引脚接到DC 0~3.3V的电源上(外部电源记得共地),即可采集到ADC数据。ADC采集到的数据和转换后的电压值将在LCD屏中显示。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1 :通道3 – PA3
3. 原理图
ADC属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验通过ADC1的通道3(PA3)来采集外部电压值,我们只需要1根杜邦线,一端接到PA3上,另外一端就接在要测试的电压点(如果是外部电源记得共地),即可测量电压。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.2.3程序设计
4.4.2.3.1 ADC的HAL库驱动
ADC在HAL库中的驱动代码在stm32f4xx_hal_adc.c和stm32f4xx_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参数初始化结构体 */
__IO uint32_t NbrOfCurrentConversionRank; /* 当前转换序列 */
DMA_HandleTypeDef *DMA_Handle; /* DMA配置结构体 */
HAL_LockTypeDef Lock; /* ADC锁定对象 */
__IO uint32_t State; /* ADC工作状态 */
__IO uint32_t ErrorCode; /* ADC错误代码 */
}ADC_HandleTypeDef;
- 该结构体定义和其他外设比较类似,我们着重看第二个成员变量Init含义,它是结构体ADC_InitTypeDef类型,结构体ADC_InitTypeDef定义为:
typedef struct
{
uint32_t ClockPrescaler; /* 设置预分频系数,即PRESC[3:0]位 */
uint32_t Resolution; /* 配置ADC的分辨率 */
uint32_t DataAlign; /* 设置数据的对齐方式 */
uint32_t ScanConvMode; /* 扫描模式 */
uint32_t EOCSelection; /* 转换完成标志位 */
FunctionalState ContinuousConvMode; /* 开启连续转换模式否则就是单次转换模式 */
uint32_t NbrOfConversion; /* 设置转换通道数目 */
FunctionalState DiscontinuousConvMode; /* 是否使用规则通道组间断模式 */
uint32_t NbrOfDiscConversion; /* 单次转换通道的数目 */
uint32_t ExternalTrigConv; /* ADC外部触发源选择 */
uint32_t ExternalTrigConvEdge; /* ADC外部触发极性*/
FunctionalState DMAContinuousRequests; /* DMA转换请求模式*/
} ADC_InitTypeDef;
1) ClockPrescaler:ADC预分频系数选择,可选的分频系数为2、4、6、8、16。
2) Resolution:配置ADC的分辨率,可选的分辨率有12位、10位、8位和6位。分辨率越高,转换数据精度越高,转换时间也越长;反之分辨率越低,转换数据精度越低,转换时间也越短。
3) DataAlign:用于设置数据的对齐方式,这里可以选择右对齐或者是左对齐,该参数可选为:ADC_DATAALIGN_RIGHT和ADC_DATAALIGN_LEFT。
4) ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。
5) EOCSelection:可选参数为ADC_EOC_SINGLE_CONV和ADC_EOC_SEQ_CONV,指定转换结束时是否产生EOS中断或事件标志。
6) ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。
7) NbrOfConversion:设置常规转换通道数目,范围是:1~16。
8) DiscontinuousConvMode:配置是否使用规则通道组间断模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道1和2,下次触发就是转换通道5和7,这样不连续的转换,依次类推。此参数只有将ScanConvMode使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。
9) NbrOfDiscConversion:配置间断模式的通道个数,禁止规则通道组间断模式后,此参数忽略。
10) ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
11) ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。
12) DMAContinuousRequests:指定DMA请求是否以一次性模式执行(当达到转换次数时,DMA传输停止)或在连续模式下(DMA传输无限制,无论转换的数量)。注:在连续模式下,DMA必须配置为循环模式。否则,当达到DMA缓冲区最大指针时将触发溢出。注意:当常规组和注入组都没有转换时(禁用ADC,或启用ADC,没有连续模式或可以启动转换的外部触发器),必须修改此参数。该参数可设置为“启用”或“禁用”。- 函数返回值:HAL_StatusTypeDef枚举类型的值。
- 2. HAL_ADC_ConfigChannel函数ADC通道配置函数,其声明如下:
• HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc,
ADC_ChannelConfTypeDef *sConfig);
- 函数描述:调用了HAL_ADC_Init函数配置了相关的功能后,就可以调用此函数配置ADC具体通道。
- 函数形参:形参1是ADC_HandleTypeDef结构体类型指针变量。形参2是ADC_ChannelConfTypeDef结构体类型指针变量,用于配置ADC采样时间,使用的通道号,单端或者差分方式的配置等。该结构体定义如下:
typedef struct
{
uint32_t Channel; /* ADC转换通道*/
uint32_t Rank; /* ADC转换顺序 */
uint32_t SamplingTime; /* ADC采样周期 */
uint32_t Offset; /* 保留功能 */
} ADC_ChannelConfTypeDef;
1) Channel:ADC转换通道,范围:0~18。
2) Rank:在规则转换中的规则组的转换顺序,可以选择1~16。
3) SamplingTime:ADC的采样周期,最大480个ADC时钟周期。
4) Offset:未定义,不需要关注。- 函数返回值:HAL_StatusTypeDef枚举类型的值。
3. HAL_ADC_Start函数
ADC转换启动函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc); - 函数描述:当配置好ADC的基础的功能后,就调用此函数启动ADC。
- 函数形参:ADC_HandleTypeDef结构体类型指针变量。
- 函数返回值:HAL_StatusTypeDef枚举类型的值。4. 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枚举类型的值。5. HAL_ADC_GetValue函数获取常规组ADC转换值函数,其声明如下:
• uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc);
- 函数描述:一般先调用HAL_ADC_Start函数启动转换,再调用HAL_ADC_PollForConversion函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
- 函数形参:形参1是ADC_HandleTypeDef结构体类型指针变量。
- 函数返回值:当前的转换值,uint32_t类型数据。
单通道ADC采集配置步骤
1)开启ADCx和通道输入的GPIO时钟,配置该IO口的模拟输入功能
首先开启ADCx的时钟,然后配置GPIO为模拟输入。本实验我们默认用到ADC1通道3,对应IO是PA3,它们的时钟开启方法如下:
__HAL_RCC_ADC1_CLK_ENABLE(); /* 使能ADC1时钟 */
__HAL_RCC_GPIOA_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 GPIOA
#define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_3
#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_3
#define ADC_ADCX_CHY_CLK_ENABLE()
do{ __HAL_RCC_ADC1_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
ADC的通道与引脚的对应关系在《STM32F407IGT6.pdf》数据手册可以查到,我们这里使用ADC1的通道3,在数据手册中的表格为:
表4.4.2.3.3.1通道3对应引脚查看表
下面直接开始介绍adc.c的程序,首先是ADC初始化函数。
/**
初始化函数
无
无
*/
void adc_init(void)
{
g_adc_handle.Instance = ADC_ADCX;
g_adc_handle.Init.ClockPrescaler =
ADC_CLOCKPRESCALER_PCLK_DIV4; /* 4分频,21Mhz */
g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 右对齐 */
g_adc_handle.Init.ScanConvMode = DISABLE; /* 非扫描模式 */
g_adc_handle.Init.ContinuousConvMode = DISABLE; /* 关闭连续转换 */
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请求 */
HAL_ADC_Init(&g_adc_handle); /* 初始化ADC */
}
该函数调用HAL_ADC_Init函数配置了选择哪个ADC、数据对齐方式、是否使用扫描模式等参数。另外HAL_ADC_Init函数会调用它的MSP回调函数HAL_ADC_MspInit,该函数用来存放使能ADC和通道对应IO的时钟和初始化IO口等代码,其定义如下:
/**
底层驱动,引脚配置,时钟使能
此函数会被HAL_ADC_Init()调用
句柄
无
*/
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时钟 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC采集对应IO */
gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟输入 */
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, & gpio_init_struct);
}
}
可以看到在HAL_ADC_MspInit函数中,我们使能了ADC和通道对应IO时钟、初始化IO为模拟输入模式。
接下来要介绍的函数是adc_channel_set,其定义如下:
/**
* @brief 设置ADC通道采样时间
* @param adcx : adc句柄指针,ADC_HandleTypeDef
* @param ch : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_18
* @param stime: 采样时间 0~7, 对应关系为:
* @arg ADC_SAMPLETIME_3CYCLES, 3个ADC时钟周期
ADC_SAMPLETIME_15CYCLES, 15个ADC时钟周期
* @arg ADC_SAMPLETIME_28CYCLES, 28个ADC时钟周期
ADC_SAMPLETIME_56CYCLES, 56个ADC时钟周期
* @arg ADC_SAMPLETIME_84CYCLES, 84个ADC时钟周期
ADC_SAMPLETIME_112CYCLES,112个ADC时钟周期
* @arg ADC_SAMPLETIME_144CYCLES,144个ADC时钟周期
ADC_SAMPLETIME_480CYCLES,480个ADC时钟周期
* @param rank: 多通道采集时需要设置的采集编号,
假设你定义channle1的rank=1,channle2 的rank=2,
那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结
果,AdcDMA[1]就是通道2的转换结果。
单通道DMA设置为 ADC_REGULAR_RANK_1
* @arg 编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16
* @retval 无
*/
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch,
uint32_t rank, uint32_t stime)
{
ADC_ChannelConfTypeDef adc_channel;
adc_channel.Channel = ch; /* 通道 */
adc_channel.Rank = rank; /* 设置采样序列 */
adc_channel.SamplingTime = stime; /* 设置采样时间 */
HAL_ADC_ConfigChannel( adc_handle, &adc_channel); /* 通道配置 */
}
该函数主要是通过HAL_ADC_ConfigChannel函数选择要配置的ADC规则组通道,并设置通道的序列号和采样时间。
下面要介绍的是获得ADC转换后的结果函数,其定义如下:
/**
获取ADC值
通道值 0~18,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_18
无
*/
uint32_t adc_get_result(uint32_t ch)
{
/* 设置通道,序列和采样时间 */
adc_channel_set(&g_adc_handle , ch, 1, ADC_SAMPLETIME_480CYCLES);
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某通道多次转换结果平均值函数,函数定义如下:
/**
获取通道ch的转换值,取times次,然后平均
通道号, 0~18
获取次数
通道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(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_init(); /* 初始化ADC */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH3_VAL:", BLUE);
/* 先在固定位置显示小数点 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH3_VOL:0.000V", BLUE);
while (1)
{
/* 获取ADC通道的转换值,10次取平均 */
adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
/* 显示ADC采样后的平均值 */
lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE);
/* 获取计算后的带小数的实际电压值,比如3.1111 */
temp = (float)adcx * (3.3 / 4096);
adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111 - 3 = 0.1111 */
temp -= adcx;
/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数 */
temp *= 1000;
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111 */
lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
LED0_TOGGLE();
delay_ms(100);
}
}
main函数中,我们在LCD上显示一些提示信息后,将每隔100ms刷新一次ADC1通道3的值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。ADC值的显示简单介绍一下:首先在LCD固定位置显示了小数点,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就能在LCD上面显示转换结果的整数和小数部分。
4.4.2.4下载验证
下载代码后,可以看到LCD显示如图4.4.2.4.1所示:
图4.4.2.4.1单通道ADC采集实验测试图
上图中,我们使用1根杜邦线将PA3接到要测试的电压点(如果是外部电源记得共地),即可测出电压值,可测量的电压范围:0~3.3V。LED0闪烁,提示程序运行。
注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.3单通道ADC采集(DMA读取)实验
本实验我们来学习单通道ADC采集(DMA读取)。本实验使用规则组单通道的连续转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
4.4.3.1 ADC&DMA寄存器
本实验我们很多的设置和单通道ADC采集实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集实验不同设置的ADC_CR2寄存器进行介绍,其他的配置基本一样的。另外因为我们用到DMA读取数据,所以还会介绍如何配置相关DMA的寄存器。
- ADC配置寄存器(ADC_CR2)ADCx配置寄存器描述如图4.4.3.1.1所示:
-
图4.4.3.1.1 ADC_CR2寄存器
ADC_CR2寄存器中我们主要跟前面设置不同的有两个位,分别如下:
DMA位用于配置使用DMA模式,本实验该位置1。在单通道ADC采集实验中,默认设置为0,即不使用DMA模式,规则组转换的结果存储在ADC_DR寄存器,然后通过手动读取ADC_DR寄存器的方式得到转换结果。本实验我们使用ADC的连续转换模式,并通过DMA读取转换结果,这样DMA就会自动在ADC_DR寄存器中读取转换结果。
CONT位用于设置单次转换模式还是连续转换模式,本实验我们使用连续转换模式,所以CONT位置1即可。
这里介绍ADC_CR2寄存器的这两个位,其它请参考上一个实验的配置。下面介绍DMA一些比较重要的寄存器配置。 - DMA数据流x外设地址寄存器(DMA_SxPAR)(x = 0…7)DMA数据流x外设地址寄存器描述如图4.4.3.1.2所示:
-
图4.4.3.1.2 DMA_SxPAR寄存器
该寄存器存放的是DMA读或者写数据的外设数据寄存器的基址。本实验,我们需要通过DMA读取ADC转换后存放在ADC规则数据寄存器 (ADC_DR) 的结果数据。所以我们需要给DMA_SxPAR寄存器写入ADC_DR寄存器的地址。这样配置后,DMA就会从ADC_DR寄存器的地址读取ADC的转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_SxPAR寄存器来设置,比如定义一个变量,把这个变量的地址值写入该寄存器。
注意:DMA_SxPAR寄存器受到写保护,只有DMA_SxCR寄存器中的EN为“0”时才可以写入,即先要禁止通道开启才可以写入。 - DMA数据流x存储器地址寄存器(DMA_SxM0AR)(x = 0…7)DMA数据流x存储器地址寄存器描述如图4.4.3.1.3所示:
-
图4.4.3.1.3 DMA_SxM0AR寄存器
该寄存器存放的是DMA读或者写数据的目标存放的地址。同样的,该寄存器也是受写保护,只有当DMA_SxCR的EN位为0时才可以写入。 - DMA数据流x数据项数寄存器(DMA_SxNDTR)(x = 0…7)DMA数据流x数据项数寄存器描述如图4.4.3.1.4所示:
- 图4.4.3.1.4 DMA_SxNDTR寄存器
前面的DMA_SxPAR寄存器是传输的源地址,而DMA_SxM0AR寄存器是传输的目的地址,DMA_SxNDTR寄存器则是要传输的数据项数目(0到65535)。
其他的DMA寄存器我们就不一一介绍了,大家有需要的话可以查阅参考手册。
4.4.3.2硬件设计
1. 例程功能
采集ADC1通道3(PA3)上的电压,并在LCD上面显示ADC规则数据寄存器12位的转换值以及将该值换算成电压后的电压值。我们使用杜邦线将PA3引脚接到DC 0~3.3V的电源上(外部电源记得共地),即可采集到ADC数据。ADC采集到的数据和转换后的电压值将在LCD屏中显示。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1 :通道3 – PA3
5)DMA(DMA2通道0)
3. 原理图
ADC属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验通过ADC1的通道3(PA3)来采集外部电压值,我们只需要1根杜邦线,一端接到PA3上,另外一端就接在要测试的电压点(如果是外部电源记得共地),即可测量电压。一定要保证测试点的电压在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);
- 函数描述:用于启动DMA传输,并开启相关中断,DMA1和DMA2都是用的这个函数。
- 函数形参:形参1是DMA_HandleTypeDef结构体类型指针变量。
形参2是DMA传输的源地址。
形参3是DMA传输的目的地址。
形参4是要传输的数据项数目。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。2. HAL_ADC_Start_DMA函数启动ADC(DMA传输)方式函数,其声明如下:
• HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,
uint32_t *pData, uint32_t Length);
- 函数描述:用于启动ADC,转换结果以DMA传输方式读取。
- 函数形参:形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是ADC采样数据传输的目的地址。
形参3是要传输的数据项数目。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。
- 注意事项: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通道3,对应IO是PA3,它们的时钟开启方法如下:
__HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
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转换,通过DMA传输结果。
通过HAL_DMA_Start_IT函数启动DMA读取,使能DMA中断。
通过HAL_NVIC_EnableIRQ函数使能DMA数据流中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
6)编写中断服务函数
DMA的每个数据流都有一个中断服务函数,比如DMA2_ Stream4的中断服务函数为DMA2_Stream4_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 DMA2_Stream4
#define ADC_ADCX_DMASx_Chanel DMA_CHANNEL_0
#define ADC_ADCX_DMASx_IRQn DMA2_Stream4_IRQn
#define ADC_ADCX_DMASx_IRQHandler DMA2_Stream4_IRQHandler
/* 判断DMA2 Stream4传输完成标志, 这是一个假函数形式,只能用在if等语句里面 */
#define ADC_ADCX_DMASx_IS_TC()
( __HAL_DMA_GET_FLAG(&g_dma_adc_handle, DMA_FLAG_TCIF0_4) )
/* 清除DMA2 Stream4传输完成标志 */
#define ADC_ADCX_DMASx_CLR_TC()
do{ __HAL_DMA_CLEAR_FLAG(&g_dma_adc_handle, DMA_FLAG_TCIF0_4); }
while(0)
下面给大家介绍adc.c文件里面的函数,首先是ADC DMA读取初始化函数。
/**
读取 初始化函数
存储器地址
无
*/
void adc_dma_init(uint32_t mar)
{
GPIO_InitTypeDef gpio_init_struct;
ADC_ChannelConfTypeDef adc_ch_conf = {0};
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟 */
ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启GPIO时钟 */
/* 大于DMA2的基地址, 则为DMA2的数据流通道了 */
if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
/* 设置AD采集通道对应IO引脚工作模式 */
gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
/* 初始化DMA */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 设置DMA数据流 */
g_dma_adc_handle.Init.Channel = DMA_CHANNEL_0; /* 设置DMA通道 */
从外设到存储器模式 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
外设数据长度:16位 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_adc_handle);
g_adc_dma_handle.Instance = ADC_ADCX;
/* 4分频,21Mhz */
g_adc_dma_handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_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 = DISABLE; /* 非扫描模式 */
g_adc_dma_handle.Init.ContinuousConvMode = ENABLE; /* 开启连续转换 */
本实验用到1个规则通道序列 */
g_adc_dma_handle.Init.NbrOfConversion = 1;
禁止不连续采样模式 */
g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;
g_adc_dma_handle.Init.NbrOfDiscConversion = 0; /* 不连续采样通道数为0 */
g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
g_adc_dma_handle.Init.DMAContinuousRequests = ENABLE; /* 开启DMA请求 */
HAL_ADC_Init(&g_adc_dma_handle); /* 初始化ADC */
/* 把ADC和DMA连接起来 */
__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);
/* 配置ADC通道 */
adc_ch_conf.Channel = ADC_ADCX_CHY; /* 通道 */
adc_ch_conf.Rank = 1; /* 序列 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_480CYCLES; /* 采样时间 */
HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf); /* 通道配置 */
/* 配置DMA数据流请求中断优先级 */
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);
/* 启动DMA,并开启中断 */
HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
HAL_ADC_Start_DMA(&g_adc_dma_handle,&mar,0); /* 开启ADC,通过DMA传输结果 */
}
adc_dma_init函数包含了输入通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。下面来看看该函数的代码内容。
第一部分,使能ADC、DMA和GPIO的时钟。
第二部分,设置ADC采集通道对应IO引脚工作模式。
第三部分,初始化DMA、ADC,配置ADC时钟预分频系数为4,得到ADC的输入时钟频率是21MHz。
第四部分,通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
第五部分,配置ADC通道。
第六部分,配置DMA数据流请求中断优先级,并使能该中断。
第七部分,启动DMA并开启DMA中断,以及启动ADC并通过DMA传输转换结果。
为了方便代码的管理和移植性等,这里就没有使用HAL_ADC_MspInit这个函数来存放使能时钟、GPIO、NVIC相关的代码,而是全部存放在adc_dma_init函数中。
接下来给大家介绍使能一次ADC DMA传输函数,其定义如下:
/**
使能一次ADC DMA传输
传输的次数
无
*/
void adc_dma_enable(uint16_t cndtr)
{
__HAL_ADC_DISABLE(&g_adc_dma_handle); /* 先关闭ADC */
__HAL_DMA_DISABLE(&g_dma_adc_handle); /* 关闭DMA传输 */
g_dma_adc_handle.Instance->NDTR = cndtr; /* 重设DMA传输数据量 */
__HAL_DMA_ENABLE(&g_dma_adc_handle); /* 开启DMA传输 */
__HAL_ADC_ENABLE(&g_adc_dma_handle); /* 重新启动ADC */
ADC_ADCX->CR2 |= 1 << 30; /* 启动规则转换通道 */
}
该函数的某些部分我们使用寄存器来操作,因为用HAL库操作会对adc_dma_init配置好的某些参数修改。HAL_DMA_Start_IT函数已经配置好了DMA传输的源地址和目标地址,本函数只需要调用g_dma_adc_handle.Instance->CNDTR=cndtr;语句给DMA_SxNDTR寄存器写入要传输的数据量,然后启动DMA就可以传输了。
下面介绍的是ADC DMA采集中断服务函数,函数定义如下:
/**
采集中断服务函数
无
无
*/
void ADC_ADCX_DMASx_IRQHandler(void)
{
if (ADC_ADCX_DMASx_IS_TC())
{
g_adc_dma_sta = 1; /* 标记DMA传输完成 */
ADC_ADCX_DMASx_CLR_TC(); /* 清除DMA2 数据流4 传输完成中断 */
}
}
在该函数里,通过判断DMA传输完成标志位是否是1,是1就给g_adc_dma_sta 变量赋值为1,标记DMA传输完成,最后清除DMA的传输完成标志位。
最后在main.c里面编写如下代码:
#define ADC_DMA_BUF_SIZE 100 /* ADC DMA采集 BUF大小 */
uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE]; /* ADC DMA BUF */
extern uint8_t g_adc_dma_sta; /* DMA传输状态标志,0,未完成;1,已完成 */
int main(void)
{
uint16_t i, adcx;
uint32_t sum;
float temp;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "ADC DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH3_VAL:", BLUE);
/* 先在固定位置显示小数点 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH3_VOL:0.000V", BLUE);
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动ADC DMA采集 */
while (1)
{
if (g_adc_dma_sta == 1)
{
/* 计算DMA 采集到的ADC数据的平均值 */
sum = 0;
for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */
{
sum += g_adc_dma_buf[i];
}
adcx = sum / ADC_DMA_BUF_SIZE; /* 取平均值 */
/* 显示结果 */
lcd_show_xnum(134, 110, adcx, 4, 16, 0, BLUE); /* 显示采样后的原始值 */
/* 获取计算后的带小数的实际电压值,比如3.1111 */
temp = (float)adcx * (3.3 / 4096);
/* 赋值整数部分给adcx变量,因为adcx为u16整形 */
adcx = temp;
显示电压值的整数部分,3.1111的话,这里就是显示3 */
lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
temp -= adcx;
/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数 */
temp *= 1000;
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
g_adc_dma_sta = 0; /* 清除DMA采集完成状态标志 */
adc_dma_enable(ADC_DMA_BUF_SIZE); /* 启动下一次ADC DMA采集 */
}
LED0_TOGGLE();
delay_ms(100);
}
}
此部分代码,和单通道ADC采集实验十分相似,只是这里使能了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,这里我们对数组的数据取平均值,减少误差。在LCD屏显示结果的处理和单通道ADC采集实验一样。首先我们在LCD固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在LCD上面显示转换结果的整数和小数部分。
4.4.3.4下载验证
下载代码后,LED0闪烁,提示程序运行,可以看到LCD显示如图4.4.3.4.1所示:
图4.4.3.4.1 单通道ADC采集(DMA读取)实验测试图
上图中,我们使用1根杜邦线将PA3接到要测试的电压点(如果是外部电源记得共地),即可测出电压值,可测量的电压范围:0~3.3V。LED0闪烁,提示程序运行。
注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.4多通道ADC采集(DMA读取)实验
本实验我们来学习多通道ADC采集(DMA读取)。本实验使用规则组多通道的连续转换模式,并且通过软件触发,即由ADC_CR2寄存器的SWSTART位启动。由于使用连续转换模式,所以使用DMA读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。
4.4.4.1 ADC寄存器
本实验我们很多的设置和单通道ADC采集(DMA读取)实验是一样的,所以下面介绍寄存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC采集(DMA读取)实验不同设置的ADC_SQRx寄存器进行介绍,其他的配置基本一样的。另外我们用到DMA读取数据,配置上和单通道ADC采集(DMA读取)实验是一样的。
ADC规则序列寄存器有四个(ADC_SQR1~ ADC_SQR3),具体怎么配置,需要看我们用多少个通道,比如本实验我们使用3个通道同时采集ADC数据,具体配置如下:
- ADC规则序列寄存器1(ADC_SQR1)ADC规则序列寄存器1描述如图4.4.4.1.1所示:
- 图4.4.4.1.1ADC_SQR1寄存器
L[3:0]位用于设置规则序列的长度,取值范围:0~15,表示规则序列长度为1~16。本实验使用到3个通道,所以设置这几个位的值为2即可。
SQ13[4:0]~SQ16[4:0]位设置规则组序列的第13~16个转换编号,第1~12个转换编号的设置请查看ADC_SQR2和ADC_SQR3寄存器。设置过程非常简单,忘记了请参考前面给大家整理出来的规则序列寄存器控制关系汇总表。
下面我们来看看本实验是怎么设置的:SQ1[4:0]位赋值为3、SQ2[4:0]位赋值为4、SQ3[4:0]位赋值为5,即规则序列1到3分别对应的输入通道是3到5。SQ1~SQ3位都是在ADC_SQR3寄存器中配置。
4.4.4.2硬件设计
1. 例程功能
使用ADC1采集(DMA读取)通道3\4\5\的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA3\PA4\PA5到你想测量的电压源(0~3.3V),然后通过LCD显示的电压值。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
– PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1 :通道3 – PA3、通道4 – PA4、通道5– PA5
5)DMA(DMA2通道0)
3. 原理图
ADC和DMA属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,但是还需要我们将待测量的电压源连接到ADC通道上,以便ADC测量。本实验,我们通过ADC1的通道3\4\5来采集外部电压值,并通过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通道3、4、5,对应IO是PA3、PA4和PA5,它们的时钟开启方法如下:
__HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
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的每个数据流都有一个中断服务函数,比如DMA2_ Stream4的中断服务函数为DMA2_Stream4_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_CH3_GPIO_PORT GPIOA
#define ADC_ADCX_CH3_GPIO_PIN GPIO_PIN_3
#define ADC_ADCX_CH3_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define ADC_ADCX_CH4_GPIO_PORT GPIOA
#define ADC_ADCX_CH4_GPIO_PIN GPIO_PIN_4
#define ADC_ADCX_CH4_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define ADC_ADCX_CH5_GPIO_PORT GPIOA
#define ADC_ADCX_CH5_GPIO_PIN GPIO_PIN_5
#define ADC_ADCX_CH5_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */
#define ADC_ADCX ADC1
#define ADC_ADCX_CH3 ADC_CHANNEL_3
#define ADC_ADCX_CH4 ADC_CHANNEL_4
#define ADC_ADCX_CH5 ADC_CHANNEL_5
#define ADC_ADCX_CHY_CLK_ENABLE()
do{ __HAL_RCC_ADC1_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
#define ADC_CH_NUM 3 /* 需要转换的通道数目 */
#define ADC_COLL 1000 /* 单采集次数 */
#define ADC_SUM ADC_CH_NUM * ADC_COLL /* 总采集次数 */
/* ADC单通道/多通道 DMA采集 DMA及通道 定义*/
#define ADC_ADCX_DMASx DMA2_Stream4 /* DMA2数据流4 */
#define ADC_ADCX_DMASx_Chanel DMA_CHANNEL_0 /* DMA通道0 */
#define ADC_ADCX_DMASx_IRQn DMA2_Stream4_IRQn
#define ADC_ADCX_DMASx_IRQHandler DMA2_Stream4_IRQHandler
/* 判断DMA2 Stream4传输完成标志, 这是一个假函数形式,只能用在if等语句里面 */
#define ADC_ADCX_DMASx_IS_TC()
( __HAL_DMA_GET_FLAG(&g_dma_nch_adc_handle, DMA_FLAG_TCIF0_4) )
/* 清除DMA2 Stream4传输完成标志 */
#define ADC_ADCX_DMASx_CLR_TC()
do{ __HAL_DMA_CLEAR_FLAG(&g_dma_nch_adc_handle,DMA_FLAG_TCIF0_4); }
while(0)
在adc.h头文件中,我们添加了ADC1通道4、5相关的宏定义。
下面开始介绍adc.c的函数,首先看ADC的N通道(3通道) DMA读取初始化函数,其定义如下:
/**
通道(3通道) DMA读取 初始化函数
无
无
*/
void adc_nch_dma_init(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
ADC_ChannelConfTypeDef sConfig = {0};
ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟 */
ADC_ADCX_CH3_GPIO_CLK_ENABLE(); /* 开启GPIO时钟 */
ADC_ADCX_CH4_GPIO_CLK_ENABLE();
ADC_ADCX_CH5_GPIO_CLK_ENABLE();
/* 大于DMA2基地址,则为DMA2的数据流通道了 */
if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
/* 设置ADC1通道3~5对应的IO口模拟输入 */
gpio_init_struct.Pin = ADC_ADCX_CH3_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(ADC_ADCX_CH3_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = ADC_ADCX_CH4_GPIO_PIN;
HAL_GPIO_Init(ADC_ADCX_CH4_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = ADC_ADCX_CH5_GPIO_PIN;
HAL_GPIO_Init(ADC_ADCX_CH5_GPIO_PORT, &gpio_init_struct);
/* DMA配置 */
g_dma_nch_adc_handle.Instance = ADC_ADCX_DMASx;
g_dma_nch_adc_handle.Init.Channel = DMA_CHANNEL_0; /* 设置DMA通道 */
外设到存储器模式 */
g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
/* 外设数据长度:16位 */
g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_nch_adc_handle);
/* 将DMA与adc联系起来 */
__HAL_LINKDMA(&g_adc_nch_dma_handle,DMA_Handle,g_dma_nch_adc_handle);
g_adc_nch_dma_handle.Instance = ADC_ADCX;
/* 4分频,21Mhz */
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.ScanConvMode = ENABLE; /* 使能扫描模式 */
g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能连续转换 */
禁止规则通道组间断模式 */
g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;
g_adc_nch_dma_handle.Init.ExternalTrigConvEdge =
ADC_EXTERNALTRIGCONVEDGE_NONE; /* 使用软件触发 */
g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 右对齐 */
本实验用到3个规则通道序列 */
g_adc_nch_dma_handle.Init.NbrOfConversion = ADC_CH_NUM;
/* 开启DMA连续转换 */
g_adc_nch_dma_handle.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&g_adc_nch_dma_handle);
sConfig.Channel = ADC_ADCX_CH3;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_480CYCLES;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig); /* 配置ADC通道3 */
sConfig.Channel = ADC_ADCX_CH4;
sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig); /* 配置ADC通道4 */
sConfig.Channel = ADC_ADCX_CH5;
sConfig.Rank = 3;
HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &sConfig); /* 配置ADC通道5 */
/* 配置中断 */
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 2, 1);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);
/* 开启ADC,使用DMA读取 */
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle,(uint32_t *)g_adc_value,ADC_SUM);
}
adc_nch_dma_init函数包含了输出通道对应IO的初始代码、NVIC、使能时钟、ADC时钟预分频系数、ADC工作参数和ADC通道配置等代码。大部分代码和单通道ADC采集(DMA读取)实验一样,下面来看看该函数的代码内容。
第一部分,使能ADC、DMA和GPIO的时钟。
第二部分,设置ADC采集通道对应IO引脚工作模式,这里用到6个通道。
第三部分,初始化DMA、ADC,配置ADC时钟预分频系数为4,得到ADC的输入时钟频率是21MHz。
第四部分,通过__HAL_LINKDMA宏定义将DMA相关的配置关联到ADC的句柄中。
第五部分,配置ADC通道,这里有3个通道需要配置。
第六部分,配置DMA数据流请求中断优先级,并使能该中断。
第七部分,启动DMA并开启DMA中断,以及启动ADC并通过DMA传输转换结果。
为了方便代码的管理和移植性等,这里就没有使用HAL_ADC_MspInit这个函数来存放使能时钟、GPIO、NVIC相关的代码,而是全部存放在adc_dma_init函数中。
接下来我们看ADC采集中断回调函数,其定义如下:
/**
采集中断回调函数
:ADC句柄
无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
HAL_ADC_Stop_DMA(hadc); /* 关闭DMA传输 */
adc[0] = adc_get_result_average(0); /* 计算ADC平均值 */
adc[1] = adc_get_result_average(1);
adc[2] = adc_get_result_average(2);
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t*)&g_adc_value, ADC_SUM);
}
进入中断回调函数之后,先关闭DMA传输,然后进行3个通道的ADC平均值计算,计算完平均值后再重新开启DMA。ADC平均值计算函数在单通道ADC采集实验已经介绍过,这里不再赘述。
在main.c里面编写如下代码:
extern int16_t adc[3]; /* ADC平均值 */
float g_adc_u_value[ADC_CH_NUM] = {0}; /* 存储ADC转换后的电压值 */
int main(void)
{
uint16_t i,adcx;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为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 3CH DMA TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH3_VAL:", BLUE);
/* 先在固定位置显示小数点 */
lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH3_VOL:0.000V", BLUE);
/* 先在固定位置显示小数点 */
lcd_show_string(30, 150, 200, 16, 16, "ADC1_CH4_VAL:", BLUE);
lcd_show_string(30, 170, 200, 16, 16, "ADC1_CH4_VOL:0.000V", BLUE);
lcd_show_string(30, 190, 200, 16, 16, "ADC1_CH5_VAL:", BLUE);
/* 先在固定位置显示小数点 */
lcd_show_string(30, 210, 200, 16, 16, "ADC1_CH5_VOL:0.000V", BLUE);
while (1)
{
for(i = 0; i<3; i++)
{
/* 显示ADC采样后的平均值 */
lcd_show_xnum(134, 110+(i*40), adc[i], 5, 16, 0, BLUE);
g_adc_u_value[i] = (float)adc[i] * 3.3f /4096; /* 计算电压值 */
adcx = g_adc_u_value[i];
/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
lcd_show_xnum(134, 130+(i*40), adcx, 1, 16, 0, BLUE);
/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
g_adc_u_value[i] -= adcx;
/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数 */
g_adc_u_value[i] *= 1000;
/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
lcd_show_xnum(150, 130+(i*40), g_adc_u_value[i],3,16,0X80, BLUE);
}
LED1_TOGGLE();
delay_ms(100);
}
}
main函数中,我们初始化了ADC和DMA,在LCD模块上显示一些提示信息后,每隔100ms刷新一次ADC1通道3、4、5的值,并显示读到的ADC值(数字量),以及它们转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。ADC值的显示简单介绍一下:首先在LCD固定位置显示了小数点,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就能在LCD上面显示转换结果的整数和小数部分。
4.4.4.4下载验证
下载代码后,LED0闪烁,提示程序运行。可以看到LCD显示如图4.4.4.4.1所示:
图4.4.4.4.1 多通道ADC采集(DMA读取)实验测试图
使用ADC1采集(DMA读取)通道3\4\5的电压,在LCD模块上面显示对应的ADC转换值以及换算成电压后的电压值。可以使用杜邦线连接PA3\PA4\PA5到你想测量的电压源(0~3.3V),如果测量外部电源需要共地。
这4个通道可以同时测量不同测试点的电压,只需要用杜邦线分别接到不同的电压测试点即可。注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
4.4.5内部温度传感器实验
本实验我们来学习STM32F407的内部温度传感器并使用它来读取温度值,然后在LCD模块上显示出来。下面先带大家来了解一下STM32的内部温度传感器。
4.4.5.1 内部温度传感器简介
STM32有一个内部的温度传感器,可以用来测量CPU及周围的温度(内部温度传感器更适合于检测温度的变化,需要测量精确温度的情况下,建议使用外置传感器)。对于STM32F407来说,该温度传感器在内部和ADC1_IN16输入通道相连接,此通道把传感器输出的电压转换成数字值,温度传感器模拟输入采样时间不能小于10us。STM32内部温度传感器支持的温度范围为:-40~125度。精度为±1.5℃左右。
STM32内部温度传感器的使用很简单,只要设置一下内部ADC,并激活其内部温度传感器通道就差不多了。关于ADC的设置,我们在上一章已经进行了详细的介绍,这里就不再多说。接下来我们介绍一下和温度传感器设置相关的两个地方。
第一个地方,我们要使用STM32的内部温度传感器,必须先激活ADC的内部通道,通过ADC_CR2的TSVREFE位设置。设置该位为1则启用内部温度传感器。
第二个地方,STM32F4的内部温度传感器固定的连接在ADC1的通道16上,所以,我们在设置好ADC1之后只要读取通道16的值,就是温度传感器返回来的电压值了。根据这个值,我们就可以计算出当前温度。计算公式如下:
式子中:
V25 = Vsense在25度时的数值(典型值为:0.76)
Avg_Slope = 温度与Vsense曲线的平均斜率(单位:mv/℃或uv/℃)(典型值:2.5mv/℃)。
利用以上公式,我们就可以方便的计算出当前温度传感器的温度了。
4.4.5.2硬件设计
1. 例程功能
通过ADC的通道16读取STM32F407内部温度传感器的电压值,并将其转换为温度值,显示在LCD屏上。LED0闪烁用于提示程序正在运行。
2. 硬件资源
1)LED灯
– PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC1通道16
5)内部温度传感器
3. 原理图
ADC和内部温度传感器都属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,我们需要用到TFTLCD模块显示结果。
4.4.5.3程序设计
4.4.5.3.1 ADC的HAL库驱动
本实验用到的ADC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍读取内部温度传感器ADC值的配置步骤。
读取STM32内部温度传感器ADC值的配置步骤
1)开启ADC时钟
通过__HAL_RCC_ADC1_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头文件,其定义如下:
#define ADC_ADCX ADC1
#define ADC_TEMPSENSOR_CHX ADC_CHANNEL_TEMPSENSOR /* 内部温度传感器专用通道 */
#define ADC_ADCX_CHY_CLK_ENABLE()
do{ __HAL_RCC_ADC1_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
ADC_CHANNEL_TEMPSENSOR就是内部温度传感器的通道宏定义,它实际上连接到了ADC1的通道16,因此,我们也可以直接用ADC_CHANNEL_16来定义内部温度温度传感器的通道。
下面我们直接介绍与内部温度传感器相关的adc.c的代码,首先是ADC内部温度传感器初始化函数,其定义如下:
/**
内部温度传感器 初始化函数
无
无
*/
void adc_temperature_init(void)
{
ADC_ChannelConfTypeDef adc_ch_conf = {0};
ADC_ADCX_CHY_CLK_ENABLE();
g_adc_handle.Instance = ADC_ADCX;
/* 4分频,21Mhz */
g_adc_handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4;
g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 右对齐 */
g_adc_handle.Init.ScanConvMode = DISABLE; /* 非扫描模式 */
g_adc_handle.Init.ContinuousConvMode = DISABLE; /* 关闭连续转换 */
g_adc_handle.Init.NbrOfConversion = 1; /* 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请求 */
HAL_ADC_Init(&g_adc_handle); /* 初始化ADC */
adc_ch_conf.Channel = ADC_TEMPSENSOR_CHX; /* ADC通道 */
adc_ch_conf.Rank = 1; /* 序列 */
adc_ch_conf.SamplingTime = ADC_SAMPLETIME_480CYCLES; /* 采样时间 */
HAL_ADC_ConfigChannel(&g_adc_handle,&adc_ch_conf); /* 通道配置 */
}
内部温度传感器相关的ADC初始化步骤与普通ADC类似,只是采集通道换成了内部温度传感器专用的通道,这里不再赘述。
下面讲解一下获取内部温度传感器温度值函数,其定义如下:
/**
获取内部温度传感器温度值
无
温度值(扩大了100倍,单位:℃)
*/
short adc_get_temperature(void)
{
uint32_t adcx;
short result;
double temperature;
/* 读取内部温度传感器通道,20次取平均 */
adcx = adc_get_result_average(ADC_TEMPSENSOR_CHX, 20);
temperature = (float)adcx * (3.3 / 4096); /* 转化为电压值 */
temperature = (temperature - 0.76) / 0.0025 + 25; /* 计算温度 */
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(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
adc_temperature_init(); /* 初始化ADC内部温度传感器采集 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "Temperature TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 120, 200, 16, 16, "TEMPERATE: 00.00C", BLUE);
while (1)
{
temp = adc_get_temperature(); /* 得到温度值 */
if (temp < 0) /* 显示负号 */
{
temp = -temp;
lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, "-", BLUE);
}
else /* 无符号 */
{
lcd_show_string(30 + 10 * 8, 120, 16, 16, 16, " ", BLUE);
}
/* 显示整数部分 */
lcd_show_xnum(30 + 11 * 8, 120, (temp / 100), 2, 16, 0, BLUE);
/* 显示小数部分 */
lcd_show_xnum(30 + 14 * 8, 120, (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 内部温度传感器实验测试图
大家可以看看显示温度值与实际是否相符合(因为芯片会发热,所以一般会比实际温度偏高)。
4.5 DAC
本小节我们将介绍STM32F407的DAC(Digital-to-analog converters,数模转换器)功能。我们通过三个实验来学习DAC,分别是DAC输出实验、DAC输出三角波实验和DAC输出正弦波实验。
本小节分为如下几个部分:
4.5.1 DAC简介
4.5.2 DAC输出实验
4.5.3 DAC输出三角波实验
4.5.4 DAC输出正弦波实验
4.5.1 DAC简介
STM32F407的DAC模块(数字/模拟转换模块)是12位数字输入,电压输出型的DAC。DAC可以配置为8位或12位模式,也可以与DMA控制器配合使用。DAC工作在12位模式时,数据可以设置成左对齐或右对齐。DAC模块有2个输出通道,每个通道都有单独的转换器。在双DAC模式下,2个通道可以独立地进行转换,也可以同时进行转换并同步地更新2个通道的输出。DAC可以通过引脚输入参考电压Vref+以获得更精确的转换结果。
STM32的DAC模块主要特点有:
① 两个 DAC 转换器:各对应一个输出通道;
② 12 位模式下数据采用左对齐或右对齐;
③ 同步更新功能;
④ DMA下溢错误检测;
⑤ 噪声\三角波形生成;
⑥ DAC 双通道单独或同时转换;
⑦ 每个通道都有DMA功能;
DAC通道框图如图4.5.1.1所示:
图4.5.1.1 DAC通道框图
图中4.5.1.1中,VDDA和VSSA为DAC模块模拟部分的供电,而Vref+则是DAC模块的参考电压输入引脚。DAC_OUTx就是DAC的两个输出通道了(对应PA4或者PA5引脚)。DAC的这些输入/输出引脚信息如下表所示:
引脚名称 | 信号类型 | 说明 |
VREF+ | 正模拟参考电压输入 | DAC高/正参考电压,VREF+≤VDDAmax |
VDDA | 模拟电源输入 | 模拟电源 |
VSSA | 模拟电源地输入 | 模拟电源地 |
DAC_OUTx | 模拟输出信号 | DAC通道x模拟输出,x=1/2 |
表4.5.1.1 DAC输入/输出引脚
从图4.5.1.1可以看出,DAC输出是受DORx(x=1/2,下同)寄存器直接控制的,但是我们不能直接往DORx寄存器写入数据,而是通过DHRx间接的传给DORx寄存器,实现对DAC输出的控制。
前面我们提到,STM32F407的DAC支持8/12位模式,8位模式的时候是固定的右对齐的,而12位模式又可以设置左对齐/右对齐。DAC单通道模式下的数据寄存器对齐方式,总共有3种情况,如下图所示:
图4.5.1.2单通道模式下的数据寄存器对齐方式
① 8位数据右对齐:用户将数据写入DAC_DHR8Rx[7:0]位(实际存入DHRx[11:4]位)。
② 12位数据左对齐:用户将数据写入DAC_DHR12Lx[15:4]位(实际存入DHRx[11:0]位)。
③ 12位数据右对齐:用户将数据写入DAC_DHR12Rx[11:0]位(实际存入DHRx[11:0]位)。
我们本章实验中使用的都是单通道模式下的DAC通道1,采用12位右对齐格式,所以采用第③种情况。另外DAC还具有双通道转换功能。
对于 DAC 双通道(可用时),也有三种可能的方式,如下图所示:
图4.5.1.3双通道模式下的数据寄存器对齐方式
① 8位数据右对齐:用户将DAC通道1的数据写入DAC_DHR8RD[7:0]位(实际存入DHR1位),将DAC通道2的数据写入DAC_DHR8RD[15:8]位(实际存入DHR2位)。
② 12位数据左对齐:用户将DAC通道1的数据写入DAC_DHR12LD位(实际存入DHR1[11:0]位),将DAC通道2的数据写入DAC_DHR12LD [31:20]位(实际存入DHR2[11:0]位)。
③ 12位数据右对齐:用户将DAC通道1的数据写入DAC_DHR12RD [11:0]位(实际存入DHR1[11:0]位),将DAC通道2的数据写入DAC_DHR12RD [27:16]位(实际存入DHR2[11:0]位)。
DAC可以通过软件或者硬件触发转换,通过配置TENx控制位来决定。如果没有选中硬件触发(寄存器DAC_CR1的TENx位置0),存入寄存器DAC_DHRx的数据会在1个APB1时钟周期后自动传至寄存器DAC_DORx。如果选中硬件触发(寄存器DAC_CR1的TENx位置1),数据传输在触发发生以后3个APB1时钟周期后完成。一旦数据从DAC_DHRx寄存器装入DAC_DORx寄存器,在经过时间tSETTLING之后,输出即有效,这段时间的长短依电源电压和模拟输出负载的不同会有所变化。我们可以从《STM32F407IGT6.pdf》数据手册查到tSETTLING的典型值为3us,最大是6us,所以DAC的转换速度最快是333K左右。
不使用硬件触发(TEN=0),其转换的时间框图如图4.5.1.4所示:
图4.5.1.4时DAC模块转换时间框图
当DAC的参考电压为VREF+的时候,DAC的输出电压是线性的从0~VREF+,12位模式下DAC输出电压与VREF以及DORx的计算公式如下:
DACx输出电压= VREF(DORx/4095)
如果使用硬件触发(TEN=1),可通过外部事件(定时计数器、外部中断线)触发DAC转换。由TSELx[2:0]控制位来决定选择8个触发事件中的一个来触发转换。这8个触发事件如下表所示:
表4.5.1.3 DAC触发选择
原表见《STM32F4xx参考手册_V4(中文版).pdf》第292页表58。
每个DAC通道都有DMA功能,两个DMA通道分别用于处理两个DAC通道的DMA请求。如果DMAENx位置1时,如果发生外部触发(而不是软件触发),就会产生一个DMA请求,然后DAC_DHRx寄存器的数据被转移到DAC_NORx寄存器。
4.5.2 DAC输出实验
本实验我们来学习DAC输出。
4.5.2.1 DAC寄存器
下面,我们介绍要实现DAC的通道1输出,需要用到的一些DAC寄存器。
- DACx控制寄存器(DACx_CR)DACx控制寄存器描述如图4.5.2.1.1所示:
-
图4.5.2.1.1 DACx_CR寄存器
DAC_CR的低16位用于控制通道1,而高16位用于控制通道2,我们这里仅列出本实验需要设置的一些位:
EN1位:用于DAC通道1的使能,我们要用到DAC通道1的输出,该位必须设置为1。
BOFF1位:用于DAC输出缓存控制,本教程的三个DAC实验我们都不使用输出缓存,即该位设置为1。
TEN1位:用于DAC通道1的触发使能,我们设置该位为0,不使用触发。写入DHR1的值会在1个APB1周期后传送到DOR1,然后输出到PA4口上。
TSEL1[2:0]位,用于选择DAC通道1的触发方式,这里我们没有用到外部触发,所以这几位设置为0即可。
WAVE1[1:0]位,用于控制DAC通道1的噪声/波形输出功能,我们这里没用到波形发生器,所以默认设置为00,不使能噪声/波形输出。
MAMP[3:0]位,是屏蔽/幅值选择器,用来在噪声生成模式下选择屏蔽位,在三角波生成模式下选择波形的幅值。本实验没有用到波形发生器,所以设置为0即可。
DMAEN1位,用于DAC通道1的DMA使能,本实验没有用到DMA功能,所以设置为0。 - DACx通道1 12位右对齐数据保持寄存器(DACx_DHR12R1)
DACx通道1 12位右对齐数据保持寄存器描述如图4.5.2.1.2所示:
图4.5.2.1.2 DAC_DHR12R1寄存器
该寄存器用来设置DAC输出,通过写入12位数据到该寄存器,就可以在DAC输出通道1(PA4)得到我们所要的结果。
4.5.2.2硬件设计
1. 例程功能
使用KEY0/KEY1两个按键,控制STM32内部DAC的通道1输出电压大小,然后通过ADC1的通道3采集DAC输出的电压,在LCD模块上面显示ADC采集到的电压值以及DAC的设定输出电压值等信息。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
– PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键 :
KEY0 - PE2
KEY1 - PE3
5)ADC1:
通道3 – PA3
6)DAC1 :
通道1 - PA4
3. 原理图
我们只需要通过杜邦线连接PA3和PA4,就可以使得ADC1通道3和DAC1通道1连接起来。
4.5.2.3程序设计
4.5.2.3.1 DAC的HAL库驱动
DAC在HAL库中的驱动代码在stm32f4xx_hal_dac.c和stm32f4xx_hal_dac_ex.c文件(及其头文件)中。
1. HAL_DAC_Init函数
DAC的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Init(DAC_HandleTypeDef *hdac);
- 函数描述:用于初始化DAC。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
DAC_TypeDef *Instance; /* DAC寄存器基地址 */
__IO HAL_DAC_StateTypeDef State; /* DAC 工作状态 */
HAL_LockTypeDef Lock; /* DAC锁定对象 */
DMA_HandleTypeDef *DMA_Handle1; /* 通道1的DMA处理句柄指针 */
DMA_HandleTypeDef *DMA_Handle2; /* 通道2的DMA处理句柄指针 */
__IO uint32_t ErrorCode; /* DAC错误代码 */
} DAC_HandleTypeDef;
从该结构体看到该函数并没有设置任何DAC相关寄存器,即没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC,为后面HAL库操作DAC做好准备。- 函数返回值:HAL_StatusTypeDef枚举类型的值。
- 注意事项:DAC的MSP初始化函数HAL_DAC_MspInit,该函数声明如下:
void HAL_DAC_MspInit(DAC_HandleTypeDef* hdac);
- 2. HAL_DAC_ConfigChannel函数DAC 的通道参数初始化函数,其声明如下:
• HAL_StatusTypeDef HAL_DAC_ConfigChannel(DAC_HandleTypeDef *hdac,
DAC_ChannelConfTypeDef *sConfig, uint32_t Channel);
- 函数描述:该函数用来配置DAC通道的触发类型以及输出缓冲。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量。形参2是DAC_ChannelConfTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
uint32_t DAC_Trigger; /* DAC触发源的选择 */
uint32_t DAC_OutputBuffer; /* 输出缓冲*/
} DAC_ChannelConfTypeDef;
形参3用于选择要配置的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。- 函数返回值:HAL_StatusTypeDef枚举类型的值。3. HAL_DAC_Start函数使能启动DAC转换通道函数,其声明如下:
• HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef *hdac, uint32_t Channel);
- 函数描述:使能启动DAC转换通道。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要启动的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。4. HAL_DAC_SetValue函数DAC的通道输出值函数,其声明如下:
• HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef *hdac, uint32_t Channel,
uint32_t Alignment, uint32_t Data);
- 函数描述:配置DAC的通道输出值。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要输出的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
形参3用于指定数据对齐方式。
形参4设置要加载到选定数据保存寄存器中的数据。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。5. HAL_DAC_GetValue函数DAC读取通道输出值函数,其声明如下:
• uint32_t HAL_DAC_GetValue(DAC_HandleTypeDef *hdac, uint32_t Channel);
- 函数描述:获取所选DAC通道的最后一个数据输出值。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要读取的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。 - 函数返回值:获取到的输出值。
DAC输出配置步骤
1)开启DAC和输出通道的GPIO时钟,配置该IO口的模拟输出功能
首先开启DAC和GPIO的时钟,然后配置GPIO为模拟模式。本实验我们默认用到DAC1通道1,对应IO是PA4,它们的时钟开启方法如下:
__HAL_RCC_DAC_CLK_ENABLE (); /*使能DAC1时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /*开启GPIOA时钟 */
IO口模拟输出功能是通过函数HAL_GPIO_Init来配置的。
2)初始化DAC
通过HAL_DAC_Init函数来设置需要初始化的DAC。该函数并没有设置任何DAC相关寄存器,也就是说没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC。
注意:该函数会调用HAL_DAC_MspInit函数来存放DAC和对应通道的IO时钟使能和初始化IO等代码。
3)配置DAC通道并启动DA转换器
在HAL库中,通过HAL_DAC_ConfigChannel函数来设置配置DAC的通道,根据需求设置触发类型以及输出缓冲。
配置好DAC通道之后,通过HAL_DAC_Start函数启动DA转换器。
4)设置DAC的输出值
通过HAL_DAC_SetValue函数设置DAC的输出值。
4.5.2.3.2 程序流程图
图4.5.2.3.2.1 DAC输出实验程序流程图
4.5.2.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC驱动源码包括两个文件:dac.c和dac.h。另外我们使用ADC1来测量DAC输出的电压是否准确,所以还需要用到单通道ADC采集实验里面的adc.c和adc.h文件,关于ADC的程序这里就不再讲解。
dac.h文件只有一些函数声明,下面直接开始介绍dac.c的程序,首先是DAC初始化函数。
/**
初始化函数
要初始化的通道. 1,通道1; 2,通道2
无
*/
void dac_init(uint8_t outx)
{
GPIO_InitTypeDef gpio_init_struct;
DAC_ChannelConfTypeDef dac_ch_conf;
__HAL_RCC_DAC_CLK_ENABLE(); /* 使能DAC1的时钟 */
/* 使能DAC OUT1/2的IO口时钟(都在PA口,PA4/PA5) */
__HAL_RCC_GPIOA_CLK_ENABLE();
/* STM32单片机, 总是PA4=DAC1_OUT1, PA5=DAC1_OUT2 */
gpio_init_struct.Pin = (outx==1)? GPIO_PIN_4 : GPIO_PIN_5;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
g_dac_handle.Instance = DAC;
HAL_DAC_Init(&g_dac_handle); /* 初始化DAC */
dac_ch_conf.DAC_Trigger = DAC_TRIGGER_NONE; /* 不使用触发功能 */
dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE; /* 关闭输出缓冲 */
switch(outx)
{
case 1: /* DAC通道1配置 */
HAL_DAC_ConfigChannel(&g_dac_handle,&dac_ch_conf,DAC_CHANNEL_1);
HAL_DAC_Start(&g_dac_handle,DAC_CHANNEL_1); /* 开启DAC通道1 */
break;
case 2: /* DAC通道2配置 */
HAL_DAC_ConfigChannel(&g_dac_handle,&dac_ch_conf,DAC_CHANNEL_2);
HAL_DAC_Start(&g_dac_handle,DAC_CHANNEL_2); /* 开启DAC通道2 */
break;
default:break;
}
}
该函数主要调用HAL_DAC_Init和HAL_DAC_ConfigChannel函数初始化DAC,并调用HAL_DAC_Start函数使能DAC通道。HAL_DAC_Init函数会调用HAL_DAC_MspInit回调函数,该函数用于存放DAC和对应通道的IO时钟使能和初始化IO等代码。本实验为了让dac_init函数支持DAC的OUT1/2两个通道的初始化,就没有用到该函数。
下面是设置DAC通道1/2输出电压函数,其定义如下:
/**
设置通道1/2输出电压
通道1; 2,通道2
代表0~3.3V
无
*/
void dac_set_voltage(uint8_t outx, uint16_t vol)
{
double temp = vol;
temp /= 1000;
temp = temp * 4096 / 3.3;
if (temp >= 4096)temp = 4095; /* 如果值大于等于4096, 则取4095 */
if (outx == 1) /* 通道1 */
{
/* 12位右对齐数据格式设置DAC值 */
HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,temp);
}
else /* 通道2 */
{
/* 12位右对齐数据格式设置DAC值 */
HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_2,DAC_ALIGN_12B_R,temp);
}
}
该函数实际就是将电压值转换为DAC输入值,形参1用于设置通道,形参2设置要输出的电压值,设置的范围:0~3300,代表0~3.3V。
最后在main函数里面编写如下代码:
int main(void)
{
uint16_t adcx;
float temp;
uint8_t t = 0;
uint16_t dacval = 0;
uint8_t key;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
adc_init(); /* 初始化ADC */
dac_init(1); /* 初始化DAC1_OUT1通道 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "DAC TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1:+ KEY0:-", RED);
lcd_show_string(30, 130, 200, 16, 16, "DAC VAL:", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "DAC VOL:0.000V", BLUE);
lcd_show_string(30, 170, 200, 16, 16, "ADC VOL:0.000V", BLUE);
while (1)
{
t++;
key = key_scan(0); /* 按键扫描 */
if (key == KEY1_PRES) /* 按键1按下 */
{
if (dacval < 4000)
{
dacval += 200; /* 输出增大200 */
}
HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R,
dacval);
}
else if (key == KEY0_PRES) /* 按键0按下 */
{
if (dacval > 200)
{
dacval -= 200; /* 输出减少200 */
}
else
{
dacval = 0;
}
HAL_DAC_SetValue(&g_dac_handle, DAC_CHANNEL_1, DAC_ALIGN_12B_R,
dacval);
}
/* KEY0/KEY1按下了,或者定时时间到了 */
if (t == 10 || key == KEY0_PRES || key == KEY1_PRES)
{
/* 读取前面设置DAC1_OUT1的值 */
adcx = HAL_DAC_GetValue(&g_dac_handle, DAC_CHANNEL_1);
lcd_show_xnum(94, 130, adcx, 4, 16, 0, BLUE); /* 显示DAC寄存器值 */
temp = (float)adcx * (3.3 / 4096); /* 得到DAC电压值 */
adcx = temp;
lcd_show_xnum(94, 150, temp, 1, 16, 0, BLUE); /* 显示电压值整数部分 */
temp -= adcx;
temp *= 1000;
/* 显示电压值的小数部分 */
lcd_show_xnum(110, 150, temp, 3, 16, 0X80, BLUE);
/* 得到ADC1通道3的转换结果 */
adcx = adc_get_result_average(ADC_ADCX_CHY, 20);
temp = (float)adcx * (3.3 / 4096); /* 得到ADC电压值 */
adcx = temp;
lcd_show_xnum(94, 170, temp, 1, 16, 0, BLUE); /* 显示电压值整数部分 */
temp -= adcx;
temp *= 1000;
/* 显示电压值的小数部分 */
lcd_show_xnum(110, 170, temp, 3, 16, 0X80, BLUE);
LED0_TOGGLE(); /* LED0闪烁 */
t = 0;
}
delay_ms(10);
}
}
此部分代码,我们通过KEY0和KEY1来实现对DAC输出的幅值控制。按下KEY1增加,按KEY0减小。同时在LCD上面显示DHR12R1寄存器的值、DAC设置输出电压以及ADC采集到的DAC输出电压。
4.5.2.4下载验证
下载代码后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示如图4.5.2.4.1所示:
图4.5.2.4.1 DAC输出实验测试图
验证试验前记得先通过杜邦线连接PA3和PA4,然后我们可以通过按KEY1按键,增加DAC输出的电压,这时ADC采集到的电压也会增大,通过按KEY0减小DAC输出的电压,这时ADC采集到的电压也会减小。
4.5.3 DAC输出三角波实验
本实验我们来学习使用如何让DAC输出三角波,DAC初始化部分还是用DAC输出实验的,所以做本实验的前提是先学习DAC输出实验。
4.5.3.1 DAC寄存器
本实验用到的寄存器在DAC输出实验都有介绍。
4.5.3.2硬件设计
1. 例程功能
使用DAC输出三角波,通过KEY0/KEY1两个按键,控制DAC1的通道1输出两种三角波,需要通过示波器接PA4进行观察。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
– PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)独立按键 :
KEY0 - PE2
KEY1 - PE3
5)DAC1 :
通道1 - PA4
3. 原理图
我们只需要把示波器的探头接到DAC1通道1(PA4)引脚,就可以在示波器上显示DAC输出的波形。
4.5.3.3程序设计
本实验用到的DAC的HAL库API函数前面都介绍过,具体调用情况请看程序解析部分。下面介绍DAC输出三角波的配置步骤。
DAC输出三角波配置步骤
1)开启DACx和输出通道的GPIO时钟,配置该IO口的模拟输出功能
首先开启DACx和GPIO的时钟,然后配置GPIO为模拟模式。本实验我们默认用到DAC1通道1,对应IO是PA4,它们的时钟开启方法如下:
__HAL_RCC_DAC_CLK_ENABLE (); /*使能DAC1时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /*开启GPIOA时钟 */
IO口模拟输出功能是通过函数HAL_GPIO_Init来配置的。
2)初始化DACx
通过HAL_DAC_Init函数来设置需要初始化的DAC。该函数并没有设置任何DAC相关寄存器,也就是说没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC。
注意:该函数会调用HAL_DAC_MspInit函数来存放DAC和对应通道的IO时钟使能和初始化IO等代码。
3)配置DAC通道并启动DA转换器
在HAL库中,通过HAL_DAC_ConfigChannel函数来设置配置DAC的通道,根据需求设置触发类型以及输出缓冲。
配置好DAC通道之后,通过HAL_DAC_Start函数启动DA转换器。
4)设置DAC的输出值
通过HAL_DAC_SetValue函数设置DAC的输出值。这里我们根据三角波的特性,创建了dac_triangular_wave函数用于控制输出三角波。
4.5.3.3.1 程序流程图
图4.5.3.3.1.1 DAC输出三角波实验程序流程图
4.5.3.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC驱动源码包括两个文件:dac.c和dac.h。本实验代码在DAC输出实验代码后进行追加。
dac.h文件只有一些声明,下面直接开始介绍dac.c的程序,本实验的DAC初始化我们还是用到dac_init函数,就添加了一个设置DAC_OUT1输出三角波函数,其定义如下:
/**
设置DAC_OUT1输出三角波
输出频率 ≈ 1000 / (dt * samples) Khz, 不过在dt较小的时候,比如小于5us
时, 由于delay_us本身就不准了(调用函数,计算等都需要时间,延时很小的时候,这些
时间会影响到延时), 频率会偏小.
*
最大值(0 < maxval < 4096), (maxval + 1)必须大于等于samples/2
每个采样点的延时时间(单位: us)
采样点的个数, samples必须小于等于(maxval + 1) * 2 ,
且maxval不能等于0
输出波形个数,0~65535
*
无
*/
void dac_triangular_wave(uint16_t maxval, uint16_t dt, uint16_t samples,
uint16_t n)
{
uint16_t i, j;
float incval; /* 递增量 */
float Curval; /* 当前值 */
if(samples > ((maxval + 1) * 2))return ; /* 数据不合法 */
incval = (maxval + 1) / (samples / 2); /* 计算递增量 */
for(j = 0; j < n; j++)
{
Curval = 0; /* 先输出0 */
HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
for(i = 0; i < (samples / 2); i++) /* 输出上升沿 */
{
Curval += incval; /* 新的输出值 */
HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,
Curval);
delay_us(dt);
}
for(i = 0; i < (samples / 2); i++) /* 输出下降沿 */
{
Curval -= incval; /* 新的输出值 */
HAL_DAC_SetValue(&g_dac_handle,DAC_CHANNEL_1,DAC_ALIGN_12B_R,
Curval);
delay_us(dt);
}
}
}
该函数用于设置DAC通道1输出三角波,输出频率 ≈ 1000 / (dt * samples) Khz,形参意义在源码已经有详细注释。该函数中,我们使用HAL_DAC_SetValue函数来设置DAC的输出值,这样得到的三角波在示波器上可以看到。如果有跳动现象(不平稳),是正常的,因为调用函数,计算等都需要时间,这样就会导致输出的波形是不太稳定的。越高性能的MCU,得到的波形会越稳定。除此之外,用HAL库函数操作的效率没有直接操作寄存器高,如果对波形质量要求较高,可以直接操作DHR12R1寄存器,这样得到的波形会相对稳定些。
最后在main.c里面编写如下代码:
int main(void)
{
uint8_t t = 0;
uint8_t key;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
dac_init(1); /* 初始化DAC1_OUT1通道 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "DAC Triangular WAVE TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY0:Wave1 KEY1:Wave2", RED);
lcd_show_string(30, 130, 200, 16, 16, "DAC None", BLUE); /* 默认提示无输出 */
while (1)
{
t++;
key = key_scan(0); /* 按键扫描 */
if (key == KEY0_PRES) /* 高采样率 , 约88.5Hz波形 */
{
lcd_show_string(30, 130, 200, 16, 16, "DAC Wave1 ", BLUE);
/* 幅值4095, 采样点间隔5us, 2000个采样点, 100个波形 */
dac_triangular_wave(4095, 5, 2000, 100);
lcd_show_string(30, 130, 200, 16, 16, "DAC None ", BLUE);
}
else if (key == KEY1_PRES) /* 低采样率 , 约100Hz波形 */
{
lcd_show_string(30, 130, 200, 16, 16, "DAC Wave2 ", BLUE);
/* 幅值4095, 采样点间隔500us, 20个采样点, 100个波形 */
dac_triangular_wave(4095, 500, 20, 100);
lcd_show_string(30, 130, 200, 16, 16, "DAC None ", BLUE);
}
if (t == 10) /* 定时时间到了 */
{
LED0_TOGGLE(); /* LED0闪烁 */
t = 0;
}
delay_ms(10);
}
}
该部分代码功能是,按下KEY0后,DAC输出三角波1,按下KEY1后,DAC输出三角波2,将dac_triangular_wave的形参代入公式:输出频率 ≈ 1000 / (dt * samples) Khz,得到三角波1和三角波2的频率都是0.1KHZ,由于三角波1采样点之间时延比较小,频率会偏小。
4.5.3.4下载验证
下载代码后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示如图4.5.3.4.1所示:
图4.5.3.4.1 DAC输出三角波实验测试图
没有按下任何按键之前,LCD屏显示DAC None,当按下KEY0后,DAC输出三角波1,LCD屏显示DAC Wave1,三角波1输出完成后LCD屏继续显示DAC None,当按下KEY1后,DAC输出三角波2,LCD屏显示DAC Wave2,三角波2输出完成后LCD屏继续显示DAC None。
其中三角波1和三角波2在示波器的显示情况如下图所示:
图4.5.3.4.2 DAC输出的三角波1
图4.5.3.4.3 DAC输出的三角波2
由上面两副测试图可以知道,三角波1的频率是88.5Hz,三角波2的频率是99.9Hz。三角波2基本接近我们算出来的结果0.1KHz,三角波1有较大误差,在介绍dac_triangular_wave函数时也说了原因,加上三角波1的采样率比较高,所以误差就会比较大。
4.5.4 DAC输出正弦波实验
本实验我们来学习使用如何让DAC输出正弦波。实验将用定时器2的更新事件来触发DAC进行转换,输出正弦波,并以DMA的方式传输数据。
4.5.4.1 DAC寄存器
本实验用到的寄存器在DAC输出实验都有介绍。
4.5.4.2硬件设计
1. 例程功能
使用DAC1的通道1输出正弦波,需要通过示波器接PA4进行观察。TFTLCD显示实验名称。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
– PE0
2)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)定时器2
5)DAC1 :
通道1 - PA4
6)DMA(DMA1_Channel7)
3. 原理图
我们只需要把示波器的探头接到DAC1通道1(PA4)引脚,就可以在示波器上显示DAC输出的波形。
4.5.4.3程序设计
4.5.4.3.1 DAC的HAL库驱动
下面将介绍本实验用到且没有介绍过的HAL库API函数。
1. HAL_DAC_Start_DMA函数
启动DAC使用DMA方式传输函数,其声明如下:
HAL_StatusTypeDef HAL_DAC_Start_DMA(DAC_HandleTypeDef *hdac, uint32_t Channel,
uint32_t *pData, uint32_t Length, uint32_t Alignment);
- 函数描述:用于启动DAC使用DMA的方式。
- 函数形参:形参1是DAC_HandleTypeDef结构体类型指针变量。
形参2用于选择要启动的通道,可选择DAC_CHANNEL_1或者DAC_CHANNEL_2。
形参3是使用DAC输出数据缓冲区的指针。
形参4是DAC输出数据的长度。
形参5是指定DAC通道的数据对齐方式,有:DAC_ALIGN_8B_R(8位右对齐)、DAC_ALIGN_12B_L(12位左对齐)和DAC_ALIGN_12B_R(12位右对齐)三种方式。 - 函数返回值:HAL_StatusTypeDef枚举类型的值。2. HAL_TIMEx_MasterConfigSynchronization函数配置主模式下的定时器触发输出选择函数,其声明如下:
• HAL_StatusTypeDef HAL_TIMEx_MasterConfigSynchronization(
TIM_HandleTypeDef *htim, TIM_MasterConfigTypeDef *sMasterConfig);
- 函数描述:用于配置主模式下的定时器触发输出选择。
- 函数形参:形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是TIM_MasterConfigTypeDef结构体类型指针变量,用于配置定时器工作在主/从模式,以及触发输出(TRGO和TRGO2)的选择。 - 函数返回值:
HAL_StatusTypeDef枚举类型的值。
DAC输出正弦波配置步骤
1)开启DACx、DMA和输出通道的GPIO时钟,配置该IO口的模拟输出功能
首先开启DACx、DMA和GPIO的时钟,然后配置GPIO为模拟模式。本实验我们默认用到DAC1通道1,对应IO是PA4,它们的时钟开启方法如下:
__HAL_RCC_DAC_CLK_ENABLE (); /*使能DAC时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /*开启GPIOA时钟 */
__HAL_RCC_DMA1_CLK_ENABLE (); /*开启DMA1时钟 */
IO口模拟输出功能是通过函数HAL_GPIO_Init来配置的。
2)配置DMA并关联DAC
通过HAL_DMA_Init函数初始化DMA,包括配置通道,外设地址,存储器地址,传输数据量等。
HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。这个宏定义为__HAL_LINKDMA。
3)初始化DACx
通过HAL_DAC_Init函数来设置需要初始化的DAC。该函数并没有设置任何DAC相关寄存器,也就是说没有对DAC进行任何配置,它只是HAL库提供用来在软件上初始化DAC。
注意:该函数会调用HAL_DAC_MspInit函数来存放DAC和对应通道的IO时钟使能和初始化IO等代码。
4)配置定时器控制触发DAC
通过HAL_TIM_Base_Init函数设置定时器溢出频率。
通过HAL_TIMEx_MasterConfigSynchronization函数配置定时器溢出事件用做触发器。
通过HAL_TIM_Base_Start函数启动计数。
5)产生正弦波序列
通过dac_creat_sin_buf函数产生一组正弦波序列,其本质上就是正弦波曲线上的点。
6)配置DAC通道并开启DMA传输
通过HAL_DAC_ConfigChannel函数来设置配置DAC的通道,根据需求设置触发类型以及输出缓冲等。
再通过HAL_DAC_Start_DMA函数启动DMA传输以及DAC输出。
4.5.4.3.2 程序流程图
图4.5.4.3.2.1 DAC输出正弦波实验程序流程图
4.5.4.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC驱动源码包括两个文件:dac.c和dac.h。
dac.h文件只有一些声明,下面直接开始介绍dac.c的程序,首先看产生正弦波序列的函数,其定义如下:
uint16_t g_dac_sin_buf[4096]; 正弦波序列数据缓冲区 */
/**
产生正弦波函序列
需保证: maxval > samples/2
*
* @param maxval : 峰值(0 < maxval < 2048)
采样点的个数
*
无
*/
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples)
{
uint8_t i;
float outdata = 0;
if( maxval <= (samples/2) )return ; /* 数据不合法 */
/*
正弦波最小正周期为2π,约等于2 * 3.1415926
曲线上相邻的两个点在x轴上的间隔 = 2 * 3.1415926 / 采样点数量
* DAC无法输出负电压,所以需要将曲线向上偏移一个峰值的量,让整个曲线都落在正数区域
*/
float inc = (2 * 3.1415926) / samples; /* 计算相邻两个点的x轴间隔 */
for (i = 0; i < samples; i++) /* 连续打samples个点 */
{
/*
正弦波函数解析式:y = Asin(wx + φ)+ b
计算每个点的y值,将峰值放大maxval倍,并将曲线向上偏移maxval到正数区域
*/
outdata = maxval * sin(inc * i) + maxval;
if (outdata > 4095)
{
outdata = 4095; /* y值上限限定
}
g_dac_sin_buf[i] = outdata;
}
}
该函数用于产生正弦波序列,即正弦波曲线上的各个采样点,各个点最终控制的是DAC_DHR12R1寄存器的值。在DAC输出频率固定、波形周期一定的情况下,采样点越多,实际的曲线越接近于正弦波,但是波形的频率也会越小。
产生正弦波序列函数的实现思路请看注释,值得注意的是,DAC是无法输出负电压的,所以我们需要将正弦波的曲线向上偏移一个峰值maxval的量,让整个曲线都落在正数区域。
下面介绍DAC DMA初始化函数,其定义如下:
/**
初始化函数
要初始化的通道.1,通道1; 2,通道2
通道单次传输数据量(采样点数量)
无
*/
void dac_init(uint8_t outx,uint16_t cndtr)
{
GPIO_InitTypeDef gpio_init_struct;
DAC_ChannelConfTypeDef DACCH1_Config;
__HAL_RCC_DAC_CLK_ENABLE(); /* 使能DAC的时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能DAC OUT1/2的IO口时钟 */
__HAL_RCC_DMA1_CLK_ENABLE();
/* STM32单片机, 总是PA4=DAC1_OUT1, PA5=DAC1_OUT2 */
gpio_init_struct.Pin = (outx==1)? GPIO_PIN_4 : GPIO_PIN_5;
gpio_init_struct.Mode = GPIO_MODE_ANALOG;
gpio_init_struct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
g_dma_dac_handle.Instance = DMA1_Stream5; /* DMA1数据流5 */
g_dma_dac_handle.Init.Channel = DMA_CHANNEL_7; /* 通道7 */
g_dma_dac_handle.Init.Direction = DMA_MEMORY_TO_PERIPH;/* 从存储器到外设 */
g_dma_dac_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
g_dma_dac_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
外设数据长度:16位 */
g_dma_dac_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_dac_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_dac_handle.Init.Mode = DMA_CIRCULAR; /* 循环模式 */
g_dma_dac_handle.Init.Priority = DMA_PRIORITY_LOW; /* 中等优先级 */
g_dma_dac_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 不开启FIFO */
HAL_DMA_Init(&g_dma_dac_handle); /* 初始化DMA */
/* 把DAC和DMA关联 */
__HAL_LINKDMA(&g_dac_dma_handle,DMA_Handle1,g_dma_dac_handle);
g_dac_dma_handle.Instance = DAC;
HAL_DAC_Init(&g_dac_dma_handle); /* 初始化DAC */
/* 使用定时器2的TRGO事件触发DAC转换 */
DACCH1_Config.DAC_Trigger = DAC_TRIGGER_T2_TRGO;
/* DAC1输出缓冲关闭 */
DACCH1_Config.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;
dac_creat_sin_buf(2048,cndtr); /* 产生正弦波序列,即画点 */
switch(outx)
{
case 1:
HAL_DAC_ConfigChannel(&g_dac_dma_handle,&DACCH1_Config,
DAC_CHANNEL_1); /* DAC通道1配置 */
HAL_DAC_Start_DMA(&g_dac_dma_handle,DAC_CHANNEL_1,
(uint32_t *)g_dac_sin_buf,cndtr,DAC_ALIGN_12B_R); /* 开启DAC通道1 */
break;
case 2:
HAL_DAC_ConfigChannel(&g_dac_dma_handle,&DACCH1_Config
DAC_CHANNEL_2); /* DAC通道2配置 */
HAL_DAC_Start_DMA(&g_dac_dma_handle,DAC_CHANNEL_2,
(uint32_t *)g_dac_sin_buf,cndtr,DAC_ALIGN_12B_R); /* 开启DAC通道2 */
break;
default:break;
}
}
该函数用于初始化DAC用DMA的方式输出正弦波,我们采用定时器2触发DAC进行转换输出。本函数用到的API函数起前面都介绍过,请结合前面介绍过的相关内容来理解源码。值得注意的是,这里调用了dac_creat_sin_buf函数来产生正弦波序列,采样点的个数通过入口参数cndtr传进来。
接着看定时器相关的gtim.c文件,我们只关注通用定时器的初始化函数,其定义如下:
/**
通用定时器TIMX初始化函数
自动重装值。
时钟预分频数
无
*/
void gtim_timx_int_init(uint16_t arr, uint16_t psc)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
GTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
g_timx_handle.Instance = GTIM_TIMX_INT; /* 通用定时器x */
g_timx_handle.Init.Prescaler = psc; /* 预分频系数 */
g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_timx_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_timx_handle);
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; /* 更新事件用于触发 */
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&g_timx_handle, &sMasterConfig);
HAL_TIM_Base_Start(&g_timx_handle); /* 使能定时器x */
}
该函数调用了HAL_TIM_Base_Init函数对定时器2的基本参数进行了初始化,并通过HAL_TIMEx_MasterConfigSynchronization函数配置TIM2,将其更新事件作为DAC输出的触发源,然后开启定时器计数。
最后在main.c里面编写如下代码:
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
gtim_timx_int_init(10-1,84-1); /* 初始化定时器,定时触发DAC */
dac_init(1,100); /* 初始化DAC1_OUT1通道,100个点,正弦波频率1k Hz */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "DAC DMA Sine WAVE TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
while (1)
{
LED0_TOGGLE(); /* LED0翻转 */
delay_ms(1000);
}
}
gtim_timx_int_init函数初始化定时器2,其计数频率为1M Hz,计数溢出(发生一次更新事件)的频率为100K Hz,不记得怎么计算的朋友,请回顾基本定时器的相关内容,这里直接把公式列出:
Tout= ((arr+1)*(psc+1))/Tclk
看到dac_init(1,100);这个语句,第一个形参是DAC的通道,本实验使用通道1;第二个形参是采集点的个数,本实验默认的采集点个数为100。可以得到正弦波的频率为100KHz/100 = 1KHz。
4.5.4.4下载验证
下载代码后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示如图4.5.4.4.1所示:
图4.5.4.4.1 DAC输出三角波实验测试图
接下来我们借助示波器来观察波形,注意要将探头接到PA4的排针上(注意开发板和示波器共地)。实际输出的1KHz(100个采样点)正弦波如下图所示:
图4.5.4.4.2 DAC输出的1K Hz正弦波