上一篇我们搞定了 LED 驱动的终极形态——
leds-gpio零代码方案让你设备树写几个属性就能控制 LED,外加 MISC 框架让简单字符设备注册变成 3 行代码。但嵌入式开发不只是点灯——你还需要按键。今天,我们从输出转向输入,用 Linux 输入子系统 把一个 GPIO 按键变成一个标准输入设备。写完你会发现:按键驱动根本不需要/dev/button和write/read那一套,内核早就帮你定义了按键应该怎么上报。
/dev/button?unsetunset上一篇我们用 MISC 框架写了一个 /dev/misc_led,通过 write 控制 LED。按照同样的思路,按键驱动自然就是:
按键按下 → GPIO 中断 → 驱动记录状态 → App read(fd, buf, ...) 读到 "1"这能跑,但一对比标准 Linux 做法就露馅了:
# 我们原来的做法:自己定义格式cat /dev/button # 输出 "1"(按下了)或 "0"(没按)—— 只能一个进程读# Linux 标准做法:任何一个用户态程序都能读到按键事件evtest # 选择 /dev/input/event0,按键事件自动输出# Event: time 1234.567890, type 1 (EV_KEY), code 2 (KEY_1), value 1# Event: time 1234.567890, -------------- SYN_REPORT ------------# Event: time 1234.678901, type 1 (EV_KEY), code 2 (KEY_1), value 0差距在哪里?
/dev/button | ||
|---|---|---|
input_event 结构体 | ||
read | ||
KEY_1、KEY_POWER、KEY_VOLUMEUP...) | ||
| 图形系统、getevent、evtest 开箱即用 | ||
| 内核上报直接进 Android InputReader |
输入子系统让"按键事件"变成了一种标准化的内核服务——就像 LED 子系统让"控制 LED"标准化一样。
Linux 输入子系统分为三层:

