DMA(Direct Memory Access) :直接存储器存取,是单片机的一个外设
F407开发指南28.1,P365。
STM32F4 最多有 2 个 DMA 控制器(DMA1 和 DMA2),共 16 个数据流(每个控制器 8 个),每一个 DMA 控制器都用于管理一个或多个外设的存储器访问请求。
F407开发指南28.1,P365。

F407开发指南,28.1,P366
这里特别注意一下,存储器到存储器需要外设接口可以访问存储器,而仅 DMA2 的外设接口可以访问存储器,所以仅 DMA2 控制器支持存储器到存储器的传输,DMA1 不支持。
F407开发指南,28.1,P365




定义一个变量
其中a存储在了全局变量区域,但是在RAM中。
第二行是将内存数据转移到串口的数据寄存器中。
当串口的数据寄存器接收到数据之后就会由串口外设自动发送出去
将数据从内存转移到外设寄存器中也是有CPU操作的。
一次转移大概需要1ms。如果是1万个,就需要10S。
对于单片机来说,10S种太长了。
因此,出现了DMA。
结合F407开发指南,28.1,P365讲仲裁器的部分




一对多的关系
DMA_SxCR 控制数据流到底使用哪一个通道,每个数据流有 8 个通道可供选择,每次只能选择其中一个通道进行 DMA 传输。
要注意的是 DMA2 只存在于大容量的单片机中。
DMA 控制器独立于内核,属于一个单独的外设,结构比较简单

STEM32的DMA 有 DMA1 和 DMA2 两个控制器, DMA1 有 7 个通道, DMA2 有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求


其中 ADC3、 SDIO 和 TIM8 的 DMA 请求只在大容量产品中存在

如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。
常用的是 TCIFx,即通道 DMA 传输完成与否的标志。

DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。因为DMA_ISR寄存器为只读寄存器,所以在这些位被置位之后,只能这样来清除。
该寄存器有1-7个
该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。
这个寄存器控制 DMA 通道 x 的每次传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。
所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。
该寄存器用来存储 STM32 外设的地址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使用其他外设,就修改成相应外设的地址就行了。
该寄存器和 DMA_CPARx 差不多,但是用来放存储器的地址的。
比如我们使用 SendBuf[5200]数组来做存储器,那么我们在DMA_CMARx 中写入&SendBuff 就可以了。
我们要使用的是串口 1 的 DMA 传送,也就是要用到通道 4。
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr)
{
DMA_InitTypeDef DMA_InitStructure;
if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
}else
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
}
DMA_DeInit(DMA_Streamx);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}//等待DMA可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = mar;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式
DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
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(DMA_Streamx, &DMA_InitStructure);//初始化DMA Stream
}
//开启一次DMA传输
//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7
//ndtr:数据传输量
void MYDMA_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{
DMA_Cmd(DMA_Streamx, DISABLE); //关闭DMA传输
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){} //确保DMA可以被设置
DMA_SetCurrDataCounter(DMA_Streamx,ndtr); //数据传输量
DMA_Cmd(DMA_Streamx, ENABLE); //开启DMA传输
}
if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
{ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
}else
{RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
}
DMA_Stream_TypeDef *DMA_Streamx
可选项:
#define DMA1 ((DMA_TypeDef *) DMA1_BASE)
#define DMA1_Stream0 ((DMA_Stream_TypeDef *) DMA1_Stream0_BASE)
#define DMA1_Stream1 ((DMA_Stream_TypeDef *) DMA1_Stream1_BASE)
#define DMA1_Stream2 ((DMA_Stream_TypeDef *) DMA1_Stream2_BASE)
#define DMA1_Stream3 ((DMA_Stream_TypeDef *) DMA1_Stream3_BASE)
#define DMA1_Stream4 ((DMA_Stream_TypeDef *) DMA1_Stream4_BASE)
#define DMA1_Stream5 ((DMA_Stream_TypeDef *) DMA1_Stream5_BASE)
#define DMA1_Stream6 ((DMA_Stream_TypeDef *) DMA1_Stream6_BASE)
#define DMA1_Stream7 ((DMA_Stream_TypeDef *) DMA1_Stream7_BASE)
#define DMA2 ((DMA_TypeDef *) DMA2_BASE)
#define DMA2_Stream0 ((DMA_Stream_TypeDef *) DMA2_Stream0_BASE)
#define DMA2_Stream1 ((DMA_Stream_TypeDef *) DMA2_Stream1_BASE)
#define DMA2_Stream2 ((DMA_Stream_TypeDef *) DMA2_Stream2_BASE)
#define DMA2_Stream3 ((DMA_Stream_TypeDef *) DMA2_Stream3_BASE)
#define DMA2_Stream4 ((DMA_Stream_TypeDef *) DMA2_Stream4_BASE)
#define DMA2_Stream5 ((DMA_Stream_TypeDef *) DMA2_Stream5_BASE)
#define DMA2_Stream6 ((DMA_Stream_TypeDef *) DMA2_Stream6_BASE)
#define DMA2_Stream7 ((DMA_Stream_TypeDef *) DMA2_Stream7_BASE)
DMA_DeInit(DMA_Streamx);
可选项同上。
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,DMA_InitTypeDef* DMA_InitStruct)
函数的第一个参数是指定初始化的 DMA 通道号
第二个参数是通过初始化结构体成员变量值来达到初始化的目的
typedef struct
{
uint32_t DMA_Channel;
uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_Memory0BaseAddr;
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;
uint32_t DMA_FIFOThreshold;
uint32_t DMA_MemoryBurst;
uint32_t DMA_PeripheralBurst;
}DMA_InitTypeDef;
用来设置 DMA 数据流对应的通道。
#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 传输的外设基地址
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
(u32)&USART1->DR
(u32)SendBuff
内存基地址,也就是我们存放 DMA 传输数据的内存地址。
设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数据发送到外设,也就是外设是源地还是目的地。
#define DMA_DIR_PeripheralToMemory ((uint32_t)0x00000000)
#define DMA_DIR_MemoryToPeripheral ((uint32_t)0x00000040)
#define DMA_DIR_MemoryToMemory ((uint32_t)0x00000080)
何为外设?
外设指的是单片机外部的外围功能模块,STM32的基本外设有:GPIO(基本输入输出接口),Timer/Counter(定时器和计数器),USART(串行收发处理器),I2C(串行总线),SPI(串行外设接口,使用SPI协议,可能能直接通过串口控制外设),I2S(集成电路的内置音频总线),SD卡接口(闪存卡接口),SDIO(和usb相似,兼容SD卡,可以接wifi模块),USB接口
设置一次传输数据量的大小,这个很容易理解。
const u8 TEXT_TO_SEND[]={"ALIENTEK Explorer STM32F4 DMA 串口实验"};
#define SEND_BUF_SIZE 8200
//发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍.
设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加 1
#define DMA_PeripheralInc_Enable ((uint32_t)0x00000200)
#define DMA_PeripheralInc_Disable ((uint32_t)0x00000000)
设置传输数据时候内存地址是否递增。这个参数DMA_PeripheralInc 意思接近,只不过针对的是内存。
#define DMA_MemoryInc_Enable ((uint32_t)0x00000400)
#define DMA_MemoryInc_Disable ((uint32_t)0x00000000)
DMA_PeripheralDataSize 用来设置外设的数据长度是为字节传输(8bits),半字传输(16bits)还是字传输 (32bits)
#define DMA_PeripheralDataSize_Byte ((uint32_t)0x00000000)
#define DMA_PeripheralDataSize_HalfWord ((uint32_t)0x00000800)
#define DMA_PeripheralDataSize_Word ((uint32_t)0x00001000)
这里我们是 8 位字节传输,所以 值设置为DMA_PeripheralDataSize_Byte。
是用来设置内存的数据长度,和第七个参数意思接近
#define DMA_MemoryDataSize_Byte ((uint32_t)0x00000000)
#define DMA_MemoryDataSize_HalfWord ((uint32_t)0x00002000)
#define DMA_MemoryDataSize_Word ((uint32_t)0x00004000)
用来设置 DMA 模式是否循环采集
#define DMA_Mode_Normal ((uint32_t)0x00000000)
#define DMA_Mode_Circular ((uint32_t)0x00000100)
比如我们要从内存中采集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内存的第一个地址采集,如此循环。如果设置此参数为循环采集,那么你会看到串口不停的打印数据,不会中断
如果设置为一次连续采集完成之后不循环。所以设置值为 DMA_Mode_Normal。
用来设置 DMA 通道的优先级,有低,中,高,超高三种模式。
假设有多个数据流开启(最多 8 个),那么就要设置优先级了
#define DMA_Priority_Low ((uint32_t)0x00000000)
#define DMA_Priority_Medium ((uint32_t)0x00010000)
#define DMA_Priority_High ((uint32_t)0x00020000)
#define DMA_Priority_VeryHigh ((uint32_t)0x00030000)
用来设置是否开启 FIFO 模式。这里我们不开启所以选择DMA_FIFOMode_Disable。
#define DMA_FIFOMode_Disable ((uint32_t)0x00000000)
#define DMA_FIFOMode_Enable ((uint32_t)0x00000004)
用来选择 FIFO 阈值。根据前面讲解可以为 FIFO 容量的1/4,1/2,3/4 以及 1 倍。
#define DMA_FIFOThreshold_1QuarterFull ((uint32_t)0x00000000)
#define DMA_FIFOThreshold_HalfFull ((uint32_t)0x00000001)
#define DMA_FIFOThreshold_3QuartersFull ((uint32_t)0x00000002)
#define DMA_FIFOThreshold_Full ((uint32_t)0x00000003)
用来配置存储器突发传输配置。
可以选择为
MBURST:存储器突发传输配置 (Memory burst transfer configuration)
这些位将由软件置 1 和清零。
00:单次传输
01: INCR4( 4 个节拍的增量突发传输)
10: INCR8( 8 个节拍的增量突发传输)
11: INCR16( 16 个节拍的增量突发传输)
这些位受到保护,只有 EN 为“0”时才可以写入
在直接模式中,当位 EN =“1”时,这些位由硬件强制置为 0x0。
当使用内部 FIFO 时,源和目标数据的数据宽度可以通过DMA_SxCR 寄存器的 PSIZE 和 MSIZE 位(可以是 8、16 或32 位)编程。
当 PSIZE 和 MSIZE 不相等时
DMA 控制器可以产生单次传输或 4 个、8 个和 16 个节拍的增量突发传输。
当 AHB 外设端口被配置为单次传输时,根据 DMA_SxCR 寄存器 PSIZE[1:0] 位的值,每个 DMA 请求产生一次字节、半字或字的数据传输。
当 AHB 外设端口被配置为突发传输时,根据 DMA_SxCR 寄存器 PBURST[1:0] 和PSIZE[1:0] 位的值,每个 DMA 请求相应地生成 4 个、8 个或 16 个节拍的字节、半字或字的传输。
注意: 仅在使能指针递增模式时允许突发模式:
— 当 PINC 位为“ 0 ”时,也应将 PBURST 位清为“ 00 ”
— 当 MINC 位为“ 0 ”时,也应将 MBURST 位清为“ 00 ”
用来配置外设突发传输配置。跟前面一个参数DMA_MemoryBurst 作用类似,只不过一个针对的是存储器,一个是外设。
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA 外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = mar;//DMA 存储器 0 地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式
DMA_InitStructure.DMA_BufferSize = ndtr;//数据传输量
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(DMA_Streamx, &DMA_InitStructure);//初始化 DMA Stream
进行 DMA 配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是:
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);
如果是要使能串口 DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。
使能串口 DMA 发送之后,我们接着就要使能 DMA 传输通道:
DMA_Cmd(DMA_CHx, ENABLE);
通过以上 3 步设置,我们就可以启动一次 USART1 的 DMA 传输了。
使能 DMA 数据流的函数为:
void DMA_Cmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState)
使能 DMA2_Stream7,启动传输的方法为:
DMA_Cmd (DMA2_Stream7,ENABLE);
通过以上 4 步设置,我们就可以启动一次 USART1 的 DMA 传输了。
在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是:
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)
比如我们要查询 DMA 通道 4 传输是否完成,方法是:
DMA_GetFlagStatus(DMA2_FLAG_TC4);
这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx)
比如我们要获取 DMA 通道 4 还有多少个数据没有传输,方法是:
DMA_GetCurrDataCounter(DMA1_Channel4);
在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是:
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)
比如我们要查询 DMA 数据流 7 传输是否完成,方法是:
DMA_GetFlagStatus(DMA2_Stream7,DMA_FLAG_TCIF7);
这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:
uint16_t DMA_GetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx);
比如我们要获取 DMA 数据流 7 还有多少个数据没有传输,方法是:
DMA_GetCurrDataCounter(DMA1_Channel4);
同样,我们也可以设置对应的 DMA 数据流传输的数据量大小,函数为:
void DMA_SetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx, uint16_t Counter);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}//等待DMA可配置
DMA_DeInit(DMA_Streamx);
DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。