6. Linux内核输入子系统

在Linux内核驱动开发中,输入子系统是一套专门用于管理各类输入设备的标准化框架, 其核心作用是统一处理键盘、鼠标、按键、触摸屏、红外遥控等各类输入设备的事件, 为驱动开发者提供标准化的接口,简化输入设备驱动的开发流程,同时为应用层提供统一的事件读取接口,实现输入事件的规范化传输。

不同于异步通知、阻塞/非阻塞IO等通用IO机制,输入子系统是针对输入设备的专用框架,它屏蔽了不同输入设备的硬件差异, 将各类输入设备的操作抽象为统一的事件(如按键按下/松开、坐标移动等),使得驱动开发者无需关注应用层的事件处理逻辑, 只需按照子系统规范实现硬件相关的事件采集,即可完成输入设备的驱动开发。

6.1. 输入子系统概念

输入子系统(Input Subsystem)是Linux内核中一套标准化的框架,用于统一管理所有输入设备,抽象输入设备的共性, 提供标准化的驱动开发接口和应用层访问接口。整体工作流程可概括为:硬件触发输入信号->驱动层采集并转换为标准事件->核心层接收并转发事件->事件处理层将事件传递给应用层->应用层处理事件。 具体工作流程如下:

../_images/input_sub_system01.png
  • Drivers(驱动层):最底层,直接与硬件设备交互,负责初始化输入硬件、采集硬件输入信号(如按键按下/松开、鼠标移动等),并将采集到的硬件信号转换为输入子系统定义的标准事件,提交给核心层。驱动层的核心任务是“硬件适配”,无需关注事件的后续处理与转发。

  • Input Core(输入子系统核心层):中间层,是输入子系统的核心枢纽,负责管理所有输入驱动和事件处理程序,提供标准化的接口供驱动层和事件处理层调用。核心层接收驱动层提交的事件,进行统一管理、过滤和转发,同时负责维护输入设备的信息,协调驱动层与事件处理层的交互。

  • handlers(事件处理层):最上层,负责将核心层转发的标准事件传递给应用层,提供应用层可访问的设备节点(如/dev/input/eventX),应用层通过读取该设备节点,即可获取输入事件。事件处理层屏蔽了底层硬件差异,为应用层提供了统一的事件读取接口。

6.2. 输入子系统结构体与按键码

输入子系统的核心功能通过一系列内核结构体实现,这些结构体由内核定义, 驱动开发者只需声明和使用这些结构体,无需手动定义。

6.2.1. 输入设备结构体(input_dev)

该结构体是输入驱动层的核心结构体,用于描述一个输入设备的基本信息,其具体定义位于内核源码/include/linux/input.h中, 核心源码片段如下:

struct input_dev结构体(内核源码/include/linux/input.h)
1
2
3
4
5
6
7
8
9
struct input_dev {
    const char *name;                            // 输入设备名称
    const char *phys;                            // 设备物理地址
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];  // 设备支持的事件类型位图
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];// 设备支持的按键码位图
    struct input_id id;                          // 设备标识信息
    struct list_head node;                       // 用于将设备加入输入子系统链表
    // 省略其他辅助成员
};

每个输入设备都需要创建一个该结构体,并将其注册到输入子系统核心层,核心层通过该结构体管理输入设备。 其中 evbit 和 keybit 是核心成员,分别通过位图标识设备支持的事件类型(如EV_KEY按键事件)和具体按键码。

6.2.2. 输入事件结构体(input_event)

该结构体是输入子系统中事件的标准化表示,用于存储输入事件的相关信息,其具体定义位于内核源码/include/uapi/linux/input.h中, 源码定义如下:

struct input_event结构体(内核源码/include/uapi/linux/input.h)
1
2
3
4
5
6
struct input_event {
    struct timeval time;  // 事件发生的时间戳
    __u16 type;           // 事件类型
    __u16 code;           // 事件代码
    __s32 value;          // 事件值
};

驱动层采集到硬件输入信号后,会将其转换为该结构体表示的标准事件,提交给核心层; 应用层读取输入事件时,也是通过读取该结构体的数据,获取输入设备的操作信息。

6.2.3. 输入事件处理结构体(input_handler)

该结构体用于描述事件处理程序的基本信息,其具体定义位于内核源码/include/linux/input.h中,核心源码片段如下:

