正点原子STM32(基于HAL库)4

2023-11-10

目录

ADC 实验

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

ADC 简介

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

在这里插入图片描述

图中,我们按照ADC 的配置流程标记了七处位置,分别如下:
①输入电压
在前面ADC 的主要特性也对输入电压有所提及,ADC 输入范围VREF–≤VIN≤VREF+,最终
还是由VREF–、VREF+、VDDA和VSSA决定的。下面看一下这几个参数的关系,如图30.1.2 所示:
在这里插入图片描述
从上图可以知道,VDDA和VREF+接VCC3.3,而VSSA和VREF-是接地,所以ADC 的输入范
围即0~3.3V。
②输入通道
在确定好了ADC 输入电压后,如何把外部输入的电压输送到ADC 转换器中呢,在这里引
入了ADC 的输入通道,在前面也提及到了ADC1 和ADC2 都有16 个外部通道和2 个内部通
道;而ADC3 就有8 个外部通道。外部通道对应的是上图中的ADCx_IN0、ADCx_IN1…
ADCx_IN15。ADC1 的通道16 就是内部通道,连接到芯片内部的温度传感器,通道17 连接到
Vrefint。而ADC2 的通道16 和17 连接到内部的VSS。ADC3 的通道9、14、15、16 和17 连接
到的是内部的VSS。具体的ADC 通道表见表30.1.1 所示:
在这里插入图片描述
③转换顺序
当ADC 的多个通道以任意顺序进行转换就诞生了成组转换,这里有两种成组转换类型:
规则组和注入组。规则组就是图中的规则通道,注入组就是图中的注入通道。为了避免大家对
输入通道,以及规则通道和注入通道的理解混淆,后面规则通道以规则组来代称,注入通道以
注入组来代称。
规则组最多允许16 个输入通道进行转换,而注入组最多允许4 个输入通道进行转换。这里
讲解一下规则组和注入组。
规则组(规则通道)
规则组,按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最
多也是规则组。
注入组(注入通道)

注入组,按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中
断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转
换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。
为了便于理解,下面看一下规则组和注入组的执行优先级对比图,如图30.1.3 所示:
在这里插入图片描述
了解了规则组和注入组的概念后,下面来看看它们的转换顺序,即转换序列。转换序列可
以分为规则序列和注入序列。下面分别来介绍它们。
规则序列
规则组最多允许16 个输入通道进行转换,那么就需要设置通道转换的顺序,即规则序列。
规则序列寄存器有3 个,分别为SQR1、SQR2 和SQR3。SQR3 控制规则序列中的第1 个到第
6 个转换;SQR2 控制规则序列中第7 个到第12 个转换;SQR1 控制规则序列中第13 个到第16
个转换。规则序列寄存器控制关系汇总如表30.1.2 所示:
在这里插入图片描述
从上表可以知道,当我们想设置ADC 的某个输入通道在规则序列的第1 个转换,只需要
把相应的输入通道号的值写入SQR3 寄存器中的SQ1[4:0]位即可。例如想让输入通道5 先进行转换,那么就可以把5 这个数值写入SQ1[4:0]位。如果还想让输入通道8 在第2 个转换,那么
就可以把8 这个数值写入SQ2[4:0]位。最后还要设置你的这个规则序列的输入通道个数,只需
把输入通道个数写入SQR1 的SQL[3:0]位。注意:写入0 到SQL[3:0]位,表示这个规则序列有
1 个输入通道的意思,而不是0 个输入通道。
注入序列
注入序列,跟规则序列差不多,决定的是注入组的顺序。注入组最大允许4 个通道输入,
它的注入序列由JSQR 寄存器配置。注入序列寄存器JSQR 控制关系如表30.1.3 所示:

在这里插入图片描述
注入序列有多少个输入通道,只需要把输入通道个数写入到JL [ 1 : 0 ]位,范围是0~3。注
意:写入0 表示这个注入序列有一个输入通道,而不是0 个输入通道。这个内容很简单。编程
时容易犯错的是注入序列的转换顺序问题,下面给大家讲解一下。
如果JL[ 1 : 0 ]位的值小于3,即设置注入序列要转换的通道个数小于4,则注入序列的转
换顺序是从JSQx[ 4 : 0 ](x=4-JL[1:0])开始。例如:JL [ 1 : 0 ]=10、JSQ4 [ 4 : 0 ]= 00100、JSQ3 [ 4 : 0 ]= 00011、JSQ2 [ 4 : 0 ]= 00111、JSQ1 [ 4 : 0 ]= 00010,意味着这个注入序列的转换顺序
是:7、3、4,而不是2、7、3。如果JL[ 1 : 0 ]=00,那么转换顺序是从JSQ4 [ 4 : 0 ]开始。
④触发源
在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC 的触发转换有两种方法:
分别是通过ADON 位或外部事件触发转换。
(1)ADON 位触发转换
当ADC_CR2 寄存器的ADON 位为1 时,再独立给ADON 位写1(其它位不能一起改变,
这是为了防止误触发),这时会启动转换。这种控制ADC 启动转换的方式非常简单。
(2)外部触发转换
另一种方法是通过外部事件触发转换,例如定时器捕获、EXTI 线和软件触发,可以分为规
则组外部触发和注入组外部触发。
规则组外部触发使用方法是将EXTTRIG 位置1,并且通过EXTSET[2:0]位选择规则组启动
转换的触发源。如果EXTSET[2:0]位设置为111,那么可以通过SWSTART 为启动ADC 转换,
相当于软件触发。
注入组外部触发使用方法是将JEXTTRIG 位置1,并且通过JEXTSET[2:0]位选择注入组启
动转换的触发源。如果JEXTSET[2:0]位设置为111,那么可以通过JSWSTART 为启动ADC 转
换,相当于软件触发。
ADC1 和ADC2 的触发源是一样的,ADC3 的触发源和ADC1/2 有所不同,这个需要注意。
⑤转换时间
(1)ADC 时钟
在学习转换时间之前,我们先来了解ADC 时钟。从标号⑤框出来部分可以看到ADC 时钟是
要经过ADC 预分频器的,那么ADC 的时钟源是什么?ADC 预分频器的分频系数可以设置的
范围又是多少?以及ADC 时钟频率的最大值又是多少?下面将为大家解答。
ADC 的输入时钟是由PCLK2 经过分频产生的,分频系数是由RCC_CFGR 寄存器的
ADCPRE[1:0]位设置的,可选择2/4/8/16 分频。需要注意的是,ADC 的输入时钟频率最大值是
14MHz,如果超过这个值将会导致ADC 的转换结果准确度下降。
一般我们设置PCLK2 为72MHz。为了不超过ADC 的最大输入时钟频率14MHz,我们设
置ADC 的预分频器分频系数为6,就可以得到ADC 的输入时钟频率为72MHz/6,即12MHz。
例程中,我们也是如此设置的。

(2)转换时间
STM32F103 的ADC 总转换时间的计算公式如下:
在这里插入图片描述
⑥数据寄存器
ADC 转换完成后的数据输出寄存器。根据转换组的不同,规则组的完成转换的数据输出到
ADC_DR 寄存器,注入组的完成转换的数据输出到ADC_JDRx 寄存器。假如是使用双重模式,
规则组的数据也是存放在ADC_DR 寄存器。下面给大家简单介绍一下这两个寄存器。
(1)ADC 规则数据寄存器(ADC_DR)
ADC 规则组数据寄存器ADC_DR 是一个32 位的寄存器,独立模式时只使用到该寄存器低
16 位保存ADC1/2/3 的规则转换数据。在双ADC 模式下,高16 位用于保存ADC2 转换的数
据,低16 位用于保存ADC1 转换的数据。
因为ADC 的精度是12 位的,ADC_DR 寄存器无论高16 位还是低16,存放数据的位宽都
是16 位的,所以允许选择数据对齐方式。由ADC_CR2 寄存器的ALIGN 位设置数据对齐方式,
可选择:右对齐或者左对齐。
细心的朋友可能发现,规则组最多有16 个输入通道,而ADC 规则数据寄存器只有一个,
如果一个规则组用到好几个通道,数据怎么读取?如果使用多通道转换,那么这些通道的数据
也会存放在DR 里面,按照规则组的顺序,上一个通道转换的数据,会被下一个通道转换的数
据覆盖掉,所以当通道转换完成后要及时把数据取走。比较常用的方法是使用DMA 模式。当
规则组的通道转换结束时,就会产生DMA 请求,这样就可以及时把转换的数据搬运到用户指
定的目的地址存放。注意:只有ADC1 和ADC3 可以产生DAM 请求,而由ADC2 转换的数据
可以通过双ADC 模式,利用ADC1 的DMA 功能传输。
(2)ADC 注入数据寄存器x(ADC_JDRx)(x=1~4)
ADC 注入数据寄存器有4 个,注入组最多有4 个输入通道,刚好每个通道都有自己对应的
数据寄存器。ADC_JDRx 寄存器是32 位的,低16 位有效,高16 位保留,数据也同样需要选
择对齐方式。也是由ADC_CR2 寄存器的ALIGN 位设置数据对齐方式,可选择:右对齐或者左
对齐。
⑦中断
ADC 中断可分为三种:规则组转换结束中断、注入组转换结束中断、设置了模拟看门狗状
态位中断。它们都有独立的中断使能位,分别由ADC_CR 寄存器的EOCIE、JEOCIE、AWDIE
位设置,对应的标志位分别是EOC、JEOC、AWD。
模拟看门狗中断

模拟看门狗中断发生条件:首先通过ADC_LTR 和ADC_HTR 寄存器设置低阈值和高阈值,
然后开启了模拟看门狗中断后,当被ADC 转换的模拟电压低于低阈值或者高于高阈值时,就
会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V 的时候,就会产生模拟看
门狗中断,低阈值的情况类似。
DMA 请求
规则组和注入组的转换结束后,除了可以产生中断外,还可以产生DMA 请求,我们利用
DMA 及时把转换好的数据传输到指定的内存里,防止数据被覆盖。
注意:只有ADC1 和ADC3 可以产生DAM 请求,DMA 相关的知识请回顾DMA 实验。
⑧单次转换模式和连续转换模式
单次转换模式和连续转换模式在框图中是没有标号,为了更好地学习后续的内容,这里简
单给大家讲讲。
(1)单次转换模式
通过将ADC_CR2 寄存器的CONT 位置0 选择单次转换模式。该模式下,ADC 只执行一
次转换,由ADC_CR2 寄存器的ADON 位启动(只适用于规则组),也可以通过外部触发启动
(适用于规则组或注入组)。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16 位ADC_DR 寄存器中、
EOC(转换结束)标志位被置1、如果设置了EOCIE 位,则产生中断,然后ADC 停止。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16 位ADC_DRJx 寄存器中、
JEOC(转换结束)标志位被置1、如果设置了JEOCIE 位,则产生中断,然后ADC 停止。
(2)连续转换模式
通过将ADC_CR2 寄存器的CONT 位置1 选择连续转换模式。该模式下,ADC 完成上一
个通道的转换后会马上自动地启动下一个通道的转换,由ADC_CR2 寄存器的ADON 位启动,
也可以通过外部触发启动。
如果规则组的一个输入通道被转换,那么转换的数据被储存在16 位ADC_DR 寄存器中、
EOC(转换结束)标志位被置1、如果设置了EOCIE 位,则产生中断。
如果注入组的一个输入通道被转换,那么转换的数据被储存在16 位ADC_DRJx 寄存器中、
JEOC(转换结束)标志位被置1、如果设置了JEOCIE 位,则产生中断。
⑨扫描模式
扫描模式在框图中是没有标号,为了更好地学习后续的内容,这里简单给大家讲讲。
可以通过ADC_CR1 寄存器的SCAN 位配置是否使用扫描模式。如果选择扫描模式,ADC
会扫描所有被ADC_SQRx 寄存器或ADC_JSQR 选中的所有通道,并对规则组或者注入组的每
个通道执行单次转换,然后停止转换。但如果还设置了CONT 位,即选择连续转换模式,那么
转换不会在选择组的最后一个通道上停止,而是再次从选择组的第一个通道继续转换。
如果设置了DMA 位,在每次EOC 后,DMA 控制器把规则组通道的转换数据传输到SRAM
中。而注入通道转换的数据总是存储在ADC_JDRx 寄存器中。
到这里,我们基本上介绍了ADC 的大多数基础的知识点,其它知识后面用到会继续补充,
如果还有不懂的内容,请参考《STM32F10xxx 参考手册_V10(中文版).pdf》的第11 章。

单通道ADC 采集实验

本实验我们来学习单通道ADC 采集实验。本实验使用规则组单通道的单次转换模式,并
且通过软件触发,即由ADC_CR2 寄存器的SWSTART 位启动。下面先带大家来了解本实验要
配置的寄存器。

ADC 寄存器

