首先說明一下,本文是基于Linux-2.6.38版本核心來分析Linux輸入子系統架構和原理的。這陣子本來沒有打算花時間來分析Linux input system的,然而當在研究S3C6410觸摸屏驅動的時候悲劇不期而至,核心中并沒有實作6410的觸摸屏驅動,不過有關于S3C2410觸摸屏的驅動,往s3c2410_ts.c檔案裡面一看,居然實作過程中用到了輸入子系統這一神馬機制。瞄了下代碼,流程基本知道了,但是關于輸入子系統的原理是怎樣的呢?全部知其然,好吧,就去了解這一神秘的東東吧!Now。。。
1、為何引入input system?
以前我們寫一些輸入裝置(鍵盤、滑鼠等)的驅動都是采用字元裝置、混雜裝置處理的。問題由此而來,Linux開源社群的大神們看到了這大量輸入裝置如此分散不堪,有木有可以實作一種機制,可以對分散的、不同類别的輸入裝置進行統一的驅動,是以才出現了輸入子系統。
輸入子系統引入的好處:
(1)統一了實體形态各異的相似的輸入裝置的處理功能。例如,各種滑鼠,不論PS/2、USB、還是藍牙,都被同樣處理。
(2)提供了用于分發輸入報告給使用者應用程式的簡單的事件(event)接口。你的驅動不必建立、管理/dev節點以及相關的通路方法。是以它能夠很友善的調用輸入API以發送滑鼠移動、鍵盤按鍵,或觸摸事件給使用者空間。X windows這樣的應用程式能夠無縫地運作于輸入子系統提供的event接口之上。
(3)抽取出了輸入驅動的通用部分,簡化了驅動,并提供了一緻性。例如,輸入子系統提供了一個底層驅動(成為serio)的集合,支援對序列槽和鍵盤控制器等硬體輸入的通路。
注:更多較長的描述可參見《精通Linux裝置驅動程式開發》這本書。
2、輸入子系統架構
上圖展示了輸入子系統的操作。此子系統包括一前一後運作的兩類驅動:輸入事件(event)驅動和輸入裝置(device)驅動。
輸入事件驅動負責和應用程式的接口;
而輸入裝置驅動負責和底層輸入裝置的通信。
輸入事件驅動和輸入裝置驅動都可以利用輸入子系統的高效、可重用的核心提供的服務。
Now,我們看到輸入子系統中有兩個類型的驅動,當我們要為一個輸入裝置(如觸摸屏)的編寫驅動的時候,我們是要編寫兩個驅動:輸入裝置驅動和輸入事件驅動??
答案是否定的。在子系統中,事件驅動是标準的,對所有的輸入類都是可以用的,是以你更可能的是實作輸入裝置驅動而不是輸入事件驅動。你的裝置可以利用一個已經存在的,合适的輸入事件驅動通過輸入核心和使用者應用程式接口。
3、主要資料結構和核心程式設計接口
input_event
include/linux/input.h
evdev産生的每個事件包都采用此格式。
input_dev
代表一個輸入裝置。
input_handler
include/linux/serial_core.h
事件驅動支援的入口函數。
psmouse_protocol
drivers/input/mouse/psmouse-base.c
所支援的PS/2滑鼠協定驅動相關的資訊。
psmouse
drivers/input/mouse/psmouse.h
PS/2滑鼠驅動支援的方法。
核心程式設計接口概述 核心接口位置描述
input_register_device()
drivers/input/input.c
向input核心注冊一個裝置。
input_unregister_device()
從input核心移除一個裝置。
input_report_rel()
在某個方向産生相對移動。
input_report_abs()
在某個方向産生絕對移動。
input_report_key()
産生一個按鍵或按鈕按擊。
input_sync()
表明輸入子系統能收集以前産生的事件,将這些事件組成一個evdev包,并通過/dev/input/ inputX發送給使用者空間。
input_register_handler()
注冊一個使用者事件驅動。
sysfs_create_group()
fs/sysfs/group.c
用特定屬性建立sysfs節點組。
sysfs_remove_group()
移除用sysfs_create_group()建立的sysfs組。
tty_insert_flip_char()
include/linux/tty_flip.h
發送一個字元給線路規程層。
platform_device_register_simple()
drivers/base/platform.c
建立一個簡單平台裝置。
platform_device_unregister()
解除安裝一個平台裝置。
4、輸入裝置驅動(注意,以下是結構較長的描述)
在輸入子系統的裝置驅動中,最重要的資料結構是struct input_dev,需要完成的大部分工作都是圍繞着它來的,它是驅動的主體。每個struct input_dev代表一個輸入裝置。/* include/linux/input.h */
struct input_dev {
const char *name; /*裝置名 */
const char *phys;
const char *uniq;
struct input_id id; /*用于比對事件處理層handler */
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /*用于記錄支援的事件類型的位圖*/
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /*記錄支援的按鍵值的位圖 */
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /*記錄支援的相對坐标的位圖 */
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /*記錄支援的絕對坐标的位圖 */
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*led */
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /*beep */
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
unsigned int keycodemax; /*支援的按鍵值的個數 */
unsigned int keycodesize; /*每個鍵值的位元組數 */
void *keycode; /*存儲按鍵值的數組首位址 */
int(*setkeycode)(struct input_dev *dev, int scancode, int keycode); /* 修改鍵值的函數,可選 */
int(*getkeycode)(struct input_dev *dev, int scancode, int *keycode); /* 擷取掃描碼的鍵值,可選 */
structff_device *ff;
unsigned int repeat_key; /*最近一次按鍵值,用于連擊 */
struct timer_list timer; /*自動連擊計時器 */
int sync; /*最後一次同步後沒有新的事件置1*/
int abs[ABS_MAX + 1]; /* 目前各個坐标的值 */
int rep[REP_MAX + 1]; /*自動連擊的參數 */
unsigned longkey[BITS_TO_LONGS(KEY_CNT)]; /*反映目前按鍵狀态的位圖 */
unsigned long led[BITS_TO_LONGS(LED_CNT)]; /*反映目前led狀态的位圖 */
unsigned long snd[BITS_TO_LONGS(SND_CNT)]; /*反映目前beep狀态的位圖 */
unsigned long sw[BITS_TO_LONGS(SW_CNT)];
int absmax[ABS_MAX + 1]; /*記錄各個坐标的最大值 */
int absmin[ABS_MAX + 1]; /*記錄各個坐标的最小值 */
int absfuzz[ABS_MAX + 1]; /*記錄各個坐标的分辨率 */
int absflat[ABS_MAX + 1]; /*記錄各個坐标的基準值 */
int(*open)(struct input_dev *dev); /*打開函數 */
void(*close)(struct input_dev *dev); /*關閉函數 */
int(*flush)(struct input_dev *dev, struct file *file); /* 斷開連接配接時沖洗資料 */
int(*event)(struct input_dev *dev, unsigned int type, unsigned int code, intvalue); /* 回調函數,可選 */
struct input_handle *grab;
spinlock_t event_lock;
struct mutex mutex;
unsigned int users;
int going_away;
struct device dev;
struct list_head h_list; /*handle連結清單 */
struct list_head node; /*input_dev連結清單 */
};
struct input_event是事件傳送的載體,輸入子系統的事件都是包裝成struct input_event傳給使用者空間。 struct input_event成員介紹
/* include/linux/input.h */
struct input_event {
struct timeval time; /*時間戳 */
__u16 type; /*事件類型 */
__u16 code; /*事件代碼 */
__s32 value; /*事件值,如坐标的偏移值 */
struct input_dev注冊的時候需要跟比對的hanlder建立連接配接,比對的依據就是struct input_dev所包含的struct input_id。
struct input_id成員描述
struct input_id {
__u16bustype; /*總線類型 */
__u16vendor; /*生産商編号 */
__u16product; /*産品編号 */
__u16version; /*版本号 */
5、input_dev成員詳解
(1) 打開和關閉函數
struct input_dev中有open和close兩個函數指針。在與handler第一次連接配接之後會調用open函數,斷開連接配接會調用close。open中應該完成硬體初始化的相關工作,并且申請用到的其他資源,如中斷号。close函數做相反的工作。
(2) 事件類型
Linux輸入子系統支援的事件類型如下:
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /*按鍵事件 */
#define EV_REL 0x02 /*相對坐标 */
#define EV_ABS 0x03 /*絕對坐标 */
#define EV_MSC 0x04
#define EV_SW 0x05
#define EV_LED 0x11 /*led */
#define EV_SND 0x12 /*beep */
#define EV_REP 0x14 /*連擊事件 */
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
按鍵事件(EV_KEY)是最簡單的事件類型,用來描述按鍵或者按鈕。報告按鍵事件使用以下函數:
input_report_key(struct input_dev *dev, int code, int value)
code的值在<核心>include/linux/input.h中定義,大小從0到KEY_MAX。value為0時表示按鍵擡起,非0時代表按鍵按下。對同一個按鍵來說,隻有當value的值和上次不同才會産生一次事件。
除了按鍵事件,相對坐标事件(EV_REL)和絕對坐标事件(EV_ABS)也是常用的事件。為了使裝置支援這兩類事件,需要在初始化時對struct input_dev的evbit相應比特置1,并且還要分别在relbit和absbit位圖中為支援的坐标軸置1。
相對坐标事件用來描述類似滑鼠移動的消息。報告相對坐标的函數如下:
input_report_rel(structinput_dev *dev, int code, int value)
code描述坐标軸,value代表相對移動(可正可負)。隻有當value的值非零時才能産生一個有效的事件。
絕對坐标需要額外的工作。初始化時需要填充input_dev中的一些資料域。對于支援的每個坐标軸調用如下函數:
input_set_abs_params(struct input_dev *dev, int axis,int min, int max, int fuzz, int flat);
函數參數從右往左依次代表輸入裝置指針、坐标軸、最小值、最大值、分辨率、基準值。最後兩個參數也可以填為0,代表裝置非常精确并且總能精确的回到中心位置。
input_set_abs_params函數的代碼
static inline void input_set_abs_params(structinput_dev *dev, int axis, int min, int max, int fuzz, int flat)
{
dev->absmin[axis]= min;
dev->absmax[axis]= max;
dev->absfuzz[axis]= fuzz;
dev->absflat[axis]= flat;
dev->absbit[BIT_WORD(axis)]|= BIT_MASK(axis);
/* 這一行已經注冊了坐标 */
}
另外輸入裝置驅動中經常用到同步事件(EV_SYN),輸入子系統會預設支援此事件,驅動無需注冊。滑鼠、觸摸闆之類的裝置需要用它來提示上層已經發送完了一個完整的事件報告。同步事件的報告形式如下:
input_sync(zlgkpd->input);
(3) keycode、 keycodemax和keycodesize
首先說明掃描碼和鍵值的差別。如程式清單 1.4<!--[if gte mso 9]><![endif]-->所示,例子中包含四個按鍵的值,那麼掃描碼的範圍是0~3,鍵值就是keypad_keycode中的四個值。
keycode、 keycodemax和keycodesize這三個資料域儲存的值分别是鍵值數組的首位址、鍵值的個數和每個鍵值的位元組大小。有了這三個變量,就可以在運作是改變鍵盤的鍵值映射。比如原來數組中的第四個鍵值為KEY_Z,可以根據需要改為KEY_D或者其他的值。這樣同樣的掃描碼就對應的不同的鍵值。
這三個域正确填充之後,核心可以使用input_dev的成員函數來修改鍵值映射。修改映射的函數就是input_dev中的setkeycode、getkeycode這兩個函數指針對應的函數,如果注冊之前不初始化它們,則初始化函數把系統預設的函數賦給它們。
EVIOCGKEYCODE和EVIOCSKEYCODE這兩個ioctl指令分别用來檢視和修改鍵值。
(4) 按鍵的自動連擊
我們都有這樣的經曆:按住方向鍵不松開,一直把文檔往某個方向拉。這個功能就是自動連擊,也就是在按鍵擡起之前連續發送按鍵事件。
按鍵連擊事件(EV_REP)的開啟非常簡單,在input_devd的evbit相應比特置1即可。dev->rep[REP_DELAY]和dev->rep[REP_PERIOD]分别存儲連擊的延時和周期,如果驅動不對它們指派,則系統為他們分别賦為250和33。按鍵延時即按鍵到第一次連擊的間隔,按鍵周期即兩次連擊之間的間隔。它們的機關都是毫秒。
(5) 總線類型
Input_dev中input_id用到了總線類型。輸入子系統支援的總線類型如下所示。
#define BUS_PCI 0x01
#define BUS_ISAPNP 0x02
#define BUS_USB 0x03
#define BUS_HIL 0x04
#define BUS_BLUETOOTH 0x05
#define BUS_VIRTUAL 0x06
#define BUS_ISA 0x10
#define BUS_I8042 0x11
#define BUS_XTKBD 0x12
#define BUS_RS232 0x13
#define BUS_GAMEPORT 0x14
#define BUS_PARPORT 0x15
#define BUS_AMIGA 0x16
#define BUS_ADB 0x17
#define BUS_I2C 0x18
#define BUS_HOST 0x19
#define BUS_GSC 0x1A
#define BUS_ATARI 0x1B
(6) 其他的事件類型
EV_LED和EV_SND事件是針對鍵盤上的led和蜂鳴器。這兩類事件是由輸入子系統核心發送給驅動的。
如果驅動支援這兩類事件的話,應該填充input_dev中的event函數指針,其實這是一個回調函數。另外需要填充input_dev中evbit對應的比特位。