赞
踩
DMA是什么,DMA是Direct memory access的简称意为直接存储器访问 ,用于在外设与存储器之间以及存储器与存储器之间提供高速数据传输。可以在无需任何 CPU 操作的情况下通过 DMA 快速移动数据,节省的 CPU 资源可供其它操作使用。
在实际使用的过程中我比较关心哪些外设可以请求DMA传输,DMA传输的方向,以及传输的数据量大小等参数。
如下表所示即为STM32F4一些外设事件可以请求DMA传输。
可以看到STM32F4有两个DMA控制器,DMA1和DMA2,这两个控制器分别搭载不同的外设进行数据的传输。
比如说本篇文章要使用的是串口1的DMA,通过上面图可以查找到在DMA2的通道4的数据流5可以作为串口1接收的DMA请求,DMA2的通道4的数据流7可以用作串口1发送的DMA请求。再来看下面这个描述图更加加深理解。
从左上角开始看,REQ_STR1_CH0-7代表数据流1的8个通道的请求事件,比如说串口1的发送请求事件就在DMA2的CH4通道4上,同理最左侧都是一些请求事件的通道。然后请求事件的通道具体在DMA的数据流x中,比如说串口1的发送,请求通道是4,数据流通道是7对应REQ_STREAM7,至此我们就知道了串口1的发送请求事件在DMA2的通道4的数据流7上,最后每个 DMA 请求会立即启动对存储器的传输。图中也可以看到DMA使用的是双 AHB 主总线架构,一个用于存储器访问,另一个用于外设访问。
再来看一下传输的方向比较好理解,有外设到存储器,存储器到外设,存储器到存储器,如表。
本篇要实现的是外设到存储器以及存储器到外设。
一般来说使用DMA可以不使用它的FIFO参与传输,FIFO 用于在源数据传输到目标之前临时存储这些数据。每个数据流都有一个独立的 4 字 FIFO,阈值级别可由软件配置为 1/4、1/2、3/4 或满,这里一个字对应32位二进制数即一个字是4个字节。
不使用FIFO时,不管是哪个传输方向,发多少个字节就传输多少个字节过去,可以理解为发1个数据就传输1个数据。那FIFO有什么作用呢,可以通过配置FIFO打包数据,自动封装/解封必要的传输数据来优化带宽。
比如说串口1的接收要使用DMA传输(外设到存储器)同时配置了FIFO的阈值为4个字节,那么只有当往串口1中写了4个字节的内容时DMA才把数据传输到自己指定的数组里。当串口1里的数据到了4个字节才启动传输,如果现在只有3个字节,那么这3个字节的内容只会存放在FIFO中。同理当串口里接收到5个字节的内容时,只会有4个字节能被传输到指定的数组里,剩余的1个字节只有等下次凑齐4个字节了才能一起被传递到数组中。
主要是用于源和目标存储宽度不一致的情况。
DMA中的数据传输方式有单次传输和突发传输。可以产生单次传输或 4 个、8 个和 16 个节拍的增量突发传输,关于这个增量突发传输我的理解也还不够,主要是参考了如下帖子进行总结和归纳:
1、正点原子论坛
2、CSDN论坛
先贴一下官方的介绍:
上图的PSIZE[1:0]指的是外设数据大小,PBURST[1:0]指外设突发传输配置,MBURST指存储器突发传输配置。
单次传输:在一般的DMA传输中都是使用这个模式,此模式就是把一次请求DMA作为一个节拍来传输一节拍的数据,例如串口1的接收,源数据即外设端口是8位宽度,单次传输一次就是请求发送一次DMA就是传递一次8位宽度(一个字节)的数据。
突发传输:突发传输是传输4 个、8 个和 16 个节拍的数据,需要注意的是突发大小指示突发中的节拍数,而不是传输的字节数,并不是单纯理解的传输4、8、16个字节。比如串口1的接收,源数据假设外设端口设置16位宽度(两个字节)即半字的长度,配置突发传输的是4个节拍,每一个节拍对应的是两个字节,突发传输4个节拍传输了八个字节的数据。
两种传输方式的区别:除了传输数据宽度不同之外
其一是单次传输每次传输都需要跟总线仲裁提要求,总线仲裁才来安排传输,这样每次传输一个数据请求一次总裁,可能对大规模的数据传输造成影响。而突发增量传输就规定了总线的一次权限可以使用多久(可以连续传输几个数据),而不会由于失去权限,不需要每个数据都请求一次总线仲裁。
其二是在单次传输过程中由于一个字节一个字节传输,传输过程中如果发生被打断的情况,会遇到数据出错的风险。而突发传输在一次突发传输期间,相应的总线是被锁定的,在突发传输完成之后,总线矩阵会再次分配权限,响应其他外设的请求,不会被打断降低错误率。
实际使用中我用到现在都是用的单次传输没有什么问题,至于配置为突发传输其实是DMA内部对传输数据进行处理并对总线进行锁定后再传输,若配置为突发传输也没什么问题,不管设置什么都可以传输,这里只是更好的理解一下为下面的参数理解做个说明。
DMA参数这里只列举DMA_InitTypeDef结构体参数:
typedef struct { uint32_t DMA_Channel; 通道选择 uint32_t DMA_PeripheralBaseAddr; DMA外设地址 uint32_t DMA_Memory0BaseAddr; DMA 存储器0地址 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; DMA模式 uint32_t DMA_Priority; DMA优先级 uint32_t DMA_FIFOMode; DMAFIFO模式 uint32_t DMA_FIFOThreshold; FIFO阈值 uint32_t DMA_MemoryBurst; 存储器传输数据模式 uint32_t DMA_PeripheralBurst; 外设传输数据模式 }DMA_InitTypeDef;
参数 | 说明 |
---|---|
通道选择 | 可以选择通道0-7(DMA_Channel_0…7) |
DMA外设地址 | 需要指定外设地址(如串口的DR寄存器 (u32)&USART1->DR) |
DMA 存储器0地址 | 可以自己定义储存DMA接收/发送的数组(如(u32)SendBuff 取数组的首地址),这里赋值的是存储器0,对应的还有一个存储器1是用作DMA双缓冲的,还没用到暂时还不需要配置在第四节会说明。 |
传输方向 | 三种方向可以选择 |
数据传输量 | 指DMA 数据流 x 每次传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为 16 位,那么传输一次(一个项)就是 2 个字节。 |
外设增量模式 | 外设地址是否递增(一般外设地址是固定的不增量) |
存储器增量模式 | 存储器地址是否递增(一般是需要递增的,才能把数据依次发送或者接收) |
外设数据长度 | 数据位宽设置(一般外设都可以设置8位位宽) |
存储器数据长度 | 数据位宽设置(数据接收没啥特殊要求都可以设置为8位位宽) |
DMA模式 | 有普通模式和循环模式,第三节介绍 |
DMA优先级 | 开启了多个DMA传输时根据这个来判断优先级(单纯只开了一个DMA随便设置),若多个DMA这里设置相同则根据数据流编号来决定,越小优先级越高 |
DMAFIFO模式 | 可以开启或者关闭FIFO模式,如上一节介绍的 |
FIFO阈值 | 开启了FIFO模式设置这个才有效,控制阈值来发送数据。没开启FIFO设置无效 |
存储器传输数据模式 | 单次传输或者突发4、8、16节拍传输,如上一节介绍的(一般设置为单次传输) |
外设传输数据模式 | 单次传输或者突发4、8、16节拍传输,如上一节介绍的(一般设置为单次传输) |
DMA结构体配置
#define SEND_BUF_SIZE 10 //发送数据长度 u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区 //....... RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能 while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE){}//等待DMA可配置 DMA_InitStructure.DMA_Channel = DMA_Channel_4; //通道选择 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART1->DR;//DMA外设地址 DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff;//DMA 存储器0地址 DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式 DMA_InitStructure.DMA_BufferSize = SEND_BUF_SIZE;//数据传输量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式 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_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输 DMA_Init(DMA2_Stream7, &DMA_InitStructure);//初始化DMA Stream USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
启动DMA传输后,将数组SendBuff里面的10个字节的数据一个一个传送到串口中,通过串口发送出来,由于配置的是普通模式,DMA传输完成后会停止传输,如果想再次启动传输需要通过以下函数实现:
DMA_Cmd(DMA_Streamx, DISABLE); //关闭DMA传输
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){} //确保DMA可以被设置
DMA_SetCurrDataCounter(DMA_Streamx,ndtr); //数据传输量
DMA_Cmd(DMA_Streamx, ENABLE); //开启DMA传输
先关闭DMA传输,等待DMA_SxCR 寄存器中EN为0,重新配置数据传输量,再开启传输,实现下一次DMA数据发送。
一般来说都是DMA普通模式发送完一次DMA再次启动都是按照上述的流程,而且在STM32F4参考手册里也提到需要重新对数据流配置并使能:
但是我在实际测试中发现初始化结构体后设置了数据量的大小,第一次DMA正常发送,第二次想要再次发送时不配置数据量单纯的把EN清零和使能也能正常发送,这一点还无法理解原因。
还有一点是设置数据量的大小,发现如果设置的数据量大于要发生的数据也是能正常发送的。还是按照规范要发多少就指定多大的数据量把。
接收需要调整一下配置的结构体:
#define SEND_BUF_SIZE 10 //发送数据长度 u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区 //....... RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能 while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE){}//等待DMA可配置 DMA_InitStructure.DMA_Channel = DMA_Channel_4; //通道选择 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART1->DR;//DMA外设地址 DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff;//DMA 存储器0地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器 DMA_InitStructure.DMA_BufferSize = SEND_BUF_SIZE;//数据传输量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输 DMA_Init(DMA2_Stream5, &DMA_InitStructure);//初始化DMA Stream USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); //使能串口1的DMA接收 DMA_Cmd(DMA2_Stream5, ENABLE); //开启DMA传输
改为串口接收,只需要将DMA数据流改为5,传输方向改为外设到内存,这里将DMA模式改为循环模式。
在循环模式下,串口每接收一个数据时都会把数据放到数组里,当数据量为0时,代表DMA传输完成,由于是循环模式不需要重新配置DMA,下一个数据进来会继续放到数组的首地址依次存放数据。相当于串口接收到的数据会不停的覆盖整个接收的数组,当一次DMA传输还没达到设置的数据量时,下一个数据会接着往下一个地址存放,当达到设定的数据量时,下一个数据就重新从数组的首地址开始存放。
如图设置了10个字节的数据量,但是串口只收到8个数据
当再次发送5个数据时,由于超过了设置的数据量又设置了循环模式,会从数组首地址在存放
需要注意的是DMA_Cmd这个函数,在使能数据流后DMA传输会马上开始执行,所以DMA做数据发送时会把它放在需要发送的时候调用,做接收时可以直接放在初始化后调用。
还是上面配置的DMA串口1接收,修改FIFO模式和阈值其余和3.2小节配置的相同:
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
开启FIFO模式,配置阈值为四分之一字(4个字节)。当串口收到的内容还没到4个字节时DMA不会立即把数据传输到数组里,会先暂时存放在FIFO中,当串口又收到一个字节时,此时达到FIFO的阈值4字节,DMA会把数据传输到数组里,如下两图所示
串口接收3个字节数据在数组里不会显示
收到第四个字节时达到FIFO阈值后一起显示
类似的如果这时候再发送5个字节,数组里只会出现4个数据如下图
可以看到一次只更新4个字节,这就是一个DMAFIFO模式的用法
由于数据传输模式单次和突发传输最终得到的效果是一样的这里不做演示。实际使用用两者都可以实现数据传输,我用大部分是单次传输。
DMA双缓冲模式,这里演示用串口1接收,把数据放到两个内存中。
DMA结构体配置
#define SEND_BUF_SIZE 10 //发送数据长度 u8 SendBuff[2][SEND_BUF_SIZE]; //发送数据缓冲区 //....... RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能 while (DMA_GetCmdStatus(DMA2_Stream5) != DISABLE){}//等待DMA可配置 DMA_InitStructure.DMA_Channel = DMA_Channel_4; //通道选择 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&USART1->DR;//DMA外设地址 DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff[0];//DMA 存储器0地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式 DMA_InitStructure.DMA_BufferSize = 5;//数据传输量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用循环模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输 DMA_Init(DMA2_Stream5, &DMA_InitStructure);//初始化DMA Stream //双缓冲 DMA_DoubleBufferModeConfig(DMA2_Stream5, (uint32_t)SendBuff[1], DMA_Memory_0); //双缓冲模式,buffer0先被传输 DMA_DoubleBufferModeCmd(DMA2_Stream5, ENABLE); USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); DMA_Cmd(DMA2_Stream5, ENABLE);
说明一下,双缓冲我用的二维数组做接收,记得之前说的一般初始化时初始的是存储器0,双缓冲还需要指定一个存储器1,这里将SendBuff[0]作为存储器0的地址,SendBuff[1]作为存储器1的地址。通过绑定存储器1,使能双缓冲模式就可以开启DMA双缓冲的功能。为方便演示,设置了数据量为5,当存储器0的5个空间存满后,剩余的数据会自动存放到存储器1中,如下图:
当超出了设置的数据量后,由于是循环模式又会和上一节的DMA接收一样,继续从数组首地址存放如图:
双缓冲模式可以用在很多地方,比如ADC等,相对于单缓冲来说可以避免数据被覆盖。上面的例子只是简单的接收了一下基本的使用方法。
本篇对STM32 DMA做了介绍和运用,没有配合到DMA中断,算是比较基本的一些用法,后续再补充吧
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。