当前位置: 首页 > news >正文

Linux中input子系统

一、Input子系统输入设备管理

Input子系统的核心目标是屏蔽硬件差异,提供统一的软件接口——对驱动开发者,只需按子系统规范注册设备;对应用开发者,只需从固定节点(如/dev/input/event0)读取输入事件,无需关心底层硬件是键盘还是触摸屏。

其核心价值体现在:

  • 兼容性:支持所有类型的输入设备(按键、触摸、鼠标、摇杆等),新设备只需适配子系统规范即可接入。
  • 简化开发:驱动开发者无需关注字符设备注册、数据分发等通用逻辑,只需专注硬件操作(如中断处理、数据读取)。
  • 统一接口:应用层通过标准的文件IO接口(open/read/poll)与所有输入设备交互,无需针对不同设备编写适配代码。

二、输入设备的驱动框架

Input子系统采用分层架构,将驱动分为“硬件适配层”和“事件处理层”,中间通过核心层(Input Core)衔接,结构如下:

在这里插入图片描述

各层功能拆解

层级功能描述核心角色
硬件适配层与具体硬件交互,负责初始化硬件、处理中断、读取硬件数据,并上报给核心层。input_dev(输入设备结构体)
核心层(Input Core)管理所有输入设备和处理程序,负责设备与处理程序的匹配、事件分发。内核模块input.c
事件处理层接收核心层分发的事件,为应用层提供访问接口(如字符设备节点)。input_handler(处理程序)

分层优势:硬件适配层与事件处理层解耦——同一类处理程序(如evdev)可适配所有符合规范的输入设备,同一设备也可被多个处理程序匹配(如触摸屏既支持evdev也支持tslib)。

三、Input子系统核心结构体

Input子系统的核心是三个结构体:input_dev(设备)、input_handler(处理程序)、input_handle(匹配关系),三者协同实现设备管理与事件分发。
在这里插入图片描述

在这里插入图片描述

1. 核心结构体定义与功能

(1)input_dev:描述输入设备

input_dev是硬件适配层的核心,代表一个具体的输入设备,需由驱动开发者初始化并注册到内核。

struct input_dev {const char *name;          // 设备名称(如"GPIO Key")const char *phys;          // 物理路径(如"/dev/input/event0")const char *uniq;          // 唯一标识(可选)struct input_id id;        // 设备ID(用于与handler匹配)unsigned long evbit[NBITS(EV_MAX)];  // 支持的事件类型(如EV_KEY、EV_SYN)unsigned long keybit[NBITS(KEY_MAX)];// 支持的按键(如KEY_ENTER、KEY_VOLUMEUP)unsigned long relbit[NBITS(REL_MAX)];// 支持的相对坐标(如鼠标REL_X、REL_Y)unsigned long absbit[NBITS(ABS_MAX)];// 支持的绝对坐标(如触摸屏ABS_X、ABS_Y)struct list_head h_list;   // 关联的input_handle链表(设备匹配的handler)struct list_head node;     // 用于挂载到内核的input_dev_list链表// ... 其他成员(如中断号、私有数据等)
};

关键字段说明

  • evbit:必须初始化,指定设备支持的事件类型(如EV_KEY表示按键事件,EV_SYN表示同步事件)。
  • keybit/relbit/absbit:根据设备类型初始化(如按键设备需设置keybit,鼠标需设置relbit)。
  • id:包含bustype(总线类型,如BUS_HOST)、vendor(厂商ID)、product(产品ID),用于与input_handlerid_table匹配。
(2)input_handler:描述事件处理程序

input_handler是事件处理层的核心,负责接收设备上报的事件,并为应用层提供访问接口(如创建/dev/input/eventX节点)。内核已实现多个通用input_handler,如:

  • evdev:通用事件处理程序,支持所有输入设备,生成/dev/input/eventX节点,是最常用的handler。
  • kbd:专门处理键盘事件,用于控制台键盘。
  • mousedev:专门处理鼠标事件。
struct input_handler {void (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);// 批量处理事件(可选,优先级高于event)void (*events)(struct input_dev *dev, const struct input_event *ev, unsigned int count);// 事件过滤(可选,可修改或丢弃事件)int (*filter)(struct input_dev *dev, 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 struct input_device_id *id_table;  // 支持的设备ID列表(用于匹配input_dev)struct list_head h_list;                 // 关联的input_handle链表(handler匹配的设备)struct list_head node;                   // 用于挂载到内核的input_handler_list链表// ... 其他成员(如名称、私有数据等)
};

关键字段说明

  • id_table:静态匹配规则,列出handler支持的input_devidbustype/vendor/product),是最常用的匹配方式。
  • connect:匹配成功后调用,核心工作是创建input_handle并注册字符设备(如evdevconnect中创建/dev/input/eventX)。
  • event/events:接收input_dev上报的事件,最终分发给应用层。
