ESC
输入关键词搜索文章标题和内容

输入子系统:从按键到触摸屏的驱动开发实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

输入子系统:从按键到触摸屏的驱动开发实战

按键、触摸屏、摇杆、鼠标——Linux把所有输入设备统一到input子系统里管理。驱动开发者不需要自己造字符设备、自己定义ioctl,只要调input子系统的API,用户空间就能通过/dev/input/eventX读到标准化的事件。本文基于Linux 6.8内核,从三层架构讲起,用GPIO按键和电阻触摸屏两个完整驱动把input子系统的用法串起来。

一、输入子系统架构

Linux输入子系统分三层,驱动开发者只需要写最底层的输入设备驱动,中间层和事件处理层内核已经实现好了。

flowchart TB A["输入设备驱动<br/>input_dev"] -->|"input_register_device<br/>input_event"| B["输入核心<br/>input-core"] B -->|"事件分发<br/>input_handle_event"| C["事件处理器层<br/>evdev / mousedev / jsdev"] C -->|"read / poll"| D["用户空间<br/>/dev/input/eventX"] A --- A1["- 操作硬件获取原始数据<br/>- 上报input_event事件<br/>。设置设备支持的事件类型"] B --- B1["- 管理input_dev注册/注销<br/>- 事件分发到各handler<br/>- 提供input_report_* API"] C --- C1["- evdev:原始事件流<br/>- mousedev:模拟鼠标br/>- jsdev:摇杆设备"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style D fill:#F3E5F5 style A1 fill:#E3F2FD style B1 fill:#FFF8E1 style C1 fill:#E8F5E9

各层职责

**输入设备驱动(我们写的部分)。
- 初始化硬件,申请中断或轮。- 填充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的坐标和状态。

来自 linuxros.cn · linuxROS
// 初始化:声明支持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看不到事件?

按这个清单排查:

  1. input_report_key和input_sync是否都调了?缺sync事件不会提交
  2. 中断是否正常触发?加printk确认ISR有没有进
  3. evbit和keybit是否设对了?evtest只显示驱动声明支持的事件
  4. 设备节点是否选对了?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,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页