今天我们接着来学习stm32,没错,今天我们又带来两个紧密联系的实验,分别是ADC实验和DMA传输实验,这个两个实验的内容实用性也是很强的,例如可以使用在遥控手柄这些摇杆值的获取和传输上面。接下来我们一起来认识一下什么是ADC和DMA传输,它们的使用有什么意义?带着问题,我们往下看。
实验目的
在ADC实验中,我们将学习使用stm32的ADC功能,通过ADC通道采样外部电压值。在DMA传输实验中我们将学习实验stm32的DMA传输功能,实现串口数据传输。
实验内容
(1)ADC实验
ADC简介
首先我们来认识一下什么是ADC,ADC英文全称是Analog to Digital Converter,中文是模拟数字转换器,简单来说就是可以将模拟电子信号转变为数字电子信号的设备。电压表大家都用过了吧?没错,ADC就可以类比为一个电压表,可以获得我们想要得到的电压的数值。由于ADC的功能太多,在本实验中只能讲到其中的冰山一角,剩下的需要小伙伴们自己去探索。在实验中我们学习的是使用ADC的方法,学会了方法就可以一步步地去慢慢深入探索其中更多的功能了。
我们的stm32上面有1~3个ADC,不同的系列数目不同。我使用的是stm32f1zte6,所以有 3 个,它们既可以单独使用,又可以使用双重模式,使用双重模式可以提高采样率。同时在使用ADC的时候要注意的是,stm32的ADC最大传输速率为 1MHz,也就是转换周期为 1us(在ADCCLK = 14M,采样周期为 1.5 个ADC时钟下得到的),所以不要让ADC的时钟(ADC的时钟由PCLK2分频得到)超过 14M,否则将导致结果的精确度下降。此外还要注意输入的电压范围,ADC的输入范围在 0~3.3V 之间,如果电压超过这个范围有烧坏ADC的可能。
ADC通道组
STM32将ADC的转换分为 2 个通道组:规则通道组和注入通道组。规则通道相当于你正常运行的程序,而注入通道呢,就相当于中断。在你程序正常执行的时候,中断是可以打断你的执行的。同这个类似,注入通道的转换可以打断规则通道的转换,在注入通道被转换完成之后,规则通道才得以继续转换。STM32 其 ADC 的规则通道组最多包含 16 个转换,而注入通道组最多包含 4 个通道。
ADC转换模式
stm32的ADC有两种转换模式,一种是单次转换,一种是连续转换。顾名思义,单次转换就是只转换一次,而连续转换是转换完一次会自动进行下一次转换。
介绍完我们的ADC,下面我们就来看看怎么在程序中配置使用我们的ADC。
ADC配置
第一步必然就是开启时钟,初始化相应的IO口。先用RCC_APB2PeriphClockCmd()函数使能相应的时钟,再参考下面stm32的ADC通道与GPIO的对应表,使用GPIO_Init()函数初始化要使用的IO口就可以了。
在本实验中我们要使用的是ADC1的通道1,所以我们要使能ADC1的时钟和GPIOA的时钟,并配置端口PA1。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
第二步是复位ADC,设置ADC时钟分频因子。复位ADC使用的是函数ADC_DeInit(),该函数的参数是对应的ADC。设置ADC时钟分频因子所用的函数是RCC_ADCCLKConfig(),该函数的参数是分频因子2、4、6、8,在有文件stm32f10x_rcc.h里有定义。
#define RCC_PCLK2_Div2 ((uint32_t)0x00000000)
#define RCC_PCLK2_Div4 ((uint32_t)0x00004000)
#define RCC_PCLK2_Div6 ((uint32_t)0x00008000)
#define RCC_PCLK2_Div8 ((uint32_t)0x0000C000)
由于ADC时钟的最大频率最好不超过14M,所以我选择的是分频因子6(RCC_PCLK2_Div6),最终获得的时钟是 72M / 6 = 12M,没有超过14M且是这四钟分频下所能取到的最大的频率。
ADC_DeInit(ADC1);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 (72M / 6 = 12M),ADC时钟不能超14M
第三部就是设置ADC的初始化参数了。在这一步我们使用了ADC的初始化函数ADC_Init(),下面我们来看看这个函数使用到的结构体ADC_InitTypeDef组成。
typedef struct
{
uint32_t ADC_Mode;
FunctionalState ADC_ScanConvMode;
FunctionalState ADC_ContinuousConvMode;
uint32_t ADC_ExternalTrigConv;
uint32_t ADC_DataAlign;
uint8_t ADC_NbrOfChannel;
}ADC_InitTypeDef;
第一个参数ADC_Mode是配置ADC工作模式的,ADC的模式非常多。下面是ADC控制寄存器1(ADC_CR1)设置ADC模式的 [19:16] 位相关配置参数表。在本实验我们选择的是独立模式,即ADC_Mode_Independent。
表中按顺序对应模式的参数在头文件stm32f10x_adc.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)
第二个参数ADC_ScanConvMode是用来设置是否开启扫描模式。因为我们使用的是单次转换模式,所以不用开启扫描模式。
第三个参数ADC_ContinuousConvMode是用来设置是否开启连续转换模式的,因为在实验在我们使用单次转换模式,所以也不需要开启。
第四个参数ADC_ExternalTrigConv是用来设置启动规则转换组转换的外部事件,启动规则转换组的外部事件也是非常多的,而且ADC1、ADC2这两个ADC和ADC3的规则组转换启动的外部事件还不完全相同。这些启动规则转换组转换的外部事件的相应定义在头文件stm32f10x_adc.h里。我们选择的是软件触发,即ADC_ExternalTrigConv_None。
//只可以在ADC1、ADC2设置
#define ADC_ExternalTrigConv_T1_CC1 ((uint32_t)0x00000000)
#define ADC_ExternalTrigConv_T1_CC2 ((uint32_t)0x00020000)
#define ADC_ExternalTrigConv_T2_CC2 ((uint32_t)0x00060000)
#define ADC_ExternalTrigConv_T3_TRGO ((uint32_t)0x00080000)
#define ADC_ExternalTrigConv_T4_CC4 ((uint32_t)0x000A0000)
#define ADC_ExternalTrigConv_Ext_IT11_TIM8_TRGO ((uint32_t)0x000C0000)
//在ADC1、ADC2、ADC3都可以设置
#define ADC_ExternalTrigConv_T1_CC3 ((uint32_t)0x00040000)
#define ADC_ExternalTrigConv_None ((uint32_t)0x000E0000)
//只可以在ADC3设置
#define ADC_ExternalTrigConv_T3_CC1 ((uint32_t)0x00000000)
#define ADC_ExternalTrigConv_T2_CC3 ((uint32_t)0x00020000)
#define ADC_ExternalTrigConv_T8_CC1 ((uint32_t)0x00060000)
#define ADC_ExternalTrigConv_T8_TRGO ((uint32_t)0x00080000)
#define ADC_ExternalTrigConv_T5_CC1 ((uint32_t)0x000A0000)
#define ADC_ExternalTrigConv_T5_CC3 ((uint32_t)0x000C0000)
第五个参数ADC_DataAlign是用来设置ADC数据对齐方式是左对齐还是右对齐,我们在这里选择的是右对齐。左对齐和右对齐的在头文件stm32f10x_adc.h定义如下。
#define ADC_DataAlign_Right ((uint32_t)0x00000000)
#define ADC_DataAlign_Left ((uint32_t)0x00000800)
第六个参数ADC_NbrOfChannel是用来设置规则序列的长度的,因为我们使用的是单次转换,所以设置为1就行了。
下面是本实验ADC相关参数的配置
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
第四步是使能ADC并校准。使能ADC使用的是函数ADC_Cmd()。校准ADC使用的函数有两个,分别是使能复位校准函数ADC_ResetCalibration()和开启ADC校准函数ADC_StartCalibration()。每次进行校准之后需要等待校准完成,这里是通过使用ADC_GetResetCalibrationStatus()函数和ADC_GetCalibrationStatus()函数来获取校准状态来判断是否校准是否结束
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
到这里,我们的ADC初始化配置已经就完成了,剩下的就还有获取ADC值的部分了。
ADC值获取
要获取ADC值我们需要做的首先是设置规则序列 1 里面的通道、采样顺序、以及通道的采样周期,然后启动 ADC 转换,在转换结束后就可以读取ADC的转换值了。这里设置规则序列的通道、采样顺序和采样周期的函数是ADC_RegularChannelConfig()。
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)
该函数一共需要四个参数,第一个参数ADCx是选择ADC,我们使用的是ADC1;第二个参数ADC_Channel是选择要采样的模拟通道,这里我们通过一个形参channel输入;第三个参数Rank是设置ADC的采样序列号,因为实验中只有一个转换,所以是规则序列的第一个转换,设置为1;第四个参数ADC_SampleTime是设置采样周期,因为我们实验中只有一个转换,所以采样周期可以设置为长一点,我们选择采样周期为239.5,设置其它的也没问题。
然后是使能ADC的软件转换启动功能,这里使用的函数是ADC_SoftwareStartConvCmd()。
开启转换之后,我们就可以获取ADC转换的结果数据了,获取ADC转换结果数据的使用的函数是ADC_GetConversionValue()。
同时,在AD转换中,我们要判断AD转换是否完成,我们可以使用函数ADC_GetFlagStatus()来获取ADC转换的状态信息。
u16 Get_Adc(u8 channel)
{
ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); //等待转换结束
return ADC_GetConversionValue(ADC1);
}
最后,我们还可以写一个用于提高精确度的函数。具体的实现方法就是通过多次获取ADC的转换值,然后取平均值从而提高精确度,这个函数没什么好说的,大家直接看代码就行了。
u16 Get_Adc_Average(u8 channel,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(channel);
delay_ms(5);
}
return temp_val/times;
}
至此,我们的ADC实验就算是完成了。
下面我们继续来看一看与ADC实验在实际使用中紧密联系的实验——DMA传输实验。
(2)DMA传输实验
DMA简介
在这之前,我们要先问一个问题,DMA传输是什么?为什么要学DMA传输,它有什么牛逼的地方?DMA的英文全称是Direct Memory Access,即是直接存储器访问。DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。有的小伙伴也行还不知道这有多牛,不就是数据传输吗,有什么特别的吗?要知道我们的系统运作的核心是CPU,CPU是很忙的,它无时无刻都要处理大量的任务,所以CPU是很重要的资源。而DMA传输可以取代CPU数据转移的工作(尤其是大量的数据转移,这很占用CPU资源),传输速度还更快,而且腾出的这部分CPU资源还可以去处理其他的任务。现在应该意识到DMA数据传输有多牛了吧。吹完DMA传输的牛逼,接下来我们一起来看看这个DMA传输是怎么使用的。
stm32的外设 DMA 请求,可以通过设置相应的外设寄存器中的控制位,被独立地开启或关闭,下面是DMA1和DMA2各通道对应各外设一览表。
这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1),这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也是类似的。
而在本实验,我们选择使用的是串口1的DMA传输,所以要使用的是DMA1的通道4。
DMA配置
老规矩,第一步还是使能相应的时钟。我们使用的是DMA1的通道4,所以使能DMA1时钟。
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
第二步就是初始化DMA通道参数。在这一步我们使用的函数是DMA初始化函数DMA_Init(),我们来看看该函数里使用到的结构体DMA_InitTypeDef的组成。
typedef struct
{
uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_MemoryBaseAddr;
uint32_t DMA_DIR;
uint32_t DMA_BufferSize;
uint32_t DMA_PeripheralInc;
uint32_t DMA_MemoryInc;
uint32_t DMA_PeripheralDataSize;
uint32_t DMA_MemoryDataSize;
uint32_t DMA_Mode;
uint32_t DMA_Priority;
uint32_t DMA_M2M;
}DMA_InitTypeDef;
可以看到,这个结构体的组成变量非常多,我们一个个地来分析。首先来拿看第一个成员变量DMA_PeripheralBaseAddr,是用来设置DMA传输的外设基地址; 第二个成员变量DMA_MemoryBaseAddr是用来设置DMA传输的内存基地址;第三个成员变量DMA_DIR是用来设置数据传输方向的,即方向是从内存到外设还是从外设到内存,我们选择内存到外设方向;第四个成员变量DMA_BufferSize是用来设置一次传输数据量的大小;第五个成员变量DMA_PeripheralInc是用来设置传输数据的时候外设地址的不变的还是递增,因为我们一直对一个外设发送数据,我们选择外设地址不变;第六个成员变量DMA_MemoryInc是用来设置传输数据的时候内存地址是否递增,和第五个成员变量类似,这里我们是将内存中连续存储单元的数据发送到串口,所以内存地址是递增的;第七个成员变量DMA_PeripheralDataSize是用来设置外设的数据传输长度是为字节传输(8bit)、半字传输(16bit)还是字传输(32bit);第八个成员变量DMA_MemoryDataSize是用来设置内存的数据传输长度的,和第七个成员变量类似,我们的外设数据传输长度和内存数据传输长度都是选的字节传输;第九个成员变量DMA_Mode是用来设置DMA的数据传输模式是为循环采集还是正常采集,在实验中选择的是正常采集;第十个成员变量DMA_Priority是用来设置DMA通道优先级的,有低、中、高、超高这四个级别,实验中我们对优先级没有要求,所以选择中等级别就行;第十一个成员变量DMA_M2M是用来设置是否为存储器到存储器传输,因为我们的数据传输方向是内存到外设,所以不使能。在配置DMA前我们还可以使用DMA_DeInit()函数来重设置。
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar;
DMA_InitStructure.DMA_MemoryBaseAddr = cmar;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = cndtr;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA_CHx, &DMA_InitStructure);
第三步是使能串口DMA发送。这里使用到的函数是USART_DMACmd(),这一步我们是放在主函数里面,在使用串口DMA发送前从使能的。
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
如果是要使能串口DMA接收,就把USART_DMAReq_Tx换成USART_DMAReq_Rx。
第四步使能DMA通道,开启传输。我们使能DMA通道的函数是DMA_Cmd(),因为我们想要实现显示DMA的传输进度,但是每次传输完后我们的DMA通道的缓存大小(DMA_BufferSize设置的一次传输的数据量)会清零,所以在使能DMA通道前我们还要重新设置这个变量,设置这个变量的函数是DMA_SetCurrDataCounter()。
void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx)
{
DMA_Cmd(DMA_CHx, DISABLE );
DMA_SetCurrDataCounter(DMA_CHx,DMA1_MEM_LEN);
DMA_Cmd(DMA_CHx, ENABLE);
}
这里的DMA1_MEM_LEN是前面我们保存在另一个全局变量里的一次传输的数据量大小。
最后我们再来介绍一下另外两个我们实验用到的也是很重要的函数,一个是查询DMA通道状态的函数DMA_GetFlagStatus(),我们查询DMA1通道4是否传输完成方式如下。
DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET
还有一个函数就是获取当前剩余数据量大小的函数DMA_GetCurrDataCounter(),下面是我们获取DMA1通道4剩余数据量大小的方式。文章来源:https://uudwc.com/A/9d6RB
DMA_GetCurrDataCounter(DMA1_Channel4);
到这里,我们这一期重点的内容已经讲完了,在下一期我们来将一个这两个实验在实际的应用中的联系使用。文章来源地址https://uudwc.com/A/9d6RB