这里,我们只介绍本实验用到的寄存器的关键位,其它寄存器后续用到会继续介绍。
⚫ ADC 控制寄存器1(ADC_CR1)
ADC 控制寄存器1 描述如图30.2.1.1 所示:
在这里插入图片描述
SCAN 位用于选择是否使用扫描模式。本实验我们使用单通道采集,所以没必要选择扫描
模式,该位置0 即可。
DUALMOD[3:0]位用来设置ADC 的操作模式,我们的例程中ADC 相关的实验都是使用独
立模式,所以设置为0000 即可。
⚫ ADC 控制寄存器2(ADC_CR2)
ADC 控制寄存器2 描述如图30.2.1.2 所示:
在这里插入图片描述
该寄存器我们针对性的介绍一些位:ADON 位用于打开或关闭AD 转换器,还可以用于触
发ADC 转换。CONT 位用于设置单次转换模式还是连续转换模式,本实验我们使用单次转换
模式,所以CONT 位置0 即可。CAL 位用于开启AD 校准。RSTCAL 位用于判断校准寄存器是
否已初始化。ALIGN 用于设置数据对齐,我们使用右对齐,所以该位置0。EXTSEL[2:0]位用
于选择规则组启动转换的外部事件触发源,本实验使用的是软件触发(SWSTART),所以这三
个位置为111。EXTTRIG 位必须置1,EXTSEL[2:0]位才能选择软件触发(SWSTART),别漏了
这步,否则设置软件触发会不成功。SWSTART 位用于启动一次规则组通道的转换,即软件触发转换。
⚫ ADC 采样事件寄存器1(ADC_SMPR1)
ADC 采样事件寄存器1 描述如图30.2.1.3 所示:

在这里插入图片描述
⚫ ADC 采样事件寄存器2(ADC_SMPR2)
ADC 采样事件寄存器2 描述如图30.2.1.4 所示:
在这里插入图片描述
ADC 采样时间设置需要由两个寄存器设置,ADC_SMPR1 和ADC_SMPR1,分别设置通道
10~17 和通道0~9 的采样时间,每个通道用3 个位设置。可以看出ADC 的每个通道的采样时间
是支持单独设置的。
一般每个要转换的通道,采样时间建议尽量长一点,以获得较高的准确度,但是这样会降
低ADC 的转换速率,看大家怎么衡量选择了。本实验中,我们设置采样时间是239.5 个周期。
结合前面介绍过的转换时间公式:
在这里插入图片描述
⚫ ADC 规则序列寄存器1
ADC 规则序列寄存器共有3 个,这几个寄存器的功能都差不多,这里我们仅介绍一下ADC
规则序列寄存器1(ADC_SQR1),描述如图30.2.1.5 所示:
在这里插入图片描述
L[3:0]用于设置规则组序列的长度,取值范围:015,表示规则组的长度是116。本实验
只用了1 个输入通道,所以L[3:0]位设置为0000 即可。
SQ13[4:0]SQ16[4:0]位设置规则组序列的第1316 个转换编号,第1~12 个转换编号的设
置请查看ADC_SQR2 和ADC_SQR3 寄存器。设置过程非常简单,忘记了请参考前面给大家整
理出来的规则序列寄存器控制关系汇总表。
本实验我们使用单通道,ADC1 通道14,所以规则组序列里只有一个输入通道,我们将
ADC_SQR3 寄存器的SQ1[4:0]位的值设置为14 即可。
⚫ ADC 规则数据寄存器(ADC_DR)
ADC 规则数据寄存器描述如图30.2.1.6 所示:
在这里插入图片描述
在规则序列中AD 转换结果都将被存在这个寄存器里面,而注入通道的转换结果被保存在
ADC_JDRx 里面。该寄存器的数据可以通过ADC_CR2 的ALIGN 位设置左对齐还是右对齐。
在读取数据的时候要注意。
⚫ ADC 状态寄存器(ADC_SR)
ADC 状态寄存器描述如图30.2.1.7 所示:
在这里插入图片描述
该寄存器保存了ADC 转换时的各种状态。本实验我们通过EOC 位的状态来判断ADC 转
换是否完成,如果查询到EOC 位被硬件置1,就可以从ADC_DR 寄存器中读取转换结果,否
则需要等待转换完成。
至此,本实验要用到的ADC 关键寄存器全部介绍完了,对于未介绍的部分,请大家参考
《STM32F10xxx 参考手册_V10(中文版).pdf》第11 章相关内容。

硬件设计

  1. 例程功能
    采集ADC1 通道1(PA1)上面的电压,并在LCD 模块上面显示ADC 规则数据寄存器12
    位的转换值以及将该值换算成电压后的电压值。使用杜邦线将ADC 和RV1 排针连接,使得PA1
    连接到电位器上,然后将ADC 采集到的数据和转换后的电压值在TFTLCD 屏中显示。用户可
    以通过调节电位器的旋钮改变电压值。LED0 闪烁,提示程序运行。

  2. 硬件资源
    1)LED 灯
    LED0 – PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)ADC1 :
    通道1 –PA1

  3. 原理图
    ADC 属于STM32F103 内部资源,实际上我们只需要软件设置就可以正常工作,另外还需
    要将待测量的电压源连接到ADC 通道上,以便ADC 测量。本实验,我们通过ADC1 的通道1
    (PA1)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通
    过杜邦线将PA1 与电位器连接,如下图所示:

在这里插入图片描述

使用杜邦线将ADC 和RV1 排针连接好后,并下载程序后,就可以用螺丝刀调节电位器变
换多种电压值进行测量。
有的朋友可能还想测量其它地方的电压值,我们只需要1 根杜邦线,一端接到ADC 排针
上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V 的电压范围,否则可
能烧坏我们的ADC,甚至是整个主控芯片。

程序设计

30.2.3.1 ADC 的HAL 库驱动
ADC 在HAL 库中的驱动代码在stm32f1xx_hal_adc.c 和stm32f1xx_hal_adc_ex.c 文件(及
其头文件)中。

  1. HAL_ADC_Init 函数
    ADC 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);

⚫ 函数描述:
用于初始化ADC。
⚫ 函数形参:
形参1 是ADC_HandleTypeDef 结构体类型指针变量,其定义如下:

typedef struct
{
	ADC_TypeDef *Instance; /* ADC寄存器基地址*/
	ADC_InitTypeDef Init; /* ADC参数初始化结构体变量*/
	DMA_HandleTypeDef *DMA_Handle; /* DMA配置结构体*/
	HAL_LockTypeDef Lock; /* ADC锁定对象*/
	__IO uint32_t State; /* ADC工作状态*/
	__IO uint32_t ErrorCode; /* ADC错误代码*/
}ADC_HandleTypeDef;

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

typedef struct {
	uint32_t DataAlign; /* 设置数据的对齐方式*/
	uint32_t ScanConvMode; /* 扫描模式*/
	FunctionalState ContinuousConvMode; /* 开启连续转换模式否则就是单次转换模式*/
	uint32_t NbrOfConversion; /* 设置转换通道数目*/
	FunctionalState DiscontinuousConvMode; /* 是否使用规则通道组间断模式*/
	uint32_t NbrOfDiscConversion; /* 配置间断模式的规则通道个数*/
	uint32_t ExternalTrigConv; /* ADC外部触发源选择*/
} ADC_InitTypeDef;
  1. DataAlign:用于设置数据的对齐方式,这里可以选择右对齐或者是左对齐,该参数可选为:
    ADC_DATAALIGN_RIGHT 和ADC_DATAALIGN_LEFT。
  2. ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果
    是多通道转换使用ADC_SCAN_ENABLE。
  3. ContinuousConvMode:可选参数为ENABLE 和DISABLE,配置自动连续转换还是单次转换。
    使用ENABLE 配置为使能自动连续转换;使用DISABLE 配置为单次转换,转换一次后停止
    需要手动控制才重新启动转换。
  4. NbrOfConversion:指定规则组转换通道数目,范围是:1~16。
  5. DiscontinuousConvMode:配置是否使用规则通道组间断模式,比如要转换的通道有1、2、
    5、7、8、9,那么第一次触发会进行通道1 和2,下次触发就是转换通道5 和7,这样不连
    续的转换,依次类推。此参数只有将ScanConvMode 使能,还有ContinuousConvMode 失能
    的情况下才有效,不可同时使能。
  6. NbrOfDiscConversion:配置间断模式的通道个数,禁止规则通道组间断模式后,此参数忽略。
  7. ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。
    ⚫ 函数返回值:
    HAL_StatusTypeDef 枚举类型的值。
  1. HAL_ADCEx_Calibration_Start 函数
    ADC 的自校准函数,其声明如下:
HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc);

⚫ 函数描述:
首先调用HAL_ADC_Init 函数配置了相关的功能后,再调用此函数进行ADC 自校准功能。
⚫ 函数形参:
ADC_HandleTypeDef 结构体类型指针变量。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
3. HAL_ADC_ConfigChannel 函数
ADC 通道配置函数,其声明如下:

HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc,
				ADC_ChannelConfTypeDef *sConfig);

⚫ 函数描述:
调用了HAL_ADC_Init 函数配置了相关的功能后,就可以调用此函数配置ADC 具体通道。
⚫ 函数形参:
形参1 是ADC_HandleTypeDef 结构体类型指针变量。
形参2 是ADC_ChannelConfTypeDef 结构体类型指针变量,用于配置ADC 采样时间,使
用的通道号,单端或者差分方式的配置等。该结构体定义如下:

typedef struct {
	uint32_t Channel; /* ADC转换通道*/
	uint32_t Rank; /* ADC转换顺序*/
	uint32_t SamplingTime; /* ADC采样周期*/
} ADC_ChannelConfTypeDef;
  1. Channel:ADC 转换通道,范围:0~19。
  2. Rank:在常规转换中的常规组的转换顺序,可以选择1~16。
  3. SamplingTime:ADC 的采样周期,最大810.5 个ADC 时钟周期,要求尽量大以减少误差。
    ⚫ 函数返回值:
    HAL_StatusTypeDef 枚举类型的值。
  1. HAL_ADC_Start 函数
    ADC 转换启动函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);

⚫ 函数描述:
当配置好ADC 的基础的功能后,就调用此函数启动ADC。
⚫ 函数形参:
ADC_HandleTypeDef 结构体类型指针变量。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
5. HAL_ADC_PollForConversion 函数
等待ADC 规则组转换完成函数,其声明如下:

HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc,
										uint32_t Timeout);

⚫ 函数描述:
一般先调用HAL_ADC_Start 函数启动转换,再调用该函数等待转换完成,然后再调用
HAL_ADC_GetValue 函数来获取当前的转换值。
⚫ 函数形参:
形参1 是ADC_HandleTypeDef 结构体类型指针变量。
形参2 是等待转换的等待时间,单位是毫秒(ms)。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
6. HAL_ADC_GetValue 函数
获取常规组ADC 转换值函数,其声明如下:

uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc);

⚫ 函数描述:
一般先调用HAL_ADC_Start 函数启动转换,再调用HAL_ADC_PollForConversion 函数等
待转换完成,然后再调用HAL_ADC_GetValue 函数来获取当前的转换值。
⚫ 函数形参:
形参1 是ADC_HandleTypeDef 结构体类型指针变量。
⚫ 函数返回值:
当前的转换值,uint32_t 类型数据。

单通道ADC 采集配置步骤

1)开启ADCx 和ADC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启ADCx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到ADC1 通道1,
对应IO 是PA1,它们的时钟开启方法如下:

__HAL_RCC_ADC1_CLK_ENABLE(); /* 使能ADC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

2)初始化ADCx,配置其工作参数
通过HAL_ADC_Init 函数来设置ADCx 时钟分频系数、分辨率、模式、扫描方式、对齐方
式等信息。
注意:该函数会调用:HAL_ADC_MspInit 回调函数来存放ADC 及GPIO 时钟使能、GPIO
初始化等代码。
3)配置ADC 通道并启动AD 转换器
在HAL 库中,通过HAL_ADC_ConfigChannel 函数来选择要配置ADC 的通道,并设置规
则序列、采样时间等。
配置好ADC 通道之后,通过HAL_ADC_Start 函数启动AD 转换器。
4)读取ADC 值
这里选择查询方式读取,在读取ADC 值之前需要调用HAL_ADC_PollForConversion 等待
上一次转换结束。然后就可以通过HAL_ADC_GetValue 来读取ADC 值。

程序流程图

在这里插入图片描述

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

/* ADC及引脚定义*/
#define ADC_ADCX_CHY_GPIO_PORT GPIOA
#define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_1
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE();\
}while(0) /* PA口时钟使能*/
#define ADC_ADCX ADC1
#define ADC_ADCX_CHY ADC_CHANNEL_1 /* 通道Y, 0 <= Y <= 16 */
/* ADC1时钟使能*/
#define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC1_CLK_ENABLE();}while(0)

ADC 的通道与引脚的对应关系在《STM32F103ZET6.pdf》数据手册可以查到,我们这里使
用ADC1 的通道1,在数据手册中的表格为:
在这里插入图片描述
下面直接开始介绍adc.c 的程序,首先是ADC 初始化函数。

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

