概述

通用直接内存访问(General Direct Memory Access,GDMA)控制器,也称 DMAC,用于在内存与外设之间传输数据。搬运过程中无需 CPU 介入, 降低了 CPU 的工作负载。GDMA 架构示意图如下:

../../../rst_peripherals/rtos/8_dmac/figures/dmac_arch.svg

GDMA 支持以下特性:

  • 支持多达 8 个独立通道,通道的源端口和目的端口均可自由分配给 Memory 或者支持 GDMA 功能的外设

  • 支持通道优先级配置

  • 支持多种传输方向

    • 外设到内存

    • 外设到外设

    • 内存到外设

    • 内存到内存

  • 每个通道均内置独立的 FIFO,其中各通道 FIFO 大小不同,可满足不同应用场景

  • 可以灵活设定流控(Flow Control),由用户决定是由源外设、目的外设还是 DMAC 控制传输请求和节奏

  • 支持单次和突发(burst)传输模式

  • 支持单个数据块和多个数据块传输

  • 支持安全传输模式

  • 支持低功耗

  • 支持数据传输过程中暂停、恢复和关闭传输通道

GDMA 配置

DMA 一般配置流程如下:

  1. 初始化时钟:打开 DMA 控制器(DMAC)时钟

  2. 配置相关 DMA 通道

  1. 申请一个空闲的 DMA 通道

  2. 根据应用需求,配置通道控制参数

  1. 传输方向和流控:如内存到外设、外设到内存等,以及由源或目的外设、或 DMAC 控制传输请求和节奏

  2. 源端口和目的端口地址

  3. 传输宽度(1/2/4 Bytes)

  4. 传输模式(1/4/8/16)

  5. 传输块大小(Block Size)

  6. 优先级设置

  1. 使能通道中断(如需传输完成/异常通知)

  2. 启动数据传输:打开通道使能位,启动 DMA 通道,外设/DMAC 发起 DMA 请求,启动数据搬运过程

  3. 结束与后处理

    1. DMA 传输完成后,产生中断

    2. 在中断服务函数中处理完成信号、释放资源等

DMA 参数示意图如下所示:

../../../rst_peripherals/rtos/8_dmac/figures/dmac_block_size_diagram.svg

通道申请和释放

DMA 通过以下两个 API 实现通道的申请和释放:

  • GDMA_ChnlAlloc() :按顺序从通道 0 开始分配

  • GDMA_ChnlFree() :按照指定的通道编号释放通道

通道申请过程中,可能出现两个 CPU 同时申请一个通道,这会导致程序运行异常。为解决该问题,在通道申请过程中,会使用硬件 SEMA 来 保护。

传输方向和流控制器

目前共有四种传输方向和两种流控制器设置,共八种可用配置。

  • 当外设作为流控制器时,DMA 将根据外设发出的单次/突发请求进行数据传输。

  • 当 DMAC 作为流控制器时,所有来自外设的请求都将按照配置的请求进行处理。

CTLx寄存器的TT_FC[2:0]字段(x为通道编号)

方向

流控控制器

000

内存 到 内存

DMAC

001

内存 到 外设

DMAC

010

外设 到 内存

DMAC

011

外设 到 外设

DMAC

100

外设 到 内存

外设

101

外设 到 外设

源外设

110

内存 到 外设

外设

111

外设 到 外设

目的外设

流控制器配置原则:

  • 如果 block_ts 已知,使用 DMAC 作为流控制器。例如如播放音乐、图片等,以及搬运内存数据。

  • 如果 block_ts 未知,使用外设作为流控制器。例如 UART 接受不定长数据,可以让 UART 做流控制器,每当有数据来临再请求传输。

警告

  • 仅当 DMAC 用作流控制器时,才能设置 block_ts 参数。

  • 使用外设作为流控制器,需要确保 IP 在硬件设计上支持触发 DMA 请求。详情可参考 握手 章节。

数据块大小

