STM32 DMA-MDMA 链式传输

简介

本文档描述了 STM32 DMA-MDMA 链式传输功能。但在深入探讨之前,让我们先介绍一下所涉及的外设。

为了将数据传输从 CPU 中卸载,STM32 微处理器 (MPU) 嵌入了直接内存访问控制器 (DMA)。

STM32MP1 SoC 嵌入了 STM32 DMA 和 STM32 MDMA 控制器。STM32 DMA 请求路由功能通过 DMA 请求多路复用器 (STM32 DMAMUX) 得到增强。

STM32 DMAMUX

STM32 DMAMUX 将来自给定外设的任何 DMA 请求路由到任何 STM32 DMA 控制器(STM32MP1 包含两个 STM32 DMA 控制器)的通道。

STM32 DMA

STM32 DMA 主要用于为不同的外设实现中央数据缓冲区存储(通常在系统 SRAM 中)。它可以访问外部 RAM,但无法生成方便的突发传输来确保 AXI 的最佳负载。

STM32 MDMA

STM32 MDMA(主 DMA)主要用于管理 RAM 数据缓冲区之间的直接数据传输,而无需 CPU 干预。它也可以用于分层结构中,其中 STM32 DMA 作为 AHB 外设的一级数据缓冲接口,而 STM32 MDMA 则作为具有更好性能的二级 DMA。作为 AXI/AHB 主设备,STM32 MDMA 可以控制 AXI/AHB 总线。

原理

STM32 DMA-MDMA 链式传输功能依赖于 STM32 DMA 和 STM32 MDMA 控制器的优势。

STM32 DMA 具有循环双缓冲模式 (DBM)。在每次事务结束时(当 DMA 数据计数器 - DMA_SxNDTR - 达到 0 时),存储器指针(使用 DMA_SxSM0AR 和 DMA_SxM1AR 配置)会被交换,并且 DMA 数据计数器会自动重新加载。这允许软件或 STM32 MDMA 在第二个存储区域正在被 STM32 DMA 传输填充/使用时处理一个存储区域。

在 STM32 MDMA 链接列表模式下,单个请求会启动要传输的数据数组(节点集合),直到通道的链接列表指针为空。除非第一个和最后一个节点相互链接,否则最后一个节点的通道传输完成是传输的结束,在这种情况下,链接列表会循环以创建循环 MDMA 传输。

STM32 MDMA 与 STM32 DMA 直接连接。这实现了外设之间的自主通信和同步,从而节省了 CPU 资源和总线拥塞。STM32 DMA 通道的传输完成信号可以触发 STM32 MDMA 传输。STM32 MDMA 可以通过写入其中断清除寄存器(其地址存储在 MDMA_CxMAR 中,位掩码存储在 MDMA_CxMDR 中)来清除 STM32 DMA 生成的请求。

STM32 MDMA 与 STM32 DMA 的互连表

STM32 DMAMUX 通道

STM32 DMA 通道

STM32 DMA 传输完成信号

STM32 MDMA 请求

通道 0

DMA1 通道 0

dma1_tcf0

0x00

通道 1

DMA1 通道 1

dma1_tcf1

0x01

通道 2

DMA1 通道 2

dma1_tcf2

0x02

通道 3

DMA1 通道 3

dma1_tcf3

0x03

通道 4

DMA1 通道 4

dma1_tcf4

0x04

通道 5

DMA1 通道 5

dma1_tcf5

0x05

通道 6

DMA1 通道 6

dma1_tcf6

0x06

通道 7

DMA1 通道 7

dma1_tcf7

0x07

通道 8

DMA2 通道 0

dma2_tcf0

0x08

通道 9

DMA2 通道 1

dma2_tcf1

0x09

通道 10

DMA2 通道 2

dma2_tcf2

0x0A

通道 11

DMA2 通道 3

dma2_tcf3

0x0B

通道 12

DMA2 通道 4

dma2_tcf4

0x0C

通道 13

DMA2 通道 5

dma2_tcf5

0x0D

通道 14

DMA2 通道 6

dma2_tcf6

0x0E

通道 15

DMA2 通道 7

dma2_tcf7

0x0F

