1. 建立輸入裝置驅動程式

1.1. 最簡單的示例

下面是一個非常簡單的輸入裝置驅動程式示例。該裝置只有一個按鈕,並且可以在 i/o 埠 BUTTON_PORT 訪問該按鈕。按下或釋放時,會發生 BUTTON_IRQ。驅動程式可能如下所示

#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>

#include <asm/irq.h>
#include <asm/io.h>

static struct input_dev *button_dev;

static irqreturn_t button_interrupt(int irq, void *dummy)
{
        input_report_key(button_dev, BTN_0, inb(BUTTON_PORT) & 1);
        input_sync(button_dev);
        return IRQ_HANDLED;
}

static int __init button_init(void)
{
        int error;

        if (request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL)) {
                printk(KERN_ERR "button.c: Can't allocate irq %d\n", button_irq);
                return -EBUSY;
        }

        button_dev = input_allocate_device();
        if (!button_dev) {
                printk(KERN_ERR "button.c: Not enough memory\n");
                error = -ENOMEM;
                goto err_free_irq;
        }

        button_dev->evbit[0] = BIT_MASK(EV_KEY);
        button_dev->keybit[BIT_WORD(BTN_0)] = BIT_MASK(BTN_0);

        error = input_register_device(button_dev);
        if (error) {
                printk(KERN_ERR "button.c: Failed to register device\n");
                goto err_free_dev;
        }

        return 0;

err_free_dev:
        input_free_device(button_dev);
err_free_irq:
        free_irq(BUTTON_IRQ, button_interrupt);
        return error;
}

static void __exit button_exit(void)
{
        input_unregister_device(button_dev);
        free_irq(BUTTON_IRQ, button_interrupt);
}

module_init(button_init);
module_exit(button_exit);

1.2. 示例的作用

首先,它必須包含 <linux/input.h> 檔案,該檔案與輸入子系統介面。這提供了所有需要的定義。

在 _init 函式中,該函式在模組載入或啟動核心時呼叫,它會獲取所需的資源(它還應檢查裝置是否存在)。

然後,它使用 input_allocate_device() 分配一個新的輸入裝置結構,並設定輸入位域。這樣,裝置驅動程式會告訴輸入系統的其他部分它是什麼 - 哪些事件可以由該輸入裝置生成或接受。我們的示例裝置只能生成 EV_KEY 型別的事件,並且這些事件中只有 BTN_0 事件程式碼。因此,我們只設置這兩個位。我們可以使用

set_bit(EV_KEY, button_dev->evbit);
set_bit(BTN_0, button_dev->keybit);

同樣,但對於多個位,第一種方法往往更短。

然後,示例驅動程式透過呼叫

input_register_device(button_dev);

來註冊輸入裝置結構。這會將 button_dev 結構新增到輸入驅動程式的連結串列中,並呼叫裝置處理模組的 _connect 函式,以告訴他們新的輸入裝置已出現。 input_register_device() 可能會休眠,因此不得從中斷或持有自旋鎖的情況下呼叫。

在使用中,驅動程式唯一使用的函式是

button_interrupt()

它在來自按鈕的每次中斷時檢查其狀態,並透過

input_report_key()

呼叫輸入系統來報告它。無需檢查中斷例程是否未向輸入系統報告兩個相同的值事件(例如,按下,按下),因為 input_report_* 函式會自行檢查。

然後是

input_sync()

呼叫,以告訴那些接收事件的人,我們已傳送完整的報告。這在單按鈕情況下似乎並不重要,但對於滑鼠移動來說非常重要,因為您不希望 X 和 Y 值被單獨解釋,因為那會導致不同的移動。

1.3. dev->open() 和 dev->close()

如果驅動程式必須重複輪詢裝置,因為它沒有來自裝置的中斷,並且輪詢過於昂貴而無法一直進行,或者如果裝置使用有價值的資源(例如,中斷),則可以使用開啟和關閉回撥來了解何時可以停止輪詢或釋放中斷,以及何時必須恢復輪詢或再次獲取中斷。為此,我們會將以下內容新增到我們的示例驅動程式中

static int button_open(struct input_dev *dev)
{
        if (request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL)) {
                printk(KERN_ERR "button.c: Can't allocate irq %d\n", button_irq);
                return -EBUSY;
        }

        return 0;
}

