LPC800前生今世-第九章 直接存储器访问 (DMA)

描述

DMA是直接存储器访问(DirectMemory Access)的缩写。在MCU芯片中,DMA是除CPU之外,最常见的总线主设备。作为总线主设备,DMA控制器可以输出地址和控制信号到总线上,主动地发起和控制数据传输过程,它能够按照程序的配置,在两个从设备之间传输数据。例如在存储器和I2C模块之间传输数据,实现I2C数据的发送或接收,或从ADC读出数据再传送到USART进行发送。

 

 

下图是LPC82x的部分框图,图中醒目标出了总线主设备,和DMA控制器。mcu                       图1.LPC82x结构框图(部分)LPC82x的所有片内外设中,只有DMA控制器是总线主设备,其它都是总线从设备,只有主设备才能主动发起数据的传输操作。

DMA的优势是,可以在CPU最少的干预下,高效地执行数据块的传输,节省CPU的时间,同时可以在CPU执行内部操作而不访问总线时,更高效地利用总线的时间。

 

 

 1.1  DMA控制器的一些基本操作在介绍LPC800DMA控制器之前,先通过这个传输示意图,回顾一下通用DMA控制器必须具备的基本操作。mcu

                          

图2.DMA传输示意图

 

产生数据传输的源地址和目标地址:DMA控制器在接到传输请求后,在内部总线上产生源数据地址SA,读出要传输的数据,然后再产生存放数据的目标地址DA,将数据写入指定的地方。

 

 

控制每次传输后地址的变化:可以控制每次DMA传输是涉及到一个连续的地址区域还是单个独立的地址。每次读写的源地址和目标地址分别改变或不改变,也可以同步地改变。

 

 

控制传输的数据长度:软件需要指定每次DMA传输的数据数量n

 

 

指定传输的数据宽度:软件需要指定每次DMA读写的数据宽度,一般是以内部数据总线的宽度为限。对于32MCU,可以是1个字节、2个字节(半字)4个字节()

 

 

控制传输数据的节奏,即传输数据的时机:每次DMA读写都需要在有传输请求时才会执行。传输请求可以来自于数据源设备,例如ADC转换结束;传输请求也可以来自于数据目标设备,例如SPI的发送就绪。因此每两次传输请求的间隔可以不一致。DMA的传输请求也可以由DMA控制器内部产生,用于内存中数据块的传送。

 

 

状态查询和中断控制:DMA控制器的状态和中断可以是多种多样,通常有传输开始、传输结束、传输错误等。

 

 

LPC800DMA控制器实现了上述所有的基本控制功能,而且还有不少自己的特色,下面一一介绍。

 

 

 1.2  DMA传输与CPU指令的执行不管是CPU还是DMA控制器,都要通过同一条总线访问存储器和各种片内外设,进行数据传输。CPU的基本操作就是取指、译码、运算、执行的过程,取指操作需要占用总线,执行阶段的读数据或写数据操作也需要占用总线。DMA控制器可以充分地利用CPU不占用总线的时间,在总线上传输数据。

 

如果在同一个时间,CPUDMA控制器都需要占用总线,这种情况下需要有仲裁机制,协调两个总线主设备的动作。

 

如果在总线已经被某个主设备(例如CPU)占用的时候,另一个主设备(例如DMA控制器)就会稍作等待,待总线空闲时,再开始数据传输。

 

从以上描述可以看出,DMA可以在不需CPU干预的情况下,利用CPU不占用总线的空闲时间进行数据传输。这样不但提高了总线的利用率,还减轻了CPU搬运数据的负担,提高了系统的并行性,能够实现更复杂的控制要求,或降低整体的功耗。

 

 

 1.3  LPC800DMA控制器LPC800DMA控制器具有如下特性:多个通道,每个通道唯一地连接到一个片内外设的输入或输出请求,例如USARTSPII2C等通信外设。▲DMA传输可以由片内或片外事件触发,每个DMA通道都可以有多个触发输入源,每次传输只能选择一个触发源。可以指定每个DMA通道的优先级,当通道之间的传输需求发生冲突时,高优先级通道先进行传输。传输描述符机制,通过多个传输描述符互联,可以实现链式的DMA传输控制。每次(每个传输描述符)最多可以传输1024个字(1024x4=4096字节)地址增量的多种选项,允许灵活的数据包处理。LPC800各系列的DMA配置如下:

 

系列

 

 

通道数目

 

 

触发输入源数目

 

 

LPC80xLPC81x

 

 

0

 

 

0

 

 

LPC82x

 

 

18

 

 

9

 

 

LPC83x

 

 

18

 

 

8

 

 

LPC84x

 

 

25

 

 

13

 

 

1.3.1 DMA寄存器一览

DMA控制器有15个寄存器,涉及到所有通道,可以分为四组。

 

 

寄存器组

 

 

寄存器名称

 

 

功能

 

 

说明

 

 

通用寄存器组

 

 

CTL

 

 

DMA控制器寄存器

 

 

只有一个控制位,使能DMA控制器

 

 

INTSTAT

 

 

中断状态寄存器

 

 

标志是否有挂起的中断

 

 

SRAMBASE

 

 

传输描述符地址寄存器

 

 

所有通道第一个传输描述符的存放地址(必须512字节对齐)

 

 

通道控制寄存器组

 

 

ENABLESET0

 

 

通道使能寄存器

 

 

每个通道占用一位。表示是否使能对应通道

 

 

ENABLECLR0

 

 

通道失能寄存器

 

 

每个通道占用一位。表示是否失能对应通道

 

 

ACTIVE0

 

 

通道激活状态寄存器

 

 

每个通道占用一位。表示对应通道是否加载了传输描述符

 

 

BUSY0

 

 

通道忙状态寄存器

 

 

每个通道占用一位。表示对应通道是否正在搬运数据

 

 

通道中断寄存器组

 

 

ERRINT0

 

 

错误中断状态寄存器

 

 

每个通道占用一位。表示是否有错误中断

 

 

INTENSET0

 

 