该函数主要调用了两个HAL 库函数,HAL_ADC_Init 函数配置了选择哪个ADC、数据对
齐方式、是否使用扫描模式等参数,HAL_ADCEx_Calibration_Start 函数用于校准ADC。另外
HAL_ADC_Init 函数会调用它的MSP 回调函数HAL_ADC_MspInit,该函数用来存放使能ADC
和通道对应IO 的时钟和初始化IO 口等代码,其定义如下:

/**
* @brief ADC底层驱动,引脚配置,时钟使能
此函数会被HAL_ADC_Init()调用
* @param hadc:ADC句柄
* @retval 无
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
	if(hadc->Instance == ADC_ADCX)
	{
		GPIO_InitTypeDef gpio_init_struct;
		RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
		ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟*/
		ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启GPIO时钟*/
		/* 设置ADC时钟*/
		adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;/* ADC外设时钟*/
		/* 分频系数为6,所以ADC的时钟为72M/6=12MHz */
		adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
		HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 设置ADC时钟*/
		/* 设置AD采集通道对应IO引脚工作模式*/
		gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN; /* ADC通道IO引脚*/
		gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟*/
		HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
	}
}

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

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

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

/**
* @brief 获得ADC转换后的结果
* @param ch: 通道值0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17
* @retval 无
*/
uint32_t adc_get_result(uint32_t ch)
{
	adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1, ADC_SAM-PLETIME_239CYCLES_5); /* 设置通道/序列和采样时间*/
	HAL_ADC_Start(&g_adc_handle); /* 开启ADC */
	HAL_ADC_PollForConversion(&g_adc_handle, 10); /* 等待转换结束*/
	/* 返回最近一次ADC1规则组的转换结果*/
	return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
}

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

/**
* @brief 获取通道ch的转换值,取times次,然后平均
* @param ch : 通道号, 0~17
* @param times : 获取次数
* @retval 通道ch的times次转换结果平均值
*/
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{
	uint32_t temp_val = 0;
	uint8_t t;
	for (t = 0; t < times; t++) /* 获取times次数据*/
	{
		temp_val += adc_get_result(ch);
		delay_ms(5);
	}
	return temp_val / times; /* 返回平均值*/
}

该函数用于获取ADC 多次转换结果的平均值,从而提高准确度。
最后在main 函数里面编写如下代码:

int main(void)
{
	uint16_t adcx;
	float temp;
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	lcd_init(); /* 初始化LCD */
	adc_init(); /* 初始化ADC */
	lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
	lcd_show_string(30, 70, 200, 16, 16, "ADC TEST", RED);
	lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
	lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH1_VAL:", BLUE);
	/* 先在固定位置显示小数点*/
	lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH1_VOL:0.000V", BLUE);
	while (1)
	{
		/* 获取通道5的转换值,10次取平均*/
		adcx = adc_get_result_average(ADC_ADCX_CHY, 10);
		lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE); /* 显示ADCC采样后的原始值*/
		temp = (float)adcx * (3.3/4096);/* 获取计算后的带小数的实际电压值,比如3.1111 */
		adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形*/
		/* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
		lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
		temp -= adcx; /* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111 */
		temp *= 1000;/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数*/
		/* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
		lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
		LED0_TOGGLE();
		delay_ms(100);
	}
}

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

下载验证

下载代码后,可以看到LCD 显示如图30.2.4.1 所示:
在这里插入图片描述
上图中,我们使用杜邦线将ADC 和RV1 排针连接,使得PA1 连接到电位器上,测试的是
电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0 闪烁,提示程
序运行。

大家也可以用杜邦线将ADC 排针接到其它待测量的电压点,看看测量到的电压值是否准
确?但是要注意:一定要保证测试点的电压在0~3.3V 的电压范围,否则可能烧坏我们的ADC,
甚至是整个主控芯片。

单通道ADC 采集(DMA 读取)实验

本实验我们来学习单通道ADC 采集(DMA 读取)实验。本实验使用规则组单通道的连续
转换模式,并且通过软件触发,即由ADC_CR2 寄存器的SWSTART 位启动。由于使用连续转
换模式,所以使用DMA 读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。

ADC & DMA 寄存器

本实验我们很多的设置和单通道ADC 采集实验是一样的,所以下面介绍寄存器的时候我
们不会继续全部都介绍,而是针对性选择与单通道ADC 采集实验不同设置的ADC_CR2 寄存
器进行介绍,其他的配置基本一样的。另外因为我们用到DMA 读取数据,所以还会介绍如何配置相关DMA 的寄存器。
⚫ ADC 配置寄存器(ADC_CR2)
ADCx 配置寄存器描述如图30.3.1.1 所示:

在这里插入图片描述
ADC_CR2 寄存器中我们主要跟前面设置不同的有两个位,分别如下:
DMA 位用于配置使用DMA 模式,本实验该位置1。在单通道ADC 采集实验中,默认设
置为0,即不使用DMA 模式,规则组转换的结果存储在ADC_DR 寄存器,然后通过手动读取
ADC_DR 寄存器的方式得到转换结果。本实验我们使用ADC 的连续转换模式,并通过DMA
读取转换结果,这样DMA 就会自动在ADC_DR 寄存器中读取转换结果。
CONT 位用于设置单次转换模式还是连续转换模式,本实验我们使用连续转换模式,所以
CONT 位置1 即可。
这里介绍ADC_CR2 寄存器的这两个位,其它请参考上一个实验的配置。下面介绍DMA
一些比较重要的寄存器配置。
⚫ DMA 通道x 外设地址寄存器(DMA_CPARx)(x = 1…7)
DMA 通道x 外设地址寄存器描述如图30.3.1.2 所示:
在这里插入图片描述
该寄存器存放的是DMA 通道x 外设地址。本实验,我们需要通过DMA 读取ADC 转换后
存放在ADC 规则数据寄存器(ADC_DR) 的结果数据。所以我们需要给DMA_CPARx 寄存器
写入ADC_DR 寄存器的地址。这样配置后,DMA 就会从ADC_DR 寄存器的地址读取ADC 的
转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_CMARx寄存器来设置,
比如定义一个变量,把这个变量的地址值写入该寄存器。
注意:DMA_CPARx 寄存器受到写保护,只有DMA_CCRx 寄存器中的EN 为“0”时才可
以写入,即先要禁止通道开启才可以写入。
⚫ DMA 通道x 存储器地址寄存器(DMA_CMARx)(x = 1…7)
DMA 通道x 存储器地址寄存器描述如图30.3.1.3 所示:

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

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

硬件设计

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

在这里插入图片描述
使用杜邦线将ADC 和RV1 排针连接好后,并下载程序后,就可以用螺丝刀调节电位器变
换多种电压值进行测量。
有的朋友可能还想测量其它地方的电压值,我们只需要1 根杜邦线,一端接到ADC 排针
上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V 的电压范围,否则可
能烧坏我们的ADC,甚至是整个主控芯片。

程序设计

30.3.3.1 ADC & DMA 的HAL 库驱动

单通道ADC 采集实验已经介绍了一部分ADC 的HAL 库API 函数,这里要介绍的是
HAL_DMA_Start_IT 和HAL_ADC_Start_DMA 函数。

  1. HAL_DMA_Start_IT 函数
    启动DMA 传输并开启相关中断函数,其声明如下:
    HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma,
    uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
    ⚫ 函数描述:
    用于启动DMA 传输,并开启相关中断,DMA1 和DMA2 都是用的这个函数。
    ⚫ 函数形参:
    形参1 是DMA_HandleTypeDef 结构体类型指针变量。
    形参2 是DMA 传输的源地址。
    形参3 是DMA 传输的目的地址。
    形参4 是要传输的数据项数目。
    ⚫ 函数返回值:
    HAL_StatusTypeDef 枚举类型的值。
  2. HAL_ADC_Start_DMA 函数
    启动ADC(DMA 传输)方式函数,其声明如下:
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,
									uint32_t *pData, uint32_t Length);

⚫ 函数描述:
用于启动ADC(DMA 传输)方式的函数。
⚫ 函数形参:
形参1 是ADC_HandleTypeDef 结构体类型指针变量。
形参2 是ADC 采样数据传输的目的地址。
形参3 是要传输的数据项数目。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

单通道ADC 采集(DMA 读取)配置步骤

1)开启ADCx 和ADC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启ADCx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到ADC1 通道1,
对应IO 是PA1,它们的时钟开启方法如下:

__HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

2)初始化ADCx,配置其工作参数
通过HAL_ADC_Init 函数来设置ADCx 时钟分频系数、分辨率、模式、扫描方式、对齐方
式等信息。
注意:该函数会调用:HAL_ADC_MspInit 回调函数来存放ADC 及GPIO 时钟使能、GPIO
初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP 回调函数。
3)配置ADC 通道并启动AD 转换器
在HAL 库中,通过HAL_ADC_ConfigChannel 函数来选择要配置ADC 的通道,并设置规
则序列、采样时间等。
4)初始化DMA
通过HAL_DMA_Init 函数初始化DMA,包括配置通道,外设地址,存储器地址,传输
数据量等。
HAL 库为了处理各类外设的DMA 请求,在调用相关函数之前,需要调用一个宏定义
标识符,来连接DMA 和外设句柄。这个宏定义为__HAL_LINKDMA。
5)使能DMA 对应数据流中断,配置DMA 中断优先级并开启中断,启动ADC 和DMA
通过HAL_ADC_Start_DMA 函数开启ADC 转换,通过DMA 传输结果。
通过HAL_DMA_Start_IT 函数启动DMA 读取,使能DMA 中断。
通过HAL_NVIC_EnableIRQ 函数使能DMA 数据流中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
6)编写中断服务函数
DMA 的每个数据流几乎都有一个中断服务函数,比如DMA1_Channel1 的中断服务函
数为DMA1_Channel1_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判
断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法
就是如此。
还可以通过调用HAL 库提供的DMA 中断公用处理函数HAL_DMA_IRQHandler,然
后定重新义相关的中断回调处理函数。

程序流程图
在这里插入图片描述

程序解析
由于本实验用到DMA,所以在adc.h 头文件定义了以下一些宏定义:

/* ADC单通道/多通道DMA采集DMA及通道定义
* 注意: ADC1的DMA通道只能是: DMA1_Channel1, 因此只要是ADC1, 这里是不能改动的
* ADC2不支持DMA采集
* ADC3的DMA通道只能是: DMA2_Channel5, 因此如果使用ADC3 则需要修改
*/
#define ADC_ADCX_DMACx DMA1_Channel1
#define ADC_ADCX_DMACx_IRQn DMA1_Channel1_IRQn
#define ADC_ADCX_DMACx_IRQHandler DMA1_Channel1_IRQHandler
/*判断DMA1_Channel1传输完成标志, 是一个假函数形式, 不能当函数使用, 只能用在if等语句里面*/
#define ADC_ADCX_DMACx_IS_TC() ( DMA1->ISR & (1 << 1) )
/* 清除DMA1_Channel1 传输完成标志*/
#define ADC_ADCX_DMACx_CLR_TC() do{ DMA1->IFCR |= 1 << 1; }while(0)

下面给大家介绍adc.c 文件里面的函数,首先是ADC DMA 读取初始化函数。

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

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

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

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

下面介绍的是ADC DMA 采集中断服务函数,函数定义如下:

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

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

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

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

下载验证

下载代码后,可以看到LCD 显示如图30.3.4.1 所示:
在这里插入图片描述
上图中,我们使用短路帽将ADC 和RV1 排针连接,使得PA1 连接到电位器上,测试电位
器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。LED0 闪烁,提示程序运
行。
大家也可以用杜邦线将ADC 排针接到其它待测量的电压点,看看测量到的电压值是否准
确?但是要注意:一定要保证测试点的电压在0~3.3V 的电压范围,否则可能烧坏我们的ADC,
甚至是整个主控芯片。

多通道ADC 采集(DMA 读取)实验

本实验我们来学习多通道ADC 采集(DMA 读取)实验。本实验使用规则组多通道的连续
转换模式,并且通过软件触发,即由ADC_CR2 寄存器的SWSTART 位启动。由于使用连续转
换模式,所以使用DMA 读取转换结果的方式。下面先带大家来了解本实验要配置的寄存器。

ADC 寄存器

本实验我们很多的设置和单通道ADC 采集(DMA 读取)实验是一样的,所以下面介绍寄
存器的时候我们不会继续全部都介绍,而是针对性选择与单通道ADC 采集(DMA 读取)实验
不同设置的ADC_SQRx 寄存器进行介绍,其他的配置基本一样的。另外我们用到DMA 读取数据,配置上和单通道ADC 采集(DMA 读取)实验是一样的。
ADC 规则序列寄存器有四个(ADC_SQR1~ ADC_SQR3),具体怎么配置,需要看我们用多少个通道,比如本实验我们使用6 个通道同时采集ADC 数据,具体配置如下:
⚫ ADC 规则序列寄存器1(ADC_SQR1)
ADC 规则序列寄存器1 描述如图30.4.1.1 所示:
在这里插入图片描述
L[3:0]位用于设置规则序列的长度,取值范围:015,表示规则序列长度为116。本实验
使用到6 个通道,所以设置这几个位的值为5 即可。
SQ13[4:0]SQ16[4:0]位设置规则组序列的第1316 个转换编号,第1~12 个转换编号的设
置请查看ADC_SQR2 和ADC_SQR3 寄存器。设置过程非常简单,忘记了请参考前面给大家整
理出来的规则序列寄存器控制关系汇总表。
下面我们来看看本实验是怎么设置的:SQ1[4:0]位赋值为0、SQ2[4:0]位赋值为1、SQ3[4:0]
位赋值为2、SQ4[4:0]位赋值为3、SQ5[4:0]位赋值为4、SQ6[4:0]位赋值为5,即规则序列1 到
6 分别对应的输入通道是0 到5。SQ1~SQ6 位都是在ADC_SQR3 寄存器中配置。

硬件设计

  1. 例程功能
    使用ADC1 采集(DMA 读取)通道1\2\3\4\5\6 的电压,在LCD 模块上面显示对应的ADC
    转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5 到你想测
    量的电压源(0~3.3V),然后通过TFTLCD 显示的电压值。LED0 闪烁,提示程序运行。

  2. 硬件资源
    1)LED 灯
    LED0 –PE5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)ADC1 :通道1–PA0、通道2–PA1、通道3–PA2、
    通道4–PA3、通道5–PA4、通道6–PA5
    5)DMA(DMA1 通道1)

  3. 原理图
    ADC 和DMA 属于STM32F103 内部资源,实际上我们只需要软件设置就可以正常工作,
    另外还需要将待测量的电压源连接到ADC 通道上,以便ADC 测量。本实验,我们通过ADC1
    的通道1\2\3\4\5\6 来采集外部电压值,并通过DMA 来读取。

