Linux驱动开发(速记版)--输入子系统

时间:2024-10-08 12:08:04

第114章 初识 input 子系统

114.1 什么是输入子系统?

        Linux 中的 input 子系统是一个专为处理输入设备(如键盘、鼠标、触摸屏等)设计的框架,旨在规范和简化驱动开发,提高通用性和兼容性。

输入子系统

114.2 输入设备和节点的关系

        在输入子系统中,输入设备与设备节点之间有一定的对应关系。

        以下是判断设备节点与输入设备的方法

1. 设备名:

        输入子系统设备节点可以分为通用设备名专用设备名

        专用设备名通常可以从设备名中直接识别出设备类型,

        例如"keyboard"(键盘)或"mouse"(鼠标)。

        而通用设备名则不能直接确定设备类型。

        如下图所示,event0-event4 属于通用设备名,而 mouse0 和mouse2 属于专用设备名。

通用设备名和专用设备名

2. 试探性方法:

        可以使用 "cat"命令打开设备节点文件,然后对物理设备进行操作,观察终端是否有输出

        例如,对于键盘设备,你可以运行"cat /dev/input/eventX",其中 "/dev/input/eventX"是设备节点的路径,然后按下键盘按键,观察终端是否输出相应的字符。

        通过这种试探性的方法,你可以判断设备节点与具体设备之间的对应关系。例如使用以下命令来测试鼠标,如下所示:

        sudo cat /dev/input/mouse0
鼠标设备节点文件打开效果

        注:鼠标节点也可能是 mouse1 或者 mouse2。

3. 查看输入设备信息:

        可以使用以下命令查看"/proc/bus/input/devices"文件:

cat /proc/bus/input/devices
查看输入设备信息

        该文件记录了当前系统的所有输入设备的信息在该文件中,你可以找到与设备节点相关的信息,例如设备名称、供应商 ID、产品 ID 等。

        通过对比设备节点的路径和设备信息中的对应字段,你可以确定设备节点与特定输入设备之间的关系,例如可以通过上述打印信息查看到,键盘对应的设备节点为/dev/input/event1,如

设备节点与特定输入设备之间的关系

设备信息解释

总线、供应商、产品、版本:

        Bus=0011:设备总线类型。

        Vendor=0001:供应商ID。

        Product=0001:产品ID。

        Version=ab41:固件版本。

设备名称:

        Name="AT Translated Set 2 keyboard":设备名称为AT标准键盘。

物理位置:

        Phys=isa0060/serio0/input0:设备在系统中的物理路径。

sysfs路径:

        Sysfs=/devices/platform/i8042/serio0/input/input1:设备在sysfs中的位置。

唯一标识符:

        Uniq=:设备唯一标识符(此例为空)。

处理程序:

        Handlers=sysrq kbd event1 leds:处理设备事件的程序,如event1对应/dev/input/event1。

设备属性:

        PROP=0:设备属性(此例无特定属性)。

支持的事件类型:

        EV=120013:支持同步、按键、杂项和LED等事件。

支持的按键:

        KEY=...:以16进制表示按键状态,每位对应一个按键。

支持的杂项事件:

        MSC=10:支持扫描码事件。

支持的LED灯:

        LED=7:支持3个LED灯,状态用7位二进制表示。

        上面讲解的 cat 命令是一种试探性的方法,

        除此之外还可以使用 hexdump 命令对输出信息进行二进制内容查看,

        hexdump 是一个用于查看二进制文件内容的工具,它能以十六进制和ASCII码的形式显示文件内容。

sudo hexdump /dev/input/event1  //查看键盘输出信息的二进制内容
/*常用选项
-C:以规范的十六进制和ASCII码格式显示内容,便于查看二进制文件。
-b:以单字节八进制形式显示内容。
-c:以单字节字符形式显示内容,非打印字符显示为.。
-d:以双字节十进制形式显示内容。
-o:以双字节八进制形式显示内容。
-x:以双字节十六进制形式显示内容。
-s offset:从指定偏移量开始显示内容,偏移量可用十进制或十六进制表示。*/

114.3.确定开发板输出设备节点

        RK3568 开发板上的的输入设备如下所示:

        总共有 4 个输入设备,分别是触摸屏红外接收器adc 按键以及耳机接口,然后启动开发板,进入系统之后如下图所示:

        首先通过以下命令查看总共有多少个输入设备,如

ls /dev/input/

        可以看到总共有 event0-event4 五个通用设备名,

        上面我们总共找出来了 4 个输入设备,那这里为什么会有五个节点呢,所以我们就需要使用下面的命令查看详细的输入设备信息了,

 cat /proc/bus/input/devices

        从上到下依次是红外接收器、电源 power 按键、触摸屏、adc 按键和耳机,

        为了更简单的进行演示,这里选取触摸屏和 adc 按键来进行测试,

        从上面的信息可以得到触摸屏和 adc 按键分别对应 event2 和 event3,所以可以使用以下命令对两个输入设备的输出信息进行二进制内容查看,

