linux io使用dma传输,linux驱动之DMA

一、前言

在 嵌入式Linux 的内核及驱动中,DMA 常常被人提起。我们也许清楚它的原理且很明白它非常重要,但在某种程度上,对于 DMA 的使用者来说,我们一般使用其接口,而很少去了解整个 DMA 的运作方式。那么本文就从头到尾,简单地说一下 DMA 吧

注意:本文对DMA的概念不做讲述,请各位读者自行了解DMA的概念。

二、正文

2.1 高端内存

2.1.1 内核虚拟内存

在了解 DMA 之前,我们需要先了解一下 高端内存 的相关内容。这有助于我们理解 DMA。

在 32位 的操作系统上,常常把程序的 0-3G(即PAGE_OFFSET) 作为 用户空间,而 3-4G 作为 内核空间。

e1b622234d13

用户空间与内核空间

每个进程的 用户空间 是完全独立、互不相干 的,因为 用户进程 各自有不同的 页表 。

但 内核空间 是由 内核负责映射,它并 不会随进程切换而切换,内核空间 的 虚拟地址 到 物理地址 映射是 所有进程 共享的。内核的 虚拟空间 独立其他程序。

我们重点关注一下 内核的虚拟内存空间,笔者将文档 Documentation/arm/memory.txt 中的 内核内存分布 列表如下(有所删减):

起始地址

结束地址

用处

0xffff8000

0xffffffff

用于 copy_user_page 和 clear_user_page

0xffff1000

0xffff7fff

保留,任何平台都不能使用该段 虚拟内存空间

0xffff0000

0xffff0fff

异常向量表 所在的内存区域

0xffc00000

0xffefffff

专用页面映射区(固定页面映射区),使用 fix_to_vir()可以获取该区域的逻辑地址

0xfee00000

0xfeffffff

PCI技术的IO映射空间

VMALLOC_START

VMALLOC_END

使用 vmlloac() 和 ioremap() 获取的地址都处于该内存段

PAGE_OFFSET

high_memory-1

内存直接映射区域(常规内存映射区域),该段内存地址也称为 逻辑地址 ,可以用于 DMA寻址

PKMAP_BASE

PAGE_OFFSET-1

持久映射区域,一般用于映射 高端内存

2.1.2 逻辑地址及高端内存

我们知道内核的虚拟地址空间为 3G-4G。在这个范围内,有一段大小为 896M 的虚拟内存是直接映射到 0-896M 的 物理地址空间。逻辑地址 与 物理地址 之间的转换是通过加上一个偏移 PAGE_OFFSET 来实现的。

按照笔者的理解:逻辑地址也是虚拟地址。其与一般虚拟地址不同的是,逻辑地址采用了线性映射,直接造成了逻辑地址与物理地址一一对应的关系。

举个例子我们可以按照一般情况来假设 PAGE_OFFSET 为 0xc0000000,那么内核虚拟空间就是 0xc0000000-0xffffffff 的 1G 空间,这会造成一个问题。如果物理地址的 0-1G 地址空间映射到虚拟地址的 0xc0000000-0xffffffff,高于物理地址 0-1G 的地址范围我们就无法访问。那么此时就产生了 高端内存。

通常在 32bit的内核 ,将 0-896M 的物理地址空间直接映射到 内存直接映射区域。将高于 896M 的物理地址映射到 高端内存区域,即 PKMAP_BASE ~ PAGE_OFFSET-1 这段空间。

在访问高于 896M 的 物理空间时,先从 高端内存区域 申请一段虚拟地址,并把需要访问的物理地址映射到该段虚拟地址,这样就可以访问高于 896M 大小的物理地址了。这种方式往往是使用 alloc_page 来获取内存。

按照上面的理解,当我们使用 vmlloc 时,也可以在 vmalloc区域 分配一段虚拟地址空间来映射到 物理高端内存,通过这种方式也可以访问 物理高端内存。

e1b622234d13

高端内存映射

总结访问高端内存的 2 种方式:

alloc_page

vmalloc

2.1.3 DMA寻址

