OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 3585|回复: 0

《STM32F407 探索者开发指南》第三十一章 ADC实验(上)

[复制链接]

1140

主题

1152

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4895
金钱
4895
注册时间
2019-5-8
在线时间
1248 小时
发表于 2023-8-3 15:42:55 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-8-3 15:46 编辑

第三十一章 ADC实验
1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1

3)购买链接:https://detail.tmall.com/item.htm?id=609294673401

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

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

31.1 ADC简介
ADC即模拟数字转换器,英文详称Analog-to-digitalconverter,可以将外部的模拟信号转换为数字信号。

STM32F4xx系列芯片拥有3个ADC,这些ADC可以独立使用,其中ADC1和ADC2还可以组成双重模式(提高采样率)。STM32的ADC是12位逐次逼近型的模拟数字转换器。它有19个通道,可测量16个外部和2个内部信号源和Vbat通道的信号ADC中的各个通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以以左对齐或者右对齐存储在16位数据寄存器中。ADC具有模拟看门狗的特性,允许应用检测输入电压是否超过了用户自定义的阈值上限或下限。

STM32F407的ADC主要特性我们可以总结为以下几条:
1、可配置12位、10位、8位或6位分辨率;
2、转换结束、注入转换结束和发生模拟看门狗事件时产生中断
3、单次和连续转换模式
4、自校准
5、带内嵌数据一致性的数据对齐
6、采样间隔可以按通道分别编程
7、规则转换和注入转换均有外部触发选项
8、间断模式
9、双重模式(带2个或以上ADC的器件)
10、ADC转换时间:最大转换速率为2.4MHz,转换时间为0.41us
11、ADC供电要求:2.4V到3.6V
12、ADC输入范围:VREF–≤VIN≤VREF+
13、规则通道转换期间有DMA请求产生

下面来介绍ADC的框图:                              
image001.png
图31.1.1 ADC框图

图中,我们按照ADC的配置流程标记了七处位置,分别如下:

输入电压
在前面ADC的主要特性也对输入电压有所提及,ADC输入范围VREF–≤VIN≤VREF+,最终还是由VREF–、VREF+、VDDA和VSSA决定的。下面看一下这几个参数的关系,如图31.1.2所示:     
image003.png
图31.1.2 参数关系图

从上图可以知道,VDDA和VREF+接VCC3.3,而VSSA和VREF-是接地,所以ADC的输入范围即0~3.3V。R55默认焊接,R54默认不焊接。

输入通道
在确定好了ADC输入电压后,如何把外部输入的电压输送到ADC转换器中呢,在这里引入了ADC的输入通道,在前面也提及到了ADC1有16个外部通道和3个内部通道,而ADC2和ADC3只有有16个外部通道。ADC1的外部通道是通道17、通道18和通道19,分别连接到内部温度传感器、内部Vrefint和Vbat。外部通道对应的是图31.1.1中的ADCx_IN0、ADCx_IN1…ADCx_IN15。ADC通道表见表31.1.1所示:
QQ截图20230801143855.png
表31.1.1 ADC通道表

转换顺序
当任意ADCx多个通道以任意顺序进行一系列转换就诞生了成组转换,这里就有两种成组转换类型:规则组和注入组。规则组就是图上的规则通道,注入组也就是图上的注入通道。为了避免大家对输入通道加上规则通道和注入通道理解有所模糊,后面规则通道以规则组来代称,注入通道以注入组来代称。

规则组允许最多16个输入通道进行转换,而注入组允许最多4个输入通道进行转换。这里讲解一下规则组和注入组。

规则组(规则通道)
规则组,按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最多也是规则组。

注入组(注入通道)
注入组,按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。
便于理解,下面看一下规则组和注入组的对比图,如图31.1.3所示:
image006.png
图31.1.3 规则组和注入组的对比图

在了解了规则组和注入组后,现在了解一下它们两者的转换顺序。

规则序列
规则组是允许16个通道进行转换的,那么就需要安排通道转换的次序即规则序列。规则序列寄存器有3个,分别为SQR1、SQR2和SQR3。SQR3控制规则序列中的第1个到第6个转换的通道;SQR2控制规则序列中第7个到第12个转换的通道;SQR1控制规则序列中第13个到第16个转换的通道。规则序列寄存器SQRx详表如表31.1.2所示:
QQ截图20230801143911.png
表31.1.2 规则序列寄存器SQRx详表

从上表可以知道,当我们想把ADC的输入通道1安排到第1个转换,那么只需要在SQR3寄存器中的SQ1[4:0]位写入该ADC输入通道即写1处理即可。SQR1的SQL[3:0]决定了具体使用多少个通道。

注入序列
注入序列,跟规则序列差不多,都是有顺序的安排。由于注入组最大允许4个通道输入,所以这里就使用了一个寄存器JSQR。注入序列寄存器JSQR详表如表31.1.3所示:
QQ截图20230801144005.png
表31.1.3 注入序列寄存器JSQR详表

触发源
在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC的触发转换有两种方法:分别是通过软件或外部事件(也就是硬件)触发转换。

我们先来看看通过写软件触发转换的方法。方法是:通过写ADC_CR2寄存器的ADON这个位来控制,写1就开始转换,写0就停止转换,这个控制ADC转换的方式非常简单。

另一种就是通过外部事件触发转换的方法,有定时器和输入引脚触发等等。这里区分规则组和注入组。方法是:通过ADC_CR2寄存器的EXTSET[2:0]选择规则组的触发源,JEXTSET[2:0]选择注入组的触发源。通过ADC_CR2的EXTTRIG和JEXTTRIG这两位去激活触发源。ADC3的触发源和ADC1/2不同,这里需要注意,那么在框图里已经标记出来了。

转换时间
STM32F407的ADC总转换时间的计算公式如下:
TCONV= 采样时间 + 12个周期
采样时间可通过ADC_SMPR1和ADC_SMPR2寄存器中的SMP[2:0]位编程,ADC_SMPR2控制的是通道0~9,ADC_SMPR1控制的是通道10~18。所有通道都可以通过编程来控制使用不同的采样时间,可选采样时间值如下:
l  SMP = 000:3个ADC时钟周期
l  SMP = 001:15个ADC时钟周期
l  SMP = 010:28个ADC时钟周期
l  SMP = 011:56个ADC时钟周期
l  SMP = 100:84个ADC时钟周期
l  SMP = 101:112个ADC时钟周期
l  SMP = 110:144个ADC时钟周期
l  SMP = 111:480个ADC时钟周期