上图阐述了 GDMA 传输数据大小的设置。block_ts 表示将在单个数据块中传输的数据量,需要设置为 总数据量/SRC_TR_WIDTH,最大值是 {{IC_PARAM_GDMA_BLOCK_SIZE}}

事务模式和宽度

GDMA 传输时,单次传输事务(Transaction)的大小可配置:

  • msize > 1:突发传输

  • msize = 1:单次传输

CTLx 寄存器的 SRC_MSIZE[2:0]/DEST_MSIZE[2:0] 字段

传输 msize

000

1

001

4

010

8

011

16

100 及以上

不支持

GDMA 支持以下传输宽度:

CTLx 寄存器的 SRC_TR_WIDTH[2:0]/DST_TR_WIDTH[2:0] 字段

传输宽度(字节)

000

1

001

2

010

4

011 及以上

不支持

  • 当 DMAC 作为流控制器,单个数据块传输到最后,剩余的数据量不足以完成一次 Msize * Width 传输,但满足一次 1 * Width 传输,DMAC 将发起单次传输请求,完成传输。

  • 当外设作为流控制器,外设决定发出单次传输还是突发传输请求。

备注

  • 读写外设时: SRC_TR_WIDTH/DST_TR_WIDTH 根据外设的数据宽度决定。

  • 读写内存时:

    • 如果缓存被禁用,内存地址无需对齐,但需确保总数据量能被 SRC_TR_WIDTH 整除,以保证 block_ts 为整数。

    • 如果缓存被启用,内存地址必须满足缓冲区边界对齐,且需按缓存行对齐。

  • 当源或者目的是内存时(如 P2M、M2M 模式): 内存对应的 DST_TR_WIDTH`或者 `SRC_TR_WIDTH 参数将被忽略,实际写入/读取操作始终基于总线宽度(默认为 32 位,即 4 字节)。

  • 为保证 FIFO 不会发生 undef flow 或者 overflow, SRC_MSIZE * SRC_TR_WIDTHDST_MSIZE * DST_TR_WIDTH 需要保持相等。

传输类型

GDMA 支持以下传输类型:

  • Single Block: 仅包含一个数据块

  • Multi-Block:包含多个数据块

    • 自动重加载(Auto-reloading)模式

    • 链表(Linked-list)模式

各个模式使用场景及特点如下:

GDMA 各模式特点

模式

子模式

使用场景

特点

Single Block

地址空间连续且仅传输一次

  • 传输完成后dma立即停止

Multi-Block

auto-reload

地址空间连续且源端或者目的端需要重复加载某一个数据块

  • 若开启块中断,每个块传输完成都会暂停,直到中断处理结束

link-list

地址空间非连续

  • 若开启块中断,每个块传输完成后触发中断,但将立即传输下一个块

  • 该模式下,即便进入中断也不会阻碍DMA数据传输

continuous

单个地址空间连续的数据块

  • 始终保持地址递增模式,若源和目的端均为continues模式,则传输与 single block相同

Auto-reloading 模式

在 auto-reloading 模式下,源和目标可分别选择使用的方法。

Auto-reloading 传输类型

设置

说明

Src auto reload

PGDMA_InitTypeDef->GDMA_ReloadSrc = 1

PGDMA_InitTypeDef->GDMA_ReloadDst = 0

对多块传输(multi-block transfers),每块结束时SAR寄存器会从初始值自动重载,目标地址连续,如

Multi-Block 传输-源地址自动重载&目标地址连续 所示。

Dst auto reload

PGDMA_InitTypeDef->GDMA_ReloadSrc = 0

PGDMA_InitTypeDef->GDMA_ReloadDst = 1

对多块传输,每块结束时DAR寄存器会从初始值自动重载,源地址连续。

Src & Dst auto reload

PGDMA_InitTypeDef->GDMA_ReloadSrc = 1

PGDMA_InitTypeDef->GDMA_ReloadDst = 1

对多块传输,每块结束时SAR和DAR寄存器都会从初始值自动重载,如 Multi-Block 传输-源和目标地址均自动重载 所示。