中断使能寄存器

 

 

每个通道占用一位。表示是否使能对应通道的中断

 

 

INTENCLR0

 

 

中断失能寄存器

 

 

每个通道占用一位。表示是否失能对应通道的中断

 

 

INTA0

 

 

中断A状态寄存器

 

 

每个通道占用一位。表示是否有中断A

 

 

INTB0

 

 

中断B状态寄存器

 

 

每个通道占用一位。表示是否有中断B

 

 

传输控制寄存器

 

 

SETVALID0

 

 

设置“有效”控制位寄存器

 

 

每个通道占用一位。用于设置描述符的“有效”控制位

 

 

SETTRIG0

 

 

设置“触发”控制位寄存器

 

 

每个通道占用一位。用于触发对应的通道传输

 

 

ABORT0

 

 

通道中止传输寄存器

 

 

每个通道占用一位。用于中止对应的通道传输

 

 

每个DMA通道还分别有下表中的三个寄存器,用于配置各个通道的参数,将在1.4节详细介绍。

 

寄存器名称

 

 

功能

 

 

说明

 

 

CFG

 

 

通道配置寄存器

 

 

用于配置通道的使能、触发、成组传输(Burst)和优先级的选项

 

 

CTLSTAT

 

 

通道控制状态寄存器

 

 

用于标示通道的有效和触发状态

 

 

XFERCFG

 

 

通道传输配置寄存器

 

 

用于配置通道的各个配置选项

 

 

1.3.2 DMA传输描述符

 

除了上述寄存器外,LPC800DMA控制器通过位于内存中的传输描述符,控制每次的DMA传输。每个传输描述符有4个字(32/),内容如下:mcu多个传输描述符可以构成一个连续的链条或循环链,链条中的每一个描述符对应一次DMA传输。传输的数据数量、数据宽度以及地址变化的方式等,由XFERCFG寄存器的内容指定。

 

每次DMA传输开始前,DMA控制器都要把一个完整的描述符读入,传输描述符中偏移地址为0x0的字会被传送到XFERCFG寄存器中,用于控制各项传输参数。一个链条的第一次传输参数,需要由软件直接写入到XFERCFG寄存器,因此链条中的第一个描述符的第一个字为保留位。在传输开始时,DMA控制器会把传输区的地址读入内部寄存器中。

 

 

使用描述符的链接特性,可以方便地实现多种数据传输控制。

 

 

例如需要使用SPI驱动一个LCD屏幕产生动画效果时,可以配置为下图所示的乒乓结构的DMA描述符链条,CPU只需要不断地生成显示图片,由DMA控制器平行地进行图片数据至LCD屏幕的传送。

 

 

mcu                    图3.乒乓结构的DMA描述符链

这里使用了三个传输描述符。第一个描述符(链头)指示将缓冲区A的数据传输到SPI的发送寄存器,后面两个描述符分别指示缓冲区B和缓冲区A的数据,轮流传输到SPI的发送寄存器。CPU只需要在对应的缓冲区准备好数据,再设置对应的描述符为“有效”,接下来DMA控制器就会直接把数据传送到SPI模块进行发送。

 

 

1.3.3 DMA传输通道每个通信外设的发送传输可以产生DMA请求,接收传输也可以产生DMA请求。

 

LPC800DMA控制器非常简单,每个片内外设产生的DMA传输请求信号,唯一地连接到一个固定的DMA通道。即如果把片内外设作为DMA传输的源或目标,并且希望由该外设来控制传输的节奏(通过DMA传输请求信号),则必须使用对应的通道。如果能够使用其它的方法(例如时钟触发等),保证外设不会发生数据溢出的情况,则可以使用任意通道,但这种用法不能最优地利用带宽时间,除非需要特殊的时序控制,一般不建议使用。

 

每个通道对应的DMA请求源如下表所示:

mcumcu

              表1.DMA通道与DMA请求源的对应表

1.3.4 DMA触发源的选择

 

 

DMA控制器之外,有一个DMA触发输入 (DMA TRIGMUX)模块,每个DMA通道都在这个模块中有一个对应的多选一选择器,由DMA_ITRIG_INMUXn寄存器控制(n对应表1的通道号),用户可以在多种信号中选择一个作为DMA的触发信号。下表列出了所有可能的选项:mcu 表2.DMA触发选项(1):在LPC82x/83x中,n取值0~17;在LPC84x中,n取值0~24

 

*:每一个DMA通道都输出一个触发信号,所有通道输出的触发信号都连接到两个多选一的选择器:DMA_INMUX_INMUX0DMA_INMUX_INMUX1,这两个多选一选择器的输出可以作为另一个DMA通道的触发源选项之一。这个配置允许多个DMA通道的协同操作。

 

mcu                       图4.DMA触发输入框图

上图为某个DMA通道的触发输入模块框图,每个通道都有一个这样相同的电路用于选择它的触发输入。    1.4 DMA传输的请求、触发与生成传输概念每个通道的DMA触发信号相当于这个通道的总开关,只有打开这个总开关,才能够进行随后的DMA传输操作。

每次DMA传输(即图2示意中的每一次读写)的时机,则由DMA请求信号决定。

 

 

成组传输(Burst)是指在一次触发之后,按照指定的次数进行一组数据的传输,这组数据传输完成后,需要另一次的触发条件才能进行下一组的数据传输。

 

成组传输控制和DMA传输请求控制组合为四种操作模式,他们与触发信号的关系如下表所示。

 

表中的DMA传输请求信号以USARTTXRDY为例:

组合模式

 

 

使能成组传输(TRIGBURST,见1.5.1)

 

 

 

使能外设请求(PERIPHREQEN,见1.5.1)

 

 

   

操作模式说

 

 

0

 

 

0

 

 

0

 

 

触发信号用于启动完整的DMA传输过程,只需一个触发信号即可完成所有数据传输。

 

 

DMA将以最快的速度,连续不断地传送数据,直到完成所有数据。

 

 

1

 

 

0

 

 

1

 

 