程序设计

30.4.3.1 ADC 的HAL 库驱动
本实验用到的ADC 的HAL 库API 函数前面都介绍过,具体调用情况请看程序解析部分。
下面介绍多通道ADC 采集(DMA 读取)配置步骤。
多通道ADC 采集(DMA 读取)配置步骤
1)开启ADCx 和ADC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启ADCx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到ADC1 通道0、
1、2、3、4、5,对应IO 是PA0、PA1、PA2、PA3、PA4 和PA5,它们的时钟开启方法如下:

__HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

2)初始化ADCx,配置其工作参数
通过HAL_ADC_Init 函数来设置ADCx 时钟分频系数、分辨率、模式、扫描方式、对齐方
式等信息。
注意:该函数会调用:HAL_ADC_MspInit 回调函数来存放ADC 及GPIO 时钟使能、GPIO
初始化等代码。我们也可以不存放在这个函数里,本实验就没用到这个MSP 回调函数。
3)配置ADC 通道并启动AD 转换器
在HAL 库中,通过HAL_ADC_ConfigChannel 函数来选择要配置ADC 的通道,并设置规
则序列、采样时间等。
配置好ADC 通道之后,通过HAL_ADC_Start 函数启动AD 转换器。
4)初始化DMA
通过HAL_DMA_Init 函数初始化DMA,包括配置通道,外设地址,存储器地址,传输
数据量等。
HAL 库为了处理各类外设的DMA 请求,在调用相关函数之前,需要调用一个宏定义
标识符,来连接DMA 和外设句柄。这个宏定义为__HAL_LINKDMA。
5)使能DMA 对应数据流中断,配置DMA 中断优先级,使能ADC,使能并启动DMA
通过HAL_ADC_Start_DMA 函数开启ADC 转换,通过DMA 传输结果。
通过HAL_DMA_Start_IT 函数启动DMA 读取,使能DMA 中断。
通过HAL_NVIC_EnableIRQ 函数使能DMA 数据流中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
6)编写中断服务函数
DMA 的每个数据流几乎都有一个中断服务函数,比如DMA1_Channel1 的中断服务函
数为DMA1_Channel1_IRQHandler。简单的做法就是在,对应的中断服务函数里面,通过判断相关的中断标志位的方式,完成中断逻辑代码,最后清楚该中断标志位,本实验的做法
就是如此。
还可以通过调用HAL 库提供的DMA 中断公用处理函数HAL_DMA_IRQHandler,然
后定重新义相关的中断回调处理函数。
30.4.3.2 程序流程图
在这里插入图片描述

程序解析
在本实验中adc.h 头文件只是添加了一些函数声明,下面开始介绍adc.c 的函数,本实验只
增加了一个函数,ADC 的N 通道(6 通道) DMA 读取初始化函数,其定义如下:

/**
* @brief ADC N通道(6通道) DMA读取初始化函数
* @note 由于本函数用到了6个通道, 宏定义会比较多内容,
* 因此,本函数就不采用宏定义方式来修改通道了,
* 直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道.
*
* 注意: 本函数还是使用ADC_ADCX(默认=ADC1)ADC_ADCX_DMACx(DMA1_Channel1)
及其相关定义。不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用.
* @param mar : 存储器地址
* @retval 无
*/
void adc_nch_dma_init(uint32_t mar)
{
	GPIO_InitTypeDef gpio_init_struct;
	RCC_PeriphCLKInitTypeDef adc_clk_init = {0};
	ADC_ChannelConfTypeDef adc_ch_conf = {0};
	ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟*/
	__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/
	/* 大于DMA1_Channel7, 则为DMA2的通道了*/
	if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)
	{
		__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能*/
	}
	else
	{
		__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能*/
	}
	/* 设置ADC时钟*/
	adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC; /* ADC外设时钟*/
	/* 分频因子6时钟为72M/6=12MHz */
	adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;
	HAL_RCCEx_PeriphCLKConfig(&adc_clk_init); /* 设置ADC时钟*/
	/*
	设置ADC1通道0~5对应的IO口模拟输入
	AD采集引脚模式设置,模拟输入
	PA0对应ADC1_IN0
	PA1对应ADC1_IN1
	PA2对应ADC1_IN2
	PA3对应ADC1_IN3
	PA4对应ADC1_IN4
	PA5对应ADC1_IN5
	*/
	gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
	|GPIO_PIN_4|GPIO_PIN_5; /* GPIOA0~5 */
	gpio_init_struct.Mode = GPIO_MODE_ANALOG; /* 模拟*/
	HAL_GPIO_Init(GPIOA, &gpio_init_struct);
	/* 初始化DMA */
	g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx; /* 设置DMA通道*/
	/* 从外设到存储器模式*/
	g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
	g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式*/
	g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式*/
	/* 外设数据长度:16位*/
	g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
	/* 存储器数据长度:16位*/
	g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
	g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式*/
	g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;/* 中等优先级*/
	HAL_DMA_Init(&g_dma_nch_adc_handle);
	/* 将DMA与adc联系起来*/
	__HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);
	/* 初始化ADC */
	g_adc_nch_dma_handle.Instance = ADC_ADCX; /* 选择哪个ADC */
	g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐*/
	g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 使能扫描模式*/
	g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE; /* 使能连续转换*/
	/* 赋值范围是1~16,本实验用到6个规则通道序列*/
	g_adc_nch_dma_handle.Init.NbrOfConversion = 6;
	/* 禁止规则组间断模式*/
	g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;
	/* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略*/
	g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;
	g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;/* 软件触发*/
	HAL_ADC_Init(&g_adc_nch_dma_handle); /* 初始化*/
	HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle); /* 校准ADC */
	/* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_0; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_1; /* 采样序列里的第1个*/
	/* 采样时间,设置最大采样周期:239.5个ADC周期*/
	adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_1; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_2; /* 采样序列里的第2个*/
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_2; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_3; /* 采样序列里的第3个*/
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_3; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_4; /* 采样序列里的第4个*/
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_4; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_5; /* 采样序列里的第5个*/
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf); /* 配置ADC通道*/
	adc_ch_conf.Channel = ADC_CHANNEL_5; /* 配置使用的ADC通道*/
	adc_ch_conf.Rank = ADC_REGULAR_RANK_6; /* 采样序列里的第6个*/
	HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);/* 配置ADC通道*/
	/* 配置DMA数据流请求中断优先级*/
	HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);
	HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);
	/* 启动DMA,并开启中断*/
	HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
	/* 开启ADC,通过DMA传输结果*/
	HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);
}

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

最后在main.c 里面编写如下代码:

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

这里使用了DMA 传输数据,DMA 传输的数据存放在g_adc_dma_buf 数组里,该数组的大
小是50 * 6。本实验用到6 个通道,每个通道使用50 个uint16_t 大小的空间存放ADC 的结果。
输入通道0 的转换数据存放在g_adc_dma_buf[0]到g_adc_dma_buf[49],输入通道1 的转换数据
存放在g_adc_dma_buf[50]到g_adc_dma_buf[99],后面的以此类推。然后对数组的每个通道的
数据取平均值,减少误差。最后在LCD 屏上显示ADC 的转换值和换算成电压后的电压值。

下载验证

下载代码后,LED0 闪烁,提示程序运行。可以看到LCD 显示如图30.4.4.1 所示:
在这里插入图片描述
使用ADC1 采集(DMA 读取)通道0\1\2\3\4\5 的电压,在LCD 模块上面显示对应的ADC
转换值以及换算成电压后的电压值。可以使用杜邦线连接PA0\PA1\PA2\PA3\PA4\PA5 到你想测
量的电压源(0~3.3V)。

这6 个通道对应引出来的引脚PA0\PA1\PA2\PA3\PA4\PA5 在开发板上的位置,如下图所示:

在这里插入图片描述
这六个通道可以同时测量不同测试点的电压,只需要用杜邦线分别接到不同的电压测试点
即可。注意:一定要保证测试点的电压在0~3.3V 的电压范围,否则可能烧坏我们的ADC,甚
至是整个主控芯片。

单通道ADC 过采样(16 位分辨率)实验

本实验我们来学习单通道ADC 过采样(16 位分辨率)实验。STM32F103 自带的ADC 分
辨率只有12 位,虽然可以满足一般的应用,但是有些场合可能需要更高的分辨率,怎么办呢?
可以使用外部专用的ADC,或者换一个带更高分辨率ADC 的主控芯片。这样做往往会增加额
外的成本,那么有没有其它办法呢?答案是有的,可以通过引入过采样技术来实现。
ADC 过采样技术,是利用ADC 多次采集的方式,来提高ADC 分辨率。下面,简单介绍一
下怎么提高ADC 测量的分辨率?
下面直接给大家介绍一个方程,根据要增加分辨率计算过采样频率方程,方程如下:
fos = 4w ⋅ fs

其中,w 是希望增加的分辨率位数,fs是初始采样频率要求,fos是过采样频率。
方程的推导过程比较复杂,这里就不带大家去推导,感兴趣的朋友可以通过下面这个链接
自行学习:https://max.book118.com/html/2018/0506/165038217.shtm。
由该方程可以知道,采样速度每提高4 倍,分辨率位数可以提高1 位。结合ADC 的实际
情况,换个思路来说,分辨率位数每提高1 位,如果采样频率不变的情况下,那么采样速度就
会降低4 倍。本实验要求得到16 位分辨率,即需要增加4 位分辨率,那么采样速度就会降低
256 倍,即需要采集256 次才能得出1 次数据,相当于ADC 速度慢了256 倍。
理论上只要ADC 足够快,我们可以无限提高ADC 精度,但实际上ADC 并不是无限快的,
而且由于ADC 性能限制,并不是位数无限提高,结果就越好,需要根据自己的实际需求和ADC
的实际性能来权衡的。
下面来看一下我们怎么实现单通道ADC 过采样(16 位分辨率)实验的?。

ADC 寄存器

本实验我们很多的设置和单通道ADC 采集(DMA 读取)实验是一样的,代码实现也是基
于该实验实现的,寄存器的介绍请参考前面的ADC 实验。

硬件设计

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

程序设计

30.5.3.1 ADC 的HAL 库驱动
本实验用到的ADC 的HAL 库API 函数前面都介绍过,具体调用情况请看程序解析部分。
30.5.3.2 程序流程图
在这里插入图片描述

程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC 驱动源码包
括两个文件:adc.c 和adc.h。本实验沿用前面实验中的函数,并没有改动。
下面介绍一下main.c 里面编写的代码:

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

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

下载验证

下载代码后,LED0 闪烁,提示程序运行。可以看到LCD 显示如图30.5.4.1 所示:

在这里插入图片描述

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

内部温度传感器实验

本章,我们将介绍STM32F103 的内部温度传感器并使用它来读取温度值,然后在LCD 模
块上显示出来。

内部温度传感器简介

