当前位置:   article > 正文

DMA:一个自动搬运车的自述_dma搬运数据过程

dma搬运数据过程

DMA:直接存储器访问,顾名思义,不需要CPU过问就可以直接把数据从一个地方搬运到另一个地方,大大降低了CPU的劳动负担,就好比你养了一只能够自动帮你取快递的狗子一样。DMA传输数据是从一个地址空间复制到另一个地址空间,当CPU初始化这个传输动作,传输过程的控制则交给DMA控制器而无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,为CPU减负

一、功能

系统框图:
在这里插入图片描述看这个原理图:(1)先从DMA请求源看:请求源可以来自各种外设即从外设(TIM、ADC、SPI、I2C、USART)产生DMA请求,这些外设想要DMA帮忙搬运数据,需要产生一个DMA请求信号,输入到DMA控制器。也就意味着在配置这些外设时需要使能它的DMA参数。DMA请求可以用软件调用进行触发,也可以配置中断等事件的发生来触发(2)每个外设沿着各自的DMA通道到达DMA控制器,面临这么多请求,需要仲裁器决定各个请求源的优先级,划分了响应的先后顺序:有4级:很高、高、中等、低。根据这个图看,仲裁的是请求源而不是DMA通道。(3)收到请求后DMA开始干活:把外设内的数据(源地址)顺着DMA通道搬到总线上,再运到对应的存储器上(目的地址)。支持外设和存储器、存储器和外设、存储器和存储器的传输,闪存、SRAM、外设的SRAM、APB1、APB2、AHB上的外设都可以作为访问的源和目标。(4)上述三点叙述了【谁在请求?谁优先传输?从哪送到哪 】这三个基本问题,接下来需要面临最复杂的问题:按照什么形式传输?以把数组arr[100]内容通过DMA搬运到USART的发送端的数据寄存器TDR为例:

简单复习一下USART:

(1)属于异步通信即收发双方的时钟不必同频率。但是呢用作同步通信也不是不可以:支持同步单向通信单线半双工通信;全双工的,异步通信;(2)可编程数据字长度(8位或9位),即每一次发送的数据是8位或9位;可配置的停止位-支持1或2个停止位,再算上这些起始位和停止位、校验位等,每次发送的位实际至少有10了;(3)任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX),即通道是双线,实现全双工
STM32中usart的一个重要特点:直接向TDR写入数据就会直接自动把数据通过usart发送出去了!对于接收寄存器RDR,一般需要配置usart的中断函数的触发条件为接收区RDR满了:就能在收完数据后触发这个usart中断函数,在这个中断函数里写程序提取RDR的值!
在这里插入图片描述在这里插入图片描述这里主要是关注一下DR寄存器:STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR,是9位的,意味着USART每次只能传输8位或9数据,对应到配置参数时的8位模式和9位模式。为什么会提到这些呢?这是因为DMA的外设接收数据时,可以选择每次接收8位或者16位或者32位,既然这里的外设是USART,那就只能选择8位了,并且除了确定接收的位数,还有个参数是”地址偏移量“,说真的这个东西并不复杂,就是不容易说清楚:假设我们是其他的外设,它的寄存器是32位的,因此需要接收32位数据才能执行后续的操作,而如果我们配置每次从DMA接收8位,意味着需要接收4次才能接收完整,那么每次接收的数据存的地址会有所区别:比如第一次是[0:7],接收地址是寄存器的第[0]位对应的地址;第二次是存到第[8:15]位,接收地址是寄存器的第[8]位对应的地址,这两次的地址之间偏移量就是8位,因此我们需要设置DMA接收端的偏移量是8位。而USART的DR寄存器就是8位,接收一次就能执行后续步骤,就不需要偏移量了
在这里插入图片描述USART的一般步骤,主要关注会涉及那些模块:RX和TX本质都是IO口因此必须配置IO;串口参数配置:波特率、数据位数、停止位、校验位等;开启中断以及中断服务函数;时钟是必不可少的:串口用的时钟和IO口用的时钟;另外再关注一下USART的中断函数:
在这里插入图片描述在这里插入图片描述
USART的中断函数:所有的中断事件共用一个中断服务函数!所以说,一旦使能了USART的中断功能,必须关注有哪些事件会触发中断函数:我们最常用的当然就是发送数据寄存器位空、发送完成、检测到数据就绪可读这三个。由于中断类型比较多,因此我们需要配置选择是哪种中断事件,比如开启接收到数据从而触发中断:USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); 另一个比如发送完成后触发中断:USART_ITConfig(USART1,USART_IT_TC,ENABLE); 这俩应该是最常用的usart中断了吧另外注意到的一点是:仅当使用DMA时才使能这个标志位?即USART一定要使能自身的DMA参数:USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); 这一句使能的是usart的接收寄存器接收DMA搬运来的数据可选参数有 USART_DMAReq_Tx 和 USART_DMAReq_Rx 即收发

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//usart开启接收中断即接受寄存器RDR收满后会触发中断函数
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //usart允许发送引脚寄存器TDR接收DMA传输来的数据,随后发送出去
  • 1
  • 2

