在嵌入式系統和高效能運算中,直接記憶體存取(DMA)扮演著關鍵角色,允許周邊裝置直接與系統記憶體交換資料,無需 CPU 干預,從而顯著提升資料傳輸效率並降低 CPU 負擔。然而,使用 DMA 也引入了一些挑戰,例如快取一致性問題。本文將探討 Linux 核心如何處理 DMA 相關議題,並提供裝置驅動程式中使用 DMA 的實務,包含 DMA Engine API 的使用、一致性與串流式 DMA 對映、以及相關核心程式碼的解讀。瞭解這些技術細節,有助於開發人員編寫高效且穩定的裝置驅動程式。

直接記憶體存取(DMA)在裝置驅動程式中的應用

直接記憶體存取(DMA)是一種嵌入式處理器上的系統控制器,允許部分主要周邊裝置(SPI、I2C、UART、通用計時器、DAC和ADC)直接將I/O資料傳輸到主記憶體或從主記憶體讀取,而無需主處理單元的干預。DMA也用於管理RAM資料緩衝區之間的直接資料傳輸,無需CPU干預。在DMA傳輸進行時,CPU可以繼續執行程式碼。當DMA傳輸完成時,DMA系統控制器將透過中斷通知CPU。

DMA的典型應用場景

DMA在區塊記憶體複製的典型應用場景中非常有用,例如網路封包路由和視訊串流應用。在區塊傳輸較大或傳輸是重複操作的情況下,DMA具有明顯的優勢,可以節省大量的CPU處理時間。

快取一致性問題

在使用DMA的快取系統中,主要問題之一是快取內容與系統記憶體之間可能存在不一致性。例如,當CPU存取位於主記憶體中的資料X時,如果X的當前值已被處理器快取,則後續對X的操作將更新X的快取副本,但不會更新外部記憶體中的X版本(假設是寫回式快取)。如果在下一次裝置(DMA)嘗試傳輸X之前沒有將快取重新整理到主記憶體,則裝置將接收到X的過時值。同樣,如果在裝置(DMA)將新值寫入主記憶體之前沒有使X的快取副本無效,則CPU將操作X的過時值。此外,當快取被重新整理時,過時的資料將被寫回主記憶體,覆寫DMA儲存的新資料。最終結果是主記憶體中的資料不正確。

硬體解決方案:匯流排偵聽

一些處理器包含一種稱為匯流排偵聽或快取偵聽的機制。該系統通知快取控制器對DMA記憶體區域的存取,使相應的快取行無效(DMA讀取)或清除(DMA寫入)。這些系統被稱為一致性架構,提供硬體來處理快取一致性相關問題。硬體本身將維護快取和主記憶體之間的一致性,確保所有子系統(CPU和DMA)對記憶體有相同的檢視。

軟體解決方案

對於非一致性架構,裝置驅動程式應在啟動傳輸或使資料緩衝區可供匯流排控制周邊裝置使用之前,明確重新整理或使資料快取無效。這也可能使軟體更加複雜,並導致快取和主記憶體之間更多的傳輸,但它允許應用程式使用任何任意區域的快取記憶體作為資料緩衝區。

Linux核心的DMA操作

Linux核心為ARM處理器提供了兩個dma_map_ops結構,一個用於非一致性架構(arm_dma_ops),另一個用於一致性ARM架構(arm_coherent_dma_ops)。arm_dma_ops不提供額外的硬體支援來進行一致性管理,因此需要軟體來處理。arm_coherent_dma_ops提供硬體來處理快取一致性。

struct dma_map_ops arm_dma_ops = {
    .alloc = arm_dma_alloc,
    .free = arm_dma_free,
    .mmap = arm_dma_mmap,
    .get_sgtable = arm_dma_get_sgtable,
    .map_page = arm_dma_map_page,
    .unmap_page = arm_dma_unmap_page,
    .map_sg = arm_dma_map_sg,
    .unmap_sg = arm_dma_unmap_sg,
    .sync_single_for_cpu = arm_dma_sync_single_for_cpu,
    .sync_single_for_device = arm_dma_sync_single_for_device,
    .sync_sg_for_cpu = arm_dma_sync_sg_for_cpu,
    .sync_sg_for_device = arm_dma_sync_sg_for_device,
};
EXPORT_SYMBOL(arm_dma_ops);

