一、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供电引脚解释

  • VDDA:VDDA是模拟部分工作电源。如果要保证ADC精度,可以使用独立的模拟电源输入,但是VDDA最高不能超过4V。
  • VSSA:VSSA是模拟部分接地工作电源。与数字电源共地。
  • VREF+:VREF+ 是ADC转换的正参考电压。如果要保证ADC精度,可以使用单独的参考电压芯片输出的精密参考电压, VREF+不能超过VDDA,最低值为1.8V。一般情况下,将VREF+与VDDA连接。
  • VREF-:VREF-是将ADC的负参考电压,必须与VSSA连接,也就是必须和数字电源共地。
  • 需要注意的是: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。

  • ADC1_IN16(温度传感器):芯片内部温度传感器,测温范围为 -40摄氏度-125摄氏度,精度为 ±1.5°。
  • ADC1_IN17(VERFINT):内部参考电压,实际连接内部1.2V调压器的输出电压。
  • ADC1_IN18(VBAT):备用电源电压,因为VBAT电压可能高于VDDA,内部有桥梁分压器,实际测量的电压是VBAT/2。
  • 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类启动或触发转换的方式。

  • 软件启动:可以通过软件命令直接触发ADC转换,而不需要外部触发源。直接将控制器寄存器ADC_CR2的ADON位 置1启动ADC转换,写入0时停止ADC转换,这种方式常用与轮询的ADC转换。
  • 内部定时器触发:可以使用定时器的输出比较事件或更新事件来触发ADC转换,选择某个定时器的触发输出信号(TRGO)或输入捕获信号作为触发源。例如,选择TIM2_TRGO信号作为启动触发信号,而TIM2的TRGO设置为UEV事件信号,这样的定时器TIM2每次定时溢出时就启动一次转换。这种方式可用于周期性ADC转换。
  • 外部IO触发:可以使用外部引脚的信号来触发ADC转换。可以选择外部中断线EXTI_11或EXTI_15作为规则组或注入组的外部触发源。
    在STM32CUBEMAX中的规则通道的触发源设置如下图。

    图1-9:规则通道的触发源设置
  • 规则通道和注入通道的触发源由ADC 控制寄存器 2 (ADC_CR2)位[29:16]所设置。其中:

  • 位 30 SWSTART:为规则组选择软件触发;
  • 位 29:28 EXTEN:规则通道的外部触发使能(如:上升沿上的触发检测);
  • 位 27:24 EXTSEL[3:0]:为规则组选择外部事件(如:定时器事件、外部中断事件);
  • 位 22 JSWSTART:为注入组选择软件触发;
  • 位 21:20 JEXTEN:注入通道的外部触发使能(如:上升沿上的触发检测);
  • 位 19:16 JEXTSEL[3:0]:为注入组选择外部事件(如:定时器事件、外部中断事件);
  • 位 0 ADON:A/D 转换器开启/关闭;
  • 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 请求。

  • 模拟看门狗中断:首先通过ADC_LTR和ADC_HTR寄存器设置低阈值和高阈值,然后开启了模拟看门狗中断后,当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。例如我们设置高阈值是 3.0V,那么模拟电压超过 3.0V 的时候,就会产生模拟看门狗中断,低阈值的情况类似。
  • DMA 请求:规则组和注入组的转换结束后,除了产生中断外,还可以产生 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参数示意图
    图中有几个参数需要选择,如:

  • Scan Conversion Mode:扫描转换模式,设置为Enabled(规则序列中的多通道连续转换,转换完一个通道后,会自动转换组内下一个通道,直到一组通道都转换完成);设置为Disabled(只能采集到第一的通道的数据,后续的通道扫描不到)。
  • Continuous Conversion Mode:连续转换模式。设置为Enabled(ADC结束规则序列中所有通道的转换后立即启动一个新的转换);设置为Disabled(ADC结束规则序列中所有通道的转换后需要HAL_ADC_Start启动新的转换);
  • Discontinuous Conversion Mode:非连续转换模式(间断模式),设置为Enabled(ADC的转换序列被分割成多个小序列。每个小序列可以包含一个或多个通道,具体数量由Number Of Discontinuous Conversions参数决定,在每个小序列转换完成后,ADC会停止转换,并等待外部触发或软件指令来开始下一个小序列的转换)。通常情况下设置为Disabled;
  • DMA Continuous Requests: 是否连续产生DMA请求,设置为Disabled(在最后一个传输后不发出新的DMA请求);设置为Enable(只要发生数据转换且使用了DMA,就发出DMA请求)。
  • EOC标志产生方式,有以下2种选项。
    ① EOC flag at the end of single channel conversion:在每个通道转换完成后产生EOC标志。
    ② EOC flag at the end of all conversion:在规则序列的所有通道转换完成后产生EOC标志。
    寄存器在ADC_CR2位 10 EOCS(结束转换选择)
  • ADC_Regular_ConversionMode组,具体包括如下参数。
    ① 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

    作者:小欧不要怂

    物联沃分享整理
    物联沃-IOTWORD物联网 » HAL库STM32常用外设-ADC

    发表回复