12个周期是ADC输入时钟ADC_CLK决定的。ADC_CLK是由APB2经过分频产生,分频系数是由RCC_CFGR寄存器中的PPRE2[2:0]进行设置,有2/4/6/8/16分频选项。

采样时间最小是3个时钟周期,这个采样时间下,我们可以得到最快的采样速度。举个例子,我们采用最高的采样速率,使用采样时间为3个ADC时钟周期,那么得到:
TCONV = 3个ADC时钟周期 + 12个ADC时钟周期 = 15个ADC时钟周期

一般APB2的时钟是84MHz,经过ADC分频器的4分频后,ADC时钟频率就为21MHz。通过换算可得到:
TCONV= 14个ADC时钟周期 =(1/21000000)*14s = 0.71us

数据寄存器
ADC转换完成后的数据输出寄存器。根据转换组的不同,规则组的完成转换的数据输出到ADC_DR寄存器,注入组的完成转换的数据输出到ADC_JDRx寄存器。假如是使用双重模式,规则组的数据也是存放在ADC_DR寄存器。这两个寄存器的讲解将会在后面讲解,这里就不列出来了。

中断
规则和注入组转换结束时能产生中断,当模拟看门狗状态位被设置时也能产生中断。它们在ADC_SR中都有独立的中断使能位,后面讲解ADC_SR寄存器时再进行展开。这里讲解一下,模拟看门狗中断以及DMA请求。

模拟看门狗中断
模拟看门狗中断发生条件:首先通过ADC_LTR和ADC_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被ADC转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是3.0V,那么模拟电压超过3.0V的时候,就会产生模拟看门狗中断,低阈值的情况类似。

DMA请求
规则组和注入组的转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据存储在内存里面,防止读取不及时数据被覆盖。

31.2 单通道ADC采集实验
STM32F407的ADC可以进行很多种不同的转换模式,在《STM32F4xx参考手册_V4(中文版).pdf》的第11章也都有详细介绍。本实验我们来学习使用规则单通道的单次转换模式。

STM32F407的ADC在单次转换模式下(CONT位为0),只执行一次转换,该模式可以通过ADC_CR2寄存器的ADON位(只适用于规则通道)启动,也可以通过外部触发启动(适用于规则通道和注入通道,但是必须先设置EXTTRIG/JEXTTRIG位)。

以规则通道为例,一旦所选择的通道转换完成,转换结果将被存在ADC_DR寄存器中,EOC(转换结束)标志将被置位,如果设置了EOCIE,则会产生中断。然后ADC将停止,直到下次启动。

31.2.1 ADC寄存器
下面,我们介绍执行规则通道的单次转换,需要用到的一些ADC寄存器。

l  ADC控制寄存器1ADC_CR1
ADC控制寄存器1描述如图31.2.1.1所示:     
image009.png
图31.2.1.1 ADC_CR1寄存器(部分)

该寄存器本章用到SCAN位,用于设置扫描模式,由软件设置和清除,如果设置为1,则使用扫描模式,如果为0,则关闭扫描模式。在本章实验中使用的是非扫描模式。在扫描模式下,由ADC_SQRx或ADC_JSQRx寄存器选中的通道被转换。如果设置了EOCIE或JEOCIE,只会在最后一个通道转换完成后才会产生EOC或JEOC中断。

l  ADC控制寄存器2ADC_CR2
ADC控制寄存器2描述如图31.2.1.2所示:     
image011.png
图31.2.1.2 ADC_CR2寄存器(部分)

该寄存器我们针对性的介绍一些位:ADON位用于开关AD转换器。而CONT位用于设置是否进行连续转换,我们使用单次转换,所以CONT位必须为0。CAL和RSTCAL用于AD校准。ALIGN用于设置数据对齐,我们使用右对齐,所以该位设置为0。EXTSEL[3:0]用于选择启动规则转换组转换的外部事件,我们这里使用的是软件触发(SWSTART),所以这里设置这3位为111。SWSTART位用于开始规则通道的转换,我们每次转换(单次转换模式下)都需要向该位写1。

l  ADC采样事件寄存器1ADC_SMPR1
ADC采样事件寄存器1描述如图31.2.1.3所示:     
image013.png
图31.2.1.3 ADC_SMPR1寄存器(部分)

l  ADC采样事件寄存器2ADC_SMPR2
ADC采样事件寄存器2描述如图31.2.1.4所示:     
image015.png
图31.2.1.4 ADC_SMPR2寄存器(部分)

这里结合两个ADC采样事件寄存器进行讲解,这两个寄存器用于设置通道0~18的采样事件,每个通道占用3个位。

对于每个要转换的通道,采样事件建议尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率。

l  ADC规则序列寄存器1
ADC规则序列寄存器共有3个,这几个寄存器的功能都差不多,这里我们仅介绍一下ADC规则序列寄存器1(ADC_SQR1),描述如图31.2.1.5所示:     
image017.png
图31.2.1.5 ADC_SQR1寄存器

L[3:0]:用于存储规则序列的长度,取值范围:0~15,我们这里只用了一个,所以设置这几位的值为0。其他的SQ13~SQ16则存储了规则序列中的第13~16通道的编号(编号范围:0~18)。另外两个规则序列寄存器同ADC_SQR1大同小异,这里就不再介绍了,要说明一点的是:我们选择的是单次转换,所以只有一个通道在规则序列里,这个序列就是SQ1,通过ADC_SQR3的最低5位(也就是SQ1)设置。