../../../rst_peripherals/rtos/8_dmac/figures/mbd_source_auto_dest_cont.png

Multi-Block 传输-源地址自动重载&目标地址连续

../../../rst_peripherals/rtos/8_dmac/figures/mbd_source_dest_auto.png

Multi-Block 传输-源和目标地址均自动重载

Linked list 模式

在 linked list(链表)模式下,数据块之间的地址无需连续。

Link list 传输类型

设置

说明

Src: Continue address

Dst: Link list

PGDMA_InitTypeDef->GDMA_SrcAddr = pSrc

PGDMA_InitTypeDef->GDMA_LlpDstEn = 1

源内存为连续数据块,目标数据块以链表方式组织。

Src: Auto-reloading

Dst: Link list

PGDMA_InitTypeDef->GDMA_ReloadSrc = 1

PGDMA_InitTypeDef->GDMA_SrcAddr = pSrc

PGDMA_InitTypeDef->GDMA_LlpDstEn = 1

源端,SAR寄存器可在每块结束时由初始值自动重载,目标数据块为链表结构,见 Multi-block 传输-源地址自动重载&目标地址为链表结构

Src: Link list

Dst: Continue address

PGDMA_InitTypeDef->GDMA_LlpSrcEn = 1

PGDMA_InitTypeDef->GDMA_DstAddr = pDst

源数据块以链表结构组织,目标内存为连续数据块,见 Multi-block 传输-源地址为链表结构&目标地址连续

Src: Link list

Dst: Auto-reloading

PGDMA_InitTypeDef->GDMA_LlpSrcEn = 1

PGDMA_InitTypeDef->GDMA_DstAddr = pDst

PGDMA_InitTypeDef->GDMA_ReloadDst = 1

源数据块为链表结构,目标数据块为自动重载。

Src: Link list

Dst: Link list

PGDMA_InitTypeDef->GDMA_LlpSrcEn = 1

PGDMA_InitTypeDef->GDMA_LlpDstEn = 1

源和目标数据块均以链表结构组织,见 Multi-block 传输-源和目标地址均为链表结构

如果目标和源都是连续的数据块,不建议使用多块传输,应采用单块传输模式。

地址递增类型

源地址递增

包含两种模式:

  • 递增(Increment):表示在每次源端传输时,源地址是否递增。递增操作用于对齐到下一个 CTLx.SRC_TR_WIDTH 边界。

  • 不变(No change):如果设备从具有固定地址的源外设 FIFO 读取数据,则应将该字段设置为“不变”。

目标地址递增

包含两种模式:

  • 递增(Increment):表示在每次目的端传输时,目的地址是否递增。递增操作用于对齐到下一个 CTLx.DST_TR_WIDTH 边界。

  • 不变(No change):如果设备将数据写入到具有固定地址的目的外设 FIFO,则应将该字段设置为“不变”。

配置原则:

  • 如果源端或者目的端为 Memory,地址模式一般设置为 Increment

  • 如果源端或者目的端为外设,地址模式一般设置为 No Change

FIFO

GDMA 的每个通道都有自己独立的 FIFO,各个通道的 FIFO 大小并不相同。

FIFO 大小

通道编号

CH0

CH1

CH2~CH7

FIFO size/Bytes

128

128

32

中断类型

DMAC 支持多种中断类型,这些中断可独立使用或组合使用。

中断类型

说明

块中断

单个数据块传输完成触发

传输中断

所有数据块传输完毕时产生

错误中断

传输过程中发生错误

备注

  • 多块传输时,若 auto-reload 模式中的块传输被中断,数据将在中断处理函数完成后继续传输。

  • inked list 模式的传输完成条件为:最后一个数据块的下一数据块指针为空。

  • linked list 模式下,即使产生块中断,数据传输仍会继续进行。

挂起和终止

GDMA 支持通道的挂起恢复和强制终止。

  • 挂起通道:直接配置 CFGx.CH_SUSP,但不保证当前数据传输已完成。需结合 CFGx.INACTIVE 状态检测,方可安全暂停且不丢失数据。

  • 恢复传输:清除 CFGx.CH_SUSP 即可继续进行数据传输。

  • 终止传输:需持续轮询 CFGx.INACTIVE 直至该位设为 1,方可终止传输。