在 ARM架构 的 Soc 上,可能会存在 DMA寻址问题,即 DMA 无法访问所有的物理地址空间,只能访问特定的物理地址空间。上面说了 内存直接映射区域 中可以用于 DMA,意思是说如果 Soc 只能访问特定的物理空间,该段特定物理空间常常位于 内存直接映射区域。

e1b622234d13

DMA区域和常规区域

2.2 总线地址

笔者在 蜗窝科技 中发现关于 总线地址 解释的好文章 Dynamic DMA mapping Guide。笔者将根据文章中的内容并按照自己的理解描述 总线地址,有兴趣的读者请访问原文。

总线地址 的使用笔者目前仅在 PCI技术 中见到过,所以笔者也将使用 PCI技术 并结合文章来描述。

2.2.1 MMIO

MM IO 即 内存映射I/O(Memory mapping I/O),有资料说它是 PCI规范 的一部分。但是按照笔者理解,在 ARM架构 的 Soc 中我们经常见到。

按照笔者理解,MMIO 可以理解为一段内存地址,我们通过这段 内存地址 就可以直接访问对应的 控制器 的 寄存器。举个例子,以 SPI控制器 为例,我们可以在一些 ARM架构Soc的 数据手册 中见到 SPI控制器 的 寄存器地址 在某一段内存地址中,我们可以直接通过访问这些地址来访问寄存器,这就是 MMIO。这样做的好处就是我们可以使用一套 汇编语言 即可访问 外围IO设备 的寄存器。

而在 x86 中,内存空间 和 IO空间 不是共享一段内存地址,所以需要使用另外一套 IO汇编语言 来访问 IO空间,这种方式也称为 port IO。

2.2.2 例子说明

1. 访问MMIO上的寄存器

假设某 Soc 的 PCI设备 在内存中有一段 MMIO空间,我们需要通过对 PCI设备 的寄存器进行访问才能相应的控制 PCI 设备。

在一般情况下,在驱动中访问 寄存器 往往也是通过 虚拟地址 进行访问的。通常是使用 ioremap 对一个 寄存器 地址进行映射,将其映射到 虚拟地址,我们通过访问这个 虚拟地址 即可访问 寄存器。

但在 PCI 设备中,PCI桥 将这些 PCI设备 和 系统(按笔者理解,此处可以理解为CPU) 连接在一起。PCI设备 会有基地址寄存器BAR(base address register),该寄存器表示 PCI设备 在 PCI总线 上的地址,即 总线地址。这样做之后,就不能直接通过访问虚拟地址来访问 PCI设备 的寄存器,需要使用 总线地址 才能访问到 PCI设备的MMIO。

如下图所示,红圈 代表的是访问 PCI设备 的过程:

CPU 并不能通过 总线地址A直接访问 PCI设备

PCI桥(PCI host bridge) 会在 MMIO 的 地址B(物理地址) 和 总线地址A 之间进行映射。

映射完成后,可以通过 物理地址B(处于MMIO) 访问 PCI设备,访问是会通过 PCI桥 对地址进行翻译。

驱动通过 ioremap 把 物理地址B 映射成 虚拟地址C

通过 虚拟地址C 访问 PCI总线地址A

e1b622234d13

MMIO访问

2. PCI总线访问内存

假设 PCI设备 支持 DMA,那么在传输数据的时候,我们需要一块 DMA buffer 用于 接收或者发送数据,这块 DMA buffer 存在于 RAM内存区域 中。但我们之前说了,PCI 在 MMIO区域 有规定的 总线地址,那么在 RAM内存区域 也是一样,PCI设备 无法通过方位 RAM内存区域中的虚拟地址 来 获取或存放数据。但与 MMIO 不同的是,MMIO 通过 PCI桥 将 虚拟地址 映射为 总线地址,RAM内存 则是通过 IOMMU 将 虚拟地址 映射为 总线地址。

上面说的 IOMMU 与 MMU 的工作机理类似,但不同的是 MMU 是映射 物理地址到虚拟地址,IOMMU 是映射 总线地址到物理地址 。

那么 PCI设备、DMA 和 CPU 是如何在 同一块内存 中进行交互的呢?

回答这个问题,我们需要清楚以下几点:

PCI设备 使用 DMA 传输 的是数据时需要使用的是 总线地址,即 DMA 是使用 总线地址 作为 源地址 或者 目的地址