(3)input_handle:关联设备与处理程序

input_handleinput_devinput_handler的“桥梁”,当两者匹配成功后,内核会创建input_handle并关联到两者的h_list链表中。

struct input_handle {struct input_dev *dev;     // 关联的输入设备struct input_handler *handler;  // 关联的处理程序struct list_head d_node;   // 挂载到input_dev->h_list的节点struct list_head h_node;   // 挂载到input_handler->h_list的节点// ... 其他成员(如私有数据)
};

作用:通过input_handle,内核可快速找到设备对应的处理程序(或处理程序对应的设备),实现事件的精准分发。

2. 三者关系图解

input_devinput_handler通过input_handle建立多对多关系:一个设备可被多个handler匹配(如触摸屏同时被evdevtslib处理),一个handler也可匹配多个设备(如evdev处理所有按键设备)。

内核维护两个全局链表:

  • input_dev_list:所有已注册的input_dev
  • input_handler_list:所有已注册的input_handler

四、Input设备驱动注册流程:从硬件到内核

Input设备驱动的开发核心是初始化input_dev并注册到内核,同时完成硬件配置(如中断申请)。以下以“基于设备树的Platform驱动”为例,详细拆解注册流程。

在这里插入图片描述

1. 注册流程总览

  1. 设备树配置:定义输入设备节点,指定硬件资源(如中断号、GPIO、设备ID等)。
  2. Platform驱动编写:实现probe函数(驱动与设备匹配时执行),完成硬件初始化、input_dev创建与注册。
  3. 内核匹配与连接input_dev注册后,内核自动与input_handler匹配,调用connect函数建立关联并创建字符设备节点。

2. 步骤1:设备树节点配置

在设备树中添加输入设备节点(以GPIO按键为例),指定中断、设备ID等信息,供驱动解析:

/* 设备树节点:GPIO按键设备 */
gpio_key: gpio-key@0 {compatible = "my-gpio-key";  // 与Platform驱动的of_match_table匹配interrupt-parent = <&gpio1>; // 中断父控制器interrupts = <10 IRQ_TYPE_EDGE_FALLING>; // 中断引脚(GPIO1_10)、下降沿触发key-code = <KEY_ENTER>;      // 按键对应的键值(回车)linux,code = <EV_KEY>;       // 事件类型(按键事件)status = "okay";
};

关键属性

  • compatible:驱动匹配的“标识符”,需与Platform驱动的of_match_table一致。
  • interrupts:中断资源,驱动需通过platform_get_irq获取。
  • key-code:自定义属性,指定按键对应的Linux键值(如KEY_ENTER对应回车键)。

3. 步骤2:Platform驱动实现(核心代码)

