本節(jié)從整體上講解了輸入子系統(tǒng)的框架結構。有助于讀者從整體上認識linux的輸入子系統(tǒng)。在陷入代碼分析的過程中,通過本節(jié)的知識能夠找準方向,明白原理。
本節(jié)重點:
- 輸入子系統(tǒng)的框架結構
- 各層對應內核中的文件位置
- 輸入子系統(tǒng)的事件處理機制
- 輸入子系統(tǒng)的驅動層基本操作流程
- 輸入子系統(tǒng)的驅動層常用函數(shù)
本節(jié)難點:
輸入子系統(tǒng)的事件處理機制
輸入子系統(tǒng)的驅動工作流程
1 初識linux輸入子系統(tǒng)
linux輸入子系統(tǒng)(linux input subsystem)從上到下由三層實現(xiàn),分別為:輸入子系統(tǒng)事件處理層(EventHandler)、輸入子系統(tǒng)核心層(InputCore)和輸入子系統(tǒng)設備驅動層。
對于輸入子系統(tǒng)設備驅動層而言,主要實現(xiàn)對硬件設備的讀寫訪問,中斷設置,并把硬件產生的事件轉換為核心層定義的規(guī)范提交給事件處理層。
對于核心層而言,為設備驅動層提供了規(guī)范和接口。設備驅動層只要關心如何驅動硬件并獲得硬件數(shù)據(jù)(例如按下的按鍵數(shù)據(jù)),然后調用核心層提供的接口,核心層會自動把數(shù)據(jù)提交給事件處理層。
對于事件處理層而言,則是用戶編程的接口(設備節(jié)點),并處理驅動層提交的數(shù)據(jù)處理。
對于linux輸入子系統(tǒng)的框架結構如下圖1所示:
圖1 linux輸入子系統(tǒng)框架結構
由上圖所展現(xiàn)的內容就是linux輸入子系統(tǒng)的分層結構。
/dev/input目錄下顯示的是已經注冊在內核中的設備編程接口,用戶通過open這些設備文件來打開不同的輸入設備進行硬件操作。
事件處理層為不同硬件類型提供了用戶訪問及處理接口。例如當我們打開設備/dev/input/mice時,會調用到事件處理層的Mouse Handler來處理輸入事件,這也使得設備驅動層無需關心設備文件的操作,因為Mouse Handler已經有了對應事件處理的方法。
輸入子系統(tǒng)由內核代碼drivers/input/input.c構成,它的存在屏蔽了用戶到設備驅動的交互細節(jié),為設備驅動層和事件處理層提供了相互通信的統(tǒng)一界面。
下圖2簡單描述了linux輸入子系統(tǒng)的事件處理機制:
圖2 linux輸入子系統(tǒng)事件處理機制
由上圖可知輸入子系統(tǒng)核心層提供的支持以及如何上報事件到input event drivers。
作為輸入設備的驅動開發(fā)者,需要做以下幾步:
1、在驅動加載模塊中,設置你的input設備支持的事件類型,類型參見表1設置
2、 注冊中斷處理函數(shù),例如鍵盤設備需要編寫按鍵的抬起、放下,觸摸屏設備需要編寫按下、抬起、絕對移動,鼠標設備需要編寫單擊、抬起、相對移動,并且需要在必要的時候提交硬件數(shù)據(jù)(鍵值/坐標/狀態(tài)等等)
3、 將輸入設備注冊到輸入子系統(tǒng)中
表1 Linux輸入子系統(tǒng)支持的數(shù)據(jù)類型
EV_SYN 0x00 同步事件
EV_KEY 0x01 按鍵事件
EV_REL 0x02 相對坐標(如:鼠標移動,報告相對最后一次位置的偏移)
EV_ABS 0x03 絕對坐標(如:觸摸屏或操作桿,報告絕對的坐標位置)
EV_MSC 0x04 其它
EV_SW 0x05 開關
EV_LED 0x11 按鍵/設備燈
EV_SND 0x12 聲音/警報
EV_REP 0x14 重復
EV_FF 0x15 力反饋
EV_PWR 0x16 電源
EV_FF_STATUS 0x17 力反饋狀態(tài)
EV_MAX 0x1f 事件類型最大個數(shù)和提供位掩碼支持
由表1可知,設備所能表示的事件種類,一個設備可以選擇一個或多個事件類型上報給輸入子系統(tǒng)。
Linux輸入子系統(tǒng)提供了設備驅動層上報輸入事件的函數(shù),在include/linux/input.h中:
voidinput_report_key(struct input_dev *dev, unsigned int code, int value); //上報按鍵事件
voidinput_report_rel(struct input_dev *dev, unsigned int code, int value); //上報相對坐標事件
voidinput_report_abs(struct input_dev *dev, unsigned int code, int value); //上報絕對坐標事件
當提交輸入設備產生的輸入事件之后,需要調用下面的函數(shù)來通知輸入子系統(tǒng),以處理設備產生的完整事件:
void input_sync(struct input_dev *dev);
2 輸入設備驅動的簡單案例
在Linux內核文檔的documentation/input下,有一個input-programming.txt文件,講解了編寫輸入設備驅動程序的核心步驟。
提供的案例代碼描述了一個button設備,產生的事件通過BUTTON_PORT引腳獲取,當有按下/釋放發(fā)生時,BUTTON_IRQ被觸發(fā),以下是驅動的源代碼:
#include
#include
#include
#include
#include
static struct input_dev *button_dev;
static void button_interrupt(int irq, void*dummy, struct pt_regs *fp)
{
input_report_key(button_dev, BTN_1, inb(BUTTON_PORT) 1);
input_sync(button_dev);
}
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(EV_KEY);
button_dev->keybit[LONG(BTN_0)] = BIT(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);
編寫基于輸入子系統(tǒng)的設備驅動程序需要包含,因為它包含了輸入子系統(tǒng)的接口和所有的宏定義,這些內容在編寫輸入設備驅動程序時需要用到。
button_init函數(shù)說明:
當模塊加載(insmod)或內核引導過程中,button_init函數(shù)會被調用。首先做的工作是獲取能夠正確控制硬件設備的硬件資源(例如內存、IO內存、中斷和DMA),在代碼中BUTTON_IRQ作為BUTTON設備的中斷資源,通過request_irq()函數(shù)被申請注冊。當有按鍵按下/釋放時,調用button_interrupt()中斷處理函數(shù)獲取按鍵值BUTTON_PORT(BUTTON設備的I/O資源)。
那么輸入子系統(tǒng)怎么能夠知道這個設備為輸入設備呢?通過第8行為設備定義一個用于描述一個輸入設備對象。
static struct input_dev *button_dev;
定義了button_dev之后,如何通知輸入子系統(tǒng)有新的輸入設備了呢?或者說如何把一個新的輸入設備加入到輸入子系統(tǒng)中呢?可以通過輸入子系統(tǒng)核心層input.c中提供的函數(shù)分配一個輸入設備,在代碼的第25行。
button_dev= input_allocate_device();
有了輸入設備的描述,當事件產生時,輸入子系統(tǒng)怎么能夠知道設備產生的事件類型呢?通過32和33行的代碼。
button_dev->evbit[0]= BIT(EV_KEY);
button_dev->keybit[LONG(BTN_0)]= BIT(BTN_0);
其中evbit和keybit成員分別代表設備產生的事件類型和上報的按鍵值。其中輸入子系統(tǒng)的一些位操作NBITS、BIT、LONG經常被用到:
#defineNBITS(x) (((x)/BITS_PER_LONG)+1) //通過位x獲取數(shù)組的長度
#defineBIT(x) (1UL((x)%BITS_PER_LONG)) //返回位x在數(shù)組中的位域
#defineLONG(x) ((x)/BITS_PER_LONG) //返回位x的索引
以上的工作做完之后,即可注冊為輸入設備了,代碼的35行。
input_register_device(button_dev);
這個函數(shù)把button_dev輸入設備掛入輸入設備鏈表中,并且通知事件處理層調用connect函數(shù)完成設備和事件處理的綁定,當用戶打開設備時,便能夠調用到相應的事件處理接口獲得硬件上報的數(shù)據(jù)了。input_register_device()函數(shù)是會睡眠的函數(shù),因此不能夠在中斷上下文和持有自旋鎖的代碼中調用。
當我們把上面的工作做完之后,設備驅動中唯一值得關注的就是button_interrupt()中斷處理函數(shù)了。當按鍵動作發(fā)生,button_interrupt()函數(shù)被調用,完成事件的上報由其中的兩條語句完成。
input_report_key(button_dev, BTN_1, inb(BUTTON_PORT) 1);
input_sync(button_dev);
其中input_report_key上報了這是一個按鍵事件,且它的值為inb(BUTTON_PORT) 1,由于案例代碼只產生一個按鍵的值,因此input_sync()在這里不起關鍵作用。但如果是一個觸摸屏,即有x坐標和y坐標,則需要通過input_sync()函數(shù)把x和y坐標完整地傳遞給輸入子系統(tǒng)。
用于測試的應用層代碼:
testkeyread_jb51.rar