每个DMA传输请求(TXRDY),只能传输一个数据。

 

 

2

 

 

1

 

 

0

 

 

每个触发信号启动一组DMA传输,每组传送BURSTPOWER(1.5.1)个数据。因此总共需要(XFERCOUNT/BURSTPOWER)组的传输(即需要相同数目的触发信号),才能完成所有数据。

 

 

XFERCOUNT位于XFERCFG寄存器(1.5.2)

 

 

DMA将以最快的速度,连续不断地传送数据,直到完成所有数据。

 

 

3

 

 

1

 

 

1

 

 

每个DMA传输请求(TXRDY),只能传输一个数据。

 

 

            表3.成组传输和外设请求与触发信号的关系表

 

表中的组合模式0适合于从存储器至存储器的数据块拷贝;组合模式1适合于常用的通信模块的数据发送和接收。

 

 

组合模式23则视具体的应用情况,由用户自由发挥。例如,要在USART上发送若干个固定长度的数据包,而发送每个数据包的时间需要由定时器来决定,则可以使用上述的组合模式3,设置BURSTPOWER为数据包的长度,设置SCT定时器产生触发信号(见表2SCT_DMA0/1)

 

 

拿自动步枪做一个形象的比喻,成组的概念相当于子弹夹,外设请求相当于扳机,触发相当于击发保险,一次DMA传输中需要打出一箱子弹。那么每种组合模式有如下对应:组合模式0:打开击发保险后,不需其它动作,整箱子弹即全部射出。组合模式1:打开击发保险后,每扣动一次扳机,射出一发子弹,直到打完整箱子弹。组合模式2:打开击发保险后,不需其它动作,一个弹夹中的子弹即全部射出。然后再次打开击发保险,即射出另一个弹夹中的全部子弹。重复上述操作直到整箱子弹打光。组合模式3:打开击发保险后,每扣动一次扳机,射出一个弹夹中的一颗子弹,重复直到这个弹夹中的子弹打光,击发保险自动关闭。然后须再次打开击发保险,再一次次地扣动扳机,逐个射出另一个弹夹中的所有子弹,击发保险再次自动关闭。重复上述过程直到打完整箱子弹。

这里有一个要求,即XFERCOUNT必须能被BURSTPOWER整除,即一箱子弹的数目,必须是一个弹夹能容纳子弹个数的倍数。

 

 

 1.5 DMA通道参数寄存器

1.3.1节中列出的寄存器,用于控制整个DMA控制器,以及控制每个通道的使能、中断和触发等状态。每个通道的具体工作模式,由下述三个寄存器来描述。

1.5.1 DMA通道配置寄存器(CFG)

 

 

 

mcu

 

各个控制域的说明如下:▲PERIPHREQEN:使能外设请求。0 – 使能外设请求;1 – 不使能外设请求。

 

每个通道的外设请求来源是固定的,见表1。例如对应SPI0的发送就绪信号(TXRDY)DMA通道,在LPC82x/83x中是通道7,在LPC84x中是通道11HWTRIGEN:使能硬件触发。硬件触发信号源由DMA_ITRIG_INMUXn寄存器选择,1.3.4节。0 – 使能硬件触发;1 – 不使能硬件触发。TRIGPOL:触发极性。TRIGTYPE:触发类型。TRIGPOLTRIGTYPE共同决定如何使用触发信号,组合关系如下表。

 

TRIGTYPE

 

 

TRIGPOL

 

 

说明

 

 

0

 

 

0

 

 

下降沿触发

 

 

0

 

 

1

 

 

上升沿触发

 

 

1

 

 

0

 

 

低电平触发

 

 

1

 

 

1

 

 

高电平触发

 

 

TRIGBURST:触发成组传输。

0 – 触发之后不按组传输(或可理解为所有数据为一组)1 - 触发之后执行成组传输。BURSTPOWER:成组传输中每组的长度。这个域的内容为2的幂次数值,取值为0~10,表示每组长度为1(20)2(21)4(22) 8(23)……1024(210)不支持0~10之外的数值。

注意:XFERCOUNT必须是BURSTPOWER的倍数。

SRCBURSTWRAP:成组传输中每组传输结束后,是否需要恢复传输的源地址。0 – 不恢复源地址;1 – 恢复源地址。

 

这个控制项适合于重复地读出相同的数据块,或相同的一组寄存器。DSTBURSTWRAP:成组传输中每组传输结束后,是否需要恢复传输的目标地址。0 – 不恢复目标地址;1 – 恢复目标地址。

 

这个控制项适合于重复地写入相同的存储区,例如重复地读出一组传感器的数值,软件只关心即时的数值,而不关心数值变化的过程。CHPRIORITY:设置本通道的优先级。在多个通道同时请求获得总线进行传输时,优先级高的通道先得到总线的使用权限。

0 – 最高优先级;7 – 最低优先级。

 

 

1.5.2 DMA通道传输配置寄存器(XFERCFG)

mcu

该寄存器的内容给出了当前DMA传输的各项参数。一个传输描述符指定的一次传输结束后,DMA控制器会自动地读入链条中的下一个描述符,描述符的第一个字的内容会加载到XFERCFG寄存器,见1.3.2节的说明。

 

XFERCFG各个控制域的说明如下:CFGVALID:表示所对应的描述符是否有效。0 – 描述符无效;1 – 描述符有效。RELOAD:当前描述符所指定的传输完成后,是否需要读入下一个描述符。0 – 不读入下一个描述符;1 – 读入下一个描述符,允许描述符的链接操作。SWTRIG:软件触发。0 – 需要由HWTRIGENTRIGPOLTRIGTYPE指定通道的触发条件。

 

1 – 设置此位表示该通道的触发条件立即满足。

 

注意:使用软件触发时,在TRIGBURST=0时,不得使用电平触发。CLRTRIG:当前描述符的传输结束后,是否清除触发条件。0 – 不清除。如果RELOAD=1,则下一个描述符的触发条件满足。

 

1 – 清除。当前描述符指示的传输结束后,清除触发条件。

 