struct dma_map_ops arm_coherent_dma_ops = {
    .alloc = arm_coherent_dma_alloc,
    .free = arm_coherent_dma_free,
    .mmap = arm_coherent_dma_mmap,
    .get_sgtable = arm_dma_get_sgtable,
    .map_page = arm_coherent_dma_map_page,
    .map_sg = arm_dma_map_sg,
};
EXPORT_SYMBOL(arm_coherent_dma_ops);

內容解密:

上述程式碼展示了Linux核心中針對ARM架構的兩種dma_map_ops結構,分別對應非一致性和一致性DMA操作。主要函式包括:

  • .alloc.free:用於分配和釋放DMA緩衝區。
  • .map_page.unmap_page:用於對映和解除對映單個頁面到DMA空間。
  • .map_sg.unmap_sg:用於對映和解除對映分散/聚集列表到DMA空間。
  • .sync_single_for_cpu.sync_single_for_device:用於在CPU和裝置之間同步單個DMA對映。
  • .sync_sg_for_cpu.sync_sg_for_device:用於在CPU和裝置之間同步分散/聚集列表。

這些函式確保了在不同架構下,裝置驅動程式能夠正確地與DMA控制器互動,以實作高效的資料傳輸。

Linux DMA Engine API 詳解

Linux DMA Engine API 為直接記憶體存取(DMA)控制器硬體功能定義了介面,用於初始化、清理和執行 DMA 傳輸。本章節將探討 DMA Engine API 的使用步驟和相關細節。

使用 DMA Engine API 的主要步驟

根據 DMA Engine API ,slave DMA 使用的主要步驟包括:

  1. 分配 DMA slave 通道:客戶端驅動程式通常需要從特定的 DMA 控制器取得通道,甚至在某些情況下需要特定的通道。可以使用 dma_request_chan() API 請求通道。

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

    該函式會找到並傳回與 dev 裝置相關聯的 DMA 通道。透過此介面分配的通道是獨佔的,直到呼叫 dma_release_channel()

  2. 設定 slave 和控制器特定的引數:下一步是將一些特定的資訊傳遞給 DMA 驅動程式。大多數 slave DMA 可以使用的通用資訊都儲存在 dma_slave_config 結構中。這允許客戶端指定 DMA 方向、DMA 位址、匯流排寬度、DMA 突發長度等周邊裝置的引數。

    int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config);
    
  3. 取得交易的描述符:對於 slave 使用,DMA-engine 支援的多種 slave 傳輸模式包括:

    • slave_sg:將分散匯聚緩衝區列表DMA到周邊裝置或從周邊裝置DMA。
    • dma_cyclic:執行從/向周邊裝置的迴圈DMA操作,直到操作被明確停止。
    • interleaved_dma:這對於 slave 和 M2M 客戶端都是通用的。對於 slave 而言,裝置的 FIFO 位址可能已經為驅動程式所知。透過設定 dma_interleaved_template 成員的適當值,可以表達各種操作。
    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);
    
    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);
    
    struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma(
        struct dma_chan *chan, struct dma_interleaved_template *xt,
        unsigned long flags);
    

    內容解密:

    • dmaengine_prep_slave_sg()用於準備分散匯聚DMA傳輸,需要事先對sgl進行DMA對映,並在DMA操作完成前保持對映。
    • dmaengine_prep_dma_cyclic()用於準備迴圈DMA傳輸。
    • dmaengine_prep_interleaved_dma()用於準備交錯DMA傳輸,需要設定dma_interleaved_template結構來描述操作。
  4. 提交交易:一旦描述符準備好並新增了回呼資訊,就必須將其放到 DMA 引擎驅動程式的待處理佇列中。

    dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
    

    內容解密:

    • dmaengine_submit()將描述符新增到待處理佇列,但不會啟動DMA操作。它傳回一個cookie,可用於檢查DMA引擎活動的進度。
  5. 發出待處理的 DMA 請求並等待回呼通知:待處理佇列中的交易可以透過呼叫 issue_pending API 啟動。如果通道空閒,則佇列中的第一個交易將啟動,隨後的交易將排隊。每次DMA操作完成時,下一個佇列中的交易將啟動,並觸發一個tasklet。tasklet 將呼叫客戶端完成回呼例程進行通知(如果已設定)。

    void dma_async_issue_pending(struct dma_chan *chan);
    

    內容解密:

    • dma_async_issue_pending()用於啟動待處理佇列中的DMA交易。它會檢查通道是否空閒,如果是,則啟動佇列中的第一個交易。

