DMA子系统

1       DMA简介

DMADirect Memory Access的缩写,顾名思义,就是绕开CPU直接访问memory的意思。在计算机中,相比CPUmemory和外设的速度是非常慢的,因而在memorymemory(或者memory和设备)之间搬运数据,非常浪费CPU的时间,造成CPU无法及时处理一些实时事件。因此,工程师们就设计出来一种专门用来搬运数据的器件----DMA控制器,协助CPU进行数据搬运,如下图所示:

         DMA Controller, 也就是DMA控制器. 一般的ARM CPU上都有这么一个外设. 它提供DMA服务, 因此可以认为它是Provider.

         ARM CPU上有一些外设, 它们能够利用DMA搬移数据. 例如MMC, 当它收到数据时, 它会把数据存储在自己的寄存器中, 我们既可以通过CPU循环读取寄存器来获取这些数据(此时会占用CPU时间); 也可以利用DMA把寄存器的数据搬移到内存, 当搬移了一定的数量后, 通知CPU, 然后CPU在从内存中一次性读取DMA搬移过程中, CPU可以干别的事情).

这些外设使用DMA控制器提供的服务, 因此可以认为它们是Consumer.

         另外, DMA控制器在搬移数据时, 对于内存有一些要求, 例如它要求物理地址连续等. 因此这里还涉及到一个DMA Buffer分配的问题.

后文将着重从ProviderConsumerDMA Buffer分配这3个方面来介绍kernel提供的相关机制.

2       DMA相关概念介绍

2.1     DMA Controller & Channels

DMA Controller : 就是指CPU上的DMA控制器.

DMA Channels : 每个控制器内部可能会有多个通道, 每个通道陈作一个Channel.

注意, 鉴于总线访问的冲突, 以及内存一致性的考量, 这些Channels不可能真正的并行工作, 而只能分时复用, DMA控制器充当仲裁角色.

既然是分时复用, 那么我们也可以基于某一个物理Channel, 抽象出多个虚拟Channel, 由软件负责仲裁, 以决定在某一时刻由哪个虚拟Channel来使用物理Channel. Linux内核确实抽象了一些virt channel, 我们将在《DMA Provider》一节详细介绍.

2.2     DMA Slave Device

DMA Device : 就是指那些能利用DMA搬移数据的片上外设, 例如MMCUSB Controller.

有的地方也陈之为DMA Slave Device.

2.3     DMA DRQ

MMC利用DMA搬移数据的例子中, DMA控制器何时开始搬移数据才是合理的呢? 显然是MMC收到数据并存放于寄存器之后.

DMA控制器怎么才能知道MMC是否收到数据了呢? 答案是DRQ. DMA Slave DeviceDMA Controller之间有几条物理的连线, 陈作DRQ. Device准备好收发数据后, 就通过DRQ通知Controller开始搬移.

2.4     transfer size

一个最简单的DMA控制器只需要一个参数 : transfer size.

每个时钟周期, 控制器传送一个字节的数据, 直到所有的数据都被传送完毕.

2.5     transfer width

不过这在现实世界中往往不能满足需求,因为有些设备可能需要在一个时钟周期中,传输指定bit的数据,例如:

         memory之间传输数据的时候,希望能以总线的最大宽度为单位(32-bit64-bit等),以提升数据传输的效率;

         而在音频设备中,需要每次写入精确的16-bit或者24-bit的数据;

         等等

因此,为了满足这些多样的需求,DMA controller提供一个额外的参数----transfer width

2.6     burst size

当传输的源或者目的地是memory的时候,为了提高效率,DMA controller不愿意每一次传输都访问memory,而是在内部开一个buffer,将数据缓存在自己buffer中:

         memory是源的时候,一次从memory读出一批数据,保存在自己的buffer中,然后再一点点(以时钟为节拍),传输到目的地;

         memory是目的地的时候,先将源的数据传输到自己的buffer中,当累计一定量的数据之后,再一次性的写入memory

这种场景下,DMA控制器内部可缓存的数据量的大小,称作burst size

2.7     scatter-gather

一般的DMA控制器只能访问物理地址连续的内存. 但是在有些场景下, 我们只有一些物理地址不连续的内存块, 需要DMA把这些内存块的数据搬移到别处, 这种场景陈作scatter-gather.

因此从软件层面来说, DMA核心层必须提供scatter-gather的能力. 如果DMA控制器本身支持scatter-gather操作, 那就直接配置控制器即可; 如果控制器不支持, 我们就只有用软件模拟了.

 

针对那些支持scatter-gather操作的DMA控制器, 软件通常需要准备一个table或者link-list. 然后你可以把table的地址以及元素个数告知DMA控制器, 或者说把link-list的头元素告知控制器. 当控制器开始搬移时, 它会遍历每一个元素, 以便知道从哪个地址获取数据.

不同的DMA硬件可能会使用不同的形式, 有的用table, 有的用link-list, 有的用其它. 但不管哪种形式, 集合中的每一个元素都需要提供源地址、目的地址、transfer sizetransfer widthburst size这几个信息.

 

如果采用软件模拟, 那我们只有先把这些内存块的数据搬移到一个连续的地址, 然后让DMA从这个新地址开始搬移. 当然这会损耗性能.

3       DMA Consumer

Consumer指的是使用DMA功能的内核驱动. 例如MMC控制器的驱动程序, 其中的部分代码会使用DMA来进行数据搬移.

 

内核的DMA子系统由dmaengine负责管理: providerengine进行注册; consumer则使用engine提供的APIs.

 

本小节会详细讨论这些APIs.

3.1     Slave-DMA APIAsync TX API

从方向上来说,DMA传输可以分为4类:memorymemorymemorydevicedevicememory以及devicedevice

Linux kernel作为CPU的代理人,从它的视角看,外设都是slave,因此称这些有device参与的传输(MEM2DEVDEV2MEMDEV2DEV)为Slave-DMA传输。而另一种memorymemory的传输,被称为Async TX

 

为什么强调这种差别呢?因为Linux为了方便基于DMAmemcpymemset等操作,在dma engine之上,封装了一层更为简洁的API(如下所示),这种API就是Async TX API(以async_开头,例如async_memcpyasync_memsetasync_xor等)。

最后,因为memorymemoryDMA传输有了比较简洁的API,没必要直接使用dma engine提供的API,最后就导致dma engine所提供的API就特指为Slave-DMA API(把mem2mem剔除了)。

 

本节主要讨论Slave-DMA API. 后续有机会在看Asynx TX API.

3.2     Slave-DMA APIs

Consumer驱动的编写者来说,要基于dma engine提供的Slave-DMA API进行DMA传输的话,需要如下的操作步骤:

         申请一个DMA channel

         根据设备(slave)的特性,配置DMA channel的参数

         要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor

         将本次传输(transaction)提交给dma engine并启动传输

         等待传输(transaction)结束

         然后,重复3~5即可

上面5个步骤,除了3有点不好理解外,其它的都比较直观易懂,具体可参考后面的介绍。

3.2.1         申请/释放DMA channel

任何consumer在开始DMA传输之前,都要申请一个DMA channel

DMA channel(在kernel中由“struct dma_chan”数据结构表示)由provider提供,被consumer使用。

 

consumer可以通过如下的API申请DMA channel

struct dma_chan *dma_request_chan(struct device *dev, const char *name);

         API是在linux-4.5版本时加入的, 在此之前使用的APIdma_request_channel(mask, x, y)

         API会返回绑定在指定设备(dev)上名称为namedma channel.

         dma engineproviderconsumer可以使用device treeACPI或者struct dma_slave_map类型的match table提供这种绑定关系

 

申请得到的dma channel可以在不需要使用的时候通过下面的API释放掉:

void dma_release_channel(struct dma_chan *chan);

3.2.2         配置DMA channel的参数