STM32 有一个内部的温度传感器,可以用来测量CPU 及周围的温度(内部温度传感器更
适合于检测温度的变化,需要测量精确温度的情况下,应使用外置传感器)。对于STM32F103
来说,该温度传感器在内部和ADC1_IN16 输入通道相连接,此通道把传感器输出的电压转换
成数字值。温度传感器模拟输入推荐采样时间是17.1us。STM32F103 内部温度传感器支持的温
度范围为:-40~125 度。精度为±1.5℃左右。
STM32 内部温度传感器的使用很简单,只要设置一下内部ADC,并激活其内部温度传感
器通道就差不多了。关于ADC 的设置,我们在上一章已经进行了详细的介绍,这里就不再多
说。接下来我们介绍一下和温度传感器设置相关的两个地方。
第一个地方,我们要使用STM32 的内部温度传感器,必须先激活ADC 的内部通道,这里
通过ADC_CR2 的AWDEN 位(bit23)设置。设置该位为1 则启用内部温度传感器。
第二个地方,STM32 的内部温度传感器固定的连接在ADC1 的通道16 上,所以,我们在
设置好ADC1 之后只要读取通道16 的值,就是温度传感器返回来的电压值了。根据这个值,
我们就可以计算出当前温度。计算公式如下:
在这里插入图片描述

硬件设计

  1. 例程功能
    通过ADC 的通道16 读取STM32F103 内部温度传感器的电压值,并将其转换为温度值,
    显示在TFTLCD 屏上。LED0 闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)ADC1 通道16
    5)内部温度传感器
  3. 原理图
    ADC 和内部温度传感器都属于STM32F103 内部资源,实际上我们只需要软件设置就可以正常工作,我们需要用到TFTLCD 模块显示结果。

程序设计

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 值。最后根据上面介绍的
公式计算出温度传感器的温度值。

程序流程图

在这里插入图片描述

程序解析

  1. adc 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。内部温度传感器
    驱动源码包括两个文件:adc.c 和adc.h。本实验和ADC 章节实验用的都是同一个adc.c 和adc.h
    代码。
    在adc.h 头文件再加入温度传感器的相关宏定义,该宏定义如下:
/* ADC温度传感器通道定义*/
#define ADC_TEMPSENSOR_CHX ADC_CHANNEL_16

ADC_CHANNEL_16 就是ADC 通道16 连接内部温度传感器的通道16 宏定义。我们在定
义为ADC_TEMPSENSOR_CHX,可以让大家更容易理解这个宏定义的含义。
下面我们直接介绍与内部温度传感器相关的adc.c 的程序,首先是ADC 内部温度传感器初
始化函数,其定义如下:

/**
* @brief ADC 内部温度传感器初始化函数
* @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
* 注意: STM32F103内部温度传感器只连接在ADC1的通道16上, 其他ADC无法进行转换.
*
* @param 无
* @retval 无
*/
void adc_temperature_init(void)
{
	adc_init(); /* 先初始化ADC */
	/* TSVREFE = 1, 启用内部温度传感器和Vrefint */
	SET_BIT(g_adc_handle.Instance->CR2, ADC_CR2_TSVREFE);
}

该函数调用adc_init 函数配置了ADC 的基础功能参数,由于前面实验中的adc_init 实验是
对ADC_CHANNEL_1 进行配置的,而我们对内部温度传感器的初始化步骤与普通ADC 类似,
为了不重复编写代码,我们用位操作函数进行修改ADC 通道,把ADC_CR2 的TSVREFE 位置
1,即SET_BIT(g_adc_handle.Instance->CR2, ADC_CR2_TSVREFE);这样子就可以完成对内部
温度传感器通道的初始化工作。adc_init 的实现代码,可以回顾以下ADC 章节内容。
下面讲解一下获取内部温度传感器温度值函数,其定义如下:

/**
* @brief 获取内部温度传感器温度值
* @param 无
* @retval 温度值(扩大了100倍,单位:℃.)
*/
short adc_get_temperature(void)
{
	uint32_t adcx;
	short result;
	double temperature;
	/* 读取内部温度传感器通道,10次取平均*/
	adcx = adc_get_result_average(ADC_TEMPSENSOR_CHX, 20);
	temperature = (float)adcx * (3.3 / 4096); /* 转化为电压值*/
	temperature = (1.43 - temperature) / 0.0043 + 25; /* 计算温度*/
	result = temperature *= 100; /* 扩大100倍. */
	return result;
}

该函数先是调用前面ADC 实验章节写好的adc_get_result_average 函数取获取通道ch 的转
换值,然后通过温度转换公式,返回温度值。

main.c 代码
在main.c 里面编写如下代码:

int main(void)
{
	short temp;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	lcd_init(); /* 初始化LCD */
	adc_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);
	}
}

该部分的代码逻辑很简单,先是得到温度值,再根据温度值判断正负值,来显示温度符号,
再显示整数和小数部分。

下载验证

将程序下载到开发板后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示
的内容如图31.4.1 所示:

在这里插入图片描述

大家可以看看你的温度值与实际是否相符合(因为芯片会发热,所以一般会比实际温度偏
高)?

光敏传感器实验

本章,我们将学习使用STM32 开发板板载的一个光敏传感器。我们还是要使用到ADC 采
集,通过ADC 采集电压,获取光敏传感器的电阻变化,从而得出环境光线的变化,并在TFTLCD
上面显示出来。

光敏传感器简介

光敏传感器是最常见的传感器之一,它的种类繁多,主要有:光电管、光电倍增管、光敏
电阻、光敏三极管、太阳能电池、红外线传感器、紫外线传感器、光纤式光电传感器、色彩传
感器、CCD 和CMOS 图像传感器等。光传感器是目前产量最多、应用最广的传感器之一,它在
自动控制和非电量电测技术中占有非常重要的地位。
光敏传感器是利用光敏元件将光信号转换为电信号的传感器,它的敏感波长在可见光波长
附近,包括红外线波长和紫外线波长。光传感器不只局限于对光的探测,它还可以作为探测元
件组成其他传感器,对许多非电量进行检测,只要将这些非电量转换为光信号的变化即可。
STM32F103 战舰开发板板载了一个光敏二极管(光敏电阻),作为光敏传感器,它对光的
变化非常敏感。光敏二极管也叫光电二极管。光敏二极管与半导体二极管在结构上是类似的,
其管芯是一个具有光敏特征的PN 结,具有单向导电性,因此工作时需加上反向电压。无光照
时,有很小的饱和反向漏电流,即暗电流,此时光敏二极管截止。当受到光照时,饱和反向漏
电流大大增加,形成光电流,它随入射光强度的变化而变化。当光线照射PN 结时,可以使PN
结中产生电子一空穴对,使少数载流子的密度增加。这些载流子在反向电压下漂移,使反向电
流增加。因此可以利用光照强弱来改变电路中的电流。
利用这个电流变化,我们串接一个电阻,就可以转换成电压的变化,从而通过ADC 读取电
压值,判断外部光线的强弱。
本章,我们利用ADC3 的通道6(PF8)来读取光敏二极管电压的变化,从而得到环境光线
的变化,并将得到的光线强度,显示在TFTLCD 上面。关于ADC 的介绍,前面已经有详细介
绍了,这里我们就不再细说了。

硬件设计

  1. 例程功能
    通过ADC3 的通道6(PF8)读取光敏传感器(LS1)的电压值,并转换为0~100 的光线强
    度值,显示在LCD 模块上面。光线越亮,值越大;光线越暗,值越小。大家可以用手指遮挡LS1
    和用手电筒照射LS1,来查看光强变化。LED0 闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED 灯
    LED : LED0 –PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)ADC3 :通道6 –PF8
    5)光敏传感器
  3. 原理图
    我们主要来看看光敏传感器和开发板的连接,如下图所示:
    在这里插入图片描述
    图中,LS1 是光敏二极管,外观看起像与贴片LED 类似(战舰位于OLED 插座旁边,LS1),
    R34 为其提供反向电压,当环境光线变化时,LS1 两端的电压也会随之改变,通过ADC3_IN6
    通道读取LIGHT_SENSOR(PF8)上面的电压,即可得到环境光线的强弱。光线越强,电压越
    低,光线越暗,电压越高。

程序设计

ADC 的HAL 库驱动

本实验用到的ADC 的HAL 库API 函数前面都介绍过,具体调用情况请看程序解析部分。
下面介绍读取光敏传感器ADC 值的配置步骤。
读取光敏传感器ADC 值配置步骤
1)开启ADCx 和ADC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启ADCx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到ADC3 通道6,
对应IO 是PF8,它们的时钟开启方法如下:

__HAL_RCC_ADC3_CLK_ENABLE (); /* 使能ADC3时钟*/
__HAL_RCC_GPIOF_CLK_ENABLE(); /* 开启GPIOF时钟*/

2)设置ADC3,开启内部温度传感器
调用HAL_ADC_Init 函数来设置ADC3 时钟分频系数、分辨率、模式、扫描方式、对齐方
式等信息。
注意:该函数会调用:HAL_ADC_MspInit 回调函数来完成对ADC 底层的初始化,包括:
ADC3 时钟使能、ADC3 时钟源的选择等。
3)配置ADC 通道并启动AD 转换器
调用HAL_ADC_ConfigChannel()函数配置ADC3 通道6,根据需求设置通道、序列、采样
时间和校准配置单端输入模式或差分输入模式等。然后通过HAL_ADC_Start 函数启动AD 转
换器。
4)读取ADC 值,转换为光线强度值
这里选择查询方式读取,在读取ADC 值之前需要调用HAL_ADC_PollForConversion 等待
上一次转换结束。然后就可以通过HAL_ADC_GetValue 来读取ADC 值。最后把得到的ADC 值
转换为0~100 的光线强度值。

程序流程图

在这里插入图片描述

程序解析

  1. lsens 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LSENS 驱动源码
    包括两个文件:lsens.c 和lsens.h。本实验还要用到adc3.c 和adc3.h 文件的驱动代码。adc3.c\h
    文件的代码和单通道ADC 采集实验的adc.c\h 文件的代码几乎一样,这里就不再赘述了。
    lsens.h 头文件定义了一些宏定义和一些函数的声明,该宏定义如下:
/* 光敏传感器对应ADC3的输入引脚和通道定义*/
#define LSENS_ADC3_CHX_GPIO_PORT GPIOF
#define LSENS_ADC3_CHX_GPIO_PIN GPIO_PIN_8
#define LSENS_ADC3_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOF_CLK_ENABLE(); \
}while(0) /* PF口时钟使能*/
#define LSENS_ADC3_CHX ADC_CHANNEL_6 /* 通道Y, 0 <= Y <= 17 */

这些宏定义分别是PF8 及其时钟使能的宏定义,还有ADC3 通道6 的宏定义。
下面介绍lsens.c 的函数,首先是光敏传感器初始化函数,其定义如下:

/**
* @brief 初始化光敏传感器
* @param 无
* @retval 无
*/
void lsens_init(void)
{
	GPIO_InitTypeDef gpio_init_struct;
	LSENS_ADC3_CHX_GPIO_CLK_ENABLE(); /* IO口时钟使能*/
	/* 设置AD采集通道对应IO引脚工作模式*/
	gpio_init_struct.Pin = LSENS_ADC3_CHX_GPIO_PIN;
	gpio_init_struct.Mode = GPIO_MODE_ANALOG;
	HAL_GPIO_Init(LSENS_ADC3_CHX_GPIO_PORT, &gpio_init_struct);
	adc3_init(); /* 初始化ADC */
}

该函数初始化PF8 为模拟功能,然后通过adc3_init 函数初始化ADC3。
最后是读取光敏传感器值,函数定义如下:

/**
* @brief 读取光敏传感器值
* @param 无
* @retval 0~100:0,最暗;100,最亮
*/
uint8_t lsens_get_val(void)
{
	uint32_t temp_val = 0;
	temp_val = adc3_get_result_average(LSENS_ADC3_CHX, 10); /* 读取平均值*/
	temp_val /= 40;
	if (temp_val > 100)temp_val = 100;
	return (uint8_t)(100 - temp_val);
}

lsens_get_val 函数用于获取当前光照强度,该函数通过adc3_get_result_average 函数得到通
道6 转换的电压值,经过简单量化后,处理成0~100 的光强值。0 对应最暗,100 对应最亮。
2. main.c 代码
在main.c 里面编写如下代码:

int main(void)
{
	short adcx;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	lcd_init(); /* 初始化LCD */
	lsens_init(); /* 初始化光敏传感器*/
	lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
	lcd_show_string(30, 70, 200, 16, 16, "LSENS TEST", RED);
	lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
	lcd_show_string(30, 110, 200, 16, 16, "LSENS_VAL:", BLUE);
	while (1)
	{
		adcx = lsens_get_val();
		lcd_show_xnum(30 + 10 * 8, 110, adcx, 3, 16, 0, BLUE); /* 显示光线强度值*/
		LED0_TOGGLE(); /* LED0闪烁,提示程序运行*/
		delay_ms(250);
	}
}

该部分的代码逻辑很简单,初始化各个外设之后,进入死循环,通过lsens_get_val 获取光
敏传感器得到的光强值(0~100),并显示在TFTLCD 上面。

下载验证

将程序下载到开发板后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示
的内容如图32.4.1 所示:

在这里插入图片描述

我们可以通过给LS1 不同的光照强度,来观察LSENS_VAL 值的变化,光照越强,该值越
大,光照越弱,该值越小,LSENS_VAL 值的范围是0~100。

DAC 实验