STM32 DMA-MDMA 链式传输功能然后使用 SRAM 缓冲区。STM32MP1 SoC 嵌入了三个不同大小的快速访问静态内部 RAM,用于数据存储。由于 STM32 DMA 的遗留问题(在微控制器中),STM32 DMA 在 DDR 上的性能较差,而在 SRAM 上则最佳。因此,在 STM32 DMA 和 STM32 MDMA 之间使用了 SRAM 缓冲区。此缓冲区分为两个相等的时间段,STM32 DMA 使用一个时间段,而 STM32 MDMA 同时使用另一个时间段。

                dma[1:2]-tcf[0:7]
               .----------------.
 ____________ '    _________     V____________
| STM32 DMA  |    /  __|>_  \    | STM32 MDMA |
|------------|   |  /     \  |   |------------|
| DMA_SxM0AR |<=>| | SRAM  | |<=>| []-[]...[] |
| DMA_SxM1AR |   |  \_____/  |   |            |
|____________|    \___<|____/    |____________|

STM32 DMA-MDMA 链式传输使用 (struct dma_slave_config).peripheral_config 来交换配置 MDMA 所需的参数。这些参数被收集到一个包含三个值的 u32 数组中

  • STM32 MDMA 请求(实际上是 DMAMUX 通道 ID),

  • STM32 DMA 寄存器的地址,用于清除传输完成中断标志,

  • STM32 DMA 通道的传输完成中断标志的掩码。

支持 STM32 DMA-MDMA 链式传输的设备树更新

1. 分配 SRAM 缓冲区

SRAM 设备树节点在 SoC 设备树中定义。您可以在您的板设备树中引用它来定义您的 SRAM 池。

&sram {
        my_foo_device_dma_pool: dma-sram@0 {
                reg = <0x0 0x1000>;
        };
};

请注意起始索引,以防有其他 SRAM 使用者。战略性地定义您的池大小:为了优化链接,其理念是 STM32 DMA 和 STM32 MDMA 可以同时在 SRAM 的每个缓冲区上工作。如果 SRAM 周期大于预期的 DMA 传输,则 STM32 DMA 和 STM32 MDMA 将按顺序而不是同时工作。这不是功能问题,但它不是最优的。

不要忘记在您的设备节点中引用您的 SRAM 池。您需要定义一个新属性。

&my_foo_device {
        ...
        my_dma_pool = &my_foo_device_dma_pool;
};

然后在您的 foo 驱动程序中获取此 SRAM 池并分配您的 SRAM 缓冲区。

2. 分配一个 STM32 DMA 通道和一个 STM32 MDMA 通道

除了您为“经典” DMA 操作应该已经拥有的通道之外,您还需要在设备树节点中定义一个额外的通道。

这个新通道必须从 STM32 MDMA 通道中获取,因此,要使用的 DMA 控制器的 phandle 是 MDMA 控制器的 phandle。

&my_foo_device {
        [...]
        my_dma_pool = &my_foo_device_dma_pool;
        dmas = <&dmamux1 ...>,                // STM32 DMA channel
               <&mdma1 0 0x3 0x1200000a 0 0>; // + STM32 MDMA channel
};

关于 STM32 MDMA 绑定

1. 请求行号:无论这里的值是什么,它都会被 MDMA 驱动程序通过 (struct dma_slave_config).peripheral_config 传递的 STM32 DMAMUX 通道 ID 覆盖

2. 优先级:选择非常高 (0x3),以便您的通道在请求仲裁期间优先于其他通道

3. 指定 DMA 通道配置的 32 位掩码:源地址和目标地址递增,每个单次传输的块传输大小为 128 字节

4. 指定用于确认请求的寄存器的 32 位值:它将被 MDMA 驱动程序覆盖,使用通过 (struct dma_slave_config).peripheral_config 传递的 DMA 通道中断标志清除寄存器地址

5. 指定要写入以确认请求的值的 32 位掩码:它将被 MDMA 驱动程序覆盖,使用通过 (struct dma_slave_config).peripheral_config 传递的 DMA 通道传输完成标志

foo 驱动程序中支持 STM32 DMA-MDMA 链式传输的驱动程序更新

0. (可选)如果使用 dmaengine_prep_slave_sg(),则重构原始 sg_table

在使用 dmaengine_prep_slave_sg() 的情况下,原始 sg_table 不能按原样使用。必须从原始 sg_table 创建两个新的 sg_table。一个用于 STM32 DMA 传输(其中存储器地址现在指向 SRAM 缓冲区而不是 DDR 缓冲区),另一个用于 STM32 MDMA 传输(其中存储器地址指向 DDR 缓冲区)。

