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映射。關於此有一些非書面的准則,本文試圖將它們以文字的方式整理出來。
如果你通過頁分配器(比如__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尋址限制嗎?比如你的設備只有低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);
}