本帖最后由 正点原子运营 于 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
本章,我们将介绍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的框图: 图中,我们按照ADC的配置流程标记了七处位置,分别如下:
①输入电压 在前面ADC的主要特性也对输入电压有所提及,ADC输入范围VREF–≤VIN≤VREF+,最终还是由VREF–、VREF+、VDDA和VSSA决定的。下面看一下这几个参数的关系,如图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所示: ③转换顺序 当任意ADCx多个通道以任意顺序进行一系列转换就诞生了成组转换,这里就有两种成组转换类型:规则组和注入组。规则组就是图上的规则通道,注入组也就是图上的注入通道。为了避免大家对输入通道加上规则通道和注入通道理解有所模糊,后面规则通道以规则组来代称,注入通道以注入组来代称。
规则组允许最多16个输入通道进行转换,而注入组允许最多4个输入通道进行转换。这里讲解一下规则组和注入组。
规则组(规则通道) 规则组,按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最多也是规则组。
注入组(注入通道) 注入组,按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。 便于理解,下面看一下规则组和注入组的对比图,如图31.1.3所示: 在了解了规则组和注入组后,现在了解一下它们两者的转换顺序。
规则序列 规则组是允许16个通道进行转换的,那么就需要安排通道转换的次序即规则序列。规则序列寄存器有3个,分别为SQR1、SQR2和SQR3。SQR3控制规则序列中的第1个到第6个转换的通道;SQR2控制规则序列中第7个到第12个转换的通道;SQR1控制规则序列中第13个到第16个转换的通道。规则序列寄存器SQRx详表如表31.1.2所示: 从上表可以知道,当我们想把ADC的输入通道1安排到第1个转换,那么只需要在SQR3寄存器中的SQ1[4:0]位写入该ADC输入通道即写1处理即可。SQR1的SQL[3:0]决定了具体使用多少个通道。
注入序列 注入序列,跟规则序列差不多,都是有顺序的安排。由于注入组最大允许4个通道输入,所以这里就使用了一个寄存器JSQR。注入序列寄存器JSQR详表如表31.1.3所示: ④触发源 在配置好输入通道以及转换顺序后,就可以进行触发转换了。ADC的触发转换有两种方法:分别是通过软件或外部事件(也就是硬件)触发转换。
我们先来看看通过写软件触发转换的方法。方法是:通过写ADC_CR2寄存器的ADON这个位来控制,写1就开始转换,写0就停止转换,这个控制ADC转换的方式非常简单。
另一种就是通过外部事件触发转换的方法,有定时器和输入引脚触发等等。这里区分规则组和注入组。方法是:通过ADC_CR2寄存器的EXTSET[2:0]选择规则组的触发源,JEXTSET[2:0]选择注入组的触发源。通过ADC_CR2的EXTTRIG和JEXTTRIG这两位去激活触发源。ADC3的触发源和ADC1/2不同,这里需要注意,那么在框图里已经标记出来了。
⑤转换时间 STM32F407的ADC总转换时间的计算公式如下: 采样时间可通过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控制寄存器1(ADC_CR1) ADC控制寄存器1描述如图31.2.1.1所示: 该寄存器本章用到SCAN位,用于设置扫描模式,由软件设置和清除,如果设置为1,则使用扫描模式,如果为0,则关闭扫描模式。在本章实验中使用的是非扫描模式。在扫描模式下,由ADC_SQRx或ADC_JSQRx寄存器选中的通道被转换。如果设置了EOCIE或JEOCIE,只会在最后一个通道转换完成后才会产生EOC或JEOC中断。
l ADC控制寄存器2(ADC_CR2) ADC控制寄存器2描述如图31.2.1.2所示: 该寄存器我们针对性的介绍一些位:ADON位用于开关AD转换器。而CONT位用于设置是否进行连续转换,我们使用单次转换,所以CONT位必须为0。CAL和RSTCAL用于AD校准。ALIGN用于设置数据对齐,我们使用右对齐,所以该位设置为0。EXTSEL[3:0]用于选择启动规则转换组转换的外部事件,我们这里使用的是软件触发(SWSTART),所以这里设置这3位为111。SWSTART位用于开始规则通道的转换,我们每次转换(单次转换模式下)都需要向该位写1。
l ADC采样事件寄存器1(ADC_SMPR1) ADC采样事件寄存器1描述如图31.2.1.3所示: 图31.2.1.3 ADC_SMPR1寄存器(部分) l ADC采样事件寄存器2(ADC_SMPR2) ADC采样事件寄存器2描述如图31.2.1.4所示: 图31.2.1.4 ADC_SMPR2寄存器(部分) 这里结合两个ADC采样事件寄存器进行讲解,这两个寄存器用于设置通道0~18的采样事件,每个通道占用3个位。
对于每个要转换的通道,采样事件建议尽量长一点,以获得较高的准确度,但是这样会降低ADC的转换速率。
l ADC规则序列寄存器1 ADC规则序列寄存器共有3个,这几个寄存器的功能都差不多,这里我们仅介绍一下ADC规则序列寄存器1(ADC_SQR1),描述如图31.2.1.5所示: 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所示: 在规则序列中AD转换结果都将被存在这个寄存器里面,而注入通道的转换结果被保存在ADC_JDRx里面。该寄存器的数据可以通过ADC_CR2的ALIGN位设置左对齐还是右对齐。在读取数据的时候要注意。
l ADC状态寄存器(ADC_SR) ADC状态寄存器描述如图31.2.1.7所示: 该寄存器保存了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与电位器连接,如下图所示: 图31.2.2.2 ADC输入通道5与电位器连接原理 使用一根杜邦线的两端将P11的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其他地方的电压值,我们还可以通过杜邦线,一端接到P11的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。 31.2.3 程序设计31.2.3.1 ADC的HAL库驱动 ADC在HAL库中的驱动代码在stm32f4xx_hal_adc.c和stm32f4xx_hal_adc_ex.c文件(及其头文件)中。
1. HAL_ADC_Init函数 ADC的初始化函数,其声明如下: - HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc);
复制代码
l 函数描述: 用于初始化ADC。
l 函数形参: 形参1是ADC_HandleTypeDef结构体类型指针变量,其定义如下: - typedef struct
- {
- ADC_TypeDef *Instance; /* ADC寄存器基地址 */
- ADC_InitTypeDef Init; /* ADC参数初始化结构体变量 */
- __IO uint32_t NbrOfCurrentConversionRank;/* 当前转换等级的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 ClockPrescaler; /* 设置预分频系数,即PRESC[3:0]位 */
- uint32_t Resolution; /* 配置ADC的分辨率 */
- uint32_t ScanConvMode; /* 扫描模式 */
- uint32_t EOCSelection; /* 转换完成标志位 */
- FunctionalState ContinuousConvMode; /* 开启连续转换模式否则就是单次转换模式 */
- uint32_t NbrOfConversion; /* 设置转换通道数目 */
- FunctionalState DiscontinuousConvMode; /* 单次转换模式选择 */
- uint32_tNbrOfDiscConversion; /* 单次转换通道的数目 */
- uint32_t ExternalTrigConv; /* ADC外部触发源选择 */
- uint32_tExternalTrigConvEdge; /* ADC外部触发极性*/
- FunctionalState DMAContinuousRequests; /* DMA转换请求模式*/
- }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的自校准函数,其声明如下: - 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通道配置函数,其声明如下: - HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef*hadc,ADC_ChannelConfTypeDef *sConfig);
复制代码l 函数描述: 调用HAL_ADC_Init函数配置了相关的功能后,就可以调用此函数进行ADC自校准功能。
l 函数形参: 形参1是ADC_HandleTypeDef结构体类型指针变量。 形参2是ADC_ChannelConfTypeDef结构体类型指针变量,用于配置ADC采样时间,使用的通道号,单端或者差分方式的配置等。该结构体定义如下: - typedef struct {
- uint32_t Channel; /* ADC转换通道*/
- uint32_t Rank; /* ADC转换顺序 */
- uint32_t SamplingTime; /* ADC采样周期 */
- uint32_t Offset; /* ADC偏移量 */
- }ADC_ChannelConfTypeDef;
复制代码1) Channel:ADC转换通道,范围:0~19。 2) Rank:在常规转换中的常规组的转换顺序,可以选择1~16。 3) SamplingTime:ADC的采样周期,最大480个ADC时钟周期,要求采样周期尽量大,以减少误差。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
4. HAL_ADC_Start函数 ADC转换启动函数,其声明如下: - HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
复制代码
l 函数描述: 当配置好ADC的基础的功能后,就调用此函数启动ADC。
l 函数形参: ADC_HandleTypeDef结构体类型指针变量。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
5.HAL_ADC_PollForConversion函数 等待ADC规则组转换完成函数,其声明如下: - HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef*hadc,uint32_t Timeout);
复制代码l 函数描述: 一般先调用HAL_ADC_Start函数启动转换,再调用该函数等待转换完成,然后再调用HAL_ADC_GetValue函数来获取当前的转换值。
l 函数形参: 形参1是ADC_HandleTypeDef结构体类型指针变量。 形参2是等待转换的等待时间,单位:毫秒ms。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
6. HAL_ADC_GetValue函数 获取常规组ADC转换值函数,其声明如下: - 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,它们的时钟开启方法如下: - __HAL_RCC_ADC1_CLK_ENABLE(); /* 使能ADC1时钟 */
- __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
复制代码IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化ADCx, 配置其工作参数 通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。 注意:该函数会调用:HAL_ADC_MspInit回调函数来完成对ADC底层以及其输入通道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 程序流程图 图31.2.3.2.1 单通道ADC采集实验程序流程图 31.2.3.3 程序解析 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。该章节有四个实验,每一个实验的代码都是在上一个实验后面追加。
adc.h文件针对ADC及通道引脚定义了一些宏定义,具体如下: - /* ADC及引脚 定义 */
- #define ADC_ADCX_CHY_GPIO_PORT GPIOA
- #define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_5
- #define ADC_ADCX_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE();\
- }while(0) /* PA口时钟使能 */
- #define ADC_ADCX ADC1
- #define ADC_ADCX_CHY ADC_CHANNEL_5 /* 通道Y, 0 <= Y <=16 */
- /* ADC1时钟使能 */
- #define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC1_CLK_ENABLE();}while(0)
复制代码ADC的通道与引脚的对应关系在STM32中文数据手册可以查到,我们这里使用ADC1的通道5,在数据手册中的表格为: 表31.2.3.3.1 ADC1通道5对应引脚查看表 下面直接开始介绍adc.c的程序,首先是ADC初始化函数。 - /**
- *@brief ADC初始化函数
- * @note 本函数支持ADC1/ADC2任意通道, 但是不支持ADC3
- * 我们使用12位精度, ADC采样时钟=21M, 转换时间为: 采样周期 + 12个ADC周期
- * 设置最大采样周期: 480, 则转换时间 = 492 个ADC周期 = 23.42us
- *@param 无
- *@retval 无
- */
- void adc_init(void)
- {
- g_adc_handle.Instance = ADC_ADCX;
- /*4分频,ADCCLK =PCLK2/4 = 84/4 = 21Mhz */
- g_adc_handle.Init.ClockPrescaler=ADC_CLOCKPRESCALER_PCLK_DIV4;
- g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
- g_adc_handle.Init.DataAlign =ADC_DATAALIGN_RIGHT; /* 右对齐 */
- g_adc_handle.Init.ScanConvMode = DISABLE; /* 非扫描模式 */
- g_adc_handle.Init.ContinuousConvMode= DISABLE; /* 关闭连续转换 */
- /* 1个转换在规则序列中 也就是只转换规则序列1 */
- g_adc_handle.Init.NbrOfConversion= 1;
- g_adc_handle.Init.DiscontinuousConvMode= DISABLE; /* 禁止不连续采样模式 */
- g_adc_handle.Init.NbrOfDiscConversion= 0; /* 不连续采样通道数为0 */
- g_adc_handle.Init.ExternalTrigConv=ADC_SOFTWARE_START; /* 软件触发 */
- g_adc_handle.Init.ExternalTrigConvEdge=ADC_EXTERNALTRIGCONVEDGE_NONE;
- /* 使用软件触发 */
- g_adc_handle.Init.DMAContinuousRequests= DISABLE; /* 关闭DMA请求 */
- HAL_ADC_Init(&g_adc_handle); /* 初始化 */
- }
复制代码该函数调用HAL_ADC_Init函数配置了ADC的基础功能参数,HAL_ADC_Init函数的MSP回调函数是HAL_ADC_MspInit,用来使能时钟和初始化IO口。HAL_ADC_MspInit函数定义如下: - /**
- * @brief ADC底层驱动,引脚配置,时钟使能
- 此函数会被HAL_ADC_Init()调用
- * @param hadc:ADC句柄
- * @retval 无
- */
- void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
- {
- if(hadc->Instance == ADC_ADCX)
- {
- GPIO_InitTypeDef gpio_init_struct;
- ADC_ADCX_CHY_CLK_ENABLE(); /* 使能ADCx时钟 */
- ADC_ADCX_CHY_GPIO_CLK_ENABLE(); /* 开启GPIO时钟 */
- /* AD采集引脚模式设置,模拟输入 */
- gpio_init_struct.Pin =ADC_ADCX_CHY_GPIO_PIN;
- gpio_init_struct.Mode = GPIO_MODE_ANALOG;
- gpio_init_struct.Pull = GPIO_PULLUP;
- HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);
- }
- }
复制代码下面是获得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, 1, ADC_SAMPLETIME_480CYCLES);
- /* 设置通道,序列和采样时间 */
- HAL_ADC_Start(&g_adc_handle); /* 开启ADC */
- HAL_ADC_PollForConversion(&g_adc_handle, 10); /* 轮询转换 */
- /*返回最近一次ADC1规则组的转换结果 */
- return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);
- }
复制代码该函数先是调用我们自己定义的adc_channel_set函数设置ADC通道的转换序列和采样周期等功能,再是调用HAL_ADC_Start启动转换、HAL_ADC_PollForConversion函数等待转换完成、HAL_ADC_GetValue函数获取转换后的当前结果。
adc_channel_set函数的定义如下: - /**
- *@brief 设置ADC通道采样时间
- *@param adcx : adc句柄指针,ADC_HandleTypeDef
- *@param ch : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17
- *@param stime: 采样时间 0~7, 对应关系为:
- * @arg ADC_SAMPLETIME_3CYCLES, 3个ADC时钟周期
- ADC_SAMPLETIME_15CYCLES, 15个ADC时钟周期
- * @arg ADC_SAMPLETIME_28CYCLES, 28个ADC时钟周期
- ADC_SAMPLETIME_56CYCLES, 56个ADC时钟周期
- * @arg ADC_SAMPLETIME_84CYCLES, 84个ADC时钟周期
- ADC_SAMPLETIME_112CYCLES,112个ADC时钟周期
- * @arg ADC_SAMPLETIME_144CYCLES,144个ADC时钟周期
- ADC_SAMPLETIME_480CYCLES,480个ADC时钟周期
- *@param rank: 多通道采集时需要设置的采集编号,
- 假设你定义channel1的rank=1,channel2的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 无
- */
- voidadc_channel_set(ADC_HandleTypeDef*adc_handle, uint32_t ch, uint32_t rank,
- uint32_t stime)
- {
- /* 配置对应ADC通道 */
- ADC_ChannelConfTypeDef adc_channel;
- adc_channel.Channel = ch; /* 设置ADCX对通道ch */
- adc_channel.Rank = rank; /* 设置采样序列 */
- adc_channel.SamplingTime = stime; /* 设置采样时间 */
- HAL_ADC_ConfigChannel( adc_handle, &adc_channel);
- }
复制代码该函数主要是通过HAL_ADC_ConfigChannel函数设置通道的转换序列和采样周期等功能。
下面介绍的是获取ADC某通道的转换多次后的平均值函数,函数定义如下: - /**
- * @brief 获取通道ch的转换值,取times次,然后平均
- * @param ch : 通道号, 0~17
- * @param times : 获取次数
- * @retval 通道ch的times次转换结果平均值
- */
- uint32_tadc_get_result_average(uint32_t ch, uint8_t times)
- {
- uint32_t temp_val = 0;
- uint8_t t;
- for (t = 0; t < times; t++) /* 获取times次数据 */
- {
- temp_val += adc_get_result(ch);
- delay_ms(5);
- }
- return temp_val / times; /* 返回平均值 */
- }
复制代码该函数用于多次获取ADC值,取平均,用来提高准确度。
最后在main函数里面编写如下代码: - int main(void)
- {
- uint16_t adcx;
- float temp;
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- led_init(); /* 初始化LED */
- lcd_init(); /* 初始化LCD */
- adc_init(); /* 初始化ADC */
- lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
- lcd_show_string(30, 70, 200, 16, 16, "ADCTEST", RED);
- lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
- lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH5_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH5_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采样后的原始值 */
- /* 获取计算后的带小数的实际电压值,比如3.1111 */
- temp = (float)adcx * (3.3 / 4096);
- adcx = temp; /* 赋值整数部分给adcx变量,因为adcx为u16整形 */
- /* 显示电压值的整数部分,3.1111的话,这里就是显示3 */
- lcd_show_xnum(134, 130, adcx, 1, 16, 0, BLUE);
- 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读取一次ADC1通道5的转换值,并显示读到的ADC值(数字量),以及其转换成模拟量后的电压值。同时控制LED0闪烁,以提示程序正在运行。这里关于最后的ADC值的显示我们说明一下,首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。
31.2.4 下载验证下载代码后,可以看到LCD显示如图31.2.4.1所示: 上图中,我们使用杜邦线将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所示: 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所示: 图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所示: 图31.3.1.3 DMA_SxM0AR寄存器(部分) 该寄存器存放的是DMA读或者写数据的目标存放的地址。如果用到双缓冲区模式我们还需要用到DMA_SxM1AR寄存器,本实验我们是用不到的。
l DMA数据流x数据项数寄存器(DMA_SxNDTR) DMA数据流x数据项数寄存器描述如图31.3.1.4所示: 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与电位器连接,如下图所示: 图31.3.2.1 ADC输入通道5与电位器连接原理 使用杜邦线将P11的ADC和RV1连接好后,并下载程序后,就可以用螺丝刀调节电位器变换多种电压值进行测试。
有的朋友可能还想测试其他地方的电压值,我们还可以通过杜邦线,一端接到P11的ADC排针上,另外一端就接你要测试的电压点。一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。 31.3.3 程序设计31.3.3.1 ADC & DMA的HAL库驱动 单通道ADC采集实验已经介绍本实验要用到的ADC的HAL库API函数,这里我们要介绍启动使用中断的DMA传输函数和启动ADC(DMA传输)方式函数。
1. HAL_DMA_Start函数 启动使用DMA传输函数,其声明如下: - HAL_Status TypeDefHAL_DMA_Start(DMA_HandleTypeDef*hdma,
- uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
复制代码l 函数描述: 用于启动使用DMA传输,DMA1和DMA2都是用的这个函数。
l 函数形参: 形参1是DMA_HandleTypeDef结构体类型指针变量。 形参2是DMA传输的源地址。 形参3是DMA传输的目的地址。 形参4是要传输的数据项数目。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
2. HAL_ADC_Start_DMA函数 启动ADC(DMA传输)方式函数,其声明如下: - HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc,uint32_t *pData, uint32_t Length);
复制代码l 函数描述: 用于启动ADC(DMA传输)方式的函数。
l 函数形参: 形参1是ADC_HandleTypeDef结构体类型指针变量。 形参2是ADC采样数据传输的目的地址。 形参3是要传输的数据项数目。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
l 注意事项: HAL_ADC_Start_DMA和HAL_DMA_Start都是配置并启动DMA函数,区别是:HAL_ADC_Start_DMA比较局限性,只是用于启动ADC的数据传输。HAL_DMA_Start则适用性较广泛,任何能使用DMA传输的场景都可以用该函数启动。实际应用中看个人的需求选择用哪个函数。在例程中我们使用的是HAL_DMA_Start函数。如果我们需要使用DMA中断,我们还可以使用HAL_DMA_Start_IT函数,使能了DMA全部的中断。
单通道ADC采集(DMA读取)配置步骤1)开启ADCx和通道输出的GPIO时钟,配置该IO口的复用功能输出 首先开启ADCx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到ADC1数据流5,对应IO是PA5,它们的时钟开启方法如下: - __HAL_RCC_ADC1_CLK_ENABLE (); /* 使能ADC1时钟 */
- __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启GPIOA时钟 */
复制代码IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化ADCx,配置其工作参数 通过HAL_ADC_Init函数来设置ADCx时钟分频系数、分辨率、模式、扫描方式、对齐方式等信息。 注意:该函数会调用:HAL_ADC_MspInit回调函数来完成对ADC底层以及其输入通道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 程序流程图 图31.3.3.2.1 单通道ADC采集(DMA读取)实验程序流程图 31.3.3.3 程序解析 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ADC驱动源码包括两个文件:adc.c和adc.h。本实验代码在单通道ADC采集实验代码上进行追加。
adc.h文件针对本实验用到DMA,我们定义了以下一些宏定义: - /* ADC单通道/多通道 DMA采集 DMA数据流相关 定义
- * 注意: 这里我们的通道还是使用上面的定义.
- */
- #define ADC_ADCX_DMASx DMA2_Stream4
- #define ADC_ADCX_DMASx_Chanel DMA_CHANNEL_0 /* ADC1_DMA请求源 */
- #define ADC_ADCX_DMASx_IRQn DMA2_Stream4_IRQn
- #define ADC_ADCX_DMASx_IRQHandler DMA2_Stream4_IRQHandler
- /* 判断 DMA2_Stream4 传输完成标志, 这是一个假函数形式,
- * 不能当函数使用, 只能用在if等语句里面
- */
- #define ADC_ADCX_DMASx_IS_TC() (DMA2->HISR & (1 << 5) )
- /* 清除 DMA2_Stream4 传输完成标志 */
- #define ADC_ADCX_DMASx_CLR_TC() do{DMA2->HIFCR |= 1 << 5; }while(0)
- /*****************************************************************************/
复制代码下面直接开始介绍adc.c的程序,首先是ADC DMA读取初始化函数。 - /**
- *@brief ADC DMA读取 初始化函数
- * @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
- *@param par : 外设地址
- *@param mar : 存储器地址
- *@retval 无
- */
- void adc_dma_init(uint32_t mar)
- {
- if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)/* 大于DMA1_Stream7,则为DMA2 */
- {
- __HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
- }
- else
- {
- __HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
- }
- /* DMA配置 */
- g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 设置DMA数据流 */
- g_dma_adc_handle.Init.Channel = DMA_CHANNEL_0; /* 设置DMA通道 */
- /* DIR = 1 , 外设到存储器模式 */
- g_dma_adc_handle.Init.Direction =DMA_PERIPH_TO_MEMORY;
- g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
- g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
- /*外设数据长度:16位 */
- g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
- /* 存储器数据长度:16位 */
- g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
- g_dma_adc_handle.Init.Mode = DMA_NORMAL; /* 外设流控模式 */
- g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
- HAL_DMA_Init(&g_dma_adc_handle); /* 初始化DMA */
- /* 配置DMA传输参数 */
- HAL_DMA_Start(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);
- g_adc_handle.DMA_Handle = &g_dma_adc_handle; /* 设置ADC对应的DMA */
- adc_init(); /* 初始化ADC */
- /**
- * 需要在配置的时候开,但这里为了保证不变更之前的代码,
- * 另加一行设置g_adc_handle.Init.ContinuousConvMode =ENABLE;
- * 配置ADC连续转换, DMA传输ADC数据
- */
- SET_BIT(g_adc_handle.Instance->CR2, ADC_CR2_CONT);/* CONT = 1, 连续转换模式 */
- /* 配置对应ADC通道 */
- adc_channel_set(&g_adc_handle , ADC_ADCX_CHY, 1,ADC_SAMPLETIME_480CYCLES);
- /* 设置DMA中断优先级为3,子优先级为3 */
- HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
- HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn); /* 使能DMA中断 */
- HAL_ADC_Start_DMA(&g_adc_handle, &mar, sizeof(uint16_t)); /*开始DMA数据传输*/
- /*TCIE =1 , 使能传输完成中断 */
- __HAL_DMA_ENABLE_IT(&g_dma_adc_handle, DMA_IT_TC);
- }
复制代码该函数主要分为两部分,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相关的配置,例如: - g_adc_handle.DMA_Handle =&g_dma_adc_handle;
- 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传输函数,其定义如下: - /**
- * @brief 使能一次ADC DMA传输
- * @param ndtr: DMA传输的次数
- * @retval 无
- */
- void adc_dma_enable(uint16_t ndtr)
- {
- __HAL_ADC_DISABLE(&g_adc_handle); /* 先关闭ADC */
-
- __HAL_DMA_DISABLE(&g_dma_adc_handle); /* 关闭DMA传输 */
- g_dma_adc_handle.Instance->NDTR= ndtr; /* 重设DMA传输数据量 */
- __HAL_DMA_ENABLE(&g_dma_adc_handle); /* 开启DMA传输 */
-
- __HAL_ADC_ENABLE(&g_adc_handle); /* 重新启动ADC */
- ADC_ADCX->CR2 |= 1 << 30; /* 启动规则转换通道 */
- }
复制代码该函数我们使用寄存器来操作,因为用HAL库操作会对adc_dma_init配置好的某些参数修改。HAL_DMA_Start函数已经配置好了DMA传输的源地址和目标地址,本函数只需要调用g_dma_adc_handle.Instance->NDTR =ndtr; 语句给DMA_SxNDTR寄存器写入要传输的数据量,然后启动DMA就可以传输了。
下面介绍的是ADC DMA采集中断服务函数,函数定义如下: - /**
- * @brief ADC DMA采集中断服务函数
- * @param 无
- * @retval 无
- */
- void ADC_ADCX_DMASx_IRQHandler(void)
- {
- if (ADC_ADCX_DMACx_IS_TC())
- {
- g_adc_dma_sta = 1; /* 标记DMA传输完成 */
- ADC_ADCX_DMACx_CLR_TC(); /* 清除DMA2数据流4 传输完成中断 */
- }
- }
复制代码在ADC DMA采集中断服务函数里,我们标记DMA传输完成,以及清除DMA2数据流4传输完成中断标志位。
最后在main.c里面编写如下代码: - #define ADC_DMA_BUF_SIZE 50 /* 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(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- led_init(); /* 初始化LED */
- lcd_init(); /* 初始化LCD */
- adc_dma_init((uint32_t)&g_adc_dma_buf); /* 初始化ADC DMA采集 */
- lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
- lcd_show_string(30, 70, 200, 16, 16, "ADC DMATEST", RED);
- lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
- lcd_show_string(30, 110, 200, 16, 16, "ADC1_CH5_VAL:", BLUE);
- /* 先在固定位置显示小数点 */
- lcd_show_string(30, 130, 200, 16, 16, "ADC1_CH5_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;
- }
- 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采集实验一样。首先我们在液晶固定位置显示了小数点,然后后面计算步骤中,先计算出整数部分在小数点前面显示,然后计算出小数部分,在小数点后面显示。这样就在液晶上面显示转换结果的整数和小数部分。
31.3.4 下载验证下载代码后,可以看到LCD显示如图31.3.4.1所示: 图31.3.4.1 单通道ADC采集(DMA读取)实验测试图 LED0闪烁,提示程序运行。这里的实验效果和单通道ADC采集实验是一样的,我们使用杜邦线将P11的ADC和RV1连接,使得PA5连接到电位器上,测试的是电位器的电压,并可以通过螺丝刀调节电位器改变电压值,范围:0~3.3V。
大家可以试试把杜邦线接到其他地方,看看电压值是否准确?注意:一定要保证测试点的电压在0~3.3V的电压范围,否则可能烧坏我们的ADC,甚至是整个主控芯片。 |