Linux下input子系统

input子系统简单介绍

input子系统是管理输入的子系统,和pinctrl和gpio子系统一样,都是Linux内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框架如下图所示。
在这里插入图片描述
驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
事件层:主要和用户空间进行交互。
input核心层会向Linux内核注册一个字符设备,具体的代码在/drivers/input/input.c文件中,input.c就是input输入子系统的核心层,其中注册了一个input类,系统启动后会在/sys/class目录下有一个input文件。
在这里插入图片描述
input子系统的所有设备主设备号都为13,我们在使用input子系统处理输入设备的时候不需要注册字符设备,只需要向系统注册一个input_dev即可。input_dev结构体定义在/include/linux/input.h文件中。

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

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

	unsigned int hint_events_per_packet;
	...
};

其中,keybit是按键事件使用的位图,evbit表示输入事件类型,可选的事件类型定义在/include/uapi/linux/input.h文件中,事件类型如下。

#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  //sound
#define EV_REP			0x14  //重复事件
#define EV_FF			0x15  //压力事件
#define EV_PWR			0x16  //电源事件
#define EV_FF_STATUS	0x17  //压力状态事件

如果要用到按键,就要注册EV_KEY事件,如果要多次按下的话还需要注册EV_REP事件。
Linux内核定义了很多按键值,这些按键值定义在/include/uapi/linux/input.h文件中,如下。

#define KEY_RESERVED	0
#define KEY_ESC			1
#define KEY_1			2
#define KEY_2			3
#define KEY_3			4
#define KEY_4			5
#define KEY_5			6
#define KEY_6			7
#define KEY_7			8
#define KEY_8			9
#define KEY_9			10
#define KEY_0			11
...

可以将开发板上的按键值设置为其中的一个。


相关的函数

编写input设备驱动的时候需要先申请一个input_dev结构体变量, 使用input_allocate_device函数来申请一个input_dev,该函数原型如下。

struct input_dev *input_allocate_device(void)

返回值是申请到的input_dev。
申请好input_dev以后就需要初始化,需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种。
初始化完成以后就需要向Linux内核注册input_dev了,需要用到input_register_device函数,此函数原型如下。

int input_register_device(struct input_dev *dev)

dev表示要注册的input_dev,返回值为0表示注册成功,返回负值表示注册失败。
注销的时候需要使用input_unregister_device函数,函数原型如下。

void input_unregister_device(struct input_dev *dev)

注销的input设备需要使用input_free_device函数来释放input_dev,input_free_device函数原型如下。

void input_free_device(struct input_dev *dev)

input_dev注册过程

使用input_allocate_device函数申请一个input_dev;初始化input_dev的事件类型以及事件值;使用input_register_device函数向Linux系统注册前面初始化好的input_dev;卸载input 驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的input_dev。
input_dev注册过程的大体框架如下。

struct input_dev *inputdev;  //定义input子系统结构体变量

static int __init xxx_init(void)  //驱动入口函数
{
    
    
	...
	
	inputdev = input_allocate_device(); /* 申请input_dev */
	inputdev->name = "test_inputdev"; /* 设置input_dev 名字 */
	
	/*第一种设置事件和事件值的方法*/
	__set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
	__set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
	__set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
	
	/*第二种设置事件和事件值的方法*/
	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
	
	/*第三种设置事件和事件值的方法*/
	keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
	input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
	
	/* 注册input_dev */
	input_register_device(inputdev);
	return 0;
}

static void __exit xxx_exit(void)   //驱动出口函数
{
    
    
	input_unregister_device(inputdev); /* 注销input_dev */
	input_free_device(inputdev); /* 释放input_dev */
}

上报输入事件

以上工作完成后还不能使用input设备,input设备都是具有输入功能的,但是具体是什么样的输入值Linux内核是不知道的,因此需要获取到具体的输入值或输入事件,然后将其上报给Linux内核。比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给Linux内核,这样Linux内核才能获取到正确的输入值。
input_event函数用于上报指定的事件以及对应的值,函数原型如下。

void input_event(struct input_dev *dev,unsigned int type,unsigned int code,int value)

