STM32有关 多通道ADC & DMA联合使用(小白向)

关乎STM32,在使用官方库的状况下,你想要的各类功能的配置,无非就是你对于相应外设的每一个参数的设置git

库函数介绍

STM32的库函数,相关的一些参数,参数有效值,均可以在相关库文件** .c / .h **看到详细解释,就以ADC的初始化函数 ADC_Init 为例web

/ **
   * @brief根据ADC_InitStruct中的指定参数
   * 初始化ADCx外设。  
   * @param ADCx:其中x能够是123以选择ADC外设。
   * @param ADC_InitStruct:指向ADC_InitTypeDef结构的指针,该结构包含  
   * 指定ADC外设的配置信息。  *
   * @retval无  * 
   * / 
ADC_Init ( ADC_TypeDef * ADCx , ADC_InitTypeDef * ADC_InitStruct ){
   
  uint32_t tmpreg1 = 0 ;   
  uint8_t tmpreg2 = 0 ;
  / *检查参数* / 
  assert_param (IS_ADC_ALL_PERIPH ( ADCx ));
  assert_param (IS_ADC_MODE ( ADC_InitStruct- > ADC_Mode )); 
  assert_param (IS_FUNCTIONAL_STATE ( ADC_InitStruct- > ADC_ScanConvMode ));   	
  assert_param (IS_FUNCTIONAL_STATE ( ADC_InitStruct- > ADC_ContinuousConvMode )); 
  assert_param (IS_ADC_EXT_TRIG (ADC_InitStruct- > ADC_ExternalTrigConv ));    
  assert_param (IS_ADC_DATA_ALIGN ( ADC_InitStruct- > ADC_DataAlign )); 
  assert_param (IS_ADC_REGULAR_LENGTH ( ADC_InitStruct- > ADC_NbrOfChannel )); 
  / * ---------------------------- ADCx CR1配置----------------- * / 
  / *获取使用ADCx CR1值* /
  tmpreg1 = ADCx - > CR1 ;
  / *清除DUALMOD和SCAN位* /
  tmpreg1 &==CR1_CLEAR_Mask ; 
  / *配置ADCx:双模式和扫描转换模式* /
  / *根据ADC_Mode值设置DUALMOD位* / 
  / *根据ADC_ScanConvMode值设置SCAN位* /   
  tmpreg1 | = ( uint32_t )( ADC_InitStruct- > ADC_Mode | (( uint32_t ) ADC_InitStruct- > ADC_ScanConvMode << 8 )); 
  / *写入ADCx CR1 * /   
  ADCx - > CR1 = tmpreg1 ;
  / * ---------------------------- ADCx CR2配置----------------- * / 
  / *获取使用ADCx CR2值* /   
  tmpreg1 = ADCx - > CR2 ; 
  / *清除CONT,ALIGN和EXTSEL位* /   
  tmpreg1 &= CR2_CLEAR_Mask ; 
  / *配置ADCx:外部触发事件和连续转换模式* / 
  / *根据ADC_DataAlign值设置ALIGN位* / 
  / *根据ADC_ExternalTrigConv值设置EXTSEL位* / 
  / *根据ADC_ContinuousConvMode值设置CONT位* /   
  tmpreg1 | = ( uint32_t )( ADC_InitStruct- > ADC_DataAlign | ADC_InitStruct- > ADC_ExternalTrigConv | (( uint32_t ) ADC_InitStruct- > ADC_ContinuousConvMode << 1 )); 
  / *写入ADCx CR2 * /   
  ADCx - > CR2 = tmpreg1 ; 
  / * ---------------------------- ADCx SQR1配置----------------- * / 
  / *获取使用ADCx SQR1值* /   
  tmpreg1 = ADCx - > SQR1 ; 
  / *清除L位* /   
  tmpreg1 &= SQR1_CLEAR_Mask ;
  /* Configure ADCx: regular channel sequence length */
  /* Set L bits according to ADC_NbrOfChannel value */
  tmpreg2 |= (uint8_t) (ADC_InitStruct->ADC_NbrOfChannel - (uint8_t)1);
  tmpreg1 | =  ( uint32_t ) tmpreg2 <<  20 ; 
  / *写入ADCx SQR1 * /   
  ADCx - > SQR1 = tmpreg1 ; 
}