l  ADC规则数据寄存器(ADC_DR
ADC规则数据寄存器描述如图31.2.1.6所示:     
image019.png
图31.2.1.6 ADC_DR寄存器

在规则序列中AD转换结果都将被存在这个寄存器里面,而注入通道的转换结果被保存在ADC_JDRx里面。该寄存器的数据可以通过ADC_CR2的ALIGN位设置左对齐还是右对齐。在读取数据的时候要注意。

l  ADC状态寄存器(ADC_SR
ADC状态寄存器描述如图31.2.1.7所示:     
image021.png
图31.2.1.7 ADC_SR寄存器

该寄存器保存了ADC转换时的各种状态。这里我们要用到的是EOC位,我们通过该位来决定是否此次规则通道的AD转换已经完成,如果完成我们就从ADC_DR中读取转换结果,否则等待转换完成。

至此,本章要用到的ADC相关寄存器全部介绍完毕了,对于未介绍的部分,请大家参考《STM32F4xx参考手册_V4(中文版).pdf》第11章相关内容。

31.2.2 硬件设计
1. 例程功能
使用ADC1采集通道5(PA5)上面的电压,在LCD模块上面显示ADC转换值以及换算成电压后的电压值。使用杜邦线将P11的ADC和RV1连接,使得PA5连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。

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

3. 原理图
ADC属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,不过我们需要在外部连接其端口到被测电压上面。本实验,我们通过ADC1的通道5(PA5)来采集外部电压值,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过杜邦线将ADC与电位器连接,如下图所示:
image023.png
图31.2.2.1 连接实物图

image025.png
图31.2.2.2 ADC输入通道5与电位器连接原理

使用一根杜邦线的两端将P11的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。

有的朋友可能还想测试其他地方的电压值,我们还可以通过杜邦线,一端接到P11的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
31.2.3 程序设计
31.2.3.1 ADCHAL库驱动
ADC在HAL库中的驱动代码在stm32f4xx_hal_adc.c和stm32f4xx_hal_adc_ex.c文件(及其头文件)中。

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

l  函数描述:
用于初始化ADC。

l  函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量,其定义如下:
  1. typedef struct
  2. {
  3.   ADC_TypeDef                    *Instance;        /* ADC寄存器基地址 */
  4. ADC_InitTypeDef               Init;               /* ADC参数初始化结构体变量 */
  5.   __IO uint32_t                  NbrOfCurrentConversionRank;/* 当前转换等级的ADC数 */
  6. DMA_HandleTypeDef             *DMA_Handle;      /* DMA配置结构体 */
  7. HAL_LockTypeDef               Lock;               /* ADC锁定对象 */
  8.   __IO uint32_t                 State;             /* ADC工作状态 */
  9.   __IO uint32_t                 ErrorCode;         /* ADC错误代码 */
  10. }ADC_HandleTypeDef;
复制代码
该结构体定义和其他外设比较类似,我们着重看第二个成员变量Init含义,它是结构体ADC_InitTypeDef类型,结构体ADC_InitTypeDef定义为:
  1. typedef struct {
  2. uint32_t ClockPrescaler;                     /* 设置预分频系数,即PRESC[3:0]位 */
  3. uint32_t Resolution;                          /* 配置ADC的分辨率 */
  4. uint32_t ScanConvMode;                       /* 扫描模式 */
  5. uint32_t EOCSelection;                       /* 转换完成标志位 */
  6. FunctionalState ContinuousConvMode;        /* 开启连续转换模式否则就是单次转换模式 */
  7. uint32_t NbrOfConversion;                    /* 设置转换通道数目 */
  8. FunctionalState DiscontinuousConvMode;    /* 单次转换模式选择 */
  9. uint32_tNbrOfDiscConversion;               /* 单次转换通道的数目 */
  10. uint32_t ExternalTrigConv;                   /* ADC外部触发源选择 */
  11. uint32_tExternalTrigConvEdge;              /* ADC外部触发极性*/
  12. FunctionalState DMAContinuousRequests;     /* DMA转换请求模式*/
  13. }ADC_InitTypeDef;
复制代码
1) ClockPrescaler:ADC预分频系数选择,可选的分频系数为2、4、6、8。由于ADC最大时钟不得超过36Mhz,我们这里配置4分频,即ADC的时钟频率为:84 / 4 = 21Mhz。

2) Resolution:配置ADC的分辨率,可选的分辨率有12位、10位、8位和6位。分辨率越高,转换数据精度越高,转换时间也越长;反之分辨率越低,转换数据精度越低,转换时间也越短。

3) ScanConvMode:配置是否使用扫描。如果是单通道转换使用ADC_SCAN_DISABLE,如果是多通道转换使用ADC_SCAN_ENABLE。

4) EOCSelection:可选参数为ADC_EOC_SINGLE_CONV和ADC_EOC_SEQ_CONV,指定转换结束时是否产生EOS中断或事件标志。

5) ContinuousConvMode:可选参数为ENABLE和DISABLE,配置自动连续转换还是单次转换。使用ENABLE配置为使能自动连续转换;使用DISABLE配置为单次转换,转换一次后停止需要手动控制才重新启动转换。

6)NbrOfConversion:设置常规转换通道数目,范围是:1~16。

7) DiscontinuousConvMode:配置是否使用不连续的采样模式,比如要转换的通道有1、2、5、7、8、9,那么第一次触发会进行通道1 与通道 2,下次触发就是转换通道 5 与通道 7,这样不连续的转换,依次类推。此参数只有将 ScanConvMode 使能,还有ContinuousConvMode失能的情况下才有效,不可同时使能。

8)NbrOfDiscConversion:不连续采样通道数。

9) ExternalTrigConv:外部触发方式的选择,如果使用软件触发,那么外部触发会关闭。

10) ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。

11) DMAContinuousRequests:指定DMA请求是否以一次性模式执行(当达到转换次数时,DMA传输停止)或在连续模式下(DMA传输无限制,无论转换的数量)。注:在连续模式下,DMA必须配置为循环模式。否则,当达到DMA缓冲区最大指针时将触发溢出。注意:当常规组和注入组都没有转换时(禁用ADC,或启用ADC,没有连续模式或可以启动转换的外部触发器),必须修改此参数。该参数可设置为“启用”或“禁用”。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

2.  HAL_ADCEx_Calibration_Start函数
ADC的自校准函数,其声明如下:
  1. HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef *hadc);
复制代码

l  函数描述:
首先调用HAL_ADC_Init函数配置了相关的功能后,再调用此函数进行ADC自校准功能。

l  函数形参:
ADC_HandleTypeDef结构体类型指针变量。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

3.HAL_ADC_ConfigChannel函数
ADC通道配置函数,其声明如下:
  1. HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef*hadc,ADC_ChannelConfTypeDef *sConfig);
复制代码
l  函数描述:
调用HAL_ADC_Init函数配置了相关的功能后,就可以调用此函数进行ADC自校准功能。

l  函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是ADC_ChannelConfTypeDef结构体类型指针变量,用于配置ADC采样时间,使用的通道号,单端或者差分方式的配置等。该结构体定义如下:
  1. typedef struct {
  2.   uint32_t Channel;                        /* ADC转换通道*/
  3.   uint32_t Rank;                            /* ADC转换顺序 */
  4.   uint32_t SamplingTime;                  /* ADC采样周期 */
  5.   uint32_t Offset;                         /* ADC偏移量 */
  6. }ADC_ChannelConfTypeDef;