新的 sg_list 项必须适合 SRAM 周期长度。以下是 DMA_DEV_TO_MEM 的示例

/*
  * Assuming sgl and nents, respectively the initial scatterlist and its
  * length.
  * Assuming sram_dma_buf and sram_period, respectively the memory
  * allocated from the pool for DMA usage, and the length of the period,
  * which is half of the sram_buf size.
  */
struct sg_table new_dma_sgt, new_mdma_sgt;
struct scatterlist *s, *_sgl;
dma_addr_t ddr_dma_buf;
u32 new_nents = 0, len;
int i;

/* Count the number of entries needed */
for_each_sg(sgl, s, nents, i)
        if (sg_dma_len(s) > sram_period)
                new_nents += DIV_ROUND_UP(sg_dma_len(s), sram_period);
        else
                new_nents++;

/* Create sg table for STM32 DMA channel */
ret = sg_alloc_table(&new_dma_sgt, new_nents, GFP_ATOMIC);
if (ret)
        dev_err(dev, "DMA sg table alloc failed\n");

for_each_sg(new_dma_sgt.sgl, s, new_dma_sgt.nents, i) {
        _sgl = sgl;
        sg_dma_len(s) = min(sg_dma_len(_sgl), sram_period);
        /* Targets the beginning = first half of the sram_buf */
        s->dma_address = sram_buf;
        /*
          * Targets the second half of the sram_buf
          * for odd indexes of the item of the sg_list
          */
        if (i & 1)
                s->dma_address += sram_period;
}

/* Create sg table for STM32 MDMA channel */
ret = sg_alloc_table(&new_mdma_sgt, new_nents, GFP_ATOMIC);
if (ret)
        dev_err(dev, "MDMA sg_table alloc failed\n");

_sgl = sgl;
len = sg_dma_len(sgl);
ddr_dma_buf = sg_dma_address(sgl);
for_each_sg(mdma_sgt.sgl, s, mdma_sgt.nents, i) {
        size_t bytes = min_t(size_t, len, sram_period);

        sg_dma_len(s) = bytes;
        sg_dma_address(s) = ddr_dma_buf;
        len -= bytes;

        if (!len && sg_next(_sgl)) {
                _sgl = sg_next(_sgl);
                len = sg_dma_len(_sgl);
                ddr_dma_buf = sg_dma_address(_sgl);
        } else {
                ddr_dma_buf += bytes;
        }
}

在通过 dmaengine_prep_slave_sg() 获取描述符后,不要忘记释放这些新的 sg_table。

1. 设置控制器特定参数

首先,使用带有 struct dma_slave_config 的 dmaengine_slave_config() 来配置 STM32 DMA 通道。您只需注意 DMA 地址,存储器地址(取决于传输方向)必须指向您的 SRAM 缓冲区,并设置 (struct dma_slave_config).peripheral_size != 0。

STM32 DMA 驱动程序将检查 (struct dma_slave_config).peripheral_size 以确定是否正在使用链式传输。如果使用,则 STM32 DMA 驱动程序会使用三个 u32 的数组填充 (struct dma_slave_config).peripheral_config:第一个包含 STM32 DMAMUX 通道 ID,第二个包含通道中断标志清除寄存器地址,第三个包含通道传输完成标志掩码。

然后,使用 `dmaengine_slave_config` 和另一个 `struct dma_slave_config` 结构体来配置 STM32 MDMA 通道。注意 DMA 地址,设备地址(取决于传输方向)必须指向您的 SRAM 缓冲区,而内存地址必须指向最初用于 “经典” DMA 操作的缓冲区。使用先前由 STM32 DMA 驱动程序更新的 `(struct dma_slave_config).peripheral_size` 和 `.peripheral_config`,来设置用于配置 STM32 MDMA 通道的 `struct dma_slave_config` 的 `.peripheral_size` 和 `.peripheral_config`。

struct dma_slave_config dma_conf;
struct dma_slave_config mdma_conf;

memset(&dma_conf, 0, sizeof(dma_conf));
[...]
config.direction = DMA_DEV_TO_MEM;
config.dst_addr = sram_dma_buf;        // SRAM buffer
config.peripheral_size = 1;            // peripheral_size != 0 => chaining

