DMA引擎控制器文档¶
硬件介绍¶
大多数从属 DMA 控制器具有相同的通用操作原理。
它们具有一定数量的通道用于 DMA 传输,以及一定数量的请求线路。
请求和通道几乎是正交的。通道可用于为多个甚至任何请求提供服务。简单来说,通道是执行复制的实体,请求是所涉及的端点。
请求线路实际上对应于从符合 DMA 条件的设备到控制器本身的物理线路。每当设备想要启动传输时,它将通过断言该请求线路来断言 DMA 请求 (DRQ)。
一个非常简单的 DMA 控制器只会考虑一个参数:传输大小。在每个时钟周期,它会将一个字节的数据从一个缓冲区传输到另一个缓冲区,直到达到传输大小。
这在现实世界中不会很好地工作,因为从属设备可能需要在单个周期内传输特定数量的位。例如,当执行简单的内存复制操作时,我们可能希望传输与物理总线允许的尽可能多的数据以最大化性能,但我们的音频设备可能具有更窄的 FIFO,需要一次写入 16 或 24 位的数据。这就是为什么大多数(如果不是全部)DMA 控制器都可以使用称为传输宽度的参数来调整这一点的原因。
此外,一些 DMA 控制器,每当 RAM 用作源或目标时,可以将内存中的读取或写入分组到一个缓冲区中,因此您将获得几个更大的传输,而不是进行大量小的内存访问(这不是很有效)。这是使用一个称为突发大小的参数完成的,该参数定义了允许执行多少个单独的读取/写入,而无需控制器将传输拆分为更小的子传输。
那么,我们理论上的 DMA 控制器只能执行涉及单个连续数据块的传输。但是,我们通常进行的一些传输不是这样,并且希望将数据从非连续缓冲区复制到连续缓冲区,这称为分散-收集。
DMAEngine,至少对于 mem2dev 传输,需要支持分散-收集。因此,我们这里有两种情况:要么我们有一个相当简单的 DMA 控制器不支持它,我们必须在软件中实现它,要么我们有一个更高级的 DMA 控制器,它在硬件中实现了分散-收集。
后者通常使用要传输的块的集合进行编程,并且每当启动传输时,控制器将遍历该集合,执行我们在那里编程的任何操作。
此集合通常是表格或链表。然后,您会将表格的地址及其元素数量,或列表的第一项推送到 DMA 控制器的一个通道,并且每当断言 DRQ 时,它将遍历该集合以了解从何处获取数据。
无论哪种方式,此集合的格式都完全取决于您的硬件。每个 DMA 控制器都需要不同的结构,但所有 DMA 控制器都需要为每个块至少提供源地址和目标地址,是否应递增这些地址以及我们之前看到的三个参数:突发大小、传输宽度和传输大小。
最后一点是,通常情况下,从属设备默认不会发出 DRQ,并且您必须首先在从属设备驱动程序中启用此功能,以便您愿意使用 DMA。
这些只是通用的内存到内存(也称为 mem2mem)或内存到设备(mem2dev)类型的传输。大多数设备通常支持 dmaengine 支持的其他类型的传输或内存操作,将在本文档后面详细介绍。
Linux 中的 DMA 支持¶
从历史上看,DMA 控制器驱动程序是使用异步 TX API 实现的,以卸载诸如内存复制、XOR、加密等操作,基本上是任何内存到内存的操作。
随着时间的推移,出现了对内存到设备传输的需求,并且 dmaengine 得到了扩展。如今,异步 TX API 作为 dmaengine 之上的层编写,并充当客户端。尽管如此,dmaengine 在某些情况下容纳了该 API,并做出了一些设计选择以确保其保持兼容性。
有关异步 TX API 的更多信息,请查看 异步传输/转换 API 中的相关文档文件。
DMAEngine API¶
struct dma_device
初始化¶
就像任何其他内核框架一样,整个 DMAEngine 注册都依赖于驱动程序填写一个结构并在框架中注册。在我们的例子中,该结构是 dma_device。
您需要在驱动程序中做的第一件事是分配此结构。任何常用的内存分配器都可以,但您还需要在其中初始化一些字段
channels
:应使用 INIT_LIST_HEAD 宏等初始化为列表src_addr_widths
:应包含支持的源传输宽度的位掩码dst_addr_widths
:应包含支持的目标传输宽度的位掩码directions
:应包含支持的从属方向的位掩码(即不包括 mem2mem 传输)residue_granularity
:报告给 dma_set_residue 的传输残余的粒度。这可以是Descriptor:您的设备不支持任何类型的残余报告。该框架只会知道特定的事务描述符已完成。
Segment:您的设备能够报告哪些块已传输
Burst:您的设备能够报告哪些突发已传输
dev
:应保存指向与您当前驱动程序实例关联的struct device
的指针。
支持的事务类型¶
您需要做的下一件事是设置您的设备(和驱动程序)支持哪些事务类型。
我们的 dma_device structure
有一个名为 cap_mask 的字段,其中包含支持的各种类型的事务,您需要使用 dma_cap_set 函数修改此掩码,并使用各种标志作为参数,具体取决于您支持的事务类型。
所有这些功能都在 include/linux/dmaengine.h
中的 dma_transaction_type enum
中定义
目前,可用的类型有
DMA_MEMCPY
该设备能够执行内存到内存的复制
无论源和目标的组合块的总大小是多少,都只会传输两个中最小的字节数。这意味着两个列表中的分散-收集缓冲区的大小和数量不必相同,并且该操作在功能上等效于
strncpy
,其中count
参数等于两个分散-收集列表缓冲区中最小的总大小。它通常用于在主机内存和内存映射的 GPU 设备内存之间复制像素数据,例如现代 PCI 显卡上的内存。最直接的例子是 OpenGL API 函数
glReadPielx()
,它可能需要将一个巨大的帧缓冲器从本地设备内存原样复制到主机内存。
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
设备不得设置此项,如果需要,框架将设置此项。
TODO:这是关于什么的?
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
该设备支持在传输结束 (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_channel
或dma_release_channel
时,将调用这些函数。它们负责分配/释放所有必要的资源,以便该通道对您的驱动程序有用。
这些函数可以睡眠。
device_prep_dma_*
这些函数与您之前注册的功能相匹配。
这些函数都获取正在准备的传输的相关缓冲区或分散列表,并且应该从中创建硬件描述符或硬件描述符列表。
这些函数可以从中断上下文中调用
您可能执行的任何分配都应使用 GFP_NOWAIT 标志,以便不会潜在地睡眠,但也不会耗尽紧急池。
驱动程序应尝试在探测时预先分配它们在传输设置期间可能需要的任何内存,以避免给 nowait 分配器带来过多压力。
它应返回
dma_async_tx_descriptor 结构
的唯一实例,该结构进一步表示此特定传输。可以使用函数
dma_async_tx_descriptor_init
初始化此结构。您还需要在此结构中设置两个字段
flags:TODO:它可以由驱动程序本身修改吗,还是应该始终是参数中传递的标志
tx_submit:指向您必须实现的函数的指针,该函数应该将当前事务描述符推送到挂起队列,等待 issue_pending 被调用。
在此结构中,可以初始化函数指针 callback_result,以便通知提交者事务已完成。在之前的代码中,已使用了函数指针 callback。但是,它不会为事务提供任何状态,并且将被弃用。作为
dmaengine_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_* 函数的方向参数
此调用仅对于从设备操作是强制性的。对于 memcpy 操作,不应设置或预期设置此项。如果驱动程序同时支持两者,则应仅对从设备操作使用此调用,而不是对 memcpy 操作使用。
device_pause
暂停通道上的传输
此命令应在通道上同步运行,立即暂停给定通道的工作
device_resume
恢复通道上的传输
此命令应在通道上同步运行,立即恢复给定通道的工作
device_terminate_all
中止通道上所有挂起和正在进行的传输
对于中止的传输,不应调用 complete 回调
可以在原子上下文中调用,也可以在描述符的完整回调中调用。不能休眠。驱动程序必须能够正确处理此情况。
终止可能是异步的。驱动程序不必等待当前活动的传输完全停止。请参阅 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()
才会成功。因此,如果设备驱动程序想要在 2 次传输之间跳过
dma_map_sg()
和dma_unmap_sg()
,因为没有使用 DMA 数据,它可以立即在完成传输后重新提交该传输。描述符可以通过几种方式释放
通过调用
dmaengine_desc_clear_reuse()
并提交最后一个 txn 来清除 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 驱动程序都基于类似的设计,该设计在处理程序中处理传输中断的结束,但将大部分工作推迟到 tasklet,包括每当先前传输结束时启动新的传输。
但这是一种相当低效的设计,因为传输间延迟不仅是中断延迟,还是 tasklet 的调度延迟,这将使通道在两者之间处于空闲状态,从而降低全局传输速率。
您应该避免这种做法,而不是在您的 tasklet 中选择新的传输,而是将该部分移动到中断处理程序中,以便具有更短的空闲窗口(无论如何,我们都无法真正避免)。
术语表¶
突发:在刷新到内存之前可以排队到缓冲区的连续读取或写入操作的数量。
块:连续的突发集合
传输:块的集合(无论是连续的还是不连续的)