應讀者要求講講 DMA

fans news 發佈 2021-12-14T07:29:47+00:00

DMA 概念介紹DMA 傳輸是由 CPU 發起的:CPU 會告訴 DMA 控制器,幫忙將 source 地方的數據搬到 dest 地方。CPU 發完指令之後,就不管了。具體怎麼搬,何時搬,完全由 DMA 控制器決定。

DMA 概念介紹

DMA 傳輸是由 CPU 發起的:CPU 會告訴 DMA 控制器,幫忙將 source 地方的數據搬到 dest 地方。CPU 發完指令之後,就不管了。具體怎麼搬,何時搬,完全由 DMA 控制器決定。DMA 控制器搬運數據的方向有如下幾種:

何時傳輸(DMA request lines)

因為 CPU 發起 DMA 傳輸的時候,並不知道當前是否具備傳輸條件,例如 source 設備是否有數據、dest 設備的 FIFO 是否空閒等等。那誰知道是否可以傳輸呢?設備!因此,需要設備和 DMA 控制器之間,有幾條物理的連接線(稱作DMA request,DRQ),用於通知 DMA 控制器可以開始傳輸了。

傳輸通道(DMA channels)

DMA 控制器可以同時進行的傳輸個數是有限的,每一個傳輸都需要使用到 DMA 物理通道。DMA 物理通道的數量決定了 DMA 控制器能夠同時傳輸的任務量。

在軟體上,DMA 控制器會為外設分配一個 DMA 虛擬通道,這個虛擬通道是根據 DMA request 信號來區分的。

傳輸參數

  • transfer size

  • transfer width

  • burst size

scatter gather

我們知道,一般情況下 DMA 傳輸只能處理在物理上連續的 buffer。但在有些場景下,我們需要將一些非連續的 buffer 拷貝到一個連續 buffer 中,這樣的操作稱作 scatter gather。

對於這種非連續的傳輸,大多時候都是通過軟體,將傳輸分成多個連續的小塊。但為了提高傳輸效率,有些 DMA controller 從硬體上支持了這種操作。

DMA 客戶端設備驅動

我們先看下一個 DMA 客戶端設備驅動涉及到的數據結構有哪些,然後再看下使用 dmaengine 內核框架的步驟。

數據結構

  • dma_slave_config

完成一次 DMA 傳輸所需要的參數:

struct dma_slave_config {
/*
指明傳輸的方向
DMA_MEM_TO_MEM,memory到memory的傳輸;
DMA_MEM_TO_DEV,memory到設備的傳輸;
DMA_DEV_TO_MEM,設備到memory的傳輸;
DMA_DEV_TO_DEV,設備到設備的傳輸。
*/
enum dma_transfer_direction direction;
/*
傳輸方向是dev2mem或者dev2dev時,讀取數據的位置(通常是固定的FIFO地址)。
對mem2dev類型的channel,不需配置該參數(每次傳輸的時候會指定);
*/
phys_addr_t src_addr;
/*
傳輸方向是mem2dev或者dev2dev時,寫入數據的位置(通常是固定的FIFO地址)。
對dev2mem類型的channel,不需配置該參數(每次傳輸的時候會指定);
*/
phys_addr_t dst_addr;
//src地址的寬度
enum dma_slave_buswidth src_addr_width;
//dst地址的寬度
enum dma_slave_buswidth dst_addr_width;
//src最大可傳輸的burst size
u32 src_maxburst;
//dst最大可傳輸的burst size
u32 dst_maxburst;
u32 src_port_window_size;
u32 dst_port_window_size;
u32 src_fifo_num;
u32 dst_fifo_num;
bool device_fc;
/*
外部設備通過slave_id告訴dma controller自己是誰(一般和某個request line對應)。
很多dma controller並不區分slave,只要給它src、dst、len等信息,它就可以進行傳輸,因此slave_id可以忽略。
而有些controller,必須清晰地知道此次傳輸的對象是哪個外設,就必須要提供slave_id了。
*/
unsigned int slave_id;
};
  • dma_async_tx_descriptor

用於描述一次 DMA 傳輸:

