說到DMA,就會想到Cache,兩者本身似乎是好不相關的事物。的確,假設DMA針對內(nèi)存的目的地址和Cache緩存的對象沒有重疊區(qū)域,DMA和Cache之間就相安無事,但是,如果有重疊呢,經(jīng)過DMA操作,Cache緩存對應的內(nèi)存的數(shù)據(jù)已經(jīng)被修改,而CPU本身并不知道,它仍然認為Cache中的數(shù)據(jù)仍然還是內(nèi)存中的數(shù)據(jù),以后訪問Cache映射的內(nèi)存時,它仍然使用陳舊的Cache數(shù)據(jù),這就會發(fā)生Cache與內(nèi)存之間數(shù)據(jù)"不一致性"的錯誤。一旦出現(xiàn)這樣的情況,沒有處理好,驅(qū)動就將無法正常運行。那么怎樣解決呢?最簡單的方法是直接禁止DMA目標地址范圍內(nèi)內(nèi)存的Cache功能,當然這是犧牲性能的,但卻高可靠。不是嗎,所以這兩者之間究竟怎么平衡,還真不好解決。 其實啊,Cache不一致的情況在其他地方也是可能發(fā)生的,比如,對于帶MMU功能的arm處理器,在開啟MMU之前,需要設置Cache無效,TLB也是如此。
說了,那么多DMA理論的東西,剩下的來點Linux下DMA編程的東西,當然,這里也是點一下,具體怎么操作,我可要賣個關子到下節(jié)了。 在內(nèi)存中用于與外設交互數(shù)據(jù)的一塊區(qū)域被稱作DMA緩沖區(qū),在設備不支持scatter/gatherCSG,分散/聚集操作的情況下,DMA緩沖區(qū)必須是物理上聯(lián)系的。
對于ISA設備而言,其DMA操作只能在16MB以下的內(nèi)存進行,因此,在使用kmalloc()和__get_free_pages()及其類似函數(shù)申請DMA緩沖區(qū)時應使用GFP_DMA標志,這樣能保證獲得的內(nèi)存是具備DMA能力的。內(nèi)核中定義了__get_free_pages()針對DMA的"快捷方式"__get_dma_pages(),它在申請標志中添加了GFP_DMA,如下所示:
#define __get_dma__pages(gfp_mask, order)
__get_free_pages((gfp_mask) | GFP_DMA, (order))
"我不想使用order為參數(shù)的申請DMA內(nèi)存,感覺就是怪怪的,那咋辦?"
那?這樣吧,你就用另外一個函數(shù)dma_mem_alloc()源代碼如下:
/* dma_mem_alloc()返回值為虛擬地址 */
static unsigned long dma_mem_alloc(int size)
{
int order = get_order(size);//大小->指數(shù)
return __get_dma_pages(GFP_KERNEL, order);
}
上節(jié)我們說到了dma_mem_alloc()函數(shù),需要說明的是DMA的硬件使用總線地址而非物理地址,總線地址是從設備角度上看到的內(nèi)存地址,物理地址是從CPU角度上看到的未經(jīng)轉(zhuǎn)換的內(nèi)存地址(經(jīng)過轉(zhuǎn)換的那叫虛擬地址)。在PC上,對于ISA和PCI而言,總線即為物理地址,但并非每個平臺都是如此。由于有時候接口總線是通過橋接電路被連接,橋接電路會將IO地址映射為不同的物理地址。例如,在PRep(PowerPC Reference Platform)系統(tǒng)中,物理地址0在設備端看起來是0X80000000,而0通常又被映射為虛擬地址0xC0000000,所以同一地址就具備了三重身份:物理地址0,總線地址0x80000000及虛擬地址0xC0000000,還有一些系統(tǒng)提供了頁面映射機制,它能將任意的頁面映射為連續(xù)的外設總線地址。內(nèi)核提供了如下函數(shù)用于進行簡單的虛擬地址/總線地址轉(zhuǎn)換:
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
在使用IOMMU或反彈緩沖區(qū)的情況下,上述函數(shù)一般不會正常工作。而且,這兩個函數(shù)并不建議使用。
需要說明的是設備不一定能在所有的內(nèi)存地址上執(zhí)行DMA操作,在這種情況下應該通過下列函數(shù)執(zhí)行DMA地址掩碼:
int dma_set_mask(struct device *dev, u64 mask);
比如,對于只能在24位地址上執(zhí)行DMA操作的設備而言,就應該調(diào)用dma_set_mask(dev, 0xffffffff)。DMA映射包括兩個方面的工作:分配一片DMA緩沖區(qū);為這片緩沖區(qū)產(chǎn)生設備可訪問的地址。結(jié)合前面所講的,DMA映射必須考慮Cache一致性問題。內(nèi)核中提供了一下函數(shù)用于分配一個DMA一致性的內(nèi)存區(qū)域:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
這個函數(shù)的返回值為申請到的DMA緩沖區(qū)的虛擬地址。此外,該函數(shù)還通過參數(shù)handle返回DMA緩沖區(qū)的總線地址。與之對應的釋放函數(shù)為:
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
以下函數(shù)用于分配一個寫合并(writecombinbing)的DMA緩沖區(qū):
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
與之對應的是釋放函數(shù):dma_free_writecombine(),它其實就是dma_free_conherent,只不過是用了#define重命名而已。
此外,Linux內(nèi)核還提供了PCI設備申請DMA緩沖區(qū)的函數(shù)pci_alloc_consistent(),原型為:
void *pci_alloc_consistent(struct pci_dev *dev, size_t size, dma_addr_t *dma_addrp); 對應的釋放函數(shù)為:
void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr, dma_addr_t dma_addr);
相對于一致性DMA映射而言,流式DMA映射的接口較為復雜。對于單個已經(jīng)分配的緩沖區(qū)而言,使用dma_map_single()可實現(xiàn)流式DMA映射:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction); 如果映射成功,返回的是總線地址,否則返回NULL.最后一個參數(shù)DMA的方向,可能取DMA_TO_DEVICE, DMA_FORM_DEVICE, DMA_BIDIRECTIONAL和DMA_NONE;
與之對應的反函數(shù)是:
void dma_unmap_single(struct device *dev,dma_addr_t *dma_addrp,size_t size,enum dma_data_direction direction);
通常情況下,設備驅(qū)動不應該訪問unmap()的流式DMA緩沖區(qū),如果你說我就愿意這么做,我又說寫什么呢,選擇了權(quán)利,就選擇了責任,對吧。這時可先使用如下函數(shù)獲得DMA緩沖區(qū)的擁有權(quán):
void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
在驅(qū)動訪問完DMA緩沖區(qū)后,應該將其所有權(quán)還給設備,通過下面的函數(shù):[!--empirenews.page--]
void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);如果設備要求較大的DMA緩沖區(qū),在其支持SG模式的情況下,申請多個不連續(xù)的,相對較小的DMA緩沖區(qū)通常是防止申請?zhí)蟮倪B續(xù)物理空間的方法,在Linux內(nèi)核中,使用如下函數(shù)映射SG:
int dma_map_sg(struct device *dev,struct scatterlist *sg, int nents,enum dma_data_direction direction); 其中nents是散列表入口的數(shù)量,該函數(shù)的返回值是DMA緩沖區(qū)的數(shù)量,可能小于nents。對于scatterlist中的每個項目,dma_map_sg()為設備產(chǎn)生恰當?shù)目偩€地址,它會合并物理上臨近的內(nèi)存區(qū)域。下面在給出scatterlist結(jié)構(gòu):
struct scatterlist
{
struct page *page;
unsigned int offset; //偏移量
dma_addr_t dma_address; //總線地址
unsigned int length; //緩沖區(qū)長度
}
執(zhí)行dma_map_sg()后,通過sg_dma_address()后可返回scatterlist對應緩沖區(qū)的總線結(jié)構(gòu),sg_dma_len()可返回scatterlist對應的緩沖區(qū)的長度,這兩個函數(shù)的原型是:
dma_addr_t sg_dma_address(struct scatterlist *sg); unsigned int sg_dma_len(struct scatterlist *sg);
在DMA傳輸結(jié)束后,可通過dma_map_sg()的反函數(shù)dma_unmap_sg()去除DMA映射:
void dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); SG映射屬于流式DMA映射,與單一緩沖區(qū)情況下流式DMA映射類似,如果設備驅(qū)動一定要訪問映射情況下的SG緩沖區(qū),應該先調(diào)用如下函數(shù):
int dma_sync_sg_for_cpu(struct device *dev,struct scatterlist *sg, int nents,enum dma_data_direction direction);
訪問完后,通過下列函數(shù)將所有權(quán)返回給設備:
int dma_map_device(struct device *dev,struct scatterlist *sg, int nents,enum dma_data_direction direction);
Linux 系統(tǒng)中可以有一個相對簡單的方法預先分配緩沖區(qū),那就是同步"mem="參數(shù)預留內(nèi)存。例如,對于內(nèi)存為64MB的系統(tǒng),通過給其傳遞mem=62MB命令行參數(shù)可以使得頂部的2MB內(nèi)存被預留出來作為IO內(nèi)存使用,這2MB內(nèi)存可以被靜態(tài)映射,也可以執(zhí)行ioremap()。
相應的函數(shù)都介紹完了:說真的,好費勁啊,我都想放棄了,可為了小王,我繼續(xù)哈在linux設備驅(qū)動中如何操作呢:
像使用中斷一樣,在使用DMA之前,設備驅(qū)動程序需要首先向系統(tǒng)申請DMA通道,申請DMA通道的函數(shù)如下:
int request_dma(unsigned int dmanr, const char * device_id); 同樣的,設備結(jié)構(gòu)體指針可作為傳入device_id的最佳參數(shù)。
使用完DMA通道后,應該使用如下函數(shù)釋放該通道:void free_dma(unsinged int dmanr);