Platform驱动是Linux中最常用的设备驱动框架,通过probe函数完成设备初始化。以下是GPIO按键的Input驱动核心代码:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_gpio.h>// 驱动私有数据:存储设备相关信息
struct gpio_key_data {struct input_dev *input_dev;  // 输入设备结构体int irq;                      // 中断号unsigned int key_code;        // 键值(如KEY_ENTER)
};// 中断服务程序:按键触发中断时执行,读取硬件状态并上报事件
static irqreturn_t gpio_key_irq_handler(int irq, void *dev_id) {struct gpio_key_data *data = dev_id;// 1. 上报按键事件(EV_KEY:事件类型,key_code:键值,1:按下,0:释放)input_event(data->input_dev, EV_KEY, data->key_code, 1);// 2. 上报同步事件(告知应用层:本轮事件已结束)input_sync(data->input_dev);// (可选)模拟按键释放(实际硬件需根据GPIO电平判断,此处简化)msleep(20); // 消抖input_event(data->input_dev, EV_KEY, data->key_code, 0);input_sync(data->input_dev);return IRQ_HANDLED;
}// probe函数:驱动与设备匹配时执行,初始化硬件和input_dev
static int gpio_key_probe(struct platform_device *pdev) {struct gpio_key_data *data;struct device_node *node = pdev->dev.of_node;int ret;// 1. 分配私有数据内存data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);if (!data) return -ENOMEM;platform_set_drvdata(pdev, data); // 绑定私有数据到设备// 2. 从设备树解析硬件资源// 2.1 获取键值ret = of_property_read_u32(node, "key-code", &data->key_code);if (ret) {dev_err(&pdev->dev, "Failed to read key-code\n");return ret;}// 2.2 获取中断号data->irq = platform_get_irq(pdev, 0);if (data->irq < 0) {dev_err(&pdev->dev, "Failed to get irq\n");return data->irq;}// 3. 分配并初始化input_devdata->input_dev = devm_input_allocate_device(&pdev->dev);if (!data->input_dev) return -ENOMEM;// 3.1 设置input_dev基本信息data->input_dev->name = "my-gpio-key"; // 设备名称data->input_dev->phys = "gpio-key/input0"; // 物理路径data->input_dev->id.bustype = BUS_HOST; // 总线类型data->input_dev->id.vendor = 0x1234;   // 厂商ID(自定义)data->input_dev->id.product = 0x5678;  // 产品ID(自定义)// 3.2 设置设备支持的事件类型和键值__set_bit(EV_KEY, data->input_dev->evbit); // 支持按键事件__set_bit(data->key_code, data->input_dev->keybit); // 支持的键值// 4. 注册input_dev到内核ret = input_register_device(data->input_dev);if (ret) {dev_err(&pdev->dev, "Failed to register input dev\n");return ret;}// 5. 申请中断(绑定中断服务程序)ret = devm_request_irq(&pdev->dev, data->irq, gpio_key_irq_handler,IRQF_TRIGGER_FALLING, "gpio-key-irq", data);if (ret) {dev_err(&pdev->dev, "Failed to request irq\n");return ret;}dev_info(&pdev->dev, "GPIO key driver probed successfully\n");return 0;
}// remove函数:设备移除时执行(可选,devm_xxx函数会自动释放资源)
static int gpio_key_remove(struct platform_device *pdev) {dev_info(&pdev->dev, "GPIO key driver removed\n");return 0;
}// 设备树匹配表:驱动通过compatible匹配设备
static const struct of_device_id gpio_key_of_match[] = {{ .compatible = "my-gpio-key" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);// Platform驱动结构体
static struct platform_driver gpio_key_driver = {.probe = gpio_key_probe,.remove = gpio_key_remove,.driver = {.name = "gpio-key-driver",.of_match_table = gpio_key_of_match, // 设备树匹配},
};// 模块加载与卸载
module_platform_driver(gpio_key_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("GPIO Key Input Driver");
MODULE_AUTHOR("Your Name");

代码关键步骤

  • 私有数据gpio_key_data存储input_dev、中断号等信息,避免全局变量。
  • 设备树解析:通过of_property_read_u32platform_get_irq获取硬件资源。
  • input_dev初始化
    • devm_input_allocate_device:使用devm_前缀,自动释放内存,避免内存泄漏。
    • __set_bit:设置支持的事件类型(EV_KEY)和键值(如KEY_ENTER)。
  • 中断申请devm_request_irq绑定中断服务程序,按键触发时上报事件。
  • input_dev注册input_register_device将设备加入内核input_dev_list链表,等待与input_handler匹配。

4. 步骤3:input_dev与input_handler的匹配逻辑

input_devinput_handler注册时,内核会触发匹配流程,具体如下:

(1)注册input_dev:input_register_device
int input_register_device(struct input_dev *dev) {// ... 省略其他初始化 ...// 1. 将input_dev加入全局链表input_dev_listlist_add_tail(&dev->node, &input_dev_list);// 2. 遍历所有已注册的input_handler,尝试匹配list_for_each_entry(handler, &input_handler_list, node) {input_attach_handler(dev, handler); // 执行匹配}// ... 省略其他逻辑 ...
}
(2)注册input_handler:input_register_handler
int input_register_handler(struct input_handler *handler) {// ... 省略其他初始化 ...// 1. 将input_handler加入全局链表input_handler_listlist_add_tail(&handler->node, &input_handler_list);// 2. 遍历所有已注册的input_dev,尝试匹配list_for_each_entry(dev, &input_dev_list, node) {input_attach_handler(dev, handler); // 执行匹配}// ... 省略其他逻辑 ...
}
(3)匹配核心函数:input_attach_handler

该函数实现input_devinput_handler的匹配判断,流程如下:

  1. 检查id_table匹配:判断input_dev->id是否在input_handler->id_table中(通过input_match_device函数)。
  2. 检查match函数(可选):若input_handler定义了match函数,需额外调用该函数进行复杂匹配(如根据设备特殊属性判断)。
  3. 匹配成功则调用connect:若上述两步均通过,调用input_handler->connect函数建立关联。
static int input_attach_handler(struct input_dev *dev, struct input_handler *handler) {const struct input_device_id *id;int error;// 1. 第一步:通过id_table匹配(必须满足)id = input_match_device(handler, dev);if (!id) return -ENODEV;// 2. 第二步:若有match函数,执行额外匹配(可选)if (handler->match && !handler->match(handler, dev))return -ENODEV;// 3. 匹配成功,调用connect函数error = handler->connect(handler, dev, id);if (error && error != -ENODEV)dev_err(&dev->dev, "Failed to connect handler %s\n", handler->name);return error;
}
(4)匹配规则总结
匹配场景判断逻辑
match函数仅需input_dev->idinput_handler->id_table中即可匹配
match函数先满足id_table匹配,再通过match函数的自定义逻辑(如设备树属性检查)
示例(evdev handler)evdevid_tableEVDEV_ID_TABLE,支持所有bustype,因此几乎所有input_dev都能匹配

5. 步骤4:匹配成功后的connect函数执行

以最常用的evdev(通用事件处理程序)为例,其connect函数(evdev_connect)的核心工作的是:

  1. 创建input_handle:关联input_devinput_handler,并加入两者的h_list链表。
  2. 注册字符设备:创建/dev/input/eventX节点(X为设备编号),供应用层访问。
static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id) {struct evdev *evdev; // evdev私有数据结构int error, minor;// 1. 分配evdev内存(包含input_handle和cdev)evdev = kzalloc(sizeof(*evdev), GFP_KERNEL);if (!evdev) return -ENOMEM;// 2. 初始化input_handle,关联dev和handlerevdev->handle.dev = input_get_device(dev); // 获取input_devevdev->handle.handler = handler;           // 关联input_handlerevdev->handle.private = evdev;             // 绑定私有数据INIT_LIST_HEAD(&evdev->client_list);       // 初始化客户端链表(多APP访问)init_waitqueue_head(&evdev->wait);         // 初始化等待队列(用于read/poll)// 3. 注册input_handle到内核error = input_register_handle(&evdev->handle);if (error) goto err_free_evdev;// 4. 分配字符设备次设备号(minor)minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);if (minor < 0) {error = minor;goto err_unregister_handle;}evdev->minor = minor;// 5. 初始化并注册字符设备cdev_init(&evdev->cdev, &evdev_fops); // 绑定文件操作结构体evdev_fopsevdev->cdev.owner = handler->owner;error = cdev_device_add(&evdev->cdev, &evdev->dev); // 注册字符设备if (error) goto err_release_minor;// 6. 创建/dev/input/eventX节点(由udev自动完成,内核提供设备信息)dev_info(&dev->dev, "evdev: initialized device as %s (minor %d)\n",dev_name(&evdev->dev), minor);return 0;// 错误处理(省略)
err_release_minor:input_release_minor(minor);
err_unregister_handle:input_unregister_handle(&evdev->handle);
err_free_evdev:kfree(evdev);return error;
}

关键结果connect执行完成后,应用层可通过/dev/input/eventX(如/dev/input/event0)访问该输入设备,后续即可通过read/poll读取事件。

五、输入事件读取流程:从硬件中断到应用层

当输入设备产生动作(如按键按下、鼠标移动)时,事件会通过“中断→内核上报→应用读取”的流程传递,具体如下:

1. 流程总览

  1. 硬件中断触发:设备动作(如按键按下)触发硬件中断,执行中断服务程序(ISR)。
  2. 内核事件上报:ISR中读取硬件数据,调用input_event上报事件到核心层。
  3. 事件分发到handler:核心层通过input_handle找到匹配的input_handler(如evdev),调用其event函数。
  4. 应用层读取:应用通过read函数从/dev/input/eventX读取事件数据。

2. 步骤1:硬件中断与事件上报(ISR)

以GPIO按键为例,按键按下时触发中断,执行之前注册的gpio_key_irq_handler

static irqreturn_t gpio_key_irq_handler(int irq, void *dev_id) {struct gpio_key_data *data = dev_id;int gpio_val;// 1. (可选)读取GPIO电平,判断按键状态(按下/释放)gpio_val = gpio_get_value(data->gpio); int key_pressed = (gpio_val == 0) ? 1 : 0; // 假设低电平为按下// 2. 上报按键事件:type=EV_KEY,code=键值,value=1(按下)/0(释放)input_event(data->input_dev, EV_KEY, data->key_code, key_pressed);// 3. 上报同步事件:告知应用层“本轮事件已完整,可读取”input_sync(data->input_dev);return IRQ_HANDLED;
}

核心函数

  • input_event:上报单个输入事件,是Input子系统的核心上报接口。
  • input_sync:本质是调用input_event(data->input_dev, EV_SYN, SYN_REPORT, 0),用于同步事件,避免应用层读取到不完整的事件序列。

3. 步骤2:input_event的事件分发逻辑

input_event函数负责将事件从input_dev分发到匹配的input_handler,核心代码如下:

void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) {struct input_handle *handle;// ... 省略事件合法性检查(如设备是否支持该type/code) ...// 遍历input_dev关联的所有input_handle(即所有匹配的handler)list_for_each_entry(handle, &dev->h_list, d_node) {// 1. 优先调用handler的filter函数(可选,用于过滤/修改事件)if (handle->handler->filter) {if (handle->handler->filter(dev, handle, type, code, value))continue; // 若filter返回1,跳过该handler}// 2. 分发事件到handler:优先调用events(批量处理),无则调用event(单个处理)if (handle->handler->events) {handle->handler->events(dev, &event, 1); // 批量处理(count=1)} else if (handle->handler->event) {handle->handler->event(dev, type, code, value); // 单个处理}}
}

关键逻辑

  • 事件会分发给input_dev的所有匹配handler(如触摸屏同时分发给evdevtslib)。
  • filter函数可拦截事件(如屏蔽特定按键),events函数用于高效处理批量事件(如鼠标连续移动)。

4. 步骤3:evdev的事件存储与应用读取

evdev为例,其event函数(evdev_event)会将事件存储到环形缓冲区,供应用层通过read读取:

(1)evdev_event:存储事件到缓冲区
static void evdev_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) {struct evdev *evdev = handle->private; // 从handle获取evdev私有数据struct evdev_client *client;           // 对应一个应用层客户端(open一次创建一个)struct input_event event;              // 事件结构体// 1. 构造input_event(包含时间戳、type、code、value)event.time = ktime_get_real_ts64();event.type = type;event.code = code;event.value = value;// 2. 遍历所有客户端(多个APP同时打开同一设备),将事件写入各自缓冲区rcu_read_lock();list_for_each_entry_rcu(client, &evdev->client_list, node) {evdev_feed_event(client, &event); // 写入客户端缓冲区}rcu_read_unlock();// 3. 唤醒等待队列(若有APP调用poll/read等待事件)wake_up_interruptible(&evdev->wait);
}
(2)应用层read调用:evdev_read