static void button_close(struct input_dev *dev)
{
        free_irq(IRQ_AMIGA_VERTB, button_interrupt);
}

static int __init button_init(void)
{
        ...
        button_dev->open = button_open;
        button_dev->close = button_close;
        ...
}

請注意,輸入核心會跟蹤裝置的的使用者數量,並確保僅當第一個使用者連線到裝置時才呼叫 dev->open(),並且僅當最後一個使用者斷開連線時才呼叫 dev->close()。對兩個回撥的呼叫都是序列化的。

如果成功,open() 回撥應返回 0,如果失敗,則返回任何非零值。 close() 回撥(為空)必須始終成功。

1.4. 抑制輸入裝置

抑制裝置意味著忽略來自它的輸入事件。因此,它是關於維護與輸入處理程式的關係 - 無論是已經存在的關係,還是在裝置處於抑制狀態時將要建立的關係。

如果抑制了裝置,則任何輸入處理程式都不會收到來自它的事件。

沒有人想要來自裝置的事件這一事實被進一步利用,透過在抑制和取消抑制操作上分別呼叫裝置的 close()(如果有使用者)和 open()(如果有使用者)。實際上,close() 的含義是停止向輸入核心提供事件,而 open() 的含義是開始向輸入核心提供事件。

在抑制時呼叫裝置的 close() 方法(如果有使用者)允許驅動程式節省電量。可以直接關閉裝置電源,也可以透過釋放驅動程式在使用執行時 PM 時在 open() 中獲得的執行時 PM 引用來節省電量。

抑制和取消抑制與輸入處理程式開啟和關閉裝置是正交的。在任何處理程式肯定與其匹配之前,使用者空間可能希望預先抑制裝置。

抑制和取消抑制也與裝置作為喚醒源是正交的。作為喚醒源在系統休眠時起作用,而不是在系統執行時起作用。驅動程式應如何對其抑制、休眠和作為喚醒源之間的互動進行程式設計是特定於驅動程式的。

以網路裝置為例 - 關閉網路介面並不意味著應該無法透過此介面在 LAN 上喚醒系統。因此,即使在抑制狀態下,也可能存在應被視為喚醒源的輸入驅動程式。實際上,在許多 I2C 輸入裝置中,它們的中斷被宣告為喚醒中斷,並且其處理發生在驅動程式的核心中,該核心不知道特定於輸入的抑制(也不應該知道)。包含多個介面的複合裝置可以在每個介面的基礎上進行抑制,例如,抑制一個介面不應影響裝置作為喚醒源的能力。

如果一個裝置在抑制狀態下要被視為喚醒源,則在對其 suspend() 進行程式設計時必須特別小心,因為它可能需要呼叫裝置的 open()。根據 close() 對於所討論的裝置意味著什麼,在進入休眠狀態之前不 open() 它可能會導致無法提供任何喚醒事件。裝置無論如何都會進入休眠狀態。

1.5. 基本事件型別

最簡單的事件型別是 EV_KEY,用於鍵和按鈕。它透過

input_report_key(struct input_dev *dev, int code, int value)

報告給輸入系統。有關程式碼的允許值(從 0 到 KEY_MAX),請參見 uapi/linux/input-event-codes.h。 Value 被解釋為真值,即任何非零值都表示鍵被按下,零值表示鍵被釋放。僅當值與之前不同時,輸入程式碼才會生成事件。

除了 EV_KEY 之外,還有另外兩種基本事件型別:EV_REL 和 EV_ABS。它們用於裝置提供的相對值和絕對值。相對值可能是例如滑鼠在 X 軸上的移動。滑鼠將其報告為與上次位置的相對差異,因為它沒有任何絕對座標系可以工作。絕對事件主要用於操縱桿和數字化儀 - 在絕對座標系中工作的裝置。

使裝置報告 EV_REL 按鈕與 EV_KEY 一樣簡單;只需設定相應的位並呼叫

input_report_rel(struct input_dev *dev, int code, int value)

函式。僅針對非零值生成事件。

但是,EV_ABS 需要特別小心。在呼叫 input_register_device 之前,您必須為您的裝置的每個絕對軸填充 input_dev 結構中的其他欄位。如果我們的按鈕裝置也具有 ABS_X 軸

button_dev.absmin[ABS_X] = 0;
button_dev.absmax[ABS_X] = 255;
button_dev.absfuzz[ABS_X] = 4;
button_dev.absflat[ABS_X] = 8;

