歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

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);
                }

Copyright © Linux教程網 All Rights Reserved