✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
?个人主页:@rivencode的个人主页
?系列专栏:玩转STM32
?推荐一款模拟面试、刷题神器,从基础到大厂面试题?点击跳转刷题网站进行注册学习
直接存储器存取(DMA)(Direct Memory Access)也是一个挂载在AHB总线上的外设,用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作
。两个DMA控制器有12个通道(DMA1有7个通道,DMA2(只存在于大容量和互联网产品中)有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求
。还有一个仲裁器来协调各个DMA请求的优先权
注:
大容量产品:是指闪存存储器容量在256K至512K字节之间的STM32F101xx和STM32F103xx微控制器。
互联型产品:是指STM32F105xx和STM32F107xx微控制器。
DMA 请求
每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置
DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,每个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能有一个有效
,不能同时接收多个。
如果外设要想通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求
,DMA收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
不同的DMA控制器通道对应这不同外设的DMA请求:
我这里找了部分外设发送DMA请求的函数,想开启哪个外设的DMA请求直接去相应的外设头文件里面找就行了
仲裁器
仲裁器根据通道请求的优先级来启动外设/存储器的访问。
优先权管理分2个阶段:
● 软件:每个通道的优先权可以在DMA_CCRx寄存器中设置,有4个等级:
─ 最高优先级
─ 高优先级
─ 中等优先级
─ 低优先级
● 硬件:如果2个请求有相同的软件优先级,则较低编号的通道
比较高编号的通道有较高的优先权。举个例子,通道2优先于通道4。
在大容量产品和互联产品中,DMA1控制器拥有高于DMA2控制器的优先级
先比较软件优先级,再比较通道编号
DMA控制器和Cortex™-M3核心共享系统数据总线,执行直接存储器数据传输。当CPU和DMA同时访问相同的目标(RAM或外设)时
,DMA请求会暂停CPU访问系统总线达若干个周期,总线仲裁器执行循环调度,以保证CPU至少可以得到一半的系统总线(存储器或外设)带宽。
根据上面的描述我们可以得出一个结论,DMA可以独立CPU之外进行数据的传输 (DMA可以通过数据总线访问存储器和外设的数据寄存器)
次过程不需要CPU的参与,CPU可以转而做其他事情,说白了DAM就是帮CPU打工的.
DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。具体的方向 DMA_CCR 位 4 DIR 配置:0 表示从外设到存储器,1 表示从存储器到外设。这里面涉及到的外设地址由 DMA_CPAR 配置,存储器地址由 DMA_CMAR配置。
有人就会疑惑了,存储器有哪些呢,什么数据存放在存储器上,闪存FLASH、SRAM、就是所谓的存储器。
内部FLASH
简单介绍在flash存储内容:我们写好的程序编译之后都是一条条指令(二进制代码),存放在 FLASH 中,我们常量或常变量C 语言中的 const 关键字修饰也存放在FLASH.
内部SRAM
就是我们常说的电脑内存条,程序函数内部的局部变量和全局变量,堆(malloc分配)栈(局部变量)等的开销都是基于内部的SRAM
。内核通过 DCode 总线来访问它
当然一个数组也是一个变量,当然是存放在SRAM这个存储器上咯,也就是说DMA访问这个数组也就是访问存储池器。
外设的话,一般外设都有一个数据寄存器,来暂存数据。
外设到存储器
当我们使用从外设到存储器传输时,以 ADC 采集为例。DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址
,DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址
。方向我们设置外设为源地址。
存储器到外设
当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。DMA 外设寄存器的地址对应的就是串口数据寄存器的地址
,DMA 存储器的地址就是我们自定义的变量(一般是一个数组,相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址
。方向我们设置外设为目标地址。
存储器到存储器
当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址
,DMA 存储器的地址就是我们自定义的变量
(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位 14:MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。
其实上面是我复制粘贴的,其实光看文字肯定理解不够深刻,上面提到的我后面有对应的三个实验,如果想深入理解一定要看到最后
通道配置过程
下面是配置DMA通道x的过程(x代表通道号):
在DMA_CPARx寄存器中设置外设寄存器的地址
。发生外设数据传输请求时,这个地址将是数据传输的源或目标。
在DMA_CMARx寄存器中设置数据存储器的地址
。发生外设数据传输请求时,传输的数据将从这个地址读出或写入这个地址。
在DMA_CNDTRx寄存器中设置要传输的数据量。在每个数据传输后,这个数值递减。
在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。
在DMA_CCRx寄存器中设置数据传输的方向、循环模式、外设和存储器的增量模式、外设和存储器的数据宽度、传输一半产生中断或传输完成产生中断。
传输方向:
循环模式:
外设和存储器的增量模式:
如果还不理解的话得先补习一下指针的知识—>《指针从入门到熟练掌握》
外设和存储器的数据宽度:
要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8位的,所以我们定义的要发送的数据也必须是 8 位
如果两边的设置的数据宽度不一样会怎么样呢。
设置DMA_CCRx寄存器的ENABLE位,启动该通道一旦启动了DMA通道,它即可响应连到该通道上的外设的DMA请求。
当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位(TCIE)时,将产生一个中断请求
当通道配置为非循环模式时,传输结束后(即传输计数变为0)将不再产生DMA操作。要开始新的DMA传输,需要在关闭DMA通道的情况下,在DMA_CNDTRx寄存器中重新写入传输数目。
要开始新的DMA传输必须先关闭DMA通道,在DMA_CNDTRx寄存器中重新写入传输数目,再打开DAM通道,这点非常重要。
中断
上面每个结构体成员变量要配置哪个寄存器,前面已经详述。
DMA_PeripheralBaseAddr:外设地址,一般设置为外设的数据寄存器地址
,如果是存储器到存储器模式则设置为其中一个存储器地址。
DMA_Memory0BaseAddr:存储器(FLASH、SRAM)地址,一般设置为我们自定义存储区(一般为一个数组)的首地址(数组名)。
DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。
DMA_BufferSize:设定待传输数据数量,数量范围为0~65535
DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,一般外设都是只有一个数据寄存器,所以一般不会使能该位。
DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。
DMA_PeripheralDataSize:外设数据宽度,可选字节(8 位)、半字(16 位)和字(32位),
DMA_MemoryDataSize:存储器数据宽度,可选字节(8 位)、半字(16 位)和字(32位),当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。
DMA_Mode:DMA 传输模式选择,可选一次传输或者循环传输,我们的 ADC 采集是持续循环进行的,所以使用循环传输模式。
DMA_Priority:软件设置道的优先级,有 4 个可选优先级分别为非常高、高、中和低,DMA 通道优先级只有在多个 DMA 通道
同时使用时才有意义,如果是单个通道,优先级可以随便设置。
DMA_M2M :存储器到存储器模式 .
接下来就进入实战重点,深入理解原理,一共对应三个实验,存储器到外设、外设到存储器、存储器到存储器。
我们先定义(const)一个静态的源数据,存放在内部 FLASH,然后使用 DMA 传输把源数据拷贝到目标地址上(内部 SRAM),最后对比源数据和目标地址的数据,看看是否传输准确,程序开始先亮一会红灯,等待数据传输完成然后对比数据,若数据传输正确,则亮绿灯,否则又亮红灯。
内部FLASH
简单介绍在flash存储内容:我们写好的程序编译之后都是一条条指令(二进制代码),存放在 FLASH 中,我们常量或常变量C 语言中的 const 关键字修饰也存放在FLASH.
内部SRAM
就是我们常说的电脑内存条,程序函数内部的局部变量和全局变量,堆(malloc分配)栈(局部变量)等的开销都是基于内部的SRAM
。内核通过 DCode 总线来访问它
上代码:
dma_mtm.h
ifndef DMA_MTM_Hdefine DMA_MTM_Hinclude "stm32f10x.h"// 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定define DMA_CHANNEL DMA1_Channel6define DMA_CLOCK RCC_AHBPeriph_DMA1// 传输完成标志define DMA_FLAG_TC DMA1_FLAG_TC6// 要发送的数据大小define BUFFER_SIZE 32extern const uint32_t aSRC_Const_Buffer[BUFFER_SIZE];extern uint32_t aDST_Buffer[BUFFER_SIZE];void MtM_DMA_Config(void);uint8_t Buffer_cmp(const uint32_t * pBuffer,const uint32_t *pBuffer1,uint32_t Bufferlength);endif /* DMA_MTM_H */ rgb(153, 153, 153);">2223
dma_mtm.c
include "dma_mtm.h"/* 定义aSRC_Const_Buffer数组作为DMA传输数据源 * const关键字将aSRC_Const_Buffer数组变量定义为常量类型 * 表示数据存储在内部的FLASH中 */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传输目标存储器 * 存储在内部的SRAM中 */uint32_t aDST_Buffer[BUFFER_SIZE];void MtM_DMA_Config(void){ DMA_InitTypeDef DMA_InitStructure; //开启DMA时钟(注意:DMA挂载在AHB总线上) RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);// 源数据地址 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer;// 目标地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer;// 方向:外设到存储器(这里的外设是内部的FLASH) DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;// 传输大小 DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;// 外设(内部的FLASH)地址递增 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;// 内存地址递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;// 外设数据单位 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;// 内存数据单位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; // DMA模式,一次或者循环模式 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 优先级:高 DMA_InitStructure.DMA_Priority = DMA_Priority_High;// 使能内存到内存的传输 DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;// 配置DMA通道 DMA_Init(DMA_CHANNEL, &DMA_InitStructure); //清除DMA数据流传输完成标志位,为了后面检测数据是否完成 DMA_ClearFlag(DMA_FLAG_TC);// 使能DMADMA_Cmd(DMA_CHANNEL,ENABLE);}//数组比较函数uint8_t Buffer_cmp(const uint32_t * pBuffer,const uint32_t *pBuffer1,uint32_t Bufferlength){while(Bufferlength--){if(*pBuffer !=*pBuffer1)return 0;else pBuffer++; pBuffer1++;} return 1;} rgb(153, 153, 153);">222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
main.c
include "stm32f10x.h"include "led.h"include "dma_mtm.h"define SOFT_DELAY Delay(0x0FFFFF);void Delay(__IO u32 nCount); int main(void){/* LED 端口初始化 */LED_GPIO_Config();//先亮一会红灯 LED_G(OFF); LED_R(NO); SOFT_DELAY;//DAM初始化MtM_DMA_Config();//等待DAM数据全部传输完成while ( DMA_GetFlagStatus( DMA_FLAG_TC)==RESET); /* 比较源数据与传输后数据 */if(Buffer_cmp( aSRC_Const_Buffer,aDST_Buffer,BUFFER_SIZE)){ LED_G(NO); LED_R(OFF);}else{ LED_G(OFF); LED_R(NO);}}void Delay(__IO uint32_t nCount) //简单的延时函数{for(; nCount != 0; nCount--);} rgb(153, 153, 153);">222324252627282930313233343536
这里要注意的一个点DMA是挂载在AHB总线,开启DAM时钟。
RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);1
在看该实验时若串口还不是很熟悉,请看STM32串口通信详解
我们先定义一个数组变量,存于 SRAM 中,然后通过 DMA 的方式传输到串口的数据寄存器,然后通过串口把这些数据发送到电脑的上位机显示出来,传输的同时CPU表示很闲所以边传输的时候边让CPU点灯。
利用DMA发送
使用DMA进行发送,可以通过设置USART_CR3寄存器上的DMAT位激活。当TXE位被置为’1’时,DMA就从指定的SRAM区传送数据到USART_DR寄存器
在DMA控制寄存器上将USART_DR寄存器地址配置成DMA传输的目的地址。在每个TXE事件后,数据将被传送到这个地址。
在DMA控制寄存器上将存储器地址配置成DMA传输的源地址。在每个TXE事件后,将从此存储器区读出数据并传送到USART_DR寄存器。
总结:DAM传输数据时到USATR时可以实现连续通信,原本如果我们要实现连续通信,传输一个一帧数据时我们需要等待TXE位置1才能,写入下一个数据,不然数据寄存器的数据会被覆盖,但DMA传输数据到串口数据寄存器时每传输一帧数据,DMA会自己等待TXE置1(数据寄存器为空,数据已经被转移到数据移位寄存器中),再传输下一帧数据,DMA与串口配合的天衣无缝,实现连续通信。
要想理解原理,下面两张图一定一定要看懂
这里解释一下,既然是存储器到外设,为啥串口发送的是串口发送(TX)的请求呢,DMA只是一个帮忙传输数据到串口与CPU作用一样,其实我们真正的目的是利用串口向另外一个设备(电脑)发送数据,所以串口向DMA发送串口发送请求。
上代码:
dma_mtp.h
ifndef __DMA_MTP_Hdefine __DMA_MTP_Hinclude "stm32f10x.h"// 要发送的数据大小define SENDBUFFER_SIZE 32extern uint8_t aSRC_Buffer[SENDBUFFER_SIZE];define DEBUG_USARTx USART1define DEBUG_USART_CLK RCC_APB2Periph_USART1define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmddefine DEBUG_USART_BAUDRATE 115200// USART GPIO 引脚宏定义define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA)define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd define DEBUG_USART_TX_GPIO_PORT GPIOA define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9define DEBUG_USART_RX_GPIO_PORT GPIOAdefine DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10define DMA1_MtP_FLAG_TC DMA1_FLAG_TC4define DMA1_MtP_CLK RCC_AHBPeriph_DMA1define DMA1_MtP_Channel DMA1_Channel4define USART_DR_BASE (USART1_BASE+0X04)void DMA_MtoP_Config(void);uint16_t Buffer_cmp(const uint32_t * aSRC_Buffer,uint32_t * aDST_Buffer,uint16_t length);void Usart_GPIO_Config(void);endif /*__DMA_MTP_H */ rgb(153, 153, 153);">22232425262728293031323334
dma_mtp.c
include "dma_Mtp.h"uint8_t aSRC_Buffer[SENDBUFFER_SIZE];void Usart_GPIO_Config(void){ GPIO_InitTypeDef GPIO_InitStuctrue; USART_InitTypeDef USART_InitStuctrue;//开启GPIO外设时钟DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK,ENABLE);//开启USART外设时钟DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK,ENABLE); GPIO_InitStuctrue.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN; GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStuctrue.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(DEBUG_USART_TX_GPIO_PORT,&GPIO_InitStuctrue); GPIO_InitStuctrue.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN; GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(DEBUG_USART_RX_GPIO_PORT,&GPIO_InitStuctrue); USART_InitStuctrue.USART_BaudRate = DEBUG_USART_BAUDRATE; USART_InitStuctrue.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_InitStuctrue.USART_Parity = USART_Parity_No; USART_InitStuctrue.USART_StopBits = USART_StopBits_1; USART_InitStuctrue.USART_WordLength = USART_WordLength_8b; USART_InitStuctrue.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_Init(DEBUG_USARTx,&USART_InitStuctrue);USART_Cmd(DEBUG_USARTx,ENABLE);}void DMA_MtoP_Config(void){ DMA_InitTypeDef DMA_InitStructure;RCC_AHBPeriphClockCmd(DMA1_MtP_CLK, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_BASE; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aSRC_Buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = SENDBUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//DMA的循环传输 DMA_InitStructure.DMA_Mode =DMA_Mode_Circular; DMA_InitStructure.DMA_Priority =DMA_Priority_High; DMA_InitStructure.DMA_M2M =DMA_M2M_Disable;DMA_Init(DMA1_MtP_Channel,&DMA_InitStructure);DMA_ClearFlag(DMA1_MtP_FLAG_TC);DMA_Cmd(DMA1_MtP_Channel,ENABLE);} rgb(153, 153, 153);">22232425262728293031323334353637383940414243444546474849505152535455565758596061
main.c
include "stm32f10x.h"include "led.h"include "delay.h"include "dma_Mtp.h"define SOFT_DELAY Delay(0x0FFFFF);void Delay(__IO u32 nCount); int main(void){ uint32_t i =0; delay_init();/* LED 端口初始化 */LED_GPIO_Config(); //串口初始化 Usart_GPIO_Config();//初始化数组for(i=0;i<SENDBUFFER_SIZE;i++){ aSRC_Buffer[i]='k';}//DMA配置DMA_MtoP_Config();//向DMA发送串口TX请求USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);//CPU同时点灯while(1){LED_G(NO);LED_R(OFF);delay_ms(300</spa
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
加入交流群
请使用微信扫一扫!