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_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是值。
输入子系统实验的源代码如下。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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——正点原子