DMA简介及框架简要分析

1、DMA由来

DMA(Direct Memory Access,直接存储器访问)。

在DMA出现之前,CPU与外设之间的数据传送方式有程序传送方式中断传送方式。CPU是通过系统总线与其他部件连接并进行数据传输。

1.1 程序传送方式

程序传送方式是指直接在程序控制下进行数据的输入/输出操作。分为无条件传送方式和查询(条件传送方式)两种。

1.1.1 无条件传送方式

微机系统中的一些简单的外设,如开关、继电器、数码管、发光二极管等,在它们工作时,可以认为输入设备已随时准备好向CPU提供数据,而输出设备也随时准备好接收CPU送来的数据,这样,在CPU需要同外设交换信息时,就能够用IN或OUT指令直接对这些外设进行输入/输出操作。由于在这种方式下CPU对外设进行输入/输出操作时无需考虑外设的状态,故称之为无条件传送方式。

1.1.2 查询(有条件)传送方式

查询传送也称为条件传送,是指在执行输入指令(IN)或输出指令(OUT)前,要先查询相应设备的状态,当输入设备处于准备好状态、输出设备处于空闲状态时,CPU才执行输入/输出指令与外设交换信息。为此,接口电路中既要有数据端口,还要有状态端口。

1.2 中断传送方式

中断传送方式是指当外设需要与CPU进行信息交换时,由外设向CPU发出请求信号,使CPU暂停正在执行的程序,转而去执行数据输入/输出操作,待数据传送结束后,CPU再继续执行被暂停的程序。
以上两种方式,均由CPU控制数据传输,不同的是程序传送方式由CPU来查询外设状态,CPU处于主动地位,而外设处于被动地位。这就是常说的----对外设的轮询,效率低。而中断传送方式则是外设主动向CPU发生请求,等候CPU处理,在没有发出请求时,CPU和外设都可以独立进行各自的工作。 需要进行断点和现场的保护和恢复,浪费了很多CPU的时间,适合少量数据的传送。

1.3 DMA原理

DMA的出现就是为了解决批量数据的输入/输出问题。DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。这样数据的传送速度就取决于存储器和外设的工作速度。

通常系统总线是由CPU管理的,在DMA方式时,就希望CPU把这些总线让出来,即CPU连到这些总线上的线处于第三态(高阻状态),而由DMA控制器接管,控制传送的字节数,判断DMA是否结束,以及发出DMA结束信号。

因此DMA控制器必须有以下功能:

1、能向CPU发出系统保持(HOLD)信号,提出总线接管请求;
2、当CPU发出允许接管信号后,负责对
总线的控制,进入DMA方式;
3、能对存储器寻址及能修改地址指针,实现对内存的读写;
4、能决定本次DMA传送的字节数,判断DMA传送是否结束。
5、发出DMA结束信号,使CPU恢复正常工作状态。

Linux DMA整体轮廓

linux内核dma框架
DMA工作流程示意DMA传输将从一个地址空间复制到另外一个地址空间。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实行和完成。 典型例子—移动一个外部内存的区块到芯片内部更快的内存区。

对于实现DMA传输,它是由DMA控制器直接掌管总线(地址总线、数据总线和控制总线),因此,存在一个总线控制权转移问题

DMA传输开始前:    CPU------>DMA控制器
DMA传输结束后: DMA控制器------>CPU

一个完整的DMA传输过程必须经历DMA请求、DMA响应、DMA传输、DMA结束4个步骤。

DMA方式是一种完全由硬件进行组信息传送的控制方式,具有中断方式的优点,即在数据准备阶段,CPU与外设并行工作。

2、DMA的传送过程

DMA的数据传送分为预处理、数据传送和后处理3个阶段。

2.1 预处理

