DMA Engine框架(一)
参考:
《Linux设备驱动开发》
DMA基础概念可查看:https://www.cnblogs.com/lethe1203/p/18092378
一个完整的 DMA 传输过程必须经过 DMA 请求、DMA 响应、DMA 传输、DMA 结束这四个阶段。
1、DMA 请求:CPU 对 DMA 控制器初始化,并向 I/O 接口发出操作命令,I/O 接口提出 DMA 请求
2、DMA 响应:DMA 控制器对 DMA 请求判别优先级以及屏蔽位,向总线裁决逻辑提出总线请求,当 CPU 执行完成当前的总线周期之后即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示 DMA 已经就绪,通过 DMA 控制器通知 I/O 接口开始 DMA 传输。
3、DMA 传输:在 DMA 控制器的引导下,在存储器和外设之间进行数据传送,在传送过程中不需要 CPU 的参与
4、DMA 结束:当完成既定操作之后,DMA 控制器释放总线控制权,并向 I/O 接口发出结束信号,当 I/O 接口收到结束信号之后,一方面停止 I/O 设备的工作,另一方面向 CPU 提出中断请求,使 CPU 从不介入状态解脱,并执行一段检查本次 DMA 传输操作正确性的代码。最后带着本次操作的结果以及状态继续执行原来的程序。
DMA Engine是开发DMA控制器驱动程序的通用内核框架。DMA的主要目的是在复制内存的时候减轻CPU的负担。使用通道将事务(I/O数据传输)委托给DMA Engine,DMA Engine通过其驱动程序API提供一组可供其他设备(从设备)使用的通道,如下:
DMA Engine引用的头文件如下:
#include <linux/dmaengine.h>
Slave-DMA和Async TX api:
Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API,这些API就是Async TX API
最后,因为memory到memory的DMA传输有了比较简洁的API,没必要直接使用dma engine提供的API,最后就导致dma engine所提供的API就特指为Slave-DMA API(把mem2mem剔除了)。Slave-DMA中的“slave”,指的是参与DMA传输的设备。而对应的,“master”就是指DMA controller自身。一定要明白“slave”的概念,才能更好的理解kernel dma engine中有关的术语和逻辑。
这些也会在下面的描述中具体体现
从设备DMA用法:
从设备DMA用法不复杂,具体过程如下:
1、分配DMA从通道
2、设备从设备和DMA控制器特定参数
3、获取事务的描述符
4、提交事务
5、发出挂起的请求并等待回调通知
分配DMA从通道:
使用dma_request_channel()请求通道,原型如下:
struct dma_chan *dma_request_channel(dma_cap_mask_t *mask, dma_filter_fn filter_fn, void *filter_param); // 参数说明: mask: 用于指定请求 DMA 通道的能力掩码,可以通过宏 DMA_SLAVE、DMA_MEMCPY 等进行设置。 filter_fn: 是一个回调函数,用于过滤 DMA 通道,如果返回值为真,表示该通道符合要求。 filter_param: 被传递给 filter_fn 函数作为参数。 // 返回值说明: 如果成功找到符合条件的 DMA 通道,则返回一个指向 struct dma_chan 结构的指针。 如果没有找到符合条件的 DMA 通道,则返回 NULL。
mask是位图掩码,表示该通道必须满足的功能。使用它主要是为了指定驱动程序需要执行的传输类型:
enum dma_transaction_type { DMA_MEMCPY, // 内存拷贝操作 DMA_XOR, // 异或操作 DMA_PQ, // 奇偶校验操作 DMA_XOR_VAL, // 带有异或值的操作 DMA_PQ_VAL, // 带有奇偶校验值的操作 DMA_MEMSET, // 内存设置操作 DMA_MEMSET_SG, // 散列表内存设置操作 DMA_INTERRUPT, // 中断相关的 DMA 操作 DMA_PRIVATE, // 私有的 DMA 操作 DMA_ASYNC_TX, // 异步传输的 DMA 操作 DMA_SLAVE, // 从设备相关的 DMA 操作 DMA_CYCLIC, // 循环的 DMA 操作 DMA_INTERLEAVE, // 交错的 DMA 操作 DMA_COMPLETION_NO_ORDER, // 无序完成的 DMA 操作 DMA_REPEAT, // 重复的 DMA 操作 DMA_LOAD_EOT, // 加载结束操作标志的 DMA 操作 DMA_TX_TYPE_END, // 最后一个事务类型,用于创建功能掩码 };
dma_cap_zero()和dma_cap_set()函数用于清除掩码,设置所需要的功能,例如:
dma_cap_mask mask_test; struct dma_chan *chan; dma_cap_zero(mask_test); dma_cap_set(DMA_MEMCPY, mask_test); chan = dma_request_channel(mask_test, NULL, NULL);
前面提到的dma_filter_fn,dma_filter_fn 是一个指向返回布尔类型的函数的指针,定义如下:
typedef bool (*dma_filter_fn)(struct dma_chan *chan, void *filter_param); // 参数说明 struct dma_chan *chan:表示 DMA 通道的指针,函数可以使用这个通道来检查和处理 DMA 请求。 void *filter_param:一个指向参数的指针,可以用来传递额外的过滤参数给 DMA 过滤器函数。
如果filter_fn参数为NULL,则dma_request_channel()将只返回满足功能掩码的第一个通道。否则,当掩码参数不足以指定所需通道时,可以使用filter_fn例程作为系统中可用通道的过滤器。内核为系统中每个空闲通道调用一次filter_fn例程。当看到合适的通道,filter_fn应该返回DMA_ACK,它将把给定的通道标记为dma_request_channel()的返回值。
通过此接口分配的通道由调用者独占,直到调用dma_release_channel()为止:
void dma_release_channel(struct dma_chan *chan)
设置从设备和控制器指定参数:
数据结构struct dma_slave_config,结构体如下
struct dma_slave_config { enum dma_transfer_direction direction; // 传输方向 phys_addr_t src_addr; // 源地址 phys_addr_t dst_addr; // 目的地址 enum dma_slave_buswidth src_addr_width; // 源位宽 enum dma_slave_buswidth dst_addr_width; // 目的位宽 u32 src_maxburst; // 一次burst可以发送到设备的最大字数(单位为dma_slave_buswidth) u32 dst_maxburst; u32 src_port_window_size; u32 dst_port_window_size; bool device_fc; unsigned int slave_id; void *peripheral_config; size_t peripheral_size; }; enum dma_transfer_direction { DMA_MEM_TO_MEM, DMA_MEM_TO_DEV, DMA_DEV_TO_MEM, DMA_DEV_TO_DEV, DMA_TRANS_NONE, }; enum dma_slave_buswidth { DMA_SLAVE_BUSWIDTH_UNDEFINED = 0, DMA_SLAVE_BUSWIDTH_1_BYTE = 1, DMA_SLAVE_BUSWIDTH_2_BYTES = 2, DMA_SLAVE_BUSWIDTH_3_BYTES = 3, DMA_SLAVE_BUSWIDTH_4_BYTES = 4, DMA_SLAVE_BUSWIDTH_8_BYTES = 8, DMA_SLAVE_BUSWIDTH_16_BYTES = 16, DMA_SLAVE_BUSWIDTH_32_BYTES = 32, DMA_SLAVE_BUSWIDTH_64_BYTES = 64, };
例如:
dma_cap_mask my_dma_cap_mask; struct dma_chan *chan; dma_cap_zero(my_dma_cap_mask); dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); struct dma_chan *my_dma_chan; dma_addr_t dma_src, dma_dst; struct dma_slave_config my_dma_cfg = {0}; my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL); my_dma_cfg.direction = DMA_MEM_TO_MEM; my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES; dmaengine_slave_config(my_dma_chan, my_dma_cfg); char *rx_data, *tx_data; rx_data = kzalloc(BUFFER_SIZE, GFP_DMA); tx_data = kzalloc(BUFFER_SIZE, GFP_DMA); feed_data(tx_data); dma_src_addr = dma_map_single(NULL, tx_data, BUFFER_SIZE, DMA_MEM_TO_MEM); dma_dst_addr = dma_map_single(NULL, rx_data, BUFFER_SIZE, DMA_MEM_TO_MEM);
上面的代码中,调用dma_request_channel()函数获取DMA通道,dmaengine_slave_config()来应用配置,调用dma_map_single映射rx和tx缓冲区,以便在DMA中使用
获取事务描述符:
struct dma_chan { struct dma_device *device; struct device *slave; dma_cookie_t cookie; dma_cookie_t completed_cookie; /* sysfs */ int chan_id; struct dma_chan_dev *dev; const char *name; #ifdef CONFIG_DEBUG_FS char *dbg_client_name; #endif struct list_head device_node; struct dma_chan_percpu __percpu *local; int client_count; int table_count; /* DMA router */ struct dma_router *router; void *route_data; void *private; };
dma_chan结构体包含dma_device结构体,他表示提供该通道的DMA设备(实际为控制器)。此控制器的内核驱动程序负责提供一组函数来准备DMA事务,其中每一个函数都对应着DMA事务类型。根据事务类型的不同,可能只能选择专用函数,一些函数类型如下:
struct dma_async_tx_descriptor *dma_prep_dma_memcpy(struct dma_chan *chan, dma_addr_t dest, dma_addr_t src, size_t len, unsigned long flags); 描述:准备一个用于内存拷贝的 DMA 请求。 参数: struct dma_chan *chan:DMA 通道。 dma_addr_t dest:目的地址。 dma_addr_t src:源地址。 size_t len:拷贝的数据长度。 unsigned long flags:标志位,如 DMA_CTRL_ACK、DMA_PREP_INTERRUPT 等。 struct dma_async_tx_descriptor *dma_prep_dma_xor(struct dma_chan *chan, dma_addr_t dest, dma_addr_t src1, dma_addr_t src2, size_t len, unsigned long flags); 描述:准备一个用于内存异或的 DMA 请求。 参数: struct dma_chan *chan:DMA 通道。 dma_addr_t dest:目的地址。 dma_addr_t src1:源地址1。 dma_addr_t src2:源地址2。 size_t len:异或的数据长度。 unsigned long flags:标志位,如 DMA_CTRL_ACK、DMA_PREP_INTERRUPT 等。 struct dma_async_tx_descriptor *dma_prep_dma_sg(struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_transfer_direction direction, unsigned long flags); 描述:准备一个用于散列表传输的 DMA 请求。 参数: struct dma_chan *chan:DMA 通道。 struct scatterlist *sgl:散列表的起始地址。 unsigned int sg_len:散列表的长度。 enum dma_transfer_direction direction:数据传输方向。 unsigned long flags:标志位,如 DMA_CTRL_ACK、DMA_PREP_INTERRUPT 等。 等等
以imx-sdma.c为例,上面这些函数都会返回指向struct dma_async_tx_descriptor结构体的指针,其对应于事务描述符。比如,内存到内存的复制,使用device_prep_dma_memcpy:
struct dma_device *dma_dev = my_dma_chan->device; struct dma_async_tx_descriptor *tx = NULL; tx = dma_dev->dma_prep_dma_memcpy(my_dma_chan, dma_dst_addr, dma_src_addr, BUFFER_SIZE, 0);
实际上,应该使用dmaengine_prep_* DMA Engine的操作API,但是请注意,这些函数在内部执行前面刚执行过的操作。例如,对于内存到内存的复制,可以使用device_prep_dma_memcpy()函数
struct dma_async_tx_descriptor *(* device_prep_dma_memcpy)(struct dma_chan *chan, dma_addr_t dst, dma_addr_t src, size_t len, unsigned long flags)
这个例子变为:
struct dma_async_tx_descriptor *tx = NULL; tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, dma_src_addr, BUFFER_SIZE, 0);
提交事务:
要将事务放入驱动程序的等待队列中,需要调用dmaengine_submit(),一旦准备好描述符并添加回调信息,就应该将其放在DMA Engine驱动程序等待队列中:
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *tx);
该函数返回一个cookie,可以通过其他DMA Engine检查DMA活动的进度。dmaengine_submit()不会启动DMA操作,它只是将其添加到待处理的队列中。下面将讨论启动事务:
struct completion transfer_ok; init_completion(&transfer_ok); tx->callback = my_dma_callback; dma_cookie_t cookie = dmaengine_submit(tx); if(dma_submit_error(cookie)) { ... } ...
发布待处理DMA请求并等待回调通知:
启动事务是DMA传输设置的最后一步。在通道上调用dma_async_issue_pending()来激活通道待处理队列中的事务。如果通道空闲,则队列中的第一个事务将启动,后续事务排队等候。DMA操作完成时,队列的下一个事务启动。并触发软中断tasklet。如果已经设置,则该tasklet负责调用客户端驱动程序的完成回调函数进行通知,如下:
dma_async_issue_pending(my_dma_chan); wait_for_completion(&transfer_ok); dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, BUFFER_SIZE, DMA_MEM_TO_MEM); dma_unmap_single(my_dma_chan->device->dev, dma_dst_addr, BUFFER_SIZE, DMA_MEM_TO_MEM); /* 通知rx_data和tx_data虚拟地址处理缓冲区 */
实际上发布待处理事务的DMA Engine API函数是dmaengine_issue_pending(struct dma_chan *chan),它是dma_async_issue_pending()的包装。