备注

通道进入非活跃状态的场景:

  • CFGx.INACTIVE 仅可在内存写入操作完成后被激活,且需手动取消。

  • 外设数据为 4 字节,但 DMAC FIFO 仅 2 字节。若当前无写入操作,则直接激活 CFGx.INACTIVE

聚集和分散

聚集

聚集传输是将一片内存区域中等间隔的多段数据拷贝到一段连续内存中。示例如下:

  • SRC_TR_WIDTH 为 4Bytes

  • Source Gather Interval(SGI) 为 1

  • Source Gather Count(SGC) 为 4

即源端每传输 16Bytes 的数据,然后跳过 4Bytes 的地址区间,最终数据将连续存放到目的端。

../../../rst_peripherals/rtos/8_dmac/figures/dmac_source_gather.svg

分散

分散传输:将一片连续内存数据搬运到一片不连续(等间隔)的的内存空间。示例如下:

  • DST_TR_WIDTH 为 4Bytes

  • Destination Scatter Interval(DSI) 为 16

  • Destination Scatter Count(DSC) 为 4

即源端连续发送数据,目的端每接受 16Bytes 的数据,然后跳过 64Bytes.

../../../rst_peripherals/rtos/8_dmac/figures/dmac_destination_scatter.svg

警告

  • 当使用 Source Gather 功能,将源内存 memory 收集到目的内存,则 block_ts 要和有效数据量保持一致,且必须和 SRC_TR_WIDTH 对齐。

  • 当使用 Destination Scatter 功能,将源内存数据分散到目的内存,则 block_ts 要和源地址空间大小保持一致,且必须和 DST_TR_WIDTH 对齐。

优先级

DMAC 支持两种通道优先级:

  • 软件:各通道优先级可通过 CFGx.CH_PRIOR 配置,有效值范围为 0 ~ (DMAC_NUM_CHANNELS-1)。其中, DMAC_NUM_CHANNELS 为最低优先级。

  • 硬件:若两个通道请求的软件优先级相同,或未配置软件优先级,通道编号较小的通道优先级更高(例如:通道 2 优先级高于通道 4)。

握手

GDMA 仅支持硬件握手,不支持软件握手。仅在与外设进行数据传输时,才需配置握手接口。所有硬件握手接口在 IC 设计阶段已固定,用户无法自行更改。 当前 IC 支持的硬件握手接口及其对应 IP 如下表所示:

GDMA 握手接口

功能

握手编号

说明

UART0 TX

0

UART0 RX

1

UART1 TX

2

UART1 RX

3

UART2 TX

4

UART2 RX

5

SPI0 TX

6

SPI0 RX

7

SPI1 TX

8

SPI1 RX

9

SPIC TX

10

SPIC RX

11

SPORT0 TX

12

两个FIFO,占用编号12和13

SPORT0 RX

14

两个FIFO,占用编号14和15

SPORT1 TX

16

两个FIFO,占用编号16和17

SPORT1 RX

18

两个FIFO,占用编号18和19

LEDC_TX

20

I2C0 TX

21

I2C0 RX

22

I2C1 TX

23

I2C1 RX

24

实时状态获取

GDMA 支持实时获取当前传输的源地址、目标地址和已传输数据量。

需调用对应 API 读取。

备注

若要获取已传输数据量,block_ts 必须至少大于 768,且不可在中断函数中读取;否则,获取的值始终为 0。

安全机制

默认情况下,GDMA 的安全传输功能处于关闭状态。当用户需要使用该功能时,需首先开启 Trustzone 功能

GDMA 支持针对每个通道独立配置安全传输功能。启用该功能后,DMAC 将通过 AXI 主机接口发起 Secure 访问请求。此时,GDMA 可以在安全和非安全的外设(或内存)之间传递数据。

  • 安全通道仅能在安全世界中配置,且安全通道可访问安全外设(内存)和非安全外设(内存);

  • 非安全通道仅能访问非安全外设(内存)。