consumer申请到一个为自己使用的DMA channel之后,需要根据自身的实际情况,对该channel进行一些配置。

可配置的内容由struct dma_slave_config数据结构表示(具体可参考本章后文)。

consumer配置信息填充到一个struct dma_slave_config变量中后,可以调用如下API将这些信息告诉给DMA controller

int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

3.2.3         获取传输描述tx descriptor

DMA传输属于异步传输,在启动传输之前,consumer driver需要将此次传输的一些信息(例如src/dstbuffer、传输的方向等)提交给dmaengine(本质上是dma controller driver),dma engine确认okay后,返回一个描述符(由struct dma_async_tx_descriptor抽象)。此后,consumer driver就可以以该描述符为单位,控制并跟踪此次传输。

 

struct dma_async_tx_descriptor数据结构可参考本章后文的介绍。

 

根据传输模式的不同,consumer driver可以使用下面三个API获取传输描述符:

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg(

           struct dma_chan *chan, struct scatterlist *sgl,

           unsigned int sg_len, enum dma_data_direction direction,

           unsigned long flags);

API用于scatter-gather类型的传输, 参数列表的意义如下:

         chan : 本次传输所使用的dma channel

         sgl : dma_map_sg分配的一个list

         sg_len : list中元素的个数

         direction : 数据传输的方向, 具体可参考enum dma_data_direction的定义

         flags : 用于向dma controller driver传递一些额外的信息,包括(具体可参考enum dma_ctrl_flags中以DMA_PREP_开头的定义):

        DMA_PREP_INTERRUPT : 告诉DMA controller driver,本次传输完成后,产生一个中断,并调用client提供的回调函数

        DMA_PREP_FENCE : 告诉DMA controller driver,后续的传输,依赖本次传输的结果(这样controller driver就会小心的组织多个dma传输之间的顺序)

       

 

struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic(

           struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,

           size_t period_len, enum dma_data_direction direction);

API常用于音频等场景中, 在进行一定长度的dma传输(buf_addr&buf_len)的过程中, 每传输一定的byteperiod_len, 就会调用一次传输完成的回调函数. 参数包括:

         chan : 本次传输所使用的dma channel

         buf_addrbuf_len : 传输的buffer地址和长度

         period_len : 每隔多久(单位为byte)调用一次回调函数. 需要注意的是, buf_len应该是period_len的整数倍

         direction : 数据传输的方向

 

struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma(

           struct dma_chan *chan, struct dma_interleaved_template *xt,

           unsigned long flags);

API可进行不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中,具体可参考struct dma_interleaved_template结构的定义和解释(这里不再详细介绍,需要用到的时候,再去学习也okay)。

3.2.4         设置回调函数

一旦成功的获取到描述符后, 我们就可以在描述符中添加回调函数, 然后submit这个描述符.

 

回调函数中可以做哪些事情呢? 官文有如下说明:

Note

Although the async_tx API specifies that completion callback routines cannot submit any new operations, this is not the case for slave/cyclic DMA.

For slave DMA, the subsequent transaction may not be available for submission prior to callback function being invoked, so slave DMA callbacks are permitted to prepare and submit a new transaction.

For cyclic DMA, a callback function may wish to terminate the DMA via dmaengine_terminate_async().

Therefore, it is important that DMA engine drivers drop any locks before calling the callback function which may cause a deadlock.

Note that callbacks will always be invoked from the DMA engines tasklet, never from interrupt context.

3.2.5         Submit the transaction

Once the descriptor has been prepared and the callback information added, it must be placed on the DMA engine drivers pending queue.

 

Interface:

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)

This returns a cookie can be used to check the progress of DMA engine activity via other DMA engine calls not covered in this document.

 

注意dmaengine_submit只是加入了传送队列, 并没有开启传送.

若要开启传送, 需要Issue pending DMA requests.

3.2.6         Issue pending DMA requests

The transactions in the pending queue can be activated by calling the issue_pending API. If channel is idle then the first transaction in queue is started and subsequent ones queued up.

 

On completion of each DMA operation, the next in queue is started and a tasklet triggered. The tasklet will then call the client driver completion callback routine for notification, if set.

 

Interface:

void dma_async_issue_pending(struct dma_chan *chan);

3.2.7         wait for callback notification

传输请求被提交之后,client driver可以通过回调函数获取传输完成的消息.

当然,也可以通过dma_async_is_tx_completeAPI,测试传输是否完成。不再详细说明了。

 

最后,如果等不及了,也可以使用dmaengine_pausedmaengine_resumedmaengine_terminate_xxxAPI,暂停、终止传输。

3.2.8         其它APIs

Terminate APIs

int dmaengine_terminate_sync(struct dma_chan *chan)

int dmaengine_terminate_async(struct dma_chan *chan)

int dmaengine_terminate_all(struct dma_chan *chan) /* DEPRECATED */

This causes all activity for the DMA channel to be stopped, and may discard data in the DMA FIFO which hasn’t been fully transferred. No callback functions will be called for any incomplete transfers.

 

dmaengine_terminate_async() might not wait until the DMA has been fully stopped or until any running complete callbacks have finished. But it is possible to call dmaengine_terminate_async() from atomic context or from within a complete callback.

 

dmaengine_terminate_sync() will wait for the transfer and any running complete callbacks to finish before it returns. But the function must not be called from atomic context or from within a complete callback.

 

dmaengine_terminate_all() is deprecated and should not be used in new code.

Pause API

int dmaengine_pause(struct dma_chan *chan)

This pauses activity on the DMA channel without data loss.

Resume API

int dmaengine_resume(struct dma_chan *chan)

Resume a previously paused DMA channel. It is invalid to resume a channel which is not currently paused.

Check Txn complete

enum dma_status dma_async_is_tx_complete(struct dma_chan *chan,

          dma_cookie_t cookie, dma_cookie_t *last, dma_cookie_t *used)

This can be used to check the status of the channel. Please see the documentation in include/linux/dmaengine.h for a more complete description of this API.

 

This can be used in conjunction with dma_async_is_complete() and the cookie returned from dmaengine_submit() to check for completion of a specific DMA transaction.

Note

Not all DMA engine drivers can return reliable information for a running DMA channel. It is recommended that DMA engine users pause or stop (via dmaengine_terminate_all()) the channel before using this API.

Synchronize termination API

void dmaengine_synchronize(struct dma_chan *chan)

This function should be used after dmaengine_terminate_async() to synchronize the termination of the DMA channel to the current context. The function will wait for the transfer and any running complete callbacks to finish before it returns.

 

If dmaengine_terminate_async() is used to stop the DMA channel this function must be called before it is safe to free memory accessed by previously submitted descriptors or to free any resources accessed within the complete callback of previously submitted descriptors.

 

The behavior of this function is undefined if dma_async_issue_pending() has been called between dmaengine_terminate_async() and this function.

3.3     重要数据结构说明

3.3.1         struct dma_slave_config

中包含了完成一次DMA传输所需要的所有可能的参数,其定义如下:

 

头文件 : include/linux/dmaengine.h

struct dma_slave_config

Comment

enum dma_transfer_direction direction

direction,指明传输的方向,包括:

         DMA_MEM_TO_MEMmemorymemory的传输

note : 参考《3.1, MEM-TO-MEM一般不会用Slave-DMA APIs

         DMA_MEM_TO_DEVmemory到设备的传输

         DMA_DEV_TO_MEM,设备到memory的传输

         DMA_DEV_TO_DEV,设备到设备的传输

不一定支持所有的DMA传输方向,具体要看provider的实现

phys_addr_t src_addr

传输方向是dev2mem或者dev2dev时,读取数据的位置(通常是固定的FIFO地址).

mem2dev类型的channel,不需配置该参数(每次传输的时候会指定).

phys_addr_t dst_addr