注意:只有软件触发条件和边沿触发条件可以被清除,而电平触发条件不能被清除。SETINTA:当前描述符的传输结束后,是否产生中断标志INTA0 – 不产生中断标志;1 – 产生中断标志。▲SETINTB:当前描述符的传输结束后,是否产生中断标志INTB0 – 不产生中断标志;1 – 产生中断标志。

 

INTAINTB在硬件上没有差别,用户可以用这两个中断(标志)区别是哪个描述符的传输完成了,尤其是在乒乓结构的传输中。WIDTH:表示每次DMA传输的数据宽度。源地址的读和目标地址的写,使用相同的数据宽度。0 – 8位数据传输;1 – 16位数据传输;2 – 32位数据传输;3 – 保留组合,不得使用。

 

注意:如果要求的数据宽度是16位或32位,则传输的地址也必须分别是2字节或4字节对齐的。SRCINC:表示每次传输一个数据后,源地址的增量变化。0 – 地址无变化。

 

1 – 地址按数据宽度+1,指向数据区中的下一个数据。

 

2 – 地址按数据宽度+2

 

3 – 地址按数据宽度+4DSTINC:表示每次传输一个数据后,目标地址的增量变化。该域的取值含义与SRCINC一样。

 

SRCINCDSTINC取值为0,最常见的应用场景是面对外设寄存器的读写,例如传送一个数据块至SPI0TXDAT,或从USARTRXDAT读出一组数据至存储区。

 

SRCINCDSTINC取值为1,最常见的应用场景对一个连续的存储区的读写。

 

SRCINCDSTINC取值为23时,一个应用案例是,当WIDTH=0(8位数据)时,希望传输一组数据字或半字中的某个字节,而不管其它字节。▲XFERCOUNT:传输的数据总数,寄存器中填入(总数-1)该域有10位,即最大传输数据数目为1024

 

传输的字节总数为:(XFERCOUNT + 1) *WIDTH

 

注意1:在DMA传输过程中,DMA控制器会递减该数值,因此不能在传输过程中或传输结束后,读出该域而得知预设的传输数目。

 

注意2:如果设置了TRIGBURST =1,则XFERCOUNT必须是BURSTPOWER的倍数。

 

1.5.3 DMA通道控制和状态寄存器(CTLSTAT)mcu这个寄存器只有两个标志位,用户可以检查这些标志位,获知当前DMA控制器的部分运行状态。▲VALIDPENDING:延迟的有效位。见0的说明。▲TRIG:触发标志。该位表示是否有触发条件。设置触发条件有多种途径:通道传输配置寄存器(XFERCFG)SWTRIG控制位。设置触发控制寄存器(SETTRIG0) ,该寄存器可以同时设置一个或多个通道的触发条件。■DMA触发输入模块选定的硬件触发信号,满足TRIGPOLTRIGTYPE时。清除触发条件也有多种途径:CLRTRIG=1时,描述符指定的传输结束时,清除触发条件。

当失能DMA控制器时,见CTRL寄存器。

 1.6 描述符有效位的延迟设置机制通道传输配置寄存器(XFERCFG)CFGVALID位,指定该描述符是否有效。当一个有效的描述符被读入DMA控制器后,当CTLSTAT寄存器的TRIG标志被设置后,DMA传输就会立即开始,这是最理想的情况。

 

通常的情况是,当准备好一个描述符A,尤其是使用描述符链时,描述符A所对应的存储区的数据可能还没有准备好,循环的乒乓结构就是一个很好的例子。这种情况下,就需要先设置描述符ACFGVALID=0,待数据区准备好后再设置它为有效。这样延迟设置描述符有效,是通过SETVALID0寄存器来完成。

 

SETVALID0寄存器的每一位对应一个DMA通道,第n位写’1’表示延迟设置通道n的描述符有效。

 

使用SETVALID0寄存器实现延迟设置描述符有效,是为了避免设置错误。设想一下,当DMA控制器已经在运行链上的某个描述符B时,软件无法知道另一个描述符A是否已经被读入DMA控制器。如果它还未被读入DMA控制器,则可以直接操作描述符A所在的存储区;如果它已经被读入DMA控制器,则应该操作XFERCFG寄存器。

 

SETVALID0寄存器就是为了正确地设置描述符的有效位。

 

经过以上介绍可以看到,如果当前加载到DMA控制器的描述符是有效的,设置SETVALID0寄存器表示延迟设置链中下一个描述符为有效,此时CTLSTAT寄存器的VALIDPENDING为‘1’,标示这种状态;当下一个描述符被读入DMA控制器时,经延迟的设置描述符有效的操作才最终完成,此时VALIDPENDING位被清除。如果当前加载到DMA控制器的描述符是无效的,设置SETVALID0寄存器表示改变当前这个描述符为有效,不需经过延迟,操作立即生效。

 

使用这种延迟机制,软件可以从容地先准备好描述符链,然后再按部就班地准备好数据区,逐步推进数据传输进程,而不必费周折查询等待DMA控制器的状态。

 

 

 1.7 若干DMA传输例程

本节的几个例程,分别展示几种DMA的常见用法。所有例程都会用到这样几个结构体。

 

 

结构体DMA_CHDESC_T是所有通道的描述符链中的第一个描述符。

 

 

typedef struct {

 

 

 

     uint32_t notused;  // 第一个描述符的这个位置是保留位

 

 

 

     uint32_t source;   // DMA传输源数据区的末地址  

 

 

 

     uint32_t dest;     // DMA传输目标数据区的末地址

 

 

 

     uint32_t next;     // 链接到下一个描述符  

 

 

 

} DMA_CHDESC_T;

 

 

所有通道的第一个描述符,需要按顺序放在一个DMA_CHDESC_T数组中,而且这个数组的开始地址必须是512字节对齐的内存地址:

 

 

ALIGN(512)  DMA_CHDESC_T Chan_Desc_Table[];

 

 

