HAL库STM32常用外设-ADC
一、ADC概述
1.1 ADC的特性
ADC 即模拟数字转换器(Analog-to-digital converter,ADC),可以将外部的模拟信号转换为数字信号,是模拟信号数字化的必要器件。STM32F407有3个ADC,最高12位分辨率,最多16个外部通道,ADC1还有3个内部测量通道,可以测量内部温度、参考电压和备用电池电压。
STM32F407的ADC 主要特性可总结为一下几条:
● 可配置 12 位、10 位、8 位或 6 位分辨率
● 在转换结束、注入转换结束以及发生模拟看门狗或溢出事件时产生中断
● 单次和连续转换模式
● 自校准
● 用于自动将通道 0 转换为通道“n”的扫描模式
● 数据对齐以保持内置数据一致性
● 可独立设置各通道采样时间
● 外部触发器选项,可为规则转换和注入转换配置极性
● 不连续采样模式
● 双重/三重模式(具有 2 个或更多 ADC 的器件提供)
● 双重/三重 ADC 模式下可配置的 DMA 数据存储
● 双重/三重交替模式下可配置的转换间延迟
● ADC 转换类型(参见数据手册)
● ADC 电源要求:全速运行时为 2.4 V 到 3.6 V,慢速运行时为 1.8 V
● ADC 输入范围:VREF- ~VREF+
● 规则通道转换期间可产生 DMA 请求
1.2 ADC的工作原理
STM32F407的ADC的运行框图如下所示:
图1-1ADC 框图
1.2.1 ADC的供电部分(图1-1中①):
其引脚的解释如下:
图1-2 ADC供电引脚解释
需要注意的是:ADC转换电压的输入范围是在VERF-与 VERF+之间。因为VREF-必须与VSS连接,也就是VREF-总是0,所以STM32F407的片上ADC只能转换正电压,这与某些独立的ADC芯片可转换正负范围的电压是不同的。
典型的供电方案如图1-3所示。
图1-3:ADC供电方案
1.2.2输入通道(图1-1中②)
每个ADC单元有16个外部输入引脚,对应与16个ADC输入复用引脚,即图1中的ADCx_IN0至ADCx_IN15。每个ADC单元的模拟输入复用引脚如图3所示。
图1-4:ADC通道对应引脚图
ADC1单元还有以下3个内部输入使用ADC1_IN16、ADC1_IN17、ADC1_IN18。
1.2.3注入通道和规则通道(图1-1中③)
当任意 ADCx 多个通道以任意顺序进行一系列转换就诞生了成组转换,这里就有两种成组转换类型:规则组和注入组。规则组就是图上的规则通道,注入组也就是图上的注入通道。规则组允许最多 16 个输入通道进行转换,而注入组允许最多 4 个输入通道进行转换。这里讲解一下规则组和注入组。
规则组(规则通道),按字面理解,“规则”就是按照一定的顺序,相当于正常运行的程序,平常用到最多也是规则组。
注入组(注入通道),按字面理解,“注入”就是打破原来的状态,相当于中断。当程序执行的时候,中断是可以打断程序的执行。同这个类似,注入组转换可以打断规则组的转换。假如在规则组转换过程中,注入组启动,那么注入组被转换完成之后,规则组才得以继续转换。
图1-5:规则通道和注入通道对比图。
规则序列是允许 16 个通道进行转换的,那么就需要安排通道转换的次序即规则序列。规则序列寄存器有 3 个,分别为 SQR1、SQR2 和 SQR3。SQR3 控制规则序列中的第 1 个到第 6 个转换的通道;SQR2 控制规则序列中第 7 个到第 12 个转换的通道;SQR1 控制规则序列中第 13 个到第 16 个转换的通道。如图1-6所示:
图1-6:规则序列寄存器ADC_SQRX表
其中,在ADC_SQR1寄存器中位 23:20 L[3:0]:定义规则通道序列的长度,表示需要转换的通道的数量。在STM32CUBEMAX中的设置如下。
图1-7:规则通道设置
注入序列,跟规则序列差不多,都是有顺序的安排。由于注入组最大允许 4 个通道输入,所以这里就使用了一个寄存器 JSQR。如图1-8所示:
图1-8:注入序列寄存器ADC_JSQR表
1.2.4触发源(图1-1中④)
规则通道和注入通道由单独的触发源,有以下3类启动或触发转换的方式。
在STM32CUBEMAX中的规则通道的触发源设置如下图。

图1-9:规则通道的触发源设置
规则通道和注入通道的触发源由ADC 控制寄存器 2 (ADC_CR2)位[29:16]所设置。其中:
1.2.5 ADC时钟与转换时间(图1-1中的⑤)
ADC转换需要时钟信号ADCCLK驱动,ADCCLK由PCLK2经过分频产生,最少2分频,最多8分频。STM32F407的PCLK2的最高频率为84MHz,所以ADCCLK的最高频率为42MHz(不同芯片的频率不同)。
图1-10:STM32F407时钟树
图1-11:ADC Prescalers分频
ADC Prescalers分频设置对应的寄存器为ADC 通用控制寄存器 (ADC_CCR);
其中:位 17:16 ADCPRE:ADC 预分频器置 1 和清零,以选择 ADC 的分频模式。该时钟为所有 ADC 所共用。
可以设置在N个ADCCLK周期内对信号进行采样,N的值最小为3,最大为480(如图1-12), 在允许的情况下,尽量选大一点的会使ADC 更稳定、更精确。
图1-12:ADC采样周期配置图
ADC 总转换时间的计算公式如下:
TCONV = 采样时间 + 12 个周期
以STM32F407的ADC1的通道1为例,如果 Clock Prescaler设置成“PCLK2 divided by 4”,Sampling Time设置成15 Cycles,那么ADC时钟频率为:
84MHz / 4 = 21MHz
则一个周期所需的时间为:
1/21MHz=0.047us
ADC1_IN1通道的转换时间为:
TCONV=(15+12)*0.047us
TCONV = 采样时间 + 12 个周期
采样时间可通过ADC 采样时间寄存器1((ADC_SMPR1) 和ADC 采样时间寄存器2( ADC_SMPR2 )中的 SMP[2:0]位编程,ADC_SMPR2控制的是通道 0-9,ADC_SMPR1 控制的是通道 10~18。
1.2.6数据寄存器(图1-1中的⑥)
ADC完成转换后将结果存放进数据寄存器,ADC 规则数据寄存器(ADC_DR),只有低16位有效,在多通道转换时,如果前一通道转换结束后,ADC_DR的数据未被及时读出,下一个转换通道的转换结果就会覆盖上一次结果的数据。所以,在多通道转换时,一般EOC中断里及时读取数据或通过DMA将数据传输到内存里。
注入通道有4个数据寄存器:ADC 注入数据寄存器 x (ADC_JDRx) (x= 1…4)分别对应4个注入通道的转换结果。低16位有效。
因为注入数据寄存器和ADC 规则数据寄存器是16位有效,与12位的ADC分辨率相比,多出4位。因此可通过ADC 控制寄存器 2 (ADC_CR2)位 11 ALIGN来设置数据左对齐或右对齐,一般使用右对齐。在二进制中左移n位,数据扩大2的n次方倍,所以左对齐直接读出的数值会比实际值大16倍。
同时在STM32CUBEMX也可以设置左对齐或者右对齐:
图1-13:ADC数据对齐方式
1.2.7中断(图1-1中的⑦)
规则和注入组转换结束时能产生中断,当模拟看门狗状态位被设置时也能产生中断,它们在 ADC_SR 中都有独立的中断使能位,后面讲解 ADC_SR 寄存器时再进行展开。这也是本文的重点。这里讲解一下,模拟看门狗中断以及 DMA 请求。
1.3 ADC转换结果电压计算
ADC转换的结果是一个数字量,与实际的模拟电压之间的计算关系 由VERF+和转换精度位数确定。例如,转换精度位12位,VERF+ 为3.3V,ADC转换结果位12位数字量对应的整数为X,则实际电压为:
Voltage = (3.3 * X) / 2^12 V
1.4 多重ADC模式
STM32F407有3个ADC。这三个ADC可以独立工作,也可以组成双重或三种工作模式。在多重工作模式下,ADC1是主器件,是必须使用的;双重模式就是使用的ADC1和ADC2,不能使用ADC1和ADC3;三种模式就是3个ADC都使用。
多重模式就是使用住期间ADC1的触发信号去交替触发或同步触发其他ADC启动转换。例如,对于三分量模拟输出的振动传感器,需要对X、Y、Z这3个方向的振动信号同步采集,以合成一个三维空间中的振动矢量,这时就需要使用3个ADC对3路信号同步采集,而不能使用一个ADC对3路信号通过多路复用方式进行采集。
多重ADC有多种工作模式,可以交替触发,也可以同步触发。为避免过于复杂,我们仅以双重ADC同步触发为例,说明多重ADC的工作原理和使用方法。 三种ADC和其他工作模式的原理参见STM32F407参考手册。
设置ADC1和ADC2双重工作模式,为ADC1设置的触发源同时也触发ADC2,以实现两个ADC同步转换。在多重模式下,有一个专门的32位数据寄存器ADC_CDR,用于存储多重模式下的转换结果数据。在多重模式下,ADC_CDR的高16位存储ADC2的规则转换结果数据,ADC_CDR的低16位存储ADC1的规则转换结果数据。
在多重模式下,使用DMA进行数据传输有3种模式,其中DMA模式2适用于双重ADC的数据传输。双重ADC是,DMA模式2的工作特点是:每发送一个DMA请求,就以字的形式传输 表示ADC2和ADC1转换结果的32位数据,其中高16位是ADC2的转换结果,低16位是ADC1的转换结果,相当于将ADC_CDR的数据在一个DMA请求时传输出去。
二、ADC的HAL库驱动程序
2.1主要函数
ADC的驱动程序有两个头文件:头文件stm32f4xx_hal_adc.h是ADC模块总体设置和常规通道相关的函数和定义;文件stm32f4xx_hal_adc_ex.h是注入通道和多重ADC模式相关的函数和定义。图2-1是文件stm32f4xx_hal_adc.h中的一些主要函数
图2-1主要函数功能描述图
2.2软件启动转换方式
函数HAL_ADC_Start()用于以软件方式启动ADC常规通道的转换,软件启动转换后,需要调用函数HAL_ADC_PollForConversion()查询转换是否完成,转换完成后可用函数HAL_ADC_GetValue()读出常规转换结果寄存器的32位数据。若要再次转换,需要再次使用这3个函数启动转换、查询转换是否完成、读出转换结果。使用函数HAL_ADC_Stop()停止ADC常规通道转换。
这种软件启动转换的模式适用于单通道、低采样率的ADC转换。这几个函数原型定义如下:
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc); //软件启动转换
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc); //停止转换
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc); //读取转换结果寄存器的32位数据
其中,参数hadc是ADC外设对象指针,Timeout是超时等待时间(单位是ms)。
HAL_ADC_Start()
:其本质就是将ADC 控制寄存器 2 (ADC_CR2)中的位 0 ADON置1;HAL_ADC_Stop()
;其本质就是将ADC 控制寄存器 2 (ADC_CR2)中的位 0 ADON置0;HAL_ADC_PollForConversion()
:其本质是读取判断ADC 状态寄存器 (ADC_SR)中的位 1 EOC(规则通道转换结束)是否置1;HAL_ADC_GetValue()
:其本质是读取ADC 规则数据寄存器(ADC_DR)的值。2.3中断转换方式
当ADC设置为用定时器或外部信号触发转换时,函数HAL_ADC_Start_IT()用于启动转换,这会开启ADC的中断。当ADC转换完成时会触发中断,在中断服务程序里,可以用HAL_ADC_GetValue()读取转换结果寄存器里的数据。函数HAL_ADC_Stop_IT()可以关闭中断,停止ADC转换。开启和停止ADC中断方式转换的两个函数的原型定义如下:
HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);
其中:
HAL_ADC_Start_IT()
:本质是在HAL_ADC_Start()的基础上将ADC 控制寄存器 1 (ADC_CR1)的位 5 EOCIE(EOC 中断使能)与位 26 OVRIE(溢出中断使能)置1;HAL_ADC_Stop_IT()
:本质是在HAL_ADC_Stop()的基础上将ADC 控制寄存器 1 (ADC_CR1)的位 5 EOCIE(EOC 中断使能)与位 26 OVRIE(溢出中断使能)置0;ADC1、ADC2和ADC3共用一个中断号,ISR名称是ADC_IRQHandler()
。ADC有4个中断事件源,中断事件类型的宏定义如下(在stm32f4xx_hal_adc.h中):
#define ADC_IT_EOC ((uint32_t)ADC_CR1_EOCIE) //规则通道转换结果结束(EOC)事件
#define ADC_IT_AWD ((uint32_t)ADC_CR1_AWDIE) //模拟看门狗触发事件
#define ADC_IT_JEOC ((uint32_t)ADC_CR1_JEOCIE) //注入通道转换结束事件
#define ADC_IT_OVR ((uint32_t)ADC_CR1_OVRIE) //数据溢出事件、即转换结果未被及时读出
ADC中断通用处理函数是HAL_ADC_IRQHandler()
,它内部会判断中断事件类型,并调用相应的回调函数。ADC的4个中断事件类型及其对应的回调函数如图所示。
中断事件类型 | 中断事件 | 回掉函数 |
---|---|---|
ADC_IT_EOC | 规则通道转换结果结束(EOC)事件 | HAL_ADC_ConvCpltCallback() |
ADC_IT_AWD | 模拟看门狗触发事件 | HAL_ADC_LevelOutOfWindowCallback() |
ADC_IT_JEOC | 注入通道转换结束事件 | HAL_ADCEx_InjectedConvCpltCallback() |
ADC_IT_OVR | 数据溢出事件、即转换结果未被及时读出 | HAL_ADC_ErrorCallback() |
图2-2中断事件对应回调函数
我们可以设置为在转换完一个通道后就产生EOC事件,也可以设置为转换完规则通道组的所有通道之后产生EOC事件。但是规则组只有一个转换结果寄存器,如果有多个转换通道,设置为转换完规则组的所有通道之后产生EOC,会导致数据溢出。一般设置为在转换外一个通道后就产生EOC事件如下图所示。
其原理就是通过将ADC 控制寄存器 1 (ADC_CR1)的位 8 SCAN(扫描模式)置0或者置1来控制什么时候产生EOC事件,所以,中断方式转换适用于单通道或采样频率不高的场合。
图2-3规则通道转换结果结束(EOC)事件
2.4 DMA方式转换
通过2.3节可知将ADC 控制寄存器 1 (ADC_CR1)的位 8 SCAN(扫描模式)置1,控制器就会在转换完规则组的所有通道之后产生EOC中断。但ADC 规则数据寄存器 (ADC_DR)只有一个,规则序列中的通道超过一个数据就会溢出。数据寄存器只能保存规则序列中最后一个通道的数据,如何避免数据溢出又能采集到规则通道中所有的数据。这里就需要DMA。
从图1的③和⑥中可以看到,每当ADC的通道转换结束时,就会产生一个DMA请求。如何让ADC与DMA联动起来呢?HAL库提供了一个函数:
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
其中,参数hadc是ADC外设对象指针;参数pData是uint32_t类型缓冲区指针,因为ADC转换结果寄存器是32位的,所以DMA数据宽度是32位;参数Length是缓冲区长度,单位是字(4字节)。
本质就是在HAL_ADC_Start()
函数的基础上将ADC 控制寄存器1(ADC_CR1)的位 26 OVRIE(溢出中断使能)置1,将ADC 控制寄存器 2(ADC_CR2)的位 8 DMA(直接存储器访问模式)置1(使能 DMA 模式),通过HAL_DMA_Start_IT()函数将数据寄存器 (ADC_DR)与RAM内存地址联系起来并开启DMA中断;
同时,也要在STM32CUBEMX中提前设置DMA,如下图
图2-4 设置DMA
当ADC 控制寄存器 2(ADC_CR2)的位 9 DDS(DMA 禁止选择)置1(只要发生数据转换且 DMA = 1,便会发出 DAM 请求),也可在STM32CUBEMAX直接设置如下图。
图2-5 DMA 禁止选择模式设置
停止DMA方式采集的函数是HAL_ADC_Stop_DMA();
其原型定义如下:
HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);
本质就是在HAL_ADC_Stop()
函数的基础上将ADC 控制寄存器1(ADC_CR1)的位 26 OVRIE(溢出中断使能)置0,将ADC 控制寄存器 2(ADC_CR2)的位 8 DMA(直接存储器访问模式)置0。
DMA流的主要中断事件与ADC的回调函数之间的关系如图所示。一个外设使用DMA传输方式时,DMA流的事件中断一般使用外设的事件中断回调函数。
图2-6 DMA流中断事件类型和关联的回调函数
注:注入通道没有DMA方式。
2.5 规则通道采样模式
对于ADC通道采集,采样模式的选择又很多内容,很容易犯迷糊。如下图所示。
图2-7 ADC参数示意图
图中有几个参数需要选择,如:
① EOC flag at the end of single channel conversion:在每个通道转换完成后产生EOC标志。
② EOC flag at the end of all conversion:在规则序列的所有通道转换完成后产生EOC标志。
寄存器在ADC_CR2位 10 EOCS(结束转换选择)
① Number of Conversion:规则转换序列的转换个数,最多16个,每个转换作为一个Rank(级)。这个数值不必等于输入模拟信号通道数。
② External Trigger Conversion Source:外部触发转换的信号源。本例选择为软件启动常规转换(Regular Conbversion launched by software)。周期性采集时,一般选择定时器TRGO信号或捕获比较事件信号作为触发信号,还可以选择外部中断线信号作为触发信号。
③External Trigger Conversion Edge:外部触发转换时间使用的信号边沿,可选择上跳沿、下跳沿,或双边都触发。本例中未使用外部触发,所以设置为None。
④Rank:规则组内每一个转换对应一个Rank,一个Rank需要设置输入通道(Channel) 和 采样时间(Samping Time)。一个规则组中有多个Rank时,Rank的设置顺序就规定了转换通道的序列。每个Rank的采样时间可以单独设置。采样时间的单位是ACCLK的时钟周期数,采样时间越大,转换结果越准确。
三、定时器+ADC多通道+DMA中断读取
当只有一个通道,在转换结束后可以读出结果数据寄存器的内容。当规则转换组有多个通道时,应该使用扫描转换模式(Scan Conversion Mode),ADC在转换完一个通道后立即转换下一个通道,知道规则组内的通道序列转换完。规则转换只有一个转换结果数据寄存器,虽然可以设置在每个通道转换完之后就产生EOC事件中断,但是在多通道情况下,在EOC事件中断里读取转换结果数据可能是来不及的,更谈不上对数据进行显示和处理。
如果规则转换组有多个输入通道,应该使用DMA,使转换结果数据通过DMA传输自动保存到缓存中,在一个规则组转换结束后在对数据进行处理,或者在采集多次数据后再处理。
本次示例中用到了三个规则组输入通道,使用扫描模式,通过DMA方式传输ADC转换结果数据。通过定时器TIM2定时500ms 去触发。
3.1设置TIM2
STM32F407ZGT6的TIM3在总线APB1上,定时器时钟信号频率为84MHz,TIM3的模式和配置界面如图4-2所示。模式设置部分只需设置Clock Source 为Internal Clock,启动TIM3即可。
Trigger Output(TRGO)Parameters组用于设置TRGO信号,主/从模式(Master/Slave Mode)设置为Disable,即禁用主/从模式。触发事件选择(Trigger Event Selection)设置为Updata Event,也让就是以UEV事件信号作为TRGOx信号。
这样,ADC1在TIM3的TRGO信号的每个上跳沿启动一次ADC转换,就可以实现周期性的ADC转换,转换周期由TIM3的定时周期决定。无需开启TIM3的全局中断,TRGO信号也是正常输出的。
3.2 ADC1设置
在ADC_Settings参数组,开启扫描转换模式(Scan Conversion Mode)、DMA连续请求(DMA Continous Requests)、在规则序列的所有通道转换完成后产生EOC标志(EOC flag at the end of all conversion)。
在ADC_Regular_ConversionMode参数组设置转换个数为3,下面会自动生成3个Rank的设置,分别设置每个Rank的输入通道和采样时间——每个通道的采样时间可以不一样,3个Rank里面模拟通道的出现顺序就是规则组转换的顺序.
External Trigger Conversion Source,用于设置启动ADC转换的外部触发信号源,选择Timer 2 Trigger Out evernt,也就是TIM2 的TRGO x信号。
External Trigger Conversion Edge,用于设置触发转换的跳变沿,可选上跳沿,下跳沿或双边沿都触发。这里选择上跳沿,因为TRGO是一个短时正脉冲信号。
配置如下图所示:
3.3 DMA设置
ADC只有一个DMA请求,为这个DMA请求配置DMA流DMA2 Stream 0,设置DMA传输属性参数,设置界面如图所示。DMA传输方向自动设置为Peripheral To Memory(外设到存储器)。在DMA Request Setting 组中将Mode(工作模式)设置为Circular(循环模式),将外设和存储器的数据宽度都设置为Word——因为ADC转换结果数据寄存器是32位的,存储器设置为地址自增加。
3.4 uart1设置
3.5 程序设计
在生成代码后的基础上添加以下内容:
usart.c:
#include "usart.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
UART_HandleTypeDef huart1;
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspDeInit 0 */
/* USER CODE END USART1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_USART1_CLK_DISABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
/* USER CODE BEGIN USART1_MspDeInit 1 */
/* USER CODE END USART1_MspDeInit 1 */
}
}
/* USER CODE BEGIN 1 */
int _write(int file, char *ptr, int len)
{
/* send data to serial huart1 */
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 1000);
return len;
}
/* USER CODE END 1 */
int _write(int fd, char* ptr, int len)
函数是stdio.h中printf函数的重定义,可通过printf实现串口数据的打印。
main.c:
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint32_t adc_buffer[3];
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_ADC_Start_DMA(&hadc1,adc_buffer, 3);
HAL_TIM_Base_Start(&htim2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 16;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance==ADC1)
{
float adc_value[3];
for(uint8_t i=0;i<3;i++)
{
adc_value[i]=(adc_buffer[i]*3.3)/4096;
}
printf("NVIC ADC IN1 = %fV ADC IN2 = %fV ADC IN3 = %fV \r\n",adc_value[0],adc_value[1],adc_value[2]);
}
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
其中:
uint32_t adc_buffer[3];
:定义了一个数组去存储ADC采集到的数值。也是DMA从ADC_DR提取数据的存储位置。HAL_ADC_Start_DMA(&hadc1,adc_buffer, sizeof(adc_buffer));
:该函数用于以DMA方式开启ADC转换。第一个参数是指向ADC_HandleTypeDef结构体的指针,表示要操作的ADC模块(在这里是hadc1)。第二个参数是一个指向存储转换结果的数据缓冲区的指针(在这里是dmaDataBuffer)。第三个参数是要转换的通道数量(在这里是3)。调用该函数后,ADC模块将会以DMA方式进行模拟信号的转换,并将结果存储在dmaDataBuffer数组中。HAL_TIM_Base_Start(&htim2);
:该函数用于启动定时器TIM2的基本定时器功能。第一个参数是指向TIM_HandleTypeDef结构体的指针,表示要操作的定时器(在这里是htim2)。调用该函数后,定时器TIM2将开始计数,并触发相关的定时器中断(如果已使能)void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
:这是一个由HAL库提供的回调函数,当ADC转换完成时,将自动调用该函数。它接受一个指向ADC_HandleTypeDef结构体的指针参数hadc,用于标识触发回调的ADC模块。static float adc_value[3];
:定义了一个数组adc_value,用于存放计算后的ADC转换结果。循环处理:通过循环遍历,对前两个通道的转换结果进行处理。循环变量i从0到2,即执行三次循环。
转换结果计算: adc_value[i]=adc_buffer[i]*3.3/4096;
该行代码计算了第i个通道的电压值。
打印结果: printf("NVIC ADC IN1 = %fV ADC IN2 = %fV ADC IN3 = %fV \r\n",adc_value[0],adc_value[1],adc_value[2]);
** 3.5 示例结果
软件触发,模拟看门狗,多重ADC采集,注入通道后续更新。。。
如有错误,欢迎指出,不胜感激。
参考书籍和文章:
《STM32F4xx中文参考手册》
https://openatomworkshop.csdn.net/6673ddaba1e8811a9781de12.html
https://blog.csdn.net/Pretender_1205/article/details/136379247
作者:小欧不要怂