驱动开发者只需要关注驱动层——填充 input_dev、调用 input_report_key 上报事件。上面的核心层和事件层全部由内核处理,包括事件缓冲、多路复用、ioctl 查询。
input_devunsetunset#include<linux/input.h>structinput_dev {constchar *name; /* 设备名,evtest 里显示的名字 */constchar *phys; /* 物理路径,如 "gpio-keys/button0" */constchar *uniq; /* 唯一标识符,没有就填 NULL */structinput_idid;/* 总线类型/厂商/产品/版本 *//* 下面是关键——用位图(bitmap)声明"我支持什么事件" */unsignedlong evbit[BITS_TO_LONGS(EV_CNT)]; /* 我支持哪几种事件类型 */unsignedlong keybit[BITS_TO_LONGS(KEY_CNT)]; /* 我支持哪几个按键 */unsignedlong relbit[BITS_TO_LONGS(REL_CNT)]; /* 我支持哪些相对轴 */unsignedlong absbit[BITS_TO_LONGS(ABS_CNT)]; /* 我支持哪些绝对轴 *//* ... 还有其他类型的位图 */};理解位图的含义:evbit 是一个位图,每一位代表一种事件类型。keybit 也是一个位图,每一位代表一个具体的按键。驱动程序通过设置这些位图告诉内核"我这个设备能产生什么事件",内核据此把事件路由到正确的用户态接口。
为了让中断不成为障碍,我们先用"轮询"的方式实现第一版——用一个内核定时器每 20ms 检查一次 GPIO 电平,变了就上报事件。
#include<linux/module.h>#include<linux/platform_device.h>#include<linux/of_gpio.h>#include<linux/gpio.h>#include<linux/input.h>#include<linux/timer.h>#include<linux/jiffies.h>/* 轮询间隔:20ms(兼顾响应速度和 CPU 占用) */#define POLL_INTERVAL_MS 20structgpio_key_dev {int gpio;int irq; /* 本篇暂时不用,下一篇会用 */int old_level; /* 上一次的电平 */structinput_dev *input;structtimer_listpoll_timer;};/* ============ 定时器回调:轮询 GPIO 并上报事件 ============ */staticvoidgpio_key_poll(struct timer_list *t){structgpio_key_dev *dev = from_timer(dev, t, poll_timer);int level; level = gpio_get_value(dev->gpio);if (level != dev->old_level) {/* * 电平变了 → 上报按键事件。 * input_report_key 的三个参数: * input_dev → 哪个设备 * KEY_1 → 哪个按键(定义在 <uapi/linux/input-event-codes.h>) * level → 1=按下,0=释放 * 注意:这里假设高电平 = 按下。如果你的电路是低电平有效, * 把 level 改成 !level 即可。 */ input_report_key(dev->input, KEY_1, level); input_sync(dev->input); /* 同步:告诉事件层"本次事件组结束" */ dev->old_level = level; }/* 重新启动定时器 */ mod_timer(&dev->poll_timer, jiffies + msecs_to_jiffies(POLL_INTERVAL_MS));}/* ============ probe ============ */staticintgpio_key_probe(struct platform_device *pdev){structgpio_key_dev *dev;int ret; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;/* ---- 1. 从设备树获取 GPIO ---- */ dev->gpio = of_get_named_gpio(pdev->dev.of_node, "key-gpios", 0);if (dev->gpio < 0) { pr_err("gpio_key: failed to get key-gpios\n");return dev->gpio; } ret = gpio_request(dev->gpio, "key");if (ret)return ret; gpio_direction_input(dev->gpio); /* 按键是输入设备 */ dev->old_level = gpio_get_value(dev->gpio);/* ---- 2. 分配并初始化 input_dev ---- */ dev->input = devm_input_allocate_device(&pdev->dev);if (!dev->input) { ret = -ENOMEM;goto fail_gpio; } dev->input->name = "gpio_key"; /* /dev/input/ 下的名字 */ dev->input->phys = "gpio-keys/button0"; /* 物理路径标识 */ dev->input->id.bustype = BUS_HOST; /* 总线类型:SoC 内部总线 *//* * 声明能力:我支持 EV_KEY 类型,具体按键是 KEY_1。 * 如果支持多个按键,用 set_bit() 逐个设置。 */ input_set_capability(dev->input, EV_KEY, KEY_1);/* ---- 3. 注册 input_dev ---- */ ret = input_register_device(dev->input);if (ret)goto fail_gpio;/* ---- 4. 启动轮询定时器 ---- */ timer_setup(&dev->poll_timer, gpio_key_poll, 0); mod_timer(&dev->poll_timer, jiffies + msecs_to_jiffies(POLL_INTERVAL_MS)); platform_set_drvdata(pdev, dev); pr_info("gpio_key: probed, /dev/input/eventX ready\n");return 0;fail_gpio: gpio_free(dev->gpio);return ret;}/* ============ remove ============ */staticintgpio_key_remove(struct platform_device *pdev){structgpio_key_dev *dev = platform_get_drvdata(pdev); del_timer_sync(&dev->poll_timer); /* 删定时器 */ input_unregister_device(dev->input); /* 注销输入设备 */ gpio_free(dev->gpio);return 0;}static const structof_device_idgpio_key_of_match[] = { { .compatible = "qian,gpio-key" }, { }};MODULE_DEVICE_TABLE(of, gpio_key_of_match);static structplatform_drivergpio_key_driver = { .driver = { .name = "gpio-key", .of_match_table = gpio_key_of_match, }, .probe = gpio_key_probe, .remove = gpio_key_remove,};module_platform_driver(gpio_key_driver);MODULE_LICENSE("GPL");MODULE_AUTHOR("qian");MODULE_DESCRIPTION("Polled GPIO key driver with input subsystem");1. input_report_key + input_sync 是固定组合
input_report_key(dev, KEY_1, 1); // 上报"KEY_1 按下"input_sync(dev); // 帧边界:"本次事件到此结束"input_report_key(dev, KEY_1, 0); // 上报"KEY_1 释放"input_sync(dev);input_sync 内部上报一个 EV_SYN / SYN_REPORT 事件。用户态(evdev)以 SYN_REPORT 为帧边界来分割事件。永远在按键事件后面跟一个 input_sync,否则 evtest 会看到两个按键事件粘在一起。
2. input_set_capability vs 手动 set_bit
/* 这个 */input_set_capability(dev->input, EV_KEY, KEY_1);/* 等价于这个 */__set_bit(EV_KEY, dev->input->evbit);__set_bit(KEY_1, dev->input->keybit);input_set_capability 是一个便捷宏,一次性帮你设置两个位图。用宏更安全——不容易漏设 evbit。
3. timer_setup 是 Linux 4.15+ 的新接口
老版本内核(4.15 之前)用 setup_timer + init_timer,新版本统一为 timer_setup。RK3568 的 Linux 4.19 和 5.10 都支持这个新接口。
#include <dt-bindings/gpio/gpio.h>/ { gpio_key { compatible = "qian,gpio-key"; key-gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>; status = "okay"; };};# 1. 编译驱动并推送到开发板makeadb push gpio_key.ko /root/# 2. 加载insmod gpio_key.ko# 3. 找出我们的输入设备ls /dev/input/# by-path event0 event1 mice mouse0# 不确定是 event 几?用这个命令:ls -l /dev/input/by-path/# platform-gpio-keys-button0-event -> ../event1# 4. 用 evtest 监测按键事件evtest /dev/input/event1# 按下按键:# Event: time 1234.567890, type 1 (EV_KEY), code 2 (KEY_1), value 1# Event: time 1234.567890, -------------- SYN_REPORT ------------# 松开按键:# Event: time 1234.678901, type 1 (EV_KEY), code 2 (KEY_1), value 0# Event: time 1234.678901, -------------- SYN_REPORT ------------# 5. 也可以用 getevent(Android 上常用)getevent -l /dev/input/event1# /dev/input/event1: EV_KEY KEY_1 DOWN# /dev/input/event1: EV_SYN SYN_REPORT 00000000# /dev/input/event1: EV_KEY KEY_1 UP# /dev/input/event1: EV_SYN SYN_REPORT 00000000你什么都没做,Android / Linux 图形系统就已经能读到你的按键事件了。 这就是输入子系统的价值——它定义了一套标准,所有用户态程序(evtest、getevent、Qt、Android InputReader)都遵循这套标准。
gpio-keys 驱动unsetunset和 LED 有 leds-gpio 一样,内核也提供了通用的 GPIO 按键驱动——gpio-keys(drivers/input/keyboard/gpio_keys.c)。
#include <dt-bindings/gpio/gpio.h>#include <dt-bindings/input/input.h>/ { gpio-keys { compatible = "gpio-keys"; autorepeat; /* 支持长按自动重复 */ key1 { label = "key1"; gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>; linux,code = <KEY_1>; /* 按键编码 */ debounce-interval = <5>; /* 去抖时间(ms) */ wakeup-source; /* 作为唤醒源 */ }; key2 { label = "key2"; gpios = <&gpio0 RK_PC1 GPIO_ACTIVE_LOW>; linux,code = <KEY_ENTER>; }; };};内核配置:
make menuconfig# Device Drivers → Input device support → Keyboards# <*> GPIO Buttons# 对应 CONFIG_KEYBOARD_GPIO又是零 C 代码。 而且 gpio-keys 是中断驱动的(不是轮询),自动支持去抖、长按重复、唤醒源等功能。
| 0 行 | ||
debounce-interval = <5> | ||
autorepeat | ||
| 标准 GPIO 按键(首选方案) |
和 LED 的结论一样:标准 GPIO 按键 → 优先用 gpio-keys。只有特殊逻辑才自己写。
你可能注意到了:输入子系统的驱动里,没有 file_operations、没有 cdev_add、没有 device_create。那 /dev/input/event0 是谁创建的?
答案是:input_register_device 内部帮你做了这一切。输入核心层(input.c)会调用 cdev_add,事件处理层(evdev.c)会创建 /dev/input/eventX。作为驱动开发者,你连字符设备的影子都看不到——你只需要上报事件,剩下的内核全包。
这和上一篇 LED 子系统的理念一脉相承:越是深入内核框架,你要写的代码越少,但前提是你理解框架帮你做了什么。
input_sync/* 错误:只上报按键,没有 sync */input_report_key(dev, KEY_1, 1);// evtest 不会立即输出,直到下一个 sync 才刷新事件队列/* 正确:按键后立刻 sync */input_report_key(dev, KEY_1, 1);input_sync(dev);经验法则:每完成一次"逻辑上的按键变化"(按下或释放),就跟一个 input_sync。
input_allocate_device vs devm_input_allocate_device/* 错误:用 input_allocate_device 但没有在 remove 中 free */dev->input = input_allocate_device();/* 正确:用 devm_ 版本,自动释放 */dev->input = devm_input_allocate_device(&pdev->dev);和 GPIO、内存一样,能用 devm_ 前缀的托管版本就用托管版本,省去手动释放的麻烦。
输入子系统的核心 API(input_allocate_device、input_register_device、input_report_key)在三个版本中完全一致,设备树绑定也兼容。需要注意的变化:
devm_input_allocate_device | |||
gpio-keys | drivers/input/keyboard/gpio_keys.c | ||
gpio_to_irq | gpiod_to_irq | ||
gpio_requestgpio_free | devm_gpio_request | ||
.txt | .yaml | .yaml |
本文代码在 4.19 / 5.10 / 6.1 上均可编译运行。 6.1 上建议逐渐迁移到 gpiod_* 系列 API,但传统 gpio_* API 仍然兼容。
input_dev 结构体 —— evbit / keybit 位图声明能力,input_set_capability 便捷宏input_report_key + input_sync —— 标准按键上报模式,SYN_REPORT 为帧边界timer_setup + mod_timer,每 20ms 检测一次 GPIO 电平evtest / getevent —— 用户空间调试输入设备的利器gpio-keys 驱动 —— 设备树配置 + 零 C 代码,中断驱动 + 去抖 + 自动重复本文我们用定时器轮询按键,虽然能跑,但 20ms 一定时器带来的 CPU 唤醒次数(每秒 50 次)、低效的轮询开销,在工程上是不能接受的。真正的按键驱动必须用中断——按键按下时硬件直接通知 CPU,其余时间 CPU 完全不参与。
下一篇,我们引入 Linux 中断处理:request_irq、request_threaded_irq、顶半部和底半部的区别、以及如何把轮询版按键驱动改成中断版。
关注「钱途无量嵌入式」,专注 Linux 驱动与 BSP 开发,每周硬核输出。