每个通道的第一个描述符,必须放在这个数组中与通道编号对应的单元中。例如USART1_RX_DMA通道的第一个描述符需要放在数组的第2个单元中(见表1)

 

 

结构体DMA_RELOADDESC_T适用于其它描述符。所有描述符必须位于16字节对齐的内存地址。

 

 

typedef struct {

 

 

 

     uint32_t xfercfg;  // 描述符的传输配置寄存器

 

 

 

     uint32_t source;   // DMA传输源数据区的末地址  

 

 

 

     uint32_t dest;     // DMA传输目标数据区的末地址

 

 

 

     uint32_t next;     // 链接到下一个描述符  

 

 

 

} DMA_RELOADDESC_T;

 

 

1.7.1 DMA执行内存中的数据块拷贝

 

使用DMA的最简单应用就是在内存中拷贝一个数据块,这是一个非常有效率的搬移数据块的方法,尤其是数据量比较大时,CPU可以同时执行更多的操作。DMA拷贝数据块不涉及到任何外设请求,使用软件触发,所以也不涉及到任何硬件的触发。

 

本例程先用随机数初始化数组Buffer1[ ],然后用DMABuffer1[ ]传送数据至Buffer2[ ]。数组定义如下:

 

mcu下面是初始化DMA控制器,并启动DMA的函数。代码片段1.使用DMA在内存中拷贝一个数据块

 

01  void DMA_M2M_Init(uint32_t *buf1, uint32_t *buf2, uint32_t length)
02  {   uint32_t ch_cfg_val, xfercount, xfercfg; 
03  
04      LPC_SYSCON->SYSAHBCLKCTRL |= DMA;
05      LPC_DMA->CTRL = 0;
06  
07      LPC_DMA->SRAMBASE = (uint32_t)(&Chan_Desc_Table); 
08  
09      xfercount = length - 1; 
10  ch_cfg_val = 0; 
11      xfercfg = 0 << DMA_XFERCFG_CFGVALID | // 暂时设置为无效
12                0 << DMA_XFERCFG_RELOAD   | // 没有下一个描述符
13                1 << DMA_XFERCFG_SWTRIG   | // 软件触发
14                1 << DMA_XFERCFG_CLRTRIG  | // 传输结束时清除触发标志
15                1 << DMA_XFERCFG_SETINTA  | // 传输结束时设置INTA中断
16                0 << DMA_XFERCFG_SETINTB  |
17                2 << DMA_XFERCFG_WIDTH    | // 数据宽度为32位
18                1 << DMA_XFERCFG_SRCINC   | // 每次传输后源地址递增
19                1 << DMA_XFERCFG_DSTINC   | // 每次传输后目标地址递增
20                xfercount << DMA_XFERCFG_XFERCOUNT; // 传输长度
21      LPC_DMA->CHANNEL[CH_USART0_RX].CFG = ch_cfg_val; 
22      LPC_DMA->CHANNEL[CH_USART0_RX].XFERCFG = xfercfg; 
23  
24      Chan_Desc_Table[CH_USART0_RX].source = (uint32_t)(&buf1[xfercount]);
25      Chan_Desc_Table[CH_USART0_RX].dest   = (uint32_t)(&buf2[xfercount]);
26      Chan_Desc_Table[CH_USART0_RX].next   = (uint32_t)0L; 
27  
28      LPC_DMA->INTENSET0 =  1 << CH_USART0_RX; 
29      LPC_DMA->ENABLESET0 = 1 << CH_USART0_RX; 
30  
31      LPC_DMA->CTRL = 1; 
32  
33      LPC_DMA->SETVALID0 = 1 << CH_USART0_RX; 
34  //  LPC_DMA->SETTRIG0  = 1 << CH_USART0_RX;
35  }

在这个例程中,用到了宏定义CH_USART0_RX,这是对应USART0接收方向的DMA通道号(见表1),在头文件中定义了所有的通道号:
 

#define CH_USART0_RX 0 // USART0接收就绪

 

 

 

#define CH_USART0_TX 1 // USART0发送就绪

 

 

 

#define CH_USART1_RX 2 // USART1接收就绪

 

 

 

#define CH_USART1_TX 3 // USART1发送就绪

 

 

 

......

 

 

 

......

 

 

Chan_Desc_Table所有通道的描述符链头构成的数组,这个数组的起始地址必须是512字节对齐的。

 

 

ALIGN(512) DMA_CHDESC_T Chan_Desc_Table[1];

 

 

在本例程中,由于使用的是通道0,而没有使用其它通道,所以在数组中只配置了一个单元。

 

 

原则上,这个数组中对应不使用的通道的描述符单元,可以挪做其它用途。

 

 

在这个例程里,配置好所有的寄存器和描述符,并且使能了整个DMA控制器后,在第33行配置了对应的传输符为有效,该语句执行后DMA传输立即开始了。

 

 

如果第13行没有配置XFERCFG的“软件触发”位,执行第33行后DMA通道还需要等待触发信号才能开始传输,第34行就是由软件发出DMA触发信号的另一种途径。

 

 

由上面的介绍可以看出,可以有多种方式,灵活地安排启动DMA传输的时机和方法,让用户可以更加有效地安排自己的应用流程。

 

 

下面是这个例程的主函数和中断处理程序。

 

代码片段2.使用DMA拷贝一个数据块的中断函数和主函数

 

01  uint8_t DMA_IntA_Flag;
02  void DMA_IRQHandler(void) // DMA中断处理程序
03  {
04      if (LPC_DMA->INTA0 & (1 << CH_USART0_RX)) {
05          LPC_DMA->INTA0 = 1 << CH_USART0_RX;
06          DMA_IntA_Flag = 1;
07      }
08      if (LPC_DMA->ERRINT0 & (1 << CH_USART0_RX)) 
09          LPC_DMA->ERRINT0 = 1 << CH_USART0_RX;
10  }
11  
12  void main()
13  {   uint32_t pp;
14      for (pp = 0; pp < BUF_SIZE; pp++)
15          Buffer1[pp] = rand();
16  
17      DMA_IntA_Flag = 0; 
18      DMA_M2M_Init(Buffer1, Buffer2, BUF_SIZE); 
19  
20      NVIC_EnableIRQ(DMA_IRQn); 
21      do {
22          __WFI();
23      } while (DMA_IntA_Flag == 0); 
24  
25      while (1);
26  }

