T9 輸入子系統

1.輸入子系統概念

1.1輸入設備

  • 輸入設備如按鍵,鼠標,觸摸屏,遊戲遙感等均是輸入設備
  • 若是沒有輸入子系統的話,那麼針對不一樣的屏幕將會寫不一樣的代碼,但其操做模式是同樣的,那麼將會寫不少重複代碼.明顯比較麻煩
  • 輸入子系統的出現就是實現操做與硬件分離,針對不一樣型號的屏幕採用同一套通用方法
  • 總之一句話,求同(操做方法)存異(硬件差別)就完了

1.2輸入子系統

  • linux輸入子系統兼容全部輸入設備
  • 對底層驅動提供統一的編寫驅動的方法
  • 對上層應用提供同一的操做接口,如/dev/input/event0,event1...,應用程序直接open文件,read數據便可

1.3例子

  • 以ubuntu爲例,打開/dev/input,可看到有不少輸入設備文件
topeet@ubuntu:~$ cd /dev/input/
topeet@ubuntu:/dev/input$ ls
by-id  by-path  event0  event1  event2  event3  event4  js0  mice  mouse0  mouse1  mouse2
  • 咱們能夠在/sys/class/input下查看設備詳細信息
topeet@ubuntu:/dev/input$ cd /sys/class/input/
topeet@ubuntu:/sys/class/input$ ls
event0  event1  event2  event3  event4  input0  input1  input2  input3  input4  js0  mice  mouse0  mouse1  mouse2
topeet@ubuntu:/sys/class/input$ cd event0
topeet@ubuntu:/sys/class/input/event0$ ls
dev  device  power  subsystem  uevent
topeet@ubuntu:/sys/class/input/event0$ cd device
topeet@ubuntu:/sys/class/input/event0/device$ ls
capabilities  device  event0  id  modalias  name  phys  power  properties  subsystem  uevent  uniq
# 能夠看到,event0設備對應電源鍵
topeet@ubuntu:/sys/class/input/event0/device$ cat name
Power Button

2.輸入子系統框架

2.1四層分層

  • input handler層:數據處理,將獲取的數據交給用戶
  • input核心層:管理,相似一個信使,能夠將不一樣數據交給對應的上層
  • input device層:設備層,抽象出一個對象,用於描述設備信息,獲取設備產生的數據
  • 硬件層:如按鍵,鼠標,屏幕等

2.2分層思想概述

  • 做爲編程人員,咱們只須要關注input device層就行,input核心層由內核(輸入子系統管理)
  • 字符設備驅動中設備號的申請是在input核心層完成,對於輸入設備,其設備號統一爲13
  • 以後建立類也是在覈心層完成
  • 建立設備文件(/dev/input/event0)是在handler層完成
  • 最後在硬件層完成硬件初始化
  • 其實總體就像作一個生意,一個生產商(設備層),一箇中間商(核心層),一個零售商(handler).假如生產商是產蘋果的,他將與中間商聯繫說他有蘋果賣.而零售商想要進蘋果去賣,此刻零售商也須要找中間商去尋找貨源,中間商就起一個搭橋的做用.當生產商生產了一批蘋果的時候,此可他能夠告訴中間商說我生產了一批蘋果,此可中間商就能夠找到想進蘋果的零售商,進而將生產商生產的蘋果給零售商,零售商獲取蘋果後在買個消費者
  • 同理,當底層設備產生一批數據的時候(例如一幀圖像),它將數據轉交給核心層,核心層再經過匹配機制找到對應上層,將獲取的數據給handler層,最後handler再將數據給應用程序
  • 匹配機制就是底層的device層會產生一個device對象,再將device對象註冊到核心層的input_device_list鏈表中,同時handler層也會產生一個對象並將其註冊到核心層的input_handler_list鏈表中,核心層將匹配兩條鏈表中的東西,匹配成功後直接轉交數據便可

3.輸入子系統編程(驅動1個按鍵)

3.1基礎框架搭建

  • 分配一個input device對象
/*
功能:分配一個input_dev對象
參數:無
返回值:input_dev對象指針
注意:此處在函數內部實現了內存分配,後續須要本身手動釋放內存
*/
struct input_dev *input_allocate_device(void);

