歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Linux之DMA API -- 通用設備的動態DMA映射

Linux之DMA API -- 通用設備的動態DMA映射

日期:2017/3/1 10:11:03   编辑:Linux編程

通用設備的動態DMA映射

by JHJ([email protected])

本文描述DMA API。更詳細的介紹請參看Documentation/DMA-API-HOWTO.txt。

API分為兩部分,第一部分描述API,第二部分描述可以支持非一致性內存機器的擴展API。你應該使用第一部分所描述的API,除非你知道你的驅動必須要支持非一致性平台。

第一部分 DMA API

為了可以引用DMA API,你必須 #include <linux/dma-mapping.h>

1-1 使用大塊DMA一致性緩沖區(dma-coherent buffers)

void *
dma_alloc_coherent
(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)

一致性內存:設備對一塊內存進行寫操作,處理器可以立即進行讀操作,而無需擔心處理器高速緩存(cache)的影響。同樣的,處理器對一塊內存進行些操作,設備可以立即進行讀操作。(在告訴設備讀內存時,你可能需要確定刷新處理器的寫緩存。)

此函數申請一段大小為size字節的一致性內存,返回兩個參數。一個是dma_handle,它可以用作這段內存的物理地址。 另一個是指向被分配內存的指針(處理器的虛擬地址)。

注意:由於在某些平台上,使用一致性內存代價很高,比如最小的分配長度為一個頁。因此你應該盡可能合並申請一致性內存的請求。最簡單的辦法是使用dma_pool函數調用(詳見下文)。

參數flag(僅存在於dma_alloc_coherent中)運行調用者定義申請內存時的GFP_flags(詳見kmalloc)。

void *
dma_zalloc_coherent
(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)

對dma_alloc_coherent()的封裝,如果內存分配成功,則返回清零的內存。

void
dma_free_coherent
(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t dma_handle)

釋放之前申請的一致性內存。dev, size及dma_handle必須和申請一致性內存的函數參數相同。cpu_addr必須為申請一致性內存函數的返回虛擬地址。

注意:和其他內存分配函數不同,這些函數必須要在中斷使能的情況下使用。

1-2 使用小塊DMA一致性緩沖區

如果要使用這部分DMA API,必須#include <linux/dmapool.h>。

許多驅動程序需要為DMA描述符或者I/O內存申請大量小塊DMA一致性內存。你可以使用DMA 內存池,而不是申請以頁為單位的內存塊或者調用dma_alloc_coherent()。這種機制有點像struct kmem_cache,只是它利用了DMA一致性內存分配器,而不是調用 __get_free_pages()。同樣地,DMA 內存池知道通用硬件的對齊限制,比如隊列頭需要N字節對齊。

struct dma_pool *
dma_pool_create
(const char *name, struct device *dev,
size_t size, size_t align, size_t alloc);

create( )函數為設備初始化DMA一致性內存的內存池。它必須要在可睡眠上下文調用。

name為內存池的名字(就像struct kmem_cache name一樣)。dev及size就如dma_alloc_coherent()參數一樣。align為設備硬件需要的對齊大小(單位為字節,必須為2的冪次方)。如果設備沒有邊界限制,可以設置該參數為0。如果設置為4096,則表示從內存池分配的內存不能超過4K字節的邊界。

void *
dma_pool_alloc
(struct dma_pool *pool, gfp_t gfp_flags,
dma_addr_t *dma_handle);

從內存池中分配內存。返回的內存同時滿足申請的大小及對齊要求。設置GFP_ATOMIC可以確保內存分配被block,設置GFP_KERNEL(不能再中斷上下文,不會保持SMP鎖)允許內存分配被block。和dma_alloc_coherent()一樣,這個函數會返回兩個值:一個值是cpu可以使用的虛擬地址,另一個值是內存池設備可以使用的dma物理地址。

void
dma_pool_free
(struct dma_pool *pool, void *vaddr,
dma_addr_t addr);

返回內存給內存池。參數pool為傳遞給dma_pool_alloc()的pool,參數vaddr及addr為dma_pool_alloc()的返回值。

void
dma_pool_destroy
(struct dma_pool *pool);

內存池析構函數用於釋放內存池的資源。這個函數在可睡眠上下文調用。請確認在調用此函數時,所有從該內存池申請的內存必須都要歸還給內存池。

1-3 DMA尋址限制

int
dma_supported
(struct device *dev, u64 mask)

用來檢測該設備是否支持掩碼所表示的DMA尋址能力。比如mask為0x0FFFFFF,則檢測該設備是否支持24位尋址。

返回1表示支持,0表示不支持。

注意:該函數很少用於檢測是否掩碼為可用的,它不會改變當前掩碼設置。它是一個內部API而非供驅動者使用的外部API。