要启用某通道的安全传输特性,可在 Secure 代码中配置 GDMA 参数时,设置如下结构体成员:

PGDMA_InitTypeDef->SecureTransfer = 1;

DMA 和缓存

当使用 DMA 在内存之间或者内存和外设之间传输数据,如果此时 Cache 也正常启用,需要注意 Cache 和内存数据不一致的问题。

DMA TX

当 DMA 的源端是 Memory,需要发送数据时,一般流程如下:

  1. 申请发送缓冲区,需要确保起始地址和大小都与 Cache Line 对齐。

  2. CPU 将数据写入 Memory 缓冲区。

  3. 调用 Dcache_Clean() 函数清理数据缓存

  4. 配置 DMA 发送参数

  5. DMA 启动传送

DMA RX

  1. CPU 分配接收缓冲区

  2. 执行 DCache_Clean() 确保接收缓冲区处于 clean 状态(如果接收缓冲区处于 clean 状态,可跳过此步骤)

    小心

    此步骤执行的原因是:

    • 对于 Cortex-A32,如果 Cache 中的接收缓冲区处于 dirty 状态,执行第 5 步 DCache_Invalidate() 将同时执行 cleaninvalidate 操作,可能会导致意外写入行为。

    • 如果 Cache 中的接收缓冲区处于 dirty 状态,当 CPU 的 D-Cache 满了,CPU 可能会将接收缓冲区中的脏数据写回内存,覆盖 DMA 已经写入的内容。

  3. 配置 DMA Rx 参数

  4. DMA Rx 中断处理

  5. 执行 DCache_Invalidate() 将 Cache 数据写回内存,确保 Cache 没有残留旧的接收缓冲区数据。

小心

此步骤必须执行,原因如下:

  • 对于具有自动数据预取功能的 CPU(如 Cortex-A32 和 DSP),当 CPU 读取接收缓冲区相邻地址的内容时,CPU 会在后台执行行填充操作,自动将接收缓冲区的旧值重新加载到 Cache 中。

  • 防止 CPU 在 DMA 处理期间将旧值读入 Cache。

  1. CPU 读取接收缓冲区(DMA Rx 返回的值)

备注

将缓冲区地址与 Cache Line 行对齐将减少 Cache 与内存数据不一致的问题。

DMAC 示例

单块

  1. 分配一个空闲通道

    ch_num = GDMA_ChnlAlloc(gdma.index, (IRQ_FUN) Dma_memcpy_int, (u32)(&gdma), 3);
    

    这个函数同时包含以下操作:

    • 注册 IRQ 处理程序(如果使用中断模式)

    • 启用 NVIC 中断

    • 注册要使用的 GDMA 通道

  2. 配置中断类型

    PGDMA_InitTypeDef->GDMA_IsrType = (TransferType | ErrType);
    
  3. 配置中断处理函数

    在中断处理函数中清除挂起的中断。

    GDMA_ClearINT(0, PGDMA_InitTypeDef->GDMA_ChNum);
    
  4. 传输配置

    PGDMA_InitTypeDef->GDMA_SrcMsize   = MsizeEight;
    PGDMA_InitTypeDef->GDMA_SrcDataWidth = TrWidthFourBytes;
    PGDMA_InitTypeDef->GDMA_DstMsize = MsizeEight;
    PGDMA_InitTypeDef->GDMA_DstDataWidth = TrWidthFourBytes;
    PGDMA_InitTypeDef->GDMA_BlockSize = DMA_CPY_LEN >> 2;
    PGDMA_InitTypeDef->GDMA_DstInc = IncType; // if dst type is peripheral:no change
    PGDMA_InitTypeDef->GDMA_SrcInc = IncType; // if src type is peripheral:no change
    
  5. 配置硬件握手接口(如果从机是外设)

    GDMA_InitStruct->GDMA_SrcHandshakeInterface= GDMA_HANDSHAKE_INTERFACE_AUDIO_RX;
    

    GDMA_InitStruct->GDMA_DstHandshakeInterface = GDMA_HANDSHAKE_INTERFACE_AUDIO_TX;
    
  6. 配置传输地址

    PGDMA_InitTypeDef->GDMA_SrcAddr = (u32)BDSrcTest;
    PGDMA_InitTypeDef->GDMA_DstAddr = (u32)BDDstTest;
    
  7. 使用 GDMA_Init() 函数设置 GDMA 索引、GDMA 通道、数据宽度、msize、传输方向、地址递增模式、硬件握手接口、重载控制、中断类型、块大小、多块配置,以及源地址和目标地址

    GDMA_Init(gdma.index, gdma.ch_num, PGDMA_InitTypeDef);
    
  8. 清理缓存

    DCache_CleanInvalidate();
    
  9. 启用 GDMA 通道