由CPU完成一些必要的准备工作。首先,CPU执行几条I/O指令,用以测试I/O设备状态,向DMA控制器的有关寄存器置初值,设置传送方向、启动该设备等。然后,CPU继续执行原来的程序,直到I/O设备准备好发送的数据(输入情况)或接受的数据(输出情况)时,I/O设备向DMA控制器发送DMA请求,再由DMA控制器向CPU发送总线请求(统称为DMA请求),用以传输数据。

2.2 数据传送

DMA的数据传输可以以单字节(或字)为基本单位,对于以数据块为单位的传送(如银盘),DMA占用总线后的数据输入和输出操作都是通过循环来实现。需要特别之处的是,这一循环也是由DMA控制器(而不是通过CPU执行程序)实现的,即数据传送阶段是完全由DMA(硬件)来控制的。

2.3 后处理

DMA控制器向CPU发送中断请求,CPU执行中断服务程序做DMA结束处理,包括检验送入主存的数据是否正确,测试传送过程中是否出错(错误则转入诊断程序)和决定是否继续使用DMA传送其他数据块等。

3、DMA的传输模式

通常来讲,DMA 支持以下四种传输模式:(这里的外设一般指外设的数据寄存器或者FIFO的地址,存储器一般是指片内 SRAM、外部存储器、片内 Flash 等等)

3.1 外设到存储器传输

将外设数据寄存器的内容,转移到特定的目标地址,比如使用DMA将摄像头采集的图像数据转移到显存的位置,实现液晶屏幕实时显示摄像头采集的图像;

3.2 存储器到外设传输

将目的地址的内容转移至外设的数据寄存器中,常用于外设的发送通信,如串口DMA发送大量数据;

3.3 外设到外设传输

这些有device参与的传输(MEM2DEV、DEV2MEM、DEV2DEV)为Slave-DMA传输。而另一种memory到memory的传输,被称为Async TX。

3.4 存储器到存储器传输

把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于 C 语言内存拷贝函数 memcpy,利用 DMA 传输可以达到更高的传输效率。

4、DMA框架

DMA框架主要分为三个部分:DMA provider、DMA consumer以及DMA核心层。

4.1 DMA provider

主要包括了各种SOC芯片的DMA控制器驱动代码,通常是由SOC芯片厂商提供;

4.2 DMA consumer

“人”如其名,主要是用于消费(申请)DMA请求,对于SPI、I2C等控制器,SOC芯片厂商会在其驱动代码中去申请DMA通道,使能DMA传输;

4.3 DMA CORE

而DMA框架中,最为核心的一环,便是DMA核心层,它通过提供公共接口函数,减少了冗余代码之外,使得DMA consumer无须关心底层的变化,也就是说DMA consumer侧的驱动,只要使用了DMA核心层的API,无论你是移植到哪个平台,都能轻松实现DMA传输的功能。

DMA框架的大部分代码都是由Linux内核源码维护者以及芯片厂商进行维护,一般是不需要我们进行修改的。对于DMA控制器驱动,主要是由SOC芯片厂商编写,而DMA consumer中,涉及外设的主要是总线控制器驱动,这些驱动同样是由SOC芯片厂商提供,而存储器到存储器的传输模式,通常我们都使用C库函数memcpy、memset,并不会真的去实现使用DMA进行数据拷贝。

4.4 Linux DMA源码目录

针对Linux DMA框架,我们可以把内核源码/driver/dma目录分为三个部分:DMA provider、DMA核心层以及DMA consumer。由于DMA consumer涉及到其它的驱动框架,这里就不一一列举。

4.4.1 DMA provider