int
dma_set_mask
(struct device *dev, u64 mask)

檢測該掩碼是否合法,如果合法,則更新設備參數。即更新設備的尋址能力。

返回0表示成功,返回負值表示失敗。

int
dma_set_coherent_mask
(struct device *dev, u64 mask)

檢測該掩碼是否合法,如果合法,則更新設備參數。即更新設備的尋址能力。

返回0表示成功,返回負值表示失敗。

u64
dma_get_required_mask
(struct device *dev)

該函數返回平台可以高效工作的掩碼。通常這意味著返回掩碼是可以尋址到所有內存的最小值。檢查該值可以讓DMA描述符的大小盡量的小。

請求平台需要的掩碼並不會改變當前掩碼。如果你想利用這點,可以利用改返回值通過dma_set_mask()設置當前掩碼。

1-4 流式DMA映射

dma_addr_t
dma_map_single
(struct device *dev, void *cpu_addr, size_t size,
enum dma_data_direction direction)

映射一塊處理器的虛擬地址,這樣可以讓外設訪問。該函數返回內存的物理地址。

在dma_API中強烈建議使用表示DMA傳輸方向的枚舉類型。

DMA_NONE 僅用於調試目的
DMA_TO_DEVICE 數據從內存傳輸到設備,可認為是寫操作。
DMA_FROM_DEVICE 數據從設備傳輸到內存,可認為是讀操作。
DMA_BIDIRECTIONAL 不清楚傳輸方向則可用該類型。

請注意:並非一台機器上所有的內存區域都可以用這個API映射。進一步說,對於內核連續虛擬地址空間所對應的物理地址並不一定連續(比如這段地址空間由vmalloc申請)。因為這種函數並未提供任何分散/聚集能力,因此用戶在企圖映射一塊非物理連續的內存時,會返回失敗。基於此原因,如果想使用該函數,則必須確保緩沖區的物理內存連續(比如使用kmalloc)。

更進一步,所申請內存的物理地址必須要在設備的dma_mask尋址范圍內(dma_mask表示與設備尋址能力對應的位)。為了確保由kmalloc申請的內存在dma_mask中,驅動程序需要定義板級相關的標志位來限制分配的物理內存范圍(比如在x86上,GFP_DMA用於保證申請的內存在可用物理內存的前16Mb空間,可以由ISA設備使用)。

同時還需注意,如果平台有IOMMU(設備擁有MMU單元,可以進行I/O內存總線和設備的映射,即總線地址和內存物理地址的映射),則上述物理地址連續性及外設尋址能力的限制就不存在了。當然為了方便起見,設備驅動開發者可以假設不存在IOMMU。

警告:內存一致性操作基於高速緩存行(cache line)的寬度。為了可以正確操作該API創建的內存映射,該映射區域的起始地址和結束地址都必須是高速緩存行的邊界(防止在一個高速緩存行中有兩個或多個獨立的映射區域)。因為在編譯時無法知道高速緩存行的大小,所以該API無法確保該需求。因此建議那些對高速緩存行的大小不特別關注的驅動開發者們,在映射虛擬內存時保證起始地址和結束地址都是頁對齊的(頁對齊會保證高速緩存行邊界對齊的)。

DMA_TO_DEVICE 軟件對內存區域做最後一次修改後,且在傳輸給設備前,需要做一次同步。一旦該使用該原語,內存區域可被視作設備只讀緩沖區。如果設備需要對該內存區域進行寫操作,則應該使用DMA_BIDIRECTIONAL(如下所示)

DMA_FROM_DEVICE 驅動在訪問數據前必須做一次同步,因為數據可能被設備修改了。內存緩沖區應該被當做驅動只讀緩沖區。如果驅動需要進行寫操作,應該使用DMA_BIDIRECTIONAL(如下所示)。

DMA_BIDIRECTIONAL 需要特別處理:這意味著驅動並不確定內存數據傳輸到設備前,內存是否被修改了,同時也不確定設備是否會修改內存。因此,你必須需要兩次同步雙向內存:一次在內存數據傳輸到設備前(確保所有緩沖區數據改變都從處理器的高速緩存刷新到內存中),另一次是在設備可能訪問該緩沖區數據前(確保所有處理器的高速緩存行都得到了更新,設備可能改變了緩沖區數據)。即在處理器寫操作完成時,需要做一次刷高速緩存的操作,以確保數據都同步到了內存緩沖區中。在處理器讀操作前,需要更新高速緩沖區的行,已確保設備對內存緩沖區的改變都同步到了高速緩沖區中。

void
dma_unmap_single
(struct device *dev, dma_addr_t dma_addr, size_t size,
enum dma_data_direction direction)

