2.23 DMA数据转运&DMA+AD多通道代码

导语

在正式开始DMA代码之前,我们先花一段时间做个小实验,来验证一下存储器映像的内容。

看一下我们定义的数据,是不是真的存储在了相应的地址区间里。

uint8_t AOOFOO = 0X66; 
int main(void){
	OLED_Init();
	OLED_ShowString(1,6,"AOOFOO");//字符串
	while(1)
	{
		OLED_ShowHexNum(2,1,AOOFOO,2);//十六进制数
		OLED_ShowHexNum(3,1,(uint32_t)&AOOFOO,8);//十六进制数
	}
}

上面代码描述了一个变量存储在SRAM中的,我们看一下实际的效果图:

,可以看到是2000开头的SRAM地址区。

稍微修改一下代码,将AOOFOO变量变更为常量,那么AOOFOO的地址就会相应的变为Flash的地址区。

const uint8_t AOOFOO = 0X66; //const是C语言关键字,表示常量的意思,只能读不能写。
int main(void){
	OLED_Init();
	OLED_ShowString(1,6,"AOOFOO");//字符串
	while(1)
	{
		OLED_ShowHexNum(2,1,AOOFOO,2);//十六进制数
		OLED_ShowHexNum(3,1,(uint32_t)&AOOFOO,8);//十六进制数
	}
}

我们看一下实际的效果图:,可以看到地址是0800开头,即Flash的地址区。

const的规范使用可以减少SRAM的空间,比如你定义了字库,使用的const定义的,这样这些代码就属于是Flash地址里的,不会占用SRAM的空间。

初始化DMA

第一步,RCC开启DMA时钟。

第二步,调用DMA_Init()库函数,初始化里面的各个参数(外设和存储器站点的起始地址、数据宽度、地址是否自增和方向、传输计数器、是否需要自动重装、选择触发源)。

第三步,开关控制,DMA_Cmd,给指定的通道使能。

另外,如果使用的硬件触发源,要记得在对应的外设调用一下XXX_DMACmd()库函数,开启一下触发信号的输出。

如果你需要DMA的中断,那就调用DMA_ITConfig,开启中断输出,再在NVIC里配置相应的中断通道写中断函数就行了。

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

DMA库函数

经典库函数,不必过多介绍

void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx); //缺省配置
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);//初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);//结构体初始化
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);//使能

传输计数器的写入和读取

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);//DMA设置当前数据寄存器

这个函数就是写入传输计数器数据的。

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);//DMA获取当前数据寄存器

这个函数就是获取传输计数器数据的。

DMA中断相关

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);//获取标志位状态
void DMA_ClearFlag(uint32_t DMAy_FLAG);//清除标志位
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);//获取中断状态
void DMA_ClearITPendingBit(uint32_t DMAy_IT);//清除中断状态

DMA数据转运

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{
       MyDMA_Size = Size;
	//开启DMA1的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	//配置DMA初始化函数
	DMA_InitTypeDef DMA_InitStructure;	
	/*前六个成员是外设站点的起始地址、数据宽度、地址是否自增
	和存储器站点的起始地址、数据宽度、地址是否自增*/
	DMA_InitStructure.DMA_PeripheralBaseAddr=AddrA;//起始地址
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//字节大小
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//自增
	DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;//字节大小
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//自增
	//传输方向DST是存储器到外设,SRC是外设到存储器。
	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);
	
	//DMA使能
	DMA_Cmd(DMA1_Channel1,DISABLE);
}

//调用一次这个函数,就再一次启动DMA转运
void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size;);
	DMA_Cmd(DMA1_Channel1,ENABLE);
}

 

//逻辑函数代码
uint8_t DataA[] = {0x01,0x02,0x03,0x04};
uint8_t DataB[] = {0x00,0x00,0x00,0x00};