目录/文件描述
drivers/dma/bestcomm/*Bestcomm DMA 控制器驱动源码
drivers/dma/dw/*Synopsys AHB DMA 控制器驱动源码
drivers/dma/…

表格中,列举了内核源码中部分目录以及文件的路径和说明,每个文件夹/文件都对应一个SOC芯片的DMA控制器驱动。这部分代码与硬件息息相关。每个文件都定义了对应的DMA控制器的寄存器地址,从设备树解析对应的节点(若支持设备树)、注册DMA 控制器驱动以及配置DMA传输等操作。

4.4.2 DMA core

文件描述
drivers/dma/dmaengine.cDMA框架的核心代码,提供硬件无关的接口函数
drivers/dma/of-dma.cDMA框架设备树相关的代码
include/linux/dmaengine.hDMA框架数据结构以及相关接口的头文件

DMA核心层,用于提供与硬件无关的接口函数。对于上层的DMA consumer,它提供了如何请求DMA传输的数据结构和接口;而对于下层的DMA 控制器驱动,则提供如何往Linux内核注册一个DMA设备的接口API,从而更好地实现DMA框架的分层。

4.5 dmaengine.c 简要分析

Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API(如下图所示),这种API就是Async TX API(以async_开头,例如async_memcpy、async_memset、async_xor等)。
请添加图片描述
最后,因为memory到memory的DMA传输有了比较简洁的API,没必要直接使用
dma engine提供的API,最后就导致dma engine所提供的API就特指为Slave-DMA API
(把mem2mem剔除了)。

这里主要介绍dma engine为consumers提供的功能和API,因此就不再涉及
Async TX API了。

4.5.1 DMAENGINE使用步骤

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

1)申请一个DMA channel。

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

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

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

5)等待传输(transaction)结束。

然后,重复3~5即可。

4.5.1.1 申请DMA channel

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

DMA channel(在kernel中由“struct dma_chan”数据结构表示)由provider(或者是DMA controller)提供,被consumer(或者client)使用。对consumer来说,不需要关心该数据结构的具体内容(我们会在dmaengine provider的介绍中在详细介绍)。

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

/* 从dma_device_list上找到一个合适的dma控制器,并从控制器上获取一个dma channel */
struct dma_chan *dma_request_chan(struct device *dev, const char *name);

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

void dma_release_channel(struct dma_chan *chan);
4.5.1.2 配置DMA channel参数

driver申请到一个为自己使用的DMA channel之后,需要根据自身的实际情况,以及DMA controller的能力,对该channel进行一些配置。可配置的内容由struct dma_slave_config数据结构表示(具体可参考4.1小节的介绍)。driver将它们填充到一个struct dma_slave_config变量中后,可以调用如下API将这些信息告诉给DMA controller:

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

4.5.1.3 获取传输描述符(tx descriptor)

DMA传输属于异步传输,在启动传输之前,slave driver需要将此次传输的一些信息(例如src/dst的buffer、传输的方向等)提交给dma engine(本质上是dma controller driver),dma engine确认okay后,返回一个描述符(由struct dma_async_tx_descriptor抽象)。此后,slave driver就可以以该描述符为单位,控制并跟踪此次传输。
struct dma_async_tx_descriptor数据结构可参考4.2小节的介绍。根据传输模式的不同,
slave driver可以使用下面三个API获取传输描述符:

/dmaengine_prep_slave_sg用于在“scatter gather buffers”列表和总线设备之间进行DMA传输/

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);
 

/* dmaengine_prep_dma_cyclic常用于音频等场景中,在进行一定长度的dma传输(buf_addr&buf_len)的过程中,每传输一定的byte(period_len),就会调用一次传输完成的回调函数*/
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);

/*dmaengine_prep_interleaved_dma可进行不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中。*/    
struct dma_async_tx_descriptor* dmaengine_prep_interleaved_dma(
        struct dma_chan *chan, struct dma_interleaved_template *xt,
        unsigned long flags);
4.5.1.4 启动传输

通过3.3章节介绍的API获取传输描述符之后,client driver可以通过dmaengine_submit接口将该描述符放到传输队列上,然后调用dma_async_issue_pending接口,启动传输。

/*参数为传输描述符指针,返回一个唯一识别该描述符的cookie,用于后续的跟踪、监控。*/
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)

void dma_async_issue_pending(struct dma_chan *chan);
4.5.1.5 等待传输结束

传输请求被提交之后,client driver可以通过回调函数获取传输完成的消息,当然,也可以通过dma_async_is_tx_complete等API,测试传输是否完成。不再详细说明了。