为啥搬运到TDR后就自动发送出去了呢?经过允许了吗
在USART发送期间,在TX引脚上首先移出数据的最低有效位。在此模式里,USART_DR寄存器包含了一个内部总线和发送移位寄存器之间的缓冲器。
把要发送的数据写进USART_DR寄存器(此动作清除TXE位),清零TXE位总是通过对数据寄存器的写操作来完成的
如果此时USART正在发送数据,对USART_DR寄存器的写操作把数据存进TDR寄存器,并在当前传输结束时把该数据复制进移位寄存器。如果此时USART没有在发送数据,处于空闲状态,对USART_DR寄存器的写操作直接把数据放进移位寄存器,数据传输开始,TXE位立即被置起。当一帧发送完成时(停止位发送后)并且设置了TXE位,TC位被置起,如果USART_CR1寄存器中的TCIE位被置起时,则会产生中断。可以看到:USART空闲时如果对DR进行写入数据,会导致usart传输直接开始!而DMA直接把数据搬运到DR 了,所以导致usart直接开始发送这些数据了,而没有经过软件调用:USART_SendData(USART1, data_8bit);//向串口1发送数据;这也就相当于了硬件事件触发usart的发送功能!!

所以说usart配合DMA使用的两个重要步骤是:

//接收端RDR:最好使能接收触发usart中断函数功能,这样就能在RDR收满数据后自动调用usart中断服务函数,提取出RDR的值
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//usart开启接收中断即接受寄存器RDR收满后会触发中断函数
//发送端TDR:要允许DMA向TDR搬运数据,搬运后就会自动调用usart的发送功能
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //usart允许发送引脚寄存器TDR接收DMA传输来的数据,随后发送出去

//补充对比内容:
USART_SendData(USART1, data_8bite);//软件调用函数使用usart对外发送数据
//而这个函数也不过是在向DR写入数据!!写入后就会自动发送出去了!!!
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data)); 
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF); //向DR寄存器写入数据
}
//而咱DMA就是直接向DR写入数据,也就会自动通过usart发送出去了,也就不用我们在手动调用所谓的USART_SendData(USART1, data_8bite);函数了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