取消先前的內存映射。傳入該函數的所有參數必須和映射API函數的傳入(包括返回)參數相同。

dma_addr_t
dma_map_page(struct device *dev, struct page *page,
unsigned long offset, size_t size,
enum dma_data_direction direction)

void
dma_unmap_page
(struct device *dev, dma_addr_t dma_address, size_t size,
enum dma_data_direction direction)

對頁進行映射/取消映射的API。對其他映射API的注意事項及警告對此都使用。同樣的,參數<offset>及<size>用於部分頁映射,如果你對高速緩存行的寬度不清楚的話,建議你不要使用這些參數。

int
dma_mapping_error
(struct device *dev, dma_addr_t dma_addr)

在某些場景下,通過dma_map_single及dma_map_page創建映射可能會失敗。驅動程序可以通過此函數來檢測這些錯誤。一個非零返回值表示未成功創建映射,驅動程序需要采取適當措施(比如降低當前DMA映射使用率或者等待一段時間再嘗試)。

int
dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction direction)

返回值:被映射的物理內存塊的數量(如果在分散/聚集鏈表中一些元素是物理地址或虛擬地址相鄰的,切IOMMU可以將它們映射成單個內存塊,則返回值可能比輸入值<nents>小)。

請注意如果sg已經映射過了,其不能再次被映射。再次映射會銷毀sg中的信息。

如果返回0,則表示dma_map_sg映射失敗,驅動程序需要采取適當措施。驅動程序在此時做一些事情顯得格外重要,一個阻塞驅動中斷請求或者oopsing都總比什麼都不做導致文件系統癱瘓強很多。

下面是個分散/聚集映射的例子,假設scatterlists已經存在。

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

其中nents為sglist條目的個數。

這種實現可以很方便將幾個連續的sglist條目合並成一個(比如在IOMMU系統中,或者一些頁正好是物理連續的)。

然後你就可以循環多次(可能小於nents次)使用sg_dma_address() 及sg_dma_len()來獲取sg的物理地址及長度。

void
dma_unmap_sg
(struct device *dev, struct scatterlist *sg,
int nhwentries, enum dma_data_direction direction)

取消先前分散/聚集鏈表的映射。所有參數和分散/聚集映射API的參數相同。

注意:<nents>是傳入的參數,不一定是實際返回條目的數值。

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size,
enum dma_data_direction direction)

void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size,
enum dma_data_direction direction)

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nelems,
enum dma_data_direction direction)

void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nelems,
enum dma_data_direction direction)

為CPU及外設同步single contiguous或分散/聚集映射。

注意:你必須要做這個工作,

  • 在CPU讀操作前,此時緩沖區由設備通過DMA寫入數據(DMA_FROM_DEVICE)

  • 在CPU寫操作後,緩沖區數據將通過DMA傳輸到設備(DMA_TO_DEVICE)

  • 在傳輸數據到設備前後(DMA_BIDIRECTIONAL)

dma_addr_t
dma_map_single_attrs
(struct device *dev, void *cpu_addr, size_t size,
enum dma_data_direction dir,
struct dma_attrs *attrs)

void
dma_unmap_single_attrs
(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir,
struct dma_attrs *attrs)

int
dma_map_sg_attrs
(struct device *dev, struct scatterlist *sgl,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)

void
dma_unmap_sg_attrs
(struct device *dev, struct scatterlist *sgl,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)

這四個函數除了傳入可選的struct dma_attrs*之外,其他和不帶_attrs後綴的函數一樣。

struct dma_attrs概述了一組DMA屬性。struct dma_attrs詳細定義請參見linux/dma-attrs.h。

DMA屬性的定義是和體系結構相關的,並且Documentation/DMA-attributes.txt有詳細描述。

如果struct dma_attrs* 為空,則這些函數可以認為和不帶_attrs後綴的函數相同。

下面給出一個如何使用*_attrs 函數的例子,當進行DMA內存映射時,如何傳入一個名為DMA_ATTR_FOO的屬性:

#include <linux/dma-attrs.h>
/* DMA_ATTR_FOO should be defined in linux/dma-attrs.h and
* documented in Documentation/DMA-attributes.txt */
...
DEFINE_DMA_ATTRS(attrs);
dma_set_attr(DMA_ATTR_FOO, &attrs);
....
n = dma_map_sg_attrs(dev, sg, nents, DMA_TO_DEVICE, &attr);
....

在映射/取消映射的函數中,可以檢查DMA_ATTR_FOO是否存在:

void whizco_dma_map_sg_attrs(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir,
struct dma_attrs *attrs)
{
....
int foo = dma_get_attr(DMA_ATTR_FOO, attrs);
....
if (foo)
/* twizzle the frobnozzle */
....

Copyright © Linux教程網 All Rights Reserved