在嵌入式系統開發中,高效的資料傳輸至關重要。Linux 核心提供的 DMA 機制允許外設繞過 CPU 直接存取記憶體,顯著提升系統效能。本文將引導讀者逐步瞭解 DMA 驅動程式的開發流程,並輔以程式碼範例說明關鍵步驟,涵蓋驅動程式結構定義、初始化流程、資料寫入操作、DMA 回撥函式處理以及平台驅動的註冊與解除註冊。此外,文章還會探討 Linux 輸入子系統的架構和運作原理,並以加速度計驅動開發為例,講解如何使用輪詢輸入子類別、組態 Device Tree 節點以及進行 I2C 互動等核心技術。

DMA 驅動程式開發

簡介

本章節將探討 Linux 核心中 DMA(Direct Memory Access)驅動程式的開發。DMA 是一種允許外設直接存取記憶體的技術,無需 CPU 干預,從而提高系統效能。我們將透過實作一個簡單的 DMA 驅動程式來展示其開發流程。

驅動程式結構

首先,我們需要定義一個 dma_private 結構來儲存驅動程式的私有資料,包括雜項裝置結構、裝置指標、寫入和讀取緩衝區、DMA 通道以及完成等待結構。

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

內容解密:

  • struct miscdevice dma_misc_device;:用於註冊雜項裝置。
  • struct device *dev;:裝置結構指標。
  • char *wbuf;char *rbuf;:寫入和讀取緩衝區。
  • struct dma_chan *dma_m2m_chan;:DMA 通道指標。
  • struct completion dma_m2m_ok;:完成等待結構,用於等待 DMA 操作完成。

驅動程式初始化

my_probe 函式中,我們進行驅動程式的初始化工作,包括分配記憶體、設定 DMA 功能、請求 DMA 通道以及註冊雜項裝置。

static int my_probe(struct platform_device *pdev)
{
    int retval;
    struct dma_private *dma_device;
    dma_cap_mask_t dma_m2m_mask;

    // 分配 dma_private 結構記憶體
    dma_device = devm_kzalloc(&pdev->dev, sizeof(struct dma_private), GFP_KERNEL);

    // 設定雜項裝置
    dma_device->dma_misc_device.minor = MISC_DYNAMIC_MINOR;
    dma_device->dma_misc_device.name = "sdma_test";
    dma_device->dma_misc_device.fops = &dma_fops;
    dma_device->dev = &pdev->dev;

    // 分配寫入和讀取緩衝區
    dma_device->wbuf = devm_kzalloc(&pdev->dev, SDMA_BUF_SIZE, GFP_KERNEL);
    dma_device->rbuf = devm_kzalloc(&pdev->dev, SDMA_BUF_SIZE, GFP_KERNEL);

    // 設定 DMA 功能
    dma_cap_zero(dma_m2m_mask);
    dma_cap_set(DMA_MEMCPY, dma_m2m_mask);

    // 請求 DMA 通道
    dma_device->dma_m2m_chan = dma_request_channel(dma_m2m_mask, 0, NULL);

    // 註冊雜項裝置
    misc_register(&dma_device->dma_misc_device);

    // 設定平台裝置資料
    platform_set_drvdata(pdev, dma_device);

    return 0;
}

內容解密:

  • 使用 devm_kzalloc 分配 dma_private 結構和緩衝區的記憶體。
  • 設定雜項裝置的名稱、操作函式等,並註冊。
  • 使用 dma_request_channel 請求一個支援 DMA_MEMCPY 功能的 DMA 通道。

寫入函式實作

sdma_write 函式負責處理使用者空間的寫入操作,將資料從使用者空間複製到核心空間的寫入緩衝區,並啟動 DMA 操作將資料從寫入緩衝區複製到讀取緩衝區。

