返回文章列表

Linux裝置驅動程式中斷處理

本文探討在 Linux 裝置驅動程式中處理硬體中斷的機制,涵蓋中斷處理函式的設計、裝置樹設定、平台驅動程式模型的應用以及延遲工作機制的運用。以 Raspberry Pi 上的按鈕中斷裝置為例,詳細說明瞭如何撰寫中斷處理程式、註冊中斷、處理中斷分享以及使用 Tasklets 和 Softirqs

系統程式 嵌入式系統

在 Linux 系統中,硬體中斷是驅動程式與硬體互動的重要機制。有效的中斷處理能讓系統即時回應硬體事件,本文將以 Raspberry Pi 的按鈕中斷為例,解析 Linux 驅動程式中斷處理的流程與核心概念,包含中斷處理函式的撰寫、裝置樹的設定、平台驅動程式模型的註冊,以及如何運用 Tasklets 和 Softirqs 等延遲工作機制來提升系統的反應速度和效率。理解這些核心機制對於開發高效能且穩定的 Linux 驅動程式至關重要,能確保系統在處理硬體事件時保持穩定和快速回應。

中斷處理在裝置驅動程式中的應用

在Linux裝置驅動程式開發中,中斷處理是一種關鍵機制,允許硬體裝置在需要時通知CPU。本章節將探討如何在裝置驅動程式中處理中斷,並透過一個按鈕中斷裝置的實驗室練習來演示相關概念。

中斷處理基礎

在Linux核心中,中斷處理函式的傳回型別是irqreturn_t。這個列舉型別定義了三種可能的傳回值:IRQ_NONEIRQ_HANDLEDIRQ_WAKE_THREAD

irqreturn_t列舉定義

enum irqreturn {
    IRQ_NONE = (0 << 0),
    IRQ_HANDLED = (1 << 0),
    IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
  • IRQ_NONE:表示中斷不是由該裝置產生的,或者未被處理。
  • IRQ_HANDLED:表示中斷已被裝置處理。
  • IRQ_WAKE_THREAD:請求喚醒處理該中斷的執行緒。

中斷分享與處理

驅動程式應該盡可能支援中斷分享,這需要能夠檢測硬體是否觸發了中斷。當中斷發生時,驅動程式會被呼叫,如果它確定是自己的硬體觸發了中斷,則傳回IRQ_HANDLED;否則,傳回IRQ_NONE,允許核心呼叫下一個中斷處理程式。

實驗室練習:按鈕中斷裝置模組

這個實驗室練習將實作一個簡單的驅動程式,用於管理按鈕產生的中斷。

硬體描述

使用MikroElektronika Button R click板,將其INT引腳連線到Raspberry Pi的GPIO23引腳。

裝置樹描述

需要在裝置樹中組態GPIO23為輸入,並啟用內部下拉電阻。當按鈕被按下時,GPIO輸入值被設為Vcc,釋放時設為GND,如果在request_irq()函式中傳遞了IRQF_TRIGGER_FALLING標誌,則會產生中斷。

&gpio {
    key_pin: key_pin {
        brcm,pins = <23>;
        brcm,function = <0>; /* Input */
        brcm,pull = <1>; /* Pull down */
    };
};

&soc {
    int_key {
        compatible = "arrow,intkey";
        pinctrl-names = "default";
        pinctrl-0 = <&key_pin>;
        gpios = <&gpio 23 0>;
        interrupts = <23 1>;
        interrupt-parent = <&gpio>;
    };
};

程式碼描述

驅動程式的主要部分包括:

  1. 包含必要的標頭檔案
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>
  1. probe()函式中取得Linux IRQ號

使用兩種方法取得Linux IRQ號:透過devm_gpiod_get()gpiod_to_irq(),或透過platform_get_irq()

static int __init my_probe(struct platform_device *pdev)
{
    int ret_val, irq;
    struct gpio_desc *gpio;
    struct device *dev = &pdev->dev;

    /* 第一種方法取得虛擬Linux IRQ號 */
    gpio = devm_gpiod_get(dev, NULL, GPIOD_IN);
    irq = gpiod_to_irq(gpio);

    /* 第二種方法取得虛擬Linux IRQ號 */
    irq = platform_get_irq(pdev, 0);

    devm_request_irq(dev, irq, hello_keys_isr, IRQF_TRIGGER_FALLING, HELLO_KEYS_NAME, dev);
    misc_register(&helloworld_miscdevice);
    return 0;
}

#### 內容解密:

