DMA引擎控制器文档

硬件介绍

大多数从属DMA控制器具有相同的通用操作原理。

它们具有给定数量的通道用于DMA传输,以及给定数量的请求线路。

请求和通道几乎是正交的。通道可用于服务多个或任何请求。 简单来说,通道是执行复制的实体,而请求涉及哪些端点。

请求线路实际上对应于从符合DMA条件的设备到控制器本身的物理线路。 只要设备想要启动传输,它就会通过断言该请求线路来断言DMA请求(DRQ)。

一个非常简单的DMA控制器只会考虑一个参数:传输大小。 在每个时钟周期,它会将一个字节的数据从一个缓冲区传输到另一个缓冲区,直到达到传输大小。

这在现实世界中效果不佳,因为从属设备可能需要以单个周期传输特定数量的位。 例如,我们可能希望传输与物理总线允许的数据一样多,以在执行简单的内存复制操作时最大化性能,但我们的音频设备可能具有更窄的FIFO,需要一次写入16或24位的数据。 这就是为什么大多数(如果不是全部)DMA控制器都可以使用称为传输宽度的参数来调整此参数。

此外,某些DMA控制器,只要RAM用作源或目标,就可以将内存中的读取或写入分组到缓冲区中,因此您将获得几个更大的传输,而不是大量的微小内存访问,这效率不高。 这是使用称为突发大小的参数完成的,该参数定义了允许执行多少次单独的读取/写入,而无需控制器将传输拆分为更小的子传输。

我们理论上的DMA控制器将只能执行涉及单个连续数据块的传输。 但是,我们通常拥有的一些传输不是,并且希望将数据从非连续缓冲区复制到连续缓冲区,这称为分散-聚集。

DMAEngine,至少对于mem2dev传输,需要支持分散-聚集。 因此,我们在这里剩下两种情况:要么我们有一个相当简单的DMA控制器不支持它,我们将不得不在软件中实现它,要么我们有一个更高级的DMA控制器,它在硬件中实现了分散-聚集。

后者通常使用要传输的块的集合进行编程,并且只要传输开始,控制器就会遍历该集合,执行我们在那里编程的任何操作。

此集合通常是表或链接列表。 然后,您将表的地址及其元素数量或列表的第一个项目推送到DMA控制器的一个通道,并且只要断言DRQ,它将遍历该集合以了解从何处获取数据。

无论如何,此集合的格式完全取决于您的硬件。 每个DMA控制器都需要不同的结构,但是对于每个块,所有这些都需要至少源地址和目标地址,是否应递增这些地址以及我们之前看到的三个参数:突发大小,传输宽度和传输大小。

最后一件事是,通常,从属设备默认情况下不会发出DRQ,并且只要您愿意使用DMA,就必须首先在从属设备驱动程序中启用它。

这些只是通用内存到内存(也称为mem2mem)或内存到设备(mem2dev)的传输。 大多数设备通常支持dmaengine支持的其他类型的传输或内存操作,并且将在本文档后面详细介绍。

Linux中的DMA支持

从历史上看,DMA控制器驱动程序已经使用async TX API实现,以卸载诸如内存复制,XOR,加密等操作,基本上是任何内存到内存的操作。

随着时间的推移,出现了内存到设备传输的需求,并且dmaengine得到了扩展。 如今,async TX API被编写为dmaengine之上的一个层,并充当客户端。 尽管如此,dmaengine在某些情况下会适应该API,并做出了一些设计选择,以确保它保持兼容性。

有关Async TX API的更多信息,请查看异步传输/转换 API中的相关文档文件。

DMA引擎API

struct dma_device 初始化

就像任何其他内核框架一样,整个DMAEngine注册依赖于驱动程序填写一个结构并针对该框架进行注册。 在我们的例子中,该结构是dma_device。

您需要在驱动程序中做的第一件事是分配此结构。 任何常用的内存分配器都可以,但是您还需要初始化其中的一些字段

  • channels:应使用INIT_LIST_HEAD宏等将其初始化为列表

  • src_addr_widths:应包含支持的源传输宽度的位掩码

  • dst_addr_widths:应包含支持的目标传输宽度的位掩码

  • directions:应包含支持的从属方向的位掩码(即,不包括mem2mem传输)

  • residue_granularity:报告给dma_set_residue的传输剩余粒度。 这可以是

    • 描述符:您的设备不支持任何类型的剩余报告。 该框架仅知道已完成特定的事务描述符。

    • 段:您的设备能够报告已传输的块

    • 突发:您的设备能够报告已传输的突发

  • dev:应保存指向与当前驱动程序实例关联的struct device的指针。

支持的事务类型

接下来需要做的是设置您的设备(和驱动程序)支持的事务类型。

我们的dma_device structure有一个名为cap_mask的字段,其中保存了支持的各种类型的事务,您需要使用dma_cap_set函数修改此掩码,该函数具有各种标志作为支持的事务类型的参数。

所有这些功能都在dma_transaction_type enum中定义,在include/linux/dmaengine.h

目前,可用的类型有

  • DMA_MEMCPY

    • 该设备能够执行内存到内存的复制

    • 无论源和目标组合块的总大小是多少,都只会传输与两者中最小的字节数一样多的字节。 这意味着两个列表中分散-聚集缓冲区的数量和大小不必相同,并且该操作在功能上等效于strncpy,其中count参数等于两个分散-聚集列表缓冲区的最小总大小。

    • 通常用于在主机内存和内存映射的GPU设备内存之间复制像素数据,例如在现代PCI视频图形卡上找到的数据。 最直接的例子是OpenGL API函数glReadPixels(),它可能需要将一个巨大的帧缓冲区从本地设备内存逐字复制到主机内存上。

  • DMA_XOR

    • 该设备能够对内存区域执行XOR运算

    • 用于加速XOR密集型任务,例如RAID5

  • DMA_XOR_VAL

    • 该设备能够使用XOR算法对内存缓冲区执行奇偶校验。

  • DMA_PQ

    • 该设备能够执行RAID6 P+Q计算,其中P是一个简单的XOR,Q是一个Reed-Solomon算法。

  • DMA_PQ_VAL

    • 该设备能够使用RAID6 P+Q算法对内存缓冲区执行奇偶校验。

  • DMA_MEMSET

    • 该设备能够用提供的模式填充内存

    • 该模式被视为单字节带符号值。

  • DMA_INTERRUPT

    • 该设备能够触发将生成定期中断的虚拟传输

    • 客户端驱动程序使用它来注册一个回调,该回调将通过DMA控制器中断定期调用

  • DMA_PRIVATE

    • 这些设备仅支持从属传输,因此不适用于异步传输。

  • DMA_ASYNC_TX

    • 该设备支持异步内存到内存操作,包括memcpy、memset、xor、pq、xor_val和pq_val。

    • 此功能由DMA引擎框架自动设置,设备驱动程序不得手动配置。

  • DMA_SLAVE

    • 该设备可以处理设备到内存的传输,包括分散-聚集传输。

    • 在mem2mem情况下,我们需要处理两种不同的类型来复制单个块或它们的集合,而在这里,我们只有一个应该处理两者的事务类型。

    • 如果要传输单个连续内存缓冲区,只需构建一个只有一个项目的分散列表。

  • DMA_CYCLIC

    • 该设备可以处理循环传输。

    • 循环传输是一种块集合将循环自身的传输,其中最后一个项目指向第一个项目。

    • 它通常用于音频传输,在音频传输中,您希望对一个环形缓冲区进行操作,您将用您的音频数据填充该缓冲区。

  • DMA_INTERLEAVE

    • 该设备支持交错传输。

    • 这些传输可以将数据从非连续缓冲区传输到非连续缓冲区,而不是DMA_SLAVE,DMA_SLAVE可以将数据从非连续数据集传输到连续目标缓冲区。

    • 它通常用于2D内容传输,在这种情况下,您希望将未压缩数据的一部分直接传输到显示器以进行打印

  • DMA_COMPLETION_NO_ORDER

    • 该设备不支持按顺序完成。

    • 如果设备正在设置此功能,则驱动程序应为device_tx_status返回DMA_OUT_OF_ORDER。

    • 如果设备导出此功能,则所有cookie跟踪和检查API都应视为无效。

    • 在这一点上,这与dmatest的轮询选项不兼容。

    • 如果设置了此上限,则建议用户为发送到DMA设备的每个描述符提供唯一的标识符,以便正确跟踪完成情况。

  • DMA_REPEAT

    • 该设备支持重复传输。 重复传输(由DMA_PREP_REPEAT传输标志指示)类似于循环传输,因为它在结束时会自动重复,但也可以由客户端替换。

    • 此功能仅限于交错传输,因此,如果未设置DMA_INTERLEAVE标志,则不应设置此标志。 此限制基于DMA客户端的当前需求,如果将来需要,则应添加对其他传输类型的支持。

  • DMA_LOAD_EOT

    • 该设备支持在传输结束时(EOT)通过对设置了DMA_PREP_LOAD_EOT标志的新传输进行排队来替换重复传输。

    • 在另一个点(例如突发结束而不是传输结束)替换当前正在运行的传输的支持将在以后根据DMA客户端的需求添加,如果将来需要的话。

这些各种类型也会影响源地址和目标地址随时间的变化方式。

指向RAM的地址通常在每次传输后递增(或递减)。 对于环形缓冲区,它们可能会循环(DMA_CYCLIC)。 指向设备寄存器(例如FIFO)的地址通常是固定的。

每个描述符的元数据支持

一些数据移动架构(DMA控制器和外设)使用与事务关联的元数据。 DMA控制器的作用是并行传输有效载荷和元数据。 元数据本身不被DMA引擎使用,但它包含外设或来自外设的参数,密钥,向量等。

DMAengine框架提供了一种通用的方法来促进描述符的元数据。 根据体系结构,DMA驱动程序可以实现这两种方法中的任何一种或全部,并且由客户端驱动程序选择使用哪种方法。

  • DESC_METADATA_CLIENT

    元数据缓冲区由客户端驱动程序分配/提供,并通过dmaengine_desc_attach_metadata()辅助函数附加到描述符。

    对于此模式,希望从DMA驱动程序获得以下内容

    • DMA_MEM_TO_DEV / DEV_MEM_TO_MEM

      应该准备来自所提供的元数据缓冲区的数据,以便DMA控制器与有效载荷数据一起发送。 通过复制到硬件描述符或高度耦合的数据包。

    • DMA_DEV_TO_MEM

      在传输完成时,DMA驱动程序必须将元数据复制到客户端提供的元数据缓冲区,然后再通知客户端有关完成情况。 在传输完成后,DMA驱动程序不得触摸客户端提供的元数据缓冲区。

  • DESC_METADATA_ENGINE

    元数据缓冲区由DMA驱动程序分配/管理。 客户端驱动程序可以询问元数据的指针,最大大小和当前使用的元数据大小,并且可以直接更新或读取它。 dmaengine_desc_get_metadata_ptr()和dmaengine_desc_set_metadata_len()作为辅助函数提供。

    对于此模式,希望从DMA驱动程序获得以下内容

    • get_metadata_ptr()

      应返回元数据缓冲区的指针,元数据缓冲区的最大大小以及缓冲区中当前使用的/有效的(如果有)字节数。

    • set_metadata_len()

      客户端在将元数据放置到缓冲区中后调用它,以使DMA驱动程序知道提供的有效字节数。

    注意:由于客户端将在完成回调中(在DMA_DEV_TO_MEM情况下)请求元数据指针,因此DMA驱动程序必须确保在调用回调之前不会释放描述符。

设备操作

现在我们已经描述了我们能够执行的操作,我们的dma_device结构还需要一些函数指针才能实现实际逻辑。

我们必须在那里填写的函数,因此必须实现,显然取决于您报告为支持的事务类型。

  • device_alloc_chan_resources

  • device_free_chan_resources

    • 每当驱动程序首次/最后一次在与该驱动程序关联的通道上调用dma_request_channeldma_release_channel时,将调用这些函数。

    • 它们负责分配/释放所有需要的资源,以便该通道对您的驱动程序有用。

    • 这些函数可以睡眠。

  • device_prep_dma_*

    • 这些函数与您之前注册的功能相匹配。

    • 这些函数都获取与准备传输相关的缓冲区或散列表,并且应从中创建硬件描述符或硬件描述符列表

    • 可以从中断上下文中调用这些函数

    • 您可能执行的任何分配都应使用GFP_NOWAIT标志,以便不会潜在地休眠,但也不会耗尽紧急池。

    • 驱动程序应尝试在探测时预先分配在传输设置期间可能需要的任何内存,以避免对nowait分配器施加太大的压力。

    • 它应返回dma_async_tx_descriptor structure的唯一实例,该实例进一步表示此特定传输。

    • 可以使用函数dma_async_tx_descriptor_init初始化此结构。

    • 您还需要在此结构中设置两个字段

      • flags:TODO:它可以由驱动程序本身修改,还是应该始终是参数中传递的标志

      • tx_submit:指向您必须实现的函数的指针,该函数应该将当前事务描述符推送到挂起的队列,等待调用issue_pending。

    • 在此结构中,可以初始化函数指针callback_result,以便通知提交者事务已完成。 在较早的代码中,已经使用了函数指针callback。 但是,它不提供任何事务状态,将被弃用。 作为dmaengine_result定义的result结构将传递给callback_result,该结构具有两个字段

      • result:这提供了由dmaengine_tx_result定义的传输结果。 成功或某些错误情况。

      • residue:为那些支持剩余的用户提供传输的剩余字节。

  • device_prep_peripheral_dma_vec

    • 类似于device_prep_slave_sg,但是它采用指向dma_vec结构数组的指针,该数组(从长远来看)将取代散列表。

  • device_issue_pending

    • 采用挂起队列中的第一个事务描述符,然后启动传输。 每当该传输完成时,它应该移动到列表中的下一个事务。

    • 可以在中断上下文中调用此函数

  • device_tx_status

    • 应该报告给定通道上要进行的剩余字节数

    • 应该只关心作为参数传递的事务描述符,而不是给定通道上当前活动的事务描述符

    • tx_state参数可能为NULL

    • 应该使用dma_set_residue进行报告

    • 在循环传输的情况下,它应该只考虑循环缓冲区的总大小。

    • 如果设备不支持按顺序完成并且正在按顺序完成操作,则应返回DMA_OUT_OF_ORDER。

    • 可以在中断上下文中调用此函数。

  • device_config

    • 使用作为参数给出的配置重新配置通道

    • 此命令不应同步执行,也不应在任何当前排队的传输上执行,而只能在后续传输上执行

    • 在这种情况下,该函数将接收dma_slave_config结构指针作为参数,该参数将详细说明要使用的配置。

    • 即使该结构包含方向字段,但此字段已被弃用,而赞成提供给prep_*函数的direction参数

    • 此调用仅对从属操作是强制性的。 不应为memcpy操作设置或期望设置此参数。 如果驱动程序同时支持这两种操作,则应仅将此调用用于从属操作,而不用于memcpy操作。

  • device_pause

    • 暂停通道上的传输

    • 此命令应在通道上同步操作,立即暂停给定通道的工作

  • device_resume

    • 恢复通道上的传输

    • 此命令应在通道上同步操作,立即恢复给定通道的工作

  • device_terminate_all

    • 中止通道上所有挂起和正在进行的传输

    • 对于中止的传输,不应调用完整的回调

    • 可以从原子上下文中调用,也可以从描述符的完整回调中调用。 不得睡眠。 驱动程序必须能够正确处理此问题。

    • 终止可能是异步的。 驱动程序不必等到当前活动的传输完全停止。 请参阅device_synchronize。

  • device_synchronize

    • 必须将通道的终止同步到当前上下文。

    • 必须确保DMA控制器不再访问先前提交的描述符的内存。

    • 必须确保先前提交的描述符的所有完整回调都已完成运行,并且没有计划运行的回调。

    • 可以睡眠。

