STM32F103(二十一)DMA(超详细的~)

Posted 独独白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32F103(二十一)DMA(超详细的~)相关的知识,希望对你有一定的参考价值。

学习板:STM32F103ZET6

一、DMA

1、简介

DMA,全称为:Direct Memory Access,即直接存储器访问,DMA 传输将数据从一个地址空间复制到另外一个地址空间。这个从一个地址空间到另外一个地址空间可以是:外设到存储器、存储器到存储器、存储器到外设。

通过DMA,实现一个地址空间到另一个地址空间的高速数据传输,无需CPU控制传输,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,大大提高了CPU的效率。

2、STM32的DMA数量

STM32有两个DMA控制器,其中DMA1有7个通道;DMA2有5个通道,且DMA2只存在于大容量32中。

DMA1的通道:

DMA2的通道:

二、DMA相关寄存器

1、DMA中断状态寄存器(DMA_ISR)


该寄存器分别控制1~7DMA通道,其中DMA2只有五个通道,此时位20 ~位27无效。

寄存器操作:

	DMA1->ISR=
	DMA2->ISR=

详细总结一下位0~位3,其它的类似。

位0:

中断标志位,即在通道x产生了TE、HT或TC事件时该位硬件置1。

TE:传输错误中断事件
HT:传输过半中断事件
TC:传输完成中断事件

如使能HT中断后,如果传输过半,则会发生HT事件,本寄存器位0(通道1)或位4(通道2)或位8(通道3)或位12(通道4)或位16(通道5)或位20(通道6)或位24(通道7)硬件置1,表示发生中断事件。

位1~位3:

位1:传输完成标志
位2:半传输标志
位3:传输错误标志

这3位是传输状态标志位,如传输完成,则位1置1(通道1而言,其他通道类似);传输过半,则位2硬件置1;传输错误则位3硬件置1.

这些位都需要软件清零的。操作是:在IFCR寄存器对应位写1,清除ISR寄存器的状态标志。

2、DMA中断标志清除寄存器(DMA_IFCR)

该寄存器也是被分为1~7通道配置。与前面的ISR寄存器一一对应。


以通道1为例,说明。其他通道类似。

位0:清除GIF全局中断标志:TEIF、HTIF和TCIF
位1:清除传输完成标志
位2:清除传输过半标志
位3:清除传输错误标志

3、DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)+寄存器使用代码

位0:

软件置1开启通道。

该寄存器怎么使用呢?

可以看到前面俩个寄存器在DMA_TypeDef结构体中

可以看到DMA1、DMA2被定义为DMA_TypeDef类型,故之前总结的俩个寄存器使用方法:

	DMA1->ISR=
	DMA2->ISR=
	
	DMA1->IFCR=
	DMA2->IFCR=


所以CCR寄存器的用法:

 DMA1_Channel1->CCR|=1;//开启DMA1通道1

位1~位3:

位1允许传输完成中断、位2允许半传输中断、位3允许传输错误中断。

代码:

	 DMA1_Channel1->CCR|=1<<1;//允许传输完成中断
	 DMA1_Channel1->CCR|=1<<2;//允许半传输中断
	 DMA1_Channel1->CCR|=1<<2;//允许传输错误中断
	 
	 // DMA1_Channel1->CCR|=0x111<<1;//同时设置

位4:

设置传输方向,是从外设传输到储存器还是从储存器传输到外设(储存器传输到储存器需另外设置)。

位5:


设置循环模式

在设置DMA时需要设置可编程数据量,即需要进行多少次传输,存储在CNTTRx寄存器中,每进行一次数据传输,该寄存器减1 。当递减到0时说明所有数据都传输完成。如果没有设置循环模式,此时会终止数据传输;如果设置了循环模式,CNTTRx寄存器会自动重装载之前设置的初值,重新开始传送数据,重新递减到0,这样循环传输数据。

位6~位7:


设置外设地址增量和储存器地址增量。

首先每次数据传输都要明确源地址和目标地址,外设地址储存在CPARx寄存器中,储存器地址储存在CMARx寄存器中。

如果没有地址增量,则固定地址的储存空间数据进行传输。对于源地址,如果地址空间的数据不更新,则每次传送数据是一致的;对于目标地址,每次传输的数据会覆盖上一次传输的数据。

如果只设置了源地址增量,则每传输一次数据,源地址会自增一次,而目标地址不变,所以传输的数据会在目标地址中一直被覆盖。