最后,如果等不及了,也可以使用dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx等API,暂停、终止传输,具体请参考kernel document[1]以及source code。

4.6 重要数据结构说明

4.6.1 struct dma_slave_config

/* include/linux/dmaengine.h */
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;
        u32 dst_maxburst;
        bool device_fc;
        unsigned int slave_id;
};

参数说明:
direction,指明传输的方向,包括(具体可参考enum dma_transfer_direction的定义和注释):
DMA_MEM_TO_MEM,memory到memory的传输;
DMA_MEM_TO_DEV,memory到设备的传输;
DMA_DEV_TO_MEM,设备到memory的传输;
DMA_DEV_TO_DEV,设备到设备的传输。
controller不一定支持所有的DMA传输方向,具体要看provider的实现。
MEM to MEM的传输,一般不会直接使用dma engine提供的API。

src_addr,传输方向是dev2mem或者dev2dev时,读取数据的位置(通常是固定的FIFO地址)。对mem2dev类型的channel,不需配置该参数(每次传输的时候会指定);

dst_addr,传输方向是mem2dev或者dev2dev时,写入数据的位置(通常是固定的FIFO地址)。对dev2mem类型的channel,不需配置该参数(每次传输的时候会指定);

src_addr_width、dst_addr_width,src/dst地址的宽度,包括1、2、3、4、8、16、32、64(bytes)等(具体可参考enum dma_slave_buswidth 的定义)。

src_maxburst、dst_maxburst,src/dst最大可传输的burst size(可参考[2]中有关burst size的介绍),单位是src_addr_width/dst_addr_width(注意,不是byte)。

device_fc,当外设是Flow Controller(流控制器)的时候,需要将该字段设置为true。CPU中有关DMA和外部设备之间连接方式的设计中,决定DMA传输是否结束的模块,称作flow controller,DMA controller或者外部设备,都可以作为flow controller,具体要看外设和DMA controller的设计原理、信号连接方式等,不在详细说明(感兴趣的同学可参考[4]中的介绍)。(*)

slave_id,外部设备通过slave_id告诉dma controller自己是谁(一般和某个request line对应)。很多dma controller并不区分slave,只要给它src、dst、len等信息,它就可以进行传输,因此slave_id可以忽略。而有些controller,必须清晰地知道此次传输的对象是哪个外设,就必须要提供slave_id了(至于怎么提供,可dma controller的硬件以及驱动有关,要具体场景具体对待)。

4.6.2 struct dma_async_tx_descriptor

传输描述符用于描述一次DMA传输(类似于一个文件句柄)。client driver将自己的传输请求通过3.3中介绍的API提交给dma controller driver后,controller driver会返回给client driver一个描述符。
client driver获取描述符后,可以以它为单位,进行后续的操作(启动传输、等待传输完成、等等)。也可以将自己的回调函数通过描述符提供给controller driver。
传输描述符的定义如下:

struct dma_async_tx_descriptor {
         dma_cookie_t cookie;
         enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
         dma_addr_t phys;
         struct dma_chan *chan;
         dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
         int (*desc_free)(struct dma_async_tx_descriptor *tx);
         dma_async_tx_callback callback;
         void *callback_param;
         struct dmaengine_unmap_data *unmap;
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
         struct dma_async_tx_descriptor *next;
         struct dma_async_tx_descriptor *parent;
         spinlock_t lock;
#endif
}

参数说明:
cookie,一个整型数,用于追踪本次传输。一般情况下,dma controller driver会在内部维护一个递增的number,每当client获取传输描述的时候(参考3.3中的介绍),都会将该number赋予cookie,然后加一。

flags, DMA_CTRL_开头的标记,包括:

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

DMA_CTRL_ACK,如果该flag为0,表明暂时不能被重复使用。

phys,该描述符的物理地址

chan,对应的dma channel。

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

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

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

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

  • 42
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值