在嵌入式系統開發中,高效的資料傳輸至關重要。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;
}
內容解密:
- DMA裝置初始化:首先,從
file->private_data中取得struct dma_private結構體指標,該結構體包含了DMA操作的相關資訊。 - 複製使用者空間資料:使用
copy_from_user函式將使用者空間的資料複製到核心空間的wbuf緩衝區中。 - 對映DMA緩衝區:使用
dma_map_single函式將wbuf和rbuf緩衝區對映為DMA可存取的位址。 - 準備DMA描述符:呼叫
device_prep_dma_memcpy函式準備一個DMA描述符,用於執行記憶體到記憶體的DMA傳輸。 - 設定DMA回呼函式:設定DMA操作的回呼函式
dma_m2m_callback,並將dma_private結構體指標作為回呼函式的引數。 - 提交DMA操作:使用
dmaengine_submit函式提交DMA操作,並檢查提交是否成功。 - 啟動DMA操作:呼叫
dma_async_issue_pending函式啟動DMA操作。 - 等待DMA完成:使用
wait_for_completion函式等待DMA操作完成。 - 解除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,
}
};
內容解密:
my_probe函式:在平台裝置匹配時被呼叫,用於初始化DMA私有資料結構、註冊雜項裝置以及請求DMA通道。my_remove函式:在平台裝置移除時被呼叫,用於解除註冊雜項裝置以及釋放DMA通道。platform_driver註冊:定義了一個平台驅動結構體,並註冊了探測和移除函式。
輸入子系統(Input Subsystem)詳解
輸入子系統是Linux核心中負責處理來自使用者的所有輸入事件的模組。它統一了輸入裝置驅動的介面,使得不同型別的輸入裝置(如鍵盤、滑鼠、搖桿、觸控式螢幕等)能夠以統一的格式向上層應用程式報告輸入事件。
輸入子系統架構
輸入子系統主要分為兩個部分:
裝置驅動(Device Drivers):負責與硬體裝置進行互動,捕捉硬體事件(如按鍵、加速計移動、觸控式螢幕座標等),並將其轉換為統一的輸入事件格式(
input_event結構),然後報告給輸入核心層。事件處理器(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;
};
內容解密:
input_polled_dev結構:描述輪詢輸入裝置,包含私有資料、開啟和關閉回撥函式、輪詢回撥函式、輪詢間隔等。poll()回撥函式:負責輪詢裝置狀態並產生輸入事件。input_allocate_polled_device()和input_free_polled_device():用於分配和釋放input_polled_dev結構。input_register_polled_device()和input_unregister_polled_device():用於註冊和登出輪詢輸入裝置。
驅動程式主要程式碼段
- 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地址。
- 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驅動到核心。