如果源地址和目标地址都设置了增量,每传输一次数据,源地址和目标地址都会自增。

位8~位11:

设置数据宽度,即设置源地址储存数据的数据宽度和目标地址储存数据的数据宽度。一般情况下俩个数据宽度保持一致就好。当然可以源地址32位、目标地址8位这种设置也可以,具体操作可以参考中文手册:

位13~位12:

DMA中有一个仲裁器用来判断个通道优先级的。首先DMA1优先级高于DMA2;然后就是位13:12设置的优先级了,高优先级先执行;最后就是通道编号了,如果两个通道的位13:12设置的通道优先级一样,则通道编号小的优先级高。

如:
DMA1通道1优先级:中
DMA1通道2优先级:中
DMA1通道3优先级:高
DMA2通道1优先级:中
DMA2通道2优先级:中
DMA2通道3优先级:高

优先级顺序:DMA1通道3 > MA1通道1 > MA1通道2 > DMA2通道3 > DMA2通道1 > DMA2通道2

位14:

设置从储存器到储存器模式

4、DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)

设置传输所有数据需要多少次。之前CCRx寄存器中总结循环模式时总结过。

5、DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)


储存外设地址

6、DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)

储存储存器的地址

三、DMA相关库函数

1、取消初始化函数DMA_DeInit()


参数: DMAy_Channelx
即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

操作:
①关闭通道
②CCR寄存器每一位都置0
③CNDTR寄存器置0,传输数设置为0
④CPAR、CMAR寄存器清零,清空源地址和目标地址
⑤重置通道的三大中断(完成、过半、错误中断),即清除中断标志位

2、DMA初始化函数DMA_Init()

参数:

参数1:
DMAy_Channelx

即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

参数2:DMA_InitTypeDef* DMA_InitStruct


详细总结一下这个结构体:

DMA_PeripheralBaseAddr 外设基地址
DMA_MemoryBaseAddr储存器基地址
DMA_DIR方向,是从外设到储存器还是存储器到外设
选择时:
DMA_DIR_PeripheralDST 外设作为目标,即从储存器到外设
DMA_DIR_PeripheralSRC 外设作为源,即从外设到储存器
如果方向为外设到储存器,则DMA_PeripheralBaseAddr 外设基地址为源地址,DMA_MemoryBaseAddr储存器基地址为目标地址;如果方向为存储器到外设,则DMA_PeripheralBaseAddr 外设基地址为目标地址地址,DMA_MemoryBaseAddr储存器基地址为源地址;
DMA_BufferSize通道的缓冲区大小,最大为35535 表示传输多少次
DMA_PeripheralInc外设基地址是否递增
DMA_MemoryInc储存器基地址是否递增
DMA_PeripheralDataSize外设数据的大小。有字节(8位)、半字(16位)、字(32位)
DMA_MemoryDataSize储存器储存数据大小。有字节(8位)、半字(16位)、字(32位)
DMA_Mode设置模式,是否为循环模式
DMA_Priority设置通道优先级,分为最高、高、中、低四个等级
DMA_M2M设置是否为储存器到储存器

3、结构体初始化函数DMA_StructInit()


参数:

参数为DMA_Init()函数传递的那个结构体

操作:
DMA_PeripheralBaseAddr 外设基地址设置为0
DMA_MemoryBaseAddr储存器基地址设置为0
DMA_DIR方向,设置为从外设到储存器
DMA_BufferSize通道的缓冲区大小,设置为0
DMA_PeripheralInc外设基地址是否递增,设置为不递增
DMA_MemoryInc储存器基地址是否递增,设置为不递增
DMA_PeripheralDataSize外设数据的大小。有字节(8位)、半字(16位)、字(32位),设置为字节(8位)
DMA_MemoryDataSize储存器储存数据大小。有字节(8位)、半字(16位)、字(32位),设置为字节(8位)
DMA_Mode设置模式,是否为循环模式,设置为不循环
DMA_Priority设置通道优先级,分为最高、高、中、低四个等级,设置为低优先级
DMA_M2M设置是否为储存器到储存器,设置为不启用

4、DMA使能函数DMA_Cmd( )

参数:
参数1:DMAy_Channelx

即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

参数2:DISABLE、ENABLE

操作:
对CCR寄存器位0操作

5、DMA中断配置函数DMA_ITConfig()

参数:
参数1:DMAy_Channelx

即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