Linux DMA API 中的位址型別

在 Linux DMA API 中,涉及不同型別的位址。正如第8章所述,核心通常使用虛擬位址。任何由 kmalloc()vmalloc() 和類別似介面傳回的位址都是虛擬位址。虛擬記憶體系統將虛擬位址轉換為 CPU 物理位址,後者儲存為 phys_addr_tresource_size_t

DMA 在裝置驅動程式中的應用

在 Linux 核心中,DMA(直接記憶體存取)是一種允許周邊裝置直接存取系統記憶體的技術,無需 CPU 干預。驅動程式開發人員需要了解如何正確地使用 DMA,以確保資料的正確傳輸和系統的穩定性。

DMA 對映型別

DMA 對映是一種將 DMA 緩衝區分配和產生可供裝置存取的位址的組合。Linux 核心提供了兩種型別的 DMA 對映:

  1. 一致性 DMA 對映:使用未快取的記憶體對映,通常透過 dma_alloc_coherent() 分配。這種記憶體可以同時被 CPU 和裝置存取,因此需要保證快取一致性。
  2. 串流式 DMA 對映:使用快取的對映,並根據操作使用 dma_map_single()dma_unmap_single() 進行清理或無效化。這種對映通常用於一次性 DMA 傳輸。

一致性 DMA 對映

一致性 DMA 對映透過 dma_alloc_coherent() 分配未快取、未緩衝的記憶體,以供裝置進行 DMA 操作。該函式分配頁面,傳回 CPU 可視的虛擬位址,並設定第三個引數為裝置可視的位址。

程式碼範例

#include <linux/dma-mapping.h>

dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

內容解密:

  • dma_alloc_coherent() 函式用於分配一致性 DMA 記憶體。
  • dev 是裝置指標,size 是要分配的區域大小,gfp 是標準的 GFP 旗標。
  • 該函式傳回兩個值:cpu_addr 虛擬位址(CPU 可用)和 dma_handle DMA 位址(裝置可用)。
  • dma_alloc_coherent() 保證 CPU 虛擬位址和 DMA 位址都對齊到最小的 PAGE_SIZE 順序,且大於或等於請求的大小。

要取消對映和釋放這樣的 DMA 區域,需要呼叫:

dma_free_coherent(dev, size, cpu_addr, dma_handle);

內容解密:

  • dma_free_coherent() 用於取消對映和釋放先前透過 dma_alloc_coherent() 分配的 DMA 區域。
  • devsize 必須與先前的 dma_alloc_coherent() 呼叫相同。
  • cpu_addrdma_handledma_alloc_coherent() 傳回的值。

串流式 DMA 對映

串流式 DMA 對映使用 dma_map_single()dma_unmap_single() 對快取的對映進行清理或無效化。這種對映通常用於一次性 DMA 傳輸。

程式碼範例

#define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)

static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
size_t size,
enum dma_data_direction dir,
unsigned long attrs)
{
    struct dma_map_ops *ops = get_dma_ops(dev);
    dma_addr_t addr;

    kmemcheck_mark_initialized(ptr, size);
    BUG_ON(!valid_dma_direction(dir));

    /* calls arm_dma_map_page for ARM architectures */
    addr = ops->map_page(dev, virt_to_page(ptr),
                         offset_in_page(ptr), size,
                         dir, attrs);

    debug_dma_map_page(dev, virt_to_page(ptr),
                       offset_in_page(ptr), size,
                       dir, addr, true);

    return addr;
}

內容解密:

  • dma_map_single() 用於建立串流式 DMA 對映。
  • 該函式呼叫 dma_map_single_attrs(),並將屬性設為 0。
  • dma_map_single_attrs() 使用 ops->map_page()(對於 ARM 架構,呼叫 arm_dma_map_page())來確保快取中的資料被適當地丟棄或回寫。
  • dir 指定了資料傳輸的方向。
此圖示說明:

本圖示呈現了驅動程式如何透過DMA相關API進行記憶體分配與資料傳輸。左側表示一致性DMA對映的流程,右側表示串流式DMA對映的操作過程,最終實作了裝置與記憶體之間的DMA資料傳輸。

DMA在裝置驅動程式中的應用

DMA(直接記憶體存取)是一種允許周邊裝置直接存取系統記憶體的技術,無需CPU介入。在裝置驅動程式中,DMA用於提高資料傳輸效率,減少CPU負擔。

DMA對映

在進行DMA操作之前,需要將記憶體區域對映到DMA可存取的位址空間。這透過dma_map_single()dma_map_page()函式實作。

arm_dma_map_page()函式

static dma_addr_t arm_dma_map_page(struct device *dev, struct page *page,
                                   unsigned long offset, size_t size, 
                                   enum dma_data_direction dir,
                                   unsigned long attrs)
{
    if ((attrs & DMA_ATTR_SKIP_CPU_SYNC) == 0)
        __dma_page_cpu_to_dev(page, offset, size, dir);
    return pfn_to_dma(dev, page_to_pfn(page)) + offset;
}

內容解密:

  1. arm_dma_map_page()函式用於將一頁記憶體的一部分對映為DMA可存取的位址。
  2. dev引數是有效的裝置指標,page是包含緩衝區的頁面,offset是緩衝區在頁面中的偏移量,size是緩衝區的大小,dir是DMA傳輸方向。
  3. 如果attrs中沒有設定DMA_ATTR_SKIP_CPU_SYNC,則呼叫__dma_page_cpu_to_dev()將快取中的資料寫回記憶體或丟棄,以確保資料一致性。
  4. 函式傳回DMA可存取的位址。

串流DMA對映規則

  1. 緩衝區只能按照指定的方向使用。
  2. 對映後的緩衝區屬於裝置,不屬於處理器。驅動程式必須在解除對映之前避免存取緩衝區。
  3. 用於向裝置傳送資料的緩衝區必須在對映之前包含資料。
  4. 在DMA仍活躍時解除對映將導致系統不穩定。

LAB 9.1:串流DMA模組

在這個實驗中,將開發一個使用串流DMA對映的DMA驅動程式。驅動程式將分配兩個核心緩衝區wbufrbuf,接收來自使用者空間的字元並儲存在wbuf中,然後設定DMA事務(記憶體到記憶體)將資料從wbuf複製到rbuf,最後比較兩個緩衝區是否包含相同的值。

主要程式碼段

  1. 包含必要的頭檔:
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/dmaengine.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>

內容解密:

  • 包含了實作DMA驅動程式所需的頭檔,包括DMA對映函式、檔案系統相關函式等。
  1. 定義私有結構體:
struct dma_private {
    struct miscdevice dma_misc_device;
    struct device *dev;
    char *wbuf;
    char *rbuf;
    struct dma_chan *dma_m2m_chan;
    struct completion dma_m2m_ok;
};

內容解密:

  • dma_private結構體包含驅動程式所需的資訊,如雜項裝置結構、裝置指標、緩衝區指標、DMA通道指標和完成變數。
  • 完成變數用於同步不同執行緒之間的任務。
  1. probe()函式中設定通道能力,分配緩衝區,並請求DMA通道。
// 設定dma_m2m_mask
dma_cap_mask_t dma_m2m_mask;
dma_cap_zero(dma_m2m_mask);
dma_cap_set(DMA_MEMCPY, dma_m2m_mask);

// 分配wbuf和rbuf
// ...

// 請求DMA通道
struct dma_chan *dma_m2m_chan;
dma_m2m_chan = dma_request_channel(dma_m2m_mask, NULL, NULL);

內容解密:

  • 設定dma_m2m_mask以指定DMA通道的能力為記憶體到記憶體的複製。
  • 分配wbufrbuf緩衝區。
  • 使用dma_request_channel()請求一個符合指定能力的DMA通道。