当应用调用read(/dev/input/event0, buf, sizeof(struct input_event) * N)时,会触发evdevread函数(evdev_read):

static ssize_t evdev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {struct evdev_client *client = file->private_data; // 客户端私有数据struct evdev *evdev = client->evdev;struct input_event event;size_t read = 0;// 1. 若缓冲区无数据且非非阻塞模式,休眠等待事件if (client->head == client->tail && evdev->exist && !(file->f_flags & O_NONBLOCK)) {if (wait_event_interruptible(evdev->wait, client->head != client->tail || !evdev->exist))return -ERESTARTSYS; // 被信号中断}// 2. 若设备已移除,返回0if (!evdev->exist)return 0;// 3. 从缓冲区读取事件,复制到用户空间while (read + sizeof(event) <= count && client->head != client->tail) {event = client->buffer[client->tail]; // 从环形缓冲区取事件client->tail = (client->tail + 1) % EVDEV_BUFFER_SIZE; // 移动尾指针if (copy_to_user(buf + read, &event, sizeof(event)))return -EFAULT; // 复制失败read += sizeof(event);}return read; // 返回读取的字节数
}

应用层示例代码:读取/dev/input/event0的按键事件

#include <stdio.h>
#include <fcntl.h>
#include <linux/input.h>int main() {int fd;struct input_event ev;// 打开输入设备节点fd = open("/dev/input/event0", O_RDONLY);if (fd < 0) {perror("open /dev/input/event0 failed");return -1;}// 循环读取事件while (1) {read(fd, &ev, sizeof(ev)); // 阻塞读取// 打印事件信息printf("type: %d, code: %d, value: %d\n", ev.type, ev.code, ev.value);// 若为按键释放事件且是回车键,退出循环if (ev.type == EV_KEY && ev.code == KEY_ENTER && ev.value == 0)break;}close(fd);return 0;
}

