DMA地址映射api使用

875 阅读5分钟

前言

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

CPU地址和DMA地址

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

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可以管理物理地址和总线地址的映射关系。


               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方式操作,否则会出现不可预料的问题的。

    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
    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分配的空间。

    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操作时的方向,目前有如下的值:

 DMA_BIDIRECTIONAL
 DMA_TO_DEVICE
 DMA_FROM_DEVICE
 DMA_NONE

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

流式DMA映射

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


	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接口替代。

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传输后相应地调用如下函数。

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

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

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,沟通交流。

博客地址: fishmwei.github.io

掘金主页: juejin.cn/user/208432…