当前位置:首页>Linux>Linux输入子系统-GPIO按键驱动与evdev接口

Linux输入子系统-GPIO按键驱动与evdev接口

  • 2026-10-11 06:00:52
Linux输入子系统-GPIO按键驱动与evdev接口

上一篇我们搞定了 LED 驱动的终极形态——leds-gpio 零代码方案让你设备树写几个属性就能控制 LED,外加 MISC 框架让简单字符设备注册变成 3 行代码。但嵌入式开发不只是点灯——你还需要按键。今天,我们从输出转向输入,用 Linux 输入子系统 把一个 GPIO 按键变成一个标准输入设备。写完你会发现:按键驱动根本不需要/dev/button和write/read那一套,内核早就帮你定义了按键应该怎么上报。


unsetunset先看现状:为什么不能给按键也写一个 /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
 原做法
输入子系统标准做法
数据格式
自定义(0/1 字节流)
标准的 input_event 结构体
多进程读取
只能一个进程 read
所有进程都能收到事件(通过 evdev)
按键类型
程序里硬编码
内核统一编码(KEY_1、KEY_POWER、KEY_VOLUMEUP...)
兼容性
自己写的 App 才能用
图形系统、getevent、evtest 开箱即用
Android 支持
得自己写 HAL 层
内核上报直接进 Android InputReader

输入子系统让"按键事件"变成了一种标准化的内核服务——就像 LED 子系统让"控制 LED"标准化一样。


unsetunset一、输入子系统分层架构unsetunset

Linux 输入子系统分为三层:

驱动开发者只需要关注驱动层——填充 input_dev、调用 input_report_key 上报事件。上面的核心层和事件层全部由内核处理,包括事件缓冲、多路复用、ioctl 查询。


unsetunset二、核心数据结构: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 也是一个位图,每一位代表一个具体的按键。驱动程序通过设置这些位图告诉内核"我这个设备能产生什么事件",内核据此把事件路由到正确的用户态接口。


unsetunset三、一个 GPIO 按键的完整驱动(轮询版)unsetunset

为了让中断不成为障碍,我们先用"轮询"的方式实现第一版——用一个内核定时器每 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 都支持这个新接口。


unsetunset四、设备树unsetunset

#include <dt-bindings/gpio/gpio.h>/ {    gpio_key {        compatible = "qian,gpio-key";        key-gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;        status = "okay";    };};

unsetunset五、编译、加载、用 evtest 验证unsetunset

# 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)都遵循这套标准。


unsetunset六、内核自带方案: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 是中断驱动的(不是轮询),自动支持去抖、长按重复、唤醒源等功能。


unsetunset七、自己写 vs gpio-keys:选哪个?unsetunset

自己写输入驱动
gpio-keys 内核驱动
C 代码
~100 行
0 行
驱动方式
轮询(或中断)
中断(内核自动处理)
去抖
自己写
debounce-interval = <5>
长按重复
自己写
autorepeat
 一个属性搞定
多按键
改驱动
加子节点就行
适用场景
复杂按键逻辑(组合键、长按短按区分)
标准 GPIO 按键(首选方案)

和 LED 的结论一样:标准 GPIO 按键 → 优先用 gpio-keys。只有特殊逻辑才自己写。


unsetunset八、输入子系统和字符设备的关系unsetunset

你可能注意到了:输入子系统的驱动里,没有 file_operations、没有 cdev_add、没有 device_create。那 /dev/input/event0 是谁创建的?

答案是:input_register_device 内部帮你做了这一切。输入核心层(input.c)会调用 cdev_add,事件处理层(evdev.c)会创建 /dev/input/eventX。作为驱动开发者,你连字符设备的影子都看不到——你只需要上报事件,剩下的内核全包。

这和上一篇 LED 子系统的理念一脉相承:越是深入内核框架,你要写的代码越少,但前提是你理解框架帮你做了什么。


unsetunset九、容易踩的坑unsetunset

1. 忘记 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。

2. 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_ 前缀的托管版本就用托管版本,省去手动释放的麻烦。

3. 轮询间隔太大或太小

  • 20ms(50Hz)→ 人基本感觉不到延迟,CPU 占用很小 → 推荐
  • 1ms(1000Hz)→ 即使用在按键上也浪费,CPU 占用飙升
  • 100ms(10Hz)→ 按键按下后 0.1 秒才响应,能明显感觉到迟钝

unsetunset十、Linux 4.19 / 5.10 / 6.1 版本差异unsetunset

输入子系统的核心 API(input_allocate_device、input_register_device、input_report_key)在三个版本中完全一致,设备树绑定也兼容。需要注意的变化:

变化点
4.19
5.10
6.1
devm_input_allocate_device
✅ 支持
✅ 支持
✅ 支持
gpio-keys
 驱动路径
drivers/input/keyboard/gpio_keys.c
相同
相同
gpio_to_irq
✅ 支持
✅ 支持
⚠️ 推荐改用 gpiod_to_irq
gpio_request
 / gpio_free
✅ 传统 API
✅ 兼容
⚠️ 新代码推荐 devm_gpio_request
设备树绑定文档
.txt.yaml
 schema
.yaml
 schema

本文代码在 4.19 / 5.10 / 6.1 上均可编译运行。 6.1 上建议逐渐迁移到 gpiod_* 系列 API,但传统 gpio_* API 仍然兼容。


unsetunset本文你学到了什么unsetunset

  1. 输入子系统三层架构 —— 驱动层 → 核心层 → 事件层,驱动只负责上报
  2. input_dev 结构体 —— evbit / keybit 位图声明能力,input_set_capability 便捷宏
  3. input_report_key + input_sync —— 标准按键上报模式,SYN_REPORT 为帧边界
  4. 定时器轮询模式 —— timer_setup + mod_timer,每 20ms 检测一次 GPIO 电平
  5. evtest / getevent —— 用户空间调试输入设备的利器
  6. 内核 gpio-keys 驱动 —— 设备树配置 + 零 C 代码,中断驱动 + 去抖 + 自动重复
  7. 输入子系统和 LED 子系统的共同设计哲学 —— 标准硬件用内核自带驱动,特殊逻辑才自己写

unsetunset接下来unsetunset

本文我们用定时器轮询按键,虽然能跑,但 20ms 一定时器带来的 CPU 唤醒次数(每秒 50 次)、低效的轮询开销,在工程上是不能接受的。真正的按键驱动必须用中断——按键按下时硬件直接通知 CPU,其余时间 CPU 完全不参与。

下一篇,我们引入 Linux 中断处理:request_irq、request_threaded_irq、顶半部和底半部的区别、以及如何把轮询版按键驱动改成中断版。


关注「钱途无量嵌入式」,专注 Linux 驱动与 BSP 开发,每周硬核输出。

最新文章

随机文章