static ssize_t sdma_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
    struct dma_async_tx_descriptor *dma_m2m_desc;
    struct dma_device *dma_dev;
    struct dma_private *dma_priv;
    dma_cookie_t cookie;
    dma_addr_t dma_src;
    dma_addr_t dma_dst;

    // 取得 dma_private 結構指標
    dma_priv = container_of(file->private_data, struct dma_private, dma_misc_device);
    dma_dev = dma_priv->dma_m2m_chan->device;

    // 從使用者空間複製資料到寫入緩衝區
    if (copy_from_user(dma_priv->wbuf, buf, count)) {
        return -EFAULT;
    }

    // 取得 DMA 位址
    dma_src = dma_map_single(dma_priv->dev, dma_priv->wbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE);
    dma_dst = dma_map_single(dma_priv->dev, dma_priv->rbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE);

    // 準備 DMA 操作描述符
    dma_m2m_desc = dma_dev->device_prep_dma_memcpy(dma_priv->dma_m2m_chan, dma_dst, dma_src, SDMA_BUF_SIZE, DMA_CTRL_ACK | DMA_PREP_INTERRUPT);

    // 設定 DMA 回撥函式
    dma_m2m_desc->callback = dma_m2m_callback;
    dma_m2m_desc->callback_param = dma_priv;

    // 初始化完成等待結構
    init_completion(&dma_priv->dma_m2m_ok);

    // 提交 DMA 操作
    cookie = dmaengine_submit(dma_m2m_desc);

    if (dma_submit_error(cookie)) {
        dev_err(dma_priv->dev, "Failed to submit DMA\n");
        return -EINVAL;
    }

    // 啟動 DMA 操作
    dma_async_issue_pending(dma_priv->dma_m2m_chan);

    // 等待 DMA 操作完成
    wait_for_completion(&dma_priv->dma_m2m_ok);

    // 檢查 DMA 操作結果
    dma_async_is_tx_complete(dma_priv->dma_m2m_chan, cookie, NULL, NULL);

    // 解除對映 DMA 位址
    dma_unmap_single(dma_priv->dev, dma_src, SDMA_BUF_SIZE, DMA_TO_DEVICE);
    dma_unmap_single(dma_priv->dev, dma_dst, SDMA_BUF_SIZE, DMA_TO_DEVICE);

    return count;
}

內容解密:

  • 使用 copy_from_user 將資料從使用者空間複製到核心空間的寫入緩衝區。
  • 使用 dma_map_single 取得寫入和讀取緩衝區的 DMA 位址。
  • 使用 device_prep_dma_memcpy 準備 DMA 操作描述符,並設定回撥函式。
  • 使用 dmaengine_submit 提交 DMA 操作,並使用 dma_async_issue_pending 啟動 DMA 操作。
  • 使用 wait_for_completion 等待 DMA 操作完成。

回撥函式實作

dma_m2m_callback 函式是 DMA 操作完成的回撥函式,用於通知驅動程式 DMA 操作已經完成。

static void dma_m2m_callback(void *data)
{
    struct dma_private *dma_priv = data;

    dev_info(dma_priv->dev, "%s\n finished DMA transaction", __func__);
    complete(&dma_priv->dma_m2m_ok);

    if (*(dma_priv->rbuf) != *(dma_priv->wbuf))
        dev_err(dma_priv->dev, "buffer copy failed!\n");
    else
        dev_info(dma_priv->dev, "buffer copy passed!\n");

    dev_info(dma_priv->dev, "wbuf is %s\n", dma_priv->wbuf);
    dev_info(dma_priv->dev, "rbuf is %s\n", dma_priv->rbuf);
}

內容解密:

  • 使用 complete 函式通知等待佇列,DMA 操作已經完成。
  • 檢查讀取緩衝區和寫入緩衝區的內容是否一致,以驗證 DMA 操作的正確性。

深入解析Linux核心模組:DMA在裝置驅動程式中的應用

前言

在Linux裝置驅動程式開發中,DMA(直接記憶體存取)技術扮演著至關重要的角色。DMA允許外設直接存取系統記憶體,從而大大提高了資料傳輸的效率。本篇文章將探討DMA在裝置驅動程式中的實作細節,並透過一個具體的範例來展示其應用。

DMA基礎架構

DMA是一種無需CPU干預的資料傳輸技術,它允許外設(如硬碟、網路卡等)直接與系統記憶體進行資料交換。在Linux核心中,DMA的實作涉及多個層面,包括DMA引擎、DMA通道以及DMA描述符等。

範例程式碼解析

以下是一個根據Linux核心的DMA驅動程式範例,該範例實作了記憶體到記憶體的DMA傳輸。