  • devm_gpiod_get(dev, NULL, GPIOD_IN):用於從裝置樹中的gpios屬性取得GPIO描述符,並將其組態為輸入模式。
  • gpiod_to_irq(gpio):將GPIO描述符轉換為對應的Linux IRQ號。
  • platform_get_irq(pdev, 0):從裝置樹中的interrupts屬性取得硬體IRQ號,並傳回對應的Linux IRQ號。
  • devm_request_irq(dev, irq, hello_keys_isr, IRQF_TRIGGER_FALLING, HELLO_KEYS_NAME, dev):請求IRQ線,並註冊中斷處理函式hello_keys_isr,當檢測到下降沿觸發的中斷時呼叫。

這個實驗室練習展示瞭如何在Linux裝置驅動程式中處理中斷,包括組態裝置樹、取得IRQ號、註冊中斷處理函式等關鍵步驟。透過這些知識,可以開發出能夠有效處理硬體事件的驅動程式。

在裝置驅動程式中處理中斷(Handling Interrupts in Device Drivers)

簡介

在 Linux 驅動程式開發中,處理硬體中斷是一項關鍵任務。本文將探討如何在裝置驅動程式中處理中斷,以 Raspberry Pi 為例,使用平台驅動程式模型實作按鈕中斷的處理。

驅動程式實作步驟

1. 中斷處理函式的編寫

中斷處理函式(Interrupt Handler)是驅動程式的核心部分,負責在硬體中斷發生時執行相應的處理。以下是一個範例:

static irqreturn_t hello_keys_isr(int irq, void *data)
{
    struct device *dev = data;
    dev_info(dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
    return IRQ_HANDLED;
}

內容解密:

  • irqreturn_t 是中斷處理函式的傳回型別,表示中斷處理的結果。
  • hello_keys_isr 函式接收兩個引數:irq(中斷號)和 data(與中斷相關的資料,通常是裝置結構指標)。
  • 透過 dev_info 列印中斷接收訊息到控制檯。
  • 傳回 IRQ_HANDLED 表示中斷已被正確處理。

2. 宣告支援的裝置列表

驅動程式需要宣告它所支援的裝置,這是透過 of_device_id 結構完成的:

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

內容解密:

  • .compatible 欄位指定了驅動程式支援的裝置相容字串。
  • MODULE_DEVICE_TABLE 巨集註冊裝置表格,使其可供核心使用。

3. 平台驅動程式結構註冊

平台驅動程式結構包含了驅動程式的回撥函式和相關資訊:

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

內容解密:

  • .probe.remove 分別是驅動程式的初始化和移除回撥函式。
  • .driver.name 指定了驅動程式的名稱。
  • .of_match_table 指向支援的裝置列表。
  • .owner 指定了模組的所有者,通常是 THIS_MODULE

4. 向平台匯流排註冊驅動程式

module_platform_driver(my_platform_driver);

內容解密:

  • 這行巨集呼叫註冊了平台驅動程式結構,使其可供平台匯流排使用。

程式碼範例完整呈現

以下是完整的 int_rpi3_key.c 程式碼,包含了上述所有部分:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>

static char *HELLO_KEYS_NAME = "PB_KEY";

static irqreturn_t hello_keys_isr(int irq, void *data)
{
    struct device *dev = data;
    dev_info(dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
    return IRQ_HANDLED;
}

static int __init my_probe(struct platform_device *pdev)
{
    // 省略部分程式碼...
    return 0;
}

static int __exit my_remove(struct platform_device *pdev)
{
    // 省略部分程式碼...
    return 0;
}

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

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

module_platform_driver(my_platform_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alberto Liberal <[email protected]>");
MODULE_DESCRIPTION("This is a button INT platform driver");

Linux 核心中的中斷處理與延遲工作機制

Linux 核心在處理硬體中斷時,採用了兩種不同的執行上下文:行程上下文(Process Context)與中斷上下文(Interrupt Context)。這兩種上下文決定了核心程式碼的執行方式與限制。

行程上下文與中斷上下文

  1. 行程上下文:當核心代表使用者行程執行時,例如執行系統呼叫的核心服務例程,就處於行程上下文中。此外,由工作佇列(Workqueues)和執行緒中斷(Threaded Interrupts)排程的延遲工作也在行程上下文中執行。這些核心執行緒在核心空間的行程上下文中執行,但不代表任何使用者行程。行程上下文中的程式碼可以阻塞(Block)。

  2. 中斷上下文:當硬體中斷控制器發出請求時,核心會非同步地執行中斷處理程式,這種特殊的上下文稱為「原子上下文」(Atomic Context),因為在這種上下文中執行的程式碼無法阻塞。中斷不是可排程的,它們發生並執行自己的處理程式,產生自己的上下文。Softirqs、Tasklets 和 Timers 在中斷上下文中執行,這意味著它們不能呼叫阻塞函式。

延遲工作機制

延遲工作允許將程式碼排程到稍後執行。這種機制用於補充中斷處理程式的功能,因為中斷處理有重要的需求和限制:

  • 中斷處理程式的執行時間必須盡可能短。
  • 在中斷上下文中不能使用阻塞呼叫。

使用延遲工作機制,可以在中斷處理程式中執行最少量的時間敏感工作,並將非時間敏感的工作排程到稍後執行,當中斷被啟用時。這種延遲工作在中斷處理中也稱為「下半部」(Bottom-Half),因為它的目的是執行中斷處理程式(上半部,Top-Half)剩下的工作。

下半部與上半部的區別

  • 上半部(Top-Half):立即執行時間關鍵的工作,通常是中斷處理程式本身。上半部應該盡快完成,因為所有中斷都被停用。

  • 下半部(Bottom-Half):執行被延遲的工作,即那些時間依賴性較低、較不關鍵的動作。下半部由中斷服務例程(ISR)觸發。當下半部執行時,中斷是啟用的。

Softirqs

Softirqs 在中斷上下文中執行,用於最關鍵和時間敏感的下半部處理工作。它們在所有中斷處理程式完成後執行,可以被任何上半部中斷搶佔。Softirqs 不能被裝置驅動程式使用,因為它們是為各種核心子系統保留的;在編譯時期定義了固定數量的 Softirqs。

目前的核心版本定義了以下型別的 Softirqs:

enum {
    HI_SOFTIRQ = 0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,
    NR_SOFTIRQS
};

每種型別都有特定的用途,例如 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 用於執行 Tasklets,而 TIMER_SOFTIRQ 用於執行 Timers。Softirqs 在中斷上下文中執行,因此不能呼叫阻塞函式。如果 Softirq 處理程式需要呼叫阻塞函式,可以排程工作佇列來執行這些阻塞呼叫。

Tasklets

Tasklet 是建立在 Softirqs 之上的另一種下半部機制,同樣在中斷上下文中執行。Tasklet 與 Softirq 的主要區別在於 Tasklet 可以動態分配,因此可以被裝置驅動程式使用。Tasklet 由一個 Tasklet 結構表示,需要在使用前初始化。

Tasklet 在 HI 和 TASKLET Softirqs 中執行。它們在所有中斷啟用的情況下執行,但保證在同一時間只會在一個 CPU 上執行。可以使用 DECLARE_TASKLETtasklet_init 函式初始化 Tasklet。

void handler(unsigned long data);
DECLARE_TASKLET(tasklet, handler, data);

或者手動初始化:

struct tasklet_struct tasklet;
tasklet_init(&tasklet, handler, data);

中斷處理程式可以使用 tasklet_scheduletasklet_hi_schedule 函式排程 Tasklet 的執行。

Timers

Timers 是另一種形式的延遲工作,在中斷上下文中執行,根據 Softirqs 實作。它們由 timer_list 結構定義。使用前,必須透過呼叫 setup_timer 初始化 Timer:

void setup_timer(struct timer_list *timer,
                 void (*function)(unsigned long),
                 unsigned long data);

然後,可以使用 mod_timer 排程 Timer:

int mod_timer(struct timer_list *timer, unsigned long expires);

內容解密:

  1. setup_timer 函式的作用:初始化 timer_list 結構的內部欄位,並將指定的函式作為 Timer 的處理程式。

  2. mod_timer 函式的作用:排程 Timer 的到期時間,並啟動 Timer。

  3. Timer 的使用場景:Linux 驅動程式中使用 Timer 來實作延遲工作,適用於需要週期性或延遲執行的任務。

  4. Timer 的限制:由於 Timer 在中斷上下文中執行,因此不能呼叫阻塞函式,否則會導致系統當機或未定義行為。