int main(void){
	OLED_Init();  
	MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
	
	OLED_ShowString(1,1,"DataA");//字符串
	OLED_ShowString(3,1,"DataB");//字符串
	OLED_ShowHexNum(1,8,(uint32_t)DataA,8);//十六进制数
	OLED_ShowHexNum(3,8,(uint32_t)DataB,8);//十六进制数
	
	OLED_ShowHexNum(2,1,DataA[0],2);//十六进制数
	OLED_ShowHexNum(2,4,DataA[1],2);//十六进制数
	OLED_ShowHexNum(2,7,DataA[2],2);//十六进制数
	OLED_ShowHexNum(2,10,DataA[3],2);//十六进制数
	OLED_ShowHexNum(4,1,DataB[0],2);//十六进制数
	OLED_ShowHexNum(4,4,DataB[1],2);//十六进制数
	OLED_ShowHexNum(4,7,DataB[2],2);//十六进制数
	OLED_ShowHexNum(4,10,DataB[3],2);//十六进制数
	
	while(1)
	{
		DataA[0]++;
		DataA[1]++;
		DataA[2]++;
		DataA[3]++;
		OLED_ShowHexNum(2,1,DataA[0],2);//十六进制数
		OLED_ShowHexNum(2,4,DataA[1],2);//十六进制数
		OLED_ShowHexNum(2,7,DataA[2],2);//十六进制数
		OLED_ShowHexNum(2,10,DataA[3],2);//十六进制数
		OLED_ShowHexNum(4,1,DataB[0],2);//十六进制数
		OLED_ShowHexNum(4,4,DataB[1],2);//十六进制数
		OLED_ShowHexNum(4,7,DataB[2],2);//十六进制数
		OLED_ShowHexNum(4,10,DataB[3],2);//十六进制数
		Delay_ms(1000);
		
		MyDMA_Transfer();
		OLED_ShowHexNum(2,1,DataA[0],2);//十六进制数
		OLED_ShowHexNum(2,4,DataA[1],2);//十六进制数
		OLED_ShowHexNum(2,7,DataA[2],2);//十六进制数
		OLED_ShowHexNum(2,10,DataA[3],2);//十六进制数
		OLED_ShowHexNum(4,1,DataB[0],2);//十六进制数
		OLED_ShowHexNum(4,4,DataB[1],2);//十六进制数
		OLED_ShowHexNum(4,7,DataB[2],2);//十六进制数
		OLED_ShowHexNum(4,10,DataB[3],2);//十六进制数
		Delay_ms(1000);
		
		
	}
}

通过上面的代码,可以直观的看到数据被DMA进行了转运。DataA数组中的数字自增后,转运至DataB中,如此反复。

DMA+AD多通道

ADC单次扫描+DMA单次转运:

uint16_t AD_Value[4];
void AD_Init()
{
	//开启ADC1、GPIOA、DMA1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//配置ADCCLK,6分频,ADCCLK = 72MHz/6= 12MHz
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	//配置GPIO,初始化PA0引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
	/*在AIN模式下,GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰。
		所以AIN模式是ADC的专属模式。*/
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 |GPIO_Pin_1 |GPIO_Pin_2 |GPIO_Pin_3;
	GPIO_Init(GPIOA,&GPIO_InitStructure); 
	
	//选择规则组的输入通道。采样时间是55.5个ADC周期。
	//这个函数的意思是,在规则组序列1的位置放入通道x,x经过外部传输得知。
	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
	ADC_InitTypeDef ADC_InitStruct;	
		//ADC工作模式,配置独立模式或者双ADC模式。
	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent; 
		//数据对齐,选择右对齐。
	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;	
		//外部触发转换选择,就是触发控制的触发源。
		//选择不是使用外部触发,就是软件触发的意思。
	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
		//连续转换模式,选择连续转换还是单次转换。
	ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
		//扫描转换模式,选择扫描模式还是非扫描模式。
	ADC_InitStruct.ADC_ScanConvMode=ENABLE;
	    //通道数目,指定在扫描模式下,总共会用几个通道。
		/*其实这个参数只在扫描模式下才需要用,
		如果是非扫描模式,那整个列表只有第一个序列有用*/
	ADC_InitStruct.ADC_NbrOfChannel=4;
	
	ADC_Init(ADC1,&ADC_InitStruct);
	

	//配置DMA初始化函数
	DMA_InitTypeDef DMA_InitStructure;	
	/*前六个成员是外设站点的起始地址、数据宽度、地址是否自增
	和存储器站点的起始地址、数据宽度、地址是否自增*/
	DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//ADC寄存器地址
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//半字大小
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//不自增
	DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//半字大小
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//自增
	//传输方向DST是存储器到外设,SRC是外设到存储器。
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
	//缓冲区大小(其实就是传输计数器)
	DMA_InitStructure.DMA_BufferSize=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使能
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	//开启ADC到DMA的输出
	ADC_DMACmd(ADC1,ENABLE);
	
	//开启ADC电源
	ADC_Cmd(ADC1,ENABLE);
	
	//ADC校准
	ADC_ResetCalibration(ADC1);//复位校准
	/*获取复位校准状态,所以如果要等待复位完成的话,
	还需要加个while循环,如果没有校准完成,就在这个while空循环里一直等待。*/
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);//开启校准
	while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准是否完成了。

	
}
void AD_GetValue(void)
{
	//因为DMA也是正常的单次模式,所以在触发ADC之前,需要再重新写入一下传输计数器
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);//软件触发的函数,ADC开启进行转换。
	//因为现在ADC还是单次模式,还需要软件触发一下ADC的开始。
	DMA_Cmd(DMA1_Channel1,ENABLE);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	//最后,等待ADC转换和DMA转运完成,这里因为转运总是在转换之后的,所以我们只需要等待DMA转运完成。
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	//DMA需要手动清除标志位
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

 