DMA 传输数据时,*IOMMU 可以将 总线地址 转换 物理地址 。

DMA 传输完成后,CPU 使用 虚拟地址 访问该内存块。

其步骤如下:

内存块 由 CPU 创建,此时 CPU 获取到的是 内存块的虚拟地址X。

调用接口,将该内存块的 虚拟地址X 对应的 物理地址Y 映射为 总线地址Z 并返回给 CPU。

CPU 拿到的地址有 内存块 的 虚拟地址 和 总线地址,其 物理地址 对于 CPU 来说没有意义。

将 总线地址 写入 DMA 对应的寄存器,接着就可以执行相关的 DMA操作 了。

e1b622234d13

内存访问

PS:注意如果DMA的工作不是在PCI这种有规范的设备上,那么总线地址可以认为是普通内存地址

2.3 IOMMU

上面粗略讲了 IOMMU 在 DMA 工作过程中的应用,但其实 IOMMU 的用处不止这些,下面简单地描述 IOMMI 的另外一个作用。

我们都知道,在带有 MMU 的 Soc 上,对于程序来说,虚拟地址空间 是 可连续访问的。

因为 MMU 帮我们完成了从 虚拟地址空间 到 物理地址空间 的映射,这样做固然对于程序来说可以大大提高 内存管理 的效率,但同时也带来了 物理内存空间碎片化 的结果,找到 可连续访问 的 物理地址空间 的难度将大大增加。

而当 Soc 上的 设备 使用 DMA 访问内存时,需要 可连续访问 的 物理地址空间。

一般情况下,有 2 种办法可以让 DMA 访问 连续的物理地址空间:

在初始化 内核时,将 一部分物理空间 保留下来,不进行虚拟空间的映射。当使用到 DMA 的时候,将所需要的数据放置到 内存空间。再让DMA去访问这段 物理内存。这种方法简单直接,但会使得 内存空间的使用率并不高。

DMA 带上 MMU,让其在访问 虚拟空间 时 自动完成虚拟地址到物理地址的映射,此时 DMA 可以在不保留 连续物理地址空间 的情况下 访问连续的虚拟空间 。

ARM 使用了第二种方法,增加了一个特殊的 MMU,即 IOMMU。IOMMU 在 ARM架构 中称为 SMMU。SMMU 和 MMU 一样,在配置后可以进行 translation table walk。

总结 IOMMU 的 2 个用处:

映射总线地址到物理地址

提高物理内存的使用率

2.4 DMA控制器硬件

2.4.1 DMA寄存器

按照笔者理解,DMA控制器 一般都会包含以下寄存器:

DMA硬件描述符地址寄存器:存放 DMA描述符 的地址。

DMA配置寄存器:配置 DMA 的 burst 、 width 、 传输方向 等属性。

DMA使能寄存器:使能 DMA通道

DMA中断状态寄存器:获取 DMA 传输中断状态

DMA中断使能寄存器:使能 DMA 通道中断

2.4.2 DMA描述符

DMA控制器 在工作时需要读取 DMA描述符,这个描述符如下图所示:

e1b622234d13

image.png

一般情况下,它一共包含以下信息:

src_addr:DMA源地址

dst_addr:DMA目的地址

byte_count:传输数量

link:下一个描述符地址,如果为最后一个描述符则该值为某一个特定的值。

PS:上面的信息是指在一般情况,有些厂家会根据需要调整包含信息的内容。

需要使用 DMA控制器 进行传输时,我们需要在开辟一块内存,这块内存存放的就是 DMA描述符。当 DMA控制器 进行工作时,需要程序将 DMA描述符 的地址设置到 DMA硬件描述符地址寄存器 中。这样,当使能 DMA控制器 开始工作后,会读取 DMA硬件描述符地址寄存器 中的内存地址并读取相应的 DMA描述符,根据 DMA描述符 的所描述的地址跟大小进行传输。当完成一个 DMA描述符 的传输后会根据情况读取下一个 DMA描述符。

2.4.3 LLI

上面说过驱动会创建 内存块 用于存放 DMA描述符 ,这些 内存块 我们称之为 LLI。LLI 全称为 Link List Item,一般在驱动代码中都可以看到其结构体。以笔者的学习代码,其代码如下,可以看到有几个成员与图中所描绘的一致:

