Linux驱动开发之输入子系统
2020-02-15
关键字:
Linux 中输入设备大致可分以下几种:
1、按键/键盘(keyboard)
2、鼠标(mouse)
3、触摸屏(touchscreen)
4、游戏杆(joystick)
输入子系统的目的是为了屏蔽众多输入设备在硬件上的差异化,使得在开发输入设备的程序时能更简单统一。输入子系统屏蔽差异的方式就是为各种输入设备与上层应用提供统一的编程接口。
Linux 输入子系统是一种编程框架,它可以自上而下分为以下几种层次:
1、应用层
2、input handler层:数据处理层
完成 fops 的动作,并将输入事件上报给用户。
负责创建文件节点,即 /dev/input/xxx
3、input core层:输入核心层/管理层
负责为来自输入设备层的事件分发给相应的数据处理层,它内部会维护两个链表,分别记录输入事件与注册数据处理层对象。
负责申请设备号,输入设备的设备号一般是13。
负责创建类。
4、input device层:输入设备层
负责初始化硬件。
负责将硬件的原始数据上报给核心层。
5、硬件层
如:mouse, touchscreen, keyboard, joystcik
在输入子系统的开发中,需要我们去实现的就应用层与输入设备层。其余部分内核已经实现好了,直接用就行。
下面贴出一个最简单的利用输入子系统来新增输入设备驱动的源码:
#include <linux/init.h> #include <linux/module.h> #include <linux/input.h> struct input_dev *inputdev; static int __init simple_input_init(void) { //1、分配一个input device对象 inputdev = input_allocate_device(); //2、初始化input device对象 __set_bit(EV_KEY, inputdev->evbit);//表示当前正在开发的设备能够产生按键数据。 __set_bit(KEY_POWER, inputdev->keybit);//表示当前设备能够产生POWER按键事件。 //3、注册input_device对象到输入核心层 int ret = input_register_device(inputdev); return 0; } 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");
这段代码在运行以后会自动在 /dev/input 目录下新增一个 event* 的设备节点,且在 /sys/class/input 目录下也会新增一个 event* 的目录。像传统的驱动编写过程中的申请设备号、创建文件节点等工作会由内核自动帮我们去做。由此可见,通过输入子系统,我们在开发适配输入设备时会变得简单很多。这里要强调一点,不同类型的输入设备在 /dev/input 目录下所创建的节点前缀是不一样的。例如,对于键盘/按键就是 event*,而鼠标则是 mouse*,这个需要同学自行确认。
另外提一点:要想利用输入子系统框架来编写驱动,就必须保证输入子系统正确编进了系统内核镜像中。
输入核心层与数据处理层所对应的代码文件分别为:./drivers/input/input.c , ./drivers/input/evdev.c 。
可以在 make menuconfig 中将它们开启编译,具体路径为:
make menuconfig
Device Drivers
Input device support
Generic input layier -- 表示input.c
Event interface -- 表示evdev.c
在输入子系统中,用于在输入设备层向核心层上报数据的函数接口签名如下:
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
参数1 表示哪个input device上报的数据。
参数2 表示上报的是哪种类型的数据。
参数3 表示上报的具体数据是什么,比如是哪个按键。
参数4 表示值是什么。
需要注意的是,在调用完 input_event() 函数后一定要再调用 input_sync() 函数,否则上层是不会去查收设备层上报的事件的。
除了 input_event() 函数外,还有一个更细化的上报函数:
void input_report_key(struct input_dev *dev, int code, int value);
上层应用,即用户空间所读到的输入子系统上报的数据是一个统一格式的数据包,其结构体对象原型如下:
struct input_event{ struct timeval time; __u16 type; __u16 code; __s32 value; };
以下是一个示例程序,它包含驱动程序与上层应用程序。它用于演示适配开发板上一个按键的驱动与应用。
驱动源码如下:
#include <linux/init.h> #include <linux/module.h> #include <linux/input.h> #include <linux/interrupt.h> #include <linux/of.h> #include <linux/of_irq.h> #include <asm/io.h> #define GPX3_CON 0x11400c20 struct input_dev *inputdev; int irq; void *reg_base; int get_irq_from_node() { //获取到设备树中的按键节点。 struct device_node *np = of_find_node_by_path("/key_int_node"); return irq_of_parse_and_map(np, 0);//通过节点去获取到中断号码。 } irqreturn_t input_key_irq_handler(int irq, void *devid) { int val = readl(reg_base + 4) & (1<<2);//根据地址读出相应寄存器中的值。 //因为应用了输入子系统,所以不再需要驱动自行去处理阻塞、队列等操作。只需将事件 //上报给上层,框架会自动将数据继续上报上去。 if(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) { //1、分配一个input device对象 inputdev = input_allocate_device();
//添加设备信息,即 /sys/class/input/event*/device 下的name,phys,uniq,id。
inputdev->name = "my simple input key button.";
inputdev->phys = "key/input/input0";
inputdev->uniq = "simple key0 for 4412";
inputdev->id.bustype = BUS_HOST;
inputdev->id.vendor = 0x1234;
inputdev->id.product = 0x8888;
inputdev->id.version = 0x00001;
//2、初始化input device对象 __set_bit(EV_KEY, inputdev->evbit);//表示当前正在开发的设备能够产生按键数据。 __set_bit(KEY_POWER, inputdev->keybit);//表示当前设备能够产生POWER按键事件。
/*
解释一下上面两句代码。
因为输入子系统中每个类型都是用一个长位来表示的,例如keybit中就有768个位,由24个long组成的数组来表示这768位。__set_bit()函数的原理就是将指定位位置成值1。它内部就是封装了一个long数组到长位的转换而已。
*/
//3、注册input_device对象到输入核心层 int ret = input_register_device(inputdev); //拿到定义在dts中的按键中断号。 irq = get_irq_from_node();
//如果不是在dts中定义的,也可以通过 linux/gpio.h 中的 gpio_to_irq() 函数来获得GPIO的中断号。
//申请中断。 ret = request_irq(irq, input_key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "key3_eint10", NULL); //地址映射。 reg_base = ioremap(GPX3_CON, 8); return 0; } static void __exit simple_input_exit(void) { iounmap(reg_base); free_irq(irq, NULL); input_unregister_device(inputdev); input_free_device(inputdev); } module_init(simple_input_init); module_exit(simple_input_exit); MODULE_LICENSE("GPL");
上层应用源码如下:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <linux/input.h> int main() { int fd = open("/dev/event1", O_RDWR); struct input_event event; int ret; while(1) {
/*
这里需要强调一下:用于存储读取结果的没什么特殊需要最好直接用 struct input_event 结构体对象来存储。
否则的话可能会引发读取不到数据或读取时不能阻塞直接返回-1的问题。
当传入的用于存储读取结果的空间过小时,输入子系统就会直接返回-1而不阻塞在read()中。
*/ ret = read(fd, &event, sizeof(struct input_event));//驱动层会自动阻塞。 if(event.type == EV_KEY) { if(event.code == KEY_POWER) { if(event.value) { printf("key down.\n"); } else { printf("key up.\n"); } } } } return 0; }
通过这个示例可以发现,在有了输入子系统的辅助以后,开发驱动已经简单了很多很多。
接下来我们来了解一下输入子系统的实现原理。
首先来看看 input device 初始化的过程。
input device 被抽象成 input_dev 结构体,这个结构体本身非常庞大,但同样我们仅需要了解其中一小部分即可,input_dev 结构体的部分成员定义如下:
struct input_dev { //以下四个成员就是 /sys/class/input/event*/device 目录下的 name, phys, uniq, id。用户可以通过查看这几个成员记载的信息来了解当前设备。 const char *name; // sysfs中显示出来给用户看的信息 const char *phys; const char *uniq; struct input_id id; unsigned long evbit[BITS_TO_LONGS(EV_CNT)];//一个位表,该位用于描述一个输入设备能够产生什么类型的数据,如按键、坐标值等,常见的数据类型在下方有说明。 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//表示能够产生哪种按键,如KEY_POWER,KEY_ENTER等。默认能够表示768位数据,直接用24个long来表示。 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;//表示节点。 };
常用的事件类型定义如下:
#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 //声音数据
如何去驱动多个按键呢?
首先我们要将驱动的按键信息先描述在设备树文件中:
key_int_node{ compatible = "test_key"; #address-cells = <1>; //表示reg的地址长度占1个 #size-cells = <1>;//表示reg的长度的长度占1个。 key_int@0{ key_name = "key2_power_eint"; key_code = <116>; gpio = <&gpx1 1 0>; //表示GPX1_1 reg = <0x11000c20 0x18>; //第1个是地址,第2个是长度。 interrupt-parent = <&gpx1>; interrupts = <1 0>; }; key_int@1{ key_name = "key3_vup_eint"; key_code = <115>; gpio = <&gpx1 2 0>; reg = <0x11000c20 0x18>; interrupt-parent = <&gpx1>; interrupts = <2 0>; }; key_int@2{ key_name = "key4_vdown_eint"; key_code = <114>; gpio = <&gpx3 2 0>; reg = <0x11000c20 0x18>; interrupt-parent = <&gpx3>; interrupts = <2 0>; }; };
然后要在代码当中获取这个设备树节点的所有信息,因为节点内部又有子节点,因此要用到以下函数来获取:
struct device_node *of_get_next_child(const struct device_node *parent_node, struct device_node *prev_node);
以下贴出利用输入子系统驱动多个按键的源码:
#include <linux/init.h> #include <linux/module.h> #include <linux/input.h> #include <linux/interrupt.h> #include <linux/of.h> #include <linux/of_irq.h> #include <linux/of_gpio.h> #include <asm/io.h> #define KEY_NUMS 3 //设计一个对象用来保存按键节点信息。 struct key_desc{ int irqno; int key_code; int gpionum; void *reg_base; struct device_node *node;//可以随时获取到节点的各个信息。 }; struct key_desc all_key[KEY_NUMS]; struct input_dev *inputdev; void get_all_child_from_node() { //获取到设备树中的按键节点。 struct device_node *np = of_find_node_by_path("/key_int_node"); struct device_node *child; struct device_node *prevnode = NULL; int idx = 0; do{ //获取到其中一个子节点。 child = of_get_next_chile(np, prevnode); if(child != NULL) { /* char *key_name; u32 code; int gpionum; int irq; irq = irq_of_parse_and_map(child, 0);//通过节点去获取到中断号码。 of_property_read_string(child, "key_name", &key_name); of_property_read_u32(child, "key_code", &code); gpionum = of_get_named_gpio(child, "gpio", 0);//读取自定义名称的GPIO值。官方定义的名称应该是gpios,但我们的是gpio。第3个参数是读第几个元素,这里我们只有一个,所以是0。 printk("name:%s,code:%d,gpionum:%d,irq:%d\n", key_name, code, gpionum, irq); */ all_key[idx++].node = child; } else { break; } prevnode = child; }while(1); } irqreturn_t input_key_irq_handler(int irq, void *devid) { struct key_desc *pdesc = (struct key_desc *)devid; int gpionum = of_get_named_gpio(pdesc->node, "gpio", 0); int value = gpio_get_value(gpionum);//有了这个函数就可以不用读寄存器了。 if(value) { //按键抬起。 input_event(inputdev, EV_KEY, pdesc->key_code, 0); input_sync(inputdev);//这句必须调,表示事件已经准备好,上层可以查收了。 } else { //按键按下。 input_event(inputdev, EV_KEY, pdesc->key_code, 1); input_sync(inputdev); } return IRQ_HANDLED; } static int __init simple_input_init(void) { //1、分配一个input device对象 inputdev = input_allocate_device(); //添加设备信息,即 /sys/class/input/event*/device 下的name,phys,uniq,id。 inputdev->name = "my simple input key button."; inputdev->phys = "key/input/input0"; inputdev->uniq = "simple key0 for 4412"; inputdev->id.bustype = BUS_HOST; inputdev->id.vendor = 0x1234; inputdev->id.product = 0x8888; inputdev->id.version = 0x00001; get_all_child_from_node(); //2、初始化input device对象 __set_bit(EV_KEY, inputdev->evbit);//表示当前正在开发的设备能够产生按键数据。 int i; for(i = 0; i < KEY_NUMS; i++) { //设置keybit,表示我们支持哪些按键。 //按键值从设备树中来。 int code = of_property_read_u32(all_key[i].node, "key_code", &code); __set_bit(code, inputdev->keybit); all_key[i].key_code = code; //申请中断。 int irqno = irq_of_parse_and_map(all_key[i].node, 0); char *name; of_property_read_string(all_key[i], "key_name", &name); all_key[i].name = name; all_key[i].irqno = irqno; ret = request_irq(irqno, input_key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, name, &all_key[i]);//最后一个参数就是中断处理函数中的 void *devid,用于区分不同按键。 } //3、注册input_device对象到输入核心层 int ret = input_register_device(inputdev); return 0; } static void __exit simple_input_exit(void) { int i; for(i = 0; i < KEY_NUMS; i++) free_irq(all_key[i}.irqno, &all_key[i]); input_unregister_device(inputdev); input_free_device(inputdev); } module_init(simple_input_init); module_exit(simple_input_exit); MODULE_LICENSE("GPL");
关于Linux的输入子系统,还有待于去多上机编写练习以及在对使用有了一定的掌握以后去阅读其源码实现才能真正掌握其精髓。