返回文章列表

Linux 驅動程式中斷處理實戰

本文深入解析 Linux 裝置驅動程式中斷處理機制,並以 sleeping device 模組為例,講解如何設計私有結構體、實作 probe

Linux 核心 嵌入式系統

Linux 核心中的中斷處理是驅動程式開發的關鍵環節。本文以 sleeping device 模組為例,逐步講解如何在驅動程式中有效處理中斷。首先,我們定義了一個私有結構體 key_priv,用於儲存裝置資訊,包含 GPIO 描述符、中斷號和等待佇列頭等。接著,在 my_probe 函式中,初始化等待佇列,取得中斷號,並使用 devm_request_irq 註冊中斷處理函式 hello_keys_isr。在 hello_keys_isr 中,讀取 GPIO 狀態,將按鈕按下或釋放的狀態寫入環形緩衝區,並喚醒等待中的行程。my_dev_read 函式則讓使用者空間行程睡眠,直到緩衝區有資料可讀,再將資料複製到使用者空間。最後,透過裝置樹設定和平台驅動程式註冊,完成驅動程式與硬體的匹配。

深入解析裝置驅動程式中的中斷處理

在Linux裝置驅動程式開發中,中斷處理是一項至關重要的技術。本章節將詳細探討如何在裝置驅動程式中有效地處理中斷,並透過一個具體的範例——「sleeping device」模組,來展示相關的實作細節。

私有結構體的設計

首先,我們需要定義一個私有結構體 struct key_priv,用於儲存按鈕裝置的特定資訊。這個結構體包含以下幾個關鍵欄位:

  1. struct device *dev:指向裝置結構的指標。
  2. struct gpio_desc *gpio:與按鈕相關聯的GPIO描述符指標。
  3. struct miscdevice int_miscdevice:用於處理字元裝置的雜項裝置結構。
  4. wait_queue_head_t wq_data_available:等待佇列頭,用於在中斷發生時喚醒等待中的行程。
  5. int irq:Linux中斷號。
struct key_priv {
    struct device *dev;
    struct gpio_desc *gpio;
    struct miscdevice int_miscdevice;
    wait_queue_head_t wq_data_available;
    int irq;
};

程式碼解析:

  • struct key_priv 結構體用於儲存裝置的私有資料,包括裝置結構指標、GPIO描述符、雜項裝置結構、等待佇列頭以及中斷號。
  • 這種設計使得驅動程式能夠有效地管理裝置的相關資源和狀態。

Probe函式的實作

my_probe函式中,我們執行以下關鍵操作:

  1. 初始化等待佇列頭。
  2. 從裝置樹中取得中斷號。
  3. 使用devm_request_irq函式分配中斷線,並註冊中斷處理函式hello_keys_isr
  4. 初始化並註冊雜項裝置。
static int __init my_probe(struct platform_device *pdev)
{
    struct key_priv *priv;
    struct device *dev = &pdev->dev;

    priv = devm_kzalloc(dev, sizeof(struct key_priv), GFP_KERNEL);
    priv->dev = dev;
    platform_set_drvdata(pdev, priv);
    init_waitqueue_head(&priv->wq_data_available);

    priv->gpio = devm_gpiod_get(dev, NULL, GPIOD_IN);
    priv->irq = gpiod_to_irq(priv->gpio);
    priv->irq = platform_get_irq(pdev, 0);
    devm_request_irq(dev, priv->irq, hello_keys_isr,
                     IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                     HELLO_KEYS_NAME, priv);

    priv->int_miscdevice.name = "mydev";
    priv->int_miscdevice.minor = MISC_DYNAMIC_MINOR;
    priv->int_miscdevice.fops = &my_dev_fops;
    misc_register(&priv->int_miscdevice);

    return 0;
}

程式碼解析:

  • my_probe 函式中,我們首先分配並初始化 struct key_priv 結構體。
  • 使用 devm_gpiod_getgpiod_to_irqplatform_get_irq 取得中斷號。
  • 呼叫 devm_request_irq 註冊中斷處理函式 hello_keys_isr,並設定觸發條件為上升沿和下降沿。
  • 初始化並註冊雜項裝置,使其可供使用者空間存取。