dev是需要上报的input_dev;type是上报的事件类型,比如EV_KEY;code是事件码,即注册的按键值,比如KEY_0、KEY_1等;value表示事件值,比如1表示按键按下,0表示按键松开。
input_event函数可以上报所有的事件类型和事件值,Linux内核也提供了其他的针对具体事件的上报函数, 这些函数其实都用到了input_event函数。比如上报按键所使用的input_report_key函数,此函数内容如下。

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

上报事件以后还需要使用input_sync函数来告诉Linux内核input子系统上报结束,input_sync函数本质是上报一个同步事件,此函数原型如下。

void input_sync(struct input_dev *dev)

input_event结构体定义在/include/uapi/linux/input.h文件中,其内容如下。

struct input_event {
    
    
	struct timeval time;
	__u16 type;
	__u16 code;
	__s32 value;
};

time是此事件发生的时间,type是事件类型,code是事件码,value是值。


按键的input子系统实验

输入子系统实验的源代码如下。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h> 
#include <linux/of_irq.h>
#include <linux/input.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/timer.h>

struct device_node	*dev_node;   /* 设备节点 */
struct input_dev *inputdev;  //定义input子系统结构体变量
int key_gpio;			  /* key所使用的GPIO编号*/
int irq;
int count = 0;
int value = 0;
int wq_flags = 0;  //标志位

static void timer_function(unsigned long data);
DEFINE_TIMER(key_timer,timer_function,0,0);     //静态定义结构体变量并且初始化function,expires,data成员
DECLARE_WAIT_QUEUE_HEAD(key_wq);   // 定义并初始化等待队列头

static void timer_function(unsigned long data)
{
    
    
	if(count%2 == 1)
	{
    
    
		value = 1;
		printk("key_value = %d\r\n",value);
		input_report_key(inputdev,KEY_0,value);  //上报输入事件
		input_sync(inputdev);   //同步
	}
	else
	{
    
    
		value = 0;
		printk("key_value = %d\r\n",value);
		input_report_key(inputdev,KEY_0,value);  //上报输入事件
		input_sync(inputdev);   //同步
	}
	printk("key trigger count: %d\r\n",count);
}

static irqreturn_t key_handler(int irq, void *args)
{
    
    
	mod_timer(&key_timer,jiffies + msecs_to_jiffies(20));
	++count;
	wq_flags = 1;  //这里只有先置1,下面的函数才能唤醒
	wake_up(&key_wq);  //唤醒
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int input_probe(struct platform_device *pdev)
{
    
    
	int ret=0;
	dev_node = of_find_node_by_path("/key");
	if (dev_node == NULL) 
		return -EINVAL;
	printk("Find node key!\n");
	key_gpio = of_get_named_gpio(dev_node,"key-gpio",0);
	if (key_gpio < 0) 
	{
    
    
		printk("of_get_named_gpio failed!\r\n");
		return -EINVAL;
	}
	printk("key_gpio = %d\r\n",key_gpio);
	gpio_request(key_gpio,"gpiokey");	    //申请gpio
	gpio_direction_input(key_gpio);   	//将gpio设置为输入

	irq = gpio_to_irq(key_gpio);   //获得gpio中断号
	//irq = irq_of_parse_and_map(dev_node,0);
	printk("irq is %d\n", irq);
	ret = request_irq(irq,key_handler,IRQF_TRIGGER_FALLING,"test_key",NULL);  //申请中断
	if(ret < 0)
	{
    
    
		printk("request_irq error!\r\n");
	}
	return 0;
}

static int input_remove(struct platform_device *pdev)
{
    
    
	gpio_free(key_gpio);
	free_irq(irq,NULL);
	del_timer(&key_timer);
	return 0;
}

const struct of_device_id of_match_table_key[] = {
    
    
	{
    
    .compatible = "gpio_bus_key"},         //与设备树中的compatible属性匹配
	{
    
    }
};

struct platform_driver dts_device = {
    
        
	.probe = input_probe,
	.remove = input_remove,
	.driver = {
    
    
		.owner = THIS_MODULE,
		.name = "keygpio",
		.of_match_table = of_match_table_key
	}
};

static int __init input_key_init(void)
{
    
    	
	int ret;
	platform_driver_register(&dts_device); 
	inputdev = input_allocate_device();    // 申请input_dev
	inputdev->name = "key_inputdev";      //设置input_dev名字
	__set_bit(EV_KEY, inputdev->evbit);  //设置产生按键事件
	//__set_bit(EV_REP, inputdev->evbit);  //重复事件
	__set_bit(KEY_0, inputdev->keybit);  //设置产生哪些按键值
	ret = input_register_device(inputdev);  //注册input_dev
	if(ret < 0)
		printk("input_register_device error!\n");
	printk("input_register_device ok!\n");
	return 0;
}

static void __exit input_key_exit(void)
{
    
    
	input_unregister_device(inputdev);   //注销input_dev
	input_free_device(inputdev);     //释放input_dev
	platform_driver_unregister(&dts_device);
	printk("driver exit!\n");
}

module_init(input_key_init);
module_exit(input_key_exit);
MODULE_LICENSE("GPL");

经过自己验证,不要在代码中设置EV_REP,因为这样收到的值不太正确,具体的原因自己也没找到。
测试代码的内容如下。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    
    
    int fd;
    struct input_event test_event;
    fd = open("/dev/input/event2", O_RDWR);   //这里的"/dev/input/event2"根据开发板上新生成的输入设备添加
    if(fd < 0)
    {
    
    
        perror("open key error\n"); 
        return fd;
    }
    while(1)
    {
    
    
        read(fd,&test_event,sizeof(test_event));
        if(test_event.type == EV_KEY)
        {
    
    
            printf("type is %#x.\n",test_event.type);
            printf("code is %d.\n",test_event.code);
            printf("value is %d.\n",test_event.value);
        }
    }
    return 0;
}

