引言
考虑到DMA是一个AHB Master设备,可以同处理器内核一样,主动向总线发起传输请求,将“小脏手”伸向各总线上挂载的各外设模块,因此,我更愿意把DMA看做是处理器核心服务的一部分,甚至把它当成一个小核或者协处理器都不为过。
DMA可以在CPU之外,捕获到触发信号后,自行搬运数据从指定地址到另一个指定地址,并且还可以根据预先的配置自动计算下一次搬运的地址。使用DMA可以有效地节约CPU处理海量数据传输的负载。可以想见,如果使用中断方式处理通过外设发送或者接收数据,CPU将会在频繁切换中断服务之间花费大量的时间。
另外,DMA从DMAMUX获取的触发源,能够实现的自动读写的操作,若是某些读操作或者写操作能够产生额外的触发事件,还可以传递触发,形成触发链,最终可实现一些完全不需要硬件干预的自动化任务。总之,DMA真心是一个功能丰富的模块。
本文将介绍YTM32平台上DMA的工作机制,对关键概念展开讲解。
简介
YTM32(以YTM32B1ME05为例)微控制器上集成的DMA控制器可以支持16个通道,并且搭配了一个多达128个选项的DMA MUX模块,可以对接任何一个通道。其中多个通道是复用同一个DMA控制器的,并且共用同一个数据搬运引擎。DMA MUX管理了可以触发DMA通道的硬件事件,每款芯片可能都不一样(YTM32B1MD14中的DMAMUX只有64个选项),在具体使用时,需要从具体的芯片手册中查表。
还需要特别注意的是,目前YTM32平台上的DMA尚未支持异步时钟模式,它使用core clock驱动,仅能在普通模式下工作,在休眠模式、深度休眠以及更低功耗的休眠模式下,均停止工作。
本DMA控制器通过多个通道的传输任务描述符(CTS)管理搬运数据的过程,并且还支持链接模式,即将多个传输任务描述符连链接在一起,形成传输任务链。
原理与机制
DMA控制器是一个AHB总线主机,但仍同普通的外设一样,作为一个APB总线从机,被配置成合适的工作模式。DMA控制器通过DMAMUX,可以直接收集来自片上其他外设模块发出的触发信号,进而触发DMA在地址空间搬运数据的过程。如图x所示。
图x DMA控制器的系统框图
需要注意的是,DMA的搬运过程是在地址空间内操作的,可以是从内存到内存,从外设到外设,在内存与外设之间等,对于DMA而言,只是搬数,至于数据映射到物理设备是外设还是内存,均由总线负责落实。
相对于有的DMA控制器将触发信号和搬运数据源头地址或目标地址绑定的设计,YTM32的DMA控制器将触发信号和搬运任务所使用的地址相互独立,例如,当某个定时器模块产生的触发信号触发了DMA的一个搬运数据的任务(通道),这个任务可以将ADC转换结果的数据搬运到内存中。这其中,定时器和ADC是没有直接关联的。
DMA通道的传输任务描述符
YTM32的DMA的各个通道,可以看做是各自独立的传输任务,每个任务都有自己的触发条件、对触发条件的响应方式、搬运数据的源地址和目的地址、搬运数据的带宽、搬运数据的数量、每次搬运完成后对搬运过程进行调整的策略等,如此看来,DMA控制器就是这些独立任务的调度器,当多个任务被同时触发时,以一定的调度策略安排他们依次运行。
DMA通道对应的这些独立的搬运任务,在DMA引擎的建模中,被称为CTS(DMA Channel Transfer Structure),其结构如图x所示。
图x DMA通道的传输任务描述符
这个结构的内容并不是以指针的方式存放在SRAM中,而是直接做在寄存器结构里,可以在DMA的寄存器清单中找到与之一一对映的寄存器。如图x所示。
图x CTS对应的每个通道各自下辖的一组寄存器
但CTS也可以存放在RAM中,若配置了DMA_CTS_CSR[RLDEN]=1,则在大循环完成后,直接从寄存器DMA_CTS_DTO(原来存放的是地址偏移量)存放的指针进行索引,搬运整个CTS结构体的内存覆写到CTS对应的寄存器中。
除此之外,只是借鉴CTS结构中相关的寄存器,去配置DMA传输任务的参数即可,不必受限于CTS的抽象数据结构。
DMA的触发信号
YTM32的DMA控制器为每个DMA搬运任务设计了两种触发方式:软件触发和硬件触发。其中,软件触发可由CPU直接向DMA控制器的寄存器写数(DMA_CTSn_CSR[START]),主动启动传输过程;硬件触发使用预设的硬件触发信号,当来自外设的硬件触发信号通过DMAMUX到来之时,自动启动DMA搬运任务开始搬数。DMA的每次触发,执行一次小循环的搬运过程,一个大循环可以包含多个小循环的,因此一个大循环的搬运任务可能会需要多次触发才能完成。(关于大小循环的概念,可见楼下)
软件触发
DMA的软件触发是通过软件写各通道的寄存器位DMA_CTSn_CSR[START],或者寄存器DMA_START中对应通道的控制位实现的,每写1次就发出一个触发信号。每次触发,执行一次小循环(one trigger loop)的搬运过程,TCNT寄存器中的计数器减1。
特别注意,软件触发是直接作用于DMA控制器的,不必配置DMAMUX的那个always_on的选项。但由此也可知,哪怕有可用的硬件触发通过DMAMUX输入到DMA控制器,软件触发也可以生效。相当于是,软件触发和DMAMUX导入的硬件触发信号相或,然后统一输入到DMA控制器。
硬件触发
DMA的硬件触发信号来自于DMAMUX,而DMAMUX则可以从众多触发信号的源中选择其中一个适用于某个指定的通道(寄存器DMA_CHMUXn)。具体选项可在芯片手册中查阅,如图x所示。
图x 从手册中查阅DMAMUX选项
DMAMUX选中的触发信号,还需要经过一个REQEN的门控开关(寄存器DMA_REQEN中对应通道的控制位),才能顺利进入DMA引擎。因此,每次使用DMA开始传输之前,如果要使用外部的硬件触发源,必须确保打开这个门控开关。另外,每个DMA通道的传输描述符中的寄存器位DMA_CTS_CSR[DREQ]=1还可以控制在每个大循环传输完成之后,自动关闭这个门控开关。如果DMA_CTS_CSR[DREQ]=0,则这个门控开关在大循环传输完毕后仍会保持打开。
这里提到的硬件触发信号,是直接来自于外设的DMA触发信号,通常会伴随着这些外设的某些事件的发生,大多同时也可以触发中断。以LINFlexD为例,有对应的DMA触发信号的开关,如图x所示。
图x SPI外设模块的使能DMA请求控制位
DMA的大循环和小循环
一个最完整的DMA传输,可以包含多次触发,而每次触发,会引起连续地搬运一块数据(可以是连续的多个字节)。以此,完整的DMA搬运有大循环(Major Loop)和小循环(Minor Loop)的概念,大循环包含小循环。
YTM32的手册中使用了Transfer Loop和Trigger Loop的名字:
Transfer loop means data transferred after one DMA channel trigger.
Trigger loop means DMA channel could accept how many DMA channel triggers(include software and hardware trigger).
从手册的描述中可以获知,Transfer Loop描述的是一次触发(one trigger)执行的包含若干个transfer的搬运过程,而Trigger Loop可以包含多个触发(many triggers),对应大循环和小循环。如图x所示。
图x DMA搬数过程中的大循环和小循环
小循环搬运的字节数,由各DMA通道的BCNT寄存器指定,它本身也是一个递减计数器,每传输一个字节就减1,减到0时就停止搬运。
大循环的包含的小循环的次数(不是字节数,是对触发信号的计数),由各DMA通道的TCNT_KDDIS[TCNT]寄存器字段指定,它本身也是一个递减计数器,每执行一次小循环(触发)就减1,减到0时就停止。特别注意,此处的大循环管理的仅仅是触发,而不是传输内存块,如果使用多个传输任务描述符链接起来的传输任务描述链表,则每个任务描述符(可能在不同的地址块和传输模式搬运数据)都对应属于各自的触发次数(同一个通道的触发源仍为同一个)。
大循环执行一半和完毕时都有对应的标志位(DMA_CHTLHDIF和DMA_CHTLDIF),这里有个特别的设计,只有启用DMA传输通道的大循环半完成和全完成的中断时(DMA_CTS_CSR[THDINT]=1和DMA_CTS_CSR[TDINT]=1),这两个标志位才会置位,否则哪怕对应的事件到来,也不会被置位。但另一个传输完成标志位(DMA_DONE和DMA_CTS_CSR[DONE]),无论是否开启对应的中断(DMA_CTS_CSR[LOOPINT]),都能置位。这里就有一点小纠结了,如果同时启动了DMA_CTS_CSR[LOOPINT]和DMA_CTS_CSR[TDINT]=1,DMA_DONE和DMA_CHTLDIF所对应的行为将完全一样,那么在一个大循环完成后产生中断,其中的服务程序就需要同时清零这两个标志位。(这里的设计似乎有点冗余,有似乎缺了点什么。。。)
DMA搬运任务的地址更新策略
DMA外设设计了非常灵活的搬运地址更新策略,可以覆盖最大范围的应用场景。但需要整理清楚其中的概念和更新时机,才能玩转DMA,否则,一不小心产生了错误的参数配置状态,DMA也将会停止工作并报错(DMA_ERS)。
重申一次DMA搬数中的操作单元:
一次指定带宽的总线传输被称为一个搬运Transfer
一次触发可以发起一个或多个连续的Transfer,也可被称为Transfer Loop或者a loop of transfers,这也对应文中描述的小循环Minor Loop。
一组触发可以包含一个或者多个连续的Minor Loop,也可被称为Trigger Loop 或者a loop of triggers,这对应文中描述的大循环 Major Loop。
以数据源地址指针为例(数据目标地址指针相同):
最初的数据地址存放在寄存器DMA_CSR_SADDR中。这个寄存器中的值也会随着DMA搬运过程的执行变化,始终指向即将要搬运数据的地址。
预先配置DMA_CTS_TCNT寄存器的值大于等于1,表示本次DMA传输任务至少包含1次触发产生的小循环。
一次触发将启动搬数过程。先从小循环走起。
每次总线传输搬运的数据长度(宽度),由寄存器DMA_CSR_TCR[SSIZE]配置,可以选择1 Byte、2 Byte、4 Byte,以及16 Byte和32 Byte,这也代表了DMA使用数据总线的数据带宽。每次搬运都是从当前的数据地址开始搬运带宽指定数量的字节数。搬运过后,不对当前搬运数据地址产生影响,指针保持不变。
每次搬运执行后,可以由软件指定一个地址偏移量,由寄存器DMA_CSR_SOFF配置,可以是正整数,也可以是负整数(地址向前跳)。这个偏移量是作用于当前搬运数据地址指针寄存器DMA_CSR_SADDR的,当搬运执行后,当前地址指针将会加上这个偏移量,更新成新的地址指针。
如果有多次传输,则多次传输会连续执行。DMA_CTS_BCNT寄存器预置了本次小循环的需要数据的总长度(以字节为单位),而不是地址范围(切记,地址有可能是不连续地跳跃)。DMA控制器内部会自动递减DMA_CTS_BCNT寄存器的数,但不会覆写到DMA_CTS_BCNT寄存器中,因为整个小循环的搬运过程是连续执行的,用户看不到中间状态。
当小循环完成后,可以有一个对源数据地址指针的偏移。然而,这里并没有设计。
DMA_CTS_TCNT寄存器的值减1,并覆写到DMA_CTS_TCNT寄存器中。如果值仍大于0,则说明当前的大循环任务还没执行完,继续等下一个触发启动一次小循环。如果值被减到0,说明大循环任务完成,此时,DMA_DONE和DMA_CHTLDIF(若开放中断)都会置位,同时,DMA控制器还会更新两个计数值:
源数据指针在加上了最后一次搬运的偏移量(由寄存器DMA_CSR_SOFF配置)之后,还会立即继续叠加一个大循环完成地址偏移,由寄存器DMA_CSR_STO配置,这也是一个可正可负的整数。计算的结果会覆写到寄存器DMA_CSR_SADDR中。
将DMA_CTS_TCNTRV寄存器中预存的重载值覆写到DMA_CTS_TCNT寄存器中,以便于启动下次任务时无需重新配置这些计数器和地址指针。
说起来,个人觉得,如果设计小循环结束后有一个地址偏移,比实现大循环结束后的地址偏移更加直观一些。大循环专门管理触发(管理触发的递减和循环),小循环管理指针(地址的递减和递增等),分工相对更明确些。这里实现的大循环的一次性偏移,也可以等价实现为等分到每次小循环之后的地址偏移。
读者可以自行进行实验,观察DMA寄存器中各计数器的变化。
图x 利用Keil的寄存器调试界面调试
DMA控制器还支持Scatter Gather模式,将多个DMA传输任务串联在一起,可以实现地址不规则的连续传输。在地址增长模式上,还有个回环递增的模式可以用。可以在具体用到的时候再深究。手册上的描述比较简略,届时仍需要用户发挥主观能动性,大胆猜想多做尝试。
应用要点(软件)
YTMicro SDK提供了dma外设的驱动程序,以及配合其他外设使用的样例工程,例如adc_dma、spi_slave_dma、spi_master_dma、sent_dma等。
在开源的arm-mcu-sdk仓库中,也收纳了ytm_dma驱动程序,以及对应的样例工程dma_basic。