0%

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

DMA的概念

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

几个地址的概念

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
             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做的,当然如果在内核的线性映射区,
这个映射只是加上一个偏移。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
    +-----+   +------+
| CPU | | CPU | ...
+--+--+ +--+---+
| | +-----+
-------+---+-----+----------+ DDR |
| +-----+
+-------+--------+
| Bus controller |
+----+-----------+
|
+----+----+
| Devices |
+---------+

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

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

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

流式DMA和一致性DMA

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

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

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

将DMA内存映射到用户态

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

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

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

聚散表DMA

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

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

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