//逻辑函数代码
extern uint16_t AD_Value[4];
int main(void){
	OLED_Init();
	AD_Init();
	OLED_ShowString(1,1,"AD0:");//字符串
	OLED_ShowString(2,1,"AD1:");//字符串
	OLED_ShowString(3,1,"AD2:");//字符串
	OLED_ShowString(4,1,"AD3:");//字符串
	while(1)
	{
		AD_GetValue();
		
		OLED_ShowNum(1,5,AD_Value[0],4); //十进制无符号数
		OLED_ShowNum(2,5,AD_Value[1],4); //十进制无符号数
		OLED_ShowNum(3,5,AD_Value[2],4); //十进制无符号数
		OLED_ShowNum(4,5,AD_Value[3],4); //十进制无符号数
		Delay_ms(100);
	}
}

 

ADC连续扫描+DMA循环转运模式:

uint16_t AD_Value[4];
void AD_Init()
{
	//开启ADC1、GPIOA、DMA1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//配置ADCCLK,6分频,ADCCLK = 72MHz/6= 12MHz
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	//配置GPIO,初始化PA0引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟输入
	/*在AIN模式下,GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰。
		所以AIN模式是ADC的专属模式。*/
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 |GPIO_Pin_1 |GPIO_Pin_2 |GPIO_Pin_3;
	GPIO_Init(GPIOA,&GPIO_InitStructure); 
	
	//选择规则组的输入通道。采样时间是55.5个ADC周期。
	//这个函数的意思是,在规则组序列1的位置放入通道x,x经过外部传输得知。
	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
	ADC_InitTypeDef ADC_InitStruct;	
		//ADC工作模式,配置独立模式或者双ADC模式。
	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent; 
		//数据对齐,选择右对齐。
	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;	
		//外部触发转换选择,就是触发控制的触发源。
		//选择不是使用外部触发,就是软件触发的意思。
	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
		//连续转换模式,选择连续转换还是单次转换。
	ADC_InitStruct.ADC_ContinuousConvMode=ENABLE;
		//扫描转换模式,选择扫描模式还是非扫描模式。
	ADC_InitStruct.ADC_ScanConvMode=ENABLE;
	    //通道数目,指定在扫描模式下,总共会用几个通道。
		/*其实这个参数只在扫描模式下才需要用,
		如果是非扫描模式,那整个列表只有第一个序列有用*/
	ADC_InitStruct.ADC_NbrOfChannel=4;
	
	ADC_Init(ADC1,&ADC_InitStruct);
	

	//配置DMA初始化函数
	DMA_InitTypeDef DMA_InitStructure;	
	/*前六个成员是外设站点的起始地址、数据宽度、地址是否自增
	和存储器站点的起始地址、数据宽度、地址是否自增*/
	DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//ADC寄存器地址
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//半字大小
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//不自增
	DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)AD_Value;//起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//半字大小
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//自增
	//传输方向DST是存储器到外设,SRC是外设到存储器。
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
	//缓冲区大小(其实就是传输计数器)
	DMA_InitStructure.DMA_BufferSize=4;
	//传输模式(其实就是是否使用自动重装)
	DMA_InitStructure.DMA_Mode=DMA_Mode_Circular; 
	//选择硬件触发还是软件触发,不使用软件触发	
	DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;
	//优先级,按照参数要求给个优先级就行了
	DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);
	
	//DMA使能
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	//开启ADC到DMA的输出
	ADC_DMACmd(ADC1,ENABLE);
	
	//开启ADC电源
	ADC_Cmd(ADC1,ENABLE);
	
	//ADC校准
	ADC_ResetCalibration(ADC1);//复位校准
	/*获取复位校准状态,所以如果要等待复位完成的话,
	还需要加个while循环,如果没有校准完成,就在这个while空循环里一直等待。*/
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);//开启校准
	while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准是否完成了。
	
	/*ADC触发 : 当ADC触发之后,ADC连续转换,DMA循环转运。两者一直在工作,始终把最新的转换
	  结果刷新到SRAM数组中。*/
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
}

 

//逻辑函数代码
extern uint16_t AD_Value[4];
int main(void){
	OLED_Init();
	AD_Init();
	OLED_ShowString(1,1,"AD0:");//字符串
	OLED_ShowString(2,1,"AD1:");//字符串
	OLED_ShowString(3,1,"AD2:");//字符串
	OLED_ShowString(4,1,"AD3:");//字符串
	while(1)
	{
		OLED_ShowNum(1,5,AD_Value[0],4); //十进制无符号数
		OLED_ShowNum(2,5,AD_Value[1],4); //十进制无符号数
		OLED_ShowNum(3,5,AD_Value[2],4); //十进制无符号数
		OLED_ShowNum(4,5,AD_Value[3],4); //十进制无符号数
		Delay_ms(100);
	}
}