按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux内核为此专门做了一个叫做input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了input框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input核心层负责处理这些事件。本章就来学习一下Linux内核中的input子系统。
input子系统就是管理输入的子系统,和pinctrl、gpio子系统一样,都是Linux内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,只需要按照要求上报这些输入事件即可。为此input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框
架如下图所示:
上图中左边就是最底层的具体设备,比如按键、USB键盘 /鼠标等,中间部分属于Linux内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出input子系统用到了前面讲解的驱动分层模型,编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
input核心层会向Linux内核注册一个字符设备,找到drivers/input/input.c这个文件,input.c就是input输入子系统的核心层,此文件里面有如下所示代码:
第2498行,注册一个input类,这样系统启动以后就会在/sys/class目录下有一个input子
目录,如下图所示:
第2508-2509行,注册一个字符设备,主设备号为INPUT_MAJOR,INPUT_MAJOR定义
在include/uapi/linux/major.h文件中,定义如下:
#define INPUT_MAJOR 13
因此,input子系统的所有设备主设备号都为13,在使用input子系统处理输入设备
的时候就不需要去注册字符设备了,只需要向系统注册一个input_device即可。
在使用input子系统的时候只需要注册一个input设备即可,input_dev结构体表示input设备,此结构体定义在include/linux/input.h文件中,定义如下 (有省略):
第139行,evbit表示输入事件类型,可选的事件类型定义在include/uapi/linux/input.h文件
中,事件类型如下:
比如本章要使用到按键,那么就需要注册EV_KEY事件,如果要使用连按功能的话还需要注册EV_REP事件。
继续回到示例代码38.1.2.2中,第139行-147行的evbit、keybit、relbit等等都是存放不同事件对应的值。比如本章要使用按键事件,因此要用到keybit,keybit就是按键事件使用的位图,Linux内核定义了很多按键值,这些按键值定义在include/uapi/linux/input-event-codes.h文件中,按键值如下:
可以将开发板上的按键值设置为示例代码38.1.2.4中的任意一个,比如本章实验会将STM32MP1开发板上的KEY0按键值设置为KEY_0。
在编写input设备驱动的时候需要先申请一个input_dev结构体变量,使用input_allocate_device函数来申请一个input_dev,此函数原型如下所示:
struct input_dev *input_allocate_device(void)
函数参数和返回值含义如下:
如果要注销input设备的话需要使用input_free_device函数来释放掉前面申请到的input_dev。input_free_device函数原型如下:
void input_free_device(struct input_dev *dev)
函数参数和返回值含义如下:
申请好一个input_dev以后就需要初始化这个input_dev,需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种。input_dev初始化完成以后就需要向Linux内核注册input_dev了,需要用到input_register_device函数,此函数原型如下:
int input_register_device(struct input_dev *dev)
函数参数和返回值含义如下:
同样的,注销input驱动的时候也需要使用input_unregister_device函数来注销掉前面注册的input_dev,input_unregister_device函数原型如下:
void input_unregister_device(struct input_dev *dev)
函数参数和返回值含义如下:
综上所述,input_dev注册过程如下:
input_dev注册过程示例代码如下所示:
示例代码38.1.2.5 input_dev注册流程
1 struct input_dev *inputdev; /* input结构体变量 */
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6 ......
7 inputdev = input_allocate_device(); /* 申请input_dev */
8 inputdev->name = "test_inputdev"; /* 设置input_dev名字 */
9
10 /*********第一种设置事件和事件值的方法***********/
11 __set_bit(EV_KEY, inputdev->evbit); /* 设置产生按键事件 */
12 __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */
13 __set_bit(KEY_0, inputdev->keybit); /*设置产生哪些按键值 */
14 /************************************************/
15
16 /*********第二种设置事件和事件值的方法***********/
17 keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
18 keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
19 /************************************************/
20
21 /*********第三种设置事件和事件值的方法***********/
22 keyinputdev.inputdev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
23 input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
24 /************************************************/
25
26 /* 注册input_dev */
27 input_register_device(inputdev);
28 ......
29 return 0;
30 }
31
32 /* 驱动出口函数 */
33 static void __exit xxx_exit(void)
34 {
35 input_unregister_device(inputdev); /* 注销input_dev */
36 input_free_device(inputdev); /* 删除input_dev */
37 }
第1行,定义一个input_dev结构体指针变量。
第4-30行,驱动入口函数,在此函数中完成input_dev的申请、设置、注册等工作。第7行调用input_allocate_device函数申请一个input_dev。第10-23行都是设置input设备事件和按键值,这里用了三种方法来设置事件和按键值。第27行调用input_register_device函数向Linux内核注册inputdev。
第33-37行,驱动出口函数,第35行调用input_unregister_device函数注销前面注册的input_dev,第36行调用input_free_device函数删除前面申请的input_dev。
当向Linux内核注册好input_dev以后需要获取到具体的输入值,或者说是输入事件,然后将输入事件上报给Linux内核。比如按键,需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给Linux内核,这样Linux内核才能获取到正确的输入值。不同的事件,其上报事件的API函数不同,依次来看一下一些常用的事件上报API函数。
首先是input_event函数,此函数用于上报指定的事件以及对应的值,函数原型如下:
void input_event(struct input_dev *dev,
unsigned int type,
unsigned int code,
int value)
函数参数和返回值含义如下:
input_event函数可以上报所有的事件类型和事件值,Linux内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了input_event函数。比如上报按键所使用的input_report_key函数,此函数内容如下:
从示例代码38.1.2.6可以看出,input_report_key函数的本质就是input_event函数,如果要上报按键事件的话还是建议使用input_report_key函数。
同样的还有一些其他的事件上报函数,这些函数如下所示:
void input_report_rel(struct input_dev *dev, unsigned int code, int value)
void input_report_abs(struct input_dev *dev, unsigned int code, int value)
void input_report_ff_status(struct input_dev *dev, unsigned int code, int value)
void input_report_switch(struct input_dev *dev, unsigned int code, int value)
void input_mt_sync(struct input_dev *dev)
当上报事件以后还需要使用input_sync函数来告诉Linux内核input子系统上报结束,input_sync函数本质是上报一个同步事件,此函数原型如下所示:
void input_sync(struct input_dev *dev)
函数参数和返回值含义如下:
综上所述,按键的上报事件的参考代码如下所示:
示例代码38.1.2.7 事件上报参考代码
1 /* 用于按键消抖的定时器服务函数 */
2 void timer_function(unsigned long arg)
3 {
4 unsigned char value;
5
6 value = gpio_get_value(keydesc->gpio); /* 读取IO值 */
7 if(value == 0){ /* 按下按键 */
8 /* 上报按键值 */
9 input_report_key(inputdev, KEY_0, 1); /* 最后一个参数1,按下 */
10 input_sync(inputdev); /* 同步事件 */
11 } else { /* 按键松开 */
12 input_report_key(inputdev, KEY_0, 0); /* 最后一个参数0,松开 */
13 input_sync(inputdev); /* 同步事件 */
14 }
15 }
第6行,获取按键值,判断按键是否按下。
第9-10行,如果按键值为0那么表示按键被按下了,如果按键按下的话就要使用input_report_key函数向Linux系统上报按键值,比如向Linux系统通知KEY_0这个按键按下了。
第12-13行,如果按键值为1的话就表示按键没有按下,是松开的。向Linux系统通知KEY_0这个按键没有按下或松开了。
Linux内核使用input_event这个结构体来表示 所有的输入事件,input_envent结构体定义在include/uapi/linux/input.h文件中,结构体内容如下:
依次来看一下input_event结构体中的各个成员变量:
input_envent这个结构体非常重要,因为所有的输入设备最终都是按照input_event结构体呈现给用户的,用户应用程序可以通过input_event来获取到具体的输入事件或相关的值,比如按键值等。关于input子系统就讲解到这里,接下来就以开发板上的KEY0按键为例,讲解一下如何编写input驱动。
就是之前的按键原理图。
首先在stm32mp15-pinctrl.dtsi文件中创建按键对应的pinctrl子节点,内容如下:
示例代码38.3.1.1 key_pins_a节点
1 key_pins_a: key_pins-0 {
2 pins1 {
3 pinmux = <STM32_PINMUX('G', 3, GPIO)>, /* KEY0 */
4 <STM32_PINMUX('H', 7, GPIO)>; /* KEY1 */
5 bias-pull-up;
6 slew-rate = <0>;
7 };
8
9 pins2 {
10 pinmux = <STM32_PINMUX('A', 0, GPIO)>; /* WK_UP */
11 bias-pull-down;
12 slew-rate = <0>;
13 };
14 };
这里将STM32MP1开发板上的三个按键:KEY0、KEY1和WK_UP都设置了。由于KEY0和KEY1这两个按键是低电平有效(按下以后为低电平),WK_UP是高电平有效(按下以后为高电平)。所以这里分开初始化,其中第2-7行初始化KEY0和KEY1使用的PG3和PH7这两个引脚,第9-13行初始化WK_UP使用的PA0引脚。
接下就是创建按键设备节点,可以直接在之前的key节点上修改即可,修改完成以后如下所示:
示例代码38.3.1.2 key节点
1 key {
2 compatible = "alientek,key";
3 status = "okay";
4 pinctrl-names = "default";
5 pinctrl-0 = <&key_pins_a>;
6 key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
7 interrupt-parent = <&gpiog>;
8 interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
9 };
按键结构体key_dev中,需要input_dev结构体指针*idev;需要timer_list结构体的timer来辅助按键消抖;需要int类型的gpio_key表示按键的GPIO编号以及int类型的irq_key表示按键对应的中断号。
编写按键的中断服务函数static irqreturn_t的函数key_interrupt,里面需要先disable_irq_nosync禁止中断,然后通过mod_timer激活定时器来消抖。
编写key_gpio_init来初始化按键,里面就是常规操作了,先of_get_named_gpio从设备树获取按键的GPIO,然后gpio_request申请GPIO,设置输入gpio_direction_input,通过irq_of_parse_and_map来获取对应的中断号,之后irq_get_trigger_type获取中断触发类型,并request_irq申请中断。
编写定时器的中断服务函数完成按键消抖,key_timer_function中,gpio_get_value获取当前按键值后,input_report_key上报案件时间,并通过input_sync来同步,最后enable_irq开启中断。
编写platform驱动的probe函数atk_key_probe,完成GPIO的初始化,调用key_gpio_init初始化,然后timer_setup初始化定时器;input子系统需要input_allocate_device申请设备,然后设置key.idev->name名字,通过__set_bit来设置事件以及触发的按键值(也可以通过BIT_MASK设置,效果一样),最后input_register_device注册input设备。
编写platform驱动的remove函数atk_key_remove,需要free_irq释放中断号,gpio_free释放GPIO,del_timer_sync删除timer,最后input_unregister_device释放input设备。
platform还需要编写一个of_device_id结构体的列表key_of_match[],设置.compatible属性,保持与设备树中节点的compatible一致。
然后设置platform_driver结构体的atk_key_driver这个platform驱动,设置.driver中的.name(设备树中名字)以及.of_match_table;然后设置.probe以及.remove函数。
最后module_platform_driver(atk_key_driver)即可。
传入的argc就2个;open(argv[1], O_RDWR)之后,在for死循环中read读取设备,通过传入的值判断具体的按键情况,最后写一个close就可以了。
Makefile还是老样子,改一下obj-m为keyinput.o然后“make”就可以了。
可以通过如下命令编译:
arm-none-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp |
将上一小节编译出来keyinput.ko和keyinputApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中。在加载 keyinput.ko驱动模块之前,先看一下/dev/input目录下都有哪些文件,结果如下图所示:
可以从上图看出,当前在/dev目录下不存在input文件夹,因为此时系统中并没有加载过input输入类设备,所以这个文件夹不存在。接下来输入如下命令加载keyinput.ko这个驱动模块:
depmod //第一次加载驱动的时候需要运行此命令 modprobe keyinput.ko //加载设备模块 |
当驱动模块加载成功以后再来看一下/dev/input目录下有哪些文件,结果如下图所示:
从上图可以看出,在/dev/input目录下生成了一个event0文件,这其实就是注册的驱动所对应的设备文件。keyinputApp就是通过读取/dev/input/event0这个文件来获取输入事
件信息的,输入如下测试命令:
./keyinputApp /dev/input/event0 |
之后按下KEY0按键即可被检测到。
另外,也可以不用keyinputApp来测试驱动,可以直接使用hexdump命令来查看/dev/input/event0文件内容,输入如下命令:
hexdump /dev/input/event0 |
上图就是input_event类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:
type为事件类型,查看示例代码38.1.2.3可知,EV_KEY事件值为1,EV_SYN事件值为0。因此第1行表示EV_KEY事件,第2行表示EV_SYN事件。code为事件编码,也就是按键号,查看示例代码38.1.2.4可知,KEY_0这个按键编号为11,对应的十六进制为0xb,因此第1行表示KEY_0这个按键事件,最后的value就是按键值,为1表示按下,为0的话表示松开。
综上所述,示例代码38.4.2.1中的原始事件值含义如下:
Linux内核也自带了KEY驱动,如果要使用内核自带的KEY驱动的话需要配置Linux内核,不过Linux内核一般默认已经使能了KEY驱动,但是还是要检查一下。按照如下路径找到相应的配置选项:
-> Device Drivers -> Input device support -> Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y]) -> Keyboards (INPUT_KEYBOARD [=y]) ->GPIO Buttons |
选中“GPIO Buttons”,将其编译进入Linux内核中,如下图所示:
选中以后就会在.config文件中出现“CONFIG_KEYBOARD_GPIO=y”这一行,Linux内核就会根据这一行来将KEY驱动文件编译进Linux内核。Linux内核自带的KEY驱动文件为drivers/input/keyboard/gpio_keys.c,gpio_keys.c采用了platform驱动框架,在KEY驱动上使用了input子系统实现。在gpio_keys.c文件中找到如下所示内容:
从示例代码38.5.1.1可以看出,这就是一个标准的platform驱动框架,如果要使用设备树来描述KEY设备信息的话,设备节点的compatible属性值要设置为“gpio-keys”。当设备和驱动匹配以后gpio_keys_probe函数就会执行,gpio_keys_probe函数内容如下(为了篇幅有缩减):
示例代码 38.5.1.2 gpio_keys_probe 函数代码段
764 static int gpio_keys_probe(struct platform_device *pdev)
765 {
766 struct device *dev = &pdev->dev;
767 const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
768 struct fwnode_handle *child = NULL;
769 struct gpio_keys_drvdata *ddata;
770 struct input_dev *input;
771 int i, error;
772 int wakeup = 0;
773
774 if (!pdata) {
775 pdata = gpio_keys_get_devtree_pdata(dev);
776 if (IS_ERR(pdata))
777 return PTR_ERR(pdata);
778 }
......
793 input = devm_input_allocate_device(dev);
794 if (!input) {
795 dev_err(dev, "failed to allocate input device\n");
796 return -ENOMEM;
797 }
.....
806 input->name = pdata->name ? : pdev->name;
807 input->phys = "gpio-keys/input0";
808 input->dev.parent = dev;
809 input->open = gpio_keys_open;
810 input->close = gpio_keys_close;
811
812 input->id.bustype = BUS_HOST;
813 input->id.vendor = 0x0001;
814 input->id.product = 0x0001;
815 input->id.version = 0x0100;
.....
821 /* Enable auto repeat feature of Linux input subsystem */
822 if (pdata->rep)
823 __set_bit(EV_REP, input->evbit);
824
825 for (i = 0; i < pdata->nbuttons; i++) {
826 const struct gpio_keys_button *button = &pdata->buttons[i];
827
828 if (!dev_get_platdata(dev)) {
829 child = device_get_next_child_node(dev, child);
830 if (!child) {
831 dev_err(dev,
832 "missing child device node for entry %d\n",
833 i);
834 return -EINVAL;
835 }
836 }
837
838 error = gpio_keys_setup_key(pdev, input, ddata,
839 button, i, child);
840 if (error) {
841 fwnode_handle_put(child);
842 return error;
843 }
844
845 if (button->wakeup)
846 wakeup = 1;
847 }
848
849 fwnode_handle_put(child);
850
851 error = input_register_device(input);
852 if (error) {
853 dev_err(dev, "Unable to register input device, error: %d\n",
854 error);
855 return error;
856 }
857
858 device_init_wakeup(dev, wakeup);
859
860 return 0;
861 }
第775行,调用gpio_keys_get_devtree_pdata函数从设备树中获取到KEY相关的设备节点信息。
第793行,使用devm_input_allocate_device函数申请input_dev。
第806-815,初始化input_dev。
第823行,设置input_dev事件,这里设置了EV_REP事件。
第838行,调用gpio_keys_setup_key函数继续设置KEY,此函数会设置input_dev的EV_KEY事件以及事件码(也就是KEY模拟哪个按键)。
第851行,调用input_register_device函数向Linux系统注册input_dev。
接下来再来看一下gpio_keys_setup_key函数,此函数内容如下:
第614行,调用input_set_capability函数设置EV_KEY事件以及KEY的按键类型,也就是KEY作为哪个按键?会在设备树里面设置指定的KEY作为哪个按键。
一切都准备就绪以后剩下的就是等待按键按下,然后向Linux内核上报事件,事件上报是在gpio_keys_irq_isr函数中完成的,此函数内容如下:
gpio_keys_irq_isr是按键中断处理函数,第447行向Linux系统上报EV_KEY事件,表示按键按下。第448行使用input_sync函数向系统上报EV_REP同步事件。
综上所述,Linux内核自带的gpio_keys.c驱动文件思路和前面编写的keyinput.c驱动文件基本一致。都是申请和初始化input_dev,设置事件,向Linux内核注册input_dev。最终在按键中断服务函数或者消抖定时器中断服务函数中上报事件和按键值。
要使用Linux内核自带的按键驱动程序很简单,只需要根据Documentation/devicetree/bindings/input/gpio-keys.txt这个文件在设备树中添加指定的设备节点
即可,节点要求如下:
这里将开发板上的三个按键都用起来,KEY0、KEY1和WKUP分别模拟为键盘上的:L、S和Enter(回车)健。
打开stm32mp157d-atk.dts,先添加一个头文件“dt-bindings/input/input.h”此文件就是“linux,code”属性的按键宏定义,如下图所示:
头文件添加完成以后再根据上面的要求创建对应的设备节点,设备节点内容如下所示:
示例代码 38.5.2.1 gpio-keys 节点内容
1 gpio-keys {
2 compatible = "gpio-keys";
3 pinctrl-names = "default";
4 pinctrl-0 = <&key_pins_a>;
5 autorepeat;
6
7 key0 {
8 label = "GPIO Key L";
9 linux,code = <KEY_L>;
10 gpios = <&gpiog 3 GPIO_ACTIVE_LOW>;
11 };
12
13 key1 {
14 label = "GPIO Key S";
15 linux,code = <KEY_S>;
16 gpios = <&gpioh 7 GPIO_ACTIVE_LOW>;
17 };
18
19 wkup {
20 label = "GPIO Key Enter";
21 linux,code = <KEY_ENTER>;
22 gpios = <&gpioa 0 GPIO_ACTIVE_HIGH>;
23 gpio-key,wakeup;
24 };
25 };
第5行,autorepeat表示按键支持连按。
第7-11行,STM32MP1开发板KEY0按键信息,名字设置为“GPIO Key L”,这里将开发板上的KEY0按键设置为“EKY_L”这个按键,也就是‘L’键,效果和键盘上的‘L’键一样。
第13-17行,KEY1按键设置,模拟键盘上的‘S’键。
第19-24行,WKUP按键,模拟键盘上的‘回车’键,注意,WKUP按键连接的PA0引脚,查看原理图可以知道,WKUP按下去以后是高电平,因此这里设置高电平有效。
最后一定要检查一下设备树看看这些引脚有没有被用到其他外设上,如果有的话要删除掉相关代码!
重新编译设备树,然后用新编译出来的stm32mp157d-atk.dtb启动Linux系统,系统启动以后查看/dev/input目录,看看都有哪些文件,结果如下图所示:
从上图可以看出存在event0这个文件,这个文件就是KEY对应的设备文件,使用hexdump命令来查看/dev/input/event0文件,输入如下命令:
hexdump /dev/input/event0 |
然后按下STM32MP1开发板上的按键,终端输出如下图所示内容:
如果按下KEY按键以后会在终端上输出上图所示的信息那么就表示Linux内核的按键驱动工作正常。
如果按键没有反应,可以检查一下如下3方面:
gpio-keys gpio-keys: failed to get gpio: -16 |
input子系统可以驱动输入的外设,使用起来就是platform驱动加上input自己的驱动内容。
编写驱动前,需要先在stm32mp15-pinctrl.dtsi文件中创建对应的pinctrl子节点,并在stm32mp157d-atk.dts设备树中添加对应节点。
驱动程序中,初始化就是gpio的初始化写法,没什么花头,最多就是跟本篇笔记中一样加一个中断的初始化。
然后是定时器服务函数,这里在gpio_get_value之后获取状态,需要input_report_key和input_sync上报按键事件,并enable_irq开启中断。
probe函数就是调用GPIO的初始化,然后timer_setup初始化定时器;关键就是input_allocate_device然后__set_bit设置事件,最后input_register_devize注册输入设备。
remove函数中,区别就是加上input_unregister_device释放input设备。
最后module_platform_driver加上接好的platform_driver结构体的驱动。
如果是Linux内核相关,则先打开图形化界面然后去使能;比如本次的按键输入,使能之后,在stm32mp157d-atk.dts这个设备树文件中,首先需要添加头文件"dt-bindings/input/input.h",然后添加对应的GPIO节点,之后就可以使用了。