🔨 整理中 · 这一篇拆 input 子系统:为什么内核用统一的"事件(type/code/value)"模型抽象所有输入设备、
input_dev的能力位图怎么声明"这设备能产生哪些事件"、framework 怎么把驱动上报的事件分发到/dev/input/eventX。拿gpio_keys.c(GPIO 按键驱动,1122 行,最典型的 input 驱动)逐段走读——从 GPIO 中断到input_report_key到用户态拿到事件,完整链路每步带行号。动手部分在真板验过之前标"待亲测"。
做什么
输入设备五花八门——按键、键盘、鼠标、触摸屏、摇杆、加速度计——它们的硬件接口和原始数据格式天差地别(GPIO 电平、I2C 坐标、USB HID 报告...)。但它们做的是同一件事:产生"某类型、某编码、某值"的事件(按了 KEY_A、X 方向移动了 10、触摸坐标 1024/768)。内核 input 子系统统一这套:设备驱动只管"读硬件 → 转成 input 事件 → input_report_* 上报",事件路由/分发/给用户态(/dev/input/eventX)由 input core + handler(evdev 等)处理。这一篇拆"事件模型 + 能力位图 + 事件分发",拿 GPIO 按键走完整条链。
要了解什么
〇、硬件背景:输入设备类型与统一事件模型
输入设备按"产生什么类型的数据"分几类,每类对应一个事件类型(type):
- 按键/键盘:按下/松开一个键 →
EV_KEY事件,code= 键码(KEY_A=30、KEY_ENTER=28、BTN_TOUCH...),value= 1(按下)/0(松开)/2(重复)。 - 鼠标(相对坐标):移动了多少 →
EV_REL,code=REL_X/REL_Y,value= 位移量(+10 = 右移 10 像素)。 - 触摸屏/摇杆(绝对坐标):当前坐标 →
EV_ABS,code=ABS_X/ABS_Y,value= 坐标值(0-1023 之类)。 - 同步:一组相关事件结束 →
EV_SYN(code=SYN_REPORT),告诉消费者"这一帧齐了,可以处理了"。
用户态从 /dev/input/eventX 读到的是统一的 struct input_event(include/uapi/linux/input.h:28):
struct input_event { /* 用户态读到的格式 */
struct timeval time; /* 时间戳 */
__u16 type; /* EV_KEY / EV_REL / EV_ABS / EV_SYN ... */
__u16 code; /* KEY_A / REL_X / ABS_X ... */
__s32 value; /* 1/0(按键) 或 位移量(鼠标) 或 坐标值(触摸) */
};不论设备是什么,用户态用同一套 struct input_event 读——这是 input 子系统的核心抽象。evtest/libinput/X/Wayland 都基于它。
一、framework:input_dev + 能力位图 + 事件分发
struct input_dev(include/linux/input.h:137)——一个输入设备的化身,核心是能力位图:
struct input_dev { /* input.h:137 */
const char *name;
unsigned long evbit[]; /* ⭐ 支持哪些事件类型(EV_KEY? EV_REL? EV_ABS?) */
unsigned long keybit[]; /* ⭐ 支持哪些按键(KEY_A? KEY_ENTER? ...) */
unsigned long relbit[]; /* 支持哪些相对轴(REL_X? REL_Y? ...) */
unsigned long absbit[]; /* 支持哪些绝对轴(ABS_X? ABS_Y? ...) */
struct input_absinfo *absinfo; /* 绝对轴的范围(min/max/fuzz/flat) */
...
};能力位图是 input 的精髓:evbit 声明"我能产生哪些 type"、keybit 声明"在 EV_KEY 里我能产生哪些键"、absbit 声明"在 EV_ABS 里我有哪些轴"。用户态 udev/libinput 读这些位图知道"这设备能干啥、出什么格式"。设能力用 __set_bit(KEY_A, dev->keybit) 或 input_set_capability(dev, EV_KEY, KEY_A)(input.c:2071),绝对轴用 input_set_abs_params(dev, ABS_X, min, max, fuzz, flat)(:455 设范围 + 分配 absinfo)。
注册:devm_input_allocate_device(dev) 分配 + 设能力 + input_register_device(dev)(input.c:2312)——后者把设备加进全局列表、匹配 handler(evdev 等)建连接、/dev/input/eventX 出现。注册时 framework 自动加 EV_SYN(所有设备都必须能 sync)。
事件上报 + 分发:驱动用 input_report_key(dev, KEY_A, 1)(input.h:437,封装 input_event(dev, EV_KEY, KEY_A, 1))上报 → input_event(input.c:391)→ input_handle_event(:358)→ 攒一批事件 → input_pass_values(:111)分发给所有连上的 handler。evdev handler 的 evdev_events(evdev.c:291)接收 → 写进环形 buffer → 唤醒用户态读。
二、evdev handler:事件到用户态的桥
drivers/input/evdev.c 是最常用的 handler——每个 input_dev 连一个 evdev → /dev/input/eventN。事件来时 evdev_events(:291)调 evdev_pass_values(:244)把每个 input_value 转成 struct input_event 写进 evdev_client 的环形 buffer(__pass_event:214)→ SYN_REPORT 时 wake_up_interruptible_poll(:284)唤醒阻塞在 read 的用户态 → evdev_read(:556)从 buffer 取事件 copy_to_user。buffer 满时丢旧事件 + 插一个 SYN_DROPPED(:220)告诉用户态"丢了,重新同步"。
三、源码走读:gpio_keys.c(GPIO 按键 input 驱动)
drivers/input/keyboard/gpio_keys.c(1122 行)是最典型的 input 驱动——N 个 GPIO 按键,每个按/松开产生 EV_KEY 事件。
probe(:837):
static int gpio_keys_probe(struct platform_device *pdev)
{
/* 1. 从设备树读所有按键定义(gpio/键码/去抖间隔) */
pdata = gpio_keys_get_devtree_pdata(dev);
/* 2. 分配 input_dev + 驱动私有数据 */
input = devm_input_allocate_device(dev);
input->name = "gpio-keys";
/* 3. 每个按键:获取 GPIO + 设能力 + 请求 IRQ */
for (i = 0; i < pdata->nbuttons; i++) {
gpio_keys_setup_key(pdev, input, ddata, &pdata->buttons[i], i, child);
/* setup_key 内部(:496):
* gpiod = devm_fwnode_gpiod_get(dev, child, NULL, GPIOD_IN); // 获取 GPIO
* input_set_capability(input, EV_KEY, keycode); // ⭐ 声明"能产生这个键"
* irq = gpiod_to_irq(gpiod); // GPIO → IRQ 号
* devm_request_any_context_irq(dev, irq, gpio_keys_gpio_isr,
* IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, ...); // ⭐ 双边沿(按+松都触发)
*/
}
/* 4. 注册 */
input_register_device(input);
return 0;
}ISR + 去抖 + 上报(:411/:401/:360):
/* gpio_keys.c:411 — GPIO 中断(按下或松开时触发,双边沿) */
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
struct gpio_button_data *bdata = dev_id;
/* ⭐ 去抖:不立刻报事件,启动定时器等抖动过去 */
if (bdata->debounce_use_hrtimer)
hrtimer_start(&bdata->debounce_timer, ms_to_ktime(bdata->software_debounce), ...);
else
mod_delayed_work(system_wq, &bdata->work, msecs_to_jiffies(bdata->software_debounce));
return IRQ_HANDLED;
}
/* :401 — 去抖定时器到时回调(抖动已过,现在是稳定状态) */
static enum hrtimer_restart gpio_keys_debounce_timer(struct hrtimer *t)
{
gpio_keys_debounce_event(bdata); /* → :384 报事件 */
return HRTIMER_NORESTART;
}
/* :384 — 真正上报 */
static void gpio_keys_debounce_event(struct gpio_button_data *bdata)
{
gpio_keys_gpio_report_event(bdata); /* → :360 读 GPIO + input_report_key */
input_sync(bdata->input); /* ⭐ 一帧结束,必须 sync */
}
/* :360 — 读 GPIO 状态 + 上报 */
static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata)
{
int state = gpiod_get_value(bdata->gpiod); /* 读 GPIO 电平(0=松开/1=按下) */
input_event(input, type, *bdata->code, state); /* → input_report_key(dev, KEY_A, 1/0) */
}核心链路:GPIO 电平变 → 双边沿 IRQ(:411)→ 去抖 hrtimer(等抖动过)→ gpio_keys_gpio_report_event(:360)读 GPIO 实际电平 → input_report_key(dev, KEY_X, state) + input_sync。
四、数据流:按键到用户态的完整链路
- 用户按按键 → GPIO 电平变化。
- GPIO 控制器检测到边沿 → 触发 IRQ →
gpio_keys_gpio_isr(gpio_keys.c:411)。 - 去抖:启动 hrtimer 等
debounce_intervalms →gpio_keys_debounce_timer(:401)→gpio_keys_gpio_report_event(:360)。 - 读电平 + 上报:
gpiod_get_value读 GPIO →input_report_key(dev, KEY_A, 1)(:360/input.h:437)→input_event(input.c:391)。 - input core 攒事件:
input_handle_event(:358)把事件存进dev->vals[]数组。 input_sync(:360)→ 触发input_pass_values(input.c:111)→ 遍历所有连上的 handler 调handle_events。- evdev 接收:
evdev_events(evdev.c:291)→evdev_pass_values(:244)→ 逐事件__pass_event(:214)写进环形 buffer。 - 唤醒用户态:
SYN_REPORT时wake_up_interruptible_poll(:284)。 - 用户态 read:
evdev_read(:556)从 buffer 取struct input_event→copy_to_user→ 用户态evtest/libinput 拿到{type=EV_KEY, code=KEY_A, value=1}。
整条链:GPIO 电平 → IRQ(:411)→ debounce hrtimer(:401)→ input_report_key(:360)→ input_event(:391)→ input_pass_values(:111)→ evdev_events(:291)→ __pass_event(:214)→ evdev_read(:556)→ 用户态。
五、源码真踩坑(都是读源码才发现的)
input_sync必须调(input.c:2312注册时自动加 EV_SYN + 事件攒到 sync 才分发)。驱动报了一组事件(如触摸的 ABS_X + ABS_Y + BTN_TOUCH)后必须input_sync,否则 handler 不处理这一帧——事件攒在dev->vals[]里等着。漏 sync,用户态收不到事件。EV_ABS 必须设范围(
input.c:2319注册检查 absinfo)。触摸屏的 ABS_X/ABS_Y 要input_set_abs_params(dev, ABS_X, 0, 1023, 0, 0)设 min/max(范围),否则input_register_device返回-EINVAL。libinput/X 靠这个范围做坐标映射。fuzz(去抖)/flat(死区)参数也在这里设。GPIO 按键要双边沿 IRQ(
gpio_keys.c:599IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)。只触发沿(仅按下)的话,松开事件收不到——按键永远"按着"。双边沿 = 按+松都触发 ISR。去抖:硬件优先、软件兜底(
gpio_keys.c:554)。先试gpiod_set_debounce(gpiod, interval)(硬件去抖,GPIO 控制器支持的话);失败(不支持)回退到软件去抖(hrtimer 延迟debounce_intervalms 再读电平)。机械按键抖动会导致按下瞬间多次 IRQ(几 ms 内抖动),没有去抖会报"按下松开按下松开"一堆假事件。自动重复(
EV_REP)由 core 处理,驱动不用管(input.c:2216)。如果设了EV_REP(probe 里__set_bit(EV_REP, input->evbit)),用户按住不放时 core 自动每REP_PERIOD(33ms)发一个重复事件——驱动只报"按下 1"和"松开 0",重复由 core 的定时器插。环形 buffer 满 →
SYN_DROPPED(evdev.c:220)。用户态 read 太慢(缓冲区满),evdev 丢旧事件 + 插SYN_DROPPED。用户态收到SYN_DROPPED后应该丢弃到下一个SYN_REPORT之间的所有事件(因为可能不完整),重新同步——libinput 就是这么处理的。
动手试试
ls /sys/class/input/;cat /proc/bus/input/devices(看每个设备的能力位图 evbit/keybit/absbit + 对应的 eventX)。evtest /dev/input/eventN(装 evtest)实时看事件流;按一个键,看{EV_KEY, KEY_A, 1}+{EV_SYN, SYN_REPORT, 0}一帧。- 读
drivers/input/keyboard/gpio_keys.c的 probe(:837)+ ISR(:411)+ report(:360),对照本篇第三/四节走完整链。 cat /sys/class/input/eventN/device/capabilities/key(hex 位图,看支持哪些键)。- 思考题:为什么 input 要"一组事件后
input_sync",而不是每条事件直接送用户态?(提示:原子性——触摸的 X+Y+触摸态是一帧,要一起给消费者才一致)
延伸阅读
- 源码(本仓库
third_party/linux/,6.19.9):- framework:
drivers/input/input.c(input_register_device:2312/input_event:391/input_pass_values:111/input_set_abs_params:455/input_set_capability:2071)、include/linux/input.h:137 struct input_dev、include/uapi/linux/input.h:28 struct input_event(用户态格式)、include/uapi/linux/input-event-codes.h(所有 EV_/KEY_/ABS_* 常量)。 - handler:
drivers/input/evdev.c(evdev_connect:1334/evdev_events:291/evdev_pass_values:244/__pass_event:214/evdev_read:556)——通用事件透传到/dev/input/eventX。 - 典型驱动:
drivers/input/keyboard/gpio_keys.c(本篇走读)、drivers/input/touchscreen/(各种触摸屏,用 I2C/SPI 读坐标 + EV_ABS)、drivers/input/mouse/(鼠标)、IIO 传感器drivers/iio/。
- framework:
- kernel.org:Input、
Documentation/input/(event codes/edev...)。 - 关联本站:05 irq GPIO 按键的中断、15 gpio GPIO 读电平、23 i2c I2C 触摸屏常挂 I2C。