DMA简介
DMA,即Direct Memory Access,是一种在无需CPU参与的情况下,将数据在存储器(单片机的RAM)和外设(一般是I/O设备)之间高效传输的硬件机制。实现这种功能的集成电路单元叫做DMA Controller,即DMA 控制器。
DMA的优势
一般情况下,为了实现单片机的RAM和外设之间的数据传输,有三种常用的方法:轮循法(polling),中断法(interrupt)以及本文要介绍的DMA方法。
轮循法(polling):在主循环中,CPU不断检查外设的相关标志位,来判断其是否需要进行数据的传输,如果有,则CPU将数据在外设和内存之间搬运,实现数据传输。当数据传输服务请求频繁或者的数据量很大时,会影响其他任务的实时性。由于其他任务的存在,也会影响数据传输的实时性。
中断法(interrupt):当外设需要传输数据时,会触发中断,CPU会暂停正在处理的任务,转而去处理外设的数据传输任务。CPU无需反复检查外设的标志位,中断机制会指示CPU何时去处理外设数据,但是依然需要CPU去完成数据搬运和传输过程。当外设数据传输服务发生不频繁,且数据量不大时,中断法也是不错的选择。当中断连续不断且频繁发生时,中断法变得不再高效,因为在恢复主流程的执行和中断响应的上下文切换上会占用大量CPU时间。
DMA传输法:DMA控制器是一种单片机中的硬件单元,他的功能就是允许I/O外设和存储器之间高效传输数据,且传输过程中无需CPU参与。
DMA的工作原理
- 当一个IO设备需要发送数据给存储器或者从存储器读取数据时,它会给DMA 控制器发送一个DRQ请求。DMA控制器收到请求后,向CPU发送HLD请求,要求CPU放弃对总线的使用,因为DMA控制器传输数据时,要使用系统总线。
- CPU收到DMA控制器的HLD请求后,让出总线使用权给DMA控制器,并向DMA控制器响应HLDA信号。
- 当DMA控制器收到CPU的HLDA信号后,DMA控制器会通知IO设备一个DACK信号,告知IO设备可以进行数据的传输。然后DMA控制器占用系统总线,完成IO设备和存储器之间的数据传输。
- 当数据传输完成后,DMA控制器向CPU申请中断,告知CPU数据传输完成。同时CPU恢复系统总线的使用权。
从上描述可以发现:
- 在数据传输过程中,CPU完全不需要参与,只有传输完成后才有必要参与进来,大大的节约了CPU时间。
- DMA数据传输请求总是IO外设发起的。
STM32F103的DMA
STM32F103系列单片机最多拥有2个DMA控制器DMA1和DMA2。其中DMA1有7个通道,DMA2有5个通道。注意DMA2仅在大容量和互联型型号中才存在,例如STM32F103x8, STM32F103xB只有DAM1,而STM32F103xC, STM32F103xD, STM32F103xE有DMA1和DMA2。
每个DMA控制器下有多个通道,DMA通道是实现外设与存储器之间数据传输的管道。通道之间的是相互独立工作的。所以在具体实现某个外设与存储器之间DMA传输时,就需要围绕通道展开,配置相关通道的寄存器。
每个DMA通道可允许多个外设与存储器之间进行数据传输,但任意时刻只能选择其中一个来进行。详见《参考手册》的DMA请求映像。如下图所示可以发现,STM32F103的DMA1的通道1允许ADC1、TIM2_CH3或者TIM4_CH1与存储器之间进行数据传输。
通道数据传输方向
通道的数据传输方向主要有两种:
- 存储器到外设:例如USART使用DMA方式发送数据,待发送的数据可能是字符串或者一个字节数组。当使用DMA发送时,这些数据从存储器按字节为单位依次传输到USART的发送缓冲寄存器TDR进行发送。
- 外设到存储器:例如USART使用DMA方式接收数据,会使用一个字节数组来缓存收到的数据。当使用DMA接收时,收到的数据从USART的接收缓冲寄存器RDR依次传输到存储器进行缓存。
通道传输方向通过DMA_CCRx寄存器的DIR位进行选择:
- DMA_CCRx.DIR=0:外设为数据源,方向为外设到存储器
- DMA_CCRx.DIR=1:存储器为数据源,方向为存储器到外设
使用标准外设库进行配置时,通过如下代码进行设置:
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //外设是数据目的地,存储器->外设
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设是数据源,外设->存储器
通道外设和存储器的数据位宽
DMA通道传输数据的时候,每次传输需要从外设读出数据写入到存储器,或者相反从存储器读出数据写入到外设,那么每次读/写的单个数据占多少字节大小呢?这分别通过设置【外设数据宽度】和【存储器的数据宽度】来指定。
如果【外设数据宽度】和【存储器的数据宽度】是一样的,则很好理解,无需详细讨论。但如果不一样,则在传输到目的地时会发生高位补0或者截断高位的情况。如下表所示。
数据源宽度(位) | 源数据 | 数据目的地宽度(位) | 目的地数据 |
8 | 0xB1 | 16 | 0x00B1 |
8 | 0xB1 | 32 | 0x000000B1 |
16 | 0xB1B2 | 8 | 0xB2 |
32 | 0xB1B2B3B4 | 8 | 0xB4 |
如果我们把地址自增的情况也考虑进来,就会发现数据宽度也影响了地址自增时,地址值的增量大小。以存储器为例,地址模式设置为自增模式。
- 如果数据宽度为 8位,则每次读/写一个字节后,地址值增加1。
- 如果数据宽度为16位,则每次读/写两个字节后,地址值增加2。
- 如果数据宽度为32位,则每次读/写四个字节后,地址值增加4。
外设数据宽度通过DMA_CCRx.PSIZE设置:
- DMA_CCRx.PSIZE=00:8位
- DMA_CCRx.PSIZE=01:16位
- DMA_CCRx.PSIZE=10:32位
- DMA_CCRx.PSIZE=11:保留
存储器数据宽度通过DMA_CCRx.MSIZE设置:
- DMA_CCRx.MSIZE=00:8位
- DMA_CCRx.MSIZE=01:16位
- DMA_CCRx.MSIZE=10:32位
- DMA_CCRx.MSIZE=11:保留
使用标准外设库进行配置时,通过如下代码进行设置:
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度8位
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //16
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; //32位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; //32位
通道外设/存储器地址与地址生成算法
在进行传输前,需要为通道设置好外设基地址和存储器基地址,同时也要设置外设地址和存储器地址在每次传输一个数据时应该怎样变化。这样DMA才知道每次应该从哪个地址去取数据并传输到哪里。
例如,我们使用DMA实现USART发送数据,待发送的数据缓存在字节数组DMA_txbuff中,USART的发送缓冲寄存器为USART_TDR,则存储器基地址为DMA_txbuff数组在内存中的地址,外设地址为USART_TDR寄存器的地址。USART通过DMA发送一个字节时传输时,第一次会从DMA_txbuff [0] 取出一个字节传输到USART_TDR,然后下一次会从DMA_txbuff [1]取出一个字节传输到USART_TDR,因此可以发现,传输时存储器地址是自增的,而外设地址则是固定的。所以这就是地址生成算法的两种选择:地址递增模式和地址固定模式。
一般情况下,我们会设置DMA通道的外设地址是固定的,而存储器地址是递增变化的。
请注意,即便是在地址自动递增模式下,DMA_CPARx/DMA_CMARx寄存器的值都不会随着传输的进行而自动改变,除非代码重新给他们赋值。DMA通道通过内部的隐藏的地址游标寄存器进行递增变化来实现地址递增模式的,为了方便描述本文把这种隐藏寄存器叫做地址游标寄存器。也就是说实际传输时DMA是通过地址游标寄存器去找数据的,DMA_CPARx/DMA_CMARx寄存器都只是记录一个基地址而已。
例如对于存储器的地址递增模式,每次给DMA_CMARx寄存器进行赋值时,内部隐藏的地址游标寄存器DMA_CMARx_cursor也会被赋值。假设初始化 DMA_CMARx=0x01,则DMA_CMARx_cursor=0x01,传输一个字节后,DMA_CMARx依然为0x01,而DMA_CMARx_cursor=0x02。如此下去。
外设与存储器基地址设置分别通过DMA_CPARx寄存器和DMA_CMARx寄存器设置:
- DMA_CPARx = (uint32_t) &USART_TDR ;
- DMA_CMARx = (uint32_t) &DMA_txbuff ;
如果使用标准外设库,则代码:
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DR); //外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART1_DMA_rxbuf; //存储器基地址
外设与存储器地址生成算法的选择分别通过PINC与MINC设置,如下:
- DMA_CCRx.MINC=0:存储器地址递增模式
- DMA_CCRx.MINC=1:存储器地址固定模式
- DMA_CCRx.PINC=0:外设地址递增模式
- DMA_CCRx.PINC=1:外设地址固定模式
如果使用标准外设库,则代码:
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址递增
通道数据传输长度
通道在执行一次传输任务时,必须在初始化时指定传输长度,即本次传输需要传输多少个基本数据。
DMA_CNDTRx寄存器用于设置DMA通道的初始传输长度,在DMA传输过程中,也用于表示还剩多少数据没有完成传输。只有在通道关闭的情况下DMA_CNDTRx寄存器才能改写,一旦通道使能,DMA_CNDTRx寄存器为只读的,并在每个 DMA 传输之后值减 1。如果该寄存器的值为 0,无论通道开启与否,都不会有数据传输。
DMA_CNDTRx寄存器最大值为65535,因此通道的单次传输长度最大为65535个数据。
在使用标准库时,通过如下代码操作DMA_CNDTRx寄存器:
//在通道配置时初始化DMA_CNDTRx寄存器,设置传输长度
DMA_InitStructure.DMA_BufferSize = 255;
//设置通道的DMA_CNDTRx寄存器,设置传输长度
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
//读取通道的DMA_CNDTRx寄存器,读取剩余传输长度
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
通道的传输模式
通道有两种传输模式,一种是在完成指定的传输长度后就停止,另一种是每次完成初始指定的传输长度时又立刻恢复到初始状态继续提供DMA服务。这就是非循环模式和循环模式。
当通道配置为非循环模式时,每传输一次,DMA_CNDTRx寄存器就减少1,当传输结束,即DMA_CNDTRx寄存器变为0时,无法再接受外设的DMA传输请求。要开始新的DMA传输,需要在关闭DMA通道的情况下,向DMA_CNDTRx寄存器中重新写入新的传输长度,并再次开启DMA通道才能进行新的传输任务。
而在循环模式下,每轮传输结束时,即当DMA_CNDTRx寄存器为0的瞬间,DMA_CNDTRx寄存器的值会立刻自动加载为其初始数值(最近一次被赋值的值),因此DMA通道也就一直处于提供传输服务状态。另一方面,内部隐藏的地址游标寄存器也都会被重新加载,即设置 DMA_CPARx_cursor = DMA_CPARx ,DMA_CMARx_cursor = DMA_CMARx。简而言之就是自动重新开始一次初始化状态下的新的传输。
那么问题来了,循环模式下,DMA_CNDTRx寄存器是怎么知道其初始值的呢?这里需要补充说明:DMA_CNDTRx寄存器带有有一个隐藏的备份寄存器,暂且叫他DMA_CNDTRx_backup,在每次DMA_CNDTRx寄存器被赋值时,DMA_CNDTRx_backup会保存其初始值,当DMA_CNDTRx寄存器为0的瞬间,就会有DMA_CNDTRx = DMA_CNDTRx_backup。
通过DMA_CCRx.CIRC位来选择循环模式和非循环模式:
- DMA_CCRx.CIRC=0:非循环模式
- DMA_CCRx.CIRC=1:循环模式
在标准库开发时,通过代码:
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //非循环模式
通道软件优先级
同一个DMA控制器下可能会启动多个DMA通道,当同时有多个通道需要执行DMA传输请求时,就需要仲裁哪一个优先执行。这便是通过软件优先级来实现的。
当通道软件优先级不同时,以软件优先级来决定哪个通道的优先传输,软件优先级高的先被响应。当软件优先级相同时,那就看硬件优先级,通道号越低的硬件优先级越高,例如通道0硬件优先级高于通道1。硬件优先级详见《参考手册》DAM请求映像。
通道的优先级在DMA_CCRx.PL中设置,有4个等级:
- DMA_CCRx.PL=11:最高优先级
- DMA_CCRx.PL=10:高优先级
- DMA_CCRx.PL=01:中等优先级
- DMA_CCRx.PL=00:低优先级
使用标准外设库进行配置时代码如下:
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //设置通道优先级
通道的启用和禁用
一旦启动了DMA通道,它就可响应连到该通道上的外设的DMA请求。
DMA_CCRx.EN位用于使能DMA通道。标准外设库代码如下:
DMA_Cmd(DMA1_Channel5, ENABLE); //开启DMA通道
DMA_Cmd(DMA1_Channel5, DISABLE); //关闭DMA通道
DMA中断
DMA传输过程中可以触发三种中断:
- 传输完成中断(TC):随着传输进行,当DMA_CHxCNT递减为0时,触发传输完成中断。循环模式时每轮DMA_CHxCNT递减为0也会触发。
- 传输半完成中断(HT):假设设置的传输长度DMA_CHxCNT的值为N,则当传输完成一半(整数除法,N/2,例如7/2=3)后,触发传输半完成中断。
- 传输错误中断(TE):传输过程中发生错误时触发此中断。
通道也有一个全局中断标志GIF,GIF没有对应的中断使能,所以它不能独立触发中断。当TCIF、HTIF、TEIF任意中断标志置位时,GIF硬件置位。当清除GIF标志时,也会一并清除TCIF、HTIF、TEIF。
寄存器DMA_IFCR用于软件清除中断标志,写0无效,写1清除对应标志。
//使能或者禁用指定通道的某个中断:TC、HT、TE
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx,
uint32_t DMA_IT,
FunctionalState NewState);
//读取通道指定的中断是否处于挂起状态:TC、HT、TE、GL
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
//清除通道的中断标志:TC、HT、TE、GL
void DMA_ClearITPendingBit(uint32_t DMAy_IT);
DMA代码示例
//使能DMA1外设时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
//DMA相关宏定义
#define USART1_RX_DMA_CH DMA1_Channel5
#define USART1_TX_DMA_CH DMA1_Channel4
#define USART1_DMA_TXBUF_SIZE 255
#define USART1_DMA_RXBUF_SIZE 255
uint8_t USART1_DMA_txbuf[USART1_DMA_TXBUF_SIZE];
uint8_t USART1_DMA_rxbuf[USART1_DMA_RXBUF_SIZE];
//======USART1 RX的DMA通道配置========
DMA_DeInit(USART1_RX_DMA_CH);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(USART1->DR)); //外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART1_DMA_rxbuf; //存储器基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向:外设是数据源,外设->存储器
DMA_InitStructure.DMA_BufferSize = USART1_DMA_RXBUF_SIZE; //传输长度:DMA_CNDTRx寄存器的值
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址生成算法,外设地址不增加
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址生成算法,存储器地址增加
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设寄存器单个数据宽度:Byte代表8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器单个数据宽度:Byte代表8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环传输模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //通道的软件优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //禁止M2M模式
DMA_Init(USART1_RX_DMA_CH, &DMA_InitStructure); //初始化DMA通道
//DMA_ITConfig(USART1_RX_DMA_CH,DMA_IT_TC,ENABLE); //使能传输完成中断
DMA_Cmd(USART1_RX_DMA_CH, ENABLE); //开启DMA通道
//======USART1 TX的DMA通道配置========
DMA_DeInit(USART1_TX_DMA_CH);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(USART1->DR)); //外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART1_DMA_txbuf; //存储器基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向:外设是数据目的地,存储器->外设
DMA_InitStructure.DMA_BufferSize = USART1_DMA_TXBUF_SIZE; //传输长度:DMA_CNDTRx寄存器的值
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址生成算法,外设地址不增加
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址生成算法,存储器地址增加
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设寄存器单个数据宽度:Byte代表8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器单个数据宽度:Byte代表8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //单次传输模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //通道的软件优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //禁止M2M模式
DMA_Init(USART1_TX_DMA_CH, &DMA_InitStructure); //初始化DMA通道
//DMA_ITConfig(USART1_TX_DMA_CH,DMA_IT_TC,ENABLE); //使能传输完成中断
//DMA_Cmd(USART1_TX_DMA_CH, ENABLE); //开启DMA通道