传输方向是mem2dev或者dev2dev时,写入数据的位置(通常是固定的FIFO地址).

dev2mem类型的channel,不需配置该参数(每次传输的时候会指定).

enum dma_slave_buswidth src_addr_width

src地址的宽度,包括12348163264bytes)等(具体可参考enum dma_slave_buswidth 的定义)

enum dma_slave_buswidth dst_addr_width

类上

u32 src_maxburst

src最大可传输的burst sizeburst size的介绍参考《2.6),单位是src_addr_width(注意,不是byte

u32 dst_maxburst

类上

bool device_fc

当外设是Flow Controller(流控制器)的时候,需要将该字段设置为trueCPU中有关DMA和外部设备之间连接方式的设计中,决定DMA传输是否结束的模块,称作flow controllerDMA controller或者外部设备,都可以作为flow controller,具体要看外设和DMA controller的设计原理、信号连接方式等,不在详细说明

unsigned int slave_id

外部设备通过slave_id告诉dma controller自己是谁(一般和某个request line对应)。很多dma controller并不区分slave,只要给它srcdstlen等信息,它就可以进行传输,因此slave_id可以忽略。而有些controller,必须清晰地知道此次传输的对象是哪个外设,就必须要提供slave_id

3.3.2         struct dma_async_tx_descriptor

传输描述符用于标识一次DMA传输(类似于一个文件句柄).

 

3.2.3节的API可获取一个传输描述符, client获取描述符后,可以以它为单位,进行后续的操作(启动传输、等待传输完成、等等)。也可以将自己的回调函数通过描述符提供给dma engine

 

头文件: include/linux/dmaengine.h

struct dma_async_tx_descriptor

Comment

dma_cookie_t cookie

cookie,一个整型数,用于追踪本次传输。

一般情况下,engine会在内部维护一个递增的number,每当client获取传输描述的时候,都会将该number赋予cookie,然后加一。

enum dma_ctrl_flags flags

DMA_CTRL_开头的标记,包括:

         DMA_CTRL_REUSE,表明这个描述符可以被重复使用,直到它被清除或者释放

         DMA_CTRL_ACK,如果该flag0,表明暂时不能被重复使用

dma_addr_t phys

该描述符的物理地址??不太懂!

struct dma_chan *chan

对应的dma channel

dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx)

controller driver提供的回调函数,用于把描述符提交到待传输列表。通常由dma engine调用,client driver不会直接和该接口打交道

int (*desc_free)(struct dma_async_tx_descriptor *tx)

用于释放该描述符的回调函数,由controller driver提供,dma engine调用,client driver不会直接和该接口打交道

dma_async_tx_callback callback

传输完成的回调函数(及其参数),由client driver提供

void *callback_param

struct dmaengine_unmap_data *unmap

后面其它参数,client driver不需要关心,暂不描述了

struct dma_async_tx_descriptor *next

 

struct dma_async_tx_descriptor *parent

 

spinlock_t lock

 

4       DMA Provider

将从provider的角度,介绍怎样在linux kernel dmaengine的框架下,编写dma controller驱动。

4.1     dma controller驱动的软件框架

设备驱动的本质是描述并抽象硬件,然后为consumer提供操作硬件的友好接口。dma controller驱动也不例外,它要做的事情无外乎是:

         抽象并控制DMA控制器

         管理DMA channel(可以是物理channel,也可以是虚拟channel,),并向client driver提供友好、易用的接口

         DMA channel为操作对象,响应client driverconsumer)的传输请求,并控制DMA controller,执行传输

 

当然,按照惯例,为了统一提供给consumerAPI,并减少DMA controller driver的开发难度(从论述题变为填空题),dmaengine framework提供了一套controller driver的开发框架,主要思路是

         使用struct dma_device抽象DMA controllercontroller driver只要填充该结构中必要的字段,就可以完成dma controller的驱动开发

         使用struct dma_chan(图中的DCn)抽象物理的DMA channel(图中的CHn),物理channelcontroller所能提供的通道数一一对应

         基于物理的DMA channel,使用struct virt_dma_cha抽象出虚拟的dma channel(图中的VCx)。多个虚拟channel可以共享一个物理channel,并在这个物理channel上进行分时传输

         基于这些数据结构,提供一些便于controller driver开发的API,供driver使用

上面三个数据结构的描述,可参考后文的介绍。

 

同时, 我们会在本节后文介绍相关的APIcontroller driver的开发思路和步骤以及dmaengine中和controller driver有关的重要流程.

4.2     主要数据结构描述

4.2.1         struct dma_device

用于抽象dma controllerstruct dma_device是一个庞杂的数据结构, 不过真正需要dma controller driver关心的内容却不是很多

 

头文件 : include/linux/dmaengine.h

struct dma_device

Comment

channels

一个链表头,用于挂载controller支持的所有dma channels. 在初始化的时候,dma controller driver首先要调用INIT_LIST_HEAD初始化它.

在添加channel, 调用list_add_tail将所有的channel添加到该链表头中.

src_addr_widths

dst_addr_widths

一个bitmap,表示该controller支持哪些宽度的src/dst类型,包括12348163264bytes)等(具体可参考enum dma_slave_buswidth 的定义)

directions

一个bitmap,表示该controller支持哪些传输方向,包括DMA_MEM_TO_MEMDMA_MEM_TO_DEVDMA_DEV_TO_MEMDMA_DEV_TO_DEV,具体可参考enum dma_transfer_direction的定义和注释

max_burst

支持的最大的burst传输的size

descriptor_reuse

指示该controller的传输描述可否可重复使用(client driver可只获取一次传输描述,然后进行多次传输)

residue_granularity

granularity of the transfer residue reported to dma_set_residue. This can be either:

    • Descriptor: your device doesn’t support any kind of residue reporting. The framework will only know that a particular transaction descriptor is done.
    • Segment: your device is able to report which chunks have been transferred
  • Burst: your device is able to report which burst have been transferred

 

 

cap_mask

一个bitmap,用于指示该dma controller所具备的能力(可以进行什么样的DMA传输).

         DMA_MEMCPY,可进行memory copy

         DMA_XOR

         等等, 详情参考官网描述

另外,该bitmap的定义,需要和后面device_prep_dma_xxx形式的回调函数对应(bitmap中支持某个传输类型,就必须提供该类型对应的回调函数)

 

 

device_alloc_chan_resources

device_free_chan_resources