六、QEMU调试Input设备:模拟与验证

在这里插入图片描述

在开发Input驱动时,通常需要通过QEMU模拟嵌入式环境进行调试,以下是关键配置与操作步骤:

1. QEMU Input设备模拟原理

QEMU支持模拟多种输入设备(如键盘、鼠标、触摸屏),并通过tslib(触摸屏库)处理触摸事件。其核心逻辑是:

  • QEMU将宿主机的输入事件(如鼠标点击、键盘按键)转发给虚拟机内的Input子系统。
  • 虚拟机内的Input驱动(如evdev)接收事件,生成/dev/input/eventX节点,供应用层(如Qt、TSLIB应用)访问。

2. QEMU启动参数配置(以ARM架构为例)

启动QEMU时,需指定模拟的输入设备(如触摸屏),关键参数如下:

qemu-system-arm \-M vexpress-a9 \                  # 模拟ARM vexpress-a9开发板-kernel zImage \                  # 内核镜像-dtb vexpress-v2p-ca9.dtb \       # 设备树-rootfs rootfs.ext4 \             # 根文件系统-append "root=/dev/mmcblk0 rw console=ttyAMA0,115200" \ # 内核启动参数-serial stdio \                   # 串口重定向到宿主机终端-usb \                            # 启用USB控制器-device usb-mouse \               # 模拟USB鼠标-device usb-kbd \                 # 模拟USB键盘-device virtio-touchscreen-pci \  # 模拟PCI触摸屏(需内核支持virtio驱动)

