开发环境: MDK:Keil5。30 开发板:GD32F207IEVAL MCU:GD32F207IK11。1DMA工作原理11。1。1DMA介绍 DMA(DirectMemoryAccess,直接存储器存取),是一种可以大大减轻CPU工作量的数据存取方式,DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,因而被广泛地使用。早在8086的应用中就已经有Intel的8237这种典型的DMA控制器,而GD32的DMA则是以类似外设的形式添加到Cortex内核之外的。可以说,DMA就是CPU的高级代理,DMA大大减轻了CPU的负担。 在硬件系统中,主要由CPU(内核)、外设、内存(SRAM)、总线等结构组成,数据经常要在内存与外设之间转移,或从外设A转移到外设B。例如:当CPU需要处理由ADC外设采集回来的数据时,CPU首先要把数据从ADC外设的寄存器读取到内存中(变量),然后进行运算处理,这是一般的处理方法。 在转移数据的过程中会占用CPU十分宝贵的资源,所以我们希望CPU更多地被用在数据运算或响应中断之中,而数据转移的工作交由其他部件完成,是不是能够更好的利用CPU的资源呢?DMA正是为CPU分担了数据转移的工作。因为DMA的存在CPU才被解放出来,它可以在DMA转移数据的过程中同时进行数据运算、响应中断,大大提高效率。再次总结下DMA,DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节。 图1DMA数据传输示意图 DMA数据传输主要涉及三种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。三种情况的数据传输分别时:外设到内存、内存到外设、内存到内存。11。1。2GD32的DMA主要特征 GD32F2系列的MCU一般有两个DMA控制器有14个通道(DMA0有7个通道,DMA1有7个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。 要使用DMA,需要确定一系列的控制参数,如外设数据的地址、内存地址、传输方向等,在开启DMA传输前还要先发出DMA请求。 DMA的主要特点如下: 14个独立的可配置的通道(请求):DMA0有7个通道,DMA1有7个通道 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置。 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推)。 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。 支持循环的缓冲器管理 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。 支持外设到存储器,存储器到外设,存储器到存储器的数据传输 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。 可编程的数据传输数目:最大为6553511。1。3GD32的DMA请求映像 从外设(TIMERx〔x0、1、2、3〕、ADC0、SPI0、SPII2S1、I2Cx〔x0、1〕和USARTx〔x0、1、2〕)产生的7个请求,通过逻辑或输入到DMA0控制器,这意味着同时只能有一个请求有效。参见下图的DMA0请求映像。 图2DMA1请求映射 外设的DMA请求,可以通过设置相应外设寄存器中的控制位,被独立地开启或关闭。 表1各个通道的DMA0通道 从外设(TIMERx〔4、5、6、7〕、ADC2、SPII2S2、UART3、DAC等7个请求,经逻辑或输入到DMA1控制器,这意味着同时只能有一个请求有效。参见下图的DMA1请求映像。 图3DMA1请求映射 外设的DMA请求,可以通过设置相应外设寄存器中的DMA控制位,被独立地开启或关闭。 表2各个通道的DMA1通道 当DMA控制器在同一时间接收到多个外设请求时,仲裁器将根据外设请求的优先级来决定响应哪一个外设请求。优先级包括软件优先级和硬件优先级,优先级规则如下: 软件优先级:分为4级,低,中,高和极高。可以通过寄存器DMACHxCTL的PRIO位域来配置。 硬件优先级:当通道具有相同的软件优先级时,编号低的通道优先级高。例:通道0和通道2配置为相同的软件优先级时,通道0的优先级高于通道2。11。1。4GD32的DMA工作过程 下图为GD32的DMA的系统框图。 图4GD32F2DMA框图 我们可以看到GD32内核,存储器,外设及DMA的连接,这些硬件最终通过各种各样的线连接到总线矩阵中,硬件结构之间的数据转移都经过总线矩阵的协调,使各个外设和谐的使用总线来传输数据。 如果不使用DMA,CPU传输数据还要以内核作为中转站,比如要将USART0的数据转移到SRAM中,这个过程是这样的: 第一步:内核经过总线矩阵协调,从获取AHB存储的外设USART0的数据。 第二步:内核再经过总线矩阵协调把数据存放到内存SRAM中。 图5GD32F2不使用DMA工作工作过程 如果使用DMA的话,数据传输需要以下步骤: 1。DMA传输时外设对DMA控制器发出请求。DMA控制器收到请求,触发DMA工作。 2。DMA控制器从AHB外设获取USART0的数据,存储到DMA通道中 3。DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设USART0的数据经由DMA通道存放到SRAM中,这个数据的传输过程中,完全不需要内核的参与,也就是不需要CPU的参与。 图6GD32F2使用DMA工作工作过程 在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。DMA传输结束,如果有更多的请求时,外设可以启动下一个周期。 DMA控制器数据流都能够提供源和目标之间的单向传输链路。每个数据流配置后都可以执行以下事务: 常规类型事务:存储器到外设、外设到存储器或存储器到存储器的传输。 双缓冲区类型事务:使用存储器的两个存储器指针的双缓冲区传输(当DMA正在进行自至缓冲区的读写操作时,应用程序可以进行至自其它缓冲区的写读操作)。 要传输的数据量(多达65535)可以编程,并与连接到外设AHB端口的外设(请求DMA传输)的源宽度相关。每个事务完成后,包含要传输的数据项总量的寄存器都会递减。 总之,每次DMA传送由3个操作组成: 1。从外设数据寄存器或者从当前外设存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMACHxPADDR或DMACHxMADDR寄存器指定的外设基地址或存储器单元; 2。存数据到外设数据寄存器或者当前外设存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMACHxPADDR或DMACHxMADDR寄存器指定的外设基地址或存储器单元; 3。执行一次DMACHxCNT寄存器的递减操作,该寄存器包含未完成的操作数目。11。2DMA的寄存器描述 第一个是DMA中断状态寄存器(DMAINTF)。该寄存器的各位描述如下图所示。我们如果开启了DMAINTF中这些中断,在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前DMA传输的状态。这里我们常用的是FTFIFx,即通道DMA传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。 图7DMAINTF寄存器各位描述 每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。为应用的灵活性考虑,通过设置寄存器的不同位来打开这些中断。 表3DMA中断请求 使能开启,我们也可以通过查询这些位来获得当前DMA传输的状态。这里我们常用的是FTFIFx位,即数据流x的DMA传输完成与否标志。 第二个是DMA中断标志清除寄存器(DMAINTC)。该寄存器的各位描述如下图所示。DMAIFCR的各位就是用来清除DMAISR的对应位的,通过写0清除。在DMAISR被置位后,我们必须通过向该位寄存器对应的位写入0来清除。 图8DMAINTC寄存器各位描述 第三个是DMA通道x配置寄存器(DMACCRx)(x17,下同)。该寄存器的我们在这里就不贴出来了,见《GD32参考手册》。该寄存器控制着DMA的很多相关信息,包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以DMACCRx是DMA传输的核心控制寄存器。 第四个是DMA通道x传输数据量寄存器(DMACHxCTL)。这个寄存器控制DMA通道x的每次传输所要传输的数据量。其设置范围为065535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前DMA传输的进度。 第五个是DMA通道x的外设地址寄存器(DMACHxPADDR)。该寄存器用来存储GD32外设的地址,比如我们使用串口0,那么该寄存器必须写入0x40013804(其实就是USART00x04)。如果使用其他外设,就修改成相应外设的地址就行了。 最后一个是DMA通道x的存储器地址寄存器(DMACHxMADDR),该寄存器和DMACHxPADDR差不多,是用来放存储器的地址的。比如我们使用SendBuf〔5200〕数组来做存储器,那么我们在DMACHxMADDR中写入SendBuff就可以了。11。3DMA实例11。3。1DMA发送数据 本节我们要用到串口0的发送,属于DMA0的通道3,接下来我们就介绍库函数下DMA1通道3的配置步骤: 1)使能DMA时钟rcuperiphclockenable(RCUDMA0);使能DMA时钟 2)初始化DMA通道3参数 前面讲解过,DMA通道配置参数种类比较繁多,包括内存地址,外设地址,传输数据长度,数据宽度,通道优先级等等。这些参数的配置在库函数中都是在函数DMAInit中完成,下面我们看看函数定义:voiddmainit(uint32tdmaperiph,dmachannelenumchannelx,dmaparameterstructinitstruct) 函数的第一个参数是指定初始化的DMA,这里有两个选择,DMA0和DMA1;第二个参数是通道号,这个很容易理解,每个DMA有7个通道,这里使用的是通道3;第二个参数通过初始化结构体成员变量值来达到初始化的目的,下面我们来看看dmaparameterstruct结构体的定义:DMAinitializestructuretypedefstruct{uint32tperiphaddr;!peripheralbaseaddressuint32tperiphwidth;!transferdatasizeofperipheraluint32tmemoryaddr;!memorybaseaddressuint32tmemorywidth;!transferdatasizeofmemoryuint32tnumber;!channeltransfernumberuint32tpriority;!channelpriorityleveluint8tperiphinc;!peripheralincreasingmodeuint8tmemoryinc;!memoryincreasingmodeuint8tdirection;!channeldatatransferdirection}dmaparameterstruct; 这个结构体的成员比较多,这里我们一一做个介绍。 第一个参数periphaddr用来设置DMA传输的外设基地址,比如要进行串口DMA传输,那么外设基地址为串口接受发送数据存储器USART00x04的地址。 第二个参数periphwidth用来设置外设的数据长度是为字节传输(8bits),半字传输(16bits)还是字传输(32bits),这里我们是8位字节传输,所以值设置为DMAPERIPHERALWIDTH8BIT。 第三个参数memoryaddr为内存基地址,也就是我们存放DMA传输数据的内存地址。 第四个参数memorywidth是用来设置内存的数据长度,和第二个参数意思接近,这里我们同样设置为字节传输DMAMEMORYWIDTH8BIT。 第五个参数number设置一次传输数据量的大小,这个很容易理解。 第六个参数priority是设置DMA通道的优先级,有低,中,高,超高四种模式,这个在前面讲解过,这里我们设置优先级别为中级,所以值为DMAPRIORITYMEDIUM。如果要开启多个通道,那么这个值就非常有意义。 第七个参数periphinc设置传输数据的时候外设地址是不变还是递增。如果设置为递增,那么下一次传输的时候地址加1,这里因为我们是一直往固定外设地址USART00x04发送数据,所以地址不递增,值为DMAPERIPHINCREASEDISABLE; 第八个参数memoryinc设置传输数据时候内存地址是否递增。这个参数和periphinc意思接近,只不过针对的是内存。这里我们的场景是将内存中连续存储单元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为DMAMEMORYINCREASEENABLE。 第九个参数direction设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数据发送到外设,也就是外设是源地还是目的地,这里我们设置为从内存读取数据发送到串口,所以外设自然就是目的地了,所以选择值为DMAMEMORYTOPERIPHERAL。 这里我们给出上面场景的实例代码:定义一个DMA配置结构体dmaparameterstructdmainitstruct;使能DMA时钟rcuperiphclockenable(RCUDMA0);初始化DMA0通道3dmadeinit(DMA0,DMACH3);dmainitstruct。directionDMAMEMORYTOPERIPHERAL;存储器到外设方向dmainitstruct。memoryaddr(uint32t)UART0TXBUF;存储器基地址dmainitstruct。memoryincDMAMEMORYINCREASEENABLE;存储器地址自增dmainitstruct。memorywidthDMAMEMORYWIDTH8BIT;存储器位宽为8位dmainitstruct。numberUART0TXLEN;传输数据个数dmainitstruct。periphaddr((uint32t)(USART00X04));外设基地址,即USART数据寄存器地址dmainitstruct。periphincDMAPERIPHINCREASEDISABLE;外设地址固定不变dmainitstruct。periphwidthDMAPERIPHERALWIDTH8BIT;外设数据位宽为8位dmainitstruct。priorityDMAPRIORITYMEDIUM;软件优先级为极高dmainit(DMA0,DMACH3,dmainitstruct); 3)DMA模式配置 我们要从内存中采集64个字节发送到串口,如果设置为重复采集,那么它会在64个字节采集完成之后继续从内存的第一个地址采集,如此循环。这里我们设置为一次连续采集完成之后不循环。因此需要关闭循环发送。 在我们下面的实验中,如果设置此参数为循环采集,那么你会看到串口不停的打印数据,不会中断,大家在实验中可以修改这个参数测试一下。循环模式可用于处理循环缓冲区和连续数据流(例如ADC扫描模式)。可以使用DMASxCR寄存器中的CIRC位使能此特性。当激活循环模式时,要传输的数据项的数目在数据流配置阶段自动用设置的初始值进行加载,并继续响应DMA请求。 还需设置是否是存储器到存储器模式传输,这里设置为不使用存储器到存储器模式。DMA循环模式配置,不使用循环模式dmacirculationdisable(DMA0,DMACH3);DMA存储器到存储器模式模式配置,不使用存储器到存储器模式dmamemorytomemorydisable(DMA0,DMACH3); 4)使能DMA0通道3,启动传输。 接着就要使能DMA传输通道。dmachannelenable(DMA0,DMACH3); 5)使能串口DMA发送 进行DMA配置之后,我们就要开启串口的DMA发送功能,使用的函数是: usartdmaenable(USART0,USARTDMATRANSMIT); 如果是要使能串口DMA接受,那么第二个参数修改为USARTDMARECEIVE即可。 这样,我们就可以启动一次USART0的DMA传输了。 6)查询DMA传输状态 在DMA传输过程中,我们要查询DMA传输通道的状态,使用的函数是:FlagStatusdmaflagget(uint32tdmaperiph,dmachannelenumchannelx,uint32tflag) 比如我们要查询DMA通道3传输是否完成,方法是:dmaflagget(DMA0,DMACH3,DMAFLAGFTF); 这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:uint32tdmatransfernumberget(uint32tdmaperiph,dmachannelenumchannelx) 比如我们要获取DMA通道3还有多少个数据没有传输,方法是:dmatransfernumberget(DMA0,DMACH3); 最后看看UART0的DMA整体配置。briefUSART0TXDMA配置,内存到外设(USART00x04)paramNoneretvalNonevoidusart0dmainit(void){定义一个DMA配置结构体dmaparameterstructdmainitstruct;使能DMA时钟rcuperiphclockenable(RCUDMA0);初始化DMA0通道3dmadeinit(DMA0,DMACH3);dmainitstruct。directionDMAMEMORYTOPERIPHERAL;存储器到外设方向dmainitstruct。memoryaddr(uint32t)SendBuff;存储器基地址dmainitstruct。memoryincDMAMEMORYINCREASEENABLE;存储器地址自增dmainitstruct。memorywidthDMAMEMORYWIDTH8BIT;存储器位宽为8位dmainitstruct。numberBUFFSIZE;传输数据个数dmainitstruct。periphaddr((uint32t)(USART00X04));外设基地址,即USART数据寄存器地址dmainitstruct。periphincDMAPERIPHINCREASEDISABLE;外设地址固定不变dmainitstruct。periphwidthDMAPERIPHERALWIDTH8BIT;外设数据位宽为8位dmainitstruct。priorityDMAPRIORITYMEDIUM;软件优先级为极高dmainit(DMA0,DMACH3,dmainitstruct);DMA循环模式配置,不使用循环模式dmacirculationdisable(DMA0,DMACH3);DMA存储器到存储器模式模式配置,不使用存储器到存储器模式dmamemorytomemorydisable(DMA0,DMACH3);DMA0通道3中断优先级设置并使能nvicirqenable(DMA0Channel3IRQn,0,0);使能DMA0通道3传输完成、传输错误中断dmainterruptenable(DMA0,DMACH3,DMAINTFTFDMAINTERR);使能DMA0通道3dmachannelenable(DMA0,DMACH3);} 主函数如下所示:briefmainfunctionparam〔in〕noneparam〔out〕noneretvalnoneintmain(void){inti;填充将要发送的数据for(i0;iBUFFSIZE;i){SendBuff〔i〕Y;}systickinitsysTickinit();LED1initledinit(LED1);usartinit1152008N1cominit(COM1,115200);DMAconfigusart0dmainit();printf(USART0DMATestr);USARTDMA发送使能usartdmaenable(USART0,USARTDMATRANSMIT);while(1){ledtoggle(LED1);delayms(1000);}} 主函数很简单,初始化串口,DMA后,向DMA发出请求即可进行数据传输了,这时CPU就可以干其他事情了。 将程序编译好后下载到板子中,通过串口助手可以看到在接收区有Y不断的打印输出,同时LED1不停闪烁。 图9实验现象 【注】在本例中串口是DMA操作的,而LED的闪烁是CPU控制,请读者朋友注意。11。3。2DMA中断接收数据 前面讲解了DMA的接收,接下来讲解DMA的接收。接收和发送差不多,对于USART0,DMA的接收通道为DMACH4,因此USART0DMA的接收和发送配置如下所示。briefUSART0TXRXDMA配置paramNoneretvalNonevoidusart0dmainit(void){定义一个DMA配置结构体dmaparameterstructdmainitstruct;使能DMA时钟rcuperiphclockenable(RCUDMA0);初始化DMA0通道4dmadeinit(DMA0,DMACH4);dmainitstruct。directionDMAPERIPHERALTOMEMORY;外设到存储器方向dmainitstruct。memoryaddr(uint32t)RecvBuff;存储器基地址dmainitstruct。memoryincDMAMEMORYINCREASEENABLE;存储器地址自增dmainitstruct。memorywidthDMAMEMORYWIDTH8BIT;存储器位宽为8位dmainitstruct。numberBUFFSIZE;传输数据个数dmainitstruct。periphaddr((uint32t)(USART00X04));外设基地址,即USART数据寄存器地址dmainitstruct。periphincDMAPERIPHINCREASEDISABLE;外设地址固定不变dmainitstruct。periphwidthDMAPERIPHERALWIDTH8BIT;外设数据位宽为8位dmainitstruct。priorityDMAPRIORITYMEDIUM;软件优先级为极高dmainit(DMA0,DMACH4,dmainitstruct);DMA循环模式配置,不使用循环模式dmacirculationdisable(DMA0,DMACH4);DMA存储器到存储器模式模式配置,不使用存储器到存储器模式dmamemorytomemorydisable(DMA0,DMACH4);初始化DMA0通道3dmadeinit(DMA0,DMACH3);dmainitstruct。directionDMAMEMORYTOPERIPHERAL;存储器到外设方向dmainitstruct。memoryaddr(uint32t)SendBuff;存储器基地址dmainitstruct。number0;传输数据个数dmainit(DMA0,DMACH3,dmainitstruct);DMA0通道4中断优先级设置并使能nvicirqenable(DMA0Channel4IRQn,0,0);使能DMA0通道4传输完成、传输错误中断dmainterruptenable(DMA0,DMACH4,DMAINTFTFDMAINTERR);使能DMA0通道4dmachannelenable(DMA0,DMACH4);使能DMA0通道3dmachannelenable(DMA0,DMACH3);} 上述配置中在DMA的发送的基础上新增了DMA中断接收,因此需要增加DMA接收的NVIC,其配置如下。DMA0通道4中断优先级设置并使能nvicirqenable(DMA0Channel4IRQn,0,0);使能DMA0通道4传输完成、传输错误中断dmainterruptenable(DMA0,DMACH4,DMAINTFTFDMAINTERR); 值得注意的,这里需要打开DMA的接收中断,对应的寄存器是DMACHxCTL,前面已经讲过了。 最后,还需要编写USART0的DMA中断接收函数。!briefthisfunctionhandlesDMA0Channel4exceptionparam〔in〕noneparam〔out〕noneretvalnonevoidDMA0Channel4IRQHandler(void){uint8tRecvLen;if(dmainterruptflagget(DMA0,DMACH4,DMAINTFLAGFTF)!RESET){dmainterruptflagclear(DMA0,DMACH4,DMAINTFLAGFTF);清除传输完成中断标志位总的buf长度减去剩余buf长度,得到接收到数据的长度RecvLensizeof(RecvBuff)dmatransfernumberget(DMA0,DMACH4);usart0dmarxclear();清空DMA接收通道usart0sendarray(RecvBuff,RecvLen);使用DMA发送数据memset(RecvBuff,,sizeof(RecvBuff));清空接收缓冲区}} 中断接收中封装了两个函数,当USART0DMA接收中断到了后,将接收的数据放入缓存区,然后需要清除DMA的接收通道,具体函数如下。brief清除DMA的传输数量寄存器paramNoneretvalNonevoidusart0dmarxclear(void){关闭DMA0通道4dmachanneldisable(DMA0,DMACH4);重新写入要传输的数据数量dmatransfernumberconfig(DMA0,DMACH4,sizeof(RecvBuff));使能DMA0通道4dmachannelenable(DMA0,DMACH4);} 然后就是将接收的数据通过USART0DMA发送出去,其函数如下。brief串口0DMA发送paramuint8tarr,uint8tlenretvalNonevoidusart0sendarray(uint8tarr,uint8tlen){if(len0)return;uint8tsendLenlensizeof(SendBuff)?sizeof(SendBuff):len;while(dmatransfernumberget(DMA0,DMACH3));检查DMA发送通道内是否还有数据if(arr){memcpy(SendBuff,arr,sendLen);}开启UARTTXDMAdmachanneldisable(DMA0,DMACH3);dmatransfernumberconfig(DMA0,DMACH3,sendLen);重新写入要传输的数据数量dmachannelenable(DMA0,DMACH3);启动DMA发送} 接下来看看主函数的代码。briefmainfunctionparam〔in〕noneparam〔out〕noneretvalnoneintmain(void){systickinitsysTickinit();ledinitledinit(LED1);usartinit1152008N1cominit(COM1,115200);DMAconfigusart0dmainit();printf(USART0DMATestr);USARTDMA发送使能usartdmaenable(USART0,USARTDMATRANSMIT);USARTDMA接收使能usartdmaenable(USART0,USARTDMARECEIVE);while(1){ledtoggle(LED1);delayms(1000);}} 主函数主要增加了串口DMA接收请求,其他的和上一个实例是一样的。 最后编译下载固件,打开串口助手,这里发送一些数据,效果如下所示。 图10实验现象