• 正点原子嵌入式linux驱动开发——Linux INPUT子系统


    按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux内核为此专门做了一个叫做input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了input框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input核心层负责处理这些事件。本章就来学习一下Linux内核中的input子系统。

    input子系统

    input子系统简介

    input子系统就是管理输入的子系统,和pinctrl、gpio子系统一样,都是Linux内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,只需要按照要求上报这些输入事件即可。为此input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框
    架如下图所示:
    input子系统结构图
    上图中左边就是最底层的具体设备,比如按键、USB键盘 /鼠标等,中间部分属于Linux内核空间,分为驱动层、核心层和事件层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出input子系统用到了前面讲解的驱动分层模型,编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:

    • 驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
    • 核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行
      处理。
    • 事件层:主要和用户空间进行交互。

    input驱动编写流程

    input核心层会向Linux内核注册一个字符设备,找到drivers/input/input.c这个文件,input.c就是input输入子系统的核心层,此文件里面有如下所示代码:
    input核心层创建字符设备
    第2498行,注册一个input类,这样系统启动以后就会在/sys/class目录下有一个input子
    目录,如下图所示:
    input类
    第2508-2509行,注册一个字符设备,主设备号为INPUT_MAJOR,INPUT_MAJOR定义
    在include/uapi/linux/major.h文件中,定义如下:

    #define INPUT_MAJOR 13
    
    • 1

    因此,input子系统的所有设备主设备号都为13,在使用input子系统处理输入设备
    的时候就不需要去注册字符设备了,只需要向系统注册一个input_device即可

    注册input_dev

    在使用input子系统的时候只需要注册一个input设备即可,input_dev结构体表示input设备,此结构体定义在include/linux/input.h文件中,定义如下 (有省略):
    input_dev结构体
    第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)
    
    • 1

    函数参数和返回值含义如下:

    • 参数 :无。
    • 返回值:申请到的input_dev。

    如果要注销input设备的话需要使用input_free_device函数来释放掉前面申请到的input_dev。input_free_device函数原型如下:

    void input_free_device(struct input_dev *dev) 
    
    • 1

    函数参数和返回值含义如下:

    • dev:需要释放的input_dev。
    • 返回值:无。

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

    int input_register_device(struct input_dev *dev) 
    
    • 1

    函数参数和返回值含义如下:

    • dev:要注册的input_dev 。
    • 返回值:0,input_dev注册成功;负值,input_dev注册失败。

    同样的,注销input驱动的时候也需要使用input_unregister_device函数来注销掉前面注册的input_dev,input_unregister_device函数原型如下:

    void input_unregister_device(struct input_dev *dev) 
    
    • 1

    函数参数和返回值含义如下:

    • dev:要注销的input_dev 。
    • 返回值:无。

    综上所述,input_dev注册过程如下:

    1. 使用input_allocate_device函数申请一个input_dev。
    2. 初始化input_dev的事件类型以及事件值。
    3. 使用input_register_device函数向Linux系统注册前面初始化好的input_dev。
    4. 卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    第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)
    
    • 1
    • 2
    • 3
    • 4

    函数参数和返回值含义如下:

    • dev:需要上报的input_dev。
    • type: 上报的事件类型,比如EV_KEY。
    • code:事件码,也就是注册的按键值,比如KEY_0、KEY_1等等。
    • value:事件值,比如1表示按键按下,0表示按键松开。
    • 返回值:无。

    input_event函数可以上报所有的事件类型和事件值,Linux内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了input_event函数。比如上报按键所使用的input_report_key函数,此函数内容如下:
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    void input_sync(struct input_dev *dev) 
    
    • 1

    函数参数和返回值含义如下:

    • dev:需要上报同步事件的input_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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第6行,获取按键值,判断按键是否按下。

    第9-10行,如果按键值为0那么表示按键被按下了,如果按键按下的话就要使用input_report_key函数向Linux系统上报按键值,比如向Linux系统通知KEY_0这个按键按下了。

    第12-13行,如果按键值为1的话就表示按键没有按下,是松开的。向Linux系统通知KEY_0这个按键没有按下或松开了。

    input_event结构体

    Linux内核使用input_event这个结构体来表示 所有的输入事件,input_envent结构体定义在include/uapi/linux/input.h文件中,结构体内容如下:
    input_event结构体
    依次来看一下input_event结构体中的各个成员变量:

    • time:时间,也就是此事件发生的时间,为timeval结构体类型,timeval结构体定义如下:
      timeval结构体
      从示例代码38.1.3.2可以看出,tv_sec和tv_usec这两个成员变量都为long类型,也就是32位,这个一定要记住,后面分析event事件上报数据的时候要用到。
    • type:事件类型,比如EV_KEY,表示此次事件为按键事件,此成员变量为16位。
    • code:事件码,比如在EV_KEY事件中code就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为16位。
    • value:值,比如EV_KEY事件中value就是按键值,表示按键有没有被按下,如果为1说明按键按下,如果为0的话说明按键没有被按下或者按键松开了

    input_envent这个结构体非常重要,因为所有的输入设备最终都是按照input_event结构体呈现给用户的,用户应用程序可以通过input_event来获取到具体的输入事件或相关的值,比如按键值等。关于input子系统就讲解到这里,接下来就以开发板上的KEY0按键为例,讲解一下如何编写input驱动

    硬件原理图分析

    就是之前的按键原理图。

    实验程序编写

    修改设备树文件

    创建按键引脚的pinctrl子节点

    首先在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 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里将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 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    按键input驱动程序编写

    按键结构体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)即可。

    编写测试APP

    传入的argc就2个;open(argv[1], O_RDWR)之后,在for死循环中read读取设备,通过传入的值判断具体的按键情况,最后写一个close就可以了。

    运行测试

    编译驱动程序

    Makefile还是老样子,改一下obj-m为keyinput.o然后“make”就可以了。

    编译测试APP

    可以通过如下命令编译:

    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目录
    可以从上图看出,当前在/dev目录下不存在input文件夹,因为此时系统中并没有加载过input输入类设备,所以这个文件夹不存在。接下来输入如下命令加载keyinput.ko这个驱动模块:

    depmod //第一次加载驱动的时候需要运行此命令
    modprobe keyinput.ko //加载设备模块

    当驱动模块加载成功以后再来看一下/dev/input目录下有哪些文件,结果如下图所示:
    加载驱动后/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类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:
    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中的原始事件值含义如下:

    • 第1行,按键(KEY_0)按下事件。
    • 第2行,EV_SYN同步事件,因为每次上报按键事件以后都要同步的上报一个EV_SYN事件。
    • 第3行,按键(KEY_0)松开事件。
    • 第4行,EV_SYN同步事件,和第2行一样。

    Linux自带按键驱动程序使用

    自带按键驱动程序源码解析

    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内核中,如下图所示:
    内核自带KEY驱动使能选项
    选中以后就会在.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文件中找到如下所示内容:
    gpio_keys文件代码段
    从示例代码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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    第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函数,此函数内容如下:
    gpio_keys_setup_key函数
    第614行,调用input_set_capability函数设置EV_KEY事件以及KEY的按键类型,也就是KEY作为哪个按键?会在设备树里面设置指定的KEY作为哪个按键。

    一切都准备就绪以后剩下的就是等待按键按下,然后向Linux内核上报事件,事件上报是在gpio_keys_irq_isr函数中完成的,此函数内容如下:
    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这个文件在设备树中添加指定的设备节点
    即可,节点要求如下:

    1. 节点名字为“gpio-keys”。
    2. gpio-keys节点的compatible属性值一定要设置为“gpio-keys”。
    3. 所有的KEY都是gpio-keys的子节点,每个子节点可以用如下属性描述自己:
      • gpios:KEY所连接的GPIO信息。
      • interrupts:KEY所使用GPIO中断信息,不是必须的,可以不写。
      • label:KEY名字。
      • linux,code:KEY要模拟的按键,也就是示例代码38.1.2.4中的这些按键。
    4. 如果按键要支持连按的话要加入autorepeat。

    这里将开发板上的三个按键都用起来,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 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    第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目录,看看都有哪些文件,结果如下图所示:
    /dev/input目录文件
    从上图可以看出存在event0这个文件,这个文件就是KEY对应的设备文件,使用hexdump命令来查看/dev/input/event0文件,输入如下命令:

    hexdump /dev/input/event0

    然后按下STM32MP1开发板上的按键,终端输出如下图所示内容:
    按键信息
    如果按下KEY按键以后会在终端上输出上图所示的信息那么就表示Linux内核的按键驱动工作正常。

    如果按键没有反应,可以检查一下如下3方面:

    1. 是否使能Linux内核KEY驱动。
    2. 设备树中gpio-keys节点是否创建成功。
    3. 在设备树中是否有其他外设也使用了KEY按键对应的GPIO,但是并没有删除掉这些外设信息。检查Linux启动log信息,看看是否有类似下面这条信息,如果有表明GPIO申请失败,有其他外设使用此GPIO:
    gpio-keys gpio-keys: failed to get gpio: -16

    总结

    input子系统可以驱动输入的外设,使用起来就是platform驱动加上input自己的驱动内容。

    自行添加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内核自带

    如果是Linux内核相关,则先打开图形化界面然后去使能;比如本次的按键输入,使能之后,在stm32mp157d-atk.dts这个设备树文件中,首先需要添加头文件"dt-bindings/input/input.h",然后添加对应的GPIO节点,之后就可以使用了。

  • 相关阅读:
    Kubernetes HPA:基于 kafka_consumergroup_lag 指标实现 Consumer Pod 水平弹性伸缩
    《Java核心知识点》+《Java面试宝典》+《1000道互联网面试专题》+《350道Java面试》,总共1045页
    Linux 环境变量配置的 6 种方法
    docker--redis容器部署及与SpringBoot整合-I
    VSCode Linux的C++代码格式化配置
    卷积神经网络(包含代码与相应图解)
    Diffusion Models在目标检测领域的应用
    10-动画animation
    写给Python社群的第7课:用 Python 模块了不起呀?就是了不起
    Vue之html中特殊符号的展示
  • 原文地址:https://blog.csdn.net/xhj12138/article/details/134005084