一、DMA简介

DMA:直接存储器访问

  • 主要功能:

    把数据从一个位置搬移到另外一个位置,而且不占用CPU,即在传输数据的时候, CPU 可以干其他的事情。
    数据传输支持从外设到存储器或者存储器到存储器,这里的存储器可以是 SRAM 或者是 FLASH。

    存储器到外设典型应用是:USART
    外设到存储器典型应用是:ADC

    在这里插入图片描述
    DMA 控制器包含了 DMA1 和 DMA2,其中 DMA1 有 7 个通道, DMA2 有 5 个通道,这里的通道可以理解为传输数据的一种管道。

  • 使用USART传输数据时

    数组的内容首先发送给CPU的CR1,CR2,CR3然后再发到串口

  • 使用DMA传输数据

    只需要CPU给DMA下达一个指令,数组内容即可通过串口发送出去

二、DMA 功能框图

在这里插入图片描述

(一)DMA请求

如果外设要想通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求, DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕

(二)DMA通道

  • DM1请求映射
    在这里插入图片描述
    如果使用串口,则对应通道2,3,4,5,6,7
    USART1的TX和RX分别对应DMA1的通道4和通道5(并不是随便使用)

  • DMA2请求映射
    在这里插入图片描述
    当多个DMA请求同时来的时候,则需要用到仲裁器来决定谁先执行

(三)DMA仲裁

仲裁器根据通道请求的优先级来启动外设/存储器的访问。

优先权管理分2个阶段:
● 软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:
─ 最高优先级
─ 高优先级
─ 中等优先级
─ 低优先级
● 硬件:如果2个请求有相同的软件优先级,则较低编号的通道比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4。

三、数据配置

  • DMA初始化结构体
    在这里插入图片描述

  • DMA数据配置

    使用 DMA,最核心就是配置要传输的数据,包括数据从哪里来,要到哪里去,传输的数据的单位是什么,要传多少数据,是一次传输还是循环传输等等。

(一)数据从哪里来到哪里去?

在这里插入图片描述

  • DMA_PeripheralBaseAddr

    外设地址,设定 DMA_CPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。

  • DMA_Memory0BaseAddr

    存储器地址,设定DMA_CMAR 寄存器值;一般设置为我们自定义存储区的首地址。—如果定义了一个数组,数组名即为首地址

  • DMA_DIR

    传输方向选择,可选外设到存储器、存储器到外设。它设定 DMA_CCR 寄存器的DIR[1:0] 位的值。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。

    DMA_DIR:规定外设是作为数据传输的目的地还是来源(数据传输方向)

     DMA_DIR_PeripheralDST	外设作为数据传输的目的地
     DMA_DIR_PeripheralSRC	外设作为数据传输的来源
    

(二)数据要传多少,传输的单位是什么?

在这里插入图片描述

  • DMA_BufferSize

    要传输的数据个数

  • DMA_PeripheralInc

    如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,(比如说外设地址定义了一个数组,地址需要递增),一般外设都是只有一个数据寄存器,所以一般不会使能该位。

  • DMA_MemoryInc

    如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能(同样定义了一个数组,地址需要递增),我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。

  • DMA_PeripheralDataSize

    外设的数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位)—这是根据定义的外设变量位数来决定

  • DMA_MemoryDataSize

    存储器数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位)–根据定义的存储器变量位数决定

    当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小

(三)什么时候传输结束?

在这里插入图片描述

  • DMA_Mode

    DMA 传输模式选择,可选一次传输或者循环传输

    例如串口发送一个数组内容的数据就可以设置位单次传输
    例如我们的 ADC 采集是持续循环进行的,所以使用循环传输模式。

四、DMA三种应用

(一)从存储器到存储器(M-to-M)

当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。

DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址, DMA存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。