dmaengine_slave_config(dma_chan, &dma_config);

memset(&mdma_conf, 0, sizeof(mdma_conf));
config.direction = DMA_DEV_TO_MEM;
mdma_conf.src_addr = sram_dma_buf;     // SRAM buffer
mdma_conf.dst_addr = rx_dma_buf;       // original memory buffer
mdma_conf.peripheral_size = dma_conf.peripheral_size;       // <- dma_conf
mdma_conf.peripheral_config = dma_config.peripheral_config; // <- dma_conf

dmaengine_slave_config(mdma_chan, &mdma_conf);

2. 获取 STM32 DMA 通道事务的描述符

以与获取 “经典” DMA 操作描述符相同的方式,您只需将原始的 sg_list(在 `dmaengine_prep_slave_sg()` 的情况下)替换为使用 SRAM 缓冲区的新 sg_list,或者将原始缓冲区地址、长度和周期(在 `dmaengine_prep_dma_cyclic()` 的情况下)替换为新的 SRAM 缓冲区。

3. 获取 STM32 MDMA 通道事务的描述符

如果您之前使用以下方式获取了(STM32 DMA 的)描述符:

  • `dmaengine_prep_slave_sg()`,则对 STM32 MDMA 使用 `dmaengine_prep_slave_sg()`;

  • `dmaengine_prep_dma_cyclic()`,则对 STM32 MDMA 使用 `dmaengine_prep_dma_cyclic()`。

使用新的 sg_list(在 `dmaengine_prep_slave_sg()` 的情况下)或,根据传输方向,使用原始 DDR 缓冲区(在 `DMA_DEV_TO_MEM` 的情况下)或 SRAM 缓冲区(在 `DMA_MEM_TO_DEV` 的情况下),源地址之前已使用 `dmaengine_slave_config()` 设置。

4. 提交两个事务

在提交事务之前,您可能需要定义在哪个描述符上希望在传输结束时(`dmaengine_prep_slave_sg()`)或在周期结束时(`dmaengine_prep_dma_cyclic()`)调用回调函数。根据传输方向,在完成整个传输的描述符上设置回调函数。

  • DMA_DEV_TO_MEM:在 “MDMA” 描述符上设置回调函数

  • DMA_MEM_TO_DEV:在 “DMA” 描述符上设置回调函数

然后,使用 `dmaengine_tx_submit()` 提交描述符,无论顺序如何。

5. 发出挂起的请求(并等待回调通知)

由于 STM32 MDMA 通道传输由 STM32 DMA 触发,您必须先发出 STM32 MDMA 通道,然后再发出 STM32 DMA 通道。

如果有任何回调函数,它将被调用以警告您整个传输或周期完成。

不要忘记终止两个通道。STM32 DMA 通道配置为循环双缓冲模式,因此它不会被硬件禁用,您需要终止它。在 sg 传输的情况下,STM32 MDMA 通道将被硬件停止,但在循环传输的情况下则不会。无论哪种传输类型,您都可以终止它。

STM32 DMA-MDMA 链式 DMA_MEM_TO_DEV 特殊情况

DMA_MEM_TO_DEV 中的 STM32 DMA-MDMA 链式是一种特殊情况。实际上,STM32 MDMA 使用 DDR 数据填充 SRAM 缓冲区,而 STM32 DMA 从 SRAM 缓冲区读取数据。因此,当 STM32 DMA 开始读取时,一些数据(第一个周期)必须复制到 SRAM 缓冲区中。

一种技巧可能是暂停 STM32 DMA 通道(这将引发传输完成信号,触发 STM32 MDMA 通道),但 STM32 DMA 读取的第一个数据可能是“错误的”。正确的方法是使用 `dmaengine_prep_dma_memcpy()` 准备第一个 SRAM 周期。然后,应该从 sg 或循环传输中“删除”此第一个周期。

由于这种复杂性,最好将 STM32 DMA-MDMA 链式用于 DMA_DEV_TO_MEM,并为 DMA_MEM_TO_DEV 保留“经典” DMA 用法,除非您不害怕复杂性。

资源

应用笔记、数据手册和参考手册可在 ST 网站上找到 (STM32MP1)。

重点关注三份应用笔记 (AN5224AN4031 & AN5001),它们分别处理 STM32 DMAMUX、STM32 DMA 和 STM32 MDMA。

作者: