DMA引擎控制器文档
硬件介绍
大多数从DMA控制器都具有相同的操作原则。
它们具有一定数量的通道用于DMA传输,并具有一定数量的请求线。
请求和通道基本上是正交的。通道可以用于为任何请求提供服务。简单来说,通道是将执行复制操作的实体,而请求是涉及的端点。
请求线实际上对应于从DMA合格设备到控制器本身的物理线路。每当设备希望启动传输时,它将通过断言请求线来断言DMA请求(DRQ)。
一个非常简单的DMA控制器只会考虑一个参数:传输大小。在每个时钟周期,它将从一个缓冲区传输一个数据字节到另一个,直到达到传输大小。
在现实世界中,这种方式效果不佳,因为从设备可能需要在单个周期内传输特定数量的位。例如,我们可能希望尽可能多地传输数据,以最大化在执行简单内存复制操作时的性能,但我们的音频设备可能具有需要精确写入16或24位数据的较窄FIFO。这就是为什么大多数,如果不是所有的DMA控制器都可以调整这一点,使用一个称为传输宽度的参数。
此外,一些DMA控制器在RAM用作源或目的地时,可以将内存中的读取或写入分组到一个缓冲区中,因此不是进行许多小内存访问,这并不是非常有效,你将获得几个更大的传输。这是使用称为突发大小的参数来完成的,它定义了在控制器不将传输拆分为较小的子传输之前允许执行多少单个读取/写入。
我们的理论DMA控制器将只能执行涉及单个连续数据块的传输。然而,我们通常进行的一些传输并非如此,希望将数据从非连续的缓冲区复制到连续的缓冲区,这称为分散-聚集。
DMA引擎,至少对于内存到设备的传输,需要支持分散-聚集。因此,我们在这里有两种情况:要么我们有一个相当简单的DMA控制器,不支持它,我们将不得不在软件中实现它,要么我们有一个更先进的DMA控制器,在硬件中实现分散-聚集。
后者通常是使用一组要传输的块进行编程的,每当传输开始时,控制器将遍历该集合,执行我们在那里编程的操作。
这个集合通常是一个表或一个链表。然后,你将将表的地址和其元素数量,或者链表的第一项推送到DMA控制器的一个通道,并且每当DRQ被断言时,它将遍历集合以知道从哪里获取数据。
无论如何,这个集合的格式完全取决于你的硬件。每个DMA控制器都需要一个不同的结构,但所有这些控制器都需要,对于每个块,至少需要源和目的地地址,它们是否应该增加这些地址或不增加,以及我们之前看到的三个参数:突发大小、传输宽度和传输大小。
最后一件事是通常情况下,从设备不会默认发出DRQ,你必须在你的从设备驱动程序中首先启用这一点,每当你愿意使用DMA时。
这些只是一般的内存到内存(也称为mem2mem)或内存到设备(mem2dev)传输。大多数设备通常支持dmaengine支持的其他种类的传输或内存操作,将在本文档的后面详细介绍。
Linux中的DMA支持
从历史上看,DMA控制器驱动程序一直使用异步TX API实现,以卸载诸如内存复制、XOR、加密等操作,基本上是任何内存到内存操作。
随着时间的推移,出现了内存到设备的传输需求,dmaengine得到了扩展。如今,异步TX API被编写为dmaengine的一层,并充当客户端。尽管如此,dmaengine在某些情况下仍然适应该API,并做出了一些设计选择,以确保它保持兼容。
有关异步传输/转换API的更多信息,请查看相关文档文件中的Asynchronous Transfers/Transforms API。
DMAEngine API
结构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结构有一个称为cap_mask的字段,其中保存了支持的各种事务类型,您需要使用dma_cap_set函数修改此掩码,其参数取决于您支持的事务类型的各种标志。
所有这些功能都在dma_transaction_type枚举中定义,位于include/linux/dmaengine.h中。
目前可用的类型有:
- DMA_MEMCPY
- 设备能够执行内存到内存的复制
- 无论源和目标的组合块的总大小如何,只会传输与两者中较小的那个一样多的字节。这意味着两个列表中的散射-聚集缓冲区的数量和大小不需要相同,并且操作在功能上等同于strncpy,其中count参数等于两个散射-聚集列表缓冲区的最小总大小。
- 通常用于在主机内存和内存映射的GPU设备内存之间复制像素数据,例如现代PCI视频显卡上的OpenGL API函数glReadPielx(),可能需要将大型帧缓冲区从本地设备内存完全复制到主机内存。
- DMA_XOR
- 设备能够在内存区域上执行异或操作
- 用于加速异或密集型任务,例如RAID5
- DMA_XOR_VAL
- 设备能够使用异或算法对内存缓冲区执行奇偶校验。
- DMA_PQ
- 设备能够执行RAID6 P+Q计算,其中P是简单的异或,Q是Reed-Solomon算法。
- DMA_PQ_VAL
- 设备能够使用RAID6 P+Q算法对内存缓冲区执行奇偶校验。
- DMA_MEMSET
- 设备能够使用提供的模式填充内存
- 该模式被视为单个字节的有符号值。
- DMA_INTERRUPT
- 设备能够触发一个生成周期性中断的虚拟传输
- 客户端驱动程序使用它来注册一个回调函数,该函数将通过DMA控制器中断定期调用。
- DMA_PRIVATE
- 设备仅支持从设备到内存的传输,因此不适用于异步传输。
- DMA_ASYNC_TX
- 设备不能设置此标志,如果需要,框架将设置它。
- 待办事项:这是什么?
- DMA_SLAVE
- 设备可以处理从设备到内存的传输,包括散射-聚集传输。
- 在mem2mem情况下,我们有两种不同的类型来处理单个要复制的块或它们的集合,而在这里,我们只有一种单一的事务类型,应该处理这两种情况。
- 如果要传输单个连续的内存缓冲区,只需构建一个只有一个项目的散射列表。
- DMA_CYCLIC
- 设备可以处理循环传输。
- 循环传输是指块集合将循环自身,最后一个项目指向第一个。
- 通常用于音频传输,您希望在一个单独的环形缓冲区上操作,该缓冲区将用音频数据填充。
- DMA_INTERLEAVE
- 设备支持交错传输。
- 这些传输可以将数据从非连续缓冲区传输到非连续缓冲区,与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
- 设备支持通过使用设置了DMA_PREP_LOAD_EOT标志的新传输来排队在传输结束(EOT)时替换重复传输。
- 根据DMA客户端的需求,将来将添加在其他位置替换当前正在运行的传输的支持(例如在传输结束而不是传输结束时)。
这些不同的类型还会影响源和目标地址随时间的变化。
指向RAM的地址通常在每次传输后递增(或递减)。在环形缓冲区的情况下,它们可能循环(DMA_CYCLIC)。指向设备寄存器(例如FIFO)的地址通常是固定的。
描述符元数据支持
某些数据移动架构(DMA控制器和外围设备)使用与事务相关的元数据。DMA控制器的作用是将有效负载和元数据一起传输。元数据本身并不被DMA引擎所使用,但它包含了外围设备的参数、密钥、向量等信息,或者来自外围设备的信息。
DMA引擎框架提供了一种通用的方式来支持描述符的元数据。根据架构的不同,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_channel或dma_release_channel时,将调用这些函数。
它们负责分配/释放所有必要的资源,以使该通道对您的驱动程序有用。
这些函数可能会休眠。
- device_prep_dma_*这些函数与您之前注册的功能相匹配。这些函数都接受准备传输的缓冲区或分散列表,并应从中创建一个或多个硬件描述符。这些函数可能会在中断上下文中调用。您可能进行的任何分配应该使用GFP_NOWAIT标志,以免潜在休眠,但又不耗尽紧急池。驱动程序应尽量在探测时预先分配可能需要的任何内存,以避免对现在ait分配器施加过大压力。它应返回dma_async_tx_descriptor结构的唯一实例,进一步表示此特定传输。可以使用函数dma_async_tx_descriptor_init初始化此结构。您还需要在此结构中设置两个字段:
- flags:TODO:驱动程序本身是否可以修改它,还是应始终使用参数传递的标志
- tx_submit:指向您必须实现的函数的指针,该函数应将当前事务描述符推送到等待调用issue_pending的挂起队列中。
- 在此结构中,函数指针callback_result可以初始化,以便通知提交者事务已完成。在早期的代码中使用了函数指针callback。但是它不提供任何事务状态,并将被弃用。传递给callback_result的结果结构定义为dmaengine_result,它具有两个字段:
- result:提供由dmaengine_tx_result定义的传输结果。成功或某些错误条件。
- residue:为支持残留的传输提供残余字节。
- device_issue_pending
获取挂起队列中的第一个事务描述符,并启动传输。每当传输完成时,它应移动到列表中的下一个事务。
此函数可以在中断上下文中调用。 - device_tx_status
应报告通道上剩余的字节数。
只关心作为参数传递的事务描述符,而不关心给定通道上当前活动的事务描述符。
tx_state参数可能为NULL。
应使用dma_set_residue进行报告。
对于循环传输,应仅考虑循环缓冲区的总大小。
如果设备不支持有序完成并且正在无序完成操作,则应返回DMA_OUT_OF_ORDER。
此函数可以在中断上下文中调用。 - device_config
使用作为参数给出的配置重新配置通道。
此命令不应同步执行,也不应在任何当前排队的传输上执行,而应仅在随后的传输上执行。
在这种情况下,该函数将接收一个dma_slave_config结构指针作为参数,该结构将详细说明要使用的配置。
即使该结构包含一个方向字段,该字段已被弃用,而应使用prep_*函数中给定的方向参数。
仅对从设备操作有效。这不应该为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_CTRL_ACK
- 如果清除,提供程序不能在客户端确认接收之前重用描述符,即在客户端有机会建立任何依赖链之前。
- 可以通过调用async_tx_ack()来确认。
- 如果设置,不意味着描述符可以被重用。
DMA_CTRL_REUSE
- 如果设置,描述符在完成后可以被重用。如果设置了此标志,则提供程序不应释放它。
- 描述符应通过调用dmaengine_desc_set_reuse()准备重用,该函数将设置DMA_CTRL_REUSE。
- dmaengine_desc_set_reuse()仅在通道支持可重用描述符时才会成功。
- 因此,如果设备驱动程序希望跳过两个传输之间的dma_map_sg()和dma_unmap_sg(),因为DMA的数据没有被使用,它可以在完成后立即重新提交传输。
- 描述符可以通过以下几种方式释放:
- 通过调用dmaengine_desc_clear_reuse()清除DMA_CTRL_REUSE,并提交最后一个事务
- 显式调用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驱动程序都基于类似的设计,处理传输结束中断的处理程序,但将大部分工作推迟到任务队列中,包括在前一个传输结束后启动新的传输。
然而,这种设计效率较低,因为传输之间的间隔时间不仅包括中断延迟,还包括任务队列的调度延迟,这将导致通道在传输之间处于空闲状态,从而降低全局传输速率。
您应该避免这种做法,而是将选举新传输的部分移到中断处理程序中,以便具有更短的空闲窗口(无论如何我们无法避免)。
术语表
- Burst:在刷新到内存之前可以排队到缓冲区的连续读取或写入操作的数量。
- Chunk:一组连续的burst。
- Transfer:一组chunk(无论是连续的还是不连续的)。