GDMA_Cmd(gdma.index, gdma.ch_num, ENABLE);

多块

这是 SRC 自动重载模式的示例。与单块相比,多块的 步骤 2 ~ 步骤 4 不同。

  1. 分配一个空闲通道

    ch_num = GDMA_ChnlAlloc(gdma.index, (IRQ_FUN) Dma_memcpy_int, (u32)(&gdma), 3);
    

    这个函数同时包含以下操作:

    • 注册 IRQ 处理程序(如果使用中断模式)

    • 启用 NVIC 中断

    • 注册要使用的 GDMA 通道

  2. 配置中断类型

    PGDMA_InitTypeDef->GDMA_IsrType = (BlockType | TransferType | ErrType);
    
  3. 配置中断处理函数

    1. 清除中断

      GDMA_ClearINT(0, GDMA_InitStruct->GDMA_ChNum);
      
    2. 在最后一个数据块开始之前清除自动重载模式

      GDMA_ChCleanAutoReload(0, GDMA_InitStruct->GDMA_ChNum, CLEAN_RELOAD_SRC);
      
  4. 传输配置

    PGDMA_InitTypeDef->GDMA_SrcMsize   = MsizeEight;
    PGDMA_InitTypeDef->GDMA_SrcDataWidth = TrWidthFourBytes;
    PGDMA_InitTypeDef->GDMA_DstMsize = MsizeEight;
    PGDMA_InitTypeDef->GDMA_DstDataWidth = TrWidthFourBytes;
    PGDMA_InitTypeDef->GDMA_BlockSize = DMA_CPY_LEN >> 2;
    PGDMA_InitTypeDef->GDMA_DstInc = IncType; // If DST type is peripheral: no change
    PGDMA_InitTypeDef->GDMA_SrcInc = IncType; // If SRC type is peripheral: no change
    PGDMA_InitTypeDef->GDMA_ReloadSrc = 1;
    PGDMA_InitTypeDef->GDMA_ReloadDst = 0;
    
  5. 配置硬件握手接口(如果从机是外设)

    GDMA_InitStruct->GDMA_SrcHandshakeInterface= GDMA_HANDSHAKE_INTERFACE_AUDIO_RX;
    

    GDMA_InitStruct->GDMA_DstHandshakeInterface = GDMA_HANDSHAKE_INTERFACE_AUDIO_TX;
    
  6. 配置传输地址

    PGDMA_InitTypeDef->GDMA_SrcAddr = (u32)BDSrcTest;
    PGDMA_InitTypeDef->GDMA_DstAddr = (u32)BDDstTest;
    
  7. 使用 GDMA_Init() 函数设置 GDMA 索引、GDMA 通道、数据宽度、msize、传输方向、地址递增模式、硬件握手接口、重载控制、中断类型、块大小、多块配置,以及源地址和目标地址

    GDMA_Init(gdma.index, gdma.ch_num, PGDMA_InitTypeDef);
    
  8. 清理缓存

    DCache_CleanInvalidate();
    
  9. 启用 GDMA 通道

    GDMA_Cmd(gdma.index, gdma.ch_num, ENABLE);