client driver申请/释放dma channel的时候,dmaengine会调用dma controller driver相应的alloc/free回调函数,以准备相应的资源。具体要准备哪些资源,则需要dma controller driver根据硬件的实际情况,自行决定(这就是dmaengine framework的流氓之处,呵呵~.

这俩函数里面可以sleep.

device_prep_dma_xxx

client driver通过dmaengine_prep_xxx API获取传输描述符的时候,damengine则会直接回调dma controller driver相应的device_prep_dma_xxx接口。至于要在这些回调函数中做什么事情,可参考官网描述.

这个函数可能在中断上下文中调用, 因此它不能sleep

device_issue_pending

client driver调用dma_async_issue_pending启动传输的时候,会调用调用该回调函数

参考官网描述.

device_tx_status

参考官网描述.

device_config

client driver调用dmaengine_slave_config[配置dma channel的时候,dmaengine会调用该回调函数.

参考官网描述.

device_pause

device_resume

device_terminate_all

client driver调用dmaengine_pausedmaengine_resumedmaengine_terminate_xxxAPI的时候,dmaengine会调用相应的回调函数.

参考官网描述.

device_synchronize

参考官网描述.

总结: dmaenginedma controller的抽象和封装,只是薄薄的一层, 仅封装出来一些回调函数,由dma controller driver实现.

dmaengine本身没有太多的操作逻辑。

4.2.2         struct dma_chan

dma_chan用于抽象dma channel,其内容为:

 

头文件 : include/linux/dmaengine.h

struct dma_device

Comment

struct dma_device *device

指向该channel所在的dma controller

dma_cookie_t cookie

client driver以该channel为操作对象获取传输描述符时,dma controller driver返回给client的最后一个cookie

dma_cookie_t completed_cookie

在这个channel上最后一次完成的传输的cookiedma controller driver可以在传输完成时调用辅助函数dma_cookie_complete设置它的value

 

 

int chan_id

 

struct dma_chan_dev *dev

 

struct list_head device_node

用于将该channel添加到dma_devicechannel列表中

struct dma_chan_percpu __percpu *local

 

int client_count

 

int table_count

 

 

 

/* DMA router */

 

struct dma_router *router

 

void *route_data

 

 

 

void *private

 

4.2.3         struct virt_dma_cha

struct virt_dma_chan用于抽象一个虚拟的dma channel,多个虚拟channel可以共用一个物理channel,并由软件调度多个传输请求,将多个虚拟channel的传输串行地在物理channel上完成。该数据结构的定义如下:

 

头文件 : drivers/dma/virt-dma.h

struct virt_dma_desc

Comment

struct dma_async_tx_descriptor tx

 

struct list_head node

 

 

struct virt_dma_chan

Comment

struct dma_chan chan

用于和client driver打交道(屏蔽物理channel和虚拟channel的差异)

struct tasklet_struct task

用于等待该虚拟channel上传输的完成(由于是虚拟channel,传输完成与否只能由软件判断)

void (*desc_free)(struct virt_dma_desc *)

 

 

 

spinlock_t lock

 

struct list_head desc_allocated

struct list_head desc_submitted

struct list_head desc_issued

struct list_head desc_completed

四个链表头,用于保存不同状态的虚拟channel描述符

 

 

struct virt_dma_desc *cyclic

虚拟channel描述符, 定义如上一个表格所示. 仅仅对struct dma_async_tx_descriptor做了一个简单的封装

4.3     APIs

damengine直接向dma controller driver提供的API并不多(大部分的逻辑交互都位于struct dma_device结构的回调函数中),主要包括:

 

头文件 : include/linux/dmaengine.h

APIs for DMA controller

Comment

int dma_async_device_register(struct dma_device *device)

dma controller driver准备好struct dma_device变量后,可以调用dma_async_device_register将它注册到kernel中。

该接口会对device指针进行一系列的检查,然后对其做进一步的初始化,最后会放在一个名称为dma_device_list的全局链表上,以便后面使用

void dma_async_device_unregister(struct dma_device *device)

注销接口

 

 

/*cookie有关的辅助接口*/

由于cookie有关的操作,有很多共性,dmaengine就提供了一些通用实现

void dma_cookie_init(struct dma_chan *chan)

初始化dma channel中的cookiecompleted_cookie字段

dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)

为传输描述(tx)分配一个cookie

void dma_cookie_complete(struct dma_async_tx_descriptor *tx)

当某一个传输(tx)完成的时候,可以调用该接口,更新该传输所对应channelcompleted_cookie字段

enum dma_status dma_cookie_status(…)

获取指定channelchan)上指定cookie的传输状态

 

 

void dma_run_dependencies(struct dma_async_tx_descriptor *tx)

client可以同时提交多个具有依赖关系的dma传输。因此当某个传输结束的时候,dma controller driver需要检查是否有依赖该传输的传输,如果有,则传输之。这个检查并传输的过程,可以借助该接口进行(dma controller driver只需调用即可,省很多事)

 

 

/*device tree有关的辅助接口*/

 

extern struct dma_chan *of_dma_simple_xlate(…)

两个接口可用于将client device node中有关dma的字段解析出来,并获取对应的dma channel

struct dma_chan *of_dma_xlate_by_chan_id(…)

 

 

 

4.4     编写一个dma controller driver的方法和步骤

编写一个dma controller driver的基本步骤包括(不考虑虚拟channel的情况):

1)         定义一个struct dma_device变量,并根据实际的硬件情况,填充其中的关键字段。

2)         根据controller支持的channel个数,为每个channel定义一个struct dma_chan变量,进行必要的初始化后,将每个channel都添加到struct dma_device变量的channels链表中。

3)         根据硬件特性,实现struct dma_device变量中必要的回调函数(device_alloc_chan_resources/device_free_chan_resourcesdevice_prep_dma_xxxdevice_configdevice_issue_pending等等)。

4)         调用dma_async_device_registerstruct dma_device变量注册到kernel中。

5)         client driver申请dma channel时(例如通过device tree中的dma节点获取),dmaengine core会调用dma controller driverdevice_alloc_chan_resources函数,controller driver需要在这个接口中channel的资源准备好。

6)         client driver配置某个dma channel时,dmaengine core会调用dma controller driverdevice_config函数,controller driver需要在这个函数中将client想配置的内容准备好,以便进行后续的传输。

7)         client driver开始一个传输之前,会把传输的信息通过dmaengine_prep_slave_xxx接口交给controller drivercontroller driver需要在对应的device_prep_dma_xxx回调中,将这些要传输的内容准备好,并返回给client driver一个传输描述符。

8)         然后,client driver会调用dmaengine_submit将该传输提交给controller driver,此时dmaengine会调用controller driver为每个传输描述符所提供的tx_submit回调函数,controller driver需要在这个函数中将描述符挂到该channel对应的传输队列中。

9)         client driver开始传输时,会调用dma_async_issue_pendingcontroller driver需要在对应的回调函数(device_issue_pending)中,依次将队列上所有的传输请求提交给硬件。

10)     等等。

5       DMA Buffer mapping framework

为了使用DMA Provider, 还有一个重要的话题, 就是为DMA Provider分配特殊Buffer. DMA Controller由于其硬件原因, 只能搬移数据到某些特定的Buffer, 这些Buffer称之DMA Buffer”.

 

本章节将会揭开DMA Buffer的神秘面纱.

5.1     物理地址、虚拟地址总线地址IOMMU

CPU做为一个控制器, 利用其地址总线访问硬件mem. CPU使用的地址称之为“虚拟地址”, 地址总线访问的地址称之为“物理地址”, 两者通过MMU进行映射.

 

在一颗处理器内部, 除了CPU, 还有很多片上外设, 这些外设有的是半智能, 它们有自己的地址总线, 并通过这个总线访问挂载在总线上的外设. 典型的如PCI控制器、NAND-Flash控制器, 甚至I2CSPI也算这种类型.

这种半智能的控制器使用的地址就叫做总线地址”. DMA Controller就是一个典型的半智能处理器. 因此DMA地址的真正学名应该叫总线地址.

 

我们用一张图示来进一步说明:

         中间的一列代表物理地址空间, 其中MMIO Space是其子集, 典型的如ARM处理器中的“外设寄存器”. RAM则是另一子集, 如板载的DDRAM.

         左侧一列是虚拟地址空间, Linux内核代码使用的就是虚拟地址. 虚拟地址到物理地址之间必须用MMU进行映射: 例如ioremap可以将MMIO空间的地址B映射到CPU空间的地址C, 这样CPU代码访问地址C即可; kmalloc()vmalloc()等类似接口可以将地址Y映射到地址X, CPU代码访问地址X即是访问DDRAM.

         右侧一列是总线地址空间,半智能外设通过总线地址访问挂载其上的Device, CPU无法直接访问总线地址A, 它能访问半智能外设MMIO地址, 然后由这些外设代替它访问地址A.

         CPU需要DDRAM, 半智能外设同样需要访问DDRAM

这些半智能外设有的可以直接访问DDRAM地址, 例如寄存器地址位于物理地址空间DMA控制器。