其中,有不少代码,可是,真正配置能用到的无非就是 assert_param 这个函数的那些,至于这个函数是什么,也不重要,你只须要了解到里面的参数就是你须要配置的那些功能。数组

在官方库函数,前面有几句注释
什么意思呢?
这个函数有两个参数,其中ADCx的意思是,你须要配置哪一个ADC,好比:ADC1 或者 ADC2 ADC3;而ADC_InitStruct的意思是,你须要将外设的功能定义以ADC_InitTypeDef类型的结构体指针的方式传递过去。svg

这就不得不提到C语言的精髓, 结构体 & 指针!函数

至于结构体,我本身的理解就是,他就是一个特殊的数组,只不过这个数组的成员之间的数据长度可能不太同样长,可是用法还有一些其余的东西,彻底和数组相似,具体的解释,我会从新开一个博客来具体介绍,这里就先谈到这里。ui

简而言之,就是,你须要先定义一个保存不少数据的数据类型,来将你的对于这个ADC的使用功能告诉给这函数,而后,他来一条一条分析你这个数据类型的每一个数据,固然,你也能够把这些参数一个一个的加在库函数的输入参数里面,好比说ADC_Mode,ADC_ScanConvMode,ADC_ContinuousConvMode,ADC_ExternalTrigConv,ADC_DataAlign,ADC_NbrOfChanneles5

assert_param(IS_ADC_MODE(ADC_InitStruct->ADC_Mode));
assert_param(IS_FUNCTIONAL_STATE(ADC_InitStruct->ADC_ScanConvMode));
assert_param(IS_FUNCTIONAL_STATE(ADC_InitStruct->ADC_ContinuousConvMode));
assert_param(IS_ADC_EXT_TRIG(ADC_InitStruct->ADC_ExternalTrigConv));
assert_param(IS_ADC_DATA_ALIGN(ADC_InitStruct->ADC_DataAlign));
assert_param(IS_ADC_REGULAR_LENGTH(ADC_InitStruct->ADC_NbrOfChannel));spa

至于说这个保存不少数据的数据类型,在库里面,人家也很贴心的给你准备了(.h文件.net

typedef struct
{
  uint32_t ADC_Mode;                      /*!< Configures the ADC to operate in independent or dual mode. This parameter can be a value of @ref ADC_mode */

  FunctionalState ADC_ScanConvMode;       /*!< Specifies whether the conversion is performed in Scan (multichannels) or Single (one channel) mode. This parameter can be set to ENABLE or DISABLE */

  FunctionalState ADC_ContinuousConvMode; /*!< Specifies whether the conversion is performed in Continuous or Single mode. This parameter can be set to ENABLE or DISABLE. */

  uint32_t ADC_ExternalTrigConv;          /*!< Defines the external trigger used to start the analog to digital conversion of regular channels. This parameter can be a value of @ref ADC_external_trigger_sources_for_regular_channels_conversion */

  uint32_t ADC_DataAlign;                 /*!< Specifies whether the ADC data alignment is left or right. This parameter can be a value of @ref ADC_data_align */

  uint8_t ADC_NbrOfChannel;               /*!< Specifies the number of ADC channels that will be converted using the sequencer for regular channel group. This parameter must range from 1 to 16. */
}ADC_InitTypeDef;

因此,一切的一切,在库函数你均可以找到相关的说明,接下来,你就会问了,那这些参数,我要怎么配置,才会让程序来识别我想要的功能,Look down:
这时候,你只须要在.h文件里面继续往下翻就会看到指针

#define ADC_Mode_Independent ((uint32_t)0x00000000)
#define ADC_Mode_RegInjecSimult ((uint32_t)0x00010000)
#define ADC_Mode_RegSimult_AlterTrig ((uint32_t)0x00020000)
#define ADC_Mode_InjecSimult_FastInterl ((uint32_t)0x00030000)
#define ADC_Mode_InjecSimult_SlowInterl ((uint32_t)0x00040000)
#define ADC_Mode_InjecSimult ((uint32_t)0x00050000)
#define ADC_Mode_RegSimult ((uint32_t)0x00060000)
#define ADC_Mode_FastInterl ((uint32_t)0x00070000)
#define ADC_Mode_SlowInterl ((uint32_t)0x00080000)
#define ADC_Mode_AlterTrig((uint32_t)0x00090000)
……

OK,介绍了这么多,我们开始步入正题:

初始化函数配置

首先,你得知道你要使用的是哪一个ADC引脚?至于引脚配置,在官方的芯片手册中第三节的引脚功能定义中找到。

在这里插入图片描述图中能够看到,ADC12_IN10在PC0引脚,ADC12_IN11在PC1引脚.。。。

其次,将你须要使用的引脚进行初始化,记得把引脚配置为 模拟输入(GPIO_Mode_AIN)

以ADC12_IN10 / ADC12_IN11 / ADC12_IN12 / ADC12_IN13为例

void GPIOC_Init(){   //GPIO引脚初始化
    GPIO_InitTypeDef GPIO_InitStructure; 

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);  //使能外设时钟
	
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;			//端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  		//输出模式配置,模拟输入
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//IO口速度为50MHz
    GPIO_Init(GPIOC, &GPIO_InitStructure);	 		//根据设定参数初始化GPIOC
}