中斷處理函式的實作

中斷處理函式hello_keys_isr的主要任務是:

  1. 讀取GPIO輸入值,判斷按鈕是被按下還是釋放。
  2. 將對應的字元(‘P’或’R’)存入緩衝區。
  3. 喚醒等待中的行程。
static irqreturn_t hello_keys_isr(int irq, void *data)
{
    int val;
    struct key_priv *priv = data;

    dev_info(priv->dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
    val = gpiod_get_value(priv->gpio);
    dev_info(priv->dev, "Button state: 0x%08X\n", val);

    if (val == 1)
        hello_keys_buf[buf_wr++] = 'P';
    else
        hello_keys_buf[buf_wr++] = 'R';

    if (buf_wr >= MAX_KEY_STATES)
        buf_wr = 0;

    wake_up_interruptible(&priv->wq_data_available);
    return IRQ_HANDLED;
}

程式碼解析:

  • 在中斷處理函式中,首先讀取GPIO的值以判斷按鈕狀態。
  • 根據按鈕狀態,將對應的字元存入環形緩衝區 hello_keys_buf
  • 使用 wake_up_interruptible 喚醒等待中的行程,使其可以處理新的資料。

使用者空間讀取操作的實作

my_dev_read函式中,我們實作了使用者空間讀取操作的處理邏輯:

  1. 將行程置於睡眠狀態,直到緩衝區中有可用資料。
  2. 將資料從核心空間複製到使用者空間。
static int my_dev_read(struct file *file, char __user *buff,
                       size_t count, loff_t *off)
{
    int ret_val;
    char ch[2];
    struct key_priv *priv = container_of(file->private_data,
                                         struct key_priv, int_miscdevice);

    wait_event_interruptible(priv->wq_data_available, buf_wr != buf_rd);

    ch[0] = hello_keys_buf[buf_rd];
    ch[1] = '\n';
    copy_to_user(buff, &ch, 2);
    buf_rd++;

    if (buf_rd >= MAX_KEY_STATES)
        buf_rd = 0;

    *off += 1;
    return 2;
}

程式碼解析:

  • 使用 wait_event_interruptible 將行程置於睡眠狀態,直到緩衝區中有新資料可用。
  • 將資料從核心空間緩衝區複製到使用者空間,並更新讀取索引。
  • 傳回讀取的位元組數。

裝置樹與驅動程式的匹配

為了使驅動程式能夠正確匹配裝置,我們需要在裝置樹中定義相容的裝置,並在驅動程式中註冊對應的裝置ID表:

static const struct of_device_id my_of_ids[] = {
    { .compatible = "arrow,intkeywait"},
    {},
};
MODULE_DEVICE_TABLE(of, my_of_ids);

程式碼解析:

  • 定義了裝置樹中相容的裝置ID,使驅動程式能夠正確匹配對應的硬體裝置。

平台驅動程式的註冊

最後,我們需要註冊平台驅動程式,使其能夠在系統啟動時被正確載入:

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

程式碼解析:

  • 定義並註冊了平台驅動程式結構,指定了 proberemove 函式,以及裝置樹匹配表。

裝置驅動程式中的中斷處理

在Linux核心中,中斷處理是一項重要的功能,尤其是在裝置驅動程式的開發中。本章節將探討如何在裝置驅動程式中處理中斷,並提供具體的例項來說明相關技術。

中斷處理的基本概念

中斷是硬體裝置用來通知CPU發生特定事件的訊號。當中斷發生時,CPU會暫停目前的任務,並執行中斷處理程式(ISR,Interrupt Service Routine)。在裝置驅動程式中,正確地處理中斷是確保系統穩定性和效能的關鍵。

實作範例:按鍵中斷處理

以下是一個具體的範例,展示如何在Linux裝置驅動程式中處理按鍵中斷。這個範例使用了一個平台驅動程式,當按鍵被按下或釋放時,會觸發中斷。

程式碼解析

首先,我們來看看中斷處理程式hello_keys_isr的實作:

static irqreturn_t hello_keys_isr(int irq, void *data)
{
    struct key_priv *priv = data;
    dev_info(priv->dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
    val = gpiod_get_value(priv->gpio);
    dev_info(priv->dev, "Button state: 0x%08X\n", val);
    if (val == 1)
        hello_keys_buf[buf_wr++] = 'P';
    else
        hello_keys_buf[buf_wr++] = 'R';
    if (buf_wr >= MAX_KEY_STATES)
        buf_wr = 0;
    /* 喚醒行程 */
    wake_up_interruptible(&priv->wq_data_available);
    return IRQ_HANDLED;
}

內容解密:

  1. struct key_priv *priv = data;:取得與中斷相關的私有資料結構。
  2. dev_info(priv->dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);:輸出中斷接收的資訊。
  3. val = gpiod_get_value(priv->gpio);:讀取按鍵目前的狀態。
  4. 根據按鍵狀態,將 ‘P’ 或 ‘R’ 寫入緩衝區hello_keys_buf,分別代表按下和釋放。
  5. wake_up_interruptible(&priv->wq_data_available);:喚醒等待在wq_data_available佇列上的行程。

接著,我們來看讀取裝置檔案的函式my_dev_read

static int my_dev_read(struct file *file, char __user *buff, size_t count, loff_t *off)
{
    int ret_val;
    char ch[2];
    struct key_priv *priv;
    priv = container_of(file->private_data, struct key_priv, int_miscdevice);
    dev_info(priv->dev, "mydev_read_file entered\n");
    ret_val = wait_event_interruptible(priv->wq_data_available, buf_wr != buf_rd);
    if(ret_val)
        return ret_val;
    /* 將資料傳送到使用者應用程式 */
    ch[0] = hello_keys_buf[buf_rd];
    ch[1] = '\n';
    if(copy_to_user(buff, &ch, 2)) {
        return -EFAULT;
    }
    buf_rd++;
    if(buf_rd >= MAX_KEY_STATES)
        buf_rd = 0;
    *off+=1;
    return 2;
}

內容解密:

  1. ret_val = wait_event_interruptible(priv->wq_data_available, buf_wr != buf_rd);:讓行程等待,直到緩衝區中有新的資料。
  2. 將緩衝區中的資料複製到使用者空間,並更新讀取索引buf_rd
  3. 傳回傳送的位元組數。

平台驅動程式註冊與初始化

在驅動程式初始化時,我們需要註冊平台驅動程式並設定相關的回呼函式:

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

內容解密:

  1. .probe = my_probe:當裝置匹配時,呼叫my_probe函式進行初始化。
  2. .remove = my_remove:當裝置移除時,呼叫my_remove函式進行清理。
  3. .of_match_table = my_of_ids:指定相容的裝置樹節點。

實驗結果

載入模組後,可以觀察到按鍵事件被正確捕捉並記錄:

root@raspberrypi:/home/pi# insmod int_rpi3_key_wait.ko
intkeywait soc:int_key_wait: my_probe() function is called.
intkeywait soc:int_key_wait: The IRQ number is: 166
intkeywait soc:int_key_wait: IRQ_using_platform_get_irq: 166
intkeywait soc:int_key_wait: my_probe() function is exited.

root@raspberrypi:/home/pi# cat /dev/mydev > states
intkeywait soc:int_key_wait: mydev_read_file entered
intkeywait soc:int_key_wait: interrupt received. key: PB_USER
intkeywait soc:int_key_wait: Button state: 0x00000001
...

檢查記錄的按鍵狀態:

root@raspberrypi:/home/pi# cat states
P
R
P
R
P
R

這個範例展示瞭如何在Linux裝置驅動程式中處理按鍵中斷,並將事件記錄傳送到使用者空間。透過這個例子,我們可以看到中斷處理和緩衝區管理的實作細節。