DMA是計算機系統的一個特性,它允許設備在沒有CPU干預的情況下訪問主系統內存RAM,然后允許它們投入到其他任務中。人們通常使用它來加速網絡流量,但它支持任何類型的拷貝。
DMA控制器是負責DMA管理的外圍設備。主要在現代處理器和微控制器中能看到它。DMA有一個用於執行內存讀寫操作而不占用CPU周期的特性。當需要傳輸數據塊時,CPU 向DMA控制器提供源地址和目的地址以及總字節數。DMA控制器然后自動地將數據從源地址傳輸到目的地址,而不占用CPU周期。當剩余字節數為0時,塊傳輸結束。
設置DMA映射
對於任何類型的 DMA 傳輸,都需要提供源地址和目的地址,以及要傳輸的字節數。在外圍DMA的情況下,外圍設備的 FIFO 既可以作為源,也可以作為目的。當外圍設備作為源時,內存位置(內部或外部)作為目的地址。當外圍設備作為目的地時,內存位置(內部或外部)作為源地址。
對於外設DMA,我們根據傳輸的方向指定源或目的地。換句話說,DMA傳輸需要合適的內存映射。
緩存一致性和DMA
根據內核內存管理的知識,最近訪問的內存區域的副本存儲在緩存中,這也適用於DMA內存。事實上,兩個獨立設備之間共享的內存通常是緩存一致性問題的根源。緩存不連貫的問題來自於其他設備可能不知道寫入設備的更新這一事實。另一方面,緩存一致性確保每個寫操作似乎是瞬間發生的,因此共享相同內存區域的所有設備看到完全相同的變化序列。
下面從LDD3摘錄說明了一個解釋良好的一致性問題場景:
讓我們想象一個配備了緩存和外部內存的CPU,可以由使用DMA的設備直接訪問。當CPU訪問內存中的位置X時,當前值將存儲在緩存中。假設是回寫緩存,X 上的后續操作將更新 X 的緩存副本,但不會更新 X 的外部內存的版本。如果在下一次設備嘗試訪問X之前,緩存沒有刷新到內存中,設備將收到過期的X值。類似地,如果當設備向內存寫入新值時,X的緩存副本沒有失效,那么CPU將對過時的X值進行操作。
有兩種方法可以解決這個問題:
- 一個基於硬件的解決方案。這樣的系統是一致的系統。
- 一種基於軟件的解決方案,其中操作系統負責確保緩存一致性。人們稱這種系統為非一致性系統。
DMA映射
任何合適的DMA傳輸都需要合適的內存映射。DMA映射包括分配DMA緩沖區並為其生成總線地址。設備實際上使用總線地址。總線地址是dma_addr_t類型的每個實例。
我們可以區分兩種類型的映射:一致性DMA映射和流式DMA映射。可以在多個傳輸中使用前者,自動解決緩存一致性問題。但是一致DMA映射資源很寶貴。流映射有很多約束,並且不能自動解決一致性問題,盡管有一個解決方案,即在每個傳輸之間包含幾個函數調用。一致性映射通常存在於驅動程序的生命周期中,而流式映射通常在DMA傳輸完成后取消映射。
我們應該盡可能地使用流式DMA映射,在必要情況下再使用一致性DMA映射。
代碼中應該包括以下頭文件當使用DMA映射時:
#include <linux/dma-mapping.h>
一致性映射
下面的函數用於設置一致性的映射:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)
這個函數處理緩沖區的分配和映射,並返回該緩沖區的內核虛擬地址,該地址的大小為 size 字節,可由CPU訪問。dev 是你的設備結構體。第三個參數是指向相關總線地址的輸出參數。為映射分配的內存保證在物理上是連續的,並且標志決定應該如何分配內存,通常是通過GFP_KERNEL或GFP_ATOMIC(如果我們是在原子上下文中)分配。
請注意,這個映射被稱為:
- 一致性,因為它為設備分配未緩存和未緩沖的內存來執行DMA
- 同步,因為設備或CPU的寫入可以立即被其中任何一個讀取,而無需擔心緩存一致性
釋放一個映射時,可以使用以下函數:
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle);
這里,cpu_addr對應於dma_alloc_coherent()返回的內核虛擬地址。這種映射是昂貴的,它可以分配的最小值是一個頁面。實際上,它只分配2的冪的頁面數。頁面的順序是通過int order = get_order(size)獲得的。應該將此映射用於持續設備生命周期的緩沖區。
流式DMA映射
流式映射約束更多,與一致性映射不同,原因如下:
- 映射需要使用已經分配的緩沖區。
- 映射可以接受幾個不相鄰和分散的緩沖區。
- 一個映射的緩沖區不再屬於CPU,而是屬於設備。CPU可以使用緩沖區之前,它應該首先解除緩沖區映射(在dma_unmap_single() 或 dma_unmap_sg() 之后)。這是為了緩存的目的。
- 對於寫事務(CPU到設備),驅動程序應該將數據放在映射之前的緩沖區中。
- 必須指定數據應該移動的方向,並且數據只能基於這個方向使用。
你可能想知道為什么在未映射之前不應該訪問緩沖區。原因很簡單: CPU映射是可緩存的。dma_map_*()家族函數,用於流映射,將首先清理/使緩存相關的緩沖區無效,並依賴CPU不訪問它,直到相應的dma_unmap_*()。然后,在CPU可以讀取設備寫入內存的任何數據之前,如果有必要,將再次使緩存失效,以防在此期間發生任何投機性的取數據。現在CPU可以訪問緩沖區。
實際上有兩種形式的流式映射:
- 單緩沖區映射,只允許單頁映射
- 分散/聚集映射,允許傳遞多個緩沖區(分散在內存中)
對於任何一個映射,方向都應該由 enum dma_data_direction 類型的符號指定,該符號在 include/linux/dma-direction.h 中定義:
enum dma_data_direction { DMA_BIDIRECTIONAL = 0, DMA_TO_DEVICE = 1, DMA_FROM_DEVICE = 2, DMA_NONE = 3, };
單緩沖區映射
這是為了偶爾映射。可以這樣設置單個緩沖區:
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);
方向應該是DMA_TO_DEVICE、DMA_FROM_DEVICE 或 DMA_BIDIRECTIONAL,如上述代碼所述。ptr 是緩沖區的內核虛擬地址,dma_addr_t 是設備返回的總線地址。確保使用真正適合您需求的方向,而不是總是 DMA_BIDIRECTIONAL。
我們應該這樣釋放映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
分散/聚集映射
分散/聚集映射是一種特殊類型的流式DMA映射,在這種映射中,可以一次性傳輸多個緩沖區區域,而不是逐個映射每個緩沖區並逐個傳輸。假設您有幾個緩沖區,它們在物理上可能不是連續的,所有這些緩沖區都需要同時傳輸到設備或從設備傳輸出去。出現這種情況的原因可能是:
- readv 或者 writev 系統調用
- 磁盤 I/O 請求
- 或者,只是映射的內核I/O緩沖區中的頁面列表
內核將散列表表示為一個一致性結構,struct scatterlist:
struct scatterlist { unsigned long page_link; unsigned int offset; unsigned int length; dma_addr_t dma_address; unsigned int dma_length; };
為了建立散列表映射,應該:
- 分配分散的緩沖區。
- 創建一個散列表的數組,使用 sg_set_buf() 用分配的內存填充它。
- 調用散列表上的 dma_map_sg()。
- 完成DMA之后,調用 dma_unmap_sg() 來取消散列表的映射。
雖然可以通過DMA發送多個緩沖區的內容,每次一個,逐個映射每個緩沖區,分散/聚集映射可以通過向設備發送指向散列表的指針以及長度(即列表中條目的數量)來一次性發送它們:
1 u32 *wbuf, *wbuf2, *wbuf3; 2 wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); 3 wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA); 4 wbuf3 = kzalloc(SDMA_BUF_SIZE/2, GFP_DMA); 5 struct scatterlist sg[3]; 6 sg_init_table(sg, 3); 7 sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE); 8 sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE); 9 sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE/2); 10 ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM);
在“單緩沖區映射”中描述的規則同樣適用於分散/聚集映射:
分散/聚集 DMA
dma_map_sg() 和 dma_unmap_sg() 負責緩存一致性。但是,如果需要使用相同的映射來訪問(讀/寫)DMA傳輸之間的數據,則必須以適當的方式在每次傳輸之間同步緩沖區,如果CPU需要訪問緩沖區,則使用 dma_sync_sg_for_cpu(); 如果是設備,則使用dma_sync_sg_for_device()。類似的單個區域映射函數有 dma_sync_single_for_cpu() 和 dma_sync_single_for_device():
1 void dma_sync_sg_for_cpu(struct device *dev, 2 struct scatterlist *sg, 3 int nents, 4 enum dma_data_direction direction); 5 void dma_sync_sg_for_device(struct device *dev, 6 struct scatterlist *sg, int nents, 7 enum dma_data_direction direction); 8 void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, 9 size_t size, 10 enum dma_data_direction dir) 11 void dma_sync_single_for_device(struct device *dev, 12 dma_addr_t addr, size_t size, 13 enum dma_data_direction dir)
在緩沖區被解除映射后,不需要再次調用前面的函數。
完成的概念
本節將簡要描述DMA傳輸使用的完成和必要的API部分。要獲得完整的描述,請參閱 documentation /scheduler/completion.txt 中的內核文檔。內核編程中的一個常見模式是在當前線程之外初始化一些活動,然后等待該活動完成。
在等待使用緩沖區時,Completion是sleep()的一個很好的替代方法。它適合於感知數據,這正是DMA回調所做的。需要包含這個頭文件:
<linux/completion.h>
像其他內核設施數據結構一樣,可以靜態或動態地創建 struct completion 結構的實例:
- 靜態聲明和初始化方法如下:
DECLARE_COMPLETION(my_comp);
- 動態分配是這樣的:
struct completion my_comp; init_completion(&my_comp);
當驅動程序開始一些必須等待完成的工作(在我們的例子中是DMA事務)時,它只需要將完成事件傳遞給 wait_for_completion() 函數:
void wait_for_completion(struct completion *comp);
當代碼的其他部分已經完成時(事務完成),它可以喚醒任何(實際上代碼需要訪問DMA緩沖區)正在等待的人:
void complete(struct completion *comp); void complete_all(struct completion *comp);
可以猜到,complete() 將只喚醒一個等待的進程,而 complete_all() 將喚醒每個等待該事件的進程。完成的實現方式是,即使在wait_for_completion()之前調用complete(),它們也能正常工作。
通過在下一節中使用的代碼示例,我們將更好地理解這是如何工作的。
DMA引擎API
DMA引擎是用於開發DMA控制器驅動程序的通用內核框架。DMA的主要目標是在復制內存時解放CPU。一種通過使用通道將事務(I/O數據傳輸)委托給DMA引擎。一個DMA引擎,通過它的驅動程序/API,公開了一組可以被其他人使用的通道設備(從機)。DMA引擎布局如下圖所示:
DMA引擎布局
在這里,我們將簡單地瀏覽(從)API,它只適用於從DMA的使用。這里的強制性頭文件如下:
#include <linux/dmaengine.h>
使用從DMA很簡單,包括以下步驟:
- 分配一個DMA從通道
- 設置從 和 控制器特定的參數
- 獲取事務的描述符
- 提交事務
- 發出掛起的請求並等待回調通知
可以將DMA通道視為I/O數據傳輸的高速公路。
分配DMA從通道
可以使用 dma_request_channel() 請求一個通道。其原型如下:
struct dma_chan *dma_request_channel(const dma_cap_mask_t *mask, dma_filter_fn fn, void *fn_param);
mask 是位掩碼,表示信道必須滿足的功能。人們基本上使用它來指定驅動程序需要執行的傳輸類型:
1 enum dma_transaction_type { 2 DMA_MEMCPY, /* Memory to memory copy */ 3 DMA_XOR, /* Memory to memory XOR*/ 4 DMA_PQ, /* Memory to memory P+Q computation */ 5 DMA_XOR_VAL, /* Memory buffer parity check using XOR */ 6 DMA_PQ_VAL, /* Memory buffer parity check using P+Q */ 7 DMA_INTERRUPT, /* The device is able to generate dummy transfer that 8 will generate interrupts */ 9 DMA_SG, /* Memory to memory scatter gather */ 10 DMA_PRIVATE, /* channels are not to be used for global memcpy. 11 Usually used with DMA_SLAVE */ 12 DMA_SLAVE, /* Memory to device transfers */ 13 DMA_CYCLIC, /* Device is able to handle cyclic transfers */ 14 DMA_INTERLEAVE, /* Memory to memory interleaved transfer */ 15 }
dma_cap_zero() 和 dma_cap_set() 函數用於清除掩碼並設置我們需要的功能。例如:
1 dma_cap_mask_t my_dma_cap_mask; 2 struct dma_chan *chan; 3 dma_cap_zero(my_dma_cap_mask); 4 dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); /* Memory to memory copy */ 5 chan = dma_request_channel(my_dma_cap_mask, NULL, NULL);
在前面的摘錄中,dma_filter_fn 定義如下:
typedef bool (*dma_filter_fn)(struct dma_chan *chan, void *filter_param);
如果filter_fn參數(可選)為NULL, dma_request_channel() 將簡單地返回滿足能力掩碼的第一個通道。否則,當掩碼參數不足以指定必要的通道時,可以使用filter_fn例程作為系統中可用通道的過濾器。內核為系統中的每個空閑通道調用一次filter_fn例程。在看到合適的通道時,filter_fn應該返回DMA_ACK,該ack將標記給定的通道為 dma_request_channel() 的返回值。
在調用dma_release_channel()之前,通過該接口分配的通道都是調用方獨占的:
void dma_release_channel(struct dma_chan *chan)
設置從和控制器特定的參數
這一步引入了一個新的數據結構,struct dma_slave_config,它表示DMA從通道的運行時配置。這允許客戶端為外圍設備指定設置,如DMA方向、DMA地址、總線寬度、DMA突發長度等。
int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)
結構體 dma_slave_config 如下所示:
/* * Please refer to the complete description in * include/linux/dmaengine.h */ struct dma_slave_config { enum dma_transfer_direction direction; phys_addr_t src_addr; phys_addr_t dst_addr; enum dma_slave_buswidth src_addr_width; enum dma_slave_buswidth dst_addr_width; u32 src_maxburst; u32 dst_maxburst; [...] };
以下是該結構中每個元素的含義:
- direction: 指示數據是否應該立即在這個從屬通道上輸入或輸出。取值包括:
/* dma transfer mode and direction indicator */ enum dma_transfer_direction { DMA_MEM_TO_MEM, /* Async/Memcpy mode */ DMA_MEM_TO_DEV, /* From Memory to Device */ DMA_DEV_TO_MEM, /* From Device to Memory */ DMA_DEV_TO_DEV, /* From Device to Device */ [...] };
- src_addr: 這是應該讀取DMA從數據(RX)的緩沖區的物理地址(實際上是總線地址)。如果源是內存,則忽略此元素。dst_addr是應該寫入DMA從數據(TX)的緩沖區的物理地址(實際上是總線地址),如果源是內存,則忽略該緩沖區.
- src_addr_width: 這是以字節為單位的源(RX)寄存器的寬度,DMA數據應該在該寄存器中讀取。如果源是內存,根據架構的不同,這可能會被忽略。合法值為1、2、4或8。因此,dst_addr_width與src_addr_width相同,但是用於目的目標(TX)。
- 任何總線寬度必須是下列枚舉之一:
enum dma_slave_buswidth { DMA_SLAVE_BUSWIDTH_UNDEFINED = 0, DMA_SLAVE_BUSWIDTH_1_BYTE = 1, DMA_SLAVE_BUSWIDTH_2_BYTES = 2, DMA_SLAVE_BUSWIDTH_3_BYTES = 3, DMA_SLAVE_BUSWIDTH_4_BYTES = 4, DMA_SLAVE_BUSWIDTH_8_BYTES = 8, DMA_SLAVE_BUSWIDTH_16_BYTES = 16, DMA_SLAVE_BUSWIDTH_32_BYTES = 32, DMA_SLAVE_BUSWIDTH_64_BYTES = 64, };
- src_maxburst:這是可以一次性發送到設備的最大字數(這里,將words作為src_addr_width成員的單位,而不是字節),通常是I/O外設的FIFO深度的一半,這樣就不會溢出。這在內存源上可能適用,也可能不適用。 dst_maxburst 與 src_maxburst 相同,但用於目的目標。
例如:
1 struct dma_chan *my_dma_chan; 2 dma_addr_t dma_src, dma_dst; 3 struct dma_slave_config my_dma_cfg = {0}; 4 5 /* No filter callback, neither filter param */ 6 my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL); 7 8 /* scr_addr and dst_addr are ignored in this structure for mem to mem copy 9 */ 10 my_dma_cfg.direction = DMA_MEM_TO_MEM; 11 my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES; 12 13 dmaengine_slave_config(my_dma_chan, &my_dma_cfg); 14 15 char *rx_data, *tx_data; 16 /* No error check */ 17 rx_data = kzalloc(BUFFER_SIZE, GFP_DMA); 18 tx_data = kzalloc(BUFFER_SIZE, GFP_DMA); 19 20 feed_data(tx_data); 21 22 /* get dma addresses */ 23 dma_src = dma_map_single(NULL, tx_data, 24 BUFFER_SIZE, DMA_MEM_TO_MEM); 25 dma_dst = dma_map_single(NULL, rx_data, 26 BUFFER_SIZE, DMA_MEM_TO_MEM);
在前面的節選中,我們調用 dma_request_channel() 函數以獲得DMA通道的所有權,在該通道上我們調用 dmaengine_slave_config() 來應用它的配置。調用dma_map_single()是為了映射rx和tx緩沖區,以便它們可以用於DMA。
獲取事務的描述符
如果您還記得本節的第一步,當請求DMA通道時,返回值是 struct dma_chan 結構體的一個實例。如果查看 include/linux/dmaengine.h 中的定義,就會注意到它包含一個struct dma_device *device字段,表示提供通道的DMA設備(實際上是控制器)。該控制器的內核驅動程序負責(這是內核API對DMA控制器驅動程序施加的規則)公開一組函數來准備DMA事務,其中每個函數對應一個DMA事務類型(在步驟1中枚舉)。人們別無選擇,只能選擇專用功能。其中一些函數是:
- device_prep_dma_memcpy(): 准備memcpy操作
- device_prep_dma_sg(): 准備一個分散/聚集memcpy操作
- device_prep_dma_xor(): 用於xor操作
- device_prep_dma_xor_val(): 准備xor驗證操作
- device_prep_dma_pq(): 准備pq操作
- device_prep_dma_pq_val(): 准備一個pqzero_sum操作
- device_prep_dma_memset(): 准備一個memset操作
- device_prep_dma_memset_sg(): 用於分散列表上的memset操作
- device_prep_slave_sg(): 准備從DMA操作
- device_prep_interleaved_dma(): 以通用方式傳輸表達式
讓我們看看drivers/dma/imx-sdma.c,這是i.MX6 dma控制器(SDMA)驅動程序。這些函數都返回一個指向struct dma_async_tx_descriptor結構體的指針,該結構體對應於事務描述符。
對於內存到內存復制,將使用device_prep_dma_memcpy:
1 struct dma_device *dma_dev = my_dma_chan->device; 2 struct dma_async_tx_descriptor *tx = NULL; 3 4 tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, 5 dma_src_addr, BUFFER_SIZE, 0); 6 7 if (!tx) { 8 printk(KERN_ERR "%s: Failed to prepare DMA transfer\n", 9 __FUNCTION__); 10 /* dma_unmap_* the buffer */ 11 }
事實上,我們應該使用dmaengine_prep_* DMA引擎API。只需注意,這些函數在內部執行我們剛才執行的操作。例如,對於內存-內存,可以使用dmaengine_prep_dma_memcpy()函數:
static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_memcpy(struct dma_chan *chan, dma_addr_t dest, dma_addr_t src, size_t len, unsigned long flags)
例子如下:
1 struct dma_async_tx_descriptor *tx = NULL; 2 tx = dmaengine_prep_dma_memcpy(my_dma_chan, dma_dst_addr, 3 dma_src_addr, BUFFER_SIZE, 0); 4 if (!tx) { 5 printk(KERN_ERR "%s: Failed to prepare DMA transfer\n", 6 __FUNCTION__); 7 /* dma_unmap_* the buffer */ 8 }
請查看 include/linux/dmaengine.h,在struct dma_device 結構體的定義中,了解所有這些回調是如何實現的。
提交事務
要將事務放入驅動程序掛起隊列中,可以使用dmaengine_submit()。一旦准備好了描述符並添加了回調信息,就應該把它放在DMA引擎驅動程序等待隊列上:
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
該函數返回一個cookie,可以使用該cookie通過其他DMA引擎檢查DMA活動的進度。dmaengine_submit()不會啟動DMA操作,它只是將其添加到掛起隊列中。下一步將討論如何啟動傳輸事務:
1 struct completion transfer_ok; 2 init_completion(&transfer_ok); 3 tx->callback = my_dma_callback; 4 5 /* Submit our dma transfer */ 6 dma_cookie_t cookie = dmaengine_submit(tx); 7 8 if (dma_submit_error(cookie)) { 9 printk(KERN_ERR "%s: Failed to start DMA transfer\n", __FUNCTION__); 10 /* Handle that */ 11 12 [...] 13 }
發出掛起的DMA請求並等待回調通知
啟動事務是DMA傳輸設置的最后一步。可以通過調用通道上的 dma_async_issue_pending() 來激活通道的掛起隊列中的事務。如果通道是空閑的,那么隊列中的第一個事務將啟動,隨后的事務將排隊。DMA操作完成后,隊列中的下一個操作將啟動,並觸發一個微線程。這個微線程負責調用客戶端驅動完成回調例程來通知,如果設置了的話:
void dma_async_issue_pending(struct dma_chan *chan);
示例如下:
1 dma_async_issue_pending(my_dma_chan); 2 wait_for_completion(&transfer_ok); 3 4 dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, 5 BUFFER_SIZE, DMA_MEM_TO_MEM); 6 dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, 7 BUFFER_SIZE, DMA_MEM_TO_MEM); 8 9 /* Process buffer through rx_data and tx_data virtualaddresses. */
wait_for_completion() 函數將阻塞,直到DMA回調函數被調用,它將更新(complete)我們的completion變量,以恢復之前阻塞的代碼。這是一個合適的替代while (!done) msleep(SOME_TIME);
static void my_dma_callback() { complete(transfer_ok); return; }
實際發出掛起事務的DMA引擎API函數是 dmaengine_issue_pending(struct dma_chan *chan),它是對 dma_async_issue_pending() 的一個封裝。
NXP SDMA (i.MX6)
SDMA引擎是i.MX6中的一個可編程控制器,每個外設在這個控制器中都有自己的復制功能。可以使用這個枚舉來確定它們的地址:
enum sdma_peripheral_type { IMX_DMATYPE_SSI, /* MCU domain SSI */ IMX_DMATYPE_SSI_SP, /* Shared SSI */ IMX_DMATYPE_MMC, /* MMC */ IMX_DMATYPE_SDHC, /* SDHC */ IMX_DMATYPE_UART, /* MCU domain UART */ IMX_DMATYPE_UART_SP, /* Shared UART */ IMX_DMATYPE_FIRI, /* FIRI */ IMX_DMATYPE_CSPI, /* MCU domain CSPI */ IMX_DMATYPE_CSPI_SP, /* Shared CSPI */ IMX_DMATYPE_SIM, /* SIM */ IMX_DMATYPE_ATA, /* ATA */ IMX_DMATYPE_CCM, /* CCM */ IMX_DMATYPE_EXT, /* External peripheral */ IMX_DMATYPE_MSHC, /* Memory Stick Host Controller */ IMX_DMATYPE_MSHC_SP, /* Shared Memory Stick Host Controller */ IMX_DMATYPE_DSP, /* DSP */ IMX_DMATYPE_MEMORY, /* Memory */ IMX_DMATYPE_FIFO_MEMORY,/* FIFO type Memory */ IMX_DMATYPE_SPDIF, /* SPDIF */ IMX_DMATYPE_IPU_MEMORY, /* IPU Memory */ IMX_DMATYPE_ASRC, /* ASRC */ IMX_DMATYPE_ESAI, /* ESAI */ IMX_DMATYPE_SSI_DUAL, /* SSI Dual FIFO */ IMX_DMATYPE_ASRC_SP, /* Shared ASRC */ IMX_DMATYPE_SAI, /* SAI */ }
盡管有通用的DMA引擎API,任何構造函數都可以提供自己的自定義數據結構。這是imx_dma_data結構的情況,它是私有數據(用於描述一個需要使用的DMA設備類型),將在過濾器回調中傳遞給struct dma_chan的.private字段:
1 struct imx_dma_data { 2 int dma_request; /* DMA request line */ 3 int dma_request2; /* secondary DMA request line */ 4 enum sdma_peripheral_type peripheral_type; 5 int priority; 6 }; 7 8 enum imx_dma_prio { 9 DMA_PRIO_HIGH = 0, 10 DMA_PRIO_MEDIUM = 1, 11 DMA_PRIO_LOW = 2 12 };
這些結構和枚舉都是特定於i.MX的,並在include/linux/platform_data/dma-imx.h中定義。現在,讓我們編寫內核DMA模塊。它分配兩個緩沖區(源和目的)。用預定義的數據填充源,並執行一個事務以將src復制到dst。可以通過使用來自用戶空間的數據(copy_from_user())來改進這個模塊。這個驅動程序的靈感來自於imx-test包中提供的一個:
1 #include <linux/module.h> 2 #include <linux/slab.h> /* for kmalloc */ 3 #include <linux/init.h> 4 #include <linux/dma-mapping.h> 5 #include <linux/fs.h> 6 #include <linux/version.h> 7 #if (LINUX_VERSION_CODE >= KERNEL_VERSION(3,0,35)) 8 #include <linux/platform_data/dma-imx.h> 9 #else 10 #include <mach/dma.h> 11 #endif 12 13 14 #include <linux/dmaengine.h> 15 #include <linux/device.h> 16 17 18 #include <linux/io.h> 19 #include <linux/delay.h> 20 21 static int gMajor; /* major number of device */ 22 static struct class *dma_tm_class; 23 u32 *wbuf; /* source buffer */ 24 u32 *rbuf; /* destination buffer */ 25 26 27 struct dma_chan *dma_m2m_chan; /* our dma channel */ 28 struct completion dma_m2m_ok; /* completion variable used in the DMA 29 callback */ 30 #define SDMA_BUF_SIZE 1024 //對於單個映射,緩沖區大小不需要是頁面大小的倍數。
讓我們定義過濾器函數。當請求DMA通道時,控制器驅動程序可以在通道列表中執行查找(它已經有了)。對於細粒度查找,可以提供一個回調方法,該方法將在找到的每個通道上調用。然后由回調函數選擇一個合適的通道來使用:
1 static bool dma_m2m_filter(struct dma_chan *chan, void *param) 2 { 3 if (!imx_dma_is_general_purpose(chan)) 4 return false; 5 6 chan->private = param; 7 return true; 8 }
imx_dma_is_general_purpose 是一個特殊的函數,用於檢查控制器驅動程序的名稱。open函數將分配緩沖區並請求DMA通道,給定我們的回調過濾器函數:
1 int sdma_open(struct inode * inode, struct file * filp) 2 { 3 dma_cap_mask_t dma_m2m_mask; 4 struct imx_dma_data m2m_dma_data = {0}; 5 6 init_completion(&dma_m2m_ok); 7 /* 初始化功能 */ 8 dma_cap_zero(dma_m2m_mask); 9 dma_cap_set(DMA_MEMCPY, dma_m2m_mask); /* Set channel capacities */ 10 m2m_dma_data.peripheral_type = IMX_DMATYPE_MEMORY; /* choose the dma 11 device type. This is proper to i.MX */ 12 m2m_dma_data.priority = DMA_PRIO_HIGH; /* we need high priority */ 13 /* 分配一個DMA slave 通道 */ 14 dma_m2m_chan = dma_request_channel(dma_m2m_mask, dma_m2m_filter, 15 &m2m_dma_data); 16 if (!dma_m2m_chan) { 17 printk("Error opening the SDMA memory to memory channel\n"); 18 return -EINVAL; 19 } 20 21 wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); 22 if(!wbuf) { 23 printk("error wbuf !!!!!!!!!!!\n"); 24 return -1; 25 } 26 27 rbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); 28 if(!rbuf) { 29 printk("error rbuf !!!!!!!!!!!\n"); 30 return -1; 31 } 32 33 return 0; 34 }
release函數與open函數完全相反;它釋放緩沖區並釋放DMA通道:
1 int sdma_release(struct inode * inode, struct file * filp) 2 { 3 dma_release_channel(dma_m2m_chan); 4 dma_m2m_chan = NULL; 5 kfree(wbuf); 6 kfree(rbuf); 7 return 0; 8 }
在read函數中,我們只是比較源和目標緩沖區,並將結果通知用戶。
1 ssize_t sdma_read (struct file *filp, char __user * buf, 2 size_t count, loff_t * offset) 3 { 4 int i; 5 for (i=0; i<SDMA_BUF_SIZE/4; i++) { 6 if (*(rbuf+i) != *(wbuf+i)) { 7 printk("Single DMA buffer copy falled!,r=%x,w=%x,%d\n", 8 *(rbuf+i), *(wbuf+i), i); 9 return 0; 10 } 11 } 12 printk("buffer copy passed!\n"); 13 return 0; 14 }
我們使用completion是為了在事務終止時獲得通知(喚醒)。這個回調函數在我們的事務完成后被調用,並將completion變量設置為complete狀態:
1 static void dma_m2m_callback(void *data) 2 { 3 printk("in %s\n",__func__); 4 complete(&dma_m2m_ok); 5 return ; 6 }
在write函數中,我們用數據填充源緩沖區,執行DMA映射以獲取與源和目標緩沖區對應的物理地址,並調用device_prep_dma_memcpy來獲取事務描述符。然后用dmaengine_submit將該事務描述符提交給DMA引擎,它還不會執行我們的事務。只有在我們調用了dma_async_issue_pending待處理事務的DMA通道之后才會執行:
1 ssize_t sdma_write(struct file * filp, const char __user * buf, 2 size_t count, loff_t * offset) 3 { 4 u32 i; 5 struct dma_slave_config dma_m2m_config = {0}; 6 struct dma_async_tx_descriptor *dma_m2m_desc; /* transaction descriptor 7 */ 8 9 dma_addr_t dma_src, dma_dst; 10 11 /* No copy_from_user, we just fill the source buffer with predefined 12 data */ 13 for (i=0; i<SDMA_BUF_SIZE/4; i++) { 14 *(wbuf + i) = 0x56565656; 15 } 16 /* 設置 slave 和 controller 的具體參數 */ 17 dma_m2m_config.direction = DMA_MEM_TO_MEM; 18 dma_m2m_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; 19 dmaengine_slave_config(dma_m2m_chan, &dma_m2m_config); 20 21 dma_src = dma_map_single(NULL, wbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE); 22 dma_dst = dma_map_single(NULL, rbuf, SDMA_BUF_SIZE, DMA_FROM_DEVICE); 23 dma_m2m_desc = 24 dma_m2m_chan->device->device_prep_dma_memcpy(dma_m2m_chan, dma_dst, 25 dma_src, SDMA_BUF_SIZE,0); //獲取事務的描述符 26 if (!dma_m2m_desc) 27 printk("prep error!!\n"); 28 dma_m2m_desc->callback = dma_m2m_callback; 29 dmaengine_submit(dma_m2m_desc); //提交事務 30 dma_async_issue_pending(dma_m2m_chan); //發出掛起的DMA請求並等待回調通知 31 wait_for_completion(&dma_m2m_ok); //也可以使用wait_for_completion_timeout() 32 dma_unmap_single(NULL, dma_src, SDMA_BUF_SIZE, DMA_TO_DEVICE); //一旦DMA傳輸事務完成,我們所需要做的 33 dma_unmap_single(NULL, dma_dst, SDMA_BUF_SIZE, DMA_FROM_DEVICE); 34 35 return 0; 36 } 37 38 struct file_operations dma_fops = { 39 open: sdma_open, 40 release: sdma_release, 41 read: sdma_read, 42 write: sdma_write, 43 };
DMA DT綁定
DMA通道的DT綁定依賴於DMA控制器節點,該節點依賴於SoC,而且某些參數(如DMA單元)可能因SoC而異。這個例子只關注i.m xsdma控制器,你可以在內核源代碼中找到它,文檔/devicetree/bindings/dma/fsl-imx-sdma.txt。
消費者綁定
根據SDMA事件映射表,下面的代碼顯示了i.m x6dual / 6Quad中外設的DMA請求信號:
1 uart1: serial@02020000 { 2 compatible = "fsl,imx6sx-uart", "fsl,imx21-uart"; 3 reg = <0x02020000 0x4000>; 4 interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>; 5 clocks = <&clks IMX6SX_CLK_UART_IPG>, 6 <&clks IMX6SX_CLK_UART_SERIAL>; 7 clock-names = "ipg", "per"; 8 dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; 9 dma-names = "rx", "tx"; 10 status = "disabled"; 11 };
DMA屬性中的第二個單元格(25和26)對應於DMA請求/事件ID。這些值來自SoC手冊(在我們的案例中是i.MX53)。請查看https:/ / community. nxp. com/ servlet/ JiveServlet/ download/ 614186- 1- 373516/ iMX6_Firmware_指南。和Linux參考手冊https://community.nxp.com/servlet/JiveServlet/download/614186-1-373515/i.MX_Lin ux_Reference_Manual.pdf。
第三個單元格表示要使用的優先級。接下來定義請求指定參數的驅動程序代碼。你可以在內核源代碼樹的drivers/tty/serial/imx.c中找到完整的代碼:
1 static int imx_uart_dma_init(struct imx_port *sport) 2 { 3 struct dma_slave_config slave_config = {}; 4 struct device *dev = sport->port.dev; 5 int ret; 6 7 /* Prepare for RX : */ 8 sport->dma_chan_rx = dma_request_slave_channel(dev, "rx"); 9 if (!sport->dma_chan_rx) { 10 [...] /* cannot get the DMA channel. handle error */ 11 } 12 13 slave_config.direction = DMA_DEV_TO_MEM; 14 slave_config.src_addr = sport->port.mapbase + URXD0; 15 slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; 16 /* one byte less than the watermark level to enable the aging timer */ 17 slave_config.src_maxburst = RXTL_DMA - 1; 18 ret = dmaengine_slave_config(sport->dma_chan_rx, &slave_config); 19 if (ret) { 20 [...] /* handle error */ 21 } 22 23 sport->rx_buf = kzalloc(PAGE_SIZE, GFP_KERNEL); 24 if (!sport->rx_buf) { 25 [...] /* handle error */ 26 } 27 28 /* Prepare for TX : */ 29 sport->dma_chan_tx = dma_request_slave_channel(dev, "tx"); 30 if (!sport->dma_chan_tx) { 31 [...] /* cannot get the DMA channel. handle error */ 32 } 33 34 slave_config.direction = DMA_MEM_TO_DEV; 35 slave_config.dst_addr = sport->port.mapbase + URTX0; 36 slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; 37 slave_config.dst_maxburst = TXTL_DMA; 38 ret = dmaengine_slave_config(sport->dma_chan_tx, &slave_config); 39 if (ret) { 40 [...] /* handle error */ 41 } 42 [...] 43 }
這里的神奇調用是 dma_request_slave_channel() 函數,它將根據DMA名稱,使用 of_dma_request_slave_channel() 來解析設備節點(在DT中),以收集通道設置。