有的则不能直接访问DDRAM, 例如总线上挂载的”Device”, 它内部的DMA控制器就不能直接访问DDRAM. CPU访问RAM时需要通过MMU映射, ”Device”在访问DDRAM时同样可由IOMMU进行映射(例如把地址Y映射到地址Z. MMU可以把非连续的物理地址映射到连续的虚拟地址, IOMMU同样可以把非连续的物理地址映射到连续的总线地址.

 

需要注意的是, DMA控制器只是众多半智能外设其中的一个, 因此它所使用的DMA地址只能算总线地址的一个子集.

5.2     什么样的系统内存可以被DMA控制器访问到

前文说到DMA地址只是总线地址的一个子集, 那到底哪些内存可作为DMA地址?

         通过伙伴系统的接口(例如__get_free_page*())或者类似kmalloc() or kmem_cache_alloc()这样的通用内存分配的接口分配物理地址连续的内存.

         使用vmalloc() 分配的DMA buffer可以直接使用吗?最好不要这样,虽然强行使用可能没有问题,但是终究是比较麻烦。

         首先,vmalloc分配的page frame是不连续的,如果底层硬件需要物理内存连续,那么vmalloc分配的内存不能满足硬件要求。

         即便是底层DMA硬件支持scatter-gathervmalloc分配出来的内存仍然存在其他问题。我们设置给DMA控制器的必须是硬件地址, vmalloc分配的虚拟地址和对应的物理地址没有线性关系(kmalloc或者__get_free_page*这样的接口,其返回的虚拟地址和物理地址有一个固定偏移的关系),若要获取其物理地址,我们需要遍历页表才可以找到。

         在驱动中定义的全局变量可以用于DMA吗?如果编译到内核,那么全局变量位于内核的数据段或者bss段。在内核初始化的时候,会建立kernel image mapping,因此全局变量所占据的内存都是连续的,并且VAPA是有固定偏移的线性关系,因此可以用于DMA操作。不过,在定义这些全局变量的DMA buffer的时候,我们要小心的进行cacheline的对齐,并且要处理CPUDMA controller之间的操作同步,以避免cache coherence问题。

         如果驱动编译成模块会怎么样呢?这时候,驱动中的全局定义的DMA buffer不在内核的线性映射区域,其虚拟地址是在模块加载的时候,通过vmalloc分配,因此这时候如果DMA buffer如果大于一个page frame,那么实际上我们也是无法保证其底层物理地址的连续性,也无法保证VAPA的线性关系,这一点和编译到内核是不同的。

         通过kmap接口返回的内存可以做DMA buffer吗?也不行,其原理类似vmalloc,这里就不赘述了。

         块设备使用的I/O buffer和网络设备收发数据的buffer是如何确保其内存是可以进行DMA操作的呢?块设备I/O子系统和网络子系统在分配buffer的时候会确保这一点的。

5.3     DMA mapping APIs

DMA mapping其实由两个动作组成:

1)         分配一个DMA buffer

2)         DMA buffer mapDMA控制器可以访问的总线地址

如果我们已经用自己的方式分配了一块buffer, 那动作1就不需要了, 只需利用动作2将其转换为总线地址即可.

 

Linux系统中, 由于DMA硬件的差异, 不同的硬件需要的地址多有不同, 为了对使用者屏蔽这种差异, Linux内核提供了一个”DMA mapping framework”, 向使用者提供统一的API.

 

后文我们将详细介绍这些API. 在此之前, 让我们看看为什么强烈建议使用这些API去分配DMA Buffer.

5.3.1         为什么要使用DMA mapping API

虽然说在某些硬件平台上, 你可以用自己的方式分配DMA Buffer(例如有的硬件可以直接用伙伴系统分配的buffer的硬件地址作为DMA Buffer, 但不是所有的平台都可以这样做.

 

DMA Buffer, 说到底就是分配一块buffer然后获取其总线地址. 看似简单, 但如要写出一个在所有硬件平台上都能保证DMA操作安全、正确的驱动却可能比你想象的要困难.

         不同的硬件平台处理处理Cache一致性的问题不尽相同, 如果你的驱动不能全面考虑Cache一致性问题, 就可能发生memorycorrupt的事情.

         某些平台有专门的硬件来处理总线地址(例如IOMMU, 这会使得DMA操作变得更简单, 但也有可能更复杂.

         不同的DMA控制器有不同的寻址能力, 例如X86平台的某些DMA控制器只能访问低32M内存.

         有时候我们需要bounce buffer”, 我们需要DMA控制器从一块它不能访问的内存搬移数据(例如上例中高于32M的内存). 此时我们需要先把数据拷贝到DMA能访问的 bounce buffer, 然后开启搬移. 虽然这样做明显的会损害性能, 但此时我们别无选择.

         等等

所幸Kernel为我们提供了一个bus- and architecture-independent DMA mapping framework. 它为我们屏蔽了底层的硬件差异, 使用者只需用它提供的API来处理DMA Buffer, 即可保证兼容性和安全性.

 

值得一提的是, 在某些平台上, 内核代码提供了如下API:

unsigned long virt_to_bus(volatile void *address);

void *bus_to_virt(unsigned long address);

虽然说着两个API可以在虚拟地址和总线地址间进行转换, 但我们强烈建议不要使用它们, 而是用”DMA mapping API”. 假如硬件平台有IOMMU, 或者涉及到类似bounce buffer问题, 这两个API都不能正常工作.

5.3.2         DMA mapping APIs概述

在使用这些APIs, 你需要引用头文件 : linux/dma-mapping.h

内核系统定义了一个新的数据类型dma_addr_t, 用于代表DMA总线地址. 对于使用者来说, 它应该被当作黑盒子, 我们不用也不能去修改它内部细节. 使用者唯一能做的就是获取它, 然后把它设置给DMA控制器.

 

内核提供了两种DMA mapping:

         Coherent DMA mappings

我们一般用它来分配&映射一块DMA Buffer.

一般情况下, 我们会在驱动probe时调用它来获取Buffer, 直到驱动卸载时才释放它们, 因此这类Buffer的生命周期会比较长, 所以也称这类Buffer为静态Buffer当然你可以在需要是用此API分配Buffer, 不需要时立马释放这些Buffer.

另外, API分配的Buffer同时对CPUDMA控制可见与之对应, 后文我们将看到其它mapping方式获取的buffer, 在同一时刻, 只能对某一方可见, 因此这些Buffer一般位于cache-coherent区域, 相对来说, 它们比较昂贵.

         Streaming DMA mappings

当我们已经拥有一块Buffer, 可用此类API将其映射DMA Buffer.

此类API映射的Buffer在某一时刻, 只能由CPUDMA控制器的其中一方访问. 如果需交换访问角色, 我们需调用某些API.

在某些硬件平台上, 使用streaming mappings能显著提高性能(也许是因为cache一致性, streaming mappings方式可使CPU继续使用cache. 而且此种方式对buffer持有周期较短(需要时mapping, 不需要时unmapping, 可以让多个驱动都有机会去使用同一个buffer例如多个驱动可能都想把同一个外设寄存器映射为DMA Buffer, 因此内核开发者建议优先考虑使用streaming mappings方式. 不过这种方式在代码编写上更加复杂一点, 需要严格遵守一些规则.

 

好了, 下面让我们来详细看看这两类API.

5.3.3         Coherent DMA mappings API

Consistent DMA mapping有下面两种特点:

         持续使用该DMA buffer(不是一次性的),因此Consistent DMA总是(但也非绝对)在初始化的时候进行map,在shutdown的时候unmap

         CPUDMA controller在发起对DMA buffer的并行访问的时候不需要考虑cache的影响,也就是说不需要软件进行cache操作,CPUDMA controller都可以看到对方对DMA buffer的更新。实际上一致性DMA映射中的那个Consistent实际上可以称为coherent,即cache coherent

 

缺省情况下,coherent mask被设定为低32 bit0xFFFFFFFF),即便缺省值是OK了,我们也建议你通过接口在驱动中设定coherent mask(详见《DMA寻址限制》)

 

一般使用Consistent DMA mapping的场景包括:

1)         网卡驱动和网卡DMA控制器往往是通过一些内存中的描述符(形成环或者链)进行交互,这些保存描述符的memory一般采用Consistent DMA mapping

2)         SCSI硬件适配器上的DMA可以主存中的一些数据结构(mailbox command)进行交互,这些保存mailbox commandmemory一般采用Consistent DMA mapping

3)         有些外设有能力执行主存上的固件代码(microcode),这些保存microcode的主存一般采用Consistent DMA mapping

