1.DMA简介
DMA,全称是Direct Memory Access,中文意思为直接存储器访问。DMA可用于实现外设与存储器之间或者存储器与存储器之间数据传输的高效性。
之所以高效,是因为DMA传输数据移动过程无需CPU直接操作,这样节省的 CPU 资源就可供其它操作使用。同时,因为没有经过CPU进行中间操作,所以效率更高!
从硬件层面来理解,DMA就好像是RAM与I/O设备间数据传输的通路,外设与存储器之间或者存储器与存储器之间可以直接在这条通路上进行数据传输。
这里说的外设一般指外设的数据寄存器,比如 ADC、 SPI、 I2C、 DCMI等外设的数据寄存器,存储器一般是指片内SRAM、外部存储器、片内 Flash 等。
STM32F1 最多有 2 个 DMA 控制器, DMA1 有 7 个通道, DMA2 有 5个通道(DMA2 仅存在STM系列的大容量产品中),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个 DMA 请求的优先权。(很明显,不通CPU来管理,那就需要另外有一个控制器来控制,这就是DMA控制器和仲裁器的作用)
2.DMA结构框图
DMA 控制器在整个芯片中,它独立于内核,是一个单独的外设,结构比较简单,从编程的角度来看,我们只需掌握结构框图中的三部分内容即可。如图:
(1)标号1:DMA请求
如果外设要想通过 DMA 的方式来传输数据,必须先给 DMA 控制器发送 DMA 请求, DMA收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
(2)标号2:DAM通道
DMA含有DMA1和DMA2两个控制器,其中DMA1含有7个通道,DMA2含有5个通道,不同的 DMA 控制器的通道对应着不同的外设请求,如下图所示:
每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
DMA1:
(例如:外设ADC1只能通过DMA1的通道1与存储器互相传输数据。)
DMA2:
(3)标号3:仲裁器
当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器管理。
仲裁器管理 DMA 通道请求分为两个阶段。
第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。
第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的软件优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
3.DMA数据具体传递方式
(1)外设到存储器
当我们使用从外设到存储器传输时,以 ADC 采集为例。 DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。简单来说,就是从ADC数据寄存器地址往存储器对应的地址存放数据!因此首先将DMA外设寄存器地址对应到ADC数据寄存器地址。
(2)存储器到外设
当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。 DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。
(3)存储器到存储器
当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。
跟上面两个不一样的是,这里需要把 DMA_CCR 位 14: MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。
4.STM32F1 DMA配置步骤
具体步骤如下:(DMA相关库函数在stm32f10x_dma.c和stm32f10x_dma.h文件中)
(1)使能DMA控制器(DMA1或DMA2)时钟
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
(2)初始化DMA通道,包括配置通道、外设和内存地址、传输数据量等
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,DMA_InitTypeDef* DMA_InitStruct);
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; // 外设地址
uint32_t DMA_MemoryBaseAddr; // 存储器地址
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_M2M; // 存储器到存储器模式
} DMA_InitTypeDef;
该结构体各成员的作用介绍如下:
DMA_PeripheralBaseAddr:外设地址,通过DMA_CPAR寄存器设置,一般设置为外设的数据寄存器地址,比如要进行串口DMA 传输,那么外设基地址为串口接受发送数据存储器USART1->DR 的地址,表示方法为&USART1->DR。如果是存储器到存储器模式则设置为其中一个存储区地址。
DMA_Memory0BaseAddr:存储器地址,通过DMA_CMAR寄存器设置,一般设置为我们自定义存储区的首地址,即我们存放DMA传输数据的内存地址。比如我们定义一个u32类型数组,将数组首地址(直接使用数组名即可)赋值给DMA_Memory0BaseAddr,在DMA传输的时候就可以把数组内的数据发送或接收。
DMA_DIR:数据传输方向选择,可选择外设到存储器、存储器到外设以及存储器到存储器。通过设定DMA_CCR寄存器的DIR[1:0]位的值决定。比如本章实验是从内存读取数据发送到串口,所以数据传输方向为存储器到外设,配置为DMA_DIR_MemoryToPeripheral。
DMA_BufferSize:用来设置一次传输数据的大小,通过DMA_CNDTR寄存器设置。
DMA_PeripheralInc:用来设置外设地址是递增还是不变,通过DMA_CCR寄存器的PINC位设置,如果设置为递增,那么下一次传输的时候地址加1。通常外设只有一个数据寄存器,所以一般不会使能该位,即配置为DMA_PeripheralInc_Disable。
DMA_MemoryInc:用来设置内存地址是否递增,通过DMA_CCR寄存器的MINC位设置。我们自定义的存储区一般都是存放多个数据的,所以需要使能存储器地址自动递增功能,即配置为DMA_MemoryInc_Enable。
DMA_PeripheralDataSize:外设数据宽度选择,可以为字节(8位)、半字(16位)、字(32位),通过DMA_CCR寄存器的PSIZE[1:0]位设置。例如本章实验数据是按照8位字节传输,所以配置为DMA_PeripheralDataSize_Byte。
DMA_MemoryDataSize:存储器数据宽度选择,可以为字节(8位)、半字(16位)、字(32位),通过DMA_CCR寄存器的MSIZE[1:0]位设置。本章实验同样设置为8位字节传输,这个要和我们定义的数组对应,所以配置为DMA_MemoryDataSize_Byte。
DMA_Mode:DMA传输模式选择,可选择一次传输或者循环传输,通过DMA_CCR寄存器的CIRC位来设定。比如我们要从内存(存储器)中传输64个字节到串口,如果设置为循环传输,那么它会在64个字节传输完成之后继续从内存的第一个地址传输,如此循环。这里我们设置为一次传输完成之后不循环。所以设置值为DMA_Mode_Normal。
DMA_Priority:用来设置DMA通道的优先级,有低,中,高,超高四种级别,可通过DMA_CCR寄存器的PL[1:0]位来设定。DMA优先级只有在多个DMA数据流同时使用时才有意义,本章实验我们只使用了一个DMA数据流,所以可以任意设置DMA优先级,这里我们就设置为中等优先级,配置参数为DMA_Priority_Medium。
DMA_M2M:用来设置存储器到存储器模式,使用存储器到存储器时用到,设定DMA_CCR 的位 14 MEN2MEN 即可启动存储器到存储器模式。
了解结构体成员功能后,就可以进行配置,本章实验配置代码如下:
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外设模式
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_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMAy_Channelx, &DMA_InitStructure);//初始化DMA
(3)使能外设DMA功能(DMA请求映射图对应的外设)
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
(4)开启DMA的通道传输
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
DMA_Cmd(DMA1_Channel4,ENABLE);
(5)查询DMA传输状态
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
例如我们要查询DMA1通道4传输是否完成,方法是:
DMA_GetFlagStatus(DMA1_FLAG_TC4);
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
5.硬件电路
本实验使用到硬件资源如下:
(1)D1和D2指示灯
(2)K_UP按键
(3)串口1
(4)DMA
D1指示灯用来提示系统运行状态,K_UP按键用来控制DMA发送,每按一次K_UP键,DMA就将内存(自定义的一个数组)内数据发送USART1,并通过串口1将发送的内容打印出来。在DMA数据传输的过程中让D2指示灯不断闪烁,直到数据传输完成。D2指示灯闪烁表CPU在执行其他的任务,说明DMA传输是不需要占用CPU的。
6.编写DMA控制程序
本实验所要实现的功能是:通过K_UP按键控制DMA串口1数据的传送,在传送过程中让D2指示灯不断闪烁,直到数据传送完成。D1指示灯闪烁提示系统正常运行。程序框架如下:
(1)初始化USART1_TX对应的DMA通道相关参数
(2)编写主函数
main.c
#include "system.h"
#include "led.h"
#include "SysTick.h"
#include "usart.h"
#include "key.h"
#include "dma.h"#define send_buf_len 5000 //5000个字节长度u8 send_buf[send_buf_len];void Send_Data(u8 *p)
{u16 i;for (i=0; i<send_buf_len; i++){*p='5';//将5000个字节里的内容全部设为字符5p++;}}int main()
{u8 i=0;u8 key=0;SysTick_Init(72);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级分组LED_Init();USART1_Init(9600);key_Init();DMAx_Init(DMA1_Channel4,(u32)&USART1->DR,(u32)send_buf,send_buf_len);Send_Data(send_buf);//将5000个字节里的内容全部设为字符5while(1){key = KEY_Scan(0);if(key == KEY_UP){USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);//使能串口1的DMA功能DMAx_Enable(DMA1_Channel4,send_buf_len);while(1){if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=0)//检查传输是否完成{DMA_ClearFlag(DMA1_FLAG_TC4);break;//完成则退出while,led2就不再闪}led2=!led2; //没完成就一直闪delay_ms(300);}}i++;if(i%20 ==0){led1=!led1;//LED1闪,用来指示主程序循环是否运行delay_ms(300);}}}
dma.c
#include "dma.h"
#include "SysTick.h"void DMAx_Init(DMA_Channel_TypeDef* DMAy_Channelx,u32 par,u32 mar,u16 ndtr)
{DMA_InitTypeDef DMA_InitStructure;RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//使能DMA1时钟DMA_InitStructure.DMA_PeripheralBaseAddr = par;//DMA外设地址DMA_InitStructure.DMA_MemoryBaseAddr = mar;//DMA 存储器0地址DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//传输方向:存储器到外设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_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输DMA_Init(DMAy_Channelx, &DMA_InitStructure); //初始化DMA通道}void DMAx_Enable(DMA_Channel_TypeDef* DMAy_Channelx,u16 ndtr)
{DMA_Cmd(DMAy_Channelx,DISABLE); //DMA_SetCurrDataCounter(DMAy_Channelx, ndtr);DMA_Cmd(DMAy_Channelx,ENABLE);}
程序写到开发板上测试,结果显示如下,LED也正常闪 ,实验是成功的!