Linux之DMA驱动详解(1)

一、DMA 简介

     CPU 和内存是计算机组成中都是不可或缺的部分。和  CPU 相比,内存是非常慢速的拖油瓶,CPU 的速度被内存限制,同时又不得不等待内存处理而无法去处理其他事件。所以使用 CPU 搬运内存数据,是非常浪费资源的。因此专门用于搬运内存数据的器件 DMA 应运而生。
        DMA 是 Direct Memory Access 的缩写,也就是直接内存读写,所谓的直接,也就是内存到内存,不通过 CPU 。 DMA 支持内存到外设、外设到内存、内存到内存的数据交互,必要时节省很多 CPU 资源。当然,虽然 DMA 几乎不占用 CPU ,但还是会占用系统总线的。

二、Linux 内核DMA驱动框架

dma 子系统的框架图:

dma slave driver: 这个是指使用 dma 功能的设备驱动,比如 支持DMA功能的SPI设备,那么这个SPI设备在驱动初始化时,会写 dma slave driver 的代码,这样就可以使用DMA功能。具体如何写,后面在介绍代码;

 dma core : 这就是Linux 内核驱动的套路,把通用的部分代码整合成驱动框架,驱动开发工程师直接调用接口,屏蔽dma 控制器驱动的代码。不展开这个套路了。

从内核代码Makefile中,可以看出包含以下驱动:

dmaengine:

是DMA子系统的核心,为DMA Device Driver提供DMA设备注册的API,为DMA调用者(Device Driver)提供屏蔽DMA设备实现细节的统一接口API;

virt-dma:

为DMA子系统提供虚拟DMA channel的支持;

of-dma:

为DMA子系统提供设备树描述DMA信息传入的支持;

acpi-dma:

为DMA子系统提供ACPI 表DMA信息传入的支持;

三、总线地址、物理地址、虚拟地址 的关系介绍

在具体分析DMA驱动源码之前,先熟悉上面三个地址之间的硬件转换方式。

内核通常使用的地址是虚拟地址。我们调用kmalloc()、vmalloc()或者类似的接口返回的地址都是虚拟地址,保存在"void *"的变量中。

虚拟内存系统(TLB、页表等)将虚拟地址(程序角度)翻译成物理地址(CPU角度),物理地址保存在“phys_addr_t”或“resource_size_t”的变量中。对于一个硬件设备上的寄存器等设备资源,内核是按照物理地址来管理的。通过/proc/iomem,你可以看到这些和设备IO 相关的物理地址。当然,驱动并不能直接使用这些物理地址,必须首先通过ioremap()接口将这些物理地址映射到内核虚拟地址空间上去。

I/O设备使用第三种地址:“总线地址”。如果设备在MMIO地址空间中有若干的寄存器,或者该设备足够的智能,它可以通过DMA执行读写系统内存的操作,这些情况下,设备使用的地址就是总线地址。在某些系统中,总线地址与CPU物理地址相同,但一般来说它们不是。iommus和host bridge可以在物理地址和总线地址之间进行映射。

从设备的角度来看,DMA控制器使用总线地址空间,不过可能仅限于总线空间的一个子集。例如:即便是一个系统支持64位地址内存和64 位地址的PCI bar,但是DMA可以不使用全部的64 bit地址,通过IOMMU的映射,PCI设备上的DMA可以只使用32位DMA地址。

我们用下面这样的系统结构来说明各种地址的概念:

 

在PCI设备枚举(初始化)过程中,内核了解了所有的IO device及其对应的MMIO地址空间(MMIO是物理地址空间的子集),并且也了解了是PCI主桥设备将这些PCI device和系统连接在一起。PCI设备会有BAR(base address register),表示自己在PCI总线上的地址,CPU并不能通过总线地址A(位于BAR范围内)直接访问总线上的PCI设备,PCI host bridge会在MMIO(即物理地址)和总线地址之间进行mapping。因此,对于CPU,它实际上是可以通过B地址(位于MMIO地址空间)访问PCI设备(反正PCI host bridge会进行翻译)。地址B的信息保存在struct resource变量中,并可以通过/proc/iomem开放给用户空间。对于驱动程序,它往往是通过ioremap()把物理地址B映射成虚拟地址C,这时候,驱动程序就可以通过ioread32(C)来访问PCI总线上的地址A了。

如果PCI设备支持DMA,那么在驱动中我们可以通过kmalloc或者其他类似接口分配一个DMA buffer,并且返回了虚拟地址X,MMU将X地址映射成了物理地址Y,从而定位了DMA buffer在系统内存中的位置。因此,驱动可以通过访问地址X来操作DMA buffer,但是PCI 设备并不能通过X地址来访问DMA buffer,因为MMU对设备不可见,而且系统内存所在的系统总线和PCI总线属于不同的地址空间。

在一些简单的系统中,设备可以通过DMA直接访问物理地址Y,但是在大多数的系统中,有一个IOMMU的硬件block用来将DMA可访问的总线地址翻译成物理地址,也就是把上图中的地址Z翻译成Y。理解了这些底层硬件,你也就知道类似dma_map_single这样的DMA API是在做什么了。驱动在调用dma_map_single这样的接口函数的时候会传递一个虚拟地址X,在这个函数中会设定IOMMU的页表,将地址X映射到Z,并且将返回z这个总线地址。驱动可以把Z这个总线地址设定到设备上的DMA相关的寄存器中。这样,当设备发起对地址Z开始的DMA操作的时候,IOMMU可以进行地址映射,并将DMA操作定位到Y地址开始的DMA buffer。

补充:IOMMU

我们都知道,在带有 MMU 的 Soc 上,对于程序来说,虚拟地址空间 是 可连续访问的
因为 MMU 帮我们完成了从 虚拟地址空间 到 物理地址空间 的映射,这样做固然对于程序来说可以大大提高 内存管理 的效率,但同时也带来了 物理内存空间碎片化 的结果,找到 可连续访问 的 物理地址空间 的难度将大大增加。
而当 Soc 上的 设备 使用 DMA 访问内存时,需要 可连续访问 的 物理地址空间

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

  1. 在初始化 内核时,将 一部分物理空间 保留下来,不进行虚拟空间的映射。当使用到 DMA 的时候,将所需要的数据放置到 内存空间。再让DMA去访问这段 物理内存。这种方法简单直接,但会使得 内存空间的使用率并不高
  2. DMA 带上 MMU,让其在访问 虚拟空间 时 自动完成虚拟地址到物理地址的映射,此时 DMA 可以在不保留 连续物理地址空间 的情况下 访问连续的虚拟空间 。

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

总结 IOMMU 的 2 个用处:

  1. 映射总线地址到物理地址
  2. 提高物理内存的使用率

根据上面的描述我们可以得出这样的结论:Linux可以使用动态DMA 映射(dynamic DMA mapping)的方法,当然,这需要一些来自驱动的协助。所谓动态DMA 映射是指只有在使用的时候,才建立DMA buffer虚拟地址到总线地址的映射,一旦DMA传输完毕,就将之前建立的映射关系销毁。

虽然上面的例子使用IOMMU为例描述,不过本文随后描述的API也可以在没有IOMMU硬件的平台上运行。

顺便说明一点:DMA API适用于各种CPU arch,各种总线类型,DMA mapping framework已经屏蔽了底层硬件的细节。对于驱动工程师而言,你应该使用通用的DMA API

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值