复制代码
1) Channel:ADC转换通道,范围:0~19。
2) Rank:在常规转换中的常规组的转换顺序,可以选择1~16。
3) SamplingTime:ADC的采样周期,最大480个ADC时钟周期,要求采样周期尽量大,以减少误差。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

4. HAL_ADC_Start函数
ADC转换启动函数,其声明如下:
  1. HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
复制代码

l  函数描述:
当配置好ADC的基础的功能后,就调用此函数启动ADC。

l  函数形参:
ADC_HandleTypeDef结构体类型指针变量。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

5.HAL_ADC_PollForConversion函数
等待ADC规则组转换完成函数,其声明如下:
  1. HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef*hadc,uint32_t Timeout);
复制代码
l  函数描述:
一般先调用HAL_ADC_Start函数启动转换,再调用该函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。

l  函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是等待转换的等待时间,单位:毫秒ms。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

6. HAL_ADC_GetValue函数
获取常规组ADC转换值函数,其声明如下:
  1. uint32_tHAL_ADC_GetValue(ADC_HandleTypeDef *hadc);
复制代码

l  函数描述:
一般先调用HAL_ADC_Start函数启动转换,再调用HAL_ADC_PollForConversion函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。

l  函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。

l  函数返回值:
当前的转换值,uint32_t类型数据。

单通道ADC采集配置步骤
1)开启ADCx和通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启ADCx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到ADC1通道5,对应IO是PA5,它们的时钟开启方法如下:
  1. __HAL_RCC_ADC1_CLK_ENABLE();            /* 使能ADC1时钟 */
  2. __HAL_RCC_GPIOA_CLK_ENABLE();           /* 开启GPIOA时钟 */
复制代码
IO口复用功能是通过函数HAL_GPIO_Init来配置的。

2)初始化ADCx配置其工作参数
通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
注意:该函数会调用:HAL_ADC_MspInit回调函数来完成对ADC底层以及其输入通道IO的初始化,包括:ADC及GPIO时钟使能、GPIO模式设置等。

3)配置ADC通道并启动AD转换器
在HAL库中,通过HAL_ADC_ConfigChannel函数来设置配置ADC的通道,根据需求设置通道、序列、采样时间和校准配置单端输入模式或差分输入模式等。
配置好ADC通道之后,通过HAL_ADC_Start函数启动AD转换器。

4)读取ADC
这里选择查询方式读取,在读取ADC值之前需要调用HAL_ADC_PollForConversion等待上一次转换结束。然后就可以通过HAL_ADC_GetValue来读取ADC值。

31.2.3.2 程序流程图
QQ截图20230801144309.png
图31.2.3.2.1 单通道ADC采集实验程序流程图

31.2.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。该章节有四个实验,每一个实验的代码都是在上一个实验后面追加。

adc.h文件针对ADC及通道引脚定义了一些宏定义,具体如下:
  1. /* ADC及引脚 定义 */
  2. #define ADC_ADCX_CHY_GPIO_PORT            GPIOA
  3. #define ADC_ADCX_CHY_GPIO_PIN             GPIO_PIN_5
  4. #define ADC_ADCX_CHY_GPIO_CLK_ENABLE()  do{ __HAL_RCC_GPIOC_CLK_ENABLE();\
  5. }while(0)              /* PA口时钟使能 */
  6. #define ADC_ADCX                            ADC1
  7. #define ADC_ADCX_CHY                     ADC_CHANNEL_5 /* 通道Y,  0 <= Y <=16 */
  8. /* ADC1时钟使能 */
  9. #define ADC_ADCX_CHY_CLK_ENABLE()      do{ __HAL_RCC_ADC1_CLK_ENABLE();}while(0)
复制代码
ADC的通道与引脚的对应关系在STM32中文数据手册可以查到,我们这里使用ADC1的通道5,在数据手册中的表格为:
image029.png
表31.2.3.3.1 ADC1通道5对应引脚查看表

下面直接开始介绍adc.c的程序,首先是ADC初始化函数。
  1. /**
  2. *@brief       ADC初始化函数
  3. *  @note      本函数支持ADC1/ADC2任意通道, 但是不支持ADC3
  4. *              我们使用12位精度, ADC采样时钟=21M, 转换时间为: 采样周期 + 12个ADC周期
  5. *              设置最大采样周期: 480, 则转换时间 = 492 个ADC周期 = 23.42us
  6. *@param       无
  7. *@retval      无
  8. */
  9. void adc_init(void)
  10. {
  11.    g_adc_handle.Instance = ADC_ADCX;
  12.     /*4分频,ADCCLK =PCLK2/4 = 84/4 = 21Mhz */
  13.    g_adc_handle.Init.ClockPrescaler=ADC_CLOCKPRESCALER_PCLK_DIV4;
  14.    g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B;     /* 12位模式 */
  15.    g_adc_handle.Init.DataAlign =ADC_DATAALIGN_RIGHT;     /* 右对齐 */
  16.    g_adc_handle.Init.ScanConvMode = DISABLE;                /* 非扫描模式 */
  17.    g_adc_handle.Init.ContinuousConvMode= DISABLE;         /* 关闭连续转换 */
  18.     /* 1个转换在规则序列中 也就是只转换规则序列1 */
  19.    g_adc_handle.Init.NbrOfConversion= 1;
  20.    g_adc_handle.Init.DiscontinuousConvMode= DISABLE;     /* 禁止不连续采样模式 */
  21.    g_adc_handle.Init.NbrOfDiscConversion= 0;              /* 不连续采样通道数为0 */
  22.    g_adc_handle.Init.ExternalTrigConv=ADC_SOFTWARE_START;   /* 软件触发 */
  23.    g_adc_handle.Init.ExternalTrigConvEdge=ADC_EXTERNALTRIGCONVEDGE_NONE;
  24.                                                                          /* 使用软件触发 */
  25.    g_adc_handle.Init.DMAContinuousRequests= DISABLE;          /* 关闭DMA请求 */
  26.    HAL_ADC_Init(&g_adc_handle);                                     /* 初始化 */
  27. }