第115章 输入子系统框架

        Input 子系统可以分为事件处理层设备驱动层核心层三层。

输入子系统的事件处理层、核心层、设备驱动层

输入子系统由三层构成:

事件处理层

        位于最上层,处理输入事件并传递给上层应用。

        创建设备节点,使应用能通过节点与设备通信。

        接收核心层事件,按类型和属性处理。

核心层

        位于中间,作为事件处理层和设备驱动层之间的匹配器。

        功能包括事件匹配、设备管理和控制、事件处理和分发,提供抽象化接口和事件处理机制。

        确保事件正确传递给处理程序,维护设备状态,提供统一接口给上层。

设备驱动层

        位于最底层,与硬件设备通信。

        抽象硬件操作和功能为统一接口。

        包括硬件初始化、中断处理、数据传输等,确保设备正常工作。

        开发者需编写此层代码以适应特定硬件。

第116章 输入子系统数据结构

        输入子系统为上层应用提供了统一的事件处理机制,

        涉及几个关键结构体:input_handlerinput_devinput_handle

        input_handler 结构体定义事件处理程序的行为,包含处理事件的函数指针匹配函数设备名称等。

/*定义事件处理程序的行为*/
struct input_handler {  
    void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);  
    void (*events)(struct input_handle *handle, const struct input_value *vals, unsigned int count);  
    bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);  
    bool (*match)(struct input_handler *handler, struct input_dev *dev);  
    int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);  
    void (*disconnect)(struct input_handle *handle);  
    const char *name;  
    const struct input_device_id *id_table;  
};

        input_register_handler() 函数注册事件处理程序,将其添加到处理程序链表中,并为每个已存在的输入设备附加该处理程序

int input_register_handler(struct input_handler *handler) {  
    // 获取互斥锁,初始化链表头,添加处理程序到全局链表  
    // 遍历输入设备链表,调用 input_attach_handler  
    // 释放互斥锁  
    return 0;  
}

        input_dev 结构体表示输入设备,包含设备名称物理位置支持的事件类型等。

struct input_dev {  
    const char *name;		// 设备名  
    const char *phys;		// 物理路径,如 /dev/input/eventX  
    const char *uniq;		// 唯一标识符  
    struct input_id id;		// 输入设备 ID,包括供应商、产品和版本信息  
  
    unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_MAX)]; // 设备属性位图  
    unsigned long evbit[BITS_TO_LONGS(EV_MAX)];	    // 支持的事件类型位图  
    unsigned long keybit[BITS_TO_LONGS(KEY_MAX)];	    // 支持的按键位图  
    unsigned long relbit[BITS_TO_LONGS(REL_MAX)];	    // 支持的相对坐标位图  
    unsigned long absbit[BITS_TO_LONGS(ABS_MAX)];	    // 支持的绝对坐标位图  
    unsigned long mscbit[BITS_TO_LONGS(MSC_MAX)];	    // 支持的杂项事件位图  
    unsigned long ledbit[BITS_TO_LONGS(LED_MAX)];	    // 支持的 LED 位图  
    unsigned long sndbit[BITS_TO_LONGS(SND_MAX)];	    // 支持的声音位图  
    unsigned long ffbit[BITS_TO_LONGS(FF_MAX)];	    // 支持的力反馈位图  
    unsigned long swbit[BITS_TO_LONGS(SW_MAX)];	    // 支持的开关位图  
  
    unsigned int hint_events_per_packet; // 每包事件提示  
  
    unsigned int keycodemax;	    // 支持的最大按键码  
    unsigned int keycodesize;	    // 按键码大小(字节)  
    void *keycode;		    // 按键码表(动态分配)  
  
    struct ff_device *ff;		    // 力反馈设备信息  
    unsigned int repeat_key;	    // 重复按键  
    struct timer_list timer;	    // 用于自动重复按键的定时器  
  
    int state;			    // 设备状态  
  
    struct input_handler *grab;	    // 当前处理输入事件的处理程序  
    struct input_handler **private;   // 私有处理程序列表  
  
    struct list_head h_list;	    // 处理程序列表节点  
    struct list_head node;	    // 输入设备列表节点  
};

        input_handle 结构体记录匹配成功的输入处理程序和输入设备之间的关系

struct input_handle {    
    void *private;      // 私有数据指针,用于存储特定于处理程序的信息  
    const char *name;   // 处理程序的名称  
    struct input_dev *dev; // 关联的输入设备  
    struct input_handler *handler; // 关联的输入处理程序  
};

        evdev_connect() 函数在 input_attach_handler() 中被调用,用于建立输入设备和处理程序之间的连接

static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id) {  
    // 获取次设备号,分配并初始化 evdev 结构体  
    // 设置设备名称、输入句柄、设备号等  
    // 注册输入句柄,初始化字符设备,添加字符设备  
    return 0;  
}

        input_register_device() 函数注册输入设备,将其添加到输入设备链表中,并为每个已存在的处理程序附加该设备。