struct dma_async_tx_descriptor {
dma_cookie_t cookie;
/*
DMA_CTRL_開頭的標記,包括:
DMA_CTRL_REUSE,表明這個描述符可以被重複使用,直到它被清除或者釋放;
DMA_CTRL_ACK,如果該flag為0,表明暫時不能被重複使用。
*/
enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
//該描述符的物理地址
dma_addr_t phys;
//對應的dma channel
struct dma_chan *chan;
/*
controller driver提供的回調函數,用於把改描述符提交到待傳輸列表。
通常由dma engine調用,client driver不會直接和該接口打交道。
*/
dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
/*
用於釋放該描述符的回調函數。
通常由dma engine調用,client driver不會直接和該接口打交道。
*/
int (*desc_free)(struct dma_async_tx_descriptor *tx);
//傳輸完成的回調函數(及其參數),由client driver提供。
dma_async_tx_callback callback;
dma_async_tx_callback_result callback_result;
//傳輸完成的回調函數(及其參數),由client driver提供。
void *callback_param;
struct dmaengine_unmap_data *unmap;
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
struct dma_async_tx_descriptor *next;
struct dma_async_tx_descriptor *parent;
spinlock_t lock;
#endif
};

設備驅動使用 dmaengine 的步驟

一個設備驅動程序使用 dmaengine 的話一般步驟如下:

  1. 申請一個 DMA channel。

  2. 根據設備(slave)的特性,配置 DMA channel 的參數。

  3. 要進行 DMA 傳輸的時候,獲取一個用於識別本次傳輸(transaction)的描述符(descriptor)。

  4. 將本次傳輸(transaction)提交給 DMA Engine 並啟動傳輸。

  5. 等待傳輸(transaction)結束。

然後,重複3~5步。下面具體看下每一步的實現和相關接口:

  • 申請 DMA channel

任何設備驅動在開始 DMA 傳輸之前,都要申請一個 DMA channel(由「struct dma_chan」數據結構表示),可以通過如下的 API 申請 DMA channel:

struct dma_chan *dma_request_chan(struct device *dev, const char *name);

可以用如下接口釋放申請得到的 DMA channel:

void dma_release_channel(struct dma_chan *chan);

  • 配置 DMA channel 的參數

設備驅動申請到一個為自己使用的 DMA channel 之後,需要根據自身的實際情況,以及 DMA controller 的能力,對該 channel 進行一些配置。設備驅動將它們填充到一個 struct dma_slave_config 變量中後,可以調用如下 API 將這些信息告訴給 DMA 控制器:

int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

  • 獲取傳輸描述

DMA 傳輸屬於異步傳輸,在啟動傳輸之前,設備驅動需要將此次傳輸的一些信息(例如src/dst的buffer、傳輸的方向等)提交給 DMA 控制器,DMA 控制器確認好後,返回一個描述符。此後,設備驅動就可以以該描述符為單位,控制並跟蹤此次傳輸。設備驅動可以使用下面三個 API 獲取傳輸描述符:

用於在「scatter gather buffers」列表和總線設備之間進行 DMA 傳輸:

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg( struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_data_direction direction, unsigned long flags);

用於音頻等場景中,在進行一定長度的dma傳輸(buf_addr&buf_len)的過程中,每傳輸一定的byte(period_len),就會調用一次傳輸完成的回調函數:

struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_data_direction direction);

可進行不連續的、交叉的DMA傳輸,通常用在圖像處理、顯示等場景中:

struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma( struct dma_chan *chan, struct dma_interleaved_template *xt, unsigned long flags);

  • 啟動傳輸

獲取傳輸描述符之後,設備驅動可以通過 dmaengine_submit 接口將該描述符放到傳輸隊列上,然後調用 dma_async_issue_pending 接口,啟動傳輸。

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);

void dma_async_issue_pending(struct dma_chan *chan);

由這兩個 API 的特徵可知,內核 DMA Engine 鼓勵設備驅動一次提交多個傳輸,然後由 DMA 控制器統一完成這些傳輸。

  • 等待傳輸結束

傳輸請求被提交之後,設備驅動可以通過回調函數獲取傳輸完成的消息,當然,也可以通過 dma_async_is_tx_complete 等 API,測試傳輸是否完成。如果等不及了,也可以使用 dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx 等 API,暫停、終止傳輸。

eDMA(DMA 控制器) 驅動

DMA 控制器驅動主要負責抽象 eDMA 硬體,管理 DMA channel,以 channel 為操作對象,響應 設備驅動的傳輸請求,並控制 eDMA 驅動,執行傳輸。

