DMA Engine框架(一)

参考:
《Linux设备驱动开发》
 
 
一个完整的 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提供一组可供其他设备(从设备)使用的通道,如下:
0
DMA Engine引用的头文件如下:
#include <linux/dmaengine.h>
 

Slave-DMA和Async TX api:

Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API,这些API就是Async TX API
0
最后,因为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()的包装。
 
 
 
 
 
posted @ 2024-03-24 22:28  lethe1203  阅读(563)  评论(0编辑  收藏  举报