前言
做驱动开发免不了要用到DMA技术, 这是一种高速的数据传输操作,允许外设直接读写存储器,不需要CPU的介入。这样CPU就可以继续做其它的事情了。控制这个操作的是DMA控制器。
CPU地址和DMA地址
系统内核使用的是虚拟地址,任何从kmalloc, valloc返回的地址都是虚拟地址,可以使用void *变量存储。虚拟地址可以通过内存管理系统(MMU)转换为CPU的物理地址。内核管理的设备资源一般都是物理地址,比如设备的寄存器地址之类的,这些地址的范围都存在于/proc/iomem
文件中。物理地址是不能直接使用的,需要通过ioremap映射得到一个虚拟地址,然后代码才可以访问。
1 | root@keep-VirtualBox:~# cat /proc/iomem |
IO设备还会用到一个概念:总线地址。设备使用总线地址读写系统的内存。有些系统总线地址等同于物理地址,但是大部分系统并不是这样的,是两套不同的地址描述。IOMMU可以管理物理地址和总线地址的映射关系。
1 |
|
内核读取设备的总线地址,转换为CPU物理地址,存储在struct resource结构中,可以在/proc/iomem
中看到。然后驱动通过ioremap
把物理地址映射到虚拟地址,并通过专门的接口读写寄存器,如:ioread32(C),访问设备对应的总线地址。这个有点儿绕,驱动访问虚拟地址,最终落到设备的总线地址上面。
使用DMA操作时,驱动通过kmalloc申请一片空间A,A对应到物理地址B上去。设备要访问物理地址B的话,需要有个IOMMU通过A转换得到一个DMA地址C对应到B上去。驱动告诉设备DMA操作的目的地址是C,IOMMU会把C映射到物理地址B上面去,最后操作的是物理地址B。驱动通过dma_map_single接口,获取到虚拟地址A对应的DMA地址C。
虚拟地址A, DMA地址C,对应的物理地址都是B。
DMA限制
不是所有的内核内存地址都可以使用DMA技术的。使用__get_free_page或者kmalloc、kmem_cache_alloc返回的地址可以使用DMA,而使用vmalloc返回的地址不能使用DMA。还需要保证地址是cacheline对齐的,否则会出现一致性的问题,如果不是cacheline对齐的,CPU和DMA会打架的,CPU和DMA同时操作一片cache,会导致内容相互覆盖。
DMA寻址能力
内核默认支持32bit的DMA地址空间,支持64bit的设备相应的可以支持64bit,设备有限制的话也可以相应的减少位数。正确的操作是调用接口dma_set_mask_and_coherent设置DMA地址空间寻址位数。接口如果返回非0的话,则表示设备不支持设置的dma空间大小,那么后续就不要通过DMA方式操作,否则会出现不可预料的问题的。
1 | if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) { |
DMA映射方式
DMA有两种类型的映射: 一致性DMA和流式DMA。
一致性DMA在驱动注册的时候就映射了,最后才解除映射。硬件会自己保证设备和CPU可以并行的访问数据,并且数据都是实时更新的,不需要开发者做额外的工作。默认一致性DMA支持的空间为32位寻址空间。如果需要,可以自行设置DMA掩码位。有一点需要注意的是,一致性DMA并不是完全按序写入数据的,如果不同地址间数据写入存在互相依赖,需要使用wmb做同步。
使用一致性DMA的场景有:
- 网卡的ring描述符
- SCSI适配的mailbox命令数据
- 设备固件的微码内存
流式DMA一般在需要做DMA转换的时候才做映射,而后动态的解除映射。使用流式DMA需要明确调用接口设置想要的操作。
使用流式DMA的场景有:
- 网络包buffers的接收发送
- 文件系统的读写buffers
1 | dma_addr_t dma_handle; |
一致性DMA的使用代码如上,接口返回CPU可以访问的虚拟地址以及dma_handle地址(传给设备的地址)。返回的地址都是PAGE_SIZE对齐的大小的,即使你传入的size小于PAGE_SIZE也是一样的。
如果需要大量的小size的内存,可以自行管理使用dma_alloc_coherent分配的空间, 也可以使用dma_pool的接口来实现, dma_pool底层实际也是管理的dma_alloc_coherent分配的空间。
1 | struct dma_pool *pool; |
DMA方向
DMA相关接口需要填写DMA方向,表示DMA操作时的方向,目前有如下的值:
1 | DMA_BIDIRECTIONAL |
一般只有流式DMA需要设置方向,一致性DMA默认是双向的DMA_BIDIRECTIONAL。
流式DMA映射
流式DMA映射可以在中断上下文中调用,可以映射一个独立的内存区域,也可以映射一个scatterlist表示的多个内存区域。
1 |
|
dma_map_single就是不能映射high memory, 可以使用dma_map_page接口替代。
1 | struct device *dev = &my_dev->dev; |
在流式DMA映射取消映射之前,CPU不应该访问DMA buffer,如果需要访问,则必须在DMA传输后相应地调用如下函数。
1 | dma_sync_single_for_cpu(dev, dma_handle, size, direction); |
CPU访问结束后,将buffer还给设备DMA使用时,需要相应调用如下函数。
1 | dma_sync_single_for_device(dev, dma_handle, size, direction); |
for_cpu 和 for_device的区别在于控制权是属于cpu还是device。
错误处理
- 调用dma_alloc_coherent后判断返回值是否为NULL, 调用dma_map_sg判断返回值是否为0
- dma_map_single和dma_map_page调用后,使用dma_mapping_error判断是否失败
在映射失败后,记得释放已经成功的地址区域。
更多
写驱动的时候遇到过DMA的问题,然后这周外部也反馈了一个问题跟DMA相关,虽然之前看了许多DMA相关的内容,但是没有总结过总觉得掌握的不够透。周末抽空看看内核中的文档关于DMA接口使用的描述(Documentation/core-api/dma-api-howto.rst),边看边整理,好好系统学习一下。刚写完这个文章去看了一下前段日子写的驱动代码,发现还是有些需要改一下。温故而知新,的确如此,当然关键是花了这个时间,真真切切地学进去了,才有收获。
周末这两天的事情主要就是小孩上学注册,昨天搬家,今天注册。上学的地方离得比较远,只好让家里领导去学校附近租个房先陪读一年,暂时过渡一下。等明年有资格买房了先买个二手房,再转学过来。这几年也辛苦娃跟着到处奔波了,余生好好工作,好好培养爱护他们。
上周又落下了一期文章,坚持不易啊。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。
博客地址: https://fishmwei.github.io
掘金主页: https://juejin.cn/user/2084329776486919