其他说明

(应该记录的内容,但不知道在哪里放置它们)

dma_run_dependencies

  • 应在异步TX传输结束时调用,并且可以在从属传输情况下忽略。

  • 确保在将其标记为完成之前运行相关操作。

dma_cookie_t

  • 它是一个DMA事务ID,会随着时间的推移而递增。

  • 自从引入了抽象它的virt-dma以来,不再真正相关。

dma_vec

  • 一个包含DMA地址和长度的小结构。

DMA_CTRL_ACK

  • 如果清除,则在客户端确认收到(即有机会建立任何依赖关系链)之前,提供程序无法重用描述符

  • 可以通过调用async_tx_ack()来确认

  • 如果设置,并不意味着可以重用描述符

DMA_CTRL_REUSE

  • 如果设置,则在完成后可以重用描述符。 如果设置了此标志,则不应由提供程序释放它。

  • 应该通过调用dmaengine_desc_set_reuse()来准备要重用的描述符,这将设置DMA_CTRL_REUSE。

  • dmaengine_desc_set_reuse() 只有在通道支持可重用描述符时才会成功,这由通道的功能 (capabilities) 决定。

  • 因此,如果设备驱动程序想要在两次传输之间跳过 dma_map_sg()dma_unmap_sg(),因为 DMA 的数据没有被使用,它可以完成传输后立即重新提交传输。

  • 描述符可以通过几种方式释放:

    • 通过调用 dmaengine_desc_clear_reuse() 清除 DMA_CTRL_REUSE 并在最后一笔事务 (txn) 中提交。

    • 显式调用 dmaengine_desc_free(),只有在 DMA_CTRL_REUSE 已经设置时才会成功。

    • 终止通道。

  • DMA_PREP_CMD

    • 如果设置,客户端驱动程序告诉 DMA 控制器,DMA API 中传递的数据是命令数据。

    • 命令数据的解释是 DMA 控制器特定的。它可以用于向其他外设发送命令/寄存器读取/寄存器写入,此时描述符应该与普通数据描述符的格式不同。

  • DMA_PREP_REPEAT

    • 如果设置,传输将在结束后自动重复,直到同一通道上排队了带有 DMA_PREP_LOAD_EOT 标志的新传输。如果通道上排队的下一个传输没有设置 DMA_PREP_LOAD_EOT 标志,则当前传输将重复,直到客户端终止所有传输。

    • 只有当通道报告 DMA_REPEAT 能力时,才支持此标志。

  • DMA_PREP_LOAD_EOT

    • 如果设置,传输将在传输结束时替换当前正在执行的传输。

    • 这是非重复传输的默认行为,因此为非重复传输指定 DMA_PREP_LOAD_EOT 不会有任何区别。

    • 当使用重复传输时,DMA 客户端通常需要在所有传输上设置 DMA_PREP_LOAD_EOT 标志,否则通道将继续重复上一次重复的传输,并忽略正在排队的新传输。未能设置 DMA_PREP_LOAD_EOT 将表现为通道卡在上一个传输上。

    • 只有当通道报告 DMA_LOAD_EOT 能力时,才支持此标志。

通用设计说明

您将看到的大多数 DMAEngine 驱动程序都基于类似的设计,该设计在处理程序中处理传输结束中断,但将大部分工作推迟到 tasklet,包括启动新的传输,只要前一个传输结束。

然而,这是一个相当低效的设计,因为传输间延迟不仅是中断延迟,而且是 tasklet 的调度延迟,这将使通道在两者之间空闲,从而减慢全局传输速率。

您应该避免这种做法,并且不要在您的 tasklet 中选择新的传输,而是将该部分移动到中断处理程序中,以便拥有更短的空闲窗口(无论如何我们都无法真正避免)。

词汇表

  • Burst: 在刷新到内存之前可以排队到缓冲区的一系列连续读取或写入操作。

  • Chunk: 连续的 burst 集合

  • Transfer: chunk 的集合(无论是否连续)