复制代码
该函数调用HAL_ADC_Init函数配置了ADC的基础功能参数,HAL_ADC_Init函数的MSP回调函数是HAL_ADC_MspInit,用来使能时钟和初始化IO口。HAL_ADC_MspInit函数定义如下:
  1. /**
  2. * @brief       ADC底层驱动,引脚配置,时钟使能
  3.                   此函数会被HAL_ADC_Init()调用
  4. * @param       hadc:ADC句柄
  5. * @retval      无
  6. */
  7. void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
  8. {
  9.     if(hadc->Instance == ADC_ADCX)
  10.     {
  11.        GPIO_InitTypeDef gpio_init_struct;
  12.        ADC_ADCX_CHY_CLK_ENABLE();       /* 使能ADCx时钟 */
  13.        ADC_ADCX_CHY_GPIO_CLK_ENABLE();  /* 开启GPIO时钟 */
  14.         /* AD采集引脚模式设置,模拟输入 */
  15.         gpio_init_struct.Pin =ADC_ADCX_CHY_GPIO_PIN;
  16.         gpio_init_struct.Mode = GPIO_MODE_ANALOG;
  17.         gpio_init_struct.Pull = GPIO_PULLUP;
  18.        HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
  19.     }
  20. }
复制代码
下面是获得ADC转换后的结果函数,其定义如下:
  1. /**
  2. * @brief       获得ADC转换后的结果
  3. * @param       ch: 通道值 0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17
  4. * @retval      无
  5. */
  6. uint32_t adc_get_result(uint32_t ch)
  7. {
  8.     adc_channel_set(&g_adc_handle, ch, 1,  ADC_SAMPLETIME_480CYCLES);
  9.                                                             /* 设置通道,序列和采样时间 */
  10.     HAL_ADC_Start(&g_adc_handle);                      /* 开启ADC */
  11.    HAL_ADC_PollForConversion(&g_adc_handle, 10);  /* 轮询转换 */
  12. /*返回最近一次ADC1规则组的转换结果 */
  13. return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
  14. }
复制代码
该函数先是调用我们自己定义的adc_channel_set函数设置ADC通道的转换序列和采样周期等功能,再是调用HAL_ADC_Start启动转换、HAL_ADC_PollForConversion函数等待转换完成、HAL_ADC_GetValue函数获取转换后的当前结果。

adc_channel_set函数的定义如下:
  1. /**
  2. *@brief       设置ADC通道采样时间
  3. *@param       adcx : adc句柄指针,ADC_HandleTypeDef
  4. *@param       ch   : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17
  5. *@param       stime: 采样时间  0~7, 对应关系为:
  6. *  @arg      ADC_SAMPLETIME_3CYCLES,  3个ADC时钟周期
  7.                  ADC_SAMPLETIME_15CYCLES, 15个ADC时钟周期
  8. *  @arg      ADC_SAMPLETIME_28CYCLES, 28个ADC时钟周期
  9.                  ADC_SAMPLETIME_56CYCLES, 56个ADC时钟周期
  10. *  @arg      ADC_SAMPLETIME_84CYCLES, 84个ADC时钟周期
  11.                  ADC_SAMPLETIME_112CYCLES,112个ADC时钟周期
  12. *  @arg      ADC_SAMPLETIME_144CYCLES,144个ADC时钟周期
  13.                  ADC_SAMPLETIME_480CYCLES,480个ADC时钟周期
  14. *@param      rank: 多通道采集时需要设置的采集编号,
  15.                 假设你定义channel1的rank=1,channel2的rank=2,
  16.                 那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结果,
  17.                 AdcDMA[1]就是通道2的转换结果。
  18.                 单通道DMA设置为 ADC_REGULAR_RANK_1
  19. *   @arg      编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16
  20. *@retval    无
  21. */
  22. voidadc_channel_set(ADC_HandleTypeDef*adc_handle, uint32_t ch, uint32_t rank,
  23. uint32_t stime)
  24. {
  25.     /* 配置对应ADC通道 */
  26.    ADC_ChannelConfTypeDef adc_channel;
  27.    adc_channel.Channel = ch;               /* 设置ADCX对通道ch */
  28.    adc_channel.Rank = rank;                /* 设置采样序列 */
  29.    adc_channel.SamplingTime = stime;       /* 设置采样时间 */
  30.    HAL_ADC_ConfigChannel( adc_handle, &adc_channel);   
  31. }
复制代码
该函数主要是通过HAL_ADC_ConfigChannel函数设置通道的转换序列和采样周期等功能。

下面介绍的是获取ADC某通道的转换多次后的平均值函数,函数定义如下:
  1. /**
  2. * @brief       获取通道ch的转换值,取times次,然后平均
  3. * @param       ch     : 通道号, 0~17
  4. * @param       times  : 获取次数
  5. * @retval      通道ch的times次转换结果平均值
  6. */
  7. uint32_tadc_get_result_average(uint32_t ch, uint8_t times)
  8. {
  9.     uint32_t temp_val = 0;
  10.     uint8_t t;
  11.     for (t = 0; t < times; t++) /* 获取times次数据 */
  12.     {
  13.         temp_val += adc_get_result(ch);
  14.         delay_ms(5);
  15.     }
  16.     return temp_val / times;    /* 返回平均值 */
  17. }
复制代码
该函数用于多次获取ADC值,取平均,用来提高准确度。

最后在main函数里面编写如下代码:
  1. int main(void)
  2. {
  3.     uint16_t adcx;
  4.     float temp;
  5.    HAL_Init();                              /* 初始化HAL库 */
  6.    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
  7.    delay_init(168);                       /* 延时初始化 */
  8.    usart_init(115200);                    /* 串口初始化为115200 */
  9.    led_init();                             /* 初始化LED */
  10.    lcd_init();                             /* 初始化LCD */
  11.    adc_init();                             /* 初始化ADC */
  12.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  13.    lcd_show_string(30, 70, 200, 16, 16, "ADCTEST", RED);
  14.    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  15.    lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH5_VAL:", BLUE);
  16.     /* 先在固定位置显示小数点 */
  17.    lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH5_VOL:0.000V", BLUE);
  18.     while (1)
  19.     {
  20.        /* 获取通道5的值,10次取平均 */
  21.        adcx =adc_get_result_average(ADC_ADCX_CHY, 10);
  22.        lcd_show_xnum(134, 110, adcx, 5, 16, 0, BLUE); /* 显示ADCC采样后的原始值 */
  23.        /* 获取计算后的带小数的实际电压值,比如3.1111 */
  24.        temp = (float)adcx * (3.3 / 4096);
  25.        adcx = temp;                 /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
  26.        /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
  27.        lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
  28.        temp -= adcx;/* 把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111*/
  29.        temp *= 1000;/* 小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数 */
  30.        /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
  31.        lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
  32.        LED0_TOGGLE();
  33.        delay_ms(100);
  34.     }
  35. }
