011 - STM32学习笔记 - DMA直接存储器
1、DMA简介
我们知道stm32在实际应用过程中具有很强大的功能,包含数据的采集、处理、逻辑功能的运算等,因此stm32一直在处理大量的事务,但是在实际使用过程中,我们知道有些事情实际上不需要CPU过多参与,比如:数据的复制和存储,当产生大量数据的时候,CPU如果转过来进行处理的话,那么对于CPU来说时额外的负载,会严重降低CPU的实际效率。
为此stm32提出了DMA(Data Memory Access)的概念,即直接存储器访问,DMA是用来将数据从一个地址空间转到另一个地址空间,并且提供在外设和存储器之间或者存储器和存储器之间的高速数据传输功能。这里外设指的是外设的数据寄存器,存储器指的是片内的SRAM或者FLASH。因此DMA主要功能就是不需要CPU参与,来实现数据的转移,从而避免CPU的过度资源消耗。
先看一下DMA的功能框图
系统中,将DMA分为6个部分:
①、通道+流
流:是数据传输的链路,每个DMA控制器有8条独立的数据流(REQ_STREAM0 ~ REQ_STREAM7),每次传输的数据量最大为65535,假设单位数据单位为字,那么一次就可以传输256Kb。
通道:每个数据流有8个通道(REQ_STRx_CH0 ~ REQ_STRx_CH0),每个通道对应不同的DMA请求。
②、仲裁器
当出现多个数据流时,DMA需要决定哪个数据流先进行传输,哪个后传输,因此增加了仲裁器,对于数据流的优先级,与中断的优先级比较类似,也是从两个方面进行控制:
软件设置:通过DMA_SxCR:PL位,可以设置为:00 - 低;01 - 中;10 - 高;11 - 非常高。
硬件设置:当多个流设置的优先级相同时,根据硬件数据流的编号来决定,编号越小,优先级越高。
③、FIFO
FIFO(First In,First Out),FIFO类似于一个管道,作为源和目标的数据中转站,每个数据流有4个字的FIFO,其中阈值级别可以分为1/4,1/2,3/4或者满,这里的意思是根据FIFO管道的空间使用情况,可以选择一次使用多大容量作为中转站。
阈值容量选择可以通过FIFO控制器DMA_SxFCR:FTH[1:0]进行配置:
00:选择FIFO的1/4;
01:选择FIFO的1/2;
10:选择FIFO的3/4;
11:选择FIFO的完整容量;
注:数据采用FIFO进行传输时,突发模式的配置一定要注意
MSIZE = 字节
当选择FIFO级别为1/4容量时,即管道容量选择为1个字,需要4个字节才能填充满,选择MBURST=INCR4(选择4个字节)时,会发生1次数据传递;
当选择FIFO级别为1/2容量时,即管道容量选择为2个字,需要8个字节才能填充满,选择MBURST=INCR4(选择4个字节)时,发生两次数据传递,或者选择MBURST=INCR8(选择8个字节)时发生1次数据传递;
当选择FIFO级别为3/4容量时,即管道容量选择为3个字,需要12个字节才能填充满,选择MBURST=INCR4(选择4个字节)时,会发生3次数据传递;
当选择FIFO级别为满容量时,即管道容量选择为4个字,需要16个字节才能填充满,选择MBURST=INCR4(选择4个字节)时,发生4次数据传递,选择MBURST=INCR8(选择8个字节)时,发生2次数据传递,选择MBURST=INCR16(选择16个字节)时,发生1次数据传递。
MSIZE = 半字
当选择FIFO级别为1/2容量时,即管道容量选择为2个字,需要4个半字才能填充满,选择MBURST=INCR4(选择4个字节)时,发生1次数据传递
当选择FIFO级别为满容量时,即管道容量选择为4个字,需要8个半字才能填充满,选择MBURST=INCR4(选择4个字节)时,发生2次数据传递,选择MBURST=INCR8(选择8个字节)时,发生1次数据传递。
MSIZE = 字
当选择FIFO级别为满容量时,即管道容量选择为4个字,需要4个字才能填充满,选择MBURST=INCR4(选择4个字节)时,发生1次数据传递。
综上所述:MBUSRT的选择,需要结合MSIZE的大小来确定。
在STM32中,DMA分为两个:DMA1和DMA2
④⑤⑥、存储器接口、外设接口、编程接口
在DMA框图中,剩余3个分别时存储器接口(M接口)、外设接口(P接口)、编程接口,如下图
图中可以看到,DMA1控制器可以通过总线矩阵访问Flash、SRAM、和外部存储器,而DMA1的P接口不经过总线矩阵可以直接访问APB1外设;
在DMA2控制其中,可以看到M接口可以通过总线矩阵访问Flash、SRAM、APB1/2、AHB1/2/3外部存储器),而P接口也可以通过总线矩阵访问Flash、SRAM、APB1/2、AHB1/2/3外部存储器)
从上可以总结出,在DMA控制器中,DMA1可以实现:P->M,M->P,而DMA2则可以实现P->M,M->P,M->M的数据传输。(这里还是有点糊涂,反复看了好几次火哥的视频,还不是很清楚,等后面学习清楚了在做补充)
下来先看一下关于DMA的初始化结构体
/* 位于stm432f4xx_dma.h */
typedef struct
{
uint32_t DMA_Channel; /* 通道选择 */
uint32_t DMA_PeripheralBaseAddr; /* 外设地址 */
uint32_t DMA_Memory0BaseAddr; /* 存储器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; /* 模式选择 */
uint32_t DMA_Priority; /* 优先级 */
uint32_t DMA_FIFOMode; /* FIFO模式 */
uint32_t DMA_FIFOThreshold; /* FIFO阈值 */
uint32_t DMA_MemoryBurst; /* 存储器突发传输 */
uint32_t DMA_PeripheralBurst; /* 外设突发传输 */
}DMA_InitTypeDef;
DMA_Channel,用来选择DMA请求通道,可选通道为0-7,每个外设对应固定的通道。寄存器为DMA_SxCR:CHSEL[2:0]
#define DMA_Channel_0 ((uint32_t)0x00000000)
#define DMA_Channel_1 ((uint32_t)0x02000000)
#define DMA_Channel_2 ((uint32_t)0x04000000)
#define DMA_Channel_3 ((uint32_t)0x06000000)
#define DMA_Channel_4 ((uint32_t)0x08000000)
#define DMA_Channel_5 ((uint32_t)0x0A000000)
#define DMA_Channel_6 ((uint32_t)0x0C000000)
#define DMA_Channel_7 ((uint32_t)0x0E000000)
具体哪个外设选择哪个通道可以参考下面两个表:
DMA_PeripheralBaseAddr,外设地址,寄存器为:DMA_SxPAR。
DMA_Memory0BaseAddr,储存器0地址,寄存器为:DMA_SxM0AR。除了存储器0以外,还有一个存储器1,用于双缓冲区模式,用于大容量数据处理。
DMA_DIR,传输方向选择,可选外设到存储器,存储器到外设以及存储器到存储器,寄存器为:DMA_SxCR:DIR[1:0]。
#define DMA_DIR_PeripheralToMemory ((uint32_t)0x00000000) /* 外设到存储器 */
#define DMA_DIR_MemoryToPeripheral ((uint32_t)0x00000040) /* 存储器到外设 */
#define DMA_DIR_MemoryToMemory ((uint32_t)0x00000080) /* 存储器到存储器 */
编程时需要用到的固件库函数
/* 初始化DMA寄存器到复位状态函数 */
void DMA_DeInit(DMA_Stream_TypeDef* DMAy_Streamx);
/* DMA初始化函数 */
void DMA_Init(DMA_Stream_TypeDef* DMAy_Streamx, DMA_InitTypeDef* DMA_InitStruct);
/* DMA使能函数 */
void DMA_Cmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState);
以下是函数实现:
/* 相关宏定义,使用存储器到存储器传输必须使用DMA2 */
#define DMA_STREAM DMA2_Stream0
#define DMA_CHANNEL DMA_Channel_0
#define DMA_STREAM_CLK RCC_AHB1Periph_DMA2
#define DMA_FLAG_TCIF DMA_FLAG_TCIF0
#define BUFFER_SIZE 32
#define TIMEOUT_MAX 10000 /* Maximum timeout value */
/* 定义aSRC_Const_Buffer数组作为DMA传输数据源,const关键字将aSRC_Const_Buffer数组变量定义为常量类型 */
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传输目标寄存器 */
uint32_t aDST_Buffer[BUFFER_SIZE];
/* 简单的延时函数 */
static void Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
static void M2M_DMA_Config(void)
{
__IO uint32_t Timeout = TIMEOUT_MAX;
DMA_InitTypeDef DMA_InitStructure;
//打开DMA2所在总线时钟
RCC_AHB1PeriphClockCmd(DMA_STREAM_CLK,ENABLE);
/* 复位初始化DMA数据流 */
DMA_DeInit(DMA_STREAM);
/* 确保DMA数据流复位完成 */
while(DMA_GetCmdStatus(DMA_STREAM) != DISABLE);
/* DMA数据流通道选择 */
DMA_InitStructure.DMA_Channel = DMA_CHANNEL;
/* 源数据地址 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer;
/* 目标地址 */
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)aDST_Buffer;
/* 存储器到存储器模式 */
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory;
/* 数据数目 */
DMA_InitStructure.DMA_BufferSize = (uint32_t)BUFFER_SIZE;
/* 使能自动递增功能 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
/* 使能自动递增功能 */
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
/* 源数据是字大小(32位) */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
/* 一次传输模式,存储器到存储器模式不能使用循环传输 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/* DMAD数据流优先级为高 */
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
/* 禁用FIFO模式 */
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
/* 单次模式 */
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
/* 单次模式 */
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
/* 完成DMA数据流参数配置 */
DMA_Init(DMA_STREAM,&DMA_InitStructure);
/* 使能DMA,开始DMA数据流传输 */
DMA_Cmd(DMA_STREAM,ENABLE);
/* 检测DMA数据流是否有效并带有超时检测功能 */
Timeout = TIMEOUT_MAX;
while ((DMA_GetCmdStatus(DMA_STREAM) != ENABLE) && (Timeout-- > 0));
/* 判断是否超时 */
if (Timeout == 0)
{
/* 超时就让程序运行下面循环:RGB彩色灯闪烁 */
while (1)
{
LED_RED;
Delay(0xFFFFFF);
LED_OFF;
Delay(0xFFFFFF);
}
}
}
编写数据校验函数,在传输完成后对数据进行校验。
/* 判断指定长度的两个数据源是否完全相等,如果完全相等返回1,只要其中一对数据不相等返回0 */
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;
}
在main函数中调用
int main(void)
{
/* 定义存放比较结果变量 */
uint8_t TransferStatus;
LED_Config();
DEBUG_USART1_Config();
M2M_DMA_Config();
/* 等待DMA传输完成 */
while(DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_TCIF)==DISABLE);
/* 比较源数据与传输后数据 */
TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
/* 判断源数据与传输后数据比较结果*/
if(TransferStatus==0)
{
/* 源数据与传输后数据不相等时RGB彩色灯显示红色 */
LED_RED;
printf("数据传输完成,但数据不相等!\n");
}
else
{
/* 源数据与传输后数据相等时RGB彩色灯显示蓝色 */
LED_BLUE;
printf("数据传输完成,且数据相等!\n");
}
while(1)
{
}
}
在本节内容中,对于DMA的使用总结如下:
- 使能 DMA 数据流时钟并复位初始化 DMA 数据流;(只要是操作外设,第一步一定是要打开时钟!!!)
- 配置 DMA 数据流参数;
- 使能 DMA 数据流,进行传输;
- 等待传输完成,并对源数据和目标地址数据进行比较