Linux之DMA动态映射指南
DMA动态映射指南
translated by JHJ([email protected])
本文通过伪代码指导驱动开发者如何正确使用DMA API。关于API更精确的描述,请参考DMA-API.txt。
大多是64位平台有一些特殊硬件可以将总线地址(DMA地址)转换为物理地址。这个和CPU如何利用页表或TLB将虚拟地址转换成物理地址有点像。这种地址转换是有必要的,就像PCI设备可以在单个寻址周期里在64位物理地址空间寻址到任何一个页面。以前linux上的64位平台需要人为设置系统的最大内存大小,这样virt_to_bus()就可以正在工作了(DMA地址转换页表在系统启动时简单初始化,通过__pa(bus_to_virt()可以将DMA地址转换为物理页地址)。
为了使linux可以使用DMA动态映射,它需要得到驱动的一些协助,也就是说需要考虑DMA地址只有在使用时才被映射,DMA传输后,需要取消映射。
当然下面这些API可以在没有这些硬件限制的情况下正常工作。
请注意这些DMA API可以在任何总线上工作,和体系结构无关。你应该是用DMA API而不是特定总线的DMA API(比pci_dma_*)。
首先,你需要确定在你的驱动程序中
#include <linux/dma-mapping.h>
该文件定义了类型dma_addr_t(),它作为一个从DMA 映射函数返回的(总线)地址,到处都会使用到。
什么样的内存是DMA可用的?
第一件你要知道的事情是什么样的内核内存可以用作DMA映射。关于此有一些非书面的准则,本文试图将它们以文字的方式整理出来。
如果你通过页分配器(比如__get_free_page*())或者通用内存分配器(比如kmalloc() or kmem_cache_alloc())分配内存,那么你可以使用由这些函数返回的内存地址用作DMA传输。
这意味着你不能使用vmalloc()返回的内存地址用作DMA。DMA使用由vmalloc申请的内存是有可能的,但是需要遍历页表来获取物理地址,然后将这些页通过类似__va()这样的函数转换成内核虚拟地址。
这条规则意味着你不能将内核镜像地址(在data/text/bss段),或者模块镜像地址,或者栈地址用于DMA。即使这些物理内存可以用于DMA,你也要确保I/O缓冲区是缓存行对齐的。如果不是这样,你将会看到由于DMA不一致性缓存导致的缓存行共享问题(数据丢失)。比如处理器可能写一个字,而DMA在同一个缓存行写另一个字,他们两中的一个将被修改。
同样的,这意味着你不能使用由kmap()调用返回的地址,理由与vmalloc()一样。
可阻塞I/O或者网络缓冲区又会怎么样呢?可阻塞I/O及网络子系统可以确保它们使用的缓冲区可以用于DMA传输。
DMA寻址限制
你的设备有DMA寻址限制吗?比如你的设备只有低24位寻址能力?如果是的,那么你就需通知内核。
默认情况下,内核假设设备可以在32位地址空间寻址。对于64位设备,设备的寻址空间将大大增加。对于一个有寻址限制的外设,如前面所讨论的,需要减小寻址空间。
特别注意对于PCI设备:PCI-X规格书要求PCI-X设备对于数据交互要支持64位寻址。至少在一个平台上(SGI SN2)需要64位一致性内存分配,这样才可以在IO总线为PCI-X模式下正常工作。
可以通过调用dma_set_mask()来通知内核相关限制:
int dma_set_mask(struct device *dev, u64 mask);
通过调用dma_set_coherent_mask()通知内核一致性内存分配的限制。
int dma_set_coherent_mask(struct device *dev, u64 mask);
这里devi为一个指向设备的指针,掩码显示与设备寻址能力对应的位。如果使用指定的mask时DMA能正常工作,则返回零。通常来说,设备数据结构是内嵌到特定总线的设备结构中的。比如一个指向PCI设备的指针为pdev->dev(pdev指向PCI设备)。
如果返回非零值,则对应设备不能使用DMA。如果强行使用则会出现一些不确定现象。此时你需要用一个不同的掩码,或者不适用DMA。这意味着如果返回失败,你有三个选择:
1)如果可能的话,使用另一个DMA掩码值;
2)如果可能的话使用非DMA模式传输数据;
3)放弃该设备,不要初始化该设备;
因此如果你不想执行第二步或者第三步时,你应该在驱动中打印一个KERN_WARNING的消息。这样的话,当驱动使用者抱怨性能很差时,或者根本检测不到设备时,你可以让他们保存内核信息来找出准确原因。
标准的32位寻址设备会如下做一些事情:
if (dma_set_mask(dev, DMA_BIT_MASK(32))) {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
另一个常见的场景是拥有64位寻址能力的设备。该方法用于尝试64位寻址,但是会使用32位掩码,这样可以确保不会失败。内核可以在64位掩码中返回失败,不是因为没有64位寻址能力,而是因为32位寻址比64位寻址更加有效率。比如Sparc64 PCI SAC寻址就比DAC寻址更加有效率。
下面的例子告诉你如何处理拥有64位处理能力的设备的流式DMA。
int using_dac;
if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
using_dac = 1;
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
} else {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
如果一个设备可以使用64位一致性内存,那么代码如下:
int using_dac, consistent_using_dac;
if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
using_dac = 1;
consistent_using_dac = 1;
dma_set_coherent_mask(dev, DMA_BIT_MASK(64));
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
consistent_using_dac = 0;
dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
} else {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
最后,如果你的设备只有低24位寻址能力,那么代码可能如下:
if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
printk(KERN_WARNING
"mydev: 24-bit DMA addressing not available.\n");
goto ignore_this_device;
}
当调用dma_set_mask()成功时,会返回零。内核会保存输入的掩码值。以后再做DMA映射时就会用到该掩码信息。
有一个特殊情况我们需要在这里提及一下。如果设备支持多重功能(比如声卡支持播放和录音功能),不同的功能有不同的DMA寻址寻址限制,你可能想探测每个掩码然后选出一个机器可以处理的值。最后调用dma_set_mask()会成为最特别的掩码值,这点很重要。
下面给出伪代码来展示如何处理该问题。
#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32)
#define RECORD_ADDRESS_BITS DMA_BIT_MASK(24)
struct my_sound_card *card;
struct device *dev;
...
if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
card->playback_enabled = 1;
} else {
card->playback_enabled = 0;
printk(KERN_WARNING "%s: Playback disabled due to DMA limitations.\n",
card->name);
}
if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
card->record_enabled = 1;
} else {
card->record_enabled = 0;
printk(KERN_WARNING "%s: Record disabled due to DMA limitations.\n",
card->name);
}