参数说明

  • -device usb-mouse/usb-kbd:模拟USB鼠标/键盘,内核会自动加载usbhid驱动,生成/dev/input/eventX节点。
  • -device virtio-touchscreen-pci:模拟触摸屏,需内核启用CONFIG_VIRTIO_INPUT配置,配合tslib使用。

3. QEMU内验证Input设备

(1)查看已注册的Input设备

通过/proc/bus/input/devices文件可查看所有已注册的Input设备信息:

cat /proc/bus/input/devices

示例输出(USB键盘):

I: Bus=0003 Vendor=0627 Product=0001 Version=0110
N: Name="QEMU USB Keyboard"
P: Phys=usb-0000:00:04.0-1/input0
S: Sysfs=/devices/pci0000:00/0000:00:04.0/usb1/1-1/1-1:1.0/0003:0627:0001.0001/input/input0
U: Uniq=
H: Handlers=sysrq kbd event0 
B: PROP=0
B: EV=120013
B: KEY=10000 0 0 0 1007b f902f bf0047f dfffe 1cfffff ffffffff fffffffe
B: MSC=10
B: LED=7
  • Handlers=event0:表示该设备对应/dev/input/event0节点。
  • EV=120013:表示支持的事件类型(EV_KEYEV_MSCEV_LED等)。
(2)使用hexdump读取事件

通过hexdump工具可直接读取/dev/input/eventX的事件数据,验证设备是否正常工作:

hexdump /dev/input

六、QEMU调试Input设备:模拟与验证

(2)使用hexdump读取事件

通过hexdump工具可直接读取/dev/input/eventX的事件数据,验证设备是否正常工作:

hexdump /dev/input/event0  # event0对应上述USB键盘

按下键盘上的“Enter”键,会输出类似以下数据:

0000000 0a4b 5c8f 0000 0000 0001 001c 0001 0000
0000010 0a4b 5c8f 0000 0000 0000 0000 0000 0000
0000020 0a4b 5c90 0000 0000 0001 001c 0000 0000
0000030 0a4b 5c90 0000 0000 0000 0000 0000 0000

数据解析(对应struct input_event结构):

  • 前8字节:时间戳(tv_sectv_usec),如0a4b 5c8f表示秒,0000 0000表示微秒。
  • 第9-12字节:事件类型(type),0001对应EV_KEY(按键事件)。
  • 第13-16字节:事件编码(code),001c对应KEY_ENTER(回车键)。
  • 第17-20字节:事件值(value),0001表示按键按下,0000表示按键释放,0002表示按键重复(长按)。
  • 后续8字节:同步事件(type=0000code=0000),表示本轮事件结束。
(3)使用tslib调试触摸屏

若QEMU模拟了触摸屏(如通过-device virtio-touchscreen-pci参数),需通过tslib工具验证触摸功能。tslib是开源的触摸屏校准与事件处理库,可屏蔽不同触摸屏的硬件差异,提供统一的触摸接口。

步骤1:安装tslib
在根文件系统中预装tslib(可通过Buildroot或Yocto编译),安装后包含以下工具:

  • ts_calibrate:触摸屏校准工具。
  • ts_test:触摸屏测试工具(显示触摸坐标、滑动轨迹)。
  • ts_print:打印触摸事件原始数据。

步骤2:配置tslib环境变量
在QEMU终端中设置tslib的输入设备节点(通常为/dev/input/event1,需根据实际设备调整):

export TSLIB_TSDEVICE=/dev/input/event1  # 触摸设备节点
export TSLIB_CONFFILE=/etc/ts.conf       # tslib配置文件路径
export TSLIB_PLUGINDIR=/usr/lib/ts       # tslib插件目录
export TSLIB_CALIBFILE=/etc/pointercal   # 校准数据存储文件

步骤3:校准与测试触摸屏

  1. 执行校准

    ts_calibrate
    

    按照提示点击屏幕上的5个校准点,校准完成后会生成/etc/pointercal文件,后续触摸事件会自动应用校准参数。

  2. 测试触摸功能

    ts_test
    

    屏幕会显示触摸坐标(X/Y)和压力值(Pressure),滑动手指可看到轨迹,验证触摸是否正常响应。

4. QEMU退出方法

在QEMU运行过程中,若需退出虚拟机,需执行以下操作:

  1. 同时按住键盘上的 Ctrl + A 组合键(无需松开Ctrl)。
  2. 松开 Ctrl + A 后,单独按下 x 键。
  3. QEMU会立即关闭,并返回宿主机终端。