寫入操作實作

static ssize_t sdma_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    struct dma_private *dma_priv = container_of(file->private_data, struct dma_private, dma_misc_device);
    struct dma_device *dma_dev;
    dma_addr_t dma_src, dma_dst;
    struct dma_async_tx_descriptor *dma_m2m_desc;
    dma_cookie_t cookie;

    dma_dev = dma_priv->dma_m2m_chan->device;

    if (copy_from_user(dma_priv->wbuf, buf, count)) {
        return -EFAULT;
    }

    dev_info(dma_priv->dev, "The wbuf string is %s\n", dma_priv->wbuf);

    dma_src = dma_map_single(dma_priv->dev, dma_priv->wbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE);
    dev_info(dma_priv->dev, "dma_src map obtained");

    dma_dst = dma_map_single(dma_priv->dev, dma_priv->rbuf, SDMA_BUF_SIZE, DMA_TO_DEVICE);
    dev_info(dma_priv->dev, "dma_dst map obtained");

    dma_m2m_desc = dma_dev->device_prep_dma_memcpy(dma_priv->dma_m2m_chan, dma_dst, dma_src, SDMA_BUF_SIZE, DMA_CTRL_ACK | DMA_PREP_INTERRUPT);
    dev_info(dma_priv->dev, "successful descriptor obtained");

    dma_m2m_desc->callback = dma_m2m_callback;
    dma_m2m_desc->callback_param = dma_priv;

    init_completion(&dma_priv->dma_m2m_ok);
    cookie = dmaengine_submit(dma_m2m_desc);

    if (dma_submit_error(cookie)) {
        dev_err(dma_priv->dev, "Failed to submit DMA\n");
        return -EINVAL;
    }

    dma_async_issue_pending(dma_priv->dma_m2m_chan);
    wait_for_completion(&dma_priv->dma_m2m_ok);

    dma_async_is_tx_complete(dma_priv->dma_m2m_chan, cookie, NULL, NULL);

    dev_info(dma_priv->dev, "The rbuf string is %s\n", dma_priv->rbuf);

    dma_unmap_single(dma_priv->dev, dma_src, SDMA_BUF_SIZE, DMA_TO_DEVICE);
    dma_unmap_single(dma_priv->dev, dma_dst, SDMA_BUF_SIZE, DMA_TO_DEVICE);

    return count;
}

內容解密:

  1. DMA裝置初始化:首先,從file->private_data中取得struct dma_private結構體指標,該結構體包含了DMA操作的相關資訊。
  2. 複製使用者空間資料:使用copy_from_user函式將使用者空間的資料複製到核心空間的wbuf緩衝區中。
  3. 對映DMA緩衝區:使用dma_map_single函式將wbufrbuf緩衝區對映為DMA可存取的位址。
  4. 準備DMA描述符:呼叫device_prep_dma_memcpy函式準備一個DMA描述符,用於執行記憶體到記憶體的DMA傳輸。
  5. 設定DMA回呼函式:設定DMA操作的回呼函式dma_m2m_callback,並將dma_private結構體指標作為回呼函式的引數。
  6. 提交DMA操作:使用dmaengine_submit函式提交DMA操作,並檢查提交是否成功。
  7. 啟動DMA操作:呼叫dma_async_issue_pending函式啟動DMA操作。
  8. 等待DMA完成:使用wait_for_completion函式等待DMA操作完成。
  9. 解除DMA對映:使用dma_unmap_single函式解除DMA緩衝區的對映。

平台驅動註冊與解除註冊

static int my_probe(struct platform_device *pdev)
{
    // 初始化DMA私有資料結構
    // 註冊雜項裝置
    // 請求DMA通道
}

static int my_remove(struct platform_device *pdev)
{
    // 解除註冊雜項裝置
    // 釋放DMA通道
}

static struct platform_driver my_platform_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "sdma_m2m",
        .of_match_table = my_of_ids,
        .owner = THIS_MODULE,
    }
};

內容解密:

  1. my_probe函式:在平台裝置匹配時被呼叫,用於初始化DMA私有資料結構、註冊雜項裝置以及請求DMA通道。
  2. my_remove函式:在平台裝置移除時被呼叫,用於解除註冊雜項裝置以及釋放DMA通道。
  3. platform_driver註冊:定義了一個平台驅動結構體,並註冊了探測和移除函式。