复制代码
此部分代码,我们在TFTLCD模块上显示一些提示信息后,将每隔100ms读取一次ADC1通道5的转换值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。这里关于最后的ADC值的显示我们说明一下,首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。

31.2.4 下载验证
下载代码后,可以看到LCD显示如图31.2.4.1所示:     
image031.png
图31.2.4.1单通道ADC采集实验测试图

上图中,我们使用杜邦线将P11的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。

LED0闪烁,提示程序运行。大家可以试试把杜邦线接到其他地方,看看电压值是否准确?注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
31.3 单通道ADC采集(DMA读取)实验
本实验我们来学习使用规则单通道的连续转换模式,并且使用DMA读取ADC的数据。

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

l  ADC配置寄存器(ADC_CR2
ADCx配置寄存器描述如图31.3.1.1所示:     
image033.png
图31.3.1.1 ADC_CR2寄存器(部分)

ADC_CR2寄存器中我们主要跟前面设置不同的有两个位,分别如下:
DMA位,用于配置DMA使用。单通道ADC采集实验我们是默认设置为0,不使用DMA模式,规则转换数据仅存储在ADC_DR中,然后通过软件去ADC_DR数据寄存器读取。本实验我们要设置为1:使用DMA模式,这样启动一次DMA传输,DMA就会自动读取一次数据。
CONT位,用于设置转换模式。单通道ADC采集实验单次转换模式,本实验中我们要设置为连续转换模式,所以该位设置为1。

本实验ADC的寄存器就介绍ADC_CR2寄存器,其他的寄存器参考上一个实验的配置。下面介绍DMA一些比较重要的寄存器配置。

l  DMA数据流x外设地址寄存器(DMA_SxPAR
DMA数据流x外设地址寄存器描述如图31.3.1.2所示:     
image035.png
图31.3.1.2 DMA_SxPAR寄存器(部分)

该寄存器存放的是DMA读或者写数据的外设数据寄存器的基址。本实验,我们需要通过DMA读取ADC转换后存放在ADC规则数据寄存器 (ADC_DR) 的结果数据。所以我们需要给DMA_SxPAR寄存器写入ADC_DR寄存器的地址。这样配置后,DMA就会从ADC_DR寄存器的地址读取ADC的转换后的数据到某个内存空间。这个内存空间地址需要我们通过DMA_SxM0AR寄存器来设置,比如定义一个变量,把这个变量的地址值写入该寄存器。

注意:DMA_SxPAR寄存器受到写保护,只有DMA_SxCR寄存器中的EN为“0”时才可以写入,即先要禁止数据流传输才可以写入。

l  DMA数据流x存储器地址寄存器(DMA_SxM0AR
DMA数据流x存储器地址寄存器描述如图31.3.1.3所示:     
image037.png
图31.3.1.3 DMA_SxM0AR寄存器(部分)

该寄存器存放的是DMA读或者写数据的目标存放的地址。如果用到双缓冲区模式我们还需要用到DMA_SxM1AR寄存器,本实验我们是用不到的。

l  DMA数据流x数据项数寄存器(DMA_SxNDTR
DMA数据流x数据项数寄存器描述如图31.3.1.4所示:     
image039.png
图31.3.1.4 DMA_SxNDTR(部分)

DMA_SxPAR寄存器是传输的源地址,DMA_SxM0AR寄存器是传输的目的地址,DMA_SxNDTR寄存器则是要传输的数据项数目(0到65535)。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前DMA传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为16位,那么传输一次(一个项)就是2个字节。

其他的DMA寄存器我们就不一一介绍了,请大家看着寄存器源码对照手册理解。

31.3.2 硬件设计
1. 例程功能
使用ADC采集(DMA读取)通道5(PA5)上面的电压,在LCD模块上面显示ADC转换值以及换算成电压后的电压值。使用杜邦线将P11的ADC和RV1连接,使得PA5连接到电位器上,然后将ADC采集到的数据和转换后的电压值在TFTLCD屏中显示。用户可以通过调节电位器的旋钮改变电压值。LED0闪烁,提示程序运行。

2. 硬件资源
1)LED灯
   LED0 – PF9
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子 2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)ADC :通道5 – PA5
5)DMA(DMA2数据流4 外设请求通道0)

3. 原理图
ADC和DMA属于STM32F407内部资源,实际上我们只需要软件设置就可以正常工作,不过我们需要在外部连接其端口到被测电压上面。本实验,我们通过ADC的通道5(PA5)来采集外部电压值,并通过DMA来读取,开发板有一个电位器,可调节的电压范围是:0~3.3V。我们可以通过杜邦线将PA5与电位器连接,如下图所示:     
image041.jpg
图31.3.2.1 ADC输入通道5与电位器连接原理

image042.png
图31.3.2.2 连接实物图

使用杜邦线将P11的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。

有的朋友可能还想测试其他地方的电压值,我们还可以通过杜邦线,一端接到P11的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
31.3.3 程序设计
31.3.3.1 ADC & DMAHAL库驱动
单通道ADC采集实验已经介绍本实验要用到的ADC的HAL库API函数,这里我们要介绍启动使用中断的DMA传输函数和启动ADC(DMA传输)方式函数。

1. HAL_DMA_Start函数
启动使用DMA传输函数,其声明如下:
  1. HAL_Status  TypeDefHAL_DMA_Start(DMA_HandleTypeDef*hdma,
  2. uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
复制代码
l  函数描述:
用于启动使用DMA传输,DMA1和DMA2都是用的这个函数。

l  函数形参:
形参1是DMA_HandleTypeDef结构体类型指针变量。
形参2是DMA传输的源地址。
形参3是DMA传输的目的地址。
形参4是要传输的数据项数目。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

2. HAL_ADC_Start_DMA函数
启动ADC(DMA传输)方式函数,其声明如下:
  1. HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,uint32_t *pData, uint32_t Length);
复制代码
l  函数描述:
用于启动ADC(DMA传输)方式的函数。

l  函数形参:
形参1是ADC_HandleTypeDef结构体类型指针变量。
形参2是ADC采样数据传输的目的地址。
形参3是要传输的数据项数目。

l  函数返回值:
HAL_StatusTypeDef枚举类型的值。

l  注意事项:
HAL_ADC_Start_DMA和HAL_DMA_Start都是配置并启动DMA函数,区别是:HAL_ADC_Start_DMA比较局限性,只是用于启动ADC的数据传输。HAL_DMA_Start则适用性较广泛,任何能使用DMA传输的场景都可以用该函数启动。实际应用中看个人的需求选择用哪个函数。在例程中我们使用的是HAL_DMA_Start函数。如果我们需要使用DMA中断,我们还可以使用HAL_DMA_Start_IT函数,使能了DMA全部的中断。

单通道ADC采集(DMA读取)配置步骤
1)开启ADCx和通道输出的GPIO时钟,配置该IO口的复用功能输出
首先开启ADCx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到ADC1数据流5,对应IO是PA5,它们的时钟开启方法如下:
  1. __HAL_RCC_ADC1_CLK_ENABLE ();           /* 使能ADC1时钟 */
  2. __HAL_RCC_GPIOA_CLK_ENABLE();          /* 开启GPIOA时钟 */