或者,您可以直接說

input_set_abs_params(button_dev, ABS_X, 0, 255, 4, 8);

此設定適用於操縱桿 X 軸,最小值為 0,最大值為 255(操縱桿必須能夠達到此值,有時報告更多也沒有問題,但它必須始終能夠達到最小值和最大值),資料中的噪聲高達 +- 4,並且中心平坦位置的大小為 8。

如果您不需要 absfuzz 和 absflat,您可以將它們設定為零,這意味著該東西是精確的,並且始終返回到完全中心位置(如果有)。

1.6. BITS_TO_LONGS()、BIT_WORD()、BIT_MASK()

來自 bitops.h 的這三個宏可以幫助進行一些位域計算

BITS_TO_LONGS(x) - returns the length of a bitfield array in longs for
                   x bits
BIT_WORD(x)      - returns the index in the array in longs for bit x
BIT_MASK(x)      - returns the index in a long for bit x

1.7. id* 和 name 欄位

dev->name 應在輸入裝置驅動程式註冊輸入裝置之前設定。它是一個字串,如“Generic button device”,包含裝置的使用者友好名稱。

id* 欄位包含裝置的匯流排 ID(PCI、USB 等)、供應商 ID 和裝置 ID。匯流排 ID 在 input.h 中定義。供應商和裝置 ID 在 pci_ids.h、usb_ids.h 和類似的包含檔案中定義。這些欄位應由輸入裝置驅動程式在註冊之前設定。

idtype 欄位可用於輸入裝置驅動程式的特定資訊。

id 和 name 欄位可以透過 evdev 介面傳遞給使用者空間。

1.8. keycode、keycodemax、keycodesize 欄位

這三個欄位應由具有密集鍵對映的輸入裝置使用。 keycode 是一個用於將掃描碼對映到輸入系統鍵碼的陣列。 keycode max 應包含陣列的大小,keycode size 應包含其中每個條目的大小(以位元組為單位)。

使用者空間可以使用相應 evdev 介面上的 EVIOCGKEYCODE 和 EVIOCSKEYCODE ioctl 來查詢和更改當前掃描碼到鍵碼的對映。當裝置填充了所有 3 個上述欄位時,驅動程式可以依賴於核心的預設實現來設定和查詢鍵碼對映。

1.9. dev->getkeycode() 和 dev->setkeycode()

getkeycode() 和 setkeycode() 回撥允許驅動程式覆蓋輸入核心提供的預設鍵碼/keycodesize/keycodemax 對映機制,並實現稀疏鍵碼對映。

1.10. 按鍵自動重複

...很簡單。它由 input.c 模組處理。不使用硬體自動重複,因為它在許多裝置中不存在,即使存在,有時也會損壞(在鍵盤上:東芝筆記型電腦)。要為您的裝置啟用自動重複,只需在 dev->evbit 中設定 EV_REP。一切都將由輸入系統處理。

1.11. 其他事件型別,處理輸出事件

到目前為止,其他事件型別是

  • EV_LED - 用於鍵盤 LED。

  • EV_SND - 用於鍵盤蜂鳴聲。

它們與例如按鍵事件非常相似,但是它們沿相反的方向 - 從系統到輸入裝置驅動程式。如果您的輸入裝置驅動程式可以處理這些事件,則必須在 evbit 中設定相應的位,並且還必須設定回撥例程

button_dev->event = button_event;

int button_event(struct input_dev *dev, unsigned int type,
                 unsigned int code, int value)
{
        if (type == EV_SND && code == SND_BELL) {
                outb(value, BUTTON_BELL);
                return 0;
        }
        return -1;
}

此回撥例程可以從中斷或 BH 呼叫(儘管這不是規則),因此不得休眠,並且完成時間不得太長。

1.12. 輪詢輸入裝置

透過將輸入裝置結構和回撥傳遞給函式

int input_setup_polling(struct input_dev *dev,
    void (*poll_fn)(struct input_dev *dev))

來設定輸入輪詢。在回撥中,裝置應使用常規的 input_report_* 函式和 input_sync,就像其他裝置一樣。

還有一個函式

void input_set_poll_interval(struct input_dev *dev, unsigned int interval)

用於配置裝置將被輪詢的間隔(以毫秒為單位)。