从字面意思上看,DMA即为“直接内存读取”的意思,换句话说DMA就是用来传输数据的,它也属于一个外设。只是在传输数据时,无需占用CPU。

  DMA请求

  某个外设在通过DMA传输数据前,必须先给DMA控制器发送请求,控制器会返回一个应答信号给外设,外设应答后并且DMA控制器收到外设应答信号后,便会启动DMA传输。这个过程类似于TCP的“三次握手”。

  DMA有DMA1和DMA2两个控制器,每个控制器都有不同的通道,每个通道对应不同的外设请求。如图12-1为DMA1的通道请求、图12-2为DMA2的通道请求。

  

图12-1

  

图12-2

  如以上两图所示,DMA1有7个通道,DMA2有5个通道,每个通道都对应着不同的外设请求。既然这样,就很有可能出现多个外设同时请求同一通道的情况。这响应先后顺序该如何处理是好?那么,这就涉及到仲裁器管理了,用仲裁器来处理请求响应先后的问题。需要分两个阶段,第一阶段是在DMA_CCRx寄存器中设置通道优先级,第二阶段则需要判断其通道编号,编号越低优先级越高。还有一点,DMA1优先级要高于DMA2。

  DMA传输方向

  DMA传输方向有三个:外设到内存,内存到外设,内存到内存。

  外设到内存。即从外设读取数据到内存。例如ADC采集数据到内存,ADC寄存器地址为源地址,内存地址为目标地址。

  内存到外设。即从内存读取数据到外设。例如串口向电脑发送数据,内存地址为源地址,串口数据寄存器地址为目标地址。此时内存存储了需要发送的变量数据。

  内存到内存。以内部flash向内部sram传输数据为例,此时内部flash地址即为源地址,内部sram地址即为目标地址。同时,需要将DMA_CCRx寄存器的MEM2MEM置位。

  传输配置

  我们需要确定数据每次传输的量,这个参数由DMA_CNDTRx寄存器配置。

  再者,还有一个源地址和目标地址数据宽度的参数配置。由DMA_CCRx的PSIZE位和MSIZE位配置。可配置为8位、16位、32位。源地址和目标地址的数据宽度需要一致才可传输。

  此外,数据想有序地传输,还需要配置源和目标数据指针的增量模式。由DMA_CCRx寄存器的PINC位和MINC位配置。例如串口向电脑发送数据,内存中的地址指针应该递增的发送数据,而串口外设只有一个,所以外设的地址指针不变,无递增。

  传输状态标识

  可以通过查询DMA_ISR寄存器的相应位的值来判断传输状态。如果在DMA_CCRx寄存器的相应位使能了相应中断,则会产生中断。

  另外,传输完成还分成一次传输完成和循环传输完成。DMA在传输完成后,需要失能DMA后重新配置才能继续传输。具体配置由DMA_CCRx寄存器的CIRC位完成。

  DMA_InitTypeDef

  /**

  * @brief DMA Init structure definition

  */

  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编程时,需要对结构体成员进行配置。

  内存到外设的DMA数据传输实验

  既然之前写过串口通讯编程的相关文章,那干脆用串口外设来和内存进行数据传输吧。这里简单讲解一个从内存读取数据到外设的DMA传输实验,并且在实验里用led灯验证DMA传输时不占用CPU。

  这里关于USART的配置就不继续赘述,之前的文章有详细的介绍,可移步阅读。直接开始DMA配置。

  #define SENDBUFF_SIZE 5000 //传输的数据量

  uint8_t SendBuff[SENDBUFF_SIZE];

  //内存里等待传输数据的数组

  void USART_DMA_Config(void)

  {

  DMA_InitTypeDef DMA_InitStructure;

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

  // 串口外设为目标地址

  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART1_BASE + 0x04;

  // 内存为源地址

  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuff;

  // 传输方向,即从内存读取数据到串口外设

  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

  // 数据传输量,初始化DMA_CNDTRx寄存器

  DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;

  // 外设地址不递增

  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_High;

  // 不使用内存到内存模式

  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

  // DMA通道配置

  DMA_Init(DMA1_Channel4, &DMA_InitStructure);

  // 清除传输完成标志位,避免产生不必要干扰

  DMA_ClearFlag(DMA1_FLAG_TC4);

  DMA_Cmd(DMA1_Channel4, ENABLE);

  }

  在main函数里调用USART、DMA、LED等的配置函数,用循环的方式往内存的数组里填充SENDBUFF_SIZE(5000)数量的字符作为等待传输的数据。然后调用库函数USART_DMACmd()向DMA发出USART_DMAReq_Tx请求。同时,可以设置led频闪状态作为在DMA传输时占用CPU的进程,以验证DMA传输不占用CPU。

  程序编译完成烧写到开发板后,能看到在DMA传输过程中led同时也在频闪,说明DMA传输过程确实不占用CPU资源,可以边传输边运行其他任务。

  PS:有关led闪烁的延时控制,可用普通的软件延时也可用SysTick定时器来完成,也很简单。有关SysTick定时器的应用在之前的文章有过介绍,可移步阅读。

       分享一些关于DMA在数据传输方面的资料作为参考   

       stm32 如何用DMA搬运数据
       http://www.makeru.com.cn/live/detail/1484.html?s=45051

01-25 17:40