/*

* Hardware representation of the LLI

*

* The hardware will be fed the physical address of this structure,

* and read its content in order to start the transfer.

*/

struct sun6i_dma_lli {

u32 cfg;

u32 src;

u32 dst;

u32 len;

u32 para;

u32 p_lli_next;

/*

* This field is not used by the DMA controller, but will be

* used by the CPU to go through the list (mostly for dumping

* or freeing it).

*/

struct sun6i_dma_lli *v_lli_next;

};

2.4.4 DMA request

一般情况下,当 外设驱动 准备好传输数据及任务配置后,需要向 DMA控制器 发送 DRQ信号(DMA request)。所以需要有物理线连接 DMA控制器 和 外设,这条物理线称为 DMA request line。。发送这个信号往往是向 DMA配置寄存器 中写入 DRQ值。每种 外设驱动 都有自己的 DRQ值,当启动 DMA传输 后,会查询 DRQ值,如果当前的 DRQ值 能够进行传输,则启动 DMA传输。

有时 DMA request (line) 又称为 DMA port。

2.4.5 DMA通道

DMA控制器 可以 同时 进行的传输个数是有限的,每一个传输都需要使用到 DMA物理通道。DMA物理通道 的数量决定了 DMA控制器 能够同时传输的任务量。

在软件上,DMA控制器 会为 外设 分配一个 DMA虚拟通道,这个虚拟通道是根据 DMA request信号 来区分。

通常来讲,DMA物理通道 是 DMA控制器 提供的服务,外设通过申请 DMA通道 ,如果申请成功将返回 DMA虚拟通道,该 DMA虚拟通道 绑定了一个 DMA物理通道。这样 DMA控制器 为 外设 提供了 DMA服务,当 外设 需要传输数据时,对 虚拟通道 进行操作即可,但本质上的工作由 物理通道 来完成。

看完了这些以后,对于 DMA硬件及其工作流程 都应该有了一定的了解。

2.5 DMA驱动讲解

2.5.1 DMA设备树

下面 2 段设备树代码例程是关于 DMA控制器 和 DMA客户端 的

/* DMA控制器设备树节点 */

dma: dma-controller@01c02000 {

compatible = "allwinner,sun8i-v3s-dma";

reg = <0x01c02000 0x1000>;

interrupts = ;

clocks = ;

resets = ;

#dma-cells = <1>;

};

/* DMA客户端设备树节点,以SPI为例 */

spi2: spi@01c6a000 {

compatible = "allwinner,sun6i-a31-spi";

reg = <0x01c6a000 0x1000>;

interrupts = <0 67 4>;

clocks = , ;

clock-names = "ahb", "mod";

dmas = , ;

dma-names = "rx", "tx";

resets = ;

};

DMA控制器 的设备树节点属性我们这里不多讲,有兴趣的读者可以阅读内核文档 。

DMA客户端 我们主要关注下面 2 个属性:

dmas:该属性一共有 2 个,第一个 DMA控制器 的 节点名,第二个为该驱动的 DMA port

dma-names:该属性用于 dma_request_chan 接口,传入该接口的参数中的 name参数 需要与设备树中的 dma-names 一致,这样才能申请到 DMA通道

PS:DMA port 在每个Soc的datasheet中有说明,使用DMA时需要将DMA port设置到DMA配置寄存器中。DMA port一般如下图所示:

e1b622234d13

image.png

更多详情可以在文档 Documentation/devicetree/bindings/dma/dma.txt 中查看

2.5.2 dmaengine框架

在驱动中,有多种使用 DMA 的方式及接口框架,本文将重点说明 dmaengine框架 的代码及使用。下面按照使用流程进行描述。

重要事情说三遍:

各个平台的实现可能有所不同,代码过程仅供参考,重在学习流程及机制,下面将按照笔者手中的学习代码为例!!!!

各个平台的实现可能有所不同,代码过程仅供参考,重在学习流程及机制,下面将按照笔者手中的学习代码为例!!!!

各个平台的实现可能有所不同,代码过程仅供参考࿰

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值