int input_register_device(struct input_dev *dev) {  
    // 检查并设置设备位掩码,分配事件值数组  
    // 添加设备到设备链表,遍历处理程序链表,调用 input_attach_handler  
    // 返回注册结果  
    return 0;  
}

        综上所述,

        输入子系统通过 input_handler、input_dev 和 input_handle 结构体定义事件处理程序、输入设备和它们之间的关系。

        通过 input_register_handler() 和 input_register_device() 函数注册处理程序和设备,将它们关联起来,从而实现事件处理。

第117章 内核中输入子系统源码裁剪

117.1 内核中输入子系统的源码

        input 子系统源码所在路径为 kernel/drivers/input,如下图所示:

117.2 输入子系统源码裁剪和配置

        默认情况下,linux 内核已经支持了很多设备,但是在实际的使用中并使用不到,这时候就需要对输入子系统进行裁剪了。可在内核的menuconfig界面配置。

        如果想要对内核裁剪和配置,只需要勾选和取消即可,至此,关于输入子系统源码裁剪和配置就完成了。

第118章 编写简单的设备驱动层代码

118.1 input_allocate_device() 函数

/*为输入设备input_dev分配内存并简单初始化*/
struct input_dev *input_allocate_device(void)
{
    static atomic_t input_no = ATOMIC_INIT(-1);
    struct input_dev *dev;
    // 分配输入设备结构体的内存
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (dev) {
        // 设置设备类型和设备类
        dev->dev.type = &input_dev_type;
        dev->dev.class = &input_class;
        // 初始化设备
        device_initialize(&dev->dev);
        // 初始化互斥锁和事件自旋锁
        mutex_init(&dev->mutex);
        spin_lock_init(&dev->event_lock);
        // 初始化定时器
        timer_setup(&dev->timer, NULL, 0);
        // 初始化链表头
        INIT_LIST_HEAD(&dev->h_list);
        INIT_LIST_HEAD(&dev->node);
        // 设置设备名称,使用原子变量递增来保证唯一性
        dev_set_name(&dev->dev, "input%lu", (unsigned long)atomic_inc_return(&input_no));
        // 增加模块引用计数
        __module_get(THIS_MODULE);
    }
    return dev;
}

        这个函数的作用是为输入设备分配内存并进行必要的初始化,为后续的输入事件处理和设备注册做准备。

118.2 初始化 input_dev 结构体

        在使用 input_allocate_device 函数创建了一个 input_dev 结构体之后,

        接下来就要初始化input_dev 结构体内容了,

        在该步骤中又有两个内容,分别为设置事件类型设置具体类型

118.2.1 设置事件类型

        input_dev结构体通过一系列位图表示输入设备的支持的事件类型和支持的功能

struct input_dev {  
    ...
    unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_MAX)]; // 设备属性位图  
    unsigned long evbit[BITS_TO_LONGS(EV_MAX)];	    // 支持的事件类型位图  
    unsigned long keybit[BITS_TO_LONGS(KEY_MAX)];	    // 支持的按键位图  
    unsigned long relbit[BITS_TO_LONGS(REL_MAX)];	    // 支持的相对坐标位图  
    unsigned long absbit[BITS_TO_LONGS(ABS_MAX)];	    // 支持的绝对坐标位图  
    unsigned long mscbit[BITS_TO_LONGS(MSC_MAX)];	    // 支持的杂项事件位图  
    unsigned long ledbit[BITS_TO_LONGS(LED_MAX)];	    // 支持的 LED 位图  
    unsigned long sndbit[BITS_TO_LONGS(SND_MAX)];	    // 支持的声音位图  
    unsigned long ffbit[BITS_TO_LONGS(FF_MAX)];	    // 支持的力反馈位图  
    unsigned long swbit[BITS_TO_LONGS(SW_MAX)];	    // 支持的开关位图  
    ...
};

        通过设置位图中的特定位,可以告知输入子系统设备所支持的事件类型和功能。

        例如,evbit用于表示设备支持的事件类型,

        如按键事件(EV_KEY)、相对位移事件(EV_REL)等。

在头文件 include/uapi/linux/input-event-codes.h 中,Linux 内核已经为我们定义了一些输入
事件类型,它们的含义如下:
(1)EV_SYN (0x00): 用于同步事件,表示一组输入事件的结束。
(2)EV_KEY (0x01): 用于按键事件,表示按下、释放或重复一个键。
(3)EV_REL (0x02): 用于相对位移事件,表示设备的相对位置变化,例如鼠标的移动。
(4)EV_ABS (0x03): 用于绝对位移事件,表示设备的绝对位置变化,例如触摸屏的坐标。
(5)EV_MSC (0x04): 用于杂项事件,包含一些特殊目的的事件类型,例如设备状态变化等。
(6)EV_SW (0x05): 用于开关事件,表示开关的状态变化,例如电源按钮、开合盖等。
(7)EV_LED (0x11): 用于 LED 事件,表示 LED 灯的状态变化。
(8)EV_SND (0x12): 用于声音事件,表示声音的播放相关事件。
(9)EV_REP (0x14): 用于重复事件,表示键盘重复发送事件。
(10)EV_FF (0x15): 用于力反馈事件,表示力反馈设备的输出事件。
(11)EV_PWR (0x16): 用于电源事件,表示电源状态变化。
(12)EV_FF_STATUS (0x17): 用于力反馈状态事件,表示力反馈设备的状态变化。
(13)EV_MAX (0x1f): 输入事件类型的最大值。
(14)EV_CNT: 输入事件类型的数量

        这样,输入子系统就能正确识别和处理来自设备的按键事件。

        其他位图的设置方式类似,都是通过__set_bit函数来设置相应的位。

        通过设置位图中的特定位,可以告知输入子系统设备所支持的事件类型和功能。

        例如,要使输入设备支持按键事件,可以使用__set_bit函数设置 evbit中的 EV_KEY位:

__set_bit(EV_KEY, myinput_dev->evbit);//使myinput_dev指代的设备支持 按键事件

        这些位图在输入设备的注册和处理过程中起着重要的作用,它们确保了输入子系统能够正确配置和过滤来自设备的输入数据。

118.2.2 设置具体类型

        在Linux内核中,设置输入设备支持的事件类型后,还需要进一步设置具体的事件子类型

        对于按键事件(EV_KEY),这意味着需要指定设备支持的具体按键。

        这些按键通过宏定义在头文件include/uapi/linux/input-event-codes.h中定义,

        如KEY_1、KEY_2等。

// 首先设置设备支持按键事件  
__set_bit(EV_KEY, myinput_dev->evbit);  
  
// 然后设置设备支持具体按键“1”  
__set_bit(KEY_1, myinput_dev->keybit);

        这样,输入子系统就能识别和处理来自该设备的按键 “1”的事件。

        其他按键的设置方式类似,都是通过__set_bit函数在keybit位图中设置相应的位。

        这些设置确保了输入子系统能够正确响应设备产生的具体按键事件。

118.3 驱动程序的编写

//定义一个输入设备结构体
struct input_dev *myinput_dev;

//驱动入口函数
static int myinput_dev_init(void)
{
    int ret;
    // 分配输入设备结构体
    myinput_dev = input_allocate_device();
    if (myinput_dev == NULL) {
        printk("input_allocate_device error\n");
        return -1;
    }

    // 设置输入设备的名称
    myinput_dev->name = "myinput_dev";// 设置输入设备支持的事件类型
    __set_bit(EV_KEY, myinput_dev->evbit); // 设置支持按键事件
    __set_bit(KEY_1, myinput_dev->keybit); // 设置支持按键 1

    // 注册输入设备
    ret = input_register_device(myinput_dev);
    if (ret < 0) {
        printk("input_register_device error\n");
        goto error;
    }

    return 0;
error:
    // 注册失败,释放输入设备结构体
    input_free_device(myinput_dev);
    return ret;
}

//驱动出口函数
static void myinput_dev_exit(void)
{
    // 注销输入设备
    input_unregister_device(myinput_dev);
}

module_init(myinput_dev_init);
module_exit(myinput_dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

        驱动加载后,/dev/input 多出来一个设备节点

        然后使用以下命令查看设备节点信息,可以根据名字确定 event4 正是加载驱动所创建的, 

cat /proc/bus/input/devices

 第119章 从最简单的设备驱动代码入手分析匹配规则

        输入设备注册完成之后会自动生成相应的设备节点,在数据结构的介绍中讲解过,设备驱动层的结构体会和事件处理层的结构体进行匹配,可是从上一章编写的代码中并没有看到匹配的过程,所以在本章中将会分析设备驱动层是怎样跟事件处理层进行匹配的?

        在Linux输入子系统中,注册输入设备时会调用 input_register_device()函数,该函数为设备建立与处理程序的连接

        重点在于input_attach_handler()函数为每个输入设备找到匹配的处理程序并建立连接

input_attach_handler()函数的工作流程如下:

        调用 input_match_device()函数,检查处理程序与输入设备是否匹配

        input_match_device遍历处理程序的输入设备ID表

        使用input_match_device_id()函数比较设备的属性(如厂商ID、产品ID等)和事件位图与ID中指定的属性是否一致

        如果设备属性与ID匹配,且处理程序的额外匹配函数(如果存在)返回true,则认为找到匹配。

        如果找到匹配的ID,调用处理程序的connect()函数建立连接。

        例如,对于通用事件处理层evdev.c,其处理程序结构体evdev_handler包含connect函数evdev_connect

/*输入事件行为结构体*/
static struct input_handler evdev_handler = {  
    .connect = evdev_connect,      //连接函数
    // 其他成员...  
    .id_table = evdev_ids,         //输入设备匹配表
};  
  
/*输入设备匹配表*/
static const struct input_device_id evdev_ids[] = {  
    { .driver_info = 1 }, /*匹配所有设备 */  
    { },
};
#include <linux/input.h>  
  
//用于标识输入设备的结构体  
struct input_device_id {  
    __u32 flags;              // 标识字段,指示哪些字段有效  
    __u16 vendor;             // 设备的供应商 ID  
    __u16 product;            // 设备的产品 ID  
    __u16 version;            // 设备的版本号  
  
    __u32 evbit[INPUT_DEVICE_ID_EV_MAX / 32 + 1];  // 支持的事件类型位图  
    __u32 keybit[BITS_TO_LONGS(KEY_MAX) + 1];      // 支持的按键位图  
    __u32 relbit[BITS_TO_LONGS(REL_MAX) + 1];      // 支持的相对坐标轴位图  
    __u32 absbit[BITS_TO_LONGS(ABS_MAX) + 1];      // 支持的绝对坐标轴位图  
    __u32 mscbit[BITS_TO_LONGS(MSC_MAX) + 1];      // 支持的杂项功能位图  
    __u32 ledbit[BITS_TO_LONGS(LED_MAX) + 1];      // 支持的 LED 指示位图  
    __u32 sndbit[BITS_TO_LONGS(SND_MAX) + 1];      // 支持的声音功能位图  
    __u32 ffbit[BITS_TO_LONGS(FF_MAX) + 1];        // 支持的力反馈效果位图  
    __u32 swbit[BITS_TO_LONGS(SW_MAX) + 1];        // 支持的开关状态位图  
  
    __u32 hint_events_per_packet;  // 每包事件数量的提示值,用于优化处理  
  
    __s32 fuzz;                   // 模糊因子,处理噪声或精度问题  
    __s32 flat;                   // 平滑因子,处理抖动或微小移动  
  
    char phys[32];                // 设备的物理路径,唯一标识设备  
    char uniq[32];                // 设备的唯一标识符,与物理路径结合使用  
  
    __u8 hats;                    // 支持的帽子(方向键)数量  
    __u8 props;                   // 设备属性位图,指示特定属性  
};  
/*input_device_id结构体主要包含匹配设备所需的字段;
而input_dev结构体则包含了设备的各种属性和状态,以及处理设备事件所需的函数指针。
input_device_id结构体在编写输入处理程序时使用;
而input_dev结构体在编写输入设备驱动程序时使用。*/

/*
INPUT_DEVICE_ID_MATCH_DEVICE:
匹配设备的整体属性,这通常包括供应商ID(vendor ID)、产品ID(product ID)和版本(version)。
INPUT_DEVICE_ID_MATCH_VENDOR:
仅匹配设备的供应商ID。
INPUT_DEVICE_ID_MATCH_PRODUCT:
仅匹配设备的产品ID。
INPUT_DEVICE_ID_MATCH_VERSION:
仅匹配设备的版本信息。
INPUT_DEVICE_ID_MATCH_EVBIT:
匹配设备支持的事件类型(event types),如按键(EV_KEY)、相对坐标(EV_REL)、绝对坐标(EV_ABS)等。
INPUT_DEVICE_ID_MATCH_KEYBIT:
匹配设备支持的按键事件(key events)的具体位。
INPUT_DEVICE_ID_MATCH_RELBIT:
匹配设备支持的相对坐标事件(relative events)的具体位。
INPUT_DEVICE_ID_MATCH_ABSBIT:
匹配设备支持的绝对坐标事件(absolute events)的具体位。
INPUT_DEVICE_ID_MATCH_MSCBIT:
匹配设备支持的其他杂项事件(miscellaneous events)的具体位。
INPUT_DEVICE_ID_MATCH_LEDBIT:
匹配设备支持的LED状态(LED states)的具体位。
INPUT_DEVICE_ID_MATCH_SNDBIT:
匹配设备支持的声音事件(sound events)的具体位。
INPUT_DEVICE_ID_MATCH_FFBIT:
匹配设备支持的力反馈效果(force feedback effects)的具体位。
INPUT_DEVICE_ID_MATCH_SWBIT:
匹配设备支持的开关状态(switch states)的具体位。
*/

        evdev_ids中的ID匹配所有设备,因为 driver_info为1且没有其他限制条件。

        input_match_device_id()函数在没有特定匹配条件时会返回true。

        对于特定事件处理层,如joydev.c,是否匹配需根据其 ID表和匹配逻辑确定。

        如果 joydev.c的 ID表和匹配逻辑与输入设备属性相符,则设备也能与joydev.c匹配并连接。

 

第120章 多对多的匹配关系分析

120.1 joydev.c 事件处理层匹配分析

        joydev.c 文件中的 joydev_handler 结构体配置了特定的匹配表连接函数

        与通用设备驱动层 evdev.c 中的 evdev_handler 不同,

        joydev_handler 使用 joydev_ids 数组来定义匹配的输入设备特征。

        joydev_ids 数组包含多个 input_device_id 结构体,每个结构体描述了一种输入设备的特征。

flags: 指定匹配方式,

            如 INPUT_DEVICE_ID_MATCH_EVBIT 和 INPUT_DEVICE_ID_MATCH_ABSBIT。

evbit: 事件类型的位掩码,如 EV_ABS(绝对事件)或 EV_KEY(按键事件)。

absbit: 绝对事件类型的位掩码,如 ABS_X(X 轴)、ABS_Z(Z 轴)等。

keybit: 按键类型的位掩码,如 BTN_JOYSTICK(游戏杆)、BTN_GAMEPAD(游戏手柄)等。

#include <linux/input.h>  
#include <linux/joystick.h>  
  
static struct joydev_id joydev_ids[] = {  
    {  
        .name      = "Thrustmaster FCS",  
        /* 假设的供应商 ID、产品 ID 等 */  
        .vendor    = 0x1234,  
        .product   = 0x5678,  
        .version   = 0x0100,  
        .bIntfNum  = 0x00,  
        .bIntfType = 0x00,  
    },  
    /* 可以继续添加更多设备 */  
    {  }  ,
};

匹配过程

input_attach_handler() 函数:

        调用 input_match_device() 匹配输入设备和处理程序

input_match_device 函数:

        遍历处理程序的 id_table(即 joydev_ids)。

        调用 input_match_device_id() 判断设备是否与当前 ID 匹配。

        如果匹配且处理程序的 match 函数(如果有)返回 true,则返回匹配的 ID。

input_match_device_id 函数:

        检查设备的总线类型、厂商 ID、产品 ID 和版本号是否匹配(这些在简单设备驱动中通常未设置)。

        使用 bitmap_subset 检查设备的事件位图是否是给定 ID 的子集

示例分析

        在简单设备驱动代码中,设置了支持按键事件 (EV_KEY) 和按键 1 (KEY_1)。

        然而,joydev_ids 中匹配的是特定类型的绝对事件和按键事件(如 ABS_X, ABS_Z, BTN_JOYSTICK 等),因此简单设备驱动不会与 joydev_ids 中的任何 ID 匹配

// 定义了一个设备ID表,用于匹配特定的输入事件和设备属性  
static const struct input_device_id joydev_ids[] = {  
    {   
        // 匹配具有ABS_X属性(通常代表X轴位置)的设备  
        .flags = INPUT_DEVICE_ID_MATCH_EVBIT |  //仅匹配设备支持的事件类型
                INPUT_DEVICE_ID_MATCH_ABSBIT,   //匹配设备支持的绝对坐标事件
        .evbit = { BIT_MASK(EV_ABS) },          //事件类型的位掩码
        .absbit = { BIT_MASK(ABS_X) }           //绝对事件类型的位掩码
    },  
    {   
        // 匹配具有ABS_Z属性(通常代表某种轴向位置,如Z轴或旋转轴)的设备  
        .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_ABSBIT,  
        .evbit = { BIT_MASK(EV_ABS) },  
        .absbit = { BIT_MASK(ABS_Z) }  
    },  
    {   
        // 匹配具有BTN_JOYSTICK按键(通常代表游戏杆上的某个按钮)的设备  
        .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_KEYBIT,  
        .evbit = { BIT_MASK(EV_KEY) },  
        .keybit = { [BIT_WORD(BTN_JOYSTICK)] = BIT_MASK(BTN_JOYSTICK) }  
    },  
    { } // 空项,作为终止符  
};  
  
// 定义了游戏杆输入处理程序的结构体  
static struct input_handler joydev_handler = {  
    .event = joydev_event,         // 指向处理输入事件的函数的指针  
    .match = joydev_match,         // 指向匹配函数的指针,用于决定是否处理该设备  
    .connect = joydev_connect,     // 指向连接函数的指针,当设备连接时调用  
    .disconnect = joydev_disconnect,// 指向断开函数的指针,当设备断开时调用  
    .legacy_minors = true,         // 使用传统的次设备号  
    .minor = JOYDEV_MINOR_BASE,    // 次设备号基数  
    .name = "joydev",              // 处理程序名称  
    .id_table = joydev_ids,        // 指向设备ID表的指针【处理程序的匹配规则】  
};  
  
// 初始化函数,注册游戏杆输入处理程序  
static int __init joydev_init(void) {  
    return input_register_handler(&joydev_handler); // 注册处理程序并返回结果  
}

        这段代码是Linux内核中用于处理游戏杆输入设备的一部分。

        它定义了一个输入处理程序,该程序能够匹配特定的输入事件和设备属性,并处理这些事件。通过注册这个处理程序,系统能够识别并管理游戏杆设备。

        满足以上匹配规则之一即可。

        在输入子系统中,输入设备和输入处理器之间的关系是多对多的。这意味着一个输入设备可以与多个输入处理器关联,而一个输入处理器也可以处理多个输入设备的事件

第121章 继续完善设备驱动层代码

121.1 上报事件

        在Linux内核中,输入设备驱动在检测到事件时,需先确定事件类型,如按键(EV_KEY)、相对位置(EV_REL)或绝对位置(EV_ABS)。

        随后,使用对应的上报函数将事件通知给输入子系统

/*按键事件*/
input_report_key(dev, KEY_A, 1); // 按下A键  
input_report_key(dev, KEY_A, 0); // 释放A键
/*相对位置事件*/
input_report_rel(dev, REL_X, 10); // X轴相对移动10个单位
/*绝对位置事件*/
input_report_abs(dev, ABS_X, 100); // X轴绝对位置100

        其中,dev是指向input_dev结构体的指针,代表输入设备。

        这些上报函数会将事件数据传递给输入子系统,进而由用户空间程序进行处理。

121.2 上报函数

        input_report_key()用于上报按键事件

void input_report_key(struct input_dev *dev, //设备
                          unsigned int code, //按键码,哪个按键
                                 int value); //按键状态,如0按下1松开

         input_report_rel()用于上报相对位置事件

void input_report_rel(struct input_dev *dev, //设备
                          unsigned int code, //位置码  
                                 int value); //位置偏移量

         input_report_abs()用于上报绝对位置事件

void input_report_abs(struct input_dev *dev, 
                          unsigned int code, 
                                  int value);

        input_report_ff_status()用于上报力反馈状态事件

void input_report_ff_status(struct input_dev *dev, //设备
                                unsigned int code, //力反馈码
                                       int value); //力反馈状态,0停止1运行中

        input_report_switch()用于上报开关事件

void input_report_switch(struct input_dev *dev,  //设备
                             unsigned int code,  //开关码
                                    int value);  //开关状态 0关1开

        input_sync()用于上报同步事件,告知输入子系统事件结束,确保事件按预期顺序处理。

void input_sync(struct input_dev *dev);

121.3 驱动程序编写

struct input_dev *myinput_dev; // 输入设备结构体指针
static void timer_function(struct timer_list *t);
DEFINE_TIMER(test_timer, timer_function); // 定义定时器

/*定时器*/
static void timer_function(struct timer_list *t) {
    static int value = 0; // 静态变量用于切换键值
    value = value ? 0 : 1;
    input_event(myinput_dev, EV_KEY, KEY_1, value); // 发送按键事件
    input_event(myinput_dev, EV_SYN, SYN_REPORT, 0); // 发送同步事件
    mod_timer(&test_timer, jiffies + msecs_to_jiffies(1000)); // 更新定时器
}

static int myinput_dev_init(void) {
    int ret;
    myinput_dev = input_allocate_device(); // 分配输入设备
    if (myinput_dev == NULL) {
        printk("input_allocate_device error\n");
        return -ENOMEM;
    }
    myinput_dev->name = "myinput_dev"; // 设置设备名
    set_bit(EV_KEY, myinput_dev->evbit); // 设置支持的事件类型:按键事件
    set_bit(EV_SYN, myinput_dev->evbit); // 设置支持的事件类型:同步事件
    set_bit(KEY_1, myinput_dev->keybit); // 设置支持的按键键值
    ret = input_register_device(myinput_dev); // 注册输入设备
    if (ret < 0) {
        printk("input_register_device error\n");
        goto error;
    }
    mod_timer(&test_timer, jiffies + msecs_to_jiffies(1000)); // 启动定时器
    return 0;
error:
    input_free_device(myinput_dev);
    return ret;
}

static void myinput_dev_exit(void) {
    del_timer(&test_timer); // 删除定时器
    input_unregister_device(myinput_dev); // 取消注册输入设备
    input_free_device(myinput_dev); // 释放输入设备内存
}

        在加载驱动之前首先使用以下命令查看当前的输入设备,如下所示:

        加载驱动,然后重新查看设备节点,可以看到多出来了一个 event4 节点,如下图所示: 然后使用下命令查看设备节点的输出信息,如下图所示:

        可以看到,每隔一秒钟就会打印一次数据,至此,关于在最简单的设备驱动程序中完善上报事件的实验就完成了。

121.4 编写应用获取上报数据

        为了在用户空间中获取输入设备的数据,可以编写一个应用程序来读取并解析

        /dev/input/eventX设备文件中的数据。

        这个过程涉及打开设备文件、读取数据、解析input_event结构体以及处理事件。

        input_event结构体定义了输入事件的关键信息,包括事件类型(type)事件码(code)事件值(value)

        事件类型用于分类事件,如按键、鼠标移动等;

        事件码进一步指定具体事件;

        事件值则提供事件的详细信息。

/*事件数据结构体*/
struct input_event {
    struct timeval time; 
    __u16 type;// 类型
    __u16 code;                // 具体事件
    __s32 value;// 对应的取值
};
/*
type:type 用于描述发生了哪一种类型的事件(对事件的分类),
Linux 系统所支持的输入事件类型如下所示:

#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
#define EV_SND 0x12
#define EV_REP 0x14
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
*/

/*
code:code 表示该类事件中的哪一个具体事件,
以上列举的每一种事件类型中都包含了一系列具体事件,譬如一个键盘上通常有很多按键,
而 code 变量则告知应用程序是哪一个按键发生了输入事件。
#define KEY_RESERVED 0
#define KEY_ESC 1 //ESC 键
#define KEY_1 2 //数字 1 键
#define KEY_2 3 //数字 2 键
#define KEY_3 4 //数字 3 键
#define KEY_4 5 //数字 4 键
#define KEY_5 6 //数字 5 键
#define KEY_6 7 //数字 6 键
#define KEY_7 8 //数字 7 键
#define KEY_8 9 //数字 8 键
#define KEY_9 10 //数字 9 键
#define KEY_0 11 //数字 0 键
#define KEY_MINUS 12 //减号键
#define KEY_EQUAL 13//加号键
#define KEY_BACKSPACE 14 //回退键
................................
*/

/*
value:内核每次上报事件都会向应用层发送一个数据 value,
对 value 值的解释随着 code的变化而变化。
譬如对于按键事件来说,
如果 value 等于 1,则表示按键按下;value 等于 0表示按键松开,
如果 value 等于 2 则表示按键长按。而在绝对位移事件中(type=3),
如果code=0(触摸点 X 坐标 ABS_X),那么 value 值就等于触摸点的 X 轴坐标值;
如果 code=1(触摸点 Y 坐标 ABS_Y),此时 value 值便等于触摸点的 Y 轴坐标值。
*/
#include <stdio.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <linux/input.h>  

int main() {  
    int fd = open("/dev/input/event4", O_RDWR); // 打开输入设备文件  
    if (fd < 0) {  
        printf("打开错误\n");  
        return -1;  
    }  
  
    struct input_event event;  
    while (1) {  
        int ret = read(fd, &event, sizeof(event)); // 读取输入事件  
        if (ret < 0) {  
            printf("读取错误\n");  
            return -2;  
        }  
  
        if (event.type == EV_KEY && event.code == KEY_1) { // 判断事件类型和键码  
            printf("键1:值为 %d\n", event.value); // 输出键值  
        }  
    }  
  
    close(fd); // 关闭设备文件(实际上在无限循环中不会执行到这一步)  
    return 0;  
}

第122章 输入子系统上报数据格式分析

        在 input_event 数据包中,有四个成员变量:time,type,code,value。

        这些成员变量的值在使用 hexdump 命令获取到的数据中是以字节的形式存储的每个字节都以十六进制的形式表示。

         一般情况下,input_event 数据包的字节顺序是小端字节序(Little Endian)。

         一个 input_event 数据包所占字节的大小为 8+8+2+2+4 =24 个字节。

对应的成员值如下:

        tv_sec: 0f09 65d3 0000 0000

        tv_usec: 36fb 0001 0000 0000

        type: 0003

        code:0039

        value: 0000 0000

        使用__u16而非unsigned short int,是因为__u16在Linux内核中定义为无符号16位整数,能确保在不同平台上大小一致(2字节)及字节顺序确定,从而避免跨平台数据不一致问题。

第123章 固定输入设备的设备节点

        在进行嵌入式 Linux 开发时,不同厂家和型号的外设在内核启动时加载的顺序可能会不同。

        例如,触摸板和 USB 转串口等设备,这会导致在/dev/input 目录下创建的 evdevx 节点(其

中 x=0,1,2,3...)不同。

        然而应用程序通常打开的是固定的设备节点,如果设备节点发生变化,就会导致应用程序打开错误的设备节点,因此,需要对输入设备创建的设备节点进行固定。

        通过分析 evdev.c 驱动程序,我们确定设备节点是在 evdev_connect 函数中创建的。因此,只需要在 evdev_connect 函数中针对需要固定设备节点的设备单独创建一个设备节点即可。

        1、使用 cat /proc/bus/input/devices 命令找到设备名称。

         2、 修改 evdev_connect 函数,根据设备名称来决定设备节点的固定方式。

        3、 打开 evdev.c 驱动程序文件,在 evdev_connect 函数中,根据设备名称进行条件判断。如果设备名称匹配特定的条件,使用 dev_set_name 函数来固定设备节点。

        设备节点的名称可以根据需求进行自定义设置,确保与应用程序中使用的设备节点名称一致。

        修改完成之后,重新烧写内核镜像。然后进入/dev/input 目录下,如下图所示: 

        我们输入“hexdump mytouch1”命令,然后按触摸屏,会发现有数据上报, 

        至此,固定输入设备的设备节点实验就完成了。