struct input_dev结构体(内核源码/include/linux/input.h)
1
2
3
4
5
6
7
8
struct input_handler {
    const char *name;                        // 事件处理程序名称
    const struct input_device_id *id_table;  // 匹配规则
    void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);             // 事件处理函数
    int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id); // 连接函数
    void (*disconnect)(struct input_handle *handle); // 断开连接函数
    struct list_head node;                   // 用于将处理程序加入输入子系统链表
};

事件处理层通过该结构体向核心层注册,核心层将驱动层提交的事件,转发给匹配的事件处理程序, 由事件处理程序将事件传递给应用层。最常用的evdev事件处理程序,其event函数会将事件写入设备节点,供应用层读取。

6.2.4. 按键码定义

输入子系统中,各类输入事件都通过标准化的按键码(Key Code)来标识,按键码由内核统一定义,确保不同驱动和应用层之间的兼容性。 按键码的核心定义位于内核头文件内核源码/include/dt-bindings/input/linux-event-codes.h中,采用宏定义方式实现, 根据输入设备的类型,分为不同的类别,常用的按键码分类及源码定义示例如下:

按键码定义(内核源码/include/dt-bindings/input/linux-event-codes.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 键盘数字键
#define KEY_0 11
#define KEY_1 2
#define KEY_2 3
...
// 键盘字母键
#define KEY_A 30
#define KEY_B 48
...
// 控制键
#define KEY_ENTER 28
#define KEY_LEFTCTRL 29
...
// 鼠标按键
#define BTN_LEFT            0x110
#define BTN_RIGHT           0x111

驱动层和应用层可直接使用这些宏定义,无需手动定义按键码值,确保按键码的统一性和兼容性。 例如驱动层设置GPIO按键对应KEY_1,直接引用KEY_1宏即可,无需硬编码数值2。

6.3. 输入子系统常用函数

Linux内核为输入子系统提供了一系列标准化的函数,涵盖输入设备的初始化、注册、注销,事件的提交, 以及驱动和事件处理程序的注册、注销等核心操作。

6.3.1. 输入设备分配

6.3.1.1. devm_input_allocate_device函数

基于devm(设备资源管理)机制,为输入设备结构体(input_dev)分配内存并初始化默认值, 该函数分配的input_dev结构体,会与指定的设备绑定,当设备被卸载或内核释放资源时, 会自动释放该input_dev结构体占用的内存,无需开发者手动调用释放函数,可有效避免内存泄漏。

函数原型:

1
struct input_dev *devm_input_allocate_device(struct device *dev);

参数说明:

  • dev:指向与该输入设备绑定的设备结构体指针。

返回值:成功返回分配好的input_dev结构体指针;失败返回NULL,通常是由于内存分配失败或参数无效导致。

6.3.2. 输入设备注册

6.3.2.1. input_register_device函数

input_register_device函数用于:将分配并配置好的输入设备结构体(input_dev)注册到输入子系统核心层, 核心层会对设备进行管理,并将其与对应的事件处理程序匹配。 注册成功后,输入设备即可正常工作,驱动层可向核心层提交输入事件。

函数原型:

1
int input_register_device(struct input_dev *dev);

参数说明:

  • dev:指向已分配并配置完成的input_dev结构体指针,是需要注册到核心层的输入设备。

返回值:成功返回0;失败返回负整数的错误码,不同错误码对应不同的失败原因,如设备注册重复、参数无效等。

6.3.3. 输入设备注销

6.3.3.1. input_unregister_device函数

input_unregister_device函数用于将已注册的输入设备从核心层注销,释放输入设备结构体占用的内存,终止输入设备的工作。 该函数会自动清理输入设备的相关资源,避免内存泄漏。

函数原型:

1
void input_unregister_device(struct input_dev *dev);

参数说明:

  • dev:指向已注册的input_dev结构体指针,是需要从核心层注销的输入设备。

6.3.4. 输入设备事件类型设置

6.3.4.1. set_bit函数

set_bit函数 根据传入参数不同,可用于将已注册的输入设备从核心层注销,释放输入设备结构体占用的内存,终止输入设备的工作。 该函数会自动清理输入设备的相关资源,避免内存泄漏。

函数原型:

1
static inline void set_bit(unsigned int bit, unsigned long *addr);

参数说明:

  • bit:需要设置的事件类型宏(如EV_KEY、EV_REL),对应事件类型的标识值;

  • addr:指向input_dev结构体中evbit成员的指针,用于存储设备支持的事件类型位图。

6.3.5. 输入设备按键码设置

6.3.5.1. set_bit函数

set_bit函数 根据传入参数不同,可用于设置输入设备支持的事件类型,如按键事件、坐标事件、相对位移事件等。 输入子系统支持多种事件类型,驱动层需通过该函数明确告知核心层,当前输入设备支持哪些事件类型。

函数原型:

1
static inline void set_bit(unsigned int bit, unsigned long *addr);

参数说明:

  • bit:需要设置的按键码宏(如KEY_1、BTN_LEFT),对应按键的标准化标识值;

  • addr:指向input_dev结构体中keybit成员的指针,用于存储设备支持的按键码位图。

6.3.6. 输入事件提交

6.3.6.1. input_event函数

input_event函数用于将驱动层采集到的硬件输入信号,转换为标准的输入事件(input_event结构体),并提交给输入子系统核心层。 核心层接收该事件后,会将其转发给对应的事件处理程序,最终传递给应用层。

函数原型:

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

参数说明:

  • dev:指向当前输入设备的input_dev结构体指针;

  • type:事件类型(如EV_KEY、EV_REL);

  • code:事件代码(对应按键码或事件细分类型,如KEY_1、REL_X);

  • value:事件值(按键事件中1为按下、0为松开,位移事件中为具体位移量)。

6.3.7. 输入事件同步

6.3.7.1. input_sync函数

input_sync函数用于同步输入事件,告知核心层一组相关的输入事件已提交完成,确保应用层能够完整接收一组事件(如触摸屏的坐标事件和点击事件)。 该函数通常在一组事件提交完成后调用,避免事件丢失或乱序。

函数原型:

1
void input_sync(struct input_dev *dev);

参数说明:

  • dev:指向当前输入设备的input_dev结构体指针,标识需要同步事件的输入设备。

6.4. Linux输入子系统实验

本实验在Linux中断子系统实验基础上进行修改, 实现“按键触发中断->驱动上报输入事件->应用层读取事件”的完整流程,无需自定义字符设备的read/write操作, 依托输入子系统框架完成按键事件的标准化上报与读取,理解输入子系统的核心工作机制、设备注册流程及事件上报规范。

本章的示例代码目录为: linux_driver/23_input_subsystem

6.4.1. 设备树插件详解

设备树插件完整代码如下:

设备树插件(位于linux_driver/23_input_subsystem/lubancat-led-overlay.dts)
 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
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/input/linux-event-codes.h>

/ {
    fragment@0 {
        target-path = "/";

        __overlay__ {
            led_test: led_test{
                compatible = "fire,led_test";
                led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
                button-gpios= <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
                interrupt-parent = <&gpio1>;
                interrupts = <RK_PB2 IRQ_TYPE_EDGE_BOTH>;
                input-name = "lubancat-key-input";
                key-code = <KEY_1>;
                pinctrl-names = "default";
                pinctrl-0 = <&led_button_pin>;
            };
        };
    };

    fragment@1 {
        target = <&pinctrl>;

        __overlay__ {
            led_button_test {
                led_button_pin: led_button_pin {
                    rockchip,pins =
                        <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
                        <1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
                };
            };
        };
    };

    fragment@2 {
        target = <&leds>;

        __overlay__ {
            status = "disabled";
        };
    };
};
  • 第7行:引入linux-event-codes.h,用于使用标准的按键事件编码;

  • 第20行:input-name:输入设备的名称,驱动会将该名称赋值给input_dev结构体,应用层可通过该名称识别设备;

  • 第21行:key-code:按键对应的事件编码,由linux-event-codes.h定义(如KEY_1=2),驱动会根据该编码上报按键事件,应用层通过编码识别按键类型.

6.4.2. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/23_input_subsystem/input_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 定义 LED 字符设备结构体 */
struct led_chrdev {
    /* 字符设备结构体 */
    struct cdev dev;
    /* 自旋锁 */
    spinlock_t spinlock;
    /* LED 状态 */
    int led_state;
    /* LED 的 GPIO 描述符 */
    struct gpio_desc *led_gpio;
    /* 按钮的 GPIO 描述符 */
    struct gpio_desc *button_gpio;
    /* 消抖定时器 */
    struct timer_list debounce_timer;
    /* 中断号 */
    int irq;
    /* 输入设备结构体指针 */
    struct input_dev *input_dev;
    /* 从设备树读取的按键码 */
    u32 key_code;
};

/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
  • 第18行:新增输入子系统的核心结构体,用于描述输入设备的属性;

  • 第20行:存储从设备树读取的按键编码,用于事件上报时指定按键类型。

驱动初始化

驱动初始化(位于linux_driver/23_input_subsystem/input_subsystem.c)
  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
static int pdrv_led_probe(struct platform_device *pdev)
{
    // 内存分配(省略重复代码)

    /* 第一步:提取平台设备提供的资源 */
    if (pdev->dev.of_node) {
        /* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
        led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
        if (IS_ERR(led_cdev->led_gpio)) {
            ret = PTR_ERR(led_cdev->led_gpio);
            printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
            return ret;
        }

        /* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
        led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
        if (IS_ERR(led_cdev->button_gpio)) {
            ret = PTR_ERR(led_cdev->button_gpio);
            printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
            return ret;
        }

        /* 从设备树读取输入设备名称 */
        ret = of_property_read_string(pdev->dev.of_node, "input-name", &input_name);
        if (ret < 0) {
            printk(KERN_ERR "Failed to get input-name from devicetree\n");
            return ret;
        }

        /* 从设备树读取按键码 */
        ret = of_property_read_u32(pdev->dev.of_node, "key-code", &led_cdev->key_code);
        if (ret < 0) {
            printk(KERN_ERR "Failed to get key-code from devicetree\n");
            return ret;
        }

    } else {
        printk("Platform device matching is not supported in this driver\n");
        return -ENOMEM;
    }

    //字符设备注册(省略重复代码)

    /* 初始化自旋锁 */
    spin_lock_init(&led_cdev->spinlock);

    /* 初始化消抖定时器 */
    timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);

    /* 默认关闭 LED */
    led_cdev->led_state = 0;

    /*  输入子系统初始化 */
    /* 1. 分配输入设备 */
    led_cdev->input_dev = devm_input_allocate_device(&pdev->dev);
    if (!led_cdev->input_dev) {
        printk(KERN_ERR "Failed to allocate input device\n");
        ret = -ENOMEM;
        goto device_err;
    }

    /* 2. 使用设备树的输入设备名称 */
    led_cdev->input_dev->name = input_name;

    /* 3. 设置支持的事件类型:EV_KEY(按键事件) */
    set_bit(EV_KEY, led_cdev->input_dev->evbit);

    /* 4. 使用设备树读取的按键码 */
    set_bit(led_cdev->key_code, led_cdev->input_dev->keybit);

    /* 5. 注册输入设备到内核 */
    ret = input_register_device(led_cdev->input_dev);
    if (ret < 0) {
        printk(KERN_ERR "Failed to register input device: %d\n", ret);
        goto device_err;
    }

    /* 获取按键GPIO对应的中断号 */
    led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
    if (led_cdev->irq < 0) {
        ret = led_cdev->irq;
        /* 打印获取中断号失败 */
        printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    /* 申请中断,双边沿触发,匹配设备树IRQ_TYPE_EDGE_BOTH */
    ret = devm_request_irq(&pdev->dev, led_cdev->irq, button_irq_handler,
                    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                    "button_irq", led_cdev);
    if (ret < 0) {
        /* 打印申请中断失败 */
        printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    return 0;

    // 错误处理(省略重复代码)
}

输入设备初始化步骤:

  • 第55行:分配输入设备:使用devm_input_allocate_device()带资源自动释放的接口,避免内存泄漏;

  • 第63行:设置设备名称:用于标识输入设备;

  • 第66行:设置支持的事件类型:通过set_bit()设置evbit,告知内核该设备支持的事件;

  • 第69行:设置支持的按键编码:通过set_bit()设置keybit,告知内核该设备支持的按键;

  • 第71行:注册输入设备:调用input_register_device()将设备注册到输入子系统,注册成功后,内核会自动创建/dev/input/eventX设备节点。

输入事件上报逻辑

输入事件上报(位于linux_driver/23_input_subsystem/input_subsystem.c)
 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
/* 消抖定时器回调函数:确认按键稳定后翻转LED + 上报输入事件 */
static void button_debounce_callback(struct timer_list *t)
{
    /* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
    struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);

    /* 用于保存中断状态信息 */
    unsigned long flags;
    /* 存储按键状态 */
    int btn_val;

    /* 获取自旋锁并保存中断状态 */
    spin_lock_irqsave(&led_cdev->spinlock, flags);

    /* 读取稳定后的按键电平 */
    btn_val = gpiod_get_value(led_cdev->button_gpio);

    /* 低电平表示按键稳定按下 */
    if(btn_val == 0){
        /* 翻转LED电平状态 */
        gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
        /* 上报按键按下事件 */
        input_event(led_cdev->input_dev, EV_KEY, led_cdev->key_code, 1);

        /* 同步事件,告知应用层事件完整 */
        input_sync(led_cdev->input_dev);
    } else {
        /* 上报按键松开事件 */
        input_event(led_cdev->input_dev, EV_KEY, led_cdev->key_code, 0);

        /* 同步事件,告知应用层事件完整 */
        input_sync(led_cdev->input_dev);
    }

    /* 释放自旋锁并恢复中断状态 */
    spin_unlock_irqrestore(&led_cdev->spinlock, flags);
}
  • 第23行和第29行:调用input_event,上报具体的输入事件,按下上报1,松开上报0

  • 第26行和第32行:调用input_sync同步事件,必须在每次事件上报完成后调用,告知应用层“本次事件已完整”,避免事件错乱。

6.4.3. 应用代码详解

应用层无需适配具体驱动,通过输入子系统提供的标准接口(/dev/input/eventX)读取按键事件,解析事件类型、编码和值, 即可获取按键状态,完整代码如下:

input_subsystem_app.c(位于linux_driver/23_input_subsystem/input_subsystem_app.c)
 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
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/input.h>

int main(int argc, char *argv[]) {
    int fd;
    int ret;

    // 输入事件结构体,用于存储读取到的输入事件
    struct input_event ev;

    // 检查参数,需传入输入设备event节点路径
    if (argc != 2) {
        printf("Usage: ./input_app /dev/input/eventX\n");
        return -1;
    }

    // 打开输入设备节点
    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open input device failed");
        return -1;
    }

    printf("Start reading input event...\n");

    // 循环读取输入事件
    while (1) {
        // 读取输入事件,struct input_event 是输入子系统的标准事件格式
        ret = read(fd, &ev, sizeof(struct input_event));
        if (ret < 0) {
            perror("read input event failed");
            close(fd);
            return -1;
        }

        // 解析事件:仅处理按键事件 EV_KEY
        if (ev.type == EV_KEY) {
            // 打印事件信息:时间戳、事件类型、按键编码、事件值
            printf("Event: type=%d, code=%d value=%d\n",
                ev.type, ev.code, ev.value);

            // 根据事件值判断按键状态
            if (ev.value == 1) {
                printf("=== 按键按下 ===\n");
            } else if (ev.value == 0) {
                printf("=== 按键松开 ===\n");
            }
        }

    }

    close(fd);

    return 0;
}
  • 第15行:输入子系统的标准事件结构体,包含事件类型(type)、事件编码(code)、事件值(value)和时间戳,内核会将按键事件封装成该结构体上报;

  • 第43行:仅处理EV_KEY类型事件,根据value值判断按键按下(1)或松开(0),code值对应KEY_1(编码为2)。

6.4.4. 编译设备树和驱动

此部分和Linux中断子系统实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb、驱动模块input_subsystem.ko和应用程序input_subsystem_app。

6.4.5. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

6.4.5.1. 实验操作

设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。

使用以下命令加载驱动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#加载驱动
sudo insmod input_subsystem.ko

#信息输出如下
[ 5846.950693] led platform driver init
[ 5846.951231] led platform driver probe
[ 5846.951367] major=236, minor=0
[ 5846.952030] input: lubancat-key-input as /devices/platform/led_test/input/input3

#查看输入设备对应的事件
ls /dev/input/by-path/ -l

#信息输出如下
total 0
lrwxrwxrwx 1 root root 9 Jun 18  2024 platform-fdd40000.i2c-platform-rk805-pwrkey-event -> ../event1
lrwxrwxrwx 1 root root 9 Jun 18  2024 platform-fdd70030.pwm-event -> ../event0
lrwxrwxrwx 1 root root 9 Apr  3 16:26 platform-led_test-event -> ../event3
lrwxrwxrwx 1 root root 9 Jun 18  2024 platform-rk-headset-event -> ../event2

此处event3就是input_subsystem驱动注册的事件,使用evtest监控系统事件上报:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#执行命令
sudo evtest

#信息输出如下,event3对应的设备名字是lubancat-key-input,就是设备树配置的输入设备名字
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:      fdd70030.pwm
/dev/input/event1:      rk805 pwrkey
/dev/input/event2:      rk-headset
/dev/input/event3:      lubancat-key-input
Select the device event number [0-3]:

#键盘输入 3 选择event3,信息输入如下
Select the device event number [0-3]: 3
Input driver version is 1.0.1
Input device ID: bus 0x0 vendor 0x0 product 0x0 version 0x0
Input device name: "lubancat-key-input"
Supported events:
Event type 0 (EV_SYN)       //表示同步事件
Event type 1 (EV_KEY)       //表示按键事件
    Event code 2 (KEY_1)    //按键编码为2对应KEY_1
Properties:
Testing ... (interrupt to exit)

可以从输出信息看到,此处event3对应的设备名字是lubancat-key-input,就是设备树配置的输入设备名字, 事件编号各系统各板卡可能不一样,需根据实际情况而定。

使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:

1
2
3
4
5
6
7
8
Event: time 1775206083.362980, type 1 (EV_KEY), code 2 (KEY_1), value 1
Event: time 1775206083.362980, -------------- SYN_REPORT ------------
Event: time 1775206083.689373, type 1 (EV_KEY), code 2 (KEY_1), value 0
Event: time 1775206083.689373, -------------- SYN_REPORT ------------
Event: time 1775206084.992803, type 1 (EV_KEY), code 2 (KEY_1), value 1
Event: time 1775206084.992803, -------------- SYN_REPORT ------------
Event: time 1775206085.369374, type 1 (EV_KEY), code 2 (KEY_1), value 0
Event: time 1775206085.369374, -------------- SYN_REPORT ------------

按键按下时会上报value 1,松开会上报value 0,验证输入子系统驱动功能正常。

使用应用程序进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#运行应用程序,event编号根据实际而定
sudo ./input_subsystem_app /dev/input/event3

#信息输出如下
Start reading input event...
Event: type=1, code=2 value=1
=== 按键按下 ===
Event: type=1, code=2 value=0
=== 按键松开 ===
Event: type=1, code=2 value=1
=== 按键按下 ===
Event: type=1, code=2 value=0
=== 按键松开 ===

结合evtest的打印信息,可以确认type=1就对应Event type 1 (EV_KEY) 表示按键事件,code=2就对应Event code 2 (KEY_1),按键按下value值为1,按键松开value值为0。

6.4.5.2. 系统对上报事件处理

如果使用屏幕终端或者桌面打开一个终端会发现,每次短接杜邦线模拟按键按下终端上都会有“1”打印, 该效果与外接USB键盘按下数字“1”键完全一致, 说明输入子系统的硬件无关性——上层应用无需区分事件来自GPIO按键还是USB键盘。

如果我们将设备树配置的key-code = <KEY_1>改为key-code = <KEY_POWER>, 那么我们注册的GPIO按键就变成了一个货真价实的系统电源键,长按按键系统将会关机!

如果希望我们注册的按键不受系统默认处理,可以注册一个空事件,可以观察到内核源码/include/dt-bindings/input/linux-event-codes.h中 编号249到255是空事件,如果使用这些空事件系统将不作处理,如设备树配置为key-code = <250>。

6.4.6. 实验注意事项

  1. 输入事件上报规范:每次按键事件(按下/松开)都需调用input_event()上报,且上报完成后必须调用input_sync()同步,否则应用层无法正确解析事件;

  2. 输入设备节点识别:若应用层无法读取事件,需通过 ls /dev/input/by-path/ -l 确认输入设备名称对应的event节点,避免使用错事件;

  3. 事件编码正确:key-code需使用Linux内核定义的标准编码(如KEY_1),需包含linux-event-codes.h头文件,否则会出现编码错误。