参数2:DMA_IT
表示哪种中断:

	 DMA_IT_TC //传输完成中断
	 DMA_IT_HT //传输过半中断
	 DMA_IT_TE //传输错误中断

参数3:DISABLE、ENABLE

6、设置传输数据量函数DMA_SetCurrDataCounter()


参数:
参数1:DMAy_Channelx

即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

参数2:0~0xffff之间的数,最多为65535,即循环一次(如果设置了循环模式)传输次数最多为65535

操作:

对CNDTR寄存器写操作

7、获取剩余传输次数函数DMA_GetCurrDataCounter()

参数:
参数1:DMAy_Channelx

即:DMA1_Channel1~DMA1_Channel7、DMA2_Channel1 ~DMA1_Channel5

操作:

对CNDTR寄存器读操作

由于CNDTR寄存器装载初值后,每传输一次会自减一次,故返回的CNDTR的值是现在剩余的数,即还可以传输多少次。

8、获取中断状态函数DMA_GetFlagStatus()


参数:

DMA1和DMA2各通道的全局中断(即完成、过半、错误任意几个中断都行)、完成中断、过半中断、错误中断。

返回:

返回SET表示发生参数类型的中断,返回RESET表示未发生中断。

9、清除中断标志函数DMA_ClearFlag()

参数:


操作:

对IFCR对应位写1清除ISR寄存器对应位。

10、获取中断状态函数DMA_GetITStatus()


DMA_GetFlagStatus()函数基本一样

11、获取中断状态函数DMA_ClearITPendingBit()


DMA_ClearFlag基本一样

四、DMA的编程顺序

1、使能DMA时钟

使用RCC_AHBPeriphClockCmd()函数

2、初始化 DMA

使用DMA_Init()函数

3、使能外设

4、使能DMA

5、状态查询

使用DMA_GetFlagStatus()查询中断
使用DMA_GetCurrDataCounter()查询还剩余多少带传输数据

五、例题

1、题

利用板子上的ADC采集光敏传感器的电压等效值(即将0~3.3V的电压值映射到0 ~ 4095的这个u16数字),利用DMA储存在一个u16数组中。存储完成后,在串口中输出数组中的值。

2、分析

ADC采集光敏传感器的实验之前博客ADC相关的几个实验—内部温度传感器、内部参照电压、光敏电阻总结过,这部分不在详述。

将数据传输到数组中,所以ADC需要使能DMA。并且之前总结光敏传感器时说到过,本板的光敏传感器连接ADC3通道6,而根据本博第一部分:STM32的DMA数量这一目中的表格可得ADC3用到DMA2通道5 。

3、代码

ADC采集光敏传感器代码的代码上一博客总结过了,唯一不同的是需要使能ADC的DMA,ADC的DMA使能在主函数中进行。
直接附ADC采集光敏传感器代码:

adc.h代码:

#ifndef _ADC_
#define _ADC_
#include "stm32f10x.h"
void adc_Init(void);
#endif

adc.c代码:

#include "adc.h"
#include "stm32f10x.h"
void adc_Init(void)
{
	ADC_InitTypeDef		 ADC_InitStruct;
	GPIO_InitTypeDef	 GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3|RCC_APB2Periph_GPIOF, ENABLE);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOF, &GPIO_InitStructure);

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	ADC_DeInit(ADC3);
	
	ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
	ADC_InitStruct.ADC_NbrOfChannel=6;
	ADC_InitStruct.ADC_ScanConvMode=DISABLE;
	ADC_Init(ADC3, &ADC_InitStruct);
	
	ADC_Cmd(ADC3,ENABLE);
	ADC_ResetCalibration(ADC3);
	while(ADC_GetResetCalibrationStatus(ADC3)) //等待复位校准结束
	{
		
	}
	ADC_StartCalibration(ADC3);
	while(ADC_GetCalibrationStatus(ADC3))//等待校准结束
	{

	}

	ADC_RegularChannelConfig(ADC3, ADC_Channel_6, 1, ADC_SampleTime_28Cycles5);//规则通道设置

}

dma.h代码:

#ifndef _DMA_
#define _DMA_
void dma_Init();
#endif

在DMA中,外设作为源,数组作为目标,外设是ADC采集到的数据,存储在ADC3的DR寄存器中,该寄存器在单ADC模式下是低16位有效,故外设数据大小为16位,外设地址就是(u32)&ADC3->DR 。储存器为数组,可以为16位数组,地址为数组名。

考虑到要知道是否传输完成,故需要设置传输完成中断。