DMA传输结束后产生中断,在中断函数中将软件标志置’1’,主函数可以知道DMA传输是否已经完成。在实际的项目中,用户程序可以替换上述21~23行的代码,执行其它的一些操作。

1.7.2 DMA执行USART0的连续发送(硬件触发)

 

下面这个例程是使用DMA通过USART0发送一个字符串,需要配置使用USART0的发送请求,并采用开发板上的USER_KEY产生硬件触发,即按下按键后才送出所有数据,用户可以在PC端的虚拟串口上看到送出的字符串。

 

首先还是DMA的初始化函数,这个函数与前面的数据块搬运初始化基本一致,指示CFGXFERCFG寄存器的内容有所变化。

 

 

代码片段3. DMA通过USART0发送字符串01  void DMA_UART_Send(uint8_t *buf, uint32_t length)

02  {   uint32_t ch_cfg_val, xfercount, xfercfg; 
03  
04      LPC_SYSCON->SYSAHBCLKCTRL |= DMA;
05      LPC_DMA->CTRL = 0;
06  
07      LPC_DMA->SRAMBASE = (uint32_t)(&Chan_Desc_Table); 
08  
09      xfercount = length - 1; 
10  ch_cfg_val = 1 << DMA_CFG_PERIPHREQEN | // 外设请求
11                   1 << DMA_CFG_HWTRIGEN |  // 硬件触发
12                   0 << DMA_CFG_TRIGTYPE |  // 边沿触发
13                   0 << DMA_CFG_TRIGPOL;    // 下降沿触发
14  
15      xfercfg = 0 << DMA_XFERCFG_CFGVALID | // 暂时设置为无效
16                0 << DMA_XFERCFG_RELOAD   | // 没有下一个描述符
17                0 << DMA_XFERCFG_SWTRIG   | // 没有软件触发
18                1 << DMA_XFERCFG_CLRTRIG  | // 传输结束时清除触发标志
19                1 << DMA_XFERCFG_SETINTA  | // 传输结束时设置INTA中断
20                0 << DMA_XFERCFG_SETINTB  |
21                0 << DMA_XFERCFG_WIDTH    | // 数据宽度为8位
22                1 << DMA_XFERCFG_SRCINC   | // 每次传输后源地址递增
23                0 << DMA_XFERCFG_DSTINC   | // 每次传输后目标地址不递增
24                xfercount << DMA_XFERCFG_XFERCOUNT; // 传输长度
25      LPC_DMA->CHANNEL[CH_USART0_TX].CFG = ch_cfg_val; 
26      LPC_DMA->CHANNEL[CH_USART0_TX].XFERCFG = xfercfg; 
27  
28      Chan_Desc_Table[CH_USART0_TX].source = (uint32_t)(&buf[xfercount]);
29      Chan_Desc_Table[CH_USART0_TX].dest   = (uint32_t)(&LPC_USART0->TXDAT);
30      Chan_Desc_Table[CH_USART0_TX].next   = (uint32_t)0L; 
31  
32      LPC_DMA->INTENSET0 =  1 << CH_USART0_TX; 
33      LPC_DMA->ENABLESET0 = 1 << CH_USART0_TX; 
34  
35      LPC_DMA->CTRL = 1; 
36  
37      LPC_DMA->SETVALID0 = 1 << CH_USART0_TX; 
38  }

上述代码里用橙色标注出与代码片段1不同的地方。还有一个明显的不同是,此处所有涉及到DMA通道号时,都换成了CH_USART0_TX

USART0的初始化部分与USART章节的代码完全一致,现抄录如下。

 

 

代码片段4.基本UART收发例程的USART0初始化

 

00  void USART0_init() {
01      LPC_SYSCON->SYSAHBCLKCTRL |= (UART0 | SWM); 
02  
03      LPC_SYSCON->PRESETCTRL &= (UART0_RST_N); 
04      LPC_SYSCON->PRESETCTRL |= ~(UART0_RST_N);
05  
06      ConfigSWM(U0_TXD, P0_4); 
07      ConfigSWM(U0_RXD, P0_0);
08  
09      LPC_SYSCON->UARTCLKDIV = LPC_SYSCON->SYSAHBCLKDIV; // 设置USART时钟的分频系数
10      LPC_SYSCON->UARTFRGMULT = 4; 
11      LPC_SYSCON->UARTFRGDIV = 255; 
12      LPC_USART0->BRG = 16 - 1;
13  
14      // 8个数据位,无校验位,1个停止位,没有硬件流控,异步模式
15      LPC_USART0->CFG = DATA_LENG_8 | PARITY_NONE | STOP_BIT_1; 
16  
17      LPC_USART0->CTL = 0;
18  
19      LPC_USART0->STAT = 0xFFFF; 
20  
21      LPC_USART0->INTENSET = RXRDY; 
22      NVIC_EnableIRQ(UART0_IRQn);
23  }

接下来是本节的重点。代码片段3的第11~13行,配置DMA通道为硬件触发信号的下降沿触发,下面是初始化PINTINT,使用USER_KEY产生触发信号,和对应的中断程序。代码片段5.初始化按键产生DMA硬件触发信号

 