而后,就是对你所须要的ADC功能的配置了

Void ADC1_Init(){   //ADC1初始化
    ADC_InitTypeDef ADC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  //使能外设时钟

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);    //设置ADC的分频因子,APB2的6分频 72M/6 = 12M,若是ADC频率超过12MHz,可能致使采样值偏差
    
    ADC_DeInit(ADC1); 	                //设置ADC1为默认值

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//独立模式,此模式表示只是单独使用ADC1,不与ADC2进行联合使用
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;		//开启扫描 扫描模式下,才能够连续采集信号
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;	//连续转换模式
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//软件触发,须要调用库函数才可使用 
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//ADC数据右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 4;			//顺序进行转换的ADC通道的数目
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5 );	//ADC规则通道配置
    ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55Cycles5 );	//ADC规则通道配置
    ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_55Cycles5 );	//ADC规则通道配置
    ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 4, ADC_SampleTime_55Cycles5 );	//ADC规则通道配置

    ADC_Cmd(ADC1, ENABLE);			//使能ADC1
    ADC_ResetCalibration(ADC1);			//使能复位校准
    while(ADC_GetResetCalibrationStatus(ADC1));		//等待ADC校准结束
    ADC_StartCalibration(ADC1);			//开启AD校准
    while(ADC_GetCalibrationStatus(ADC1));		//等待AD校准结束
    
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);    //进行一次采集
	/* 等待转换结束 */
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
}

至于具体每一个参数的做用,能够参考这个博文:STM32之ADC配置,ADC_Mode模式理解

OK,完成了ADC配置,你还须要建立一个数组,以保证ADC采集到的数据能够有存储的地方

/* 转换值 */
unsigned int adc_data[4];

这个建议作一个全局变量,以方便被其余文件调取使用

你已经有了采集的接口,存放采集数据的地方,最后,你要作的就是为他们二者之间创建一个通道,这就是DMA存在的意义

STM32共有两个DMA控制器,其中DMA1共有7个通道,DMA2有5个通道,在参考手册中DMA章节有具体说明
在这里插入图片描述在这里插入图片描述
此次咱们使用的是ADC1的传输,因此,须要选择DMA1的通道1

void DMA1_Init(void)
{
	DMA_InitTypeDef DMA_InitStruct;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);   //使能DMA外设时钟
	
	DMA_DeInit(DMA1_Channel1);   /将DMA1通道1重设为默认值

    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设基地址
    DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&adc_data;//存储器基地址
    DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;   //数据传输方向 外设到存储器
    DMA_InitStruct.DMA_BufferSize = 4;    //通道传输数据量
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   //不开启外设增量模式
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;   //开启储存器地址增量模式
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;       //外设数据长度(16位)
    DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_HalfWord;       //存储器数据长度
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;          //传输是否循环 不循环
    DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;     //DMA优先级 中等
    DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;      //是否存储器到存储器方式 不是
    DMA_Init(DMA_CHx, &DMA_InitStruct);
	
	DMA_Cmd(DMA1_Channel1, ENABLE);    //使能DMA1的通道1传输
}

最后的最后,就是搞一个main函数,将各类初始化集合在一块儿使用

int main(void)
{
	GPIOC_Init();  //GPIO配置
	DMA1_Init();   //DMA1配置
	ADC1_Init();   //ADC1配置
	while(1)
	{
		……
	}
}