注意:若直接关闭QEMU窗口,可能导致根文件系统损坏(尤其是采用rw(可写)模式挂载时),建议通过上述命令正常退出。

七、uinput:应用层模拟输入设备

uinput是Linux内核提供的一个特殊驱动模块,允许应用层程序创建虚拟输入设备(如虚拟键盘、虚拟鼠标),并模拟输入事件(如自动按键、模拟鼠标移动)。其核心价值在于无需编写内核驱动,即可通过用户空间代码扩展输入功能(如自动化测试、远程控制)。

在这里插入图片描述

1. uinput工作原理

uinput的本质是一个“用户空间输入设备驱动”,其工作流程如下:

  1. 应用层通过open("/dev/uinput")open("/dev/input/uinput")打开uinput设备节点(需内核启用CONFIG_UINPUT配置)。
  2. 应用层通过ioctl命令配置虚拟设备的属性(如支持的事件类型、键值、坐标范围)。
  3. 应用层通过write函数向uinput写入struct input_event格式的事件数据。
  4. uinput驱动将接收到的事件转发给内核Input子系统,与物理设备的事件处理流程完全一致(即分发给input_handler,供其他应用读取)。

2. uinput核心操作步骤

(1)内核配置要求

使用uinput前,需确保内核已启用uinput模块:

  • 编译内核时,开启配置项:Device Drivers → Input device support → User level driver support → <M> User input device driver(可编译为模块uinput.ko,或直接编译进内核)。
  • 若为模块,需手动加载:modprobe uinput,加载后会生成/dev/uinput/dev/input/uinput节点。
(2)应用层操作流程(C语言示例)

以下以“创建虚拟键盘并模拟按下‘Enter’键”为例,演示uinput的核心用法:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/uinput.h>
#include <linux/input.h>#define UINPUT_DEV "/dev/uinput"  // uinput设备节点// 函数:创建虚拟键盘设备
int create_virtual_keyboard(int fd) {struct uinput_setup setup;int ret;// 1. 启用键盘支持的事件类型(EV_KEY:按键事件,EV_SYN:同步事件)ret = ioctl(fd, UI_SET_EVBIT, EV_KEY);if (ret < 0) { perror("UI_SET_EVBIT EV_KEY failed"); return ret; }ret = ioctl(fd, UI_SET_EVBIT, EV_SYN);if (ret < 0) { perror("UI_SET_EVBIT EV_SYN failed"); return ret; }// 2. 启用需要模拟的键值(此处为KEY_ENTER:回车键)ret = ioctl(fd, UI_SET_KEYBIT, KEY_ENTER);if (ret < 0) { perror("UI_SET_KEYBIT KEY_ENTER failed"); return ret; }// 3. 配置虚拟设备信息(名称、总线类型、厂商ID、产品ID)memset(&setup, 0, sizeof(setup));setup.id.bustype = BUS_USB;          // 总线类型(模拟USB设备)setup.id.vendor = 0x1234;            // 厂商ID(自定义)setup.id.product = 0x5678;           // 产品ID(自定义)snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "Virtual Keyboard"); // 设备名称ret = ioctl(fd, UI_DEV_SETUP, &setup);if (ret < 0) { perror("UI_DEV_SETUP failed"); return ret; }// 4. 注册虚拟设备(此时内核会为其分配event节点,如/dev/input/event2)ret = ioctl(fd, UI_DEV_CREATE);if (ret < 0) { perror("UI_DEV_CREATE failed"); return ret; }printf("Virtual keyboard created successfully (name: %s)\n", setup.name);return 0;
}// 函数:模拟按键事件(按下/释放)
void simulate_key_event(int fd, int key_code, int value) {struct input_event ev;// 1. 构造按键事件memset(&ev, 0, sizeof(ev));ev.type = EV_KEY;    // 事件类型:按键ev.code = key_code;  // 键值(如KEY_ENTER)ev.value = value;    // 0:释放,1:按下,2:重复gettimeofday(&ev.time, NULL);  // 设置时间戳(可选,内核会自动补全)write(fd, &ev, sizeof(ev));    // 写入事件// 2. 构造同步事件(告知内核:本轮事件结束)memset(&ev, 0, sizeof(ev));ev.type = EV_SYN;    // 事件类型:同步ev.code = SYN_REPORT; // 同步类型:报告事件ev.value = 0;gettimeofday(&ev.time, NULL);write(fd, &ev, sizeof(ev));
}int main() {int fd;// 1. 打开uinput设备节点(O_RDWR:读写模式)fd = open(UINPUT_DEV, O_RDWR);if (fd < 0) {perror("open " UINPUT_DEV " failed");exit(EXIT_FAILURE);}// 2. 创建虚拟键盘if (create_virtual_keyboard(fd) != 0) {close(fd);exit(EXIT_FAILURE);}// 3. 模拟按键:按下Enter键(1秒后释放)printf("Simulating Enter key press...\n");simulate_key_event(fd, KEY_ENTER, 1);  // 按下sleep(1);                              // 保持按下1秒simulate_key_event(fd, KEY_ENTER, 0);  // 释放printf("Enter key released\n");// 4. 销毁虚拟设备(可选,关闭fd也会自动销毁)ioctl(fd, UI_DEV_DESTROY);close(fd);return 0;
}

3. 代码关键解析

(1)核心ioctl命令

uinput通过ioctl命令配置虚拟设备,常用命令如下:

命令功能描述
UI_SET_EVBIT启用虚拟设备支持的事件类型(如EV_KEYEV_REL(相对坐标)、EV_ABS(绝对坐标))。
UI_SET_KEYBIT启用虚拟设备支持的键值(如KEY_ENTERKEY_A,需先启用EV_KEY)。
UI_SET_RELBIT启用虚拟设备支持的相对坐标(如REL_XREL_Y,用于模拟鼠标)。
UI_SET_ABSBIT启用虚拟设备支持的绝对坐标(如ABS_XABS_Y,用于模拟触摸屏)。
UI_DEV_SETUP配置虚拟设备的基本信息(struct uinput_setup,包含idname)。
UI_DEV_CREATE注册虚拟设备到内核Input子系统,此时会生成/dev/input/eventX节点。
UI_DEV_DESTROY从内核中移除虚拟设备(关闭fd前建议调用,避免资源泄漏)。
(2)事件写入格式

应用层通过write函数向uinput写入的事件,必须符合struct input_event格式(与内核Input子系统的事件格式一致):

struct input_event {struct timeval time;  // 事件时间戳(可选,内核可自动补全)__u16 type;           // 事件类型(如EV_KEY、EV_SYN)__u16 code;           // 事件编码(如KEY_ENTER、REL_X)__s32 value;          // 事件值(如1=按下、0=释放;坐标值等)
};
  • 每发送一个功能事件(如按键、鼠标移动),必须跟随一个EV_SYN同步事件,否则内核无法识别事件边界。

4. uinput应用场景

  1. 自动化测试:模拟用户输入(如自动点击按钮、输入文本),用于测试GUI应用(如Qt、Android应用)。
  2. 远程控制:接收远程端的输入指令(如网络消息),通过uinput转换为本地输入事件,实现远程操控设备。
  3. 辅助工具:开发自定义输入设备(如通过语音识别生成键盘事件、通过手柄模拟鼠标)。
  4. 调试Input子系统:无需硬件设备,即可验证input_handler(如evdev)的事件处理逻辑。
http://www.dtcms.com/a/456866.html

相关文章:

  • 探索Linux:开源世界的钥匙
  • GitHub 热榜项目 - 日榜(2025-10-08)
  • 手写Function.prototype.bind:从原理到完整实现
  • 百度做网站的公司施工企业的施工现场消防安全责任人应是
  • 做电商网站报价网站开发工程师需要会写什么
  • (3)容器布局进阶:Spacer、Divider、Frame 与 Alignment
  • 墨西哥证券交易所(BMV)等多个交易所股票数据API对接文档
  • 【数据分析与可视化】2025年一季度金融业主要行业资产、负债、权益结构与增速对比
  • app网站建设阿里巴巴卓拙科技做网站吗
  • 乌苏市城乡建设局网站北京朝阳区邮政编码
  • 个人用云计算学习笔记 --18(NFS 服务器、iSCSI 服务器)
  • 智能制造——解读MES在各行业中的需求与解决方案【附全文阅读】
  • 老题新解|棋盘覆盖
  • 网站可不可以做自己的专利东莞沙田网站建设
  • Redis Hash 全解析:从入门到精通,解锁高性能对象存储的钥匙
  • 14.排序
  • Python自动化实战第一篇: 自动化备份100+台服务器Web 配置
  • 第五十二章 ESP32S3 UDP 实验
  • [鹤城杯 2021]Misc2
  • 山东省旅游网站建设网络设计是干什么的工作
  • 基于 ZYNQ ARM+FPGA+AI YOLOV4 的电网悬垂绝缘子缺陷检测系统的研究
  • 开源 C++ QT QML 开发(十二)通讯--TCP客户端
  • 【密码学实战】openHiTLS pkeyutl命令行:公钥实用工具(加解密、密钥交换)
  • 做标书有什么好的网站吗网站改版不收录
  • JDK17和JDK8的 G1
  • win10安装conda环境
  • TDengine 浮点数新编码 BSS 用户手册
  • mybatis call存储过程,out的参数怎么返回
  • 今日八股——JVM篇
  • 【论文阅读】REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS