输入子系统:从按键到触摸屏的驱动开发实战
按键、触摸屏、摇杆、鼠标——Linux把所有输入设备统一到input子系统里管理。驱动开发者不需要自己造字符设备、自己定义ioctl,只要调input子系统的API,用户空间就能通过/dev/input/eventX读到标准化的事件。本文基于Linux 6.8内核,从三层架构讲起,用GPIO按键和电阻触摸屏两个完整驱动把input子系统的用法串起来。
一、输入子系统架构
Linux输入子系统分三层,驱动开发者只需要写最底层的输入设备驱动,中间层和事件处理层内核已经实现好了。
各层职责
**输入设备驱动(我们写的部分)。
- 初始化硬件,申请中断或轮。- 填充input_dev结构体,声明设备支持哪些事件类型
- 调用input_report_*上报事件,input_sync同步
- 调用input_register_device注册到核心层
**输入核心(内核实现,drivers/input/input.c):
- 管理
input_dev的注册和注销 - 把驱动上报的事件分发到所有绑定的handler
- 提供
input_report_key、input_report_abs等上报API
事件处理器层(内核实现)。
| Handler | 设备节点 | 用途 |
|:--------|:---------|:-----|
| evdev | /dev/input/eventX | 原始事件流,最常用 |
| mousedev | /dev/input/mouseX | 兼容老式鼠标协议 |
| jsdev | /dev/input/jsX | 游戏摇杆 |
日常开发基本只用evdev,它把input_event原封不动传到用户空间。
二、核心数据结构
input_dev
// include/linux/input.h(关键字段)
struct input_dev {
const char *name; // 设备名,显示在proc/bus/input/devices
const char *phys; // 物理路径,如"gpio-keys/input0"
struct input_id 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)]; // 支持的绝对轴位图
struct device dev; // 内嵌device,支持设备模型 struct list_head h_list; // 绑定的handler链表
};
evbit是入口——驱动必须先声明支持哪种事件类型,对应的keybit/relbit/absbit才有意义。
事件类型
| 类型 | 值 | 用途 | 典型code |
|---|---|---|---|
| EV_KEY | 0x01 | 按键/开关 | KEY_POWER, KEY_ENTER, BTN_TOUCH |
| EV_REL | 0x02 | 相对位移 | REL_X, REL_Y, REL_WHEEL |
| EV_ABS | 0x03 | 绝对坐标 | ABS_X, ABS_Y, ABS_PRESSURE |
| EV_SYN | 0x00 | 同步标记 | SYN_REPORT |
EV_SYN是内核自动处理的,驱动调input_sync时内部就上报了SYN_REPORT。
input_event
用户空间从/dev/input/eventX读到的就是这个结构体。
// include/uapi/linux/input.h
struct input_event {
struct timeval time; // 时间戳 __u16 type; // 事件类型:EV_KEY/EV_REL/EV_ABS/EV_SYN
__u16 code; // 事件代码:KEY_ENTER/ABS_X等 __s32 value; // 事件值:按键1按下0松开,坐标值等
};
64位系统上struct timeval24字节,加上type+code+value8字节,再4字节对齐,整个结构体24字节。注:.8内核新增了input_event_compat处理32/64位兼容问题,写用户态程序时用sizeof别硬编码。
三、输入设备驱动开发流程
标准四步
1. devm_input_allocate_device 。 分配input_dev
2. 设置支持的事件类型和code 。 __set_bit / input_set_abs_params
3. input_register_device 。 注册到子系统
4. 上报事件 。 input_report_* + input_sync
第一步用devm_input_allocate_device而不是input_allocate_device,好处是probe失败或驱动移除时自动释放,不用在remove里手动调input_free_device。
完整代码:GPIO按键驱动(input子系统版。
// gpio_key_input.c 。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/gpio/consumer.h>
struct gpio_key_priv {
struct input_dev *input;
struct gpio_desc *key_gpio;
int irq;
u32 key_code;
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key_priv *priv = dev_id;
int val = gpiod_get_value(priv->key_gpio);
// 上报按键事件。按下/松开
input_report_key(priv->input, priv->key_code, val);
input_sync(priv->input);
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct gpio_key_priv *priv;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// 获取GPIO描述。 priv->key_gpio = devm_gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(priv->key_gpio))
return dev_err_probe(dev, PTR_ERR(priv->key_gpio),
"failed to get key gpio\n");
// 从设备树读取按键码,默认KEY_POWER
if (of_property_read_u32(dev->of_node, "linux,code", &priv->key_code))
priv->key_code = KEY_POWER;
// 分配input设备
priv->input = devm_input_allocate_device(dev);
if (!priv->input)
return -ENOMEM;
priv->input->name = "gpio-keys";
priv->input->phys = "gpio-keys/input0";
priv->input->id.bustype = BUS_HOST;
// 声明支持EV_KEY事件和具体按键码
__set_bit(EV_KEY, priv->input->evbit);
__set_bit(priv->key_code, priv->input->keybit);
// 注册input设备
ret = input_register_device(priv->input);
if (ret) {
dev_err(dev, "failed to register input device: %d\n", ret);
return ret;
}
// 申请中断,双边沿触发
priv->irq = gpiod_to_irq(priv->key_gpio);
ret = devm_request_irq(dev, priv->irq, gpio_key_isr,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"gpio-key", priv);
if (ret) {
dev_err(dev, "failed to request irq: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, priv);
return 0;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "myboard,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.driver = {
.name = "gpio-key-input",
.of_match_table = gpio_key_of_match,
},
};
module_platform_driver(gpio_key_driver);
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("GPIO key driver using input subsystem");
MODULE_LICENSE("GPL");
对应的设备树。
/ {
gpio_keys {
compatible = "myboard,gpio-key";
key-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
linux,code = <KEY_ENTER>;
};
};
几个要点。
- __set_bit(EV_KEY, priv->input->evbit)必须调,否则input_register_device返回EINVAL
- input_report_key的value,1表示按下,0表示松开,两边沿都要上报
- input_sync不能漏,它告诉核心层"这批事件发完了,可以提交给handler"
- 用devm_*系列函数,probe失败路径不用手动清理
四、触摸屏驱动
触摸屏上报的是EV_ABS绝对坐标事件,跟按键的EV_KEY是两套位图。
EV_ABS事件
| code | 含义 | 典型值 |
|---|---|---|
| ABS_X | X坐标 | 0~4095 |
| ABS_Y | Y坐标 | 0~4095 |
| ABS_PRESSURE | 按压力度 | 0~255 |
| ABS_MT_POSITION_X | 多点触控X | 0~4095 |
| ABS_MT_POSITION_Y | 多点触控Y | 0~4095 |
单点触控用ABS_X/ABS_Y,多点触控用ABS_MT_*系列。两者可以同时支持,用户空间库会自动适配置
input_set_abs_params
上报绝对坐标前,必须告诉内核这个轴的取值范围:
void input_set_abs_params(struct input_dev *dev,
unsigned int axis,
int min, int max,
int fuzz, int flat);
| 参数 | 含义 | 典型值 |
|---|---|---|
| min | 最小值 | 0 |
| max | 最大值 | 4095 |
| fuzz | 滤波阈值,变化小于此值忽略 | 0(不滤波) |
| flat | 死区范围 | 0 |
fuzz和flat在电阻触摸屏上用处不大,。就行。电容触摸屏抖动大的可以设fuzz=8。
多点触控协议
Linux 6.8支持两种多点触控协议:Type A(无状态)和Type B(有slot)。Type B是主流,所有新驱动都应该用Type B。
Type B的核心思路:每个触点占一个slot,先报告slot编号,再报告该slot的坐标和状态。
// 初始化:声明支持N个触。input_mt_init_slots(input, 10, INPUT_MT_DIRECT);
// 上报流程
input_mt_slot(input, slot_id); // 选中slot
input_mt_report_slot_state(input, MT_TOOL_FINGER, true); // 标记触点按下
input_report_abs(input, ABS_MT_POSITION_X, x); // 上报X
input_report_abs(input, ABS_MT_POSITION_Y, y); // 上报Y
input_mt_report_slot_state(input, MT_TOOL_FINGER, false); // 标记触点抬起
input_sync(input); // 同步
input_mt_init_slots的第三个参数flags。
| flag | 含义 |
|:-----|:-----|
| INPUT_MT_DIRECT | 直接触摸屏(非触控板) |
| INPUT_MT_POINTER | 触控板模式 |
| INPUT_MT_TRACK | 追踪ID模式 |
完整代码:电阻触摸屏驱动
// ts_resistive.c 。电阻触摸屏驱动(单点触控+压力检测)
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
#include <linux/of.h>
#include <linux/iio/consumer.h>
#define TS_MAX_X 4095
#define TS_MAX_Y 4095
#define TS_MAX_P 255
#define TS_MIN_P 0
struct ts_priv {
struct input_dev *input;
struct iio_channel *chan_x;
struct iio_channel *chan_y;
struct iio_channel *chan_pressure;
struct gpio_desc *pen_gpio; // 笔触中断GPIO
int pen_irq;
struct work_struct work;
};
static void ts_read_work(struct work_struct *work)
{
struct ts_priv *priv = container_of(work, struct ts_priv, work);
int x, y, pressure, pen_down;
pen_down = !gpiod_get_value(priv->pen_gpio); // 低电。按下
if (!pen_down) {
// 笔抬起,上报压力0并同。 input_report_key(priv->input, BTN_TOUCH, 0);
input_report_abs(priv->input, ABS_PRESSURE, 0);
input_sync(priv->input);
return;
}
// 读取ADC通道
if (iio_read_channel_raw(priv->chan_x, &x) ||
iio_read_channel_raw(priv->chan_y, &y) ||
iio_read_channel_raw(priv->chan_pressure, &pressure))
return;
// 裁剪到有效范。 x = clamp(x, 0, TS_MAX_X);
y = clamp(y, 0, TS_MAX_Y);
pressure = clamp(pressure, TS_MIN_P, TS_MAX_P);
// 上报触摸事件
input_report_key(priv->input, BTN_TOUCH, 1);
input_report_abs(priv->input, ABS_X, x);
input_report_abs(priv->input, ABS_Y, y);
input_report_abs(priv->input, ABS_PRESSURE, pressure);
input_sync(priv->input);
}
static irqreturn_t ts_pen_isr(int irq, void *dev_id)
{
struct ts_priv *priv = dev_id;
schedule_work(&priv->work);
return IRQ_HANDLED;
}
static int ts_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct ts_priv *priv;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
INIT_WORK(&priv->work, ts_read_work);
// 获取IIO ADC通道(X/Y/Pressure。 priv->chan_x = devm_iio_channel_get(dev, "x");
priv->chan_y = devm_iio_channel_get(dev, "y");
priv->chan_pressure = devm_iio_channel_get(dev, "pressure");
if (IS_ERR(priv->chan_x) || IS_ERR(priv->chan_y) ||
IS_ERR(priv->chan_pressure))
return dev_err_probe(dev, -EINVAL, "failed to get ADC channels\n");
// 获取笔触中断GPIO
priv->pen_gpio = devm_gpiod_get(dev, "pen", GPIOD_IN);
if (IS_ERR(priv->pen_gpio))
return dev_err_probe(dev, PTR_ERR(priv->pen_gpio),
"failed to get pen gpio\n");
// 分配input设备
priv->input = devm_input_allocate_device(dev);
if (!priv->input)
return -ENOMEM;
priv->input->name = "resistive-ts";
priv->input->id.bustype = BUS_HOST;
// 声明支持的事。 __set_bit(EV_KEY, priv->input->evbit);
__set_bit(EV_ABS, priv->input->evbit);
__set_bit(BTN_TOUCH, priv->input->keybit);
// 设置绝对坐标参数
input_set_abs_params(priv->input, ABS_X, 0, TS_MAX_X, 0, 0);
input_set_abs_params(priv->input, ABS_Y, 0, TS_MAX_Y, 0, 0);
input_set_abs_params(priv->input, ABS_PRESSURE, TS_MIN_P, TS_MAX_P, 0, 0);
ret = input_register_device(priv->input);
if (ret)
return dev_err_probe(dev, ret, "failed to register input device\n");
// 申请笔触中断
priv->pen_irq = gpiod_to_irq(priv->pen_gpio);
ret = devm_request_irq(dev, priv->pen_irq, ts_pen_isr,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"ts-pen", priv);
if (ret)
return dev_err_probe(dev, ret, "failed to request pen irq\n");
platform_set_drvdata(pdev, priv);
return 0;
}
static const struct of_device_id ts_of_match[] = {
{ .compatible = "myboard,resistive-ts" },
{ }
};
MODULE_DEVICE_TABLE(of, ts_of_match);
static struct platform_driver ts_driver = {
.probe = ts_probe,
.driver = {
.name = "resistive-ts",
.of_match_table = ts_of_match,
},
};
module_platform_driver(ts_driver);
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Resistive touchscreen driver using input subsystem");
MODULE_LICENSE("GPL");
设备树:
/ {
resistive_ts {
compatible = "myboard,resistive-ts";
pen-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
x-channel = <&adc0 0>;
y-channel = <&adc0 1>;
pressure-channel = <&adc0 2>;
};
};
这个驱动的工作流程:笔按下触发GPIO中断 。workqueue里读ADC 。上报坐标和压。。笔抬起再触发中断 。上报BTN_TOUCH=0。用workqueue而不是在中断里直接读ADC,是因为IIO读取可能睡眠。
五、用户空间接。
/dev/input/eventX
每个注册的input设备对应一个eventX节点。读取得到的是input_event结构体流。
// 用户态读取input事件示例
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
const char *dev = argc > 1 ? argv[1] : "/dev/input/event0";
struct input_event ev;
int fd = open(dev, O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
if (ev.type == EV_KEY)
printf("KEY code=%d value=%d\n", ev.code, ev.value);
else if (ev.type == EV_ABS)
printf("ABS code=%d value=%d\n", ev.code, ev.value);
else if (ev.type == EV_SYN)
printf("SYN_REPORT\n");
}
close(fd);
return 0;
}
查看设备对应哪个eventX。
cat /proc/bus/input/devices
输出里Handlers字段会显示eventX编号。
evtest工具
最常用的调试工具,实时打印所有输入事件:
# 安装
sudo apt install evtest
# 监听指定设备
sudo evtest /dev/input/event2
# 交互式选择设备
sudo evtest
输出格式。
Event: time 1717700000.123456, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 1717700000.123460, -------------- SYN_REPORT ------------
Event: time 1717700000.234567, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 1717700000.234570, -------------- SYN_REPORT ------------
value=1按下,value=0松开,value=2长按重复。
libevdev
比直接read更高级的C库,处理了结构体对齐、位图解析等细节。
#include <libevdev/libevdev.h>
struct libevdev *dev = NULL;
int fd = open("/dev/input/event0", O_RDONLY);
libevdev_new_from_fd(fd, &dev);
printf("设备。 %s\n", libevdev_get_name(dev));
printf("支持EV_KEY: %d\n", libevdev_has_event_type(dev, EV_KEY));
printf("支持KEY_ENTER: %d\n", libevdev_has_event_code(dev, EV_KEY, KEY_ENTER));
struct input_event ev;
while (libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev) == LIBEVDEV_READ_STATUS_SUCCESS) {
// 处理事件
}
libevdev_free(dev);
close(fd);
Python读取按键事件
用python-evdev库,几行代码就能监听输入。
from evdev import InputDevice, categorize, ecodes
dev = InputDevice('/dev/input/event2')
print(f"设备: {dev.name}")
for event in dev.read_loop():
if event.type == ecodes.EV_KEY:
key = ecodes.KEY[event.code] if event.code in ecodes.KEY else f"KEY_{event.code}"
state = "按下" if event.value else "松开"
print(f"{key} {state}")
elif event.type == ecodes.EV_ABS:
abs_axis = ecodes.ABS[event.code] if event.code in ecodes.ABS else f"ABS_{event.code}"
print(f"{abs_axis} = {event.value}")
安装:pip install evdev
六、常见问题
Q1:input_register_device返回EINVAL。
最常见的原因:没设置evbit。input_dev必须至少声明支持一种事件类型,否则注册时内核校验不通过。
// 错误:只设了keybit没设evbit
__set_bit(KEY_ENTER, input->keybit); // 不够
// 正确:先设evbit再设keybit
__set_bit(EV_KEY, input->evbit);
__set_bit(KEY_ENTER, input->keybit);
另一个常见原因:input_dev->name为NULL。.8内核要求name不能为空。
Q2:evtest看不到事件?
按这个清单排查:
input_report_key和input_sync是否都调了?缺sync事件不会提交- 中断是否正常触发?加
printk确认ISR有没有进 evbit和keybit是否设对了?evtest只显示驱动声明支持的事件- 设备节点是否选对了?
cat /proc/bus/input/devices确认
// 最容易漏的:input_sync
input_report_key(input, KEY_ENTER, 1);
// input_sync(input); 。漏了这行,事件不会到达用户空```
### Q3:多点触控不识别。
Type B协议必须先调`input_mt_init_slots`,否则`input_mt_slot`和`input_mt_report_slot_state`都不会生效:
```c
// 注册前初始化MT slot
ret = input_mt_init_slots(input, max_touches, INPUT_MT_DIRECT);
if (ret)
return ret;
// 上报时按顺序调用
input_mt_slot(input, slot);
input_mt_report_slot_state(input, MT_TOOL_FINGER, active);
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
input_sync(input); // 一批slot都报完再sync
另外,input_mt_init_slots会自动设置ABS_MT_SLOT和ABS_MT_TRACKING_ID,不需要手动__set_bit。
七、总结
速查看
| 操作 | API | 备注 |
|---|---|---|
| 分配设备 | devm_input_allocate_device(dev) |
推荐用devm版本 |
| 设置事件类型 | __set_bit(EV_KEY, input->evbit) |
必须设置,否则EINVAL |
| 设置按键字位 | __set_bit(KEY_ENTER, input->keybit) |
在evbit之后设置 |
| 设置绝对轴范围 | input_set_abs_params(input, ABS_X, 0, 4095, 0, 0) |
ABS事件必须设置 |
| 注册设备 | input_register_device(input) |
devm不覆盖此函数 |
| 上报按键 | input_report_key(input, code, value) |
value: 1按下 0松开 |
| 上报绝对坐标 | input_report_abs(input, code, value) |
坐标标志 |
| 同步事件 | input_sync(input) |
每批事件后必须调 |
| MT初始化 | input_mt_init_slots(input, n, flags) |
Type B多点触控必须 |
| MT选中slot | input_mt_slot(input, id) |
切换当前触点 |
| MT报告状态 | input_mt_report_slot_state(input, tool, active) |
active=true按下 |
开发流程速记
分配 。设evbit 。设具体code/abs参数 。注册 。中断里上。sync
input子系统的设计思路很清晰:驱动只管上报事件,不管谁在用。用户空间通过统一的/dev/input/eventX读取,不用关心底层是GPIO按键、I2C触摸屏还是USB鼠标。这种分层解耦让驱动开发和应用开发各干各的,互不干扰。
本文首发于linuxros.cn,转载请注明出处。