考虑到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目前不支持异步时钟模式。它使用核心时钟驱动程序,只能工作在正常模式下。在睡眠模式、深度睡眠和低功耗睡眠模式下,均停止工作。
该DMA控制器通过多个通道的传输任务描述符(CTS)来管理数据传输的过程,并且还支持链接模式,将多个传输任务描述符链接在一起形成传输任务链。
DMA控制器是AHB总线主设备,但它仍然和普通外设一样,充当APB总线从设备并配置成适当的工作模式。通过DMAMUX,DMA控制器可以直接收集芯片上其他外围模块的触发信号,从而触发DMA在地址空间传输数据的过程。如图x所示。
图x DMA控制器系统框图
需要注意的是,DMA的传输过程是在地址空间内操作的。可以是从内存到内存、从外设到外设、内存和外设之间等等。对于DMA来说,只是移动数字。至于数据是否映射到物理设备、外设还是内存,都是由总线来实现的。
与某些DMA控制器将触发信号与传输数据的源地址或目标地址绑定的设计相比,YTM32 DMA控制器使触发信号和传输任务使用的地址相互独立。例如,当定时器模块产生触发信号时,触发DMA的数据传输任务(通道)。该任务可以将ADC转换结果的数据传输到存储器。其中,定时器和ADC没有直接关系。
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结构。 Body的内存被覆盖到CTS对应的寄存器中。
另外,只需使用CTS结构中的相关寄存器来配置DMA传输任务的参数即可,而不受CTS抽象数据结构的限制。
YTM32 的DMA 控制器针对每个DMA 处理任务设计了两种触发方法:软件触发和硬件触发。其中,软件触发可以由CPU直接写入DMA控制器的寄存器(DMA_CTSn_CSR[START])来主动启动传输过程;硬件触发使用预设的硬件触发信号,当外设的硬件触发信号通过DMAMUX到达时,自动启动DMA传输任务并开始移动数据。每次触发DMA 时,都会执行一个小周期传输过程。一个大周期可以包含多个小周期,因此一个大周期传输任务可能需要多个触发器才能完成。 (大循环和小循环的概念见楼下)
软件触发DMA的软件触发是通过软件写入各个通道的寄存器位DMA_CTSn_CSR[START],或者寄存器DMA_START中相应通道的控制位来实现的。每次写入时都会发出触发信号。每触发一次,执行一个小循环(一次触发循环)传输过程,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传输就有了主循环和次循环的概念。主循环包含次循环。
YTM32 手册中使用了传输循环和触发循环的名称:
传输循环是指在一个DMA 通道触发后传输数据。触发循环是指DMA通道可以接受多少个DMA通道触发(包括软件触发和硬件触发)。从手册中的描述我们可以知道,Transfer Loop描述的是一个trigger(一个触发器)的执行,包含多个传输过程,而Trigger Loop可以包含多个trigger(很多触发器),对应大循环和小循环。如图x所示。
图x DMA中移数过程中的大循环和小循环
小周期传输的字节数由每个DMA 通道的BCNT 寄存器指定。它也是一个递减计数器,每传输一个字节就减1,到0时停止传输。
大周期中包含的小周期数(不是字节数,而是触发信号的计数)由每个DMA 通道的TCNT_KDDIS[TCNT] 寄存器字段指定。它本身也是一个减量计数器。每执行一个小循环(trigger)就减1,减到0时停止。特别注意,这里的大循环只管理trigger,不传输内存块。如果使用一个传输任务描述链表来链接多个传输任务描述符,则每个任务描述符(可能处于不同的地址块和传输模式(数据传输))都对应自己的触发时间(同一通道的触发源仍然是相同)。
大周期执行到一半和完成时,都有相应的标志位(DMA_CHTLHDIF 和DMA_CHTLDIF)。这里有一个特殊的设计。只有当DMA 传输通道的大周期半完成和完全完成中断使能(DMA_CTS_CSR[THDINT]=1 且DMA_CTS_CSR[TDINT]=1)时,这两个标志位才会被置位,否则即使如果相应的事件到来。但无论相应的中断(DMA_CTS_CSR[LOOPINT]) 是否使能,其他传输完成标志位(DMA_DONE 和DMA_CTS_CSR[DONE]) 都可以被置位。这里有一点小纠结。如果DMA_CTS_CSR[LOOPINT] 和DMA_CTS_CSR[TDINT]=1 同时启动,则DMA_DONE 和DMA_CHTLDIF 对应的行为将完全相同。那么一个大周期完成后就会产生中断,服务程序需要同时清除两个标志。 (这里的设计看起来有点多余,好像少了点什么……)
DMA外设设计有非常灵活的处理地址更新策略,可以覆盖最大范围的应用场景。然而,您需要理清概念并更新时序,然后才能使用DMA。否则,如果意外产生错误的参数配置状态,DMA将停止工作并报告错误(DMA_ERS)。
我们回顾一下DMA传输中的操作单元:
具有指定带宽的总线传输称为一次传输。传输触发器可以发起一次或多次连续传输,也可以称为传输循环或传输循环。这也对应于文章中描述的小循环。一组触发器可以包含一个或多个连续的Minor Loop,也可以称为Trigger Loop 或触发器循环,对应于文章中描述的Major Loop。以数据源地址指针为例(数据目的地址指针相同):
初始数据地址存储在寄存器DMA_CSR_SADDR中。该寄存器中的值也会随着DMA传输过程的执行而改变,并且始终指向数据即将传输的地址。预配置DMA_CTS_TCNT寄存器的值大于或等于1意味着本次DMA传输任务至少包含一个触发产生的小周期。触发器将启动移动数字的过程。从小循环开始。每次总线传输传输的数据长度(宽度)由寄存器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。