DMA是直接存储器访问(DirectMemory Access)的缩写。在MCU芯片中,DMA是除CPU之外,最常见的总线主设备。作为总线主设备,DMA控制器可以输出地址和控制信号到总线上,主动地发起和控制数据传输过程,它能够按照程序的配置,在两个从设备之间传输数据。例如在存储器和I2C模块之间传输数据,实现I2C数据的发送或接收,或从ADC读出数据再传送到USART进行发送。
下图是LPC82x的部分框图,图中醒目标出了总线主设备,和DMA控制器。 图1.LPC82x结构框图(部分)在LPC82x的所有片内外设中,只有DMA控制器是总线主设备,其它都是总线从设备,只有主设备才能主动发起数据的传输操作。
DMA的优势是,可以在CPU最少的干预下,高效地执行数据块的传输,节省CPU的时间,同时可以在CPU执行内部操作而不访问总线时,更高效地利用总线的时间。
1.1 DMA控制器的一些基本操作在介绍LPC800的DMA控制器之前,先通过这个传输示意图,回顾一下通用DMA控制器必须具备的基本操作。
图2.DMA传输示意图
■产生数据传输的源地址和目标地址:DMA控制器在接到传输请求后,在内部总线上产生源数据地址SA,读出要传输的数据,然后再产生存放数据的目标地址DA,将数据写入指定的地方。
■控制每次传输后地址的变化:可以控制每次DMA传输是涉及到一个连续的地址区域还是单个独立的地址。每次读写的源地址和目标地址分别改变或不改变,也可以同步地改变。
■控制传输的数据长度:软件需要指定每次DMA传输的数据数量n。
■指定传输的数据宽度:软件需要指定每次DMA读写的数据宽度,一般是以内部数据总线的宽度为限。对于32位MCU,可以是1个字节、2个字节(半字)或4个字节(字)。
■控制传输数据的节奏,即传输数据的时机:每次DMA读写都需要在有传输请求时才会执行。传输请求可以来自于数据源设备,例如ADC转换结束;传输请求也可以来自于数据目标设备,例如SPI的发送就绪。因此每两次传输请求的间隔可以不一致。DMA的传输请求也可以由DMA控制器内部产生,用于内存中数据块的传送。
■状态查询和中断控制:DMA控制器的状态和中断可以是多种多样,通常有传输开始、传输结束、传输错误等。
LPC800的DMA控制器实现了上述所有的基本控制功能,而且还有不少自己的特色,下面一一介绍。
1.2 DMA传输与CPU指令的执行不管是CPU还是DMA控制器,都要通过同一条总线访问存储器和各种片内外设,进行数据传输。CPU的基本操作就是取指、译码、运算、执行的过程,取指操作需要占用总线,执行阶段的读数据或写数据操作也需要占用总线。DMA控制器可以充分地利用CPU不占用总线的时间,在总线上传输数据。
如果在同一个时间,CPU和DMA控制器都需要占用总线,这种情况下需要有仲裁机制,协调两个总线主设备的动作。
如果在总线已经被某个主设备(例如CPU)占用的时候,另一个主设备(例如DMA控制器)就会稍作等待,待总线空闲时,再开始数据传输。
从以上描述可以看出,DMA可以在不需CPU干预的情况下,利用CPU不占用总线的空闲时间进行数据传输。这样不但提高了总线的利用率,还减轻了CPU搬运数据的负担,提高了系统的并行性,能够实现更复杂的控制要求,或降低整体的功耗。
1.3 LPC800的DMA控制器LPC800的DMA控制器具有如下特性:▲多个通道,每个通道唯一地连接到一个片内外设的输入或输出请求,例如USART、SPI和I2C等通信外设。▲DMA传输可以由片内或片外事件触发,每个DMA通道都可以有多个触发输入源,每次传输只能选择一个触发源。▲可以指定每个DMA通道的优先级,当通道之间的传输需求发生冲突时,高优先级通道先进行传输。▲传输描述符机制,通过多个传输描述符互联,可以实现链式的DMA传输控制。▲每次(每个传输描述符)最多可以传输1024个字(1024x4=4096字节)。▲地址增量的多种选项,允许灵活的数据包处理。LPC800各系列的DMA配置如下:
系列
|
通道数目
|
触发输入源数目
|
LPC80x、LPC81x
|
0
|
0
|
LPC82x
|
18
|
9
|
LPC83x
|
18
|
8
|
LPC84x
|
25
|
13
|
DMA控制器有15个寄存器,涉及到所有通道,可以分为四组。
寄存器组
|
寄存器名称
|
功能
|
说明
|
通用寄存器组
|
CTL
|
DMA控制器寄存器
|
只有一个控制位,使能DMA控制器
|
INTSTAT
|
中断状态寄存器
|
标志是否有挂起的中断
|
|
SRAMBASE
|
传输描述符地址寄存器
|
所有通道第一个传输描述符的存放地址(必须512字节对齐)
|
|
通道控制寄存器组
|
ENABLESET0
|
通道使能寄存器
|
每个通道占用一位。表示是否使能对应通道
|
ENABLECLR0
|
通道失能寄存器
|
每个通道占用一位。表示是否失能对应通道
|
|
ACTIVE0
|
通道激活状态寄存器
|
每个通道占用一位。表示对应通道是否加载了传输描述符
|
|
BUSY0
|
通道忙状态寄存器
|
每个通道占用一位。表示对应通道是否正在搬运数据
|
|
通道中断寄存器组
|
ERRINT0
|
错误中断状态寄存器
|
每个通道占用一位。表示是否有错误中断
|
INTENSET0
|
中断使能寄存器
|
每个通道占用一位。表示是否使能对应通道的中断
|
|
INTENCLR0
|
中断失能寄存器
|
每个通道占用一位。表示是否失能对应通道的中断
|
|
INTA0
|
中断A状态寄存器
|
每个通道占用一位。表示是否有中断A
|
|
INTB0
|
中断B状态寄存器
|
每个通道占用一位。表示是否有中断B
|
|
传输控制寄存器
|
SETVALID0
|
设置“有效”控制位寄存器
|
每个通道占用一位。用于设置描述符的“有效”控制位
|
SETTRIG0
|
设置“触发”控制位寄存器
|
每个通道占用一位。用于触发对应的通道传输
|
|
ABORT0
|
通道中止传输寄存器
|
每个通道占用一位。用于中止对应的通道传输
|
寄存器名称
|
功能
|
说明
|
CFG
|
通道配置寄存器
|
用于配置通道的使能、触发、成组传输(Burst)和优先级的选项
|
CTLSTAT
|
通道控制状态寄存器
|
用于标示通道的有效和触发状态
|
XFERCFG
|
通道传输配置寄存器
|
用于配置通道的各个配置选项
|
除了上述寄存器外,LPC800的DMA控制器通过位于内存中的传输描述符,控制每次的DMA传输。每个传输描述符有4个字(32位/字),内容如下:多个传输描述符可以构成一个连续的链条或循环链,链条中的每一个描述符对应一次DMA传输。传输的数据数量、数据宽度以及地址变化的方式等,由XFERCFG寄存器的内容指定。
每次DMA传输开始前,DMA控制器都要把一个完整的描述符读入,传输描述符中偏移地址为0x0的字会被传送到XFERCFG寄存器中,用于控制各项传输参数。一个链条的第一次传输参数,需要由软件直接写入到XFERCFG寄存器,因此链条中的第一个描述符的第一个字为保留位。在传输开始时,DMA控制器会把传输区的地址读入内部寄存器中。
使用描述符的链接特性,可以方便地实现多种数据传输控制。
例如需要使用SPI驱动一个LCD屏幕产生动画效果时,可以配置为下图所示的乒乓结构的DMA描述符链条,CPU只需要不断地生成显示图片,由DMA控制器平行地进行图片数据至LCD屏幕的传送。
图3.乒乓结构的DMA描述符链
这里使用了三个传输描述符。第一个描述符(链头)指示将缓冲区A的数据传输到SPI的发送寄存器,后面两个描述符分别指示缓冲区B和缓冲区A的数据,轮流传输到SPI的发送寄存器。CPU只需要在对应的缓冲区准备好数据,再设置对应的描述符为“有效”,接下来DMA控制器就会直接把数据传送到SPI模块进行发送。
1.3.3 DMA传输通道每个通信外设的发送传输可以产生DMA请求,接收传输也可以产生DMA请求。
LPC800的DMA控制器非常简单,每个片内外设产生的DMA传输请求信号,唯一地连接到一个固定的DMA通道。即如果把片内外设作为DMA传输的源或目标,并且希望由该外设来控制传输的节奏(通过DMA传输请求信号),则必须使用对应的通道。如果能够使用其它的方法(例如时钟触发等),保证外设不会发生数据溢出的情况,则可以使用任意通道,但这种用法不能最优地利用带宽时间,除非需要特殊的时序控制,一般不建议使用。
每个通道对应的DMA请求源如下表所示:
表1.DMA通道与DMA请求源的对应表
1.3.4 DMA触发源的选择
在DMA控制器之外,有一个DMA触发输入 (DMA TRIGMUX)模块,每个DMA通道都在这个模块中有一个对应的多选一选择器,由DMA_ITRIG_INMUXn寄存器控制(n对应表1的通道号),用户可以在多种信号中选择一个作为DMA的触发信号。下表列出了所有可能的选项: 表2.DMA触发选项(1):在LPC82x/83x中,n取值0~17;在LPC84x中,n取值0~24。
注*:每一个DMA通道都输出一个触发信号,所有通道输出的触发信号都连接到两个多选一的选择器:DMA_INMUX_INMUX0和DMA_INMUX_INMUX1,这两个多选一选择器的输出可以作为另一个DMA通道的触发源选项之一。这个配置允许多个DMA通道的协同操作。
图4.DMA触发输入框图
上图为某个DMA通道的触发输入模块框图,每个通道都有一个这样相同的电路用于选择它的触发输入。 1.4 DMA传输的请求、触发与生成传输概念每个通道的DMA触发信号相当于这个通道的总开关,只有打开这个总开关,才能够进行随后的DMA传输操作。每次DMA传输(即图2示意中的每一次读写)的时机,则由DMA请求信号决定。
成组传输(Burst)是指在一次触发之后,按照指定的次数进行一组数据的传输,这组数据传输完成后,需要另一次的触发条件才能进行下一组的数据传输。
成组传输控制和DMA传输请求控制组合为四种操作模式,他们与触发信号的关系如下表所示。
表中的DMA传输请求信号以USART的TXRDY为例:
组合模式
|
使能成组传输(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适合于常用的通信模块的数据发送和接收。
组合模式2、3则视具体的应用情况,由用户自由发挥。例如,要在USART上发送若干个固定长度的数据包,而发送每个数据包的时间需要由定时器来决定,则可以使用上述的组合模式3,设置BURSTPOWER为数据包的长度,设置SCT定时器产生触发信号(见表2的SCT_DMA0/1)。
拿自动步枪做一个形象的比喻,成组的概念相当于子弹夹,外设请求相当于扳机,触发相当于击发保险,一次DMA传输中需要打出一箱子弹。那么每种组合模式有如下对应:■组合模式0:打开击发保险后,不需其它动作,整箱子弹即全部射出。■组合模式1:打开击发保险后,每扣动一次扳机,射出一发子弹,直到打完整箱子弹。■组合模式2:打开击发保险后,不需其它动作,一个弹夹中的子弹即全部射出。然后再次打开击发保险,即射出另一个弹夹中的全部子弹。重复上述操作直到整箱子弹打光。■组合模式3:打开击发保险后,每扣动一次扳机,射出一个弹夹中的一颗子弹,重复直到这个弹夹中的子弹打光,击发保险自动关闭。然后须再次打开击发保险,再一次次地扣动扳机,逐个射出另一个弹夹中的所有子弹,击发保险再次自动关闭。重复上述过程直到打完整箱子弹。
这里有一个要求,即XFERCOUNT必须能被BURSTPOWER整除,即一箱子弹的数目,必须是一个弹夹能容纳子弹个数的倍数。
1.5 DMA通道参数寄存器
在1.3.1节中列出的寄存器,用于控制整个DMA控制器,以及控制每个通道的使能、中断和触发等状态。每个通道的具体工作模式,由下述三个寄存器来描述。
1.5.1 DMA通道配置寄存器(CFG)
各个控制域的说明如下:▲PERIPHREQEN:使能外设请求。0 – 使能外设请求;1 – 不使能外设请求。
每个通道的外设请求来源是固定的,见表1。例如对应SPI0的发送就绪信号(TXRDY)的DMA通道,在LPC82x/83x中是通道7,在LPC84x中是通道11。▲HWTRIGEN:使能硬件触发。硬件触发信号源由DMA_ITRIG_INMUXn寄存器选择,见1.3.4节。0 – 使能硬件触发;1 – 不使能硬件触发。▲TRIGPOL:触发极性。▲TRIGTYPE:触发类型。TRIGPOL和TRIGTYPE共同决定如何使用触发信号,组合关系如下表。
TRIGTYPE
|
TRIGPOL
|
说明
|
0
|
0
|
下降沿触发
|
0
|
1
|
上升沿触发
|
1
|
0
|
低电平触发
|
1
|
1
|
高电平触发
|
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)
该寄存器的内容给出了当前DMA传输的各项参数。一个传输描述符指定的一次传输结束后,DMA控制器会自动地读入链条中的下一个描述符,描述符的第一个字的内容会加载到XFERCFG寄存器,见1.3.2节的说明。
XFERCFG各个控制域的说明如下:▲CFGVALID:表示所对应的描述符是否有效。0 – 描述符无效;1 – 描述符有效。▲RELOAD:当前描述符所指定的传输完成后,是否需要读入下一个描述符。0 – 不读入下一个描述符;1 – 读入下一个描述符,允许描述符的链接操作。▲SWTRIG:软件触发。0 – 需要由HWTRIGEN、TRIGPOL和TRIGTYPE指定通道的触发条件。
1 – 设置此位表示该通道的触发条件立即满足。
注意:使用软件触发时,在TRIGBURST=0时,不得使用电平触发。▲CLRTRIG:当前描述符的传输结束后,是否清除触发条件。0 – 不清除。如果RELOAD=1,则下一个描述符的触发条件满足。
1 – 清除。当前描述符指示的传输结束后,清除触发条件。
注意:只有软件触发条件和边沿触发条件可以被清除,而电平触发条件不能被清除。▲SETINTA:当前描述符的传输结束后,是否产生中断标志INTA。0 – 不产生中断标志;1 – 产生中断标志。▲SETINTB:当前描述符的传输结束后,是否产生中断标志INTB。0 – 不产生中断标志;1 – 产生中断标志。
INTA和INTB在硬件上没有差别,用户可以用这两个中断(标志)区别是哪个描述符的传输完成了,尤其是在乒乓结构的传输中。▲WIDTH:表示每次DMA传输的数据宽度。源地址的读和目标地址的写,使用相同的数据宽度。0 – 8位数据传输;1 – 16位数据传输;2 – 32位数据传输;3 – 保留组合,不得使用。
注意:如果要求的数据宽度是16位或32位,则传输的地址也必须分别是2字节或4字节对齐的。▲SRCINC:表示每次传输一个数据后,源地址的增量变化。0 – 地址无变化。
1 – 地址按数据宽度+1,指向数据区中的下一个数据。
2 – 地址按数据宽度+2。
3 – 地址按数据宽度+4。▲DSTINC:表示每次传输一个数据后,目标地址的增量变化。该域的取值含义与SRCINC一样。
SRCINC和DSTINC取值为0,最常见的应用场景是面对外设寄存器的读写,例如传送一个数据块至SPI0的TXDAT,或从USART的RXDAT读出一组数据至存储区。
SRCINC和DSTINC取值为1,最常见的应用场景对一个连续的存储区的读写。
SRCINC和DSTINC取值为2或3时,一个应用案例是,当WIDTH=0(8位数据)时,希望传输一组数据字或半字中的某个字节,而不管其它字节。▲XFERCOUNT:传输的数据总数,寄存器中填入(总数-1)。该域有10位,即最大传输数据数目为1024。
传输的字节总数为:(XFERCOUNT + 1) *WIDTH。
注意1:在DMA传输过程中,DMA控制器会递减该数值,因此不能在传输过程中或传输结束后,读出该域而得知预设的传输数目。
注意2:如果设置了TRIGBURST =1,则XFERCOUNT必须是BURSTPOWER的倍数。
1.5.3 DMA通道控制和状态寄存器(CTLSTAT)这个寄存器只有两个标志位,用户可以检查这些标志位,获知当前DMA控制器的部分运行状态。▲VALIDPENDING:延迟的有效位。见0的说明。▲TRIG:触发标志。该位表示是否有触发条件。设置触发条件有多种途径:■通道传输配置寄存器(XFERCFG)的SWTRIG控制位。■设置触发控制寄存器(SETTRIG0) ,该寄存器可以同时设置一个或多个通道的触发条件。■DMA触发输入模块选定的硬件触发信号,满足TRIGPOL和TRIGTYPE时。清除触发条件也有多种途径:■当CLRTRIG=1时,描述符指定的传输结束时,清除触发条件。
■当失能DMA控制器时,见CTRL寄存器。
1.6 描述符有效位的延迟设置机制通道传输配置寄存器(XFERCFG)的CFGVALID位,指定该描述符是否有效。当一个有效的描述符被读入DMA控制器后,当CTLSTAT寄存器的TRIG标志被设置后,DMA传输就会立即开始,这是最理想的情况。通常的情况是,当准备好一个描述符A,尤其是使用描述符链时,描述符A所对应的存储区的数据可能还没有准备好,循环的乒乓结构就是一个很好的例子。这种情况下,就需要先设置描述符A的CFGVALID=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;
|
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;
|
使用DMA的最简单应用就是在内存中拷贝一个数据块,这是一个非常有效率的搬移数据块的方法,尤其是数据量比较大时,CPU可以同时执行更多的操作。DMA拷贝数据块不涉及到任何外设请求,使用软件触发,所以也不涉及到任何硬件的触发。
本例程先用随机数初始化数组Buffer1[ ],然后用DMA从Buffer1[ ]传送数据至Buffer2[ ]。数组定义如下:
下面是初始化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发送就绪
|
|
......
|
|
......
|
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的初始化函数,这个函数与前面的数据块搬运初始化基本一致,指示CFG和XFERCFG寄存器的内容有所变化。
代码片段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触发信号选择的是下降沿。
下图显示出了这两个信号之间的关系:
图5.引脚中断与DMA触发信号的关系图
图中的时间点④是触发DMA传输的时间,这个时间点与代码片段5第07行的执行相对应。如果清除上升沿中断的动作被推迟,则DMA传输的时间也会被推迟,所以用户要尽快地响应PINTINT中断并清除中断标志,以实现快速高效传输。
下面是主函数代码,主函数中调用了前面介绍过的DMA、USART和PINTINT函数。
代码片段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)的主要区别,就是第10、11行配置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......。
图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控制器中设置的两个中断INTA和INTB的内部机制完全一致,分为两个中断源只是为了让用户区分对应的描述符,用户可以自由安排。本例中设置描述符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 }
每次执行完一个描述符后,就会相应地产生一个中断(INTA或INTB),用户可以自行在代码片段8的第05和09行安插代码在适当的时候结束整个描述符链的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个字符,在串口助手上有如下显示:
END
更多恩智浦AI-IoT市场和产品信息,邀您同时关注“NXP客栈”微信公众号
NXP客栈
恩智浦致力于打造安全的连接和基础设施解决方案,为智慧生活保驾护航。
长按二维码,关注我们
恩智浦MCU加油站
这是由恩智浦官方运营的公众号,着重为您推荐恩智浦MCU的产品信息、开发技巧、教程文档、培训课程等内容。
长按二维码,关注我们
原文标题:LPC800前生今世-第九章 直接存储器访问 (DMA)
文章出处:【微信公众号:恩智浦MCU加油站】欢迎添加关注!文章转载请注明出处。
全部0条评论
快来发表一下你的评论吧 !