內核的 dmaengine framework 提供了一套 DMA 控制器的開發框架,如下圖所示:

  1. 使用 struct dma_device 抽象 eDMA,eDMA 驅動只要填充該結構中必要的欄位。

  2. 使用struct dma_chan(即圖中的 DCn)抽象物理的 DMA channel(即圖中的 CHn),DCn 和 CHn 一一對應。

  3. 基於物理的 DMA channel,使用 struct virt_dma_chan 抽象出虛擬的 dma channel(即圖中的 VCx)。多個虛擬 channel 可以共享一個物理 channel,並在這個物理 channel 上進行分時傳輸。

下面我們看下 dma_device,dma_chan,virt_dma_chan 的數據結構。

數據結構

  • dma_device

主要用於抽象 eDMA:

struct dma_device {
//一個鍊表頭,用於保存該controller支持的所有dma channel
//在初始化的時候,dma controller driver首先要調用 INIT_LIST_HEAD 初始化它,然後調用 list_add_tail 將所有的 channel 添加到該鍊表頭中。
unsigned int chancnt;
unsigned int privatecnt;
struct list_head channels;
......
/*
一個bitmap,用於指示該dma controller所具備的能力
DMA_MEMCPY,可進行memory copy;
DMA_MEMSET,可進行memory set;
DMA_SG,可進行 scatter list 傳輸;
DMA_CYCLIC,可進行cyclic類的傳輸;
DMA_INTERLEAVE,可進行交叉傳輸;
*/
dma_cap_mask_t cap_mask;
......
int dev_id;
struct device *dev;

//表示該controller支持哪些寬度的src類型。具體可參考 enum dma_slave_buswidth 的定義
u32 src_addr_widths;
//表示該controller支持哪些寬度的dst類型。具體可參考 enum dma_slave_buswidth 的定義
u32 dst_addr_widths;
/*
表示該controller支持哪些傳輸方向
包括DMA_MEM_TO_MEM、DMA_MEM_TO_DEV、DMA_DEV_TO_MEM、DMA_DEV_TO_DEV,具體可參考enum dma_transfer_direction的定義
*/
u32 directions;
//支持的最大的burst傳輸的size
u32 max_burst;
//指示該controller的傳輸描述可否可重複使用
bool descriptor_reuse;
enum dma_residue_granularity residue_granularity;

//申請/釋放 dma channel 的時候,dmaengine會調用dma controller driver相應的alloc/free回調函數,以準備相應的資源。
int (*device_alloc_chan_resources)(struct dma_chan *chan);
void (*device_free_chan_resources)(struct dma_chan *chan);

//設備驅動通過 dmaengine_prep_xxx API 獲取傳輸描述符的時候,damengine則會直接回調 eDMA 驅動相應的 device_prep_dma_xxx 接口。
struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(
struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
size_t len, unsigned long flags);
......
struct dma_async_tx_descriptor *(*device_prep_dma_imm_data)(
struct dma_chan *chan, dma_addr_t dst, u64 data,
unsigned long flags);

//設備驅動調用 dmaengine_slave_config 配置 dma channel 的時候,dmaengine 會調用該回調函數,交給 eDMA 驅動處理。
int (*device_config)(struct dma_chan *chan,
struct dma_slave_config *config);

//設備驅動調用 dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx 等API的時候,dmaengine 會調用相應的回調函數,交給 eDMA 驅動處理。
int (*device_pause)(struct dma_chan *chan);
......
void (*device_synchronize)(struct dma_chan *chan);

enum dma_status (*device_tx_status)(struct dma_chan *chan,
dma_cookie_t cookie,
struct dma_tx_state *txstate);
//設備驅動調用 dma_async_issue_pending 啟動傳輸的時候,會調用調用該回調函數。
void (*device_issue_pending)(struct dma_chan *chan);
};
  • dma_chan

主要用於抽象物理的 DMA channel:

struct dma_chan {
//指向該channel所在的dma controller
struct dma_device *device;
//設備驅動以該 channel 為操作對象獲取傳輸描述符時,eDMA 驅動返回給設備的最後一個cookie。
dma_cookie_t cookie;
//在這個 channel 上最後一次完成的傳輸的 cookie。eDMA 驅動可以在傳輸完成時調用輔助函數 dma_cookie_complete 設置它的 value。
dma_cookie_t completed_cookie;

/* sysfs */
int chan_id;
struct dma_chan_dev *dev;

//鍊表 node,用於將該 channel 添加到 dma_device 的 channel 列表中
struct list_head device_node;
struct dma_chan_percpu __percpu *local;
int client_count;
int table_count;

/* DMA router */
struct dma_router *router;
void *route_data;

void *private;
};
  • virt_dma_chan

主要用於抽象一個虛擬的 DMA channel:

struct virt_dma_chan {
//一個 struct dma_chan 類型的變量,用於和設備驅動打交道(屏蔽物理channel和虛擬channel的差異)。
struct dma_chan chan;
//一個 tasklet,用於等待該虛擬 channel 上傳輸的完成(由於是虛擬channel,傳輸完成與否只能由軟體判斷)。
struct tasklet_struct task;
void (*desc_free)(struct virt_dma_desc *);

spinlock_t lock;

/* protected by vc.lock */
//用於保存不同狀態的虛擬 channel 描述符(struct virt_dma_desc,僅僅對 struct dma_async_tx_descriptor 做了一個簡單的封裝)。
struct list_head desc_allocated;
struct list_head desc_submitted;
struct list_head desc_issued;
struct list_head desc_completed;

struct virt_dma_desc *cyclic;
};

eDMA 使用 dmaengine 的接口

damengine 直接向 DMA 控制器驅動提供的API並不多,主要包括:

  • struct dma_device 變量的註冊和註銷接口:

int dma_async_device_register(struct dma_device *device);

void dma_async_device_unregister(struct dma_device *device);

  • cookie 有關的輔助接口:

初始化dma channel中的cookie、completed_cookie欄位:

static inline void dma_cookie_init(struct dma_chan *chan)

為指針的傳輸描述(tx)分配一個cookie:

static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)

當某一個傳輸(tx)完成的時候,可以調用該接口,更新該傳輸所對應channel的completed_cookie欄位:

static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)

獲取指定channel(chan)上指定cookie的傳輸狀態:

static inline enum dma_status dma_cookie_status(struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *state)

eDMA 使用 dmaengine 的步驟

  1. 定義一個 struct dma_device 變量,並根據實際的硬體情況,填充其中的關鍵欄位。

  2. 根據 eDMA 支持的 channel 個數,為每個 channel 定義一個 struct dma_chan 變量,進行必要的初始化後,將每個 channel 都添加到 struct dma_device 變量的 channels 鍊表中。

  3. 根據硬體特性,實現 struct dma_device 變量中必要的回調函數(device_alloc_chan_resources/device_free_chan_resources、device_prep_dma_xxx、device_config、device_issue_pending等等)。

  4. 調用 dma_async_device_register 將 struct dma_device 變量註冊到 kernel 中。

  5. 當 DMA 客戶端設備驅動申請 dma channel 時(例如通過 device tree 中的 dma 節點獲取),dmaengine core 會調用 eDMA 驅動的 device_alloc_chan_resources 函數,controller driver需要在這個接口中獎該channel的資源準備好。

  6. 當 DMA 客戶端設備驅動配置某個 dma channel 時,dmaengine core 會調用 eDMA 驅動的 device_config 函數,eDMA 驅動需要在這個函數中將 DMA 客戶端設備驅動想配置的內容準備好,以便進行後續的傳輸。

  7. DMA 客戶端設備驅動開始一個傳輸之前,會把傳輸的信息通過 dmaengine_prep_slave_xxx 接口交給eDMA 驅動,eDMA 驅動需要在對應的 device_prep_dma_xxx 回調中,將這些要傳輸的內容準備好,並返回給DMA 客戶端設備驅動一個傳輸描述符。

  8. 然後,DMA 客戶端設備驅動會調用 dmaengine_submit 將該傳輸提交給 eDMA 驅動,此時 dmaengine 會調用 eDMA 驅動為每個傳輸描述符所提供的 tx_submit 回調函數,eDMA 驅動需要在這個函數中將描述符掛到該 channel 對應的傳輸隊列中。

  9. DMA 客戶端設備驅動開始傳輸時,會調用 dma_async_issue_pending,eDMA 驅動需要在對應的回調函數(device_issue_pending)中,依次將隊列上所有的傳輸請求提交給硬體。

Dynamic DMA mapping

待續...

5T技術資源大放送!包括但不限於:C/C++,Arm, Linux,Android,人工智慧,單片機,樹莓派,等等。在公眾號內回復「peter」,即可免費獲取!!

關鍵字: