Bootstrap

input子系统的框架和重要数据结构详解

#1024程序员节 | 征文#
在这里插入图片描述
1

往期内容

I2C子系统专栏:

  1. 专栏地址:IIC子系统_憧憬一下的博客-CSDN博客
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有往期内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有往期内容观看顺序

1. 回顾字符设备驱动程序

img

1. 确定主设备号

字符设备驱动需要一个主设备号来标识。你可以选择一个静态的设备号,或者使用动态分配的设备号。

#define DEVICE_NAME "my_chrdev"
#define CLASS_NAME  "my_class"
static int major;  // 保存主设备号

可以通过 register_chrdev() 函数静态注册一个设备号,或者通过 alloc_chrdev_region() 动态分配设备号。

2. 创建 file_operations 结构体

file_operations 结构体定义了设备文件操作的函数指针,如打开、读写、控制等。你需要在这里填充相关的操作函数:

static int drv_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int drv_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t drv_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
    printk(KERN_INFO "Reading from device\n");
    return 0;  // 返回读取的字节数
}

static ssize_t drv_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
    printk(KERN_INFO "Writing to device\n");
    return len;  // 返回写入的字节数
}

static struct file_operations fops = {
    .open = drv_open,
    .release = drv_release,
    .read = drv_read,
    .write = drv_write,
};

3. 注册 file_operations 结构体

通过 register_chrdev()file_operations 注册到内核中。如果主设备号是动态分配的,需要通过 major 保存分配的设备号。

static int __init my_driver_init(void) {
    // 动态分配主设备号
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register char device\n");
        return major;
    }
    printk(KERN_INFO "Registered char device with major number %d\n", major);

    // 创建设备类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create class\n");
        return PTR_ERR(my_class);
    }

    // 创建设备节点
    my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create device\n");
        return PTR_ERR(my_device);
    }

    return 0;
}

在这里,我们使用 class_create()device_create() 来创建设备类和设备节点,这样用户空间可以通过 /dev/my_chrdev 访问设备。

4. 编写入口函数

入口函数是模块加载时执行的代码,一般用于初始化设备、分配资源等。

static int __init my_driver_init(void) {
    printk(KERN_INFO "Initializing character device driver\n");

    // 注册字符设备
    major = register_chrdev(0, DEVICE_NAME, &fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register a major number\n");
        return major;
    }
    printk(KERN_INFO "Registered with major number %d\n", major);

    // 创建类和设备节点
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(my_class);
    }

    my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        unregister_chrdev(major, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(my_device);
    }

    printk(KERN_INFO "Device class created\n");
    return 0;
}

5. 编写出口函数

当卸载驱动时需要释放相关资源,这个过程在出口函数中完成。

static void __exit my_driver_exit(void) {
    device_destroy(my_class, MKDEV(major, 0));  // 删除设备节点
    class_unregister(my_class);  // 注销设备类
    class_destroy(my_class);  // 销毁类
    unregister_chrdev(major, DEVICE_NAME);  // 注销字符设备
    printk(KERN_INFO "Character device driver unloaded\n");
}

6. 辅助函数

使用 class_createdevice_create 可以帮助自动创建 /dev 中的设备节点,从而方便用户访问。

  • class_create(THIS_MODULE, CLASS_NAME):创建一个类
  • device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME):在 /dev 中创建设备节点

2. Input子系统框架

img

从图中可以看到,整个 Linux 输入子系统架构可以分为三大层:Input Device 层Input Core 层Input Handler 层

1. Input Device 层(输入设备层)

这个层主要是和硬件直接打交道的地方,如键盘、鼠标、触摸屏、GPIO
键等输入设备。输入设备的驱动程序从硬件中获取数据,并将这些原始数据转换为标准的输入事件,然后上报给核心层(Input Core)。

核心组件:

  • input_dev 结构体:这是设备驱动程序和输入子系统交互的核心结构体。它定义了设备驱动程序如何报告事件,比如按键按下、鼠标移动等。

简要流程: 硬件设备产生输入信号(如按下按键),设备驱动程序通过 input_dev 将这些输入信号传递给 Input Core。

2. Input Core 层(输入核心层)

这个层负责接收来自底层输入设备的事件,并将这些事件分发给上层的处理器(Handler)。它起到桥梁的作用,将底层设备的事件转换为上层可以理解的标准化事件。

核心组件:

  • input.c:是 Input Core 层的主要模块,它提供了 API 和机制来管理输入设备和输入事件。输入核心会将设备上报的事件(如键值、坐标)转交给处理这些事件的 handler

功能概述

  • 接收底层设备上报的输入事件。
  • 将事件转发给合适的 handler 进行处理。

3. Input Handler 层(输入处理层)

这个层负责处理输入核心层转发的输入事件,并向用户空间提供访问接口。在 Linux 中,evdev
是最常用的处理层,负责将输入事件暴露给用户空间。

核心组件:

  • evdev.c:该模块将输入设备的事件通过 /dev/input/eventX 设备节点暴露给用户空间的应用程序,应用程序可以通过 open()read()ioctl() 等系统调用来访问这些事件。

file_operations 结构体evdev_fops 定义了如何处理用户空间对输入设备的访问。这个结构体包括了多个操作函数,比如:

  • .read:读取输入事件。
  • .write:写入数据。
  • .poll:轮询事件。
  • .ioctl:处理设备的控制命令。

4. 用户空间访问

用户空间的应用程序可以通过 /dev/input/eventX 设备节点直接访问输入设备。这些应用程序通过
open()ioctl()poll()read() 等接口访问设备数据,处理输入事件。

举例:

  • tslib:是用于处理触摸屏输入的库。
  • libinput:是一个更高级的输入处理库,用于统一处理键盘、鼠标、触摸屏等设备的输入事件。

Input Device 层:负责将硬件设备的原始输入信号转换为标准事件。

Input Core 层:负责管理输入设备,并将标准事件传递给处理层。

Input Handler 层:将事件暴露给用户空间,用户通过访问 /dev/input/eventX 来读取输入事件。

3. Input子系统重要结构体

内核源码:

img

3.1 Input_dev

truct input_dev 是描述输入设备的核心结构体。它提供了输入设备的详细属性、支持的事件类型、处理事件的回调函数等。在开发输入设备驱动时,开发者需要初始化并注册该结构体,使其能够正确地处理输入事件并将其传递到上层。

\Linux-4.9.88\include\linux\input.h
struct input_dev {
	const char *name; //输入设备的名称。例如,"USB Keyboard"
	const char *phys; //输入设备的物理路径。例如,"usb-0000:00:1d.0-1/input0"。
	const char *uniq; //输入设备的唯一标识符,通常用于区分具有相同类型的多个设备。
	struct input_id id; //输入设备的硬件 ID,用于标识输入设备的制造商、产品 ID 和版本号。

	unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

	unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
    //设备支持的事件类型。事件类型定义在 linux/input.h 中,如 EV_KEY 表示按键事件,EV_ABS 表示绝对坐标事件。
	unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];//设备支持的按键,如键盘按键或鼠标按键。
	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)];//设备支持的 LED 事件(如键盘指示灯)。
	unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];//设备支持的声音事件(如蜂鸣器)。
	unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];//设备支持的力反馈事件(如游戏手柄的振动)。
	unsigned long swbit[BITS_TO_LONGS(SW_CNT)];//设备支持的开关事件。

	unsigned int hint_events_per_packet;
    // 每个数据包的建议事件数,优化设备的事件处理。

	unsigned int keycodemax;
	unsigned int keycodesize;
    //输入设备支持的最大键码数和每个键码的大小。
	void *keycode;//存储设备支持的键码映射表。

	int (*setkeycode)(struct input_dev *dev,
			  const struct input_keymap_entry *ke,
			  unsigned int *old_keycode);//设置设备的键码映射。
	int (*getkeycode)(struct input_dev *dev,
			  struct input_keymap_entry *ke);// 获取设备的键码映射。

	struct ff_device *ff;//力反馈设备的相关信息。

	unsigned int repeat_key;//保存最后一个重复的按键值。
	struct timer_list timer;//用于处理按键重复事件的定时器

	int rep[REP_CNT];//按键重复相关的参数,主要用于延迟与频率。

	struct input_mt *mt;//多点触控相关的数据结构。

	struct input_absinfo *absinfo;//保存绝对坐标相关的信息(如最小值、最大值、分辨率等)。

	unsigned long key[BITS_TO_LONGS(KEY_CNT)];//保存按键的当前状态
	unsigned long led[BITS_TO_LONGS(LED_CNT)];//保存 LED 的当前状态
	unsigned long snd[BITS_TO_LONGS(SND_CNT)];//保存声音设备的当前状态
	unsigned long sw[BITS_TO_LONGS(SW_CNT)];//保存开关的当前状态。

	int (*open)(struct input_dev *dev);//打开设备时调用的函数,通常用于初始化设备。
	void (*close)(struct input_dev *dev);// 关闭设备时调用的函数,通常用于释放资源。
	int (*flush)(struct input_dev *dev, struct file *file);//刷新设备时调用的函数,通常用于清空缓冲区。
	int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
    //处理输入事件的回调函数

	struct input_handle __rcu *grab;

	spinlock_t event_lock;//用于保护输入事件处理的自旋锁。
	struct mutex mutex;//用于保护设备其他部分的互斥锁。

	unsigned int users;//记录打开该设备的用户数。
	bool going_away;//标识设备是否正在被卸载。

	struct device dev;//内核设备模型中的设备结构体。

	struct list_head	h_list; // 保存handle链表
	struct list_head	node;
    //设备在输入子系统链表中的挂载点。

	unsigned int num_vals;
	unsigned int max_vals;
    //当前设备的输入值数量及其最大值。
    
	struct input_value *vals;//保存输入值的数组

	bool devres_managed;//标识设备是否由资源管理器管理
};

3.2 input_handler

input_handler 作为输入事件的处理器,它的作用在于:

  • 事件的接收与处理:它定义了如何接收和处理来自输入设备的事件,如键盘按键、鼠标移动等。
  • 事件过滤filter 函数可以在事件传递前过滤掉不需要的事件,提高处理效率。
  • 设备匹配与连接:通过 matchconnect 函数,它负责将处理程序与适当的设备连接起来。
  • 事件传递eventevents 回调函数可以在设备产生事件时,将事件传递给上层逻辑,或通过驱动程序接口传递给用户空间应用。

工作流程简述

  1. 设备连接:当一个输入设备注册时,input_handlermatch 函数会被调用,以确定该处理程序是否可以处理这个设备。如果匹配成功,connect 函数被调用来建立连接。
  2. 事件处理:一旦设备产生事件,eventevents 回调会被调用来处理输入事件。根据设备和处理程序的实现,事件可以传递给用户空间或在内核中进一步处理。
  3. 设备断开:当设备断开时,disconnect 函数会被调用,处理程序负责清理与设备连接相关的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handler {

	void *private;
    //用于存储处理程序的私有数据。驱动程序可以使用它来保存与特定设备相关的上下文信息。

	void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	//这是处理单个输入事件的回调函数。事件由 type、code 和 value 表示,分别代表事件类型、事件码和事件的值(如按键按下或松开、移动坐标等)。
    
    void (*events)(struct input_handle *handle,
		       const struct input_value *vals, unsigned int count);
    //处理多个输入事件的回调函数。vals 是一个包含多个事件的数组,count 是事件的数量。这允许处理一组输入事件,提高效率。
	
    bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
	//这是一个事件过滤函数。它用于判断某个事件是否应该被传递给上层。如果返回 true,则事件会被过滤掉,不会继续处理;如果返回 false,事件将继续传递。
   
    bool (*match)(struct input_handler *handler, struct input_dev *dev);
	//用于匹配处理程序和输入设备。返回 true 表示当前 handler 可以处理 dev 设备。
    
    int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
	//当有输入设备与 handler 匹配时调用的回调函数,用于连接设备。返回 0 表示成功连接,其他值表示失败。
    
    void (*disconnect)(struct input_handle *handle);
	//当输入设备断开时调用的回调函数,用于处理设备断开连接后的清理工作。
    
    void (*start)(struct input_handle *handle);
    //在 connect 成功之后调用的回调函数,通常用于初始化某些特定资源或启动设备处理程序。
    
	bool legacy_minors;//指示处理程序是否使用了旧的次设备号 (minor number) 机制。这个标志主要用于向后兼容旧的输入设备模型。
	int minor;//用于存储处理程序关联的次设备号。次设备号帮助区分同一类设备中的不同实例。
	const char *name;//输入处理程序的名称,用于在内核日志或其他信息输出中标识处理程序。

	const struct input_device_id *id_table;
    //这是一个输入设备 ID 表,用于定义处理程序可以匹配的输入设备类型。它通常包含输入设备的厂商 ID、产品 ID 等信息,用于识别设备。

	struct list_head	h_list;//用于将 input_handler 添加到输入子系统的处理程序列表handle中,和相应的input_dev匹配保存进去
	struct list_head	node;//用于将 input_handler 添加到设备的处理程序链表中。
};

3.3 input_handle

输入子系统中的一个核心结构体,它用于连接输入设备 (input_dev) 和输入事件处理程序 (input_handler)。当设备和处理程序之间建立连接时,input_handle 负责维护两者之间的关联,确保事件能够从设备传递到合适的处理程序进行处理。

工作流程简述

  1. 设备与处理程序连接:当输入设备与输入处理程序匹配成功时,系统会创建并初始化一个 input_handle,用来维持设备和处理程序的关系。
  2. 事件传递:一旦输入设备产生事件,input_handle 会将这些事件从 input_dev 传递给对应的 input_handler 进行处理。
  3. 设备打开/关闭管理input_handle 中的 open 标志位用于记录设备的状态,确保设备只在被打开时接收和处理事件,并且在关闭时停止事件处理。
  4. 设备断开:当输入设备断开连接时,系统会调用 input_handlerdisconnect 函数,并清理 input_handle 中的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handle {

	void *private;
    //这个指针用于保存输入处理程序的私有数据(上下文数据)。
    //输入子系统在连接设备时,可能需要为每个连接的 input_handle 维护独立的数据。

	int open; // 这个整数值用于表示设备是否已经打开。
	const char *name;
    //处理程序的名称,通常用于标识该输入处理程序,可以帮助调试和日志输出时识别当前使用的 input_handle。

	struct input_dev *dev;//它表示 input_handle 正在处理的输入设备。
	struct input_handler *handler;//它表示当前 input_handle 所使用的处理程序。

	struct list_head	d_node;
    //这个链表节点用于将 input_handle 挂接到输入设备的处理程序链表中。这意味着一个输入设备可以有多个 input_handle 进行事件处理。
	struct list_head	h_node; //同里
};

3.4 input_event/value

是 Linux 输入子系统中用来描述输入设备事件的核心结构体,它表示从输入设备(如键盘、鼠标、触摸屏等)发送到内核的每个输入事件。

\Linux-4.9.88\include\linux\input.h
struct input_event {
	struct timeval time; //用来记录事件发生的时间。它包括秒 (tv_sec) 和微秒 (tv_usec) 两个字段,表示该事件的时间戳。
	__u16 type; 
    // 表示事件的类型。它决定了事件的类别,比如按键事件、相对移动事件、绝对位置事件等。
    
	__u16 code;
    //表示具体的事件代码。其含义依赖于 type 的值。
    
	__s32 value;
    //表示事件的值,其含义取决于 type 和 code 的组合。
};

struct input_value { //差不多,用这个比较多好像
	__u16 type;
	__u16 code;
	__s32 value;
};

type:

\Linux-4.9.88\include\uapi\linux\input-event-codes.h
/*
 * Event types
 */
#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)

这些宏定义描述了 Linux 输入子系统中不同的 事件类型 (Event Types)。这些类型是用于标识从输入设备(例如键盘、鼠标、触摸屏等)传递到内核的事件类别,每个类别都有不同的意义。struct input_event 结构体中的 type 字段使用这些宏来确定事件的类别。

  1. EV_SYN (0x00)
  • 同步事件,用于标识一组输入事件的结束。例如,多个输入事件会通过 EV_SYN 通知系统完成一个事件序列。这是一个标志位,告诉系统已经处理完一个事件的所有数据。
  • 典型用途:报告鼠标或触摸屏输入后,系统应通过 EV_SYN 告诉用户空间事件已完成。
  1. EV_KEY (0x01)
  • 按键事件,表示设备上的按键或按钮状态的变化。这是键盘、鼠标按钮、游戏控制器按钮等输入设备常用的事件类型。
  • 典型用途:键盘按下某个按键(如 KEY_A)或者鼠标点击时触发。
  1. EV_REL (0x02)
  • 相对坐标事件,用于描述相对于当前坐标的位移量。常用于鼠标等设备,表示相对位置的改变。
  • 典型用途:鼠标移动过程中生成的相对位移数据(REL_X, REL_Y)。
  1. EV_ABS (0x03)
  • 绝对坐标事件,用于描述设备的绝对坐标值。常用于触摸屏、操纵杆等设备,表示其当前位置。
  • 典型用途:触摸屏上的触摸点的 X 和 Y 位置(ABS_X, ABS_Y)。
  1. EV_MSC (0x04)
  • 杂项事件,用于发送某些特定的输入设备数据,例如时间戳或者扫描码等。
  • 典型用途:键盘扫描码事件(MSC_SCAN)。
  1. EV_SW (0x05)
  • 开关事件,用于表示设备上的开关状态。例如,表示设备是否插入、盖子是否关闭等。
  • 典型用途:检测设备状态(如笔记本的盖子是否关闭)。
  1. EV_LED (0x11)
  • LED 事件,用于控制设备上的 LED 指示灯。例如,键盘上的大写锁定指示灯(Caps Lock)、数字锁定指示灯(Num Lock)等。
  • 典型用途:设置或读取键盘上的 LED 状态。
  1. EV_SND (0x12)
  • 声音事件,用于产生音频信号,例如蜂鸣器。
  • 典型用途:系统蜂鸣器发出声音。
  1. EV_REP (0x14)
  • 重复事件,用于控制按键重复行为。例如,按住某个键时,定义按键重复的时间间隔(重复速度和延迟)。
  • 典型用途:按住某个键时产生的重复输入。
  1. EV_FF (0x15)
  • 力反馈事件,用于与具有力反馈功能的设备(如游戏控制器)进行交互,产生物理反馈(震动等)。
  • 典型用途:控制游戏控制器的震动反馈。
  1. EV_PWR (0x16)
  • 电源事件,用于报告或控制设备的电源状态。
  • 典型用途:电源按钮或设备的电源管理事件。
  1. EV_FF_STATUS (0x17)
  • 力反馈状态事件,用于报告力反馈设备的状态。
  • 典型用途:检测力反馈是否成功启动或停止。
  1. EV_MAX (0x1f)
  • 事件类型的最大值,用于定义事件类型的范围。
  1. EV_CNT (EV_MAX + 1)
  • 事件类型的数量(总计 32 种事件类型)。用于计算事件类型的总数。

这些事件类型用于在内核和用户空间之间传递输入设备的事件信息,驱动程序根据事件类型来处理输入设备的数据。例如:

  • 键盘驱动程序会处理 EV_KEY 类型的事件,用于表示按键的按下与松开。
  • 鼠标驱动程序会处理 EV_REL 类型的事件,用于表示鼠标的移动。
  • 触摸屏驱动程序会处理 EV_ABS 类型的事件,用于表示触摸点的坐标。

通过这些事件类型,输入子系统可以统一处理不同输入设备的数据,并通过 /dev/input/eventX 接口传递给用户空间程序。

code:

以type为EV_KEY按键类型例子:

#define KEY_RESERVED		0
#define KEY_ESC			1
#define KEY_1			2
#define KEY_2			3
#define KEY_3			4
#define KEY_4			5
#define KEY_5			6
#define KEY_6			7
#define KEY_7			8
#define KEY_8			9
#define KEY_9			10
.......

以type为EV_ABS绝对位移类型例子:

/*
 * Absolute axes
 */
#define ABS_X			0x00 //x方向
#define ABS_Y			0x01 /y方向
#define ABS_Z			0x02
#define ABS_RX			0x03
.......

value:

对于按键,它的 value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按);

对于触摸屏,它的 value 就是坐标值、压力值。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;