上面代码中打开的文件可以通过下图所示的方式得知。
在这里插入图片描述
首先在没有加载驱动的时候看看/dev/input下的文件,然后加载驱动后多出来的那一个就是要打开的。
可以通过命令 cat /proc/bus/input/devices查看驱动加载成功后的输入设备信息,如下图所示
在这里插入图片描述
名字和代码中设置的一致,事件也对应的是驱动加载成功以后多出来的文件。
超级终端打印的信息如下图所示。
在这里插入图片描述
type的类型值为0x1,表示这是一个按键事件。

#define EV_KEY			0x01  //按键事件

code的值是11,这是因为我在代码中使用了KEY_0。

#define KEY_0			11

value的值就是通过输入子系统传来的,定义输入事件结构体读取到传来的值,可以看到,其和key_value的值相等,这就说明传过来的值是正确的。
进一步拓展,input子系统也可以配合LED的驱动来使用,分别加载按键和LED的驱动,然后在测试程序中将读取到的值写入LED中,这样,LED的亮灭就是通过按键控制的。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    
    
    int fd,fd_led;
    struct input_event test_event;
    fd = open("/dev/input/event2", O_RDWR);   //这里的"/dev/input/event2"根据开发板上新生成的输入设备添加

    fd_led = open("/dev/gpioled",O_RDWR); 
    if(fd < 0)
    {
    
    
        perror("open key error\n"); 
        return fd;
    }
    if(fd_led < 0)
    {
    
    
        perror("open led error\n"); 
        return fd_led;
    }
    while(1)
    {
    
    
        read(fd,&test_event,sizeof(test_event));  //通过输入子系统读取内核传来的值
        if(test_event.type == EV_KEY)
        {
    
    
            printf("type is %#x.\n",test_event.type);
            printf("code is %d.\n",test_event.code);
            printf("value is %d.\n",test_event.value);
            write(fd_led,&test_event.value,sizeof(test_event.value));  //给led写值
        }
    }
    return 0;
}

运行过程中的打印信息如下图所示。
在这里插入图片描述
可以看到,LED的亮灭状态随着键值的变化而变化,说明确实是按键控制着LED的状态。
总的来说,input子系统的引入不再需要我们再定义字符设备,input核心层会向Linux内核注册一个字符设备。


本文参考文档:
I.MX6U嵌入式Linux驱动开发指南V1.5——正点原子

猜你喜欢

转载自blog.csdn.net/weixin_42570192/article/details/133605246