輸入裝置編程指南(Programming input drivers)
~~~~~~~~~~~~~~~~~~~~~~~~~
1. 建立一個輸入裝置驅動程式
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1.0 一個最簡單的例子
~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本文由DroidPhone 翻譯:http://blog.csdn.net/droidphone
Kernel版本:V3.4.10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
以下是一個非常簡單的輸入裝置驅動程式。該裝置只有一個按鍵,它通過BUTTON_PORT這一i/o連接埠訪問,當按下或釋放該按鍵,會發生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.1 例子驅動的工作過程
~~~~~~~~~~~~~~~~~~~~~~~~~
首先它必須包含標頭檔<linux/input.h>,它是input子系統的介面,它提供了所有必要的定義資訊。
_init初始化函數,可以通過模組進行載入,也可以編譯進核心中,它收集裝置需要的資源(也會檢查裝置是否存在)。
接著,它用input_allocate_device()分配了一個input device結構,設定它的bitfields,裝置驅動程式通過這一方式把自身的語音總機input子系統的其他部分:它能產生或者接受什麼事件。我們的例子裝置只能產生EV_KEY類型的事件,而且只能發出BTN_0事件code。這樣我們只需要設定這兩個bits,我們也可以用
set_bit(EV_KEY, button_dev.evbit);set_bit(BTN_0, button_dev.keybit);
進行設定,但是上面例子代碼中的方法可以一次設定更多的位。
然後,例子驅動用下面的函數註冊input device結構:
input_register_device(&button_dev);
這會把button_dev結構添加到input driver的全域鏈表中,調用device handler模組中的_connect函數來通知他一個新的裝置出現了。input_register_device()可能會休眠,所以他不能在中斷或者持有一個spinlock的情況下被使用。
功能上,該驅動只使用了函數:
button_interrupt()
在每次中斷中通過檢查按鍵的狀態,並通過以下函數上報:
input_report_key()
該函數會進入input子系統。中斷服務程式不必檢查它是否會給input子系統報告value重複的事件(例如:按下,按下)。因為input_report_*函數自己會對此進行檢查。
然後就是調用:
input_sync()
該函數告訴事件的接收者,我們已經發送了一次完整的報告資訊。這對於我們這個只有一個按鍵的裝置好像不太重要,但有些情況下它是非常重要的,例如當滑鼠移動後,你不希望X和Y值被分開解釋,因為那樣會被解釋為兩次移動。
1.2 dev->open() and dev->close()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
當驅動因為裝置沒有提供中斷能力時,它需要不停地查詢裝置的狀態,但是如果一直進行這個查詢顯得有點浪費。有時裝置需要使用一些有價值的資源(例如中斷)。這時,我們可以使用open和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;...}
需要注意的是,input核心會保持裝置的使用計數來保證dev->open()只有當第一個使用者串連該裝置時才被調用,而dev->close()只有當最後一個使用者斷開和裝置的串連時才被調用。對兩個回調的調用都是序列化的。
當調用成功時,open()回調返回0,返回非0則表示發生了錯誤。傳回型別是void的close()回調必須一直成功。
1.3 基本事件類型(types)
~~~~~~~~~~~~~~~~~~~~~
最簡單的事件類型是EV_KEY,它用於鍵盤和按鈕,它通過以下函數上報給input子系統:
input_report_key(struct input_dev *dev, int code, int value)
linux/input.h定義了該類型可用的values和code (從0 到 KEY_MAX)。Value被解釋為真假值,也就是任何非0值意味著鍵被按下,0則意味著鍵被鬆開。input子系統的代碼只有當value的值和之前的值不同時才會產生一次事件。
除了EV_KEY外,還有另外兩個基本的事件類型:EV_REL和EV_ABS 。它們用來提供裝置的相對和絕對值。滑鼠移動是一種相對值,滑鼠報告相對於上一個位置的相對差值,因為它工作在沒有任何絕對座標系系統中。絕對值事件對遊戲操縱杆和數字化儀有用,這些裝置工作在一個絕對座標系統中。
裝置上報EV_REL就像EV_KEY一樣簡單,只要設定相應的位然後調用以下函數即可:
input_report_rel(struct input_dev *dev, int code, int value)
只有當非0的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,你可以把它們設定為0,這意味著它是絕對準確的並總是返回正中心位置(如果他有的話)。
1.4 BITS_TO_LONGS(), BIT_WORD(), BIT_MASK()
~~~~~~~~~~~~~~~~~~~~~~~~~~
這3個宏來自bitops.h,有助於進行位域計算:
- BITS_TO_LONGS(x) - 返回x位的位域數組需要多少個long類型來組成。
- BIT_WORD(x) - 返回位域數組中第x位所對應的按long為單位的索引。
- BIT_MASK(x) - 返回位x對應的long型的mask值。
1.5 The id* and name fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dev->name欄位應該要在驅動程式註冊輸入裝置之前設定好。它是一個像'Generic button device'之類的對方便使用的裝置名稱字串
id*欄位包含了匯流排的ID(PCI,USB...),廠商ID和裝置ID。匯流排IDs在input.h中定義。廠商和裝置IDs在pci_ids.h,usb_ids.h和類似的標頭檔中定義。這些欄位也應該在註冊裝置之前被設定好。
idtype欄位可以被用作輸入裝置的專有資訊。
這些id和name欄位可以通過evdev介面傳遞到使用者空間中。
1.6 keycode, keycodemax, keycodesize 欄位
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
這3個欄位用於需要索引值映射的輸入裝置,keycode是一個用於把掃描碼轉換為輸入系統索引值的數組,keycodemax是這個數組的大小,keycodesize則是該數組每個元素的大小(以位元組為單位)
使用者空間可以通過相應的evdev介面,使用EVIOCGKEYCODE和EVIOCSKEYCODE ioctls來查詢和修改當前的掃描碼到索引值的映射表。當裝置填充了3個上述的欄位,驅動程式可以根據kernel的預設實現來設定和查詢索引值映射表。
1.7 dev->getkeycode() and dev->setkeycode()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
getkeycode()和setkeycode()回調允許驅動程式覆寫由input核心代碼提供的對keycode/keycodemax/keycodesize的預設映射機制,從而可以實現稀疏的映射方式。
1.8 按鍵的autorepeat
~~~~~~~~~~~~~~~~~~
... 很簡單,它由input.c模組處理。硬體autorepeat沒有被使用,因為很多裝置不存在該功能,而且就算存在該功能,有時候也不正常(例如,Toshiba筆記本中的鍵盤)。要使能你的裝置的autorepeat功能,只要設定dev->evbit中的EV_REP位即可,其它的事情都有輸入子系統處理。
1.9 其它的事件types, 輸出事件處理
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
現在為止,其它的事件types有:
- EV_LED - 用作鍵盤的LEDs燈。
- 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上下文中被調用,所以它不能睡眠,不要處理太長的時間。