Linux设备驱动中DMA接口的使用
-v0.1 2018.3.13 Sherlock init Westford
本文试图讲清楚linux里的DMA接口使用时的一些基本概念。阅读本文的时候,可以先看看
内核里的Documentation/DMA*(DMA相关的一些文件), 其实这里面讲的已经很清楚了,另外
还可以看看知乎上的一篇文章:https://zhuanlan.zhihu/p/25999484, 这篇文章对
Linux里关于地址空间的各个概念有很好的讲解。
DMA的概念
DMA就是说设备可以直接进行内存的读写,不需要CPU的参与。当然,在设备启动DMA
进行读写之前,你需要通过CPU把读写的地址,大小等一些信息配置给设备。设备完成
数据读写后可以发一个中断告诉CPU,之后CPU就可以做相关的操作。但是,CPU要把
什么地址告诉设备呢?几个地址的概念
在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)也是总线控制器的一部分)
有了这样的认识,下面就好理解了。
流式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操作,是很可能有问题的。
更多推荐
Linux设备驱动中DMA接口的使用
发布评论