/*
功能:釋放分配的input_dev對象內存
參數:無
返回值:無
*/
void input_free_device(struct input_dev *dev);
  • 初始化input device對象
__set_bit(unsigned long nr, volatile void * addr);
/*例如*/
__set_bit(EV_KEY, inputdev->evbit);         //表示當前設備可產生按鍵數據
__set_bit(KEY_POWER, inputdev->keybit);     //表示當前設備可以產生power按鍵數據
  • 註冊input device對象
/*
功能:註冊input device對象
參數:產生的input device對象指針
返回值:註冊成功返回0
注意:當註冊失敗的時候須要及時釋放掉申請的input device對象的內存(建議使用goto語句集中處理錯誤)
*/
int input_register_device(struct input_dev *dev);
  • 完整代碼V1.0
#include <linux/init.h>
#include <linux/module.h>
#include <linux/input.h>

struct input_dev *inputdev;


static int __init simple_input_init(void)
{
    int ret;

    /*分配input_dev對象*/
    inputdev = input_allocate_device();
    if(inputdev == NULL)
    {
        printk(KERN_ERR "input_allocate_device error\n");
        return -ENOMEM;
    }
    
    /*初始化input_dev對象*/
    __set_bit(EV_KEY, inputdev->evbit);         //表示當前設備可產生按鍵數據
    __set_bit(KEY_POWER, inputdev->keybit);     //表示當前設備可以產生power按鍵數據

    /*註冊input_dev對象*/
    ret = input_register_device(inputdev);
    if(ret != 0)
    {
        printk(KERN_ERR "input_register_device error\n");
        goto err_0;
    }
    
    return 0;

err_0:
    input_free_device(inputdev);                //注意釋放內存
    return ret;
}

static void __exit simple_input_exit(void)
{
    input_unregister_device(inputdev);
    input_free_device(inputdev);
}

module_init(simple_input_init);
module_exit(simple_input_exit);
MODULE_LICENSE("GPL");
  • 執行結果,可見,系統默認有一個event0輸入設備,當裝載驅動後會在event0以後本身追加一個event1.同時注意,在代碼中咱們並無申請設備號和設備節點等操做,可是實際上卻生成了設備文件,由於這些過程都是由輸入子系統幫忙作了
[root@iTOP-4412]# ls /dev/input/
event0
[root@iTOP-4412]# insmod simple_input.ko 
[   40.811369] simple_input: loading out-of-tree module taints kernel.
[   40.817391] input: Unspecified device as /devices/virtual/input/input1
[root@iTOP-4412]# ls /dev/input/
event0  event1

3.2輸入子系統實現按鍵中斷

  • 基於上述框架,咱們按照中斷的方式來驅動一個按鈕
  • 首先要獲取設備樹中的中斷號碼
int get_irqno_from_node(void)
{
    //經過節點鏈表獲取設備樹中的節點
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //經過節點獲取中斷號碼
    int irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}
  • 以後在內核中申請中斷
ret = request_irq(irqno, input_key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
  • 傳統中斷回調函數以下
/*阻塞方式的中斷回調函數(傳統)*/
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引腳讀取結果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按鍵擡起
        key_dev->event.code = KEY_ENTER;//假設其爲回車鍵
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按鍵按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*有按鍵數據到達,喚醒進程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*設置標誌位,表示有數據到達*/
    key_dev->is_have_data = 1;
    return IRQ_HANDLED;
}
  • 可是基於輸入子系統的話,其編程方式大相徑庭,首先引入幾個API
/*
功能:輸入子系統向核心層上報數據
參數:
參數1:input device對象
參數2:上報的數據類型
	EV_KEY	按鍵
	EV_ABS
參數3:上報的具體數據是啥;例如按鍵,對應的按鍵值是啥,電源鍵仍是音量鍵...
參數4:值是什麼;例如按下爲1,擡起爲0
*/
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);

/*
功能:同步數據,即告訴核心層數據上報完畢
參數:input device對象
*/
static inline void input_sync(struct input_dev *dev);
  • 完整代碼
#include <linux/init.h>
#include <linux/module.h>
#include <linux/input.h>
#include <linux/of_irq.h>
#include <linux/of.h>
#include <linux/interrupt.h>

#define GPX1CON_REG 0x11000C20

int irqno;          //記錄中斷號碼
void *reg_base;
struct input_dev *inputdev;

int get_irqno_from_node(void)
{
    int irqno;
    //經過節點鏈表獲取設備樹中的節點
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //經過節點獲取中斷號碼
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

irqreturn_t input_key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引腳讀取結果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(reg_base + 4) & (1 << 2);
    if(read_val)
    {
        /*按鍵擡起*/
        input_event(inputdev, EV_KEY, KEY_POWER, 0);
        /*結束上報*/
        input_sync(inputdev);
    }
    else
    {
        input_event(inputdev, EV_KEY, KEY_POWER, 1);
        input_sync(inputdev);
    }
    return IRQ_HANDLED;
}

static int __init simple_input_init(void)
{
    int ret;

    /*分配input_dev對象*/
    inputdev = input_allocate_device();
    if(inputdev == NULL)
    {
        printk(KERN_ERR "input_allocate_device error\n");
        return -ENOMEM;
    }
    
    /*初始化input_dev對象*/
    __set_bit(EV_KEY, inputdev->evbit);         //表示當前設備可產生按鍵數據
    __set_bit(KEY_POWER, inputdev->keybit);     //表示當前設備可以產生power按鍵數據

    /*註冊input_dev對象*/
    ret = input_register_device(inputdev);
    if(ret != 0)
    {
        printk(KERN_ERR "input_register_device error\n");
        goto err_0;
    }
    /*硬件初始化*/
    irqno = get_irqno_from_node();
    ret = request_irq(irqno, input_key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        goto err_1;
    }
    reg_base = ioremap(GPX1CON_REG, 8);
    return 0;
err_1:
    input_unregister_device(inputdev);
err_0:
    input_free_device(inputdev);                //注意釋放內存
    return ret;
}

static void __exit simple_input_exit(void)
{
    iounmap(reg_base);
    free_irq(irqno, NULL);
    input_unregister_device(inputdev);
    input_free_device(inputdev);
}

module_init(simple_input_init);
module_exit(simple_input_exit);
MODULE_LICENSE("GPL");
  • 現象,裝載驅動以後能夠看到內核成功獲取中斷號,當按下/擡起按鍵後會調用2次中斷回調函數
[root@iTOP-4412]# insmod simple_input.ko 
[ 9900.667121] input: Unspecified device as /devices/virtual/input/input2
[ 9900.682842] find node ok
[ 9900.683976] irqno = 94
[root@iTOP-4412]# [ 9911.710065] ------input_key_irq_handler------
[ 9911.834621] ------input_key_irq_handler------
[ 9912.706581] ------input_key_irq_handler------
  • 在應用程序中經過input_event結構體去獲取核心層上報的數據,此處與驅動中input_event函數的參數含義相同
struct input_event {
	struct timeval time;			//時間戳,表示啥時候獲取的數據
	__u16 type;					   //數據類型
	__u16 code;					   //鍵
	__s32 value;				   //值
};
  • 所有應用層代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>

int main(int argc, char *agvr[])
{
    int fd;
    int rd_ret;
    struct input_event event_data;
    fd = open("/dev/input/event1", O_RDWR);
    if(fd < 0)
    {
        printf("open error\n");
        return -1;
    }
    while(1)
    {
        rd_ret = read(fd, &event_data, sizeof(event_data));
        if(rd_ret < 0)
        {
            printf("read error\n");
            return -1;
        }
        if(event_data.type == EV_KEY)
        {
            if(event_data.code == KEY_POWER)
            {
                if(event_data.value > 0)
                {
                    printf("__APP_USER__: power key pressed\n");
                }
                else
                {
                    printf("__APP_USER__: power key up\n");
                }
            }
        }
    }
    close(fd);
    return 0;
}
  • 現象,按下按鍵的時候可發現應用程序可成功讀取到數據
[root@iTOP-4412]# ./simple_input_test 
[ 1896.714053] ------input_key_irq_handler------
__APP_USER__: power key pressed
[ 1897.863495] ------input_key_irq_handler------
__APP_USER__: power key up
[ 1899.033702] ------input_key_irq_handler------
__APP_USER__: power key pressed
[ 1900.166032] ------input_key_irq_handler------
__APP_USER__: power key up

3.3input device初始化詳解

3.3.1輸入設備類型

  • 在實際輸入設備中不一樣設備會產生不一樣的數據類型

鍵盤:鍵盤會產生鍵值(按鍵值),在linux系統中不一樣按鍵都對應一個具體的值node

#define KEY_MUTE		113		
#define KEY_VOLUMEDOWN	 114		//音量-
#define KEY_VOLUMEUP	 115		//音量+
#define KEY_POWER		 116		//電源按鍵

觸摸屏:產生座標(絕對座標),有一個原點(0,0)linux

#define ABS_X			0x00			//x值
#define ABS_Y			0x01			//y值
#define ABS_PRESSURE		0x18
define ABS_MT_TOUCH_MAJOR	0x30	/* Major axis of touching ellipse */
#define ABS_MT_TOUCH_MINOR	0x31	/* Minor axis (omit if circular) */
#define ABS_MT_WIDTH_MAJOR	0x32	/* Major axis of approaching ellipse */
#define ABS_MT_WIDTH_MINOR	0x33	/* Minor axis (omit if circular) */
#define ABS_MT_ORIENTATION	0x34	/* Ellipse orientation */
#define ABS_MT_POSITION_X	0x35	/* Center X touch position */
#define ABS_MT_POSITION_Y	0x36	/* Center Y touch position */

鼠標:產生座標(相對座標)shell

#define REL_X			0x00
#define REL_Y			0x01
  • 正由於有不一樣輸入設備因此就有input_dev結構體,表示的是一個具體的輸入設備,它會描述設備可以產生什麼樣的數據,其內部有不少成員變量和方法,在此咱們需關係如下幾個
struct input_dev {
	const char *name;			//系統中給用戶看的信息
	const char *phys;
	const char *uniq;
	struct input_id id;

	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

    //位表,每1個位表明不一樣類型的數值,描述輸入設備可以產生什麼類型的數據
	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)];
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];

	struct device dev;			//繼承device對象

	struct list_head	h_list;	//
	struct list_head	node;	//表示節點
};
  • 3.2中按鍵數據類型用EV_KEY表示,其餘數據類型表示以下
#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		//聲音
#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),那麼就須要有按鍵的值(KEY_POWER),即哪一個按鍵被操做,而後就是按鍵的狀態(value)(0或者1)
  • 同理,對於屏幕(EV_ABS),那麼就須要相對座標值(ABS_X,ABS_Y)和壓力(ABS_PRESSURE),其值表示按下的點的位置(座標)
  • 圖解
struct input_event {
	struct timeval time;			//時間戳,表示啥時候獲取的數據
	__u16 type;					   //數據類型
	__u16 code;					   //鍵
	__s32 value;				   //值
};

3.3.2初始化輸入設備

  • 既然輸入設備有不少類型,那麼咱們在編寫驅動代碼的時候就須要告訴系統咱們所編寫設備驅動是屬於哪種類型(按鍵,屏幕...)
  • 相關API
__set_bit(int nr, volatile unsigned long *addr);
  • 因爲輸入設備在系統中會建立eventx系列文件,但在/dev/input目錄下是沒法查看其具體屬性的,所以咱們可在/sys/class/input/下查看具體輸入設備信息
topeet@ubuntu:/sys/class$ cd /sys/class/input/
topeet@ubuntu:/sys/class/input$ ls
event0  event1  event2  event3  event4  input0  input1  input2  input3  input4  js0  mice  mouse0  mouse1  mouse2
topeet@ubuntu:/sys/class/input$ cd event0
topeet@ubuntu:/sys/class/input/event0$ ls
dev  device  power  subsystem  uevent
topeet@ubuntu:/sys/class/input/event0$ cd device
# 此處能夠在設備信息下面看到具體設備信息,如name,phys,uniq,id等,此處正好對應3.3.1中input_dev對象中的成員
topeet@ubuntu:/sys/class/input/event0/device$ ls
capabilities  device  event0  id  modalias  name  phys  power  properties  subsystem  uevent  uniq
topeet@ubuntu:/sys/class/input/event0/device$ cat name
Power Button
  • 所以咱們能夠在input_dev對象中設置輸入設備的屬性,以下所示,以後即可在/sys/class/input/查看到具體信息
/*添加輸入設備信息*/
inputdev->name = "simple input key";
inputdev->phys = "key/input/key0";
inputdev->uniq = "key0 for exynos4412";
inputdev->id.bustype = BUS_HOST;    //與CPU鏈接的總線類型,此處GPIO控制
inputdev->id.vendor = 0x1234;       //供應商編號
inputdev->id.product = 66;          //產品編號
inputdev->id.version = 0x0001;      //版本號碼

3.3.3位表

  • 在linux中可使用位表來表示不一樣按鍵,假設有一個鍵盤有100個按鍵,那麼它將使用100位的二進制數字來表示,每個按鍵對應一個位
  • 那麼在linux系統中最多能夠表示多少按鍵呢,下面根據源代碼分析
/*input_dev對象中的一個成員數組*/
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];		//相似long buffer[24]

/*可見最多有0x2ff+1位(768種)*/
#define KEY_MAX			0x2ff
#define KEY_CNT			(KEY_MAX+1)
  • 能夠看出,linux系統支持768種按鍵,那麼就須要一個768位的數據類型,顯然沒有這種數據類型的存在.可是因爲數組是一段連續存儲的空間,因此可使用768/32=24個長整形(8Byte)來表示768位數據
  • 舉個例子,KEY_POWER宏定義爲#define KEY_POWER 116,即用keybit數組中的第116位來表示電源鍵,也就是說要把第116位給置1
  • 怎麼將其置1呢?很簡單,首先咱們要算出將第116位是存儲在數組的第幾個元素中,因爲數組中一個成員爲長整形(32bit),因此第116位是存儲在第116/32+1=4個元素裏面,即keybit[3].以後再計算是屬於第4個元素中的哪一位,即116%32=20位,即將keybit[3]中的第20位置1便可,經過移位便可實現.下述兩種方法原理均是如此,但方法一更簡潔,推薦使用

方法一:編程

__set_bit(KEY_POWER, inputdev->keybit);     //表示當前設備可以產生power按鍵數據

/*源碼分析*/
static inline void __set_bit(int nr, volatile unsigned long *addr)
{
	unsigned long mask = BIT_MASK(nr);							  //先移位置1
	unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);		//再經過指針偏移將具體數組元素賦值

	*p  |= mask;
}

#define BIT_MASK(nr)		(1UL << ((nr) % BITS_PER_LONG))
#define BIT_WORD(nr)		((nr) / BITS_PER_LONG)

方法二:ubuntu

inputdev->keybit[BIT_WORD(KEY_POWER)] |= BIT_MASK(KEY_POWER);   //另外一種設置位的方法

3.3.4輸入設備數據上報

  • 數據上報API

通用類型:數組

/*
功能:輸入子系統向核心層上報數據
參數:
參數1:input device對象
參數2:上報的數據類型
	EV_KEY	按鍵
	EV_ABS	絕對座標(屏幕)
參數3:上報的具體數據是啥;例如按鍵,對應的按鍵值是啥,電源鍵仍是音量鍵...
參數4:值是什麼;例如按下爲1,擡起爲0
*/
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);

專有類型:其實就是調用通用類型app

static inline void input_report_key(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_KEY, code, !!value);	//!!確保上報數據類型爲0或者1
}

static inline void input_report_rel(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_REL, code, value);
}

static inline void input_report_abs(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_ABS, code, value);
}

static inline void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_FF_STATUS, code, value);
}

static inline void input_report_switch(struct input_dev *dev, unsigned int code, int value)
{
	input_event(dev, EV_SW, code, !!value);
}

4.輸入子系統編程(驅動多個按鍵)

  • 首先來總結如下驅動一個按鍵時候須要使用那些資源

1.中斷號碼框架

2.按鍵狀態(經過GPIO數據寄存器得到)函數

3.按鍵的值(code)源碼分析

  • 因爲按鍵是經過設備樹來描述,當咱們有3個按鍵的時候天然也須要修改設備樹
相關文章
相關標籤/搜索