本章,我们将介绍STM32F103 的DAC(Digital -to- analog converters,数模转换器)功能。
我们通过三个实验来学习DAC,分别是DAC 输出实验、DAC 输出三角波实验和DAC 输出正
弦波实验。

DAC 简介

STM32F103 的DAC 模块(数字/模拟转换模块)是12 位数字输入,电压输出型的DAC。
DAC 可以配置为8 位或12 位模式,也可以与DMA 控制器配合使用。DAC 工作在12 位模式
时,数据可以设置成左对齐或右对齐。DAC 模块有2 个输出通道,每个通道都有单独的转换器。
在双DAC 模式下,2 个通道可以独立地进行转换,也可以同时进行转换并同步地更新2 个通道
的输出。DAC 可以通过引脚输入参考电压Vref+以获得更精确的转换结果。
STM32 的DAC 模块主要特点有:
①2 个DAC 转换器:每个转换器对应1 个输出通道
②8 位或者12 位单调输出
③12 位模式下数据左对齐或者右对齐
④同步更新功能
⑤噪声\三角波形生成
⑥双DAC 双通道同时或者分别转换
⑦每个通道都有DMA 功能
DAC 通道框图如图33.1.1 所示:
图33.1.1 DAC 通道框图
在这里插入图片描述
图中VDDA 和VSSA 为DAC 模块模拟部分的供电,而VREF+则是DAC 模块的参考电压。
DAC_OUTx 就是DAC 的两个输出通道了(对应PA4 或者PA5 引脚)。ADC 的这些输入/输出
引脚信息如下表所示:
在这里插入图片描述
从图33.1.1 可以看出,DAC 输出是受DORx(x=1/2,下同)寄存器直接控制的,但是我们
不能直接往DORx 寄存器写入数据,而是通过DHRx 间接的传给DORx 寄存器,实现对DAC
输出的控制。
前面我们提到,STM32F103 的DAC 支持8/12 位模式,8 位模式的时候是固定的右对齐的,
而12 位模式又可以设置左对齐/右对齐。DAC 单通道模式下的数据寄存器对齐方式,总共有3
种情况,如下图所示:
在这里插入图片描述
①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 双通道(可用时),也有三种可能的方式,如下图所示:
在这里插入图片描述
①8 位数据右对齐:用户将DAC 通道1 的数据写入DAC_DHR8RD[7:0]位(实际存入
DHR1[11:4]位),将DAC 通道2 的数据写入DAC_DHR8RD[15:8]位(实际存入
DHR2[11:4]位)。
②12 位数据左对齐:用户将DAC 通道1 的数据写入DAC_DHR12LD[15:4]位(实际存入
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_CR 的TENx 位置0),存入寄存器DAC_DHRx 的数
据会在1 个APB1 时钟周期后自动传至寄存器DAC_DORx。如果选中硬件触发(寄存器DAC_CR
的TENx 位置1),数据传输在触发发生以后3 个APB1 时钟周期后完成。一旦数据从DAC_DHRx寄存器装入DAC_DORx 寄存器,在经过时间tSETTLING之后,输出即有效,这段时间的长短依电
源电压和模拟输出负载的不同会有所变化。我们可以从《《STM32F103ZET6.pdf》数据手册查到
tSETTLING的典型值为3us,最大是4us,所以DAC 的转换速度最快是333K 左右。
不使用硬件触发(TEN=0),其转换的时间框图如图33.1.4 所示:

在这里插入图片描述
当DAC 的参考电压为Vref+的时候,DAC 的输出电压是线性的从0~Vref+,12 位模式下
DAC 输出电压与Vref+以及DORx 的计算公式如下:

DACx 输出电压= Vref *(DORx/4096

如果使用硬件触发(TENx=1),可通过外部事件(定时计数器、外部中断线)触发DAC 转
换。由TSELx[2:0]控制位来决定选择8 个触发事件中的一个来触发转换。触发事件如下表所示:
在这里插入图片描述
原表见《STM32F10xxx 参考手册_V10(中文版).pdf》第185 页表71。
每个DAC 通道都有DMA 功能,两个DMA 通道分别用于处理两个DAC 通道的DMA 请
求。如果DMAENx 位置1 时,如果发生外部触发(而不是软件触发),就会产生一个DMA 请
求,然后DAC_DHRx 寄存器的数据被转移到DAC_NORx 寄存器。

DAC 输出实验

本实验我们来学习DAC 输出实验。

DAC 寄存器

下面,我们介绍要实现DAC 的通道1 输出,需要用到的一些DAC 寄存器。
⚫ DAC 控制寄存器(DAC_CR)
DAC 控制寄存器描述如图33.2.1.1 所示:
在这里插入图片描述
DAC_CR 的低16 位用于控制通道1,高16 位用于控制通道2,下面介绍本实验需要设置
的一些位:
EN1 位用于DAC 通道1 的使能,我们需要用到DAC 通道1 的输出,该位必须设置为1。
BOFF1 位用于DAC 输出缓存控制,这里STM32 的DAC 输出缓存做的有些不好,如果使
能的话,虽然输出能力强一点,但是输出没法到0,这是个很严重的问题。所以本章的三个实
验我们都不使用输出缓存,即该位设置为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。
⚫ DAC 通道1 12 位右对齐数据保持寄存器(DAC_DHR12R1)
DAC 通道1 的12 位右对齐数据保持寄存器描述如图33.2.1.3 所示:
在这里插入图片描述

该寄存器用来设置DAC 输出,通过写入12 位数据到该寄存器,就可以在DAC 输出通道
1(PA4)得到我们所要的结果。

硬件设计

  1. 例程功能
    使用KEY1/KEY_UP 两个按键,控制STM32 内部DAC 的通道1 输出电压大小,然后通过
    ADC2 的通道14 采集DAC 输出的电压,在LCD 模块上面显示ADC 采集到的电压值以及DAC
    的设定输出电压值等信息。也可以通过usmart 调用dac_set_voltage 函数,来直接设置DAC 输
    出电压。LED0 闪烁,提示程序运行。
  2. 硬件资源
    1)LED 灯LED0 –PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)独立按键:KEY1 - PE3、WK_UP - PA0
    5)ADC3 :通道1 –PA1
    6)DAC1 :通道1 –PA4
  3. 原理图
    我们来看看原理图上ADC3 通道1(PA1)和DAC1 通道1(PA4)引出来的引脚,如下图
    所示:

在这里插入图片描述
我们只需要通过跳线帽连接ADC 和DAC,就可以使得ADC3 通道1(PA1)和DAC 1 通
道1(PA4)连接起来。对应的硬件连接如图33.2.2.2 所示:
在这里插入图片描述

程序设计

33.2.3.1 DAC 的HAL 库驱动

DAC 在HAL 库中的驱动代码在stm32f1xx_hal_dac.c 和stm32f1xx_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);
  1. 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通道输出缓冲区*/
} DAC_ChannelConfTypeDef;

形参2 用于选择要配置的通道,可选择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)开启DACx 和DAC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启DACx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到DAC1 通道1,
对应IO 是PA4,它们的时钟开启方法如下:

__HAL_RCC_DAC_CLK_ENABLE (); /* 使能DAC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

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 的输出值。

33.2.3.2 程序流程图
在这里插入图片描述
程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC 驱动源码包
括两个文件:dac.c 和dac.h。本实验有3 个实验,每一个实验的代码都是在上一个实验后面追
加。
dac.h 文件只有一些声明,下面直接开始介绍dac.c 的程序,首先是DAC 初始化函数。

/**
 * @brief DAC初始化函数
 * @note 本函数支持DAC1_OUT1/2通道初始化
 * DAC的输入时钟来自APB1, 时钟频率=36Mhz=27.8ns
 * DAC在输出buffer关闭的时候, 输出建立时间: tSETTLING = 4us
 * 因此DAC输出的最高速度约为:250Khz, 以10个点为一个周期, 最大能输出25Khz左右的波形
 * @param outx: 要初始化的通道. 1,通道1; 2,通道2
 * @retval 无
 */
void dac_init(uint8_t outx)
{
        GPIO_InitTypeDef gpio_init_struct;
        DAC_ChannelConfTypeDef dac_ch_conf;
        HAL_RCC_DAC_CLK_ENABLE(); /* 使能DAC1的时钟*/
        __HAL_RCC_GPIOA_CLK_ENABLE();/* 使能DAC OUT1/2的IO口时钟(都在PA口,PA4/PA5) */
        /* 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;/* DAC1输出缓冲关闭*/
        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通道1 */
                        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 输出电压函数,其定义如下:

/**
* @brief 设置通道1/2输出电压
* @param outx: 1,通道1; 2,通道2
* @param vol : 0~3300,代表0~3.3V
* @retval 无
*/
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 设置要输出
的电压值,设置的范围:03300,代表03.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(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        usmart_dev.init(72); /* 初始化USMART */
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        adc2_init(); /* 初始化ADC2 */
        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, "WK_UP:+ KEY1:-", 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 == WKUP_PRES)
                {
                        if (dacval < 4000)dacval += 200;
                        /* 输出增大200 */
                        HAL_DAC_SetValue(&g_dac1_handler,DAC_CHANNEL_1,DAC_ALIGN_12B_R,dacval);
                }
                else if (key == KEY1_PRES)
                {
                        if (dacval > 200)dacval -= 200;
                        else dacval = 0;
                        /* 输出减少200 */
                        HAL_DAC_SetValue(&g_dac1_handler,DAC_CHANNEL_1,DAC_ALIGN_12B_R,dacval);
                }
                /* WKUP/KEY1按下了,或者定时时间到了*/
                if (t == 10 || key == KEY1_PRES || key == WKUP_PRES)
                {
                        /* 读取前面设置DAC1_OUT1的值*/
                        adcx = HAL_DAC_GetValue(&g_dac1_handler, DAC_CHANNEL_1);
                        lcd_show_xnum(94, 150, adcx, 4, 16, 0, BLUE); /* 显示DAC寄存器值*/
                        temp = (float)adcx * (3.3 / 4096); /* 得到DAC电压值*/
                        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);/*显示电压值的小数部分*/
                        adcx = adc2_get_result_average(ADC2_CHY,20);/*得到ADC3通道1的转换结果*/
                        temp = (float)adcx * (3.3 / 4096); /*得到ADC电压值(adc是16bit的)*/
                        adcx = temp;
                        lcd_show_xnum(94, 190, temp, 1, 16, 0, BLUE); /* 显示电压值整数部分*/
                        temp -= adcx;
                        temp *= 1000;
                        lcd_show_xnum(110, 190, temp, 3, 16, 0X80, BLUE);/*显示电压值的小数部分*/
                        LED0_TOGGLE(); /* LED0闪烁*/
                        t = 0;
                }
                delay_ms(10);
        }
}

此部分代码,我们通过KEY_UP(WKUP 按键)和KEY1(也就是上下键)来实现对DAC
输出的幅值控制。按下KEY_UP 增加,按KEY1 减小。同时在LCD 上面显示DHR12R1 寄存
器的值、DAC 设置输出电压以及ADC 采集到的DAC 输出电压。

下载验证

下载代码后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示如图33.2.4.1
所示:
在这里插入图片描述
验证试验前,记得先通过跳线帽连接ADC 和DAC 排针,然后我们可以通过按WK_UP 按
键,增加DAC 输出的电压,这时ADC 采集到的电压也会增大,通过按KEY1 减小DAC 输出
的电压,这时ADC 采集到的电压也会减小。
除此之外,我们还可以通过usmart 调用dac_set_voltage 函数,来直接设置DAC 输出电压,
如下图33.2.4.2 所示:
在这里插入图片描述

DAC 输出三角波实验

本实验我们来学习使用如何让DAC 输出三角波,DAC 初始化部分还是用DAC 输出实验
的,所以做本实验的前提是先学习DAC 输出实验。

DAC 寄存器

本实验用到的寄存器在DAC 输出实验都有介绍。

硬件设计

  1. 例程功能
    使用DAC 输出三角波,通过KEY0/KEY1 两个按键,控制DAC1 的通道1 输出两种三角
    波,需要通过示波器接PA4 进行观察。也可以通过usmart 调用dac_triangular_wave 函数,来控
    制输出哪种三角波。LED0 闪烁,提示程序运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)独立按键:KEY0 –PE4、KEY1 –PE3
    5)DAC1 :通道1 - PA4
  3. 原理图
    我们只需要把示波器的探头接到DAC1 通道1(PA4)引脚,就可以在示波器上显示DAC
    输出的波形。PA4 对应P10 的DAC 排针,如图33.3.2.1 所示:

在这里插入图片描述

程序设计

本实验用到的DAC 的HAL 库API 函数前面都介绍过,具体调用情况请看程序解析部分。
下面介绍DAC 输出三角波的配置步骤。
DAC 输出三角波配置步骤
1)开启DACx 和DAC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启DACx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到DAC1 通道1,
对应IO 是PA4,它们的时钟开启方法如下:

__HAL_RCC_DAC_CLK_ENABLE (); /* 使能DAC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟*/

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 函数用于控制输出三角波。