实验目的:
我们先定义一个静态的源数据,存放在内部 FLASH,然后使用 DMA 传输把源数据拷贝到目标地址上(内部 SRAM),最后对比源数据和目标地址的数据,看看是否传输准确

  • 首先定义两个数组:一个作为外设,一个作为存储器

    内部FLASH是存放代码code
    内部SRAM是定义的变量

     /* 定义aSRC_Const_Buffer数组作为DMA传输数据源
      * const关键字将aSRC_Const_Buffer数组变量定义为常量类型
      * 表示数据存储在内部的FLASH中
      */
     const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
                                         0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
                                         0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
                                         0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
                                         0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
                                         0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
                                         0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
                                         0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
                                         0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};
     /* 定义DMA传输目标存储器
      * 存储在内部的SRAM中																		
      */
     uint32_t aDST_Buffer[BUFFER_SIZE];
    

    aSRC_Const_Buffer[BUFFER_SIZE] 定义用来存放源数据,并且使用了 const 关键字修饰,即常量类型,使得变量是存储在内部 flash 空间上

  • DMA初始化部分

    源地址和目标地址使用之前定义的数组首地址,传输的数据量为宏 BUFFER_SIZE 决定,源和目标地址指针地址递增,使用一次传输模式不能循环传输,因为只有一个 DMA 通道,优先级随便设置,最后调用 DMA_Init 函数完成 DMA 的初始化配置

     //存储器到存储器						
     void DMA_mtm_config(void)
     {
     	//1-要初始化结构体肯定要定义一个结构体变量
     	DMA_InitTypeDef DMA_InitStruct;
     	//2、配置DMA时钟
     	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1时钟 
     	
     	//数据从哪里来到哪里去(配置3个)
     	//源数据地址(把此数组看做外设)
     	DMA_InitStruct.DMA_PeripheralBaseAddr=(uint32_t)aSRC_Const_Buffer;//外设地址---是一个数组,数组名称就是首地址
     	//目标地址
     	DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)aDST_Buffer;//存储器地址 --是一个空数组
     	//方向:外设作为源地址
     	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方式
     	
     	//传多少,单位是多少
     	DMA_InitStruct.DMA_BufferSize= BUFFER_SIZE;//传输数目(要传输多少个)
     	
     	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Enable ;//外设地址递增
     	DMA_InitStruct.DMA_PeripheralDataSize= DMA_PeripheralDataSize_Word  ;//数据宽度,word是一个字32位
     	
     	//配置memory
     	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable ;//内存地址递增
     	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_Word ;
     	
     	
     	//配置模式和优先级
     	DMA_InitStruct.DMA_Mode=DMA_Mode_Normal ;//正常模式或者是循环模式
     	DMA_InitStruct.DMA_Priority=DMA_Priority_High ;//共有四种优先级
     	DMA_InitStruct.DMA_M2M=DMA_M2M_Enable;//刚好是M-M模式
     	DMA_Init(DMA1_Channel6, &DMA_InitStruct);
     	//M-T-M模式可以是任意的通道
     	
     	DMA_ClearFlag(DMA1_FLAG_TC6);//先将这个标志位清楚
     	//3、使能DMA
     	DMA_Cmd(DMA1_Channel6, ENABLE);
     	
     }
    
  • 数据是否传输完成且无误,两个数组中的内容要进行比较

     /**
       * 判断指定长度的两个数据源是否完全相等,
       * 如果完全相等返回1,只要其中一对数据不相等返回0
     	*参数1是常量指针,是不变的
     	*参数2也是一个指针
     	*参数3是要比较多长
       */
     uint8_t Buffercmp(const uint32_t* pBuffer, 
                       uint32_t* pBuffer1, uint16_t BufferLength)
     {
       /* 数据长度递减 */
       while(BufferLength--)
       {
         /* 判断两个数据源是否对应相等 */
         if(*pBuffer != *pBuffer1)
         {
           /* 对应数据源不相等马上退出函数,并返回0 */
           return 0;
         }
         /* 递增两个数据源的地址指针 */
         pBuffer++;
         pBuffer1++;
       }
       /* 完成判断并且对应数据相对 */
       return 1;  
     }
    
  • 主函数部分的内容:

     int main(void)
      {	
     	uint8_t status=0;
     	delay_init();	    	 //延时函数初始化	  
     	LED_Init();		  	//初始化与LED连接的硬件接口
     	
     	DMA_mtm_config();//DMA初始化
     	 //在比较之前最后先检测数据是否传送完毕:使用 DMA_GetFlagStatus(uint32_t DMAy_FLAG);函数
     	 //使用的DMA1通道6
     	 while(DMA_GetFlagStatus(DMA1_FLAG_TC6)==RESET);//如果返回值位reset表示还没哟传输成功
     	 
     	status=Buffercmp(aSRC_Const_Buffer,aDST_Buffer,BUFFER_SIZE);//返回值是一个8位的
     	if(status==0)//表示失败
     	{
     		LED0=0;
     	}
     	else//表示成功
     	{
     		LED1=0;
     	}
     	while(1)
     	{
     	}
      }
    

如果两个数组中的内容一样、则点亮LED1灯作为信号,反之传输失败则点亮LED0灯作为信号。
STM32-DMA(存储器到存储器传输数据)–代码链接

(二)从存储器到外设(M-to-P)

当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。 DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址

实验目的:
我们先定义一个数据变量,存于 SRAM 中,然后通过 DMA 的方式传输到串口的数据寄存器,然后通过串口把这些数据发送到电脑的上位机显示出来。