dma.c代码如下:

#include "dma.h"
#include "stm32f10x.h"
extern u16 arr[200];
void dma_Init()
{
	DMA_InitTypeDef  DMA_InitStructure;
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2,ENABLE);
	
	DMA_InitStructure.DMA_BufferSize=200;
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;
	DMA_InitStructure.DMA_MemoryBaseAddr=(u32)arr;
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
	DMA_InitStructure.DMA_PeripheralBaseAddr=(u32)&ADC3->DR;
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_Priority=DMA_Priority_High;
	DMA_Init(DMA2_Channel5,&DMA_InitStructure);
	
	DMA_ITConfig(DMA2_Channel5,DMA_IT_TC,ENABLE);
}

main.c代码:

#include "stm32f10x.h"
#include "adc.h"
#include "usart.h"
#include "dma.h"
u16 arr[200];
u16 i=0;
 int main(void)
 {	
	adc_Init();
	dma_Init();
	uart_init(115200);	
	while(1)
	{	 
		 ADC_SoftwareStartConvCmd(ADC3,ENABLE);//软件触发、并开启转换ADC
		 while(!ADC_GetFlagStatus(ADC3,ADC_FLAG_EOC))
		  {
			//转换过程中执行空语句,等待转换完成
		  }
		  ADC_DMACmd(ADC3,ENABLE);
		  DMA_Cmd(DMA2_Channel5,ENABLE); 
		  DMA_Cmd(DMA2_Channel5,DISABLE); 
		  if((DMA_GetITStatus(DMA2_IT_TC5)!=RESET))
		  {
			 DMA_ClearFlag(DMA2_FLAG_TC5);
			 break;
		  }
	  
	 }
		for(i=0;i<200;i++)
		  printf("a[%d]=%d\\r\\n",i,arr[i]);
		 
	 while(1)
	 {
	 
	 }
}

主函数的第一个while语句中,使能软件触发ADC,使ADC开始转换并等待转换完成。

接下来三句代码很重要,首先使能ADC的DMA、DMA使能;然后失能DMA,这里必须使能再失能,否则无法传输。

当全部完成,发生中断,跳出第一个while语句,并输出数组值。

4、效果

实验室光照下:

手电筒照亮光敏传感器:


六、强调几点

1、DMA中断配置函数DMA_ITConfig()


注意它的有效性说明 ,参数应该是上面的三个,而不是下面的这堆!

2、DMA清除中断函数:DMA_ClearFlag()和DMA_ClearITPendingBit()

注意参数:

虽然根据名字能看出是DMA几的通道几的哪个中断类型,而且IT和FLAG的值是相同的,但是不要混用,即DMA_ClearFlag()用DMA1_FLAG_TC1;DMA_ClearITPendingBit()函数用DMA1_IT_TC1;

3、DMA_BufferSize

DMA_BufferSize在DMA_Init()函数中,打开函数体:

可见它是对CNDTR寄存器的操作,即DMA_BufferSize表示传输多少次

4、传输时源、目标的字节大小

字节大小尽量与有效位数保持一致,有时候需要单片机自动类型转换,如32位转16位,一定要保证转换时数据不丢失。

如本例中ADC采集的DR寄存器为32位寄存器,但是只有低16位有效,故可以把“源”或者说外设的数据大小设置为DMA_PeripheralDataSize_HalfWord 16位的数。

储存器数据大小要设置对,如本例中,传递过来的数是16位的数,也把存储器数据大小设置为16位:DMA_MemoryDataSize_HalfWord ,但是数组改为u32位,就会出错,因为设置与实际存储情况不符合。把数组改为u32后:

出现上述情况的原因是u32位的数组储存区域,每一个地址单元会储存2次传递的数值,导致a[100]~a[199]没有值。

把储存器的类型也改为32位:

发现又好了:

总而言之,端到端,每一端各自设置的数据大小要与实际寄存器、存储器数据相同。可以一端是16位,另一端是32位,但必须保证16位这一端的寄存器或存储器的实际类型就是16位;32位这一端的寄存器或储存器实际类型就是32位。

5、使能与失能DMA

因为多次传输时DMA不能传输一次就中断,可以理解为每使能一次DMA,传输一次,失能后再使能,循环下去,完成DMA多次传输。

以上是关于STM32F103(二十一)DMA(超详细的~)的主要内容,如果未能解决你的问题,请参考以下文章

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十)DAC(贼详细)

stm32f103 dma是怎么实现的