程序流程图

在这里插入图片描述
程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC 驱动源码包
括两个文件:dac.c 和dac.h。

dac.h 文件只有一些声明,下面直接开始介绍dac.c 的程序,本实验的DAC 初始化我们还
是用到dac_init 函数,就添加了一个设置DAC_OUT1 输出三角波函数,其定义如下:

/**
 * @brief 设置DAC_OUT1输出三角波
 * @note 输出频率≈ 1000 / (dt * samples) Khz, 不过在dt较小的时候,比如小于5us
 * 时, 由于delay_us本身就不准了(调用函数,计算等都需要时间,延时很小的时候,这些
 * 时间会影响到延时), 频率会偏小.
 *
 * @param maxval: 最大值(0< maxval<4096), (maxval + 1)必须大于等于samples/2
 * @param dt : 每个采样点的延时时间(单位: us)
 * @param samples: 采样点的个数,必须小于等于(maxval+1)*2,且maxval不能等于0
 * @param n : 输出波形个数,0~65535
 *
 * @retval 无
 */
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((maxval + 1) <= samples)return ; /* 数据不合法*/
        incval = (maxval + 1) / (samples / 2); /* 计算递增量*/
        for(j = 0; j < n; j++)
        {
                Curval = 0; /* 先输出0 */
                HAL_DAC_SetValue(&dac1_handler,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
                for(i = 0; i < (samples / 2); i++) /* 输出上升沿*/
                {
                        Curval += incval; /* 新的输出值*/
                        HAL_DAC_SetValue(&dac1_handler,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
                        delay_us(dt);
                }
                for(i = 0; i < (samples / 2); i++) /* 输出下降沿*/
                {
                        Curval -= incval; /* 新的输出值*/
                        HAL_DAC_SetValue(&dac1_handler,DAC_CHANNEL_1,DAC_ALIGN_12B_R,Curval);
                        delay_us(dt);
                }
        }
}

该函数用于设置DAC 通道1 输出三角波,输出频率≈1000 / (dt * samples) Khz,形参含
义在源码已经有详细注释。该函数中,我们使用HAL_DAC_SetValue 函数来设置DAC 的输出
值,这样得到的三角波在示波器上可以看到。如果有跳动现象(不平稳),是正常的,因为调用
函数,计算等都需要时间,这样就会导致输出的波形是不太稳定的。越高性能的MCU,得到的
波形会越稳定。而且用HAL 库函数操作效率没有直接操作寄存器高,所以可以像寄存器版本实
验一样,直接操作DHR12R1 寄存器,得到的波形会相对稳定些。
由于使用HAL 库的函数,CPU 花费的时间会更长(因为指令变多了),在时间精度要求比
较高的应用,就不适合用HAL 库函数来操作了,这一点希望大家明白。所以学STM32 不是说
只要会HAL 库就可以了,对寄存器也是需要有一定的理解,最好是熟悉。这里用HAL 库操作
只是为了演示怎么使用HAL 库的相关函数。
最后在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        uint8_t key;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        usmart_dev.init(72); /* 初始化USMART */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        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) /* 高采样率, 约1Khz波形*/
                {
                        lcd_show_string(30, 130, 200, 16, 16, "DAC Wave1 ", BLUE);
                        /* 幅值4095, 采样点间隔5us, 200个采样点, 100个波形*/
                        dac_triangular_wave(4095, 5, 2000, 100);
                        lcd_show_string(30, 130, 200, 16, 16, "DAC None ", BLUE);
                }
                else if (key == KEY1_PRES) /* 低采样率, 约1Khz波形*/
                {
                        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。

下载验证

下载代码后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示如图33.3.4.1
所示:
在这里插入图片描述
没有按下任何按键之前,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 在示波器的显示情况如下图所示:
在这里插入图片描述
在这里插入图片描述
由上面两副测试图可以知道,三角波1 的频率是64.5Hz,三角波2 的频率是99.5Hz。三角
波2 基本接近我们算出来的结果0.1KHz,三角波1 有较大误差,在介绍dac_triangular_wave 函
数时也说了原因,加上三角波1 的采样率比较高,所以误差就会比较大。

DAC 输出正弦波实验

本实验我们来学习使用如何让DAC 输出正弦波。实验将用定时器7 来触发DAC 进行转换
输出正弦波,以DMA 传输数据的方式。

DAC 寄存器

本实验用到的寄存器在前面的实验都有介绍。

硬件设计

  1. 例程功能
    使用DAC 输出正弦波,通过KEY0/KEY1 两个按键,控制DAC1 的通道1 输出两种正弦
    波,需要通过示波器接PA4 进行观察。TFTLCD 显示DAC 转换值、电压值和ADC 的电压值。
    LED0 闪烁,提示程序运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
    4)独立按键:KEY0 –PE4、KEY1 –PE3
    5)ADC1 :通道1 –PA1
    6)DAC1 :通道1 –PA4
    7)DMA(DMA2_Channel13)
    8)定时器7
  3. 原理图
    我们只需要把示波器的探头接到DAC1 通道1(PA4)引脚,就可以在示波器上显示DAC
    输出的波形。PA4 对应P10 的DAC 排针,硬件连接如图33.4.2.1 所示:
    在这里插入图片描述

程序设计

33.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_DAC_Stop_DMA 函数
停止DAC 的DMA 方式函数,其声明如下:

HAL_StatusTypeDef HAL_DAC_Stop_DMA(DAC_HandleTypeDef *hdac, uint32_t Channel);

⚫ 函数描述:
用于停止DAC 的DMA 方式。
⚫ 函数形参:
形参1 是DAC_HandleTypeDef 结构体类型指针变量。
形参2 用于选择要启动的通道,可选择DAC_CHANNEL_1 或者DAC_CHANNEL_2。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
3. 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 和DAC 通道对应的IO 时钟,并配置该IO 为模拟功能
首先开启DACx 的时钟,然后配置GPIO 为模拟模式。本实验我们默认用到DAC1 通道1,
对应IO 是PA4,它们的时钟开启方法如下:

__HAL_RCC_DAC_CLK_ENABLE (); /* 使能DAC1时钟*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能GPIOA时钟*/

2)初始化DACx
通过HAL_DAC_Init 函数来设置需要初始化的DAC。该函数并没有设置任何DAC 相关寄
存器,也就是说没有对DAC 进行任何配置,它只是HAL 库提供用来在软件上初始化DAC。
注意:该函数会调用HAL_DAC_MspInit 函数来存放DAC 和对应通道的IO 时钟使能和初
始化IO 等代码。
3)配置DAC 通道
在HAL 库中,通过HAL_DAC_ConfigChannel 函数来设置配置DAC 的通道,根据需求设
置触发类型以及输出缓冲等。
4)配置DMA 并关联DAC
通过HAL_DMA_Init 函数初始化DMA,包括配置通道,外设地址,存储器地址,传输
数据量等。
HAL 库为了处理各类外设的DMA 请求,在调用相关函数之前,需要调用一个宏定义标识
符,来连接DMA 和外设句柄。这个宏定义为__HAL_LINKDMA。
5)配置定时器控制触发DAC
通过HAL_TIM_Base_Init 函数设置定时器溢出频率。
通过HAL_TIMEx_MasterConfigSynchronization 函数配置定时器溢出事件用做触发器。
通过HAL_TIM_Base_Start 函数启动计数。
6)启动DAC 转换并以DMA 方式传输数据
通过HAL_DAC_Stop_DMA 函数先停止之前的DMA 传输以及DAC 输出。
再通过HAL_DAC_Start_DMA 函数启动DMA 传输以及DAC 输出。
33.4.3.2 程序流程图
在这里插入图片描述
程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DAC 驱动源码包
括两个文件:dac.c 和dac.h。
dac.h 文件只有一些声明,下面直接开始介绍dac.c 的程序,本实验的DAC 以及DMA 的初
始化,我们用到dac_dma_wave_init 函数,其定义如下:

/**
 * @brief DAC DMA输出波形初始化函数
 * @note 本函数支持DAC1_OUT1/2通道初始化
 * DAC的输入时钟来自APB1, 时钟频率=36Mhz=27.7ns
 * DAC在输出buffer关闭的时候, 输出建立时间:tSETTLING = 4us(F103数据手册有写)
 * 因此DAC输出的最高速度约为:300Khz,以10个点为一个周期,最大能输出30Khz左右的波形
 *
 * @param outx: 要初始化的通道. 1,通道1; 2,通道2
 * @param par : 外设地址
 * @param mar : 存储器地址
 * @retval 无
 */
void dac_dma_wave_init(uint8_t outx, uint32_t par, uint32_t mar)
{
        GPIO_InitTypeDef gpio_init_struct;
        DAC_ChannelConfTypeDef dac_ch_conf={0};
        DMA_Channel_TypeDef *dmax_chy;
        if (outx == 1)
        {
                dmax_chy = DMA2_Channel3; /* OUT1对应DMA2_Channel3 */
        }
        else
        {
                dmax_chy = DMA2_Channel4; /* OUT2对应DMA2_Channel4 */
        }
        __HAL_RCC_GPIOA_CLK_ENABLE(); /* DAC通道引脚端口时钟使能*/
        __HAL_RCC_DAC_CLK_ENABLE(); /* 使能DAC1的时钟*/
        __HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能*/
        /* 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);
        /* 初始化DMA */
        g_dma_dac_handle.Instance = dmax_chy; /* 设置DMA通道*/
        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_MEDIUM; /* 中等优先级*/
        HAL_DMA_Init(&g_dma_dac_handle); /* 初始化DMA */
        HAL_DMA_Start(&g_dma_dac_handle, mar, par, 0); /* 配置DMA传输参数*/
        /* DMA句柄与DAC句柄关联*/
        __HAL_LINKDMA(&g_dac_dma_handle, DMA_Handle1, g_dma_dac_handle);
        /* 初始化DAC */
        g_dac_dma_handle.Instance = DAC;
        HAL_DAC_Init(&g_dac_dma_handle); /* 初始化DAC */
        /* 配置DAC通道*/
        dac_ch_conf.DAC_Trigger = DAC_TRIGGER_T7_TRGO; /* 使用TIM7 TRGO事件触发*/
        dac_ch_conf.DAC_OutputBuffer = DAC_OUTPUTBUFFER_DISABLE;/* DAC1输出缓冲关闭*/
        switch(outx)
        {
                case 1:
                        HAL_DAC_ConfigChannel(&g_dac_dma_handle, &dac_ch_conf,
                                        DAC_CHANNEL_1); /* DAC通道1配置*/
                        break;
                case 2:
                        HAL_DAC_ConfigChannel(&g_dac_dma_handle, &dac_ch_conf,
                                        DAC_CHANNEL_2); /* DAC通道2配置*/
                        break;
                default:break;
        }
}

该函数用于初始化DAC 用DMA 的方式输出正弦波。本函数用到的API 函数起前面都介
绍过,请结合前面介绍过的相关内容来理解源码。这里值得注意的是我们是采用定时器7 触发
DAC 进行转换输出的。
下面介绍DAC DMA 使能波形输出函数,其定义如下:

/**
* @brief DAC DMA使能波形输出
* @note TIM7的输入时钟频率(f)来自APB1, f = 36M * 2 = 72Mhz.
* DAC触发频率ftrgo = f / ((psc + 1) * (arr + 1))
* 波形频率= ftrgo / ndtr;
*
* @param outx : 要初始化的通道. 1,通道1; 2,通道2
* @param ndtr : DMA通道单次传输数据量
* @param arr : TIM7的自动重装载值
* @param psc : TIM7的分频系数
* @retval 无
*/
void dac_dma_wave_enable(uint16_t cndtr, uint16_t arr, uint16_t psc)
{
	TIM_HandleTypeDef tim7_handle= {0};
	TIM_MasterConfigTypeDef tim_master_config= {0};
	__HAL_RCC_TIM7_CLK_ENABLE(); /* TIM7时钟使能*/
	tim7_handle.Instance = TIM7; /* 选择定时器7 */
	tim7_handle.Init.Prescaler = psc; /* 预分频*/
	tim7_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数器*/
	tim7_handle.Init.Period = arr; /* 自动装载值*/
	HAL_TIM_Base_Init(&tim7_handle); /* 初始化定时器7 */
	/* 定时器更新事件用于触发*/
	tim_master_config.MasterOutputTrigger = TIM_TRGO_UPDATE;
	tim_master_config.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
	/* 配置定时器7的更新事件触发DAC转换*/
	HAL_TIMEx_MasterConfigSynchronization(&tim7_handle, &tim_master_config);
	HAL_TIM_Base_Start(&tim7_handle); /* 启动定时器7 */
	HAL_DAC_Stop_DMA(&g_dac_dma_handle, DAC_CHANNEL_1) /* 先停止之前的传输*/
	HAL_DAC_Start_DMA(&g_dac_dma_handle, DAC_CHANNEL_1,
	(uint32_t *)g_dac_sin_buf, cndtr, DAC_ALIGN_12B_R);
}