串口初始化部分不再赘述
stm32串口自定义协议接收一串十六进制数据

DMA代码部分:

  • 首先需要定义一个发送数据的数组

     #define SendBuff_Size 5000
     u8 SendBuffe[SendBuff_Size];//定义一个发送数组
    
  • DMA初始化工作

    因为数据是从存储器到串口,所以设置存储器为源地址,串口的数据寄存器为目标地址,要发送的数据有很多且都先存储在存储器中,则存储器地址指针递增,串口数据寄存器只有一个,则外设地址地址不变,两边数据单位设置成一致,传输模式可选一次或者循环传输,只有一个 DMA 请求,优先级随便设

     //Memory->p(USART->DR)
     //通过手册查询串口的DR寄存器偏移地址是:0x04则外设地址是USART1_BASE+0x04
     void USART1_DMA_mtp_config(void)
     {
     	//1-要初始化结构体肯定要定义一个结构体变量
     	DMA_InitTypeDef DMA_InitStruct;
     	//2、配置DMA时钟(通过查看手册可以知道USART1_TX使用DMA1的通道4)
     	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1通道4的时钟
     	
     	//3、数据从哪里来到哪里去(配置3个)
     	//外设地址(串口数据寄存器)--目标地址
     	DMA_InitStruct.DMA_PeripheralBaseAddr=(u32)&USART1->DR;//外设地址(或者USART1_BASE+0x04)
     	//存储器地址---源地址
     	DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)SendBuffe;//存储器地址(指向了数组首地址)
     	//方向:存储器到外设(外设串口作为目的地)
     	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralDST;//传输方式(MTP)
     	//规定外设是作为数据传输的目的地还是来源(数据传输方向)
     	//DMA_DIR_PeripheralDST	外设作为数据传输的目的地
     	//DMA_DIR_PeripheralSRC	外设作为数据传输的来源
     	
     	
     	//4、传多少,单位是多少
     	DMA_InitStruct.DMA_BufferSize= SendBuff_Size;//传输数目(数组的长度)
     	
     	//只有一个串口数据寄存器不需要递增,数组是U8类型,所以一次传输一个字节
     	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable ;//外设地址b不需要递增
     	DMA_InitStruct.DMA_PeripheralDataSize= DMA_PeripheralDataSize_Byte  ;//数据宽度,u8类型,一个字节
     	
     	//配置memory(定义了一个数组,发送一个会继续下一个,所以地址是递增)
     	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable ;//内存地址递增
     	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte  ;//一个字节
     	
     	
     	//5、配置模式和优先级
     	DMA_InitStruct.DMA_Mode=DMA_Mode_Normal ;//正常模式或者是循环模式
     	DMA_InitStruct.DMA_Priority=DMA_Priority_High ;//共有四种优先级
     	DMA_InitStruct.DMA_M2M=DMA_M2M_Disable  ;//不使用M-M模式
     	DMA_Init(DMA1_Channel4, &DMA_InitStruct);//串口1TX是DMA通道4
     	//
     	
     	DMA_ClearFlag(DMA1_FLAG_TC4);//先将这个标志位清除
     	//6、使能DMA
     	DMA_Cmd(DMA1_Channel4, ENABLE);
     	
     }
    
  • 主函数部分内容:

      int main(void)
      {	
    
     	 u16 i;
     	for(i=0;i<SendBuff_Size;i++)//对数组填充数据
     	{
     		SendBuffe[i]='b';
     	}
     	delay_init();	    	 //延时函数初始化	  
     	LED_Init();		  	//初始化与LED连接的硬件接口
     	uart_init(9600);
     	USART1_DMA_mtp_config();
     	 //等待串口发送数据(去usart头文件中找有关DMA的函数)
     	USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
     	 //把定义的变量发送到串口,串口通过TX引脚发送出去
     	
     	while(1)
     	{
     			//为了证明dma不占用CPU此处让led闪烁
     			LED0=0;
     			delay_ms(100);
     			LED0=1;
     			delay_ms(100);
     	}
     }
    

实验现象:串口助手可以接收到5000个数据–数据均为字符b,与此同时LED0在不停的闪烁

STM32+DMA+串口发送数据(存储器到外设数据传输)-代码链接

(三)外设到存储器(P-to-M)

当我们使用从外设到存储器传输时,以 ADC 采集为例。 DMA 外设寄存器的地址对应的就是 ADC数据寄存器的地址ADC_DR, DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址,方向我们设置外设为源地址。

STM32-ADC单通道采集数据(中断形式和DMA形式)–代码
代码参考ADC学习笔记整理部分

STM32-ADC(独立模式、双重模式)+DMA读取数据+部分基础知识-链接

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