DMA——直接存储器访问
DMA:Data Memory Access, 直接存储器访问。
DMA和我们之前学过的串口、GPIO都是类似的,都是STM32中的一个外设。串口是用来发送通信数据的,而DMA则是用来把数据从一个地方搬到另一个地方,而且不占用CPU。
举个例子:
我们如果要把一串数据发送给串口 ,CPU先要把这一串数据先一个一个取回来暂存在CPU中的寄存器中,然后再一个一个发送给串口。
这样就会导致CPU不能做其他事情,CPU一直处于被占用的状态。
当DMA出现后,CPU只需要给DMA发送一条命令,如将数据发送给串口,然后DMA就来完成这个上述需要CPU完成的工作了。这就节省了CPU的资源来完成其他操作。
上面我们解释完DMA是什么后,我们接着来看DMA具体有几种方式。
1.P->M(外设到存储器)
后面的ADC数据采集使用的是这一类
2.M->P(存储器到外设)
之前的串口发送实验属于这一类
3.M->M(存储器到存储器)
存储器到存储器的实验一会儿在后面讲
我们先来看一下DMA的功能框图
DMA功能框图讲解
我们先把DMA的功能框图分为三大主要部分来讲解:1.DMA请求。2.通道。3.仲裁器
DMA请求
通过上面的DMA框图我们可以看到,DMA请求通常是由外设发起的,例如串口、GPIO、ADC等等。
那么具体的外设是如何发送DMA请求的呢?
这里就要先提一下通道这个概念,不同的外设通过具体的通道向DMA控制器发送请求,然后DMA控制器再根据通道的优先权来处理请求。
DMA有DMA1和DMA2两个控制器,DMA1有7个通道,DMA2有5个通道,不同的DMA控制器的通道对应这不同的外设请求。
从上图可以看出,芯片的参考手册已经帮我们把不同的外设分别对应的通道分配好了,我们使用的时候只需要去查询这个表就行了。
仲裁器
虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能接收多个。
当多个通道同时向DMA控制器发送DMA请求的时候,谁可以先使用DMA控制器呢?这时就需要仲裁器来判断通道的优先级了。
仲裁器管理DMA通道请求分为两个阶段。
第一阶段属于软件阶段,可以在DMA_CCRx寄存器中设置,有4个等级:非常高、高、中和低四个优先级。见下图
第二阶段属于硬件阶段, 如果两个或以上的DMA通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道0高于通道1。
DMA数据配置
使用DMA,最核心的就是配置要传输的数据,包括数据从哪里来,要到那里去,传输数据的单位是什么,要传多少数据,是一次传输还是循环传输等等
从哪里来到那里去?
我们知道DMA传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。 具体的传输方向DMA_CCR位4 DIR配置:0表示从外设到存储器,1表示从存储器到外设。 这里面涉及到的外设地址由DMA_CPAR配置,存储器地址由DMA_CMAR配置。
我们在使用固件库编程的时候,通常不会直接配置这三个寄存器,都是通过初始化结构体来配置的,这三个寄存器对应的结构体变量如下图:
配置内容 | 对应寄存器 |
---|---|
外设地址 | DMA_CPAR |
存储器地址 | DMA_CMAR |
传输方向 | DMA_CCR:DIR |
外设到存储器
当我们使用从外设到存储器传输时,以ADC采集为例。DMA外设寄存器的地址对应的就是ADC数据寄存器的地址, DMA存储器的地址就是我们自定义的变量(用来接收存储AD采集的数据)的地址。方向我们设置外设为源地址。
存储器到外设
当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。DMA外设寄存器的地址对应的就是串口数据寄存器的地址, DMA存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。
存储器到存储器
当我们使用从存储器到存储器传输时,以内部FLASH向内部SRAM复制数据为例。 DMA外设寄存器的地址对应的就是内部FLASH(我们这里把内部FALSH当作一个外设来看)的地址, DMA存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部FLASH的数据)的地址。 方向我们设置外设(即内部FLASH)为源地址。跟上面两个不一样的是,这里需要把DMA_CCR位14:MEM2MEM:存储器到存储器模式配置为1,启动M2M模式。
数据要传多少,传的单位是什么?
配置内容 | 对应寄存器 |
---|---|
传输数目 | DMA_CNDTR |
外设地址是否递增 | DMA_CCRx:PINC |
存储器地址是否递增 | DMA_CCRx:MINC |
外设数据宽度 | DMA_CCRx:PSIZE |
存储器数据宽度 | DMA_CCRx:MSIZE |
当我们配置好数据要从哪里来到哪里去之后,我们还需要知道我们要传输的数据是多少,数据的单位是什么。
以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由DMA_CNDTR配置, 这是一个32位的寄存器,一次最多只能传输65535个数据。
要想数据传输正确,源和目标地址存储的数据宽度还必须一致
串口数据寄存器是8位的, 所以我们定义的要发送的数据也必须是8位。
外设的数据宽度由**DMA_CCR的PSIZE[1:0]**配置, 可以是8/16/32位,
存储器的数据宽度由**DMA_CCR的MSIZE[1:0]**配置,可以是8/16/32位。
那么当PSIZE和MSIZE不相同时,也能传输,但是可能传输结果与你的预期不同,我们可以参照下表来查询:
在DMA控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。
外设的地址指针由DMA_CCRx的PINC配置,存储器的地址指针由MINC配置。以串口向电脑发送数据为例,要发送的数据很多, 每发送完一个,那么存储器的地址指针就应该加1,而串口数据寄存器只有一个, 那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。
什么时候传输完成?
配置内容 | 对应寄存器 |
---|---|
模式选择(一次传输、循环传输) | DMA_CCRx:CIRC |
传输过半、传输完成、传输出错标志位 | DMA_ISR |
数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来鉴别。
每个DMA通道在DMA传输过半、 传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。
有关各个标志位的详细描述请参考DMA中断状态寄存器DMA_ISR的详细描述。
传输完成还分两种模式,是一次传输还是循环传输,
一次传输很好理解,即是传输一次之后就停止,
要想再传输的话, 必须关断DMA使能后再重新配置后才能继续传输。
循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输, 不断的重复。
具体的由DMA_CCR寄存器的CIRC 循环模式位控制。
以上就是所有关于DMA的理论部分,下面我来进行程序编写。我先设计一个程序将存储器中的数据通过DMA的方式发送给串口。
我依然按照我上面讲DMA数据配置的顺序来写程序。
DMA(存储器到外设)实现代码
首先创建一个数组作为数据源,一会儿发送给串口
//准备将存储器中的这个数组中的数据发送到串口
u32 SourceBuffer[500];
然后开始配置DMA,上面我们讲DMA数据配置的时候已经说过了,我们配置DMA的时候是通过向DMA初始化结构体中填数据来配置的,所以我们先定义一个DMA初始化结构体,然后打开DMA外设的时钟信号。
//定义DMA初始化结构体
DMA_InitTypeDef DMA_initStruct;
//首先查询DMA挂载在哪根总线上,然后打开DMA时钟信号
RCC_AHBPeriphClockCmd(RCC_AHBENR_DMA1EN,ENABLE);
数据从哪里来,到哪里去?
配置外设基地址
存储器基地址
数据传输方向
//通过上一篇文章对串口(USART)介绍的文章,可以找到串口数据发送的寄存器是放在USART_DR中的
DMA_initStruct.DMA_PeripheralBaseAddr = USART1_BASE + 0x04;
//配置存储器的基地址,也就是数组名
DMA_initStruct.DMA_MemoryBaseAddr = (u32)SourceBuffer;
//配置DMA数据传输方向,从存储器到外设,所以是外设Destination
DMA_initStruct.DMA_DIR = DMA_DIR_PeripheralDST;
数据要传多少,传的单位是什么?
我就不赘述了,请看代码中的注释
//配置发送的数据大小为500,因为数组大小为500
DMA_initStruct.DMA_BufferSize = 500;
//外设地址不用自增,因为串口的发送寄存器只有一个字节这么大
DMA_initStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
//配置外设的数据宽度为字节
DMA_initStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
//由于数组是按顺序发送,所以存储器的地址可以自增
DMA_initStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
//由于数组中的数据类型为u32,所以存储器的数据宽度配置为字
DMA_initStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
什么时候传输完成?
//发送模式配置为发送一次,建议不要配置成循环发送,因为我的查看串口发送信息的工具会卡死
DMA_initStruct.DMA_Mode = DMA_Mode_Normal;
//优先级随便配置,没人跟你抢
DMA_initStruct.DMA_Priority = DMA_Priority_High;
//存储器到存储器的单独配置位,这里我们是存储器到外设,所以这个位为disable
DMA_initStruct.DMA_M2M = DMA_M2M_Disable;
上面配置好后,才算完成DMA初始化结构体的配置,接下来调用初始化函数。并给通道使能信号
//调用初始化函数
DMA_Init(DMA1_Channel4,&DMA_initStruct);
//DMA通道使能信号
DMA_Cmd(DMA1_Channel4,ENABLE);
DMA配置好了,下面来写main函数,注意,串口也是需要配置的。但是由于我在上一篇文章里已经详细讲述过串口的配置方法,在这里我就直接调用串口的配置函数了。
int main(){
u16 i;
//先用一个for循环向数组中填数据,一会儿这个数据会通过串口发送出去
for(i=0; i<500; i++){
SourceBuffer[i]=14;
}
//调用串口配置函数
USART_GPIO_Config();
USART_Config();
//调用DMA配置函数
DMA_Config();
//串口1向DMA发出TX请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}