该函数用于使能波形输出,利用定时器7 的更新事件来触发DAC 转换输出。使能定时器7
的时钟后,调用HAL_TIMEx_MasterConfigSynchronization 函数配置TIM7 选择更新事件作为触
发输出(TRGO),然后调用HAL_DAC_Stop_DMA 函数停止DAC 转换以及DMA 传输,最后
再调用HAL_DAC_Start_DMA 函数重新配置并启动DAC 和DMA。
最后在main.c 里面编写如下代码:

uint16_t g_dac_sin_buf[4096]; /* 发送数据缓冲区*/
/**
 * @brief 产生正弦波序列
 * @note 需保证: maxval > samples/2
 *
 * @param maxval : 最大值(0 < maxval < 2048)
 * @param samples: 采样点的个数
 * @retval 无
 */
void dac_creat_sin_buf(uint16_t maxval, uint16_t samples)
{
        uint8_t i;
        float inc = (2 * 3.1415962) / samples; /* 计算增量(一个周期DAC_SIN_BUF个点)*/
        float outdata = 0;
        for (i = 0; i < samples; i++)
        {
                /* 计算以dots个点为周期的每个点的值,放大maxval倍,并偏移到正数区域*/
                outdata = maxval * (1 + sin(inc * i));
                if (outdata > 4095) outdata = 4095; /* 上限限定*/
                //printf("%f\r\n",outdata);
                g_dac_sin_buf[i] = outdata;
        }
}
/**
 * @brief 通过USMART设置正弦波输出参数,方便修改输出频率.
 * @param arr : TIM7的自动重装载值
 * @param psc : TIM7的分频系数
 * @retval 无
 */
void dac_dma_sin_set(uint16_t arr, uint16_t psc)
{
        dac_dma_wave_enable(100, arr, psc);
}
int main(void)
{
        uint16_t adcx;
        float temp;
        uint8_t t = 0;
        uint8_t key;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        usmart_dev.init(72); /* 初始化USMART */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        adc3_init(); /* 初始化ADC */
        adc3_channel_set(&g_adc3_handle, ADC3_CHY, ADC_CHANNEL_0,
                        ADC_SAMPLETIME_1CYCLE_5);
        /* 初始化DAC通道1 DMA波形输出*/
        dac_dma_wave_init(1, (uint32_t)&DAC1->DHR12R1, (uint32_t)g_dac_sin_buf);
        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);
        lcd_show_string(30, 110, 200, 16, 16, "KEY0:3Khz KEY1:30Khz", 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);
        dac_creat_sin_buf(2048, 100);
        /* 100Khz触发频率, 100个点, 得到1Khz的正弦波*/
        dac_dma_wave_enable(100, 10 - 1, 72 - 1);
        while (1)
        {
                t++;
                key = key_scan(0); /* 按键扫描*/
                if (key == KEY0_PRES) /* 高采样率, 约1Khz波形*/
                {
                        dac_creat_sin_buf(2048, 100);
                        /* 300Khz触发频率, 100个点, 得到最高3KHz的正弦波. */
                        dac_dma_wave_enable(100, 10 - 1, 24 - 1);
                }
                else if (key == KEY1_PRES) /* 低采样率, 约1Khz波形*/
                {
                        dac_creat_sin_buf(2048, 10);
                        /* 300Khz触发频率, 10个点, 可以得到最高30KHz的正弦波. */
                        dac_dma_wave_enable(10, 10 - 1, 24 - 1);
                }
                adcx = DAC1->DHR12R1; /* 获取DAC1_OUT1的输出状态*/
                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);/* 显示电压值的小数部分*/
                adcx = adc3_get_result_average(ADC3_CHY, 10);/* 得到ADC3通道1的转换结果*/
                temp = (float)adcx * (3.3 / 4096); /* 得到ADC电压值(adc是12bit的) */
                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);/* 显示电压值的小数部分*/
                if (t == 40) /* 定时时间到了*/
                {
                        LED0_TOGGLE(); /* LED0闪烁*/
                        t = 0;
                }
                delay_ms(5);
        }
}

adc3_init 函数初始化ADC3,用于测量DAC 通道1 的电压值。
dac_dma_wave_init 函数初始化DAC 通道1,并指定DMA 搬运的数据的开始地址和目标
地址。dac_creat_sin_buf 函数用于产生正弦波序列,并保存在g_dac_sin_buf 数组中,供给DAC
转换。在进入wilhe(1)循环之前,dac_dma_wave_enable 函数默认配置DAC的采样点个数时100,
并配置定时器7 的溢出频率为100KHz。这样就可以输出1KHz 的正弦波。下面给大家解释一
下为什么是输出1KHz 的正弦波?
定时器7 的溢出频率为100KHz,不记得怎么计算的朋友,请回顾基本定时器的相关内容,
这里直接把公式列出:

Tout= ((arr+1)*(psc+1))/Tclk

看到dac_dma_wave_enable(100, 10 - 1, 72 - 1);这个语句,第二个形参是自动重装载值,第
三个形参是分频系数,那么代入公式,可得:

Tout= ((arr+1)*(psc+1))/Tclk= ((9+1)*(71+1))/ 72000000= 0.00001s

得到定时器的更新事件周期是0.00001 秒,即更新事件频率为100KHz,也就得到DAC 输
出触发频率为100KHz。
再结合总一个正弦波共有100 个采样点,就可以得到正弦波的频率为100KHz/100 = 1KHz。
知道了正弦波的频率怎么来的,下面代码中,按下按键KEY0,得到3KHz 的正弦波,按
下按键KEY1,得到30KHz 的正弦波,计算方法都一样的。
dac_dma_sin_set 函数可以通过USMART 设置正弦波输出参数,方便修改输出频率。

下载验证

下载代码后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示如图33.4.4.1
所示:
在这里插入图片描述
用短路帽将ADC 和DAC 排针连接后,可以看到ADC VOL 的值随着DAC 的输出变化而
变化,即ADC 采集到的值是不停变化的。由于变化太快了,这样看不出采集到值形成什么波
形,下面我们借用示波器来进行观察,首先将探头接到DAC 的排针上。
没有按下任何按键之前,默认输出1KHz(100 个采样点)的正弦波,如下图所示:
在这里插入图片描述
当按下KEY0 后,DAC 输出3KHz(100 个采样点)的正弦波,如下图所示:
在这里插入图片描述
当按下KEY1 后,DAC 输出30KHz(10 个采样点)的正弦波,如下图所示:

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

正点原子STM32(基于HAL库)4 的相关文章

  • CCS软件的Graph功能

    如何正确使用CCS自带绘图Graph功能 Single Time使用演示 点击菜单栏Tools gt Graph gt Single Time 如图所示 点开后出现如下的对话窗口 下面对里面的每一项参数进行一下说明 Acquisition
  • linux系统PC机安装(非虚拟机,以centos为例)

    centos介绍 CentOS CommunityEnterprise Operating System 中文意思是 社区企业操作系统 我们有很多人叫它 社区企业操作系统 不管你怎么叫它 它都是linux的一个发行版本 CentOS并不是全

随机推荐

  • C语言指针知识点(一):字符指针(char *)及其格式输出(%c%d%s等)

    类型是分配内存块大小的别名 即类型 int double char 的作用就是分配相对应大小的内存 并给程序员一个名字 int double char 方便操作 指针也是一种数据类型 定义时可以对其赋值 可赋任意地址值 但习惯赋值为NULL
  • Windows10下VTR.7中VPR项目的运行

    下载VTR7和Visual Studio2022 点击sln文件 打开vpr工程 项目升级 vpr为VS2010的项目 需要先对工程文件升级后再编译 取消较小类型检查 上方菜单 项目 VPR属性 C C 代码生成 编译链接 运行 命令行运行
  • Elasticsearch Java 操作之后查询数据未及时更新

    在请求里加这个参数 request setRefreshPolicy WriteRequest RefreshPolicy IMMEDIATE 例如 public boolean saveOrUpdate String indexName
  • ListView具有多种item布局——实现微信对话列 .

    这篇文章的效果也是大家常见的 各种通讯应用的对话列表都是这种方式 像微信 whatsapp 易信 米聊等 我们这篇文章也权当为回忆 形成简单的笔记 这篇文章参考了2009年Google IO中的 TurboChargeYourUI How
  • linux启动,挂栽,共享,忘记密码的解决方法

    Linux修改linux的启动方式 修改linux启动方式 文本方式或xwindow方式 vi etc inittab 找到id x initdefault 一行 x 3为文本方式 x 5为xwindow方式 重启机器即可生效 mount用
  • Leetcode5438. 制作 m 束花所需的最少天数——另类的二分法

    文章目录 引入 二分法题解 制作 m 束花所需的最少天数 二分法题解 分割数组的最大值 二分法题解 两球之间的磁力 引入 之前在周赛遇到5438 制作 m 束花所需的最少天数 给你一个整数数组 bloomDay 以及两个整数 m 和 k 现
  • YOLOV5改进-添加EIoU,SIoU,AlphaIoU,FocalEIoU,Wise-IoU

    在YoloV5中添加EIoU SIoU AlphaIoU FocalEIoU Wise IoU 2023 2 7 更新 yolov5添加Wise IoUB站链接 重磅 YOLO模型改进集合指南 CSDN yolov5中box iou其默认用
  • java 16进制与图片互转

    十六进制转成图片 十六进制转成图片 author Administrator public static void saveToImgFile String src String output if src null src length
  • 使用JMS进行消息传递

    你需要什么 大约 15 分钟 IntelliJ IDEA或其他编辑器 JDK 1 8或更高版本 Maven 3 2 你会建立什么 本指南将指导您完成使用 JMS 代理发布和订阅消息的过程 您将构建一个应用程序 该应用程序使用Spring的
  • 关于项目属性书写应该严重注意的问题

    这样马马虎虎不注意属性的书写细节 会导致属性查询或者注入失败 public class Goods private Integer goodsId private String goodsName private String goodsT
  • C#学习笔记 常用的集合

    列表List lt T gt 列表List lt T gt 实现了IList ICollection IEnumberable IList接口 可以向该列表中动态的添加 删除 查找元素 如果列表中的元素满了 会动态分配一个容量是原来两倍的列
  • Docker 的基本概念和优势

    Docker是一个开源的容器化平台 可以将应用程序和所有依赖项打包在一起 形成一个独立的 可移植的容器 以下是Docker的基本概念和优势 基本概念 Docker镜像 Docker镜像是一个包含应用程序和所有依赖项的文件系统 它可以用来创建
  • 服务器系统如何账务处理,云服务器费用账务处理

    云服务器费用账务处理 内容精选 换一换 用户支付订单后 如果收到云服务器开通失败的短信 请致电华为云客服中心电话4000 955 988 客服会协助用户排除故障 开通云服务器 如果故障无法及时排除 用户可以选择取消订单 客服会做退费处理 将
  • Maven项目,本地jar包导入手动导入到Maven库中

    当你的项目 由于网络或者环境这些问题 无法从maven中央仓库更新jar包到本地的时候 可以尝试下面方法 手动添加jar包到Maven仓库 方法一 推荐 1 需要先拿到你的jar包 copy到本地 例如我的就是hutool all 5 8
  • sql注入详细过程

    前提 mysql5 0以上版本包含内置的information schema数据库 它储存着mysql所有的数据库和表结构信息 利用该数据库可以查询到所有的数据库和表的内容 一 5 0 暴力破解的方式获取数据 1 原理 当我们的Web ap
  • 运维工程师绩效考核表_运维人员初步 度绩效考核表

    姓 名 部 门 岗 位 上级领导 时 间 考核分类 考核维 度 权重 指标 数据来源 考核评分 复核 10 计划合理 偏差范围可控 10 分 考评 30 按计划完成的时间 30分 考评 10 机房设备的是否正常运行 10 考评 10 网站的
  • 最大流算法 - 标号法

    标号法求最大流 图论中网络的相关概念见上篇博客 算法基本思想 从某个初始流开始 重复地增加流的值到不能再改进为止 则最后所得的流将是一个最大流 为此 不妨将每条边上的流量设置为0作为初始流量 为了增加给定流量的值 我们必须找出从发点到收点的
  • python3.11安装, 解决pip is configured with locations that require TLS/SSL问题

    系统 centos7 4 虚拟机 python版本 本机自带的2 7 5 以及参考python安装的python3 11 pip版本 本机自带的8 1 2 参考pip安装 升级升级到了20 3 4 pip3版本为22 3 1 openssl
  • FPGA实战小项目2

    基于FPGA的贪吃蛇游戏 基于FPGA的贪吃蛇游戏 基于fpga的数字密码锁ego1 基于fpga的数字密码锁ego1 基于fpga的数字时钟 basys3 基于fpga的数字时钟 basys3
  • 正点原子STM32(基于HAL库)4

    目录 ADC 实验 ADC 简介 单通道ADC 采集实验 ADC 寄存器 硬件设计 程序设计 下载验证 单通道ADC 采集 DMA 读取 实验 ADC DMA 寄存器 硬件设计 程序设计 下载验证 多通道ADC 采集 DMA 读取 实验 A