Linux设备驱动中DMA接口的使用

-v0.1 2018.3.13 Sherlock init Westford

本文试图讲清楚linux里的DMA接口使用时的一些基本概念。阅读本文的时候,可以先看看
内核里的Documentation/DMA*(DMA相关的一些文件), 其实这里面讲的已经很清楚了,另外
还可以看看知乎上的一篇文章:https://zhuanlan.zhihu/p/25999484, 这篇文章对
Linux里关于地址空间的各个概念有很好的讲解。

  1. DMA的概念

    DMA就是说设备可以直接进行内存的读写,不需要CPU的参与。当然,在设备启动DMA
    进行读写之前,你需要通过CPU把读写的地址,大小等一些信息配置给设备。设备完成
    数据读写后可以发一个中断告诉CPU,之后CPU就可以做相关的操作。但是,CPU要把
    什么地址告诉设备呢?

  2. 几个地址的概念

    在kernel/Documentation/DMA-API-HOWTO.txt里讲的比较清楚,它里面有一副图是
    这样的:

               CPU                  CPU                  Bus
             Virtual              Physical             Address
             Address              Address               Space
              Space                Space

            +-------+             +------+             +------+
            |       |             |MMIO  |   Offset    |      |
            |       |  Virtual    |Space |   applied   |      |
          C +-------+ --------> B +------+ ----------> +------+ A
            |       |  mapping    |      |   by host   |      |
  +-----+   |       |             |      |   bridge    |      |   +--------+
  |     |   |       |             +------+             |      |   |        |
  | CPU |   |       |             | RAM  |             |      |   | Device |
  |     |   |       |             |      |             |      |   |        |
  +-----+   +-------+             +------+             +------+   +--------+
            |       |  Virtual    |Buffer|   Mapping   |      |
          X +-------+ --------> Y +------+ <---------- +------+ Z
            |       |  mapping    | RAM  |   by IOMMU
            |       |             |      |
            |       |             |      |
            +-------+             +------+

这里有一堆地址概念,不同地址有不用的作用。硬件可以把物理的内存和设备的寄存器
空间映射(MMIO)到CPU物理地址空间, 这里的映射和kernel没有关系,我们可以认为固件
已经为我们做好了,代码里里直接访问对应的物理地址就可以了。CPU通过CPU虚拟地址
访问物理内存和设备的MMIO, CPU虚拟地址到实际地址的映射是MMU做的,当然如果在内核
的线性映射区,这个映射只是加上一个偏移。

从概念上说,设备看到的地址叫总线地址。一般,总线地址比较难以理解,这需要一点
体系结构的知识。一般,一个计算机系统类似这样的结构。

             +-----+   +------+
             | CPU |   | CPU  |  ...
             +--+--+   +--+---+
                |         |          +-----+
         -------+---+-----+----------+ DDR |
                    |                +-----+
            +-------+--------+
            | Bus controller |
            +----+-----------+
                 |
            +----+----+
            | Devices |
            +---------+

CPU, DDR,总线控制器连接的是系统总线,外设是连接到外部总线里的。两个总线域里
的物理信号,总线报文等都不一样。两个总线域是靠总线控制器联通的。所以,比较
容易理解,实际上CPU和外设是处在两个不同的地址空间里的。设备看到的地址,是外部
总线域里的地址,我们叫总线地址。其实CPU要访问外设,最终也是通过总线控制器,把
CPU地址翻译成总线地址,才能访问到,只不过是硬件把设备地址映射到了系统总线,
软件访问直接访问系统总线地址,如果落在映射的区域,总线控制器帮你翻译下,发给
设备。

同样的道理,设备做DMA访问,设备一开始发出的地址是总线地址,当这个访问到了总线
控制器,总线控制器帮忙翻译成为系统总线地址。(这里,我们可以认为IOMMU(ARM上
叫SMMU)也是总线控制器的一部分)

有了这样的认识,下面就好理解了。

  1. 流式DMA和一致性DMA

    所以,一个DMA操作,至少要有两个地址,一个CPU可以访问CPU虚拟地址,一个是设备
    可以访问的设备总线地址(dma_addr_t),他们其实对应的是一个物理地址。
    (有回弹缓冲区的不是一个)

    dma_alloc_coherent可以分配一段物理地址, 函数的返回是指向这段物理地址的CPU
    虚拟地址和这段物理地址对应的总线地址。然后你就可以把这个总线地址配置给硬件。

    dma_map_single和上面的一致性DMA分配不一样,假设我们已经分配好了一段物理地址,
    要算出来这段地址对应的总线地址,我们就可以用dma_map_single这个函数。这种DMA
    的使用方式,叫流式DMA。

  2. 将DMA内存映射到用户态

    可以注意到,你用一致性DMA分配”一段”物理内存。是根本不保证分配的物理内存在
    内核的线性地址空间,而且不保证分配的物理内存是连续的。

    那你想把这些物理内存映射到用户态,叫用户直接访问怎么才能做到?

    dma_mmap_coherent这个API就做的是这个事情, 在驱动的mmap接口里调用这个函数就
    可以了。这个函数把DMA物理区域映射到用户态的连续的虚拟地址上。

  3. 聚散表DMA

    有的时候,做DMA的数据在内存里是不连续存放的,而且设备也支持这种不连续内存的
    DMA。这里的不连续,是指设备的DMA地址的描述就是一个聚散表类似的结构。

    这时我们可以用内核数据结构struct scatterlist来描述数据的初始内存结构,随后用
    dma_map_sg的到每一块的总线地址。然后再把这些总线地址配置到设备对应的数据结构
    里。

知道这些概念对我们编程有什么作用? 首先编程应该是基于正确语义的。你用get_free_page
或者是kmalloc分配一段地址给DMA,在特性的条件下或许没有问题。但是,语义完全是错的,
这些得到的地址都是CPU虚拟地址,CPU可以用这些地址访问数据。但是设备用这些地址发起
DMA操作,是很可能有问题的。

更多推荐

Linux设备驱动中DMA接口的使用