跳到主要内容

Linux input 子系统介绍及实践

1. input 子系统概述

Linux input 子系统为各种输入设备(如按键、键盘、鼠标、触摸屏、遥控器等)提供统一的驱动开发和事件管理框架。它将硬件输入事件标准化,极大简化了驱动开发和应用集成。

1.1. 架构分层与工作流程


  • 输入设备驱动层:负责采集硬件输入(如 GPIO 按键、I2C 触摸屏),通过 input core 上报事件。

  • input core 层:内核核心,负责事件分发、设备注册、事件缓冲、同步等。

  • 事件处理/应用层:用户空间通过 /dev/input/eventX 读取事件,可用 evtest、cat、应用程序等工具处理。

1.2. 内核实现原理

Linux input 子系统基于一套精心设计的内核架构,实现了灵活而高效的输入事件处理流程:

1.2.1. 输入设备驱动与内核连接

驱动通过 input_allocate_device() 分配 input_dev 结构体,设置设备支持的事件类型和事件代码后,调用 input_register_device() 将设备注册到内核。

struct input_dev {
const char *name;
const char *phys;
struct input_id id;

unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 支持的事件类型位图 */
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 支持的按键位图 */
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 支持的相对坐标位图 */
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 支持的绝对坐标位图 */

/* 其他设备属性与状态信息 */
void *private; /* 设备私有数据 */
};

注册过程中,内核会为该输入设备在 /dev/input/ 目录下创建一个 eventX 设备节点,并通过 sysfs 提供设备信息。

1.2.2. 输入事件上报机制

驱动层通过一系列 input_report_* 函数上报输入事件:

/* 上报按键事件 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);
/* 上报相对坐标事件(如鼠标移动) */
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);

每次上报完一组相关事件后,必须调用 input_sync() 同步事件,通知内核一个完整事件已产生:

void input_sync(struct input_dev *dev);

1.2.3. 事件处理流程

内核收到事件后,会转换为标准格式的 input_event 结构体:

struct input_event {
struct timeval time; /* 事件发生的时间戳 */
__u16 type; /* 事件类型 EV_* */
__u16 code; /* 事件代码 KEY_*, ABS_* 等 */
__s32 value; /* 事件值 */
};

这些事件被传递给输入事件处理层,经过转换和分发后,被写入到相应设备文件的缓冲区中。用户空间应用程序可通过 read() 系统调用从 /dev/input/eventX 设备文件读取这些事件。

1.3. 主要数据结构与接口

devm_input_allocate_device() / input_register_device() / input_unregister_device():input 设备的分配、注册、注销。

gpiod_get() / gpiod_put() / gpiod_get_value():GPIO 描述符获取与操作(推荐新内核用法)。

timer_list / timer_setup() / mod_timer():定时器机制,常用于轮询式输入检测。

1.4. 事件类型与代码

Input 子系统定义了多种标准事件类型,通过预定义的宏使用:

  • EV_KEY:按键事件,代码如 KEY_ENTER、KEY_POWER 等

  • EV_REL:相对坐标事件,如 REL_X、REL_Y(鼠标移动)

  • EV_ABS:绝对坐标事件,如 ABS_X、ABS_Y(触摸屏位置)

  • EV_SW:开关事件,如 SW_LID(笔记本盖子)

  • EV_SYN:同步事件,标志一次完整输入事件的结束

事件类型和代码值定义在内核头文件 path/to/your/linux-6.6/include/uapi/linux/input-event-codes.h 中。

1.5. 按键 input 子系统驱动开发流程

// 设置设备树匹配表
static const struct of_device_id bianbu_input[] = {
{ .compatible = "bianbu,input-test" },
{ },
};

// 定义platform_driver结构体
static struct platform_driver chip_demo_gpio_driver = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "bianbu_input_test",
.of_match_table = bianbu_input,
},
};