01  #define PINTSEL0 0       // 定义引脚中断0的编号
02  #define KEY_USER P0_1    // 定义按键USER_KEY的引脚
03  
04  void PININT0_IRQHandler(void)
05  {
06      if (LPC_PIN_INT->RISE & (1<
07          LPC_PIN_INT->RISE = 1<// 清除上升沿中断标志
08      if (LPC_PIN_INT->FALL & (1<
09          LPC_PIN_INT->FALL = 1<// 清除下降沿中断标志
10  }
11  
12  void PINT_Init_Key_User()
13  {
14      LPC_GPIO_PORT->DIRCLR0 = 1 << KEY_USER;   // 配置USER_KEY对应的引脚为输入
15      LPC_SYSCON->PINTSEL[PINTSEL0] = KEY_USER; // USER_KEY对应对应到引脚中断0(PINTSEL0)
16      LPC_PIN_INT->ISEL = 0 << PINTSEL0;        // 配置引脚中断0(PINTSEL0)为边沿触发
17      LPC_PIN_INT->IENR = 1 << PINTSEL0;        // 配置引脚中断0(PINTSEL0)是上升沿触发
18      LPC_PIN_INT->IENF = 0 << PINTSEL0;        // 配置引脚中断0(PINTSEL0)不是下降沿触发
19      LPC_PIN_INT->IST = 0xFF;                  // 清除所有可能的引脚中断标志
20      NVIC_EnableIRQ(PININT0_IRQn);             // 使能引脚中断
21  }

上述代码是对PINTINT的初始化,它配置USER_KEY对应的引脚产生一个中断信号,确切地说是按键按下再抬起时的上升沿将产生中断。04行的中断处理程序中,只是简单地清除可能的上升沿或下降沿中断标志。

 

这里要澄清两个概念,一个是引脚中断的触发信号,另一个是DMA的触发信号。前者是引脚上的信号,用于产生中断;后者是芯片内部的中断标志对应的信号,用于触发DMA传输。两个信号分别有上升沿和下降沿的选项,但两者是不等价的,本例程中引脚中断选择的是上升沿,而DMA触发信号选择的是下降沿。

 

下图显示出了这两个信号之间的关系:

 

mcu                      图5.引脚中断与DMA触发信号的关系图

图中的时间点④是触发DMA传输的时间,这个时间点与代码片段507行的执行相对应。如果清除上升沿中断的动作被推迟,则DMA传输的时间也会被推迟,所以用户要尽快地响应PINTINT中断并清除中断标志,以实现快速高效传输。

 

 

下面是主函数代码,主函数中调用了前面介绍过的DMAUSARTPINTINT函数。

 

 

代码片段6.硬件触发DMA传输UART发送数据例程

 

01  const unsigned char Hello[] = "Hello DMA World!
"; // 待发送的数据串
02  void main()
03  {
04      USART0_init();      // 初始化USART0
05  
06      DMA_IntA_Flag = 0;  // 清除DMA中断标记
07  
08      DMA_UART_Send((uint8_t *)Hello, sizeof(Hello)-1); // 初始化DMA控制器
09  
10      LPC_DMATRIGMUX->DMA_ITRIG_INMUX1 = 0x05; 
11      PINT_Init_Key_User();
12  
13      NVIC_EnableIRQ(DMA_IRQn); 
14      do {
15          __WFI();
16      }
17      while (DMA_IntA_Flag == 0);
18  
19      while (1);
20  }

这个主函数与前面那个例程(见代码片段2)的主要区别,就是第1011行配置DMA触发源和对触发源(引脚中断0)的初始化。

 

 

这里需要注意的是第10行配置DMA触发源,一定要在使能DMA时钟之后执行。本例程中,DMA的时钟是在DMA_UART_Send()中设置的,见代码片段3

 

 

1.7.3 DMA执行USART0的成组发送

 

这个例程是在上一个例程的基础上,增加了使用乒乓链接的描述符,同时设置成组传输。

 

 

下图所示为本例程中乒乓链接的描述符链。链头描述符指向一个开始字符串Hello,并链接到描述符B。描述符A指向一个字符串A,描述符B指向另一个字符串B。传输执行的效果是,先发送Hello,然后循环发送SpingBàSpingAàSpingB......

 

 

mcu图6.DMA执行USART0的成组发送例程的描述符链另外,例程中安排了以成组(Burst)方式发送每个字符串,即每次触发只发送BURSTPOWER指定长度的数据,见1.5.1的说明。

 

和上节一样,例程中的触发源也是与USER_KEY相连的引脚中断0

 

本例程用到下述变量。

 

  ALIGN(512) DMA_CHDESC_T Chan_Desc_Table[2]; // 所有通道描述符链头数组
  ALIGN(16) DMA_RELOADDESC_T Descriptor_A;    // 描述符A
  ALIGN(16) DMA_RELOADDESC_T Descriptor_B;    // 描述符B
  
  uint8_t DMA_IntA_Flag;    // 中断A软件标志
  uint8_t DMA_IntB_Flag;    // 中断B软件标志
                          // 1234----1234----1234----1234----
  const uint8_t Hello[] =   ">> Hello my DMA world!
";         // 链头对应的字符串
  const uint8_t StringA[] = ">> Hello dear StringA.
 ";        // 描述符A的字符串
  const uint8_t StringB[] = "<< Hello I am saying String B.
"; // 描述符B的字符串

下面是DMA初始化的代码代码片段7.执行USART0的成组发送例程的DMA初始化代码

 

01  void DMA_UART_PingPong_Send(uint8_t *buf, uint32_t length)
02  {   uint32_t ch_cfg_val, xfercount, xfercfg; 
03  
04      LPC_SYSCON->SYSAHBCLKCTRL |= DMA;
05      LPC_DMA->CTRL = 0;
06  
07      LPC_DMA->SRAMBASE = (uint32_t)(&Chan_Desc_Table); 
08  
09      xfercount = length - 1; 
10  ch_cfg_val = 1 << DMA_CFG_PERIPHREQEN |   // 外设请求
11                   1 << DMA_CFG_HWTRIGEN |  // 硬件触发
12                   0 << DMA_CFG_TRIGTYPE |  // 边沿触发
13                   0 << DMA_CFG_TRIGPOL |   // 下降沿触发
14                   1 << DMA_CFG_TRIGBURST | // 成组传输模式
15                   3 << DMA_CFG_BURSTPOWER; // 每组为8(23)个数据
16  
17      xfercfg = 0 << DMA_XFERCFG_CFGVALID | // 暂时设置为无效
18                1 << DMA_XFERCFG_RELOAD   | // 有下一个描述符
19                0 << DMA_XFERCFG_SWTRIG   | // 没有软件触发
20                1 << DMA_XFERCFG_CLRTRIG  | // 传输结束时清除触发标志
21                1 << DMA_XFERCFG_SETINTA  | // 传输结束时设置INTA中断
22                0 << DMA_XFERCFG_SETINTB  |
23                0 << DMA_XFERCFG_WIDTH    | // 数据宽度为8位
24                1 << DMA_XFERCFG_SRCINC   | // 每次传输后源地址递增
25                0 << DMA_XFERCFG_DSTINC   | // 每次传输后目标地址不递增
26                xfercount << DMA_XFERCFG_XFERCOUNT; // 传输长度
27      LPC_DMA->CHANNEL[CH_USART0_TX].CFG = ch_cfg_val; 
28      LPC_DMA->CHANNEL[CH_USART0_TX].XFERCFG = xfercfg; 
29  
30      Chan_Desc_Table[CH_USART0_TX].source = (uint32_t)(&buf[xfercount]);
31      Chan_Desc_Table[CH_USART0_TX].dest   = (uint32_t)(&LPC_USART0->TXDAT);
32      Chan_Desc_Table[CH_USART0_TX].next   = (uint32_t)& Descriptor_B; 
33  
34      xfercount = sizeof(StringB)- 2; // 去掉字符串结尾的字符’’
35      Descriptor_B.xfercfg = 1 << DMA_XFERCFG_CFGVALID |
36                             1 << DMA_XFERCFG_RELOAD   |
37                             1 << DMA_XFERCFG_CLRTRIG  |
38                             1 << DMA_XFERCFG_SETINTB  |
39                             1 << DMA_XFERCFG_SRCINC   |
40                             xfercount << DMA_XFERCFG_XFERCOUNT;
41      Descriptor_B.source = (uint32_t)(&StringB[xfercount]);
42      Descriptor_B.dest = (uint32_t)(&LPC_USART0->TXDAT);
43      Descriptor_B.next = (uint32_t)&Descriptor_A;
44  
45      xfercount = sizeof(StringA)- 2; // 去掉字符串结尾的字符’’ 
46      Descriptor_A.xfercfg = 1 << DMA_XFERCFG_CFGVALID |
47                             1 << DMA_XFERCFG_RELOAD   |
48                             1 << DMA_XFERCFG_CLRTRIG  |
49                             1 << DMA_XFERCFG_SETINTA  |
50                             1 << DMA_XFERCFG_SRCINC   |
51                             xfercount << DMA_XFERCFG_XFERCOUNT;
52      Descriptor_A.source = (uint32_t)(&StringA[xfercount]);
53      Descriptor_A.dest = (uint32_t)(&LPC_USART0->TXDAT);
54      Descriptor_A.next = (uint32_t)&Descriptor_B;
55  
56      LPC_DMA->INTENSET0 =  1 << CH_USART0_TX; 
57      LPC_DMA->ENABLESET0 = 1 << CH_USART0_TX; 
58  
59      LPC_DMA->CTRL = 1; 
60  
61      LPC_DMA->SETVALID0 = 1 << CH_USART0_TX; 
62  }

DMA控制器中设置的两个中断INTAINTB的内部机制完全一致,分为两个中断源只是为了让用户区分对应的描述符,用户可以自由安排。本例中设置描述符A执行结束后会产生中断A,描述符B执行结束后会产生中断B,因此相比前一个例程中断处理程序也多了出来INTB的代码。

 

代码片段8.执行USART0的成组发送例程中断处理程序
01  void DMA_IRQHandler(void)
02  {
03      if (LPC_DMA->INTA0 & (1 << CH_USART0_TX)) {
04          LPC_DMA->INTA0 = 1 << CH_USART0_TX;
05          DMA_IntA_Flag++;
06      }
07      if (LPC_DMA->INTB0 & (1 << CH_USART0_TX)) {
08          LPC_DMA->INTB0 = 1 << CH_USART0_TX; 
09          DMA_IntB_Flag++;
10      }
11      if (LPC_DMA->ERRINT0 & (1 << CH_USART0_TX))
12          LPC_DMA->ERRINT0 = 1 << CH_USART0_TX;
13  }

每次执行完一个描述符后,就会相应地产生一个中断(INTAINTB),用户可以自行在代码片段8的第0509行安插代码在适当的时候结束整个描述符链的DMA传输,例如当循环次数满足一定要求时。

 

代码片段9.执行USART0的成组发送例程的主函数

 

01  void main()
02  {
03      USART0_init();      // 初始化USART0
04  
05      DMA_IntA_Flag = DMA_IntB_Flag = 0;  // 清除DMA中断标记
06  
07      DMA_UART_PingPong_Send((uint8_t *)Hello, sizeof(Hello)-1); // 初始化DMA控制器
08  
09      LPC_DMATRIGMUX->DMA_ITRIG_INMUX1 = 0x05; 
10      PINT_Init_Key_User();
11  
12      NVIC_EnableIRQ(DMA_IRQn); 
13      while (1)
14          __WFI();
15  }

主函数和前面的代码片段6基本一致。执行这个例程后,每按一次按键DMA会发送8个字符,在串口助手上有如下显示:

 

mcu

 

 

   

END

   

更多恩智浦AI-IoT市场和产品信息,邀您同时关注“NXP客栈”微信公众号

mcu      

NXP客栈


恩智浦致力于打造安全的连接和基础设施解决方案,为智慧生活保驾护航。

       

长按二维码,关注我们

恩智浦MCU加油站


这是由恩智浦官方运营的公众号,着重为您推荐恩智浦MCU的产品信息、开发技巧、教程文档、培训课程等内容。

mcu  

长按二维码,关注我们

 


原文标题:LPC800前生今世-第九章 直接存储器访问 (DMA)

文章出处:【微信公众号:恩智浦MCU加油站】欢迎添加关注!文章转载请注明出处。


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分