上面的这些例子有同样的特性:CPUmemory的修改可以立刻被device感知到,反之亦然。一致性映射可以保证这一点。

 

需要注意的是:一致性的DMA映射并不意味着不需要memory barrier这样的工具来保证memory orderCPU有可能为了性能而重排对consistent memory上内存访问指令。例如:如果在DMA consistent memory上有两个word,分别是word0word1,对于device一侧,必须保证word0先更新,然后才有对word1的更新,那么你需要这样写代码:

       desc->word0 = address;          wmb();          desc->word1 = DESC_VALID;

只有这样才能保证在所有的平台上,给设备驱动可以正常的工作。

 

此外,在有些平台上,修改了DMA Consistent buffer后,你的驱动可能需要flush write buffer,以便让device侧感知到memory的变化。这个动作类似在PCI桥中的flush write buffer的动作。

 

接下来详细看看API的细节.

dma_alloc_coherent

A driver can set up a coherent mapping with a call to dma_alloc_coherent:

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);

         This function handles both the allocation and the mapping of the buffer.

         The first two arguments are the device structure and the size of the buffer needed.

         The function returns the result of the DMA mapping in two places:

         The return value from the function is a kernel virtual address for the buffer, which may be used by the driver;

         the associated bus address, meanwhile, is returned in dma_handle.

         Allocation is handled in this function so that the buffer is placed in a location that works with DMA; usually the memory is just allocated with get_free_pages (but note that the size is in bytes, rather than an order value).

The flag argument is the usual GFP_ value describing how the memory is to be allocated; it should usually be GFP_KERNEL (usually) or GFP_ATOMIC (when running in atomic context).

         API分配的bufferPAGE_SIZE对齐的, 假如你的驱动不需要那么大的DMA buffer,那么可以选择下文介绍的dma_pool接口

dma_free_coherent

当驱动需要umap并释放dma buffer的时候,需要调用下面的接口:

void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);

         这个接口函数的devsize参数上面已经描述过了,而cpu_addrdma_handle这两个参数就是dma_alloc_coherent() 接口的那两个地址返回值。

         需要强调的一点就是:和dma_alloc_coherent不同,dma_free_coherent不能在中断上下文中调用。(因为在有些平台上,free DMA的操作会引发TLB维护的操作(从而引发cpu core之间的通信),如果关闭了IRQ会锁死在SMP IPI 的代码中)。

DMA pools

如果你的驱动需非常多的小的dma buffer,那么dma pool是最适合你的机制。这个概念类似kmem_cache__get_free_pages往往获取的是连续的page frame,而kmem_cache是批发了一大批page frame,然后自己“零售”。dma pool就是通过dma_alloc_coherent接口获取大块一致性的DMA内存,然后驱动可以调用dma_pool_alloc从那个大块DMA内存中分一个小块的dma buffer供自己使用。

 

相关API如下:

头文件 : linux/dmapool.h

APIs for DMA pool

Comment

struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);

A DMA pool must be created before use

         name is a name for the pool

         dev is your device structure

         size is the size of the buffers to be allocated from this pool

         align is the required hardware alignment for allocations from the pool (expressed in bytes)

         allocation is, if nonzero, a memory boundary that allocations should not exceed

If allocation is passed as 4096, for example, the buffers allocated from this pool do not cross 4-KB boundaries

void dma_pool_destroy(struct dma_pool *pool);

You should return all allocations to the pool before destroying it

void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);

分配一块DMA buffer:

         mem_flags is the usual set of GFP_ allocation flags

         If all goes well, a region of memory (of the size specified when the pool was created) is allocated and returned

         As with dma_alloc_coherent, the address of the resulting DMA buffer is returned as a kernel virtual address and stored in handle as a bus address

void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);

Returned unneeded buffers to the pool

5.3.4         Streaming DMA mappings API

流式DMA映射是一次性的,一般是需要进行DMA传输的时候才进行mapping,一旦DMA传输完成,就立刻ummap(除非你使用dma_sync_*的接口,下面会描述)。并且硬件可以为顺序化访问进行优化。

这里的streaming可以被认为是asynchronous,或者是不属于coherent memory范围的。

 

一般使用streaming DMA mapping的场景包括:

         网卡进行数据传输使用的DMA buffer

         文件系统中的各种数据buffer,这些buffer中的数据最终到读写到SCSI设备上去,一般而言,驱动会接受这些buffer,然后进行streaming DMA mapping,之后和SCSI设备上的DMA进行交互。

 

接下来我们看看细节.

DMA传输方向

When setting up a streaming mapping, you must tell the kernel in which direction the data is moving.

 

定义如下:

enum dma_data_direction

Comment

DMA_TO_DEVICE

从内存(dma buffer)到设备

DMA_FROM_DEVICE

从设备到内存(dma buffer

DMA_BIDIRECTIONAL

双向传输

DMA_NONE

与调试相关. 在驱动知道精确的DMA方向之前, 可将传输方向设定为此种状态.

如果最终此种类型传输数据, 会触发”Kernel Panic”

我们强烈要求驱动在知道DMA传输方向的适合,精确的指明是DMA_TO_DEVICE或者DMA_FROM_DEVICE.

 

如果你确实是不知道具体的操作方向,那么设定为DMA_BIDIRECTIONAL也是可以的,表示DMA操作可以执行任何一个方向的的数据搬移。你的平台需要保证这一点可以让DMA正常工作,当然,这有可能会引入一些性能上的额外开销。

 

除了潜在的平台相关的性能优化之外,精确地指定DMA操作方向还有另外一个优点就是方便调试。有些平台实际上在创建DMA mapping的时候,页表(指将bus地址映射到物理地址的页表)中有一个写权限布尔值,这个值非常类似于用户程序地址空间中的页保护。当DMA控制器硬件检测到违反权限设置时(这时候dma buffer设定的是MA_TO_DEVICE类型,实际上DMA controller只能是读dma buffer),这样的平台可以将错误写入内核日志,从而方便了debug

 

只有streaming mappings才会指明DMA操作方向,一致性DMA映射隐含的DMA操作方向是DMA_BIDIRECTIONAL。我们举一个streaming mappings的例子:在网卡驱动中,如果要发送数据,那么在map/umap的时候需要指明DMA_TO_DEVICE的操作方向,而在接受数据包的时候,map/umap需要指明DMA操作方向是DMA_FROM_DEVICE

 

接下来看看API的细节.

Map single buffer

APIs for single buffer mapping

Comment

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);

当你想把某一块buf (参数*buffer指向这块buf, CPU可访问的虚拟地址)转换为DMA可访问的地址时, 可以使用此API.

返回值是硬件可用的DMA地址; 但若出现错误, 返回值是NULL.

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);

当传输完成后(此时我一般会收到DMA传输完成中断), 可用此API解除mapping. 参数sizedirection都要与map时保持一致.

注意: 当一块bufmap, 只有DMA硬件可使用它, CPU不可再去读写它. 只有当这块bufunmap, CPU才可访问它.

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);

当收到DMA传输完成中断时, 假如CPU需要访问buf, 但访问完成后要继续DMA传输, 此时我们可以不用unmap, 而是使用此API.