// 从设备树获取GPIO资源
input_gpio = gpiod_get(&pdev->dev, NULL, GPIOD_IN);
if (IS_ERR(input_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO for test\n");
return PTR_ERR(input_gpio);
}

// 分配input_dev结构体
test_input_dev = devm_input_allocate_device(&pdev->dev);
if (!test_input_dev) {
gpiod_put(input_gpio);
return -ENOMEM;
}

// 配置input设备信息
test_input_dev->name = "bianbu_input";
test_input_dev->phys = "bianbu/gpio72";
test_input_dev->id.bustype = BUS_HOST;

// 设置支持的事件类型和按键类型
__set_bit(EV_KEY, test_input_dev->evbit); // 支持按键事件
__set_bit(KEY_0, test_input_dev->keybit); // 支持KEY_0按键

// 注册input设备到内核
err = input_register_device(test_input_dev);
if (err) {
gpiod_put(input_gpio);
return err;
}

// 定时器轮询方式
static void poll_gpio_func(struct timer_list *t)
{
int value = gpiod_get_value(input_gpio);
if (value != last_key_state) {
// 上报按键事件
input_report_key(test_input_dev, KEY_0, value);
input_sync(test_input_dev); // 同步事件
last_key_state = value;
}
mod_timer(&poll_timer, jiffies + POLL_INTERVAL);
}

// 初始化定时器
timer_setup(&poll_timer, poll_gpio_func, 0);
mod_timer(&poll_timer, jiffies + POLL_INTERVAL);

2. input 驱动开发实例与实验流程

说明:本次实验采用 GPIO72 引脚为例。

2.1. 设备树配置

path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dts 根节点下添加:

/{
...
input_test: input_test@0 {
compatible = "bianbu,input-test";
gpios = <&gpio 72 0>;
status = "okay";
};
};

启动 Docker。

docker start cross-build-user
docker exec -it cross-build-user bash

进入 linux-6.6 目录重新编译 dtb 文件。

cd cross-builder/linux-6.6
make dtbs

在 MUSE Pi Pro 上执行下面命令查看 ip:

ifconfig

如下图所示,ip 为 10.0.91.67


执行下面命令将编译好的 k1-x_MUSE-Pi-Pro.dtb 文件传输到 MUSE Pi Pro 板子上。

:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

sudo scp arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dtb bianbu@10.0.91.67:/home/bianbu

首次通过 ssh 连接 MUSE Pi Pro 会弹出连接确认,键盘输入 yes ,接着再输入密码回车即可。


在 MUSE Pi Pro 上执行以下命令进行替换:

sudo cp ~/k1-x_MUSE-Pi-Pro.dtb /boot/spacemit/6.6.63

将 MUSE Pi Pro 进行重启:

sudo reboot

2.2. 核心代码 (部分)

// 获取设备树中分配的GPIO(label为NULL,获取gpios第一个,即72号)
input_gpio = gpiod_get(&pdev->dev, NULL, GPIOD_IN);

// 分配input_dev结构体
test_input_dev = devm_input_allocate_device(&pdev->dev);

// 注册input设备
err = input_register_device(test_input_dev);

// 初始化定时器,定期轮询GPIO
timer_setup(&poll_timer, poll_gpio_func, 0);
last_key_state = gpiod_get_value(input_gpio);
mod_timer(&poll_timer, jiffies + POLL_INTERVAL);

// 定时器回调函数
static void poll_gpio_func(struct timer_list *t)
{
int value = gpiod_get_value(input_gpio);
if (value != last_key_state) {
input_report_key(test_input_dev, KEY_0, value);
input_sync(test_input_dev);
last_key_state = value;
}
mod_timer(&poll_timer, jiffies + POLL_INTERVAL);
}

2.3. 运行和测试

2.3.1. 编译驱动程序

下载源码压缩包

input_test.zip

补充:源码文件具体作用请看压缩包里的 README.md 文件。

解压

unzip 03_input_test.zip -d ~/

编译

驱动文件需要在Ubuntu上进行编译,因为SDK在Ubuntu上。修改Makefile里的内核路径为实际路径。

cd ~/03_input_test/
vim Makefile

修改KERN_DIR 的路径,如图所示:


执行以下命令,编译驱动

make

编译成功以后就会生成一个名为 inputdrv.ko 的驱动模块文件。


2.3.2. 加载驱动

将上一小节编译好的 03_input_test 文件夹拷贝到 MUSE Pi Pro 开发板上,执行下面命令。

:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

scp -r ~/03_Linux_input/ bianbu@10.0.91.67:/home/bianbu

在开发板上进入该目录并输入如下命令加载 inputdrv.ko 这个驱动模块。

cd 03_input_test/
sudo insmod inputdrv.ko

2.3.3. 运行测试

将按键一端接到 GPIO72 ,另一端接到 GND


在终端输入下面命令,找到你驱动注册的设备(此处是 Name="bianbu_input"),它下面会有一行 H: Handlers=eventX ,这里的 eventX 就是你要用的设备节点。如图所示:

cat /proc/bus/input/devices

找到 Name="bianbu_input" 的事件号:


当驱动模块加载成功以后使用 inputtest 来测试,输入如下命令:

sudo ./inputtest /dev/input/event7 # 此处实际设备号为event7

按下/松开按键时可看到 KEY_ENTER 事件。如图所示: