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系统中不同按键都对应一个具体的值

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

触摸屏:产生坐标(绝对坐标),有一个原点(0,0)

#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 */

鼠标:产生坐标(相对坐标)

#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)

方法二:

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);

专有类型:其实就是调用通用类型

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个按键的时候自然也需要修改设备树
posted @ 2021-08-05 23:55  MHDSG  阅读(226)  评论(0)    收藏  举报