咱DMA就是直接向DR写入数据,也就会自动通过usart发送出去了,也就不用我们在手动调用所谓的USART_SendData(USART1, data_8bite);函数了
在这里插入图片描述在这里插入图片描述在这里插入图片描述DMA请求源:
STM32最多有2个DMA控制器,DMA1有7个通道,DMA2有5个通道。每个通道都能接收指定范围内的外设或存储器的传输请求(即DMA请求源),从外设(TIMx、ADC、SPIx、I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到DMA 控制器,这就意味着同时只能有一个请求有效,有个仲裁器来协调各个DMA请求源的优先级。从上图看出,想要DMA顺利执行:需要请求源、设置MEM2MEM和EN位。
在这里插入图片描述DMA中断
每个通道都有3个事件标志(DMA半传输、DMA传输完成、DMA传输错误),这三个事件标志逻辑或成为一个单独的中断请求(即三者共用一个中断服务函数?)

二、喜闻乐见寄存器

简述过程:(1)初始化DMA模块:打开某模块比如USART的DMA传输功能、配置时钟、配置IO模式、设置DMA参数:源地址和目的地址、搬运方向和偏移量以及优先级、配置中断、程序调用DMA或者中断事件发生后自动硬件触发DMA(2)从源地址取数据:第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器所指定的外设基地址或存储器单元(源地址),按照设置规则取一定能位数的数据;(3)向目的地址写入数据:同样按照设置的一次写入的数据位数规则将数据写到目的地址即DMA_CPARx或DMA_CMARx所指定的目的地址;(4)执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目即还剩多少数据没传输
在这里插入图片描述CCRx是核心寄存器,负责数据宽度、外设以及存储器宽度、通道优先级、增量模式、传输方向、中断允许、使能等
在这里插入图片描述1.从哪里获取数据:从外设获取数据就是0,从存储器获取数据就是1;我们例子是从数组arr[100]中获取数据,属于从内存中获取,设置1;
在这里插入图片描述2.DMA请求源的优先级,如果两个请求源的优先级相同,就比较两者走的DMA通道号,比如走通道2的要比通道4的优先级高;
在这里插入图片描述3.指的是一次从源地址取多少数据和一次向目的地址写入多少数据吧
在这里插入图片描述4.指针增量:是否执行增量

在这里插入图片描述5.是否循环
在这里插入图片描述6.记录DMA最多能搬运多少数据:这个寄存器会随着每次搬运而自减,从而反应搬运的进度

在这里插入图片描述三种中断标志位

三、一般配置

在这里插入图片描述
DMA配置参数:通道;优先级;传输方向;存储器/外设的数据宽度;存储器/外设的地址是否增量;循环模式;数据传输量
在这里插入图片描述结构体:对应的就是必要的DMA参数
在这里插入图片描述
程序配置:注意这个串口DMA使能函数,是属于串口配置中的一个参数

小结:配置DMA时,需要指定好源地址、目的地址、每次运多少位、总共运多少位、是否偏移、是否打开中断、记录还剩多少个待发数据、选好通道和引脚等等,对应的外设模块也需要打开DMA功能即能够发送出DMA请求。

程序:

dma.c中

DMA_InitTypeDef DMA_InitStructure;
u16 DMA1_MEM_LEN 8200;//发送数据的长度,最好比sizeof(TEXT_TO_SEND)大一些 ,这里是随便设置的个数
// cpar是外设地址即目的地址,cmar是寄存器地址即源地址,cndtr是DMA传输多少个数据
void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
{
 	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//使能DMA1的时钟
    DMA_DeInit(DMA_CHx);   //复位通道

	DMA1_MEM_LEN=cndtr; //DMA传输8200个数据
	DMA_InitStructure.DMA_PeripheralBaseAddr = cpar;  //设置外设地址
	DMA_InitStructure.DMA_MemoryBaseAddr = cmar;  //设置存储器地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;  //然后设置传输方向:从存储器到外设,”PeripheralDST:外设是目的“
	DMA_InitStructure.DMA_BufferSize = cndtr;  //DMA传输8200个数据
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //外设地址不变即不偏移
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  //DMA地址要偏移
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  //外设一次写入多少位数据,即数据宽度是8位
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器一次取出多少位数据,即数据宽度是8位
	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传输即通过软件启动DMA传输而不是中断事件触发硬件从而调用DMA
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);  //使能通道
}	  

