0%

周谈(34)-DMA地址映射api使用

前言

做驱动开发免不了要用到DMA技术, 这是一种高速的数据传输操作,允许外设直接读写存储器,不需要CPU的介入。这样CPU就可以继续做其它的事情了。控制这个操作的是DMA控制器。

CPU地址和DMA地址

系统内核使用的是虚拟地址,任何从kmalloc, valloc返回的地址都是虚拟地址,可以使用void *变量存储。虚拟地址可以通过内存管理系统(MMU)转换为CPU的物理地址。内核管理的设备资源一般都是物理地址,比如设备的寄存器地址之类的,这些地址的范围都存在于/proc/iomem文件中。物理地址是不能直接使用的,需要通过ioremap映射得到一个虚拟地址,然后代码才可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
root@keep-VirtualBox:~# cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c7fff : Video ROM
000e2000-000e2fff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-dffeffff : System RAM
a7000000-a80020df : Kernel code
a8200000-a8ca6fff : Kernel rodata
a8e00000-a918af7f : Kernel data
a945e000-a99fffff : Kernel bss
dfff0000-dfffffff : ACPI Tables
e0000000-fdffffff : PCI Bus 0000:00
e0000000-e0ffffff : 0000:00:02.0
e0000000-e0ffffff : vmwgfx probe
f0000000-f01fffff : 0000:00:02.0
f0000000-f01fffff : vmwgfx probe
f0200000-f021ffff : 0000:00:03.0
f0200000-f021ffff : e1000
f0400000-f07fffff : 0000:00:04.0
f0400000-f07fffff : vboxguest
f0800000-f0803fff : 0000:00:04.0
f0804000-f0804fff : 0000:00:06.0
f0804000-f0804fff : ohci_hcd
f0806000-f0807fff : 0000:00:0d.0
f0806000-f0807fff : ahci
fec00000-fec00fff : Reserved
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
fee00000-fee00fff : Reserved
fffc0000-ffffffff : Reserved
100000000-21fffffff : System RAM

IO设备还会用到一个概念:总线地址。设备使用总线地址读写系统的内存。有些系统总线地址等同于物理地址,但是大部分系统并不是这样的,是两套不同的地址描述。IOMMU可以管理物理地址和总线地址的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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
| | | |
| | | |
+-------+ +------+


内核读取设备的总线地址,转换为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
2
3
4
   if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))) {
dev_warn(dev, "mydev: No suitable DMA available\n");
goto ignore_this_device;
}

DMA映射方式

DMA有两种类型的映射: 一致性DMA和流式DMA。

一致性DMA在驱动注册的时候就映射了,最后才解除映射。硬件会自己保证设备和CPU可以并行的访问数据,并且数据都是实时更新的,不需要开发者做额外的工作。默认一致性DMA支持的空间为32位寻址空间。如果需要,可以自行设置DMA掩码位。有一点需要注意的是,一致性DMA并不是完全按序写入数据的,如果不同地址间数据写入存在互相依赖,需要使用wmb做同步。

使用一致性DMA的场景有:

  • 网卡的ring描述符
  • SCSI适配的mailbox命令数据
  • 设备固件的微码内存

流式DMA一般在需要做DMA转换的时候才做映射,而后动态的解除映射。使用流式DMA需要明确调用接口设置想要的操作。
使用流式DMA的场景有:

  • 网络包buffers的接收发送
  • 文件系统的读写buffers
1
2
3
4
5
   dma_addr_t dma_handle;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

dma_free_coherent(dev, size, cpu_addr, dma_handle);

一致性DMA的使用代码如上,接口返回CPU可以访问的虚拟地址以及dma_handle地址(传给设备的地址)。返回的地址都是PAGE_SIZE对齐的大小的,即使你传入的size小于PAGE_SIZE也是一样的。

如果需要大量的小size的内存,可以自行管理使用dma_alloc_coherent分配的空间, 也可以使用dma_pool的接口来实现, dma_pool底层实际也是管理的dma_alloc_coherent分配的空间。

1
2
3
4
5
6
7
8
9
   struct dma_pool *pool;

pool = dma_pool_create(name, dev, size, align, boundary); // align必须是2的次方, boundary表示内存的边界,不允许一次性从pool申请超过多少大小的内存。

cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);

dma_pool_free(pool, cpu_addr, dma_handle);

dma_pool_destroy(pool); // free pool之前要保证所有从pool申请的内存都free了

DMA方向

DMA相关接口需要填写DMA方向,表示DMA操作时的方向,目前有如下的值:

1
2
3
4
DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE

一般只有流式DMA需要设置方向,一致性DMA默认是双向的DMA_BIDIRECTIONAL。

流式DMA映射

流式DMA映射可以在中断上下文中调用,可以映射一个独立的内存区域,也可以映射一个scatterlist表示的多个内存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}

dma_unmap_single(dev, dma_handle, size, direction);

// scatterlist形式
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;

for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}

dma_unmap_sg(dev, sglist, nents, direction);

dma_map_single就是不能映射high memory, 可以使用dma_map_page接口替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;

dma_handle = dma_map_page(dev, page, offset, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}

...

dma_unmap_page(dev, dma_handle, size, direction);

在流式DMA映射取消映射之前,CPU不应该访问DMA buffer,如果需要访问,则必须在DMA传输后相应地调用如下函数。

1
2
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
dma_sync_sg_for_cpu(dev, sglist, nents, direction);

CPU访问结束后,将buffer还给设备DMA使用时,需要相应调用如下函数。

1
2
dma_sync_single_for_device(dev, dma_handle, size, direction);
dma_sync_sg_for_device(dev, sglist, nents, 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