调用此API, CPU就可访问这块buf. 不过此时DMA硬件就不能再访问它了.

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);

CPU访问结束后, 使用此API即可交换buf的角色. 此时只有DMA硬件可访问它, CPU不可再访问它.

 

在使用streaming DMA mappings, 有一些规则需要遵守:

         buf的使用必须与给定的传输方向相吻合.

         API说明里面的描述, CPUDMA硬件对buf的访问要遵守相关约定.

         DMA硬件在搬移buf的过程中, 不可unmap buf.

 

你也许会想为什么bufmap, CPU就不能再访问它了? 原因有两方面:

         首先考虑cache问题. 假设CPU想把某块数据传递给DMA, 它会先把这块数据写入mem buf. 但由于cache的存在, 这块数据可能暂存在cache, 还未写到mem. 当调用dma_map_single, 底层实现代码会强制flush, cache中的数据刷入mem. 如果在此之后CPU想直接更新buf的内容, 那这个“更新”可能仅仅存在于cache, 并未实际写入mem.

         其次, 考虑bounce buffer问题. 假设某个buf所处的地址超出了DMA硬件的寻址能力, 此时去map这块buf会出现什么情景? 有些平台上会直接失败, 但有些平台会在DMA硬件可寻址范围内创建一块bounce buffer, 然后把buf里面的内容copybounce buffer. 很显然, 这种情况下, 如果CPUmap后访问buf, 更新的只是”original buf”的内容.

 

提到bound buffer, 顺便说一下它也是为何要正确使用传输方向的原因:

         针对DMA_TO_DEVICE, 只会在dma_map_single时做copy动作.

         针对DMA_FROM_DEVICE, 只会在dma_unmap_single时做copy动作.

         针对DMA_BIDIRECTIONAL, 上述两种情况都会做copy动作.

不正确的使用传输方向”, 不仅会影响性能, 也可能出现错误.

Map single page

dma_map_single函数在进行DMA mapping的时候使用的是CPU指针(虚拟地址),这样就导致该函数有一个弊端:不能使用HIGHMEM memory进行mapping。鉴于此,map/unmap接口提供了另外一个类似的接口,这个接口不使用CPU指针,而是使用pagepage offset来进行DMA mapping

APIs for single page mapping

Comment

dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);

Map一个page.

虽然offsetsize允许你只map某个page的一部分, 但你应避免这样做.

如果map只包含了cache line的一部分, 这可能会导致 memory corruption

void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);

Unmmap一个page

Scatter/gather mappings

Scatter/gather mappings are a special type of streaming DMA mapping.

Suppose you have several buffers, all of which need to be transferred to or from the device. This situation can come about in several ways:

         from a readv or writev system call

         a clustered disk I/O request

         a list of pages in a mapped kernel I/O buffer.

You could simply map each buffer, in turn, and perform the required operation, but there are advantages to mapping the whole list at once.

 

The advantages are:

         Many devices can accept a scatterlist of array pointers and lengths, and transfer them all in one DMA operation; for example, “zero-copy” networking is easier if packets can be built in multiple pieces.

         Another reason to map scatterlists as a whole is to take advantage of systems that have IOMMU. On such systems, physically discontiguous pages can be assembled into a single, contiguous array from the device’s point of view.

This technique works only when the entries in the scatterlist are equal to the page size in length (except the first and last), but when it does work, it can turn multiple operations into a single DMA, and speed things up accordingly.

         Finally, if a bounce buffer must be used, it makes sense to coalesce the entire list into a single buffer (since it is being copied anyway).

 

So now you’re convinced that mapping of scatterlists is worthwhile in some situations.

Next, let we see some details.

 

scatterlist

struct scatterlist[]是一个体系架构相关的数据结构, 定义在<asm/scatterlist.h>.

它的主要作用是用于描述多块buffers. 你可以把它想象为一个数组或链表, 每个entry代表一块buffer.

 

不同的体系架构有不同的实现, 不过一般来说, 每个entry都有如下几个元素:

         struct page *page the buffer to be used in the scatter/gather operation.

         unsigned int length;

         unsigned int offset The length of that buffer and its offset within the page.

 

在使用下列API之前, 我们需要准备一个struct scatterlist[]数组, 并填充它的每个entry.

 

APIs

APIs for multi-buffer mapping

Comment

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)

To map a scatter/gather DMA operation, your driver should set the page, offset, and length fields in a struct scatterlist entry for each buffer to be transferred. Then call this API.

 

where nents is the number of scatterlist entries passed in.

 

The return value is the number of DMA buffers to transfer; it may be less than nents.

The bus address and length of each buffer are stored in the struct scatterlist entries, you can obtain them with following Macros.

dma_addr_t sg_dma_address(struct scatterlist *sg);

Returns the bus (DMA) address from this scatterlist entry

unsigned int sg_dma_len(struct scatterlist *sg);

Returns the length of this buffer.

Again, remember that the length of the buffers to transfer may be different from what was passed in to dma_map_sg

void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);

Once the transfer is complete, a scatter/gather mapping is unmapped with a call to dma_unmap_sg.

 

Note that nents must be the number of entries that you originally passed to dma_map_sg and not the number of DMA buffers the function returned to you.

 

 

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);

功能与map single buffer》里描述的一致.

 

错误处理

DMA地址空间在某些CPU架构上是有限的,因此分配并mapping可能会产生错误,我们可以通过下面的方法来判定是否发生了错误:

         检查是否dma_alloc_coherent() 返回了NULL或者dma_map_sg 返回0.

         检查dma_map_singledma_map_page返回了dma address(通过dma_mapping_error函数)

虽然并不是所有的DMA mapping实现都支持dma_mapping_error这个接口(调用dma_mapping_error函数实际上会调用底层dma_map_ops操作函数集中的mapping_error成员函数),但是调用它来进行出错处理仍然是一个好的做法。这样做的好处是可以确保DMA mapping代码在所有DMA实现中都能正常工作,而不需要依赖底层实现的细节。

示例代码如下:

   dma_addr_t dma_handle;

 

    dma_handle = dma_map_single(dev, addr, size, direction);

    if (dma_mapping_error(dev, dma_handle)) {

        goto map_error_handling;

    }

         当在mapping多个buf/page的时候,如果中间发生了mapping error,那么需要对那些已经mappedbuf/page进行unmap的操作。

5.3.5         DMA寻址限制API

你的设备有DMA寻址限制吗?不同的硬件平台有不同的配置方式,有的平台没有限制,外设可以访问系统内存的每一个Byte,有些则不可以。例如:系统总线有32bit,而你的设备通过DMA只能驱动低24位地址,在这种情况下,外设在发起DMA操作的时候,只能访问16M以下的系统内存。如果设备有DMA寻址的限制,那么驱动需要将这个限制通知到内核。如果驱动不通知内核,那么内核缺省情况下认为外设的DMA可以访问所有的系统总线的32 bit地址线。对于64 bit平台,情况类似,不再赘述。

 

是否有DMA寻址限制是和硬件设计相关,有时候标准总线协议也会规定这一点。例如:PCI-X规范规定,所有的PCI-X设备必须要支持64 bit的寻址。

 

如果有寻址限制,那么在该外设驱动的probe函数中,你需要询问内核,看看是否有DMA controller可以支持这个外设的寻址限制。虽然有缺省的寻址限制的设定,不过最好还是在probe函数中进行相关处理,至少这说明你已经为你的外设考虑过寻址限制这事了。

 

一旦确定了设备DMA寻址限制之后,我们可以通过下面的接口进行设定:

APIs for dma mask

Comment

int dma_set_mask_and_coherent(struct device *dev, u64 mask);