复制代码
IO口复用功能是通过函数HAL_GPIO_Init来配置的。

2)初始化ADCx,配置其工作参数
通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。
注意:该函数会调用:HAL_ADC_MspInit回调函数来完成对ADC底层以及其输入通道IO的初始化,包括:ADC及GPIO时钟使能、GPIO模式设置等。

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函数开启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,在该函数内部,会对DMA传输状态进行分析,然后调用相应的中断处理回调函数。

31.3.3.2 程序流程图
QQ截图20230801144500.png
图31.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图

31.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。本实验代码在单通道ADC采集实验代码上进行追加。

adc.h文件针对本实验用到DMA,我们定义了以下一些宏定义:
  1. /* ADC单通道/多通道 DMA采集 DMA数据流相关 定义
  2. * 注意: 这里我们的通道还是使用上面的定义.
  3. */
  4. #define ADC_ADCX_DMASx                         DMA2_Stream4
  5. #define ADC_ADCX_DMASx_Chanel                DMA_CHANNEL_0    /* ADC1_DMA请求源 */
  6. #define ADC_ADCX_DMASx_IRQn                   DMA2_Stream4_IRQn
  7. #define ADC_ADCX_DMASx_IRQHandler            DMA2_Stream4_IRQHandler
  8. /* 判断 DMA2_Stream4 传输完成标志, 这是一个假函数形式,
  9. * 不能当函数使用, 只能用在if等语句里面
  10. */
  11. #define ADC_ADCX_DMASx_IS_TC()               (DMA2->HISR & (1 << 5) )
  12. /* 清除 DMA2_Stream4 传输完成标志 */
  13. #define ADC_ADCX_DMASx_CLR_TC()              do{DMA2->HIFCR |= 1 << 5; }while(0)
  14. /*****************************************************************************/
复制代码
下面直接开始介绍adc.c的程序,首先是ADC DMA读取初始化函数。
  1. /**
  2. *@brief       ADC DMA读取 初始化函数
  3. *  @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
  4. *@param       par         : 外设地址
  5. *@param       mar         : 存储器地址
  6. *@retval      无
  7. */
  8. void adc_dma_init(uint32_t mar)
  9. {
  10.     if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)/* 大于DMA1_Stream7,则为DMA2 */
  11.     {
  12.        __HAL_RCC_DMA2_CLK_ENABLE();               /* DMA2时钟使能 */
  13.     }
  14.     else
  15.     {
  16.        __HAL_RCC_DMA1_CLK_ENABLE();                /* DMA1时钟使能 */
  17.     }
  18.     /* DMA配置 */
  19.    g_dma_adc_handle.Instance = ADC_ADCX_DMASx;         /* 设置DMA数据流 */
  20.    g_dma_adc_handle.Init.Channel = DMA_CHANNEL_0;      /* 设置DMA通道 */
  21. /* DIR = 1 ,  外设到存储器模式 */
  22. g_dma_adc_handle.Init.Direction =DMA_PERIPH_TO_MEMORY;
  23.    g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;        /* 外设非增量模式 */
  24.    g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;              /* 存储器增量模式 */
  25.     /*外设数据长度:16位 */
  26.    g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
  27.     /* 存储器数据长度:16位 */
  28.    g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
  29.    g_dma_adc_handle.Init.Mode = DMA_NORMAL;                     /* 外设流控模式 */
  30.    g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;       /* 中等优先级 */
  31.    HAL_DMA_Init(&g_dma_adc_handle);                                /* 初始化DMA */
  32.     /* 配置DMA传输参数 */
  33.    HAL_DMA_Start(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
  34.    g_adc_handle.DMA_Handle = &g_dma_adc_handle;    /* 设置ADC对应的DMA */
  35.    adc_init();    /* 初始化ADC */
  36.     /**
  37.     *   需要在配置的时候开,但这里为了保证不变更之前的代码,
  38.     *   另加一行设置g_adc_handle.Init.ContinuousConvMode =ENABLE;
  39.     *   配置ADC连续转换, DMA传输ADC数据
  40.     */
  41.    SET_BIT(g_adc_handle.Instance->CR2, ADC_CR2_CONT);/* CONT = 1, 连续转换模式 */
  42.     /* 配置对应ADC通道 */
  43.    adc_channel_set(&g_adc_handle , ADC_ADCX_CHY, 1,ADC_SAMPLETIME_480CYCLES);
  44.     /* 设置DMA中断优先级为3,子优先级为3 */
  45.    HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
  46.    HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);            /* 使能DMA中断 */
  47.    HAL_ADC_Start_DMA(&g_adc_handle, &mar, sizeof(uint16_t)); /*开始DMA数据传输*/
  48.     /*TCIE =1 , 使能传输完成中断 */
  49.    __HAL_DMA_ENABLE_IT(&g_dma_adc_handle, DMA_IT_TC);
  50. }
复制代码
该函数主要分为两部分,ADC和DMA的配置。首先使能DMA的时钟,然后进行DMA相关参数的配置,这里我们使用的是DMA2数据流4,这个可以通过DMA章节查询到。配置完成后用HAL_DMA_Init函数对DMA进行初始化,调用HAL_DMA_Start函数配置DMA传输参数。

对DMA配置完成之后,对ADC进行配置,我们还是沿用上一个实验中的adc_init函数对ADC进行初始化,但是ADC_HandleTypeDef结构体的配置与单通道ADC采集实验有所不同,我们需要使能连续转换模式和DMA单次传输ADC数据。所以在这里我们另外加入一些与ADC的DMA相关的配置,例如:
  1. g_adc_handle.DMA_Handle =&g_dma_adc_handle;
  2. SET_BIT(g_adc_handle.Instance->CR2,ADC_CR2_CONT);
