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多通道的函数设计。

相关推荐
夜间去看海37 分钟前
基于单片机的智能浇花系统
单片机·嵌入式硬件·智能浇花
VirtuousLiu1 小时前
LM74912-Q1用作电源开关
单片机·嵌入式硬件·ti·电源设计·lm74912·电源开关
打地基的小白1 小时前
软件I2C-基于江科大源码进行的原理解析和改造升级
stm32·单片机·嵌入式硬件·通信模式·i2c
朴人2 小时前
【从零开始实现stm32无刷电机FOC】【实践】【7.2/7 完整代码编写】
stm32·单片机·嵌入式硬件·foc
追梦少年时2 小时前
STM32中断——外部中断
stm32·单片机·嵌入式硬件
bai_lan_ya2 小时前
stm32定时器中断和外部中断
stm32·单片机·嵌入式硬件
玄奕子2 小时前
GPT对话知识库——在STM32的平台下,通过SPI读取和写入Flash的步骤。
stm32·单片机·gpt·嵌入式·嵌入式驱动
py.鸽鸽3 小时前
王者农药更新版
stm32
夜间去看海3 小时前
基于单片机的温度和烟雾检测
单片机·嵌入式硬件·温度烟雾