輸入子系統(Input Subsystem)詳解

輸入子系統是Linux核心中負責處理來自使用者的所有輸入事件的模組。它統一了輸入裝置驅動的介面,使得不同型別的輸入裝置(如鍵盤、滑鼠、搖桿、觸控式螢幕等)能夠以統一的格式向上層應用程式報告輸入事件。

輸入子系統架構

輸入子系統主要分為兩個部分:

  1. 裝置驅動(Device Drivers):負責與硬體裝置進行互動,捕捉硬體事件(如按鍵、加速計移動、觸控式螢幕座標等),並將其轉換為統一的輸入事件格式(input_event結構),然後報告給輸入核心層。

  2. 事件處理器(Event Handlers):負責接收來自裝置驅動的輸入事件,並將其傳遞給使用者空間的應用程式或核心中的其他消費者。evdev驅動是Linux核心中一個通用的輸入事件介面,它將原始輸入事件進行概括,並透過/dev/input/目錄下的字元裝置檔案提供給使用者空間。

input_event結構

使用者空間的應用程式可以透過讀取/dev/input/event<X>裝置檔案來取得輸入事件。input_event結構的定義如下:

struct input_event {
    struct timeval time;
    unsigned short type;
    unsigned short code;
    unsigned int value;
};

輸入子系統實驗室:加速度計驅動開發

在這個實驗室中,我們將開發一個核心模組,用於控制連線到Raspberry Pi I2C匯流排上的ADXL345加速度計板。驅動程式將週期性地掃描加速度計的一個軸,並根據板的傾斜度生成相應的輸入事件。

輪詢輸入裝置

我們將使用輪詢輸入子類別(polled input subclass)來實作加速度計驅動。輪詢輸入裝置是一種簡單的輸入裝置,它不產生中斷,而是需要定期輪詢以檢測狀態變化。

input_polled_dev結構用於描述一個輪詢輸入裝置,其定義如下:

struct input_polled_dev {
    void *private;
    void (*open)(struct input_polled_dev *dev);
    void (*close)(struct input_polled_dev *dev);
    void (*poll)(struct input_polled_dev *dev);
    unsigned int poll_interval; /* msec */
    unsigned int poll_interval_max; /* msec */
    unsigned int poll_interval_min; /* msec */
    struct input_dev *input;
    /* private: */
    struct delayed_work work;
    bool devres_managed;
};

內容解密:

  1. input_polled_dev 結構:描述輪詢輸入裝置,包含私有資料、開啟和關閉回撥函式、輪詢回撥函式、輪詢間隔等。
  2. poll() 回撥函式:負責輪詢裝置狀態並產生輸入事件。
  3. input_allocate_polled_device()input_free_polled_device():用於分配和釋放 input_polled_dev 結構。
  4. input_register_polled_device()input_unregister_polled_device():用於註冊和登出輪詢輸入裝置。

驅動程式主要程式碼段

  1. Device Tree:在bcm2710-rpi-3-b.dts檔案中新增adxl345@1c節點,以描述ADXL345加速度計裝置。
&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    clock-frequency = <100000>;
    status = "okay";
    [...]
    adxl345@1c {
        compatible = "arrow,adxl345";
        reg = <0x1d>;
    };
};

內容解密:

  • Device Tree 組態:在I2C控制器節點下新增子節點,以描述ADXL345裝置,指定其相容性和I2C地址。
  1. I2C互動:建立i2c_driver結構,並註冊到I2C匯流排。
static struct i2c_driver ioaccel_driver = {
    .driver = {
        .name = "adxl345",
        .owner = THIS_MODULE,
        .of_match_table = ioaccel_dt_ids,
    },
    .probe = ioaccel_probe,
    .remove = ioaccel_remove,
    .id_table = i2c_ids,
};

module_i2c_driver(ioaccel_driver);

內容解密:

  • i2c_driver 結構定義:描述I2C驅動,包括名稱、擁有者、匹配表、探測和移除回撥函式等。
  • module_i2c_driver():用於註冊I2C驅動到核心。