usart.c中
void uart_init(u32 bound){
  
    GPIO_InitTypeDef GPIO_InitStructure; //usart所用到的IO,这几个IO用过接收引脚和发送引脚
	USART_InitTypeDef USART_InitStructure;//usart的参数配置:波特率、停止位等
	NVIC_InitTypeDef NVIC_InitStructure;//触发中断,中断的优先级设置
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能usart时钟和io时钟
  
	//USART1_TX  电路图上对应到 GPIOA.9  发送引脚,IO接收CPU的数据然后发送给外面,对CPU负责制
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输入 -- GPIO的几种模式
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1_RX	电路图上对应到  GPIOA.10 接收引脚,IO接收外面来的数据然后输入给CPU,对CPU负责制
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

  //Usart1触发的中断的优先级的配置 NVIC 
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //表示对usart的中断进行配置
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//主优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//从优先级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//使能irq通道
	NVIC_Init(&NVIC_InitStructure);	//初始NVIC的寄存器
  
   //USART 初始化参数
	USART_InitStructure.USART_BaudRate = bound;//´波特率设置
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//每次发送的数据的位数,可选8位或9位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//是否使用停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//奇偶校验
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制?
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式,能收能发
  USART_Init(USART1, &USART_InitStructure); //初始化串口参数
  
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  //开启串口接收中断:也就是说一旦接收到数据就会自动触发中断,调用USART1_IRQHandler()
  USART_Cmd(USART1, ENABLE);                    //使能串口
}
//串口的中断服务函数: 这里配置的是当数据接收完成后触发此中断函数!
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收
		{
		  Res =USART_ReceiveData(USART1);	//接收数据
		}
}

//想要发送数据,你就要调用这个:USART_SendData(USART1, data_8bit);//向串口1发送数据



main.c中
#define SEND_BUF_SIZE 8200	//
//制造传输用的数组!!很重要的哦
u8 SendBuff[SEND_BUF_SIZE];	//待传输的内容的内存大小
const u8 TEXT_TO_SEND[]={"ALIENTEK Elite STM32F1 DMA BEAUTY"};//待传输的内容
for(i=0;i<SEND_BUF_SIZE;i++)//组装数组
{
		if(t>=j)//加入换行符
		{
			if(mask)
			{
				SendBuff[i]=0x0a;
				t=0;
			}else 
			{
				SendBuff[i]=0x0d;
				mask++;
			}	
		}else//复制TEXT_TO_SEND[]中的内容到SendBuff[]
		{
			mask=0;
			SendBuff[i]=TEXT_TO_SEND[t];
			t++;
		}    	   
 }		
 MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,(u32)SendBuff,SEND_BUF_SIZE);
 //选择DMA1的通道4进行搬运,目的地址是USART的DR寄存器,源地址是数组SendBuff[],这一趟搬运的总数据个数是SEND_BUF_SIZE

while(1)
	{
		t=KEY_Scan(0);
		if(t==KEY0_PRES)//KEY0按下
		{
			LCD_ShowString(30,150,200,16,16,"Start Transimit....");
			LCD_ShowString(30,170,200,16,16,"   %");//显示百分号
			printf("\r\nDMA DATA:\r\n"); 	    
		  USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口的DMA功能!!!!!!!!!  
		  // 这一句重点关注:使能usart的DMA功能意味着告诉usart待会会有dma给你送数据或者从你取数据,你要准备好
		  //如果送的数据存满了usart的接收寄存器,并且配置了usart的接收中断函数,那么还会触发这个中断函数
		  
			MYDMA_Enable(DMA1_Channel4);//软件调用DMA,开始一次DMA传输  
		    //等待DMA传输完成,此时我们来做另外一些事情,电灯
		    //实际应用中,传输数据器件,可以执行另外的任务
		    //为啥搬运到DR后就自动发送出去了呢???????????????
		    while(1)
		    {
				if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET)	//检查标志位,判断通道4传输是否完成
				{
					DMA_ClearFlag(DMA1_FLAG_TC4);//清楚通道4传输完成标志位
					break; 
		    }
				pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到CNT值,得到当前还剩余多少个数据
				pro=1-pro/SEND_BUF_SIZE;//计算传输进度的百分比
				pro*=100;      //扩大100倍
				LCD_ShowNum(30,170,pro,3,16);	  
		    }			    
			LCD_ShowNum(30,170,100,3,16);//显示  
			LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传输完成
		} 
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/976689
推荐阅读
相关标签
  

闽ICP备14008679号