复制代码
第一句很容易理解把前面DMA结构体指针类型数据绑定在ADC结构体指针类型中,ADC可以使用DMA。第二句,ADC中使用DMA,需要连续转换,所以我们需要使用以下的代码进行实现g_adc_handle.Init.ContinuousConvMode= ENABLE; 这里我们直接使用位操作,把控制连续转换的位置1,实现连续转换。

最后是配置ADC通道、使能ADC和启动开启中断的DMA传输。

下面介绍的使能一次ADC DMA传输函数,其定义如下:
  1. /**
  2. * @brief       使能一次ADC DMA传输
  3. * @param       ndtr: DMA传输的次数
  4. * @retval      无
  5. */
  6. void adc_dma_enable(uint16_t ndtr)
  7. {
  8.    __HAL_ADC_DISABLE(&g_adc_handle);                /* 先关闭ADC */
  9.    
  10.    __HAL_DMA_DISABLE(&g_dma_adc_handle);           /* 关闭DMA传输 */
  11.    g_dma_adc_handle.Instance->NDTR= ndtr;          /* 重设DMA传输数据量 */
  12.    __HAL_DMA_ENABLE(&g_dma_adc_handle);            /* 开启DMA传输 */
  13.    
  14.    __HAL_ADC_ENABLE(&g_adc_handle);                  /* 重新启动ADC */
  15.     ADC_ADCX->CR2 |= 1 << 30;                         /* 启动规则转换通道 */
  16. }
复制代码
该函数我们使用寄存器来操作,因为用HAL库操作会对adc_dma_init配置好的某些参数修改。HAL_DMA_Start函数已经配置好了DMA传输的源地址和目标地址,本函数只需要调用g_dma_adc_handle.Instance->NDTR =ndtr; 语句给DMA_SxNDTR寄存器写入要传输的数据量,然后启动DMA就可以传输了。

下面介绍的是ADC DMA采集中断服务函数,函数定义如下:
  1. /**
  2. * @brief       ADC DMA采集中断服务函数
  3. * @param       无
  4. * @retval      无
  5. */
  6. void ADC_ADCX_DMASx_IRQHandler(void)
  7. {
  8.     if (ADC_ADCX_DMACx_IS_TC())
  9.     {
  10.        g_adc_dma_sta = 1;           /* 标记DMA传输完成 */
  11.        ADC_ADCX_DMACx_CLR_TC();    /* 清除DMA2数据流4 传输完成中断 */
  12.     }
  13. }
复制代码
在ADC DMA采集中断服务函数里,我们标记DMA传输完成,以及清除DMA2数据流4传输完成中断标志位。

最后在main.c里面编写如下代码:
  1. #define ADC_DMA_BUF_SIZE        50            /* ADC DMA采集 BUF大小 */
  2. uint16_t g_adc_dma_buf[ADC_DMA_BUF_SIZE];  /* ADC DMA BUF */
  3. extern uint8_t g_adc_dma_sta;                /* DMA传输状态标志,0,未完成;1,已完成 */
  4. int main(void)
  5. {
  6.     uint16_t i;
  7.     uint16_t adcx;
  8.     uint32_t sum;
  9.     float temp;
  10.     HAL_Init();                                 /* 初始化HAL库 */
  11.    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟,168Mhz */
  12.     delay_init(168);                            /* 延时初始化 */
  13.     usart_init(115200);                        /* 串口初始化为115200 */
  14.     led_init();                                 /* 初始化LED */
  15.     lcd_init();                                 /* 初始化LCD */
  16.     adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
  17.     lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
  18.     lcd_show_string(30,  70, 200, 16, 16, "ADC DMATEST", RED);
  19.     lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  20. lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH5_VAL:", BLUE);
  21. /* 先在固定位置显示小数点 */
  22.     lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH5_VOL:0.000V", BLUE);
  23.     adc_dma_enable(ADC_DMA_BUF_SIZE);       /* 启动ADC DMA采集 */
  24.     while (1)
  25.     {
  26.         if (g_adc_dma_sta == 1)
  27.         {
  28.           /* 计算DMA 采集到的ADC数据的平均值 */
  29.           sum = 0;
  30.           for (i = 0; i < ADC_DMA_BUF_SIZE; i++) /* 累加 */
  31.           {
  32.              sum += g_adc_dma_buf;
  33.           }
  34.           adcx = sum / ADC_DMA_BUF_SIZE;         /* 取平均值 */
  35.           /* 显示结果 */
  36.           lcd_show_xnum(134, 110, adcx, 4, 16, 0,BLUE); /*显示ADCC采样后的原始值*/
  37.           temp=(float)adcx*(3.3/4096); /*获取计算后的带小数的实际电压值,比如3.1111*/
  38.           adcx = temp;                /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
  39. /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
  40.          lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);  
  41.           temp -= adcx;/*把已经显示的整数部分去掉,留下小数部分,比如3.1111-3=0.1111*/
  42.           temp*=1000;/*小数部分乘以1000,例如:0.1111就转换为111.1,相当于保留三位小数*/
  43. /* 显示小数部分(前面转换为了整形显示),这里显示的就是111. */
  44.          lcd_show_xnum(150, 130, temp, 3, 16, 0X80, BLUE);
  45.          g_adc_dma_sta = 0;                       /* 清除DMA采集完成状态标志 */
  46.          adc_dma_enable(ADC_DMA_BUF_SIZE);      /* 启动下一次ADC DMA采集 */
  47.         }
  48.         LED0_TOGGLE();
  49.         delay_ms(100);
  50.     }
  51. }
复制代码
此部分代码,和单通道ADC采集实验十分相似,只是这里使能了DMA传输数据,DMA传输的数据存放在g_adc_dma_buf数组里,这里我们对数组的数据取平均值,减少误差。在LCD屏显示结果的处理和单通道ADC采集实验一样。首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。

31.3.4 下载验证
下载代码后,可以看到LCD显示如图31.3.4.1所示:     
image046.png
图31.3.4.1 单通道ADC采集(DMA读取)实验测试图

LED0闪烁,提示程序运行。这里的实验效果和单通道ADC采集实验是一样的,我们使用杜邦线将P11的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。

大家可以试试把杜邦线接到其他地方,看看电压值是否准确?注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-24 08:05

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表