DMA mapcoherentstreaming这两种. 假设在这两种情况下, DMA的寻址限制一致, 则我们就可用此API进行设定. 假如两种情况下的寻址限制不一致, 那我们就要分别使用下列API.

int dma_set_mask(struct device *dev, u64 mask);

设定streaming类型的DMA地址掩码

int dma_set_coherent_mask(struct device *dev, u64 mask);

设定coherent类型的DMA地址掩码

如果调用这些接口返回0,则说明一切OK,从该设备到指定mask的内存的DMA操作是可以被系统支持的(包括DMA controllerbus layer等)。

如果返回值非0,那么说明这样的DMA寻址是不能正确完成的,如果强行这么做将会产生不可预知的后果。

另一个常见的场景是有64位寻址能力的设备。一般来说我们会首先尝试设定64位的地址掩码,但是这时候有可能会失败,从而我们会尝试将掩码降低为32位。内核之所以会在设定64位掩码的时候失败,这并不是因为平台不能进行64位寻址,而仅仅是因为32位寻址比64位寻址效率更高。例如,SPARC64 平台上,PCI SAC寻址比DAC寻址性能更好。

 

驱动必须检测返回值,如果返回值非0,那么建议修改mask或者不使用DMA

 

下面的代码描述了如何确定streaming类型DMA的地址掩码:

int using_dac;

 

if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {

    using_dac = 1;

} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {

    using_dac = 0;

} else {

    dev_warn(dev, "mydev: No suitable DMA available\n");

    goto ignore_this_device;

}

设定coherent 类型的DMA地址掩码也是类似的,不再赘述。

需要说明的是:coherent地址掩码总是等于或者小于streaming地址掩码,因此,一般来说,我们只要设定了streaming地址掩码成功了,那么使用同样的掩码或者小一些的掩码来设定coherent地址掩码总是会成功

5.3.6         PCI double-address cycle mappings API

Normally, the DMA support layer works with 32-bit bus addresses, possibly restricted by a specific device’s DMA mask.

The PCI bus, however, also supports a 64-bit addressing mode, the double-address cycle (DAC).

 

The generic DMA layer does not support this mode for a couple of reasons:

         the first of which being that it is a PCI-specific feature.

         Also, many implementations of DAC are buggy at best, and, because DAC is slower than a regular, 32-bit DMA.

 

Even so, there are applications where using DAC can be the right thing to do: if you have a device that is likely to be working with very large buffers placed in high memory, you may want to consider implementing DAC support.

 

This support is available only for the PCI bus, so PCI-specific routines must be used. To use DAC, your driver must include <linux/pci.h>.

APIs for DAC support

Comment

int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);

设定寻址限制.

You can use DAC addressing only if this call returns 0.

dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);

A special type (dma64_addr_t) is used for DAC mappings.

To establish one of these mappings, call this API.

 

DAC mappings, you will notice, can be made only from struct page pointers (they should live in high memory, after all, or there is no point in using them).

They must be created a single page at a time.

 

The direction argument is the PCI equivalent of the enum dma_data_direction used in the generic DMA layer; it should be PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE, or PCI_DMA_BIDIRECTIONAL.

No release api

DAC mappings require no external resources, so there is no need to explicitly release them after use.

 

 

void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);

功能与《map single buffer》里描述的一致.

void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);

功能与《map single buffer》里描述的一致.

5.4     DMA mapping的实现

上述API只是对上提供统一的接口. 对下它同样需要有人实现这些接口. DMA Mappings的实现也是一个很大的话题. 本文暂时不打算展开, 以后有机会再细说

5.4.1         cma

cma是其中一种实现. 代码路径是 : drivers/base/dma-contiguous.c

 

如需使用cma, 编译时需打开CONFIG_DMA_CMA=y.

 

bootargs, 我们可以通过”cma=xxxM”来指定为cma预留的内存大小. 解析这个参数的代码如下:

static int __init early_cma(char *p)

{

    pr_debug("%s(%s)\n", __func__, p);

    size_cmdline = memparse(p, &p);

    if (*p != '@')

        return 0;

    base_cmdline = memparse(p + 1, &p);

    if (*p != '-') {

        limit_cmdline = base_cmdline + size_cmdline;

        return 0;

    }

    limit_cmdline = memparse(p + 1, &p);

 

    return 0;

}

early_param("cma", early_cma);

5.5     其它注意事项

5.5.1         优化数据结构

在很多的平台上,dma_unmap_{single,page}()其实什么也没有做,是空函数。因此,跟踪映射的dma address及其长度基本上就是浪费内存空间。为了方便驱动工程师编写代码方便,我们提供了几个实用工具(宏定义),如果没有它们,驱动程序中将充分ifdef或者类似的一些“work around”。下面我们并不是一个个的介绍这些宏定义,而是给出一些示例代码,驱动工程师可以照葫芦画瓢。

 

DEFINE_DMA_UNMAP_{ADDR,LEN}。在DMA buffer数据结构中使用这个宏定义,具体例子如下:

         根据CONFIG_NEED_DMA_MAP_STATE的配置不同,DEFINE_DMA_UNMAP_{ADDR,LEN}可能是定义相关的dma address和长度的成员,也可能是空。

 

dma_unmap_{addr,len}_set()。使用该宏定义来赋值,具体例子如下:

 

dma_unmap_{addr,len}(),使用该宏来访问变量。

 

上面的这些代码基本是不需要解释你就会明白的了。另外,我们对于dma addresslen是分开处理的,因为在有些实现中,unmaping的操作仅仅需要dma address信息就够了。

5.5.2         平台移植需要注意的问题

如果你仅仅是驱动工程师,并不负责将linux迁移到某个cpu arch上去,那么后面的内容其实你可以忽略掉了。

  1. Struct scatterlist的需求

如果cpu arch支持IOMMU(包括软件模拟的IOMMU),那么你需要打开CONFIG_NEED_SG_DMA_LENGTH 这个内核选项。

  1. ARCH_DMA_MINALIGN

CPU体系结构相关的代码必须要要保证kmalloc分配的bufferDMA-safe的(kmalloc分配的buffer也是有可能用于DMA buffer),驱动和内核子系统的正确运行都是依赖这个条件的。如果一个cpu arch不是全面支持DMA-coherent的(例如硬件并不保证cpu cache中的数据等于main memory中的数据),那么必须定义ARCH_DMA_MINALIGN。而通过这个宏定义,kmalloc分配的buffer可以保证对齐在ARCH_DMA_MINALIGN上,从而保证了kmalloc分配的DMA Buffer不会和其他的buffer共享一个cacheline。想要了解具体的实例可以参考arch/arm/include/asm/cache.h

 

另外,请注意:ARCH_DMA_MINALIGN DMA buffer的对齐约束,你不需要担心CPU ARCH的数据对齐约束(例如,有些CPU arch要求有些数据对象需要64-bit对齐)。

6       DMA Buffer Share

在某个驱动模块中申请的DMA Buffer, 可以Share给其它驱动模块或其它子系统使用. 内核的dma-buf子系统提供了相关的管理框架.

7       DMA Test Guide

DMA Test Guide描述了如何利用内核自带的测试代码, 测试我们编写的DMA Controller driver是否完善.

8       RefLink

Link

Comment

https://www.kernel.org/doc/html/latest/driver-api/dmaengine/index.html

Kernel DMA Engine Doc

http://www.wowotech.net/linux_kenrel/dma_engine_overview.html

http://www.wowotech.net/linux_kenrel/dma_engine_api.html

http://www.wowotech.net/linux_kenrel/dma_controller_driver.html

wowotech DMA Engine Doc

https://www.kernel.org/doc/html/latest/driver-api/dmaengine/dmatest.html

Kernel DMA Test Guidance

 

 

https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html

Kernel dma-buf share Doc

 

posted @ 2020-12-13 18:02  johnliuxin  阅读(3640)  评论(1)    收藏  举报