STM32 DMA+AD多通道

单片机学习!


目录

一、DMA配置步骤

二、ADC配置步骤

三、DMA+AD多通道框图

四、DMA+AD多通道函数设计详细步骤

[4.1 开启RCC时钟](#4.1 开启RCC时钟)

[4.2 配置GPIO](#4.2 配置GPIO)

[4.3 配置多路开关](#4.3 配置多路开关)

[4.4 结构体初始化ADC](#4.4 结构体初始化ADC)

[4.5 DMA参数初始化配置](#4.5 DMA参数初始化配置)

[4.5.1 外设站点的三个参数](#4.5.1 外设站点的三个参数)

[4.5.2 存储器站点的三个参数](#4.5.2 存储器站点的三个参数)

[4.5.3 传输方向](#4.5.3 传输方向)

[4.5.4 传输计数器](#4.5.4 传输计数器)

[4.5.5 是否使用自动重装](#4.5.5 是否使用自动重装)

[4.5.6 选择触发源](#4.5.6 选择触发源)

[4.5.7 通道优先级](#4.5.7 通道优先级)

[4.5.8 DMA_Init](#4.5.8 DMA_Init)

[4.6 DMA开关控制](#4.6 DMA开关控制)

[4.7 开启ADC到DMA的输出](#4.7 开启ADC到DMA的输出)

[4.8 开启ADC电源](#4.8 开启ADC电源)

[4.9 ADC进行校准](#4.9 ADC进行校准)

[4.9.1 复位校准](#4.9.1 复位校准)

[4.9.2 等待复位校准完成](#4.9.2 等待复位校准完成)

[4.9.3 开始校准](#4.9.3 开始校准)

[4.9.4 等待校准完成](#4.9.4 等待校准完成)

[4.10 启动AD转换与DMA转运函数设计](#4.10 启动AD转换与DMA转运函数设计)

[4.10.1 ADC软件触发转换。](#4.10.1 ADC软件触发转换。)

[4.10.2 传输计数器赋值](#4.10.2 传输计数器赋值)

[4.10.3 标志位查看/清除](#4.10.3 标志位查看/清除)

[4.10.4 启动函数代码](#4.10.4 启动函数代码)

五、DMA+AD多通道代码

总结


一、DMA配置步骤

初始化步骤:

第一步,RCC开启DMA的时钟。

第二步,参数初始化配置。直接调用DMA_Init,初始化各个参数,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。这些所有的参数,通过一个结构体,就可以配置好了。

第三步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。

  • 如果选择的是硬件触发,需要在对应的外设调用一下XXX_DMACmd函数,开启一下触发信号的输出;
  • 如果需要DMA的中断,那就调用DMA_ITConfig,开启中断输出。再在NVIC里配置相应的中断通道,然后写中断函数就可以了。

最后,在运行的过程中,如果转运完成,传输计数器清0了,这时再想给传输计数器赋值的话,就DMA失能 -> 写传输计数器 -> DMA使能,这样就行了。

二、ADC配置步骤

参照结构框图,在原理上将ADC外设运转起来。

第一步:开启RCC时钟,包括ADC和GPIO的时钟。另外ADCCLK的分频器也需要配置。

第二步:配置GPIO,把需要用到的GPIO配置成模拟输入的模式。

第三步:配置多路开关,把通道接入到规则组列表里。

第四步:配置ADC转换器,库函数中用结构体来配置电路参数,包括ADC是单次转换还是连续转换;扫描还是非扫描;有几个通道;触发源是什么;数据对齐是左对齐还是右对齐。

  • 如果需要模拟看门狗,可以用几个库函数来配置阈值和监测通道;
  • 如果需要开启中断,在中断输出控制里用 ITConfig 函数开启对应的中断输出,然后再在NVIC里配置一下优先级。这样就能触发中断了。

最后:开关控制,调用一下ADC_Cmd函数,开启ADC.

在开启ADC之后,根据STM32手册的建议,还可以对ADC进行一下校准,这样可以减小误差。

在ADC工作的时候,如果想要软件触发转换,有库函数可以触发;如果想读取转换结果,也会有函数可以读取结果。

三、DMA+AD多通道框图

下图对应的任务是ADC扫描模式+DMA。图左边是ADC扫描模式的执行流程,有7个通道,触发一次后,7个通道依次进行AD转换。然后转换结果都放到ADC_DR数据寄存器里面。DMA要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增。这样数据就不会被覆盖了。

DMA基本结构参数配置:

首先 是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。

  1. 外设地址 应该写入 ADC_DR 这个寄存器地址,存储器地址可以在 SRAM 中定义一个数组 ADValue ,然后把 ADValue 的地址当作存储器的地址;
  2. 数据宽度,因为 ADC_DR 和 SRAM 数组需要的数据类型都是uint16_t,所以数据宽度都是16位的半字传输;
  3. 地址是否自增,应该是外设地址不自增,存储器地址自增。

第二方向参数,根据任务图,应该是外设站点转运到存储器站点了。

第三传输计数器是否要自动重装

传输计数器这里有7个通道,所以计数7次。

计数器是否自动重装,这需要看ADC的配置。

ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止;

ADC如果是连续扫描,那DMA的传输计数器就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。

第四触发选择部分,这里 ADC_DR 的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转化完成同步。那DMA的触发要选择ADC的硬件触发。

硬件触发这里需要说明一下。在ADC扫描模式下,每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不太好判断某一个通道转换完成的时机是什么时候。这里虽然单个通道转换完成后,不产生任何标志位和中断,但是应该会产生DMA请求,去触发DMA转运。

最后 ,给DMA使能,调用DMA_Cmd函数之后,数据就会从 ADC_DR 数据寄存器转运到 SRAM 数组了。

一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,而这个缺陷也使得ADC和DMA成为了最常见的伙伴。ADC对DMA的需求是非常强烈的,其它一些外设使用DMA可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多损失一些性能。但是ADC的扫描模式,如果不使用DMA,功能都会受到很大的限制。所以ADC和DMA的结合最为常见。

四、DMA+AD多通道函数设计详细步骤

这里代码使用的是ADC的扫描模式,加DMA数据转运,执行流程如图:

4.1 开启RCC时钟

第一步:开启RCC时钟。包括ADC和GPIO的时钟。另外ADCCLK的分频器也需要配置。

代码示例:

cpp 复制代码
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC1的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//还需要开启PA0口的时钟
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置ADCCLK

RCC_APB2PeriphClockCmd函数开启ADC1的时钟,ADC都是APB2上的设备,所以这里用APB2开启时钟的函数。

RCC_APB2PeriphClockCmd函数用于开启PA0口的时钟。

以上时钟就配置好了,还需要配置ADCCLK,用RCC_ADCCLKConfig/函数来配置,函数有四个参数分别是2、4、6、8分频:

  • RCC_PCLK2_Div2: ADC clock = PCLK2/2
  • RCC_PCLK2_Div4: ADC clock = PCLK2/4
  • RCC_PCLK2_Div6: ADC clock = PCLK2/6
  • RCC_PCLK2_Div8: ADC clock = PCLK2/8

函数参数配置好之后,ADC的CLOCK=PCLK2/2、PCLK2/4、PCLK2/6、PCLK2/8,参数的PCLK2就是APB2时钟的意思。

代码示例中选择6分频,分频之后,ADCCLK=72MHz/6=12MHz

4.2 配置GPIO

第二步:配置GPIO,把需要用到的GPIO配置成模拟输入的模式。

代码示例:

cpp 复制代码
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AIN;//选择模拟输入
    GPIO_InitStruct.GPIO_Pin= GPIO_Pin_0;
    GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStruct);

代码示例中选择GPIO_Mode_AIN模拟输入这个模式,在GPIO_Mode_AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰。GPIO_Mode_AIN模式就是ADC的专属模式。

4**.3 配置多路开关**

第三步:配置多路开关,把通道接入到规则组列表里。

用ADC_RegularChannelConfig函数可选择规则组的输入通道。

函数参数:

  • 第1个参数:选择ADC。
  • 第2个参数:指定通道,通道0~通道17.
  • 第3个参数:Rank,规则组序列器里的次序,在1~16之间。若只有PA0一个通道,使用的是非扫描模式,那指定的通道就放在第一个序列1的位置。
  • 第4个参数:指定通道的采样时间,采样时间参数根据需求调整,需要更快的转换,就选择小的参数;需要更稳定的转换,就选择大的参数。

如果想在序列2的位置写入其他通道,就可以复制一下这个代码,把序列数改成2,然后指定你想要的通道,若还需要继续填充序列,可以再复制这个函数,修改序列和通道,另外每个通道也可以设置不同的采样时间,在函数最后一个参数修改即可。

代码示例:

cpp 复制代码
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);

代码示例的配置是:在ADC1中,在规则组菜单列表的第一个位置,写入通道0、1、2、3通道,指定通道的采样时间参数选择的采样时间为55.5个ADCCLK的周期。

因为需要扫描PA0到PA3这4个通道,所以调用四次ADC_RegularChannelConfig函数。通道0放在序列1的位置;通道1放在序列2的位置;通道2放在序列3的位置;通道3放在序列4的位置。

需要转运的1~4号空位,填上了0~3这4个通道。通道和次序可以任意修改,修改后最终结果存放的顺序也会相应变化。

4.4 结构体初始化ADC

第四步、用结构体初始化ADC。这里使用ADC,单次转换,扫描模式。

代码示例:

cpp 复制代码
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//ADC的工作模式
    ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;//数据对齐
    ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//外部触发转换选择
    ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;//连续/单次转换模式
    ADC_InitStructure.ADC_ScanConvMode=ENABLE;//扫描/非扫描转换模式
    ADC_InitStructure.ADC_NbrOfChannel=4;//通道数目

    ADC_Init(ADC1,&ADC_InitStructure);

ADC_Mode 是ADC的工作模式,这个参数是配置ADC是工作在独立模式还是双ADC模式,代码示例选择独立模式。

ADC_DataAlign 数据对齐,这里介绍是指定ADC数据是左对齐还是右对齐。

  • ADC_DataAlign_Right右对齐;
  • ADC_DataAlign_Left左对齐

ADC_ExternalTrigConv外部触发转换选择,就是触发控制的触发源,定义用于启动规则组转换的外部触发源。参数对应结构框图中的外部触发源选择。代码示例选择 ADC_ExternalTrigConv_None 参数,就是不使用外部触发,也就是使用内部软件触发的意思。

ADC_ContinuousConvMode 连续/单次转换模式,这个参数可以选择是ENABLE连续转换模式还是DISABLE单次转换模式。

ADC_ScanConvMode扫描/非扫描转换模式,这个参数可以选择是ENABLE扫描模式(多通道)还是DISABLE非扫描模式(单通道)。

ADC_NbrOfChannel通道数目,这个是在指定扫描模式下,总共会有几个通道需要扫描,参数必须在1~16之间。这个参数仅在扫描模式下使用。因为需要扫描PA0到PA3这4个通道,所以通道数目填4.

后三个参数设置可以对应四种模式:

  • 单次转换非扫描模式。
  • 连续转换非扫描模式。
  • 单次转换扫描模式。
  • 连续转换扫描模式。

这里配置的是单次转换,扫描模式。

4.5 DMA参数初始化配置

第五步,参数初始化配置。直接调用DMA_Init,初始化各个参数。

参数包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。

这些所有的参数,通过一个结构体,就可以配置好了。

代码示例:

cpp 复制代码
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址
    DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//数据宽度
    DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//是否自增
    DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//起始地址
    DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;//数据宽度
    DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
    DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
    DMA_InitStructure.DMA_BufferSize=Size;//传输计数器
    DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//否使用自动重装
    DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;//选择硬件触发还是软件触发
    DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
    DMA_Init(DMA1_Channel1,&DMA_InitStructure);

4.5.1 外设站点的三个参数

外设站点的三个参数:

  1. 起始地址
  2. 数据宽度
  3. 地址是否自增
  1. 起始地址

DMA_PeripheralBaseAddr 起始地址,外设站点的基地址。这在里要写一个32位的地址,比如 0x20000000 这样的地址。

外设站点的起始地址是DMA转运的源头,AD转换完成后数据结果都放在ADC_DR寄存器里。所以外设站点的起始地址就填ADC_DR寄存器的地址。

ADC1的DR寄存器地址是0x4001 244C,所以可以直接填0x4001244C。但是库函数已经算好了具体地址,一般寄存器地址也不需要算好后填实际值,所以可以对用 ADC1->DR 的方法取ADC_DR寄存器地址,再强转为uint32_t类型。也就是这样: (uint32_t)&ADC1->DR ,得到的结果就是0x4001244C。

代码示例:

cpp 复制代码
    DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址

这样外设站点的地址就完成了。

  1. 数据宽度

DMA_PeripheralDataSize数据宽度,函数定义中介绍是,指定数据宽度的参数可以是以下值:

  • DMA_PeripheralDataSize_Byte Byte,字节,就是uint8_t ;
  • DMA_PeripheralDataSize_HalfWord HalfWord,半字,就是uint16_t ;
  • DMA_PeripheralDataSize_Word Word,字,就是uint32_t 。

这里需要DR寄存器低16位的数据,所以就填入DMA_PeripheralDataSize_HalfWord,半字。

  1. 地址是否自增

DMA_PeripheralInc 地址是否自增。函数定义中解释是,指定外设地址是自增或者不是。

参数取值:

  • DMA_PeripheralInc_Enable 自增
  • DMA_PeripheralInc_Disable 不自增

这里不自增,始终转运同一个位置的数据,所以这里选择 DMA_PeripheralInc_Disable 。


这样外设站点的参数就配置好了:

cpp 复制代码
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//起始地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//是否自增

4.5.2 存储器站点的三个参数

存储器站点的三个参数:

  1. 起始地址
  2. 数据宽度
  3. 地址是否自增
  1. 起始地址

DMA_MemoryBaseAddr 起始地址,存储器站点的基地址。存储器地址可以在 SRAM 中定义一个数组 ADValue ,然后把 ADValue 的地址当作存储器的地址。

这里需要把数据存在SRAM数组里,所以现在代码块最前面定义一个数组AD_Value,然后在这里把AD_Value数组的地址作为目的地,把数组的地址强转为uint32_t,

示例:

cpp 复制代码
    DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;

这样存储器站点的地址就完成了。

  1. 数据宽度

DMA_MemoryDataSize 数据宽度,和外设站点一样也选择HalfWord参数,以半字传输。

  • DMA_MemoryDataSize_Byte Byte,字节,就是uint8_t ;
  • DMA_MemoryDataSize_HalfWord HalfWord,半字,就是uint16_t ;
  • DMA_MemoryDataSize_Word Word,字,就是uint32_t 。
  1. 地址是否自增

DMA_MemoryInc 地址是否自增,存储器地址是自增的,每转运一次挪一个坑。这里选择地址自增。

  • DMA_MemoryInc_Enable 自增
  • DMA_MemoryInc_Disable 不自增

这样存储器站点的参数就配置好了:

cpp 复制代码
    DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
    DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//数据宽度
    DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增

4.5.3 传输方向

DMA_DIR 传输方向,函数定义解释是,指定外设站点是源端还是目的地。

  • DMA_DIR_PeripheralDST 外设站点作为DST,destination,目的地,外设站点作为目的地,其实就是传输方向是存储器站点到外设站点。
  • DMA_DIR_PeripheralSRC 外设站点作为SRC,source,源头,外设站点作为源头,也就是传输方向是外设站点到存储器站点。

传输方向是外设站点到存储器站点,所以这里选择 DMA_DIR_PeripheralSRC参数,外设站点作为数据源。

代码示例:

cpp 复制代码
    DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向

4.5.4 传输计数器

DMA_BufferSize 缓存区大小,其实就是传输计数器。

DMA_BufferSize 在函数定义解释是,以数据单元指定缓存区大小,数据单元等于外设数据宽度或者存储器数据宽度,数据宽度取决于传输方向。

以数据单元指定缓存区大小,就是说需要传送几个数据单元,这个数据单元等于传输源站点的DataSize,简单理解就是,DMA_BufferSize就是传输计数器,指定传输几次。

可以查看DMA_Init函数的源码,DMA_BufferSize参数其实就是直接赋值给了传输计数器的寄存器,它的取值是0~65535.

因为有4个ADC通道,传输4次,所以这里DMA_BufferSize给4。

代码示例:

cpp 复制代码
    DMA_InitStructure.DMA_BufferSize=4;//传输数量给4

这样传输次数就完成了。

4.5.5 是否使用自动重装

DMA_Mode 传输模式,其实就是指定传输计数器是否使用自动重装。

DMA_Mode 在函数定义中的解释是,指定操作方式有对应参数取值列表。

配置 DMA_Mode 参数还有一个注意事项,函数定义中写的是循环模式,也就是自动重装,不能应用在存储器到存储器的情况下。也就是之前博文说的,自动重装和软件触发不能同时使用,如果同时使用,DMA就会连续触发,永远也不会停下来。

  • DMA_Mode_Circular 循环模式,就是传输计数器自动重装
  • DMA_Mode_Normal 正常模式,就是传输计数器不自动重装,自减到0后停下来。

这里可以给正常的单次模式,也可以自动重装的循环模式。这里就先将 DMA_Mode 的配置选择正常模式DMA_Mode_Normal。

代码示例:

cpp 复制代码
    DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//否使用自动重装

4.5.6 选择触发源

DMA_M2M 选择是否存储到存储器,其实就是选择硬件触发还是软件触发。

DMA_M2M 在函数定义中的解释是,DMA是否应用于存储器到存储器的转运模式,存储器到存储器的转运模式就是软件触发。

  • DMA_M2M_Enable 使用软件触发。
  • DMA_M2M_Disable 不使用软件触发,也就是使用硬件触发。

这里不使用软件触发,需要硬件触发,触发源为ADC1。

因为 ADC_DR 的值是在ADC单个通道转换完成后才会有效,DMA转运的时机,需要和ADC单个通道转化完成同步。每个通道的AD转换好了,叫DMA一下,DMA再去转运数据,这样才是合适的时机。

所以选择 DMA_M2M_Enable 使用软件触发。

代码示例:

cpp 复制代码
    DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;//选择触发源

4.5.7 通道优先级

DMA_Priority 优先级,按照参数要求,给一个优先级。

DMA_Priority 在函数定义中的解释是,指定通道的软件优先级。

  • DMA_Priority_VeryHigh 非常高
  • DMA_Priority_High 高
  • DMA_Priority_Medium 中等
  • DMA_Priority_Low 低

这里配置DMA_Priority_Medium,中等。

代码示例:

cpp 复制代码
     DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级

4.5.8 DMA_Init

DMA_Init 函数参数配置:

第一个参数:DMAy_Channelx,y可以是1或2,用来选择是哪个DMA;对于DMA1,x可以是1~7,或者对于DMA2,x可以是1~5.x用来选择是哪一个通道。

DMA_Init函数的第一个参数,既选择了是哪个DMA,也选择了是DMA的哪个通道。

这里通道不能任意选择,需要看DMA1请求映像图。

可以看到ADC1的硬件触发只接在了DMA1的通道1上。所以这里必须要使用DMA1的通道1,其它通道都不行。所以 DMAy_Channelx,这里y写为1,选择DMA1;x选择通道,x给1,通道1.

第二个参数的位置放 DMA_InitStructure 结构体的地址,这样就是把结构体指定的参数,配置到DMA1的通道1里面去。

代码示例:

cpp 复制代码
DMA_Init(DMA1_Channel1,&DMA_InitStructure);

4.6 DMA开关控制

第六步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。

代码示例:

cpp 复制代码
    DMA_Cmd(DMA1_Channel1,ENABLE);

使能DMA之后,来看一下DMA转运的三个条件:

  • 第一个,传输计数器不为0,满足;
  • 第三个,DMA使能,满足;
  • 第二个,触发源有信号,不满足。因为这里配置的是硬件触发,ADC还没启动,就不会有触发信号。

所以这里DMA使能之后,不会立刻工作。最后在ADC使能之前还有一个事需要做,就是开启ADC到DMA的输出。

4.7 开启ADC到DMA的输出

第七步,开启ADC到DMA的输出。有三个硬件触发源,具体使用哪个,取决于哪个硬件触发源的DMA输出开启了。

这里调用 ADC_DMACmd 函数,开启DMA触发信号。

代码示例:

cpp 复制代码
    ADC_DMACmd(ADC1,ENABLE);

4.8 开启ADC电源

第五步、开启ADC电源,调用一下ADC_Cmd函数,开启ADC。

代码示例:

cpp 复制代码
ADC_Cmd(ADC1,ENABLE);

以上配置完后ADC准备就绪。

4.9 ADC进行校准

在开启ADC电源之后,根据手册的建议,还需要对ADC进行校准,校准分为以下四步。

  1. 复位校准
  2. 等待复位校准完成
  3. 开始校准
  4. 等待校准完成

4.9.1 复位校准

代码示例:

cpp 复制代码
ADC_ResetCalibration(ADC1);//复位校准

4.9.2 等待复位校准完成

代码示例:

cpp 复制代码
while(ADC_GetResetCalibrationStatus(ADC1)==SET);

ADC_GetResetCalibrationStatus函数是返回复位校准的状态,要等待复位完成的话,还需要加一个while循环,若没校准完成的话,就在这个while空循环里一直等待。

获取的标志位和是否校准完成的对应关系需参考++函数定义++ 和++寄存器说明++。

函数定义:

ADC_GetResetCalibrationStatus函数定义中返回值的说明是,ADC复位校准寄存器的状态,SET或RESET。

函数代码:

cpp 复制代码
/**
  * @brief  Gets the selected ADC reset calibration registers status.
  * @param  ADCx: where x can be 1, 2 or 3 to select the ADC peripheral.
  * @retval The new state of ADC reset calibration registers (SET or RESET).
  */
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx)
{
  FlagStatus bitstatus = RESET;
  /* Check the parameters */
  assert_param(IS_ADC_ALL_PERIPH(ADCx));
  /* Check the status of RSTCAL bit */
  if ((ADCx->CR2 & CR2_RSTCAL_Set) != (uint32_t)RESET)
  {
    /* RSTCAL bit is set */
    bitstatus = SET;
  }
  else
  {
    /* RSTCAL bit is reset */
    bitstatus = RESET;
  }
  /* Return the RSTCAL bit status */
  return  bitstatus;
}

在此函数代码中也可以看出来,它获取的就是CR2寄存器里的RSTCAL标志位。

寄存器说明:

在ADC的CR2寄存器里RSTCAL复位校准位的说明是该位由软件设置并由硬件清除,在校准寄存器被初始化后该位将被清除。

所以该标志位的用法就是软件置改位为1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清0.

因为校准的第一条代码ADC_ResetCalibration(ADC1);开始复位校准,就是将RSTCAL标志位置1,然后获取复位校准状态,就是读取RSTCAL标志位这一位,所以在读取这一位的时候:

  • 如果它是1,那就需要一直空循环等待;
  • 如果它变为0了,那就说明复位校准完成,可以跳出等待了。

所以校准第二条代码while(ADC_GetResetCalibrationStatus(ADC1)==SET);中while的条件就是,获取标志位函数 ADC_GetResetCalibrationStatus 的返回值是不是==SET,如果等于SET,while条件为真,就会一直空循环。一旦标志位被硬件清0了,这个空循环就会自动跳出来。这样就实现了等待复位校准完成的效果。这里==SET也是可以省略的,因为返回值SET直接作为条件和是不是==SET作为条件效果是一样的。

4.9.3 开始校准

代码示例:

cpp 复制代码
ADC_StartCalibration(ADC1);

调用ADC_StartCalibration函数就开始校准了,之后内部电路就会自动进行校准。

4.9.4 等待校准完成

cpp 复制代码
    while(ADC_GetCalibrationStatus(ADC1)==SET);//调用函数获取校准状态

调用ADC_GetCalibrationStatus函数获取校准状态,也需要将函数放于while循环内,和校准的第二步同理,循环条件是,校准标志位是不是==SET,这样就可以等待校准是否完成了。

4.10 启动AD转换与DMA转运函数设计

上文描述的代码设置使ADC和DMA配合工作的配置完成了。以下设计启动AD转换,获取DMA转运结果的函数块。

4.10.1 ADC软件触发转换。

因为现在ADC还是单次模式,所以还需要软件触发一下ADC开始。其他的ADC函数就不需要了。

调用ADC_SoftwareStartConvCmd函数,实现软件触发。

代码示例:

cpp 复制代码
ADC_SoftwareStartConvCmd(ADC1,ENABLE);

调用ADC_SoftwareStartConvCmd函数之后就可以触发,ADC就已经开始进行转换了。转换需要一段时间。

4.10.2 传输计数器赋值

因为DMA也是正常的单次模式,所以在触发ADC之前,需要再启动一次DMA转运,就需要重新再重新写入一下传输计数器。

重新给传输计数器赋值必须要

  1. 先给DMA失能;
  2. 然后给传输计数器赋值;
  3. 最后再给DMA使能。

代码示例:

cpp 复制代码
void MyDMA_Transfer(void)
{
    DMA_Cmd(DMA1_Channel1,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel1,4);
    DMA_Cmd(DMA1_Channel1,ENABLE);

    ......
}
  1. 调用DMA_Cmd函数,使DMA失能。
  • 第一个参数还是选择DMA1的通道1;
  • 第二个参数给DISABLE。
  1. 然后就可以给传输计数器赋值了,调用DMA_SetCurrDataCounter函数。
  • 第一个参数,选择DMA和通道。
  • 第二个参数,指定要给传输计数器写入的值,也就是传输次数,给4。
  1. 最后,再次调用DMA_Cmd函数,给DMA使能。
  • 第一个参数还是选择DMA1的通道1;
  • 第二个参数给ENABLE。

4.10.3 标志位查看/清除

最后,等待ADC转换和DMA转运完成。因为DMA转运总是在ADC转换之后的,所以可以写入等待DMA转运完成的代码,等待ADC转换完成的代码就不需要了。

等待转运完成,可以通过查看标志位来确定转运是否完成。

因为转运也是要花一些时间的,等待转运完成调用 DMA_GetFlagStatus函数可以查看标志位。

DMA_GetFlagStatus函数中总共四种标志位,以DMA1的通道1举例,其它所有的通道,都是这4种标志位。

  • DMA1_FLAG_GL1:全局标志位
  • DMA1_FLAG_TC1:转运完成标志位
  • DMA1_FLAG_HT1:转运过半标志位
  • DMA1_FLAG_TE1:转运错误标志位

代码示例:

cpp 复制代码
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
    DMA_ClearFlag(DMA1_FLAG_TC1);

这里需要检查DMA1通道1转换完成的标志位,所以选择DMA1_FLAG_TC1参数。

转运完成之后,标志位会置1,所以需要加一个while循环,等待这个标志位==RESET,如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。

标志位置1之后,不要忘记清除标志位,这个标志位需要手动清除。调用 DMA_ClearFlag函数。

4.10.4 启动函数代码

代码示例:

cpp 复制代码
void AD_GetValue(void)//函数不需要参数和返回值
{
    ADC_SoftwareStartConvCmd(ADC1,ENABLE);
    DMA_Cmd(DMA1_Channel1,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel1,4);
    DMA_Cmd(DMA1_Channel1,ENABLE);
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
    DMA_ClearFlag(DMA1_FLAG_TC1);
}

这样当调用一下AD_GetValue函数,ADC开始转换,连续扫描4个通道,DMA也同步进行转运,AD转换结果依次放在AD_Value数组里。

五、DMA+AD多通道代码

代码示例:

cpp 复制代码
uint16_t AD_Value[4];

void AD_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//开启ADC1的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启PA0口的时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//开启DMA的时钟

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
    
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AIN;
    GPIO_InitStruct.GPIO_Pin= GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
    GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStruct);
    
    
    ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
    

    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//ADC的工作模式
    ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;//数据对齐 
    ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//外部触发转换选择
    ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;//连续/单次转换模式
    ADC_InitStructure.ADC_ScanConvMode=ENABLE;//扫描/非扫描转换模式
    ADC_InitStructure.ADC_NbrOfChannel=4;//通道数目
    ADC_Init(ADC1,&ADC_InitStructure);


    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//起始地址
    DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//数据宽度
    DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//是否自增
    DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
    DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//数据宽度
    DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
    DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
    DMA_InitStructure.DMA_BufferSize=4;//传输数量给4
    DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//传输模式
    DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;//选择是否存储到存储器
    DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
    DMA_Init(DMA1_Channel1,&DMA_InitStructure);
    DMA_Cmd(DMA1_Channel1,ENABLE);//使能

    ADC_DMACmd(ADC1,ENABLE);//开启DMA触发信号

    ADC_Cmd(ADC1,ENABLE);//开启ADC电源

    ADC_ResetCalibration(ADC1);//复位校准
    while(ADC_GetResetCalibrationStatus(ADC1)==SET);
    ADC_StartCalibration(ADC1);//开始校准
    while(ADC_GetCalibrationStatus(ADC1)==SET);//获取校准状态
}

void AD_GetValue(void)//函数不需要参数和返回值了
{
    ADC_SoftwareStartConvCmd(ADC1,ENABLE);
    DMA_Cmd(DMA1_Channel1,DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel1,4);
    DMA_Cmd(DMA1_Channel1,ENABLE);
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
    DMA_ClearFlag(DMA1_FLAG_TC1);
    
}

总结

以上就是今天要讲的内容,本文仅仅简单介绍了DMA+AD多通道的函数设计。

相关推荐
青牛科技-Allen42 分钟前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森3 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白3 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D3 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术6 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt7 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘7 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang7 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n9 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件
Despacito0o13 小时前
ESP32-s3摄像头驱动开发实战:从零搭建实时图像显示系统
人工智能·驱动开发·嵌入式硬件·音视频·嵌入式实时数据库