目录
[开篇先搞懂:input 子系统到底是什么?解决了什么痛点?](#开篇先搞懂:input 子系统到底是什么?解决了什么痛点?)
[为什么要用 input 子系统?对比我们之前写的私有字符设备驱动,核心优势太明显了](#为什么要用 input 子系统?对比我们之前写的私有字符设备驱动,核心优势太明显了)
[一、input 子系统核心原理,小白必懂的知识点](#一、input 子系统核心原理,小白必懂的知识点)
[1. input 子系统的三层架构](#1. input 子系统的三层架构)
[2. 核心概念:输入事件(Event)](#2. 核心概念:输入事件(Event))
[3. 一次完整的按键事件上报流程](#3. 一次完整的按键事件上报流程)
[4. input 子系统核心 API,小白必须掌握](#4. input 子系统核心 API,小白必须掌握)
[二、实战一:基于 input 子系统的标准按键驱动开发](#二、实战一:基于 input 子系统的标准按键驱动开发)
[1. 设备树配置](#1. 设备树配置)
[2. 编译烧录验证](#2. 编译烧录验证)
[3. 自定义按键的驱动实现](#3. 自定义按键的驱动实现)
[三、实战二:GT911 触摸屏驱动开发,RK 平台最常用的触控芯片](#三、实战二:GT911 触摸屏驱动开发,RK 平台最常用的触控芯片)
[1. GT911 核心原理](#1. GT911 核心原理)
[2. 设备树配置](#2. 设备树配置)
[3. 内核配置,开启 GT911 驱动](#3. 内核配置,开启 GT911 驱动)
[4. 验证驱动是否正常工作](#4. 验证驱动是否正常工作)
[四、安卓系统 input 事件的处理流程](#四、安卓系统 input 事件的处理流程)
[五、小白 input 驱动必踩的坑,提前规避](#五、小白 input 驱动必踩的坑,提前规避)
大家好,我是黒漂技术佬。前几篇我们完成了 GPIO、I2C、SPI 等基础外设的驱动开发,后台很多兄弟反馈了一个核心痛点:
"佬,我自己写的按键驱动,只能在串口里用命令测试,安卓系统根本识别不到,没法用安卓原生的按键事件,更别说触摸屏了,这该怎么解决?"
太懂这种无力感了!之前我们写的字符设备驱动,是我们自己定义的私有协议,安卓系统根本不认识,只能我们自己写 App 调用。而Linux input 子系统,就是解决这个问题的标准答案 ------ 它是 Linux 内核里标准的输入设备框架,所有的按键、触摸屏、鼠标、键盘,全都是基于这个框架开发的,安卓系统原生支持,只要你按标准写驱动,安卓就能自动识别,不用额外做复杂的适配。
今天这篇,我就用大白话给你讲透 Linux input 子系统的核心原理,手把手带你完成标准按键输入驱动 + GT911 触摸屏驱动的完整开发,基于 RK3568 平台,实现安卓系统原生识别,学完你就能适配所有的输入设备。
开篇先搞懂:input 子系统到底是什么?解决了什么痛点?
大白话定义
Linux input 子系统,是内核里一套标准化的输入设备驱动框架,它把所有输入设备(按键、触摸屏、鼠标、键盘、游戏手柄等)的共性抽象出来,给驱动开发者提供了一套标准的 API,给上层应用提供了一套统一的事件接口。
简单说,它就像一个统一的「输入设备翻译官」:
- 驱动开发者:不用再自己写字符设备、自己定义协议,只需要调用 input 子系统的 API,上报对应的事件就行;
- 上层应用(包括安卓系统):不用关心底层是什么硬件,只需要读取标准的 input 事件,就能处理所有输入设备的操作,不管是按键、触摸屏还是鼠标,接口完全统一。
为什么要用 input 子系统?对比我们之前写的私有字符设备驱动,核心优势太明显了
- 安卓系统原生支持:只要你按 input 子系统的标准上报事件,安卓系统会自动识别,比如按键上报 KEY_POWER,安卓就会响应锁屏 / 亮屏;触摸屏上报坐标,安卓系统就会自动处理触摸事件,不用我们自己写 HAL 层、JNI 去适配,省了 90% 的工作;
- 标准化、通用性强:input 子系统是 Linux 内核的标准框架,所有 Linux 发行版、安卓系统全兼容,你写的驱动,换个平台也能直接用;
- 开发难度极低:内核已经帮我们做好了字符设备、事件缓冲区、锁机制这些底层逻辑,我们只需要初始化 input 设备,然后上报事件就行,不用再处理复杂的字符设备逻辑;
- 调试极其方便 :内核自带了全套的调试工具,比如
evtest、getevent,能直接看到设备上报的所有事件,出问题一眼就能定位。
一、input 子系统核心原理,小白必懂的知识点
1. input 子系统的三层架构
input 子系统分为三层,从上到下分别是:
- 事件处理层(Event Handler) :最上层,给用户空间提供统一的访问接口,会给每个输入设备在
/dev/input/目录下生成对应的设备文件eventX,上层应用就是通过读取这个文件,获取输入事件。我们不用管这一层,内核已经实现好了; - 核心层(Input Core):中间层,承上启下,给下层驱动提供注册 / 注销 input 设备的 API,给上层事件处理层提供事件上报的接口,是整个子系统的核心,我们只需要调用它提供的 API 就行;
- 设备驱动层:最下层,也就是我们要写的驱动代码,负责和硬件交互,获取硬件的状态(比如按键按下 / 松开、触摸屏坐标),然后调用核心层的 API,把事件上报上去。
2. 核心概念:输入事件(Event)
input 子系统的核心,就是「事件」。输入设备的所有操作,都会被封装成标准的事件上报给上层,每个事件由 3 个核心属性组成:
-
type(事件类型) :告诉上层,这是什么类型的事件,比如按键事件、坐标事件、同步事件,最常用的有这几个:
表格
事件类型 宏定义 作用 同步事件 EV_SYN 同步事件,用来标记一组事件的结束,每次上报完一组事件,必须上报一个同步事件,告诉上层 "这次的事件上报完了,你可以处理了" 按键事件 EV_KEY 按键 / 开关事件,比如键盘按键、电源键、触摸屏的按下 / 松开,所有的按键操作都用这个类型 绝对坐标事件 EV_ABS 绝对坐标事件,触摸屏的 X/Y 坐标、压力值,都用这个类型 相对坐标事件 EV_REL 相对坐标事件,鼠标的移动、滚轮,用这个类型 -
code(事件码):告诉上层,这个事件具体是哪个按键、哪个坐标。比如 EV_KEY 类型里,code=KEY_POWER 代表电源键,code=KEY_VOLUMEUP 代表音量加键;EV_ABS 类型里,code=ABS_X 代表 X 坐标,code=ABS_Y 代表 Y 坐标;
-
value(事件值):事件的具体值。比如按键事件里,value=1 代表按下,value=0 代表松开;坐标事件里,value 就是具体的坐标数值。
3. 一次完整的按键事件上报流程
比如我们按下电源键,再松开,完整的事件上报流程是这样的:
- 按键按下,驱动检测到电平变化;
- 驱动调用
input_report_key()函数,上报 EV_KEY 事件,code=KEY_POWER,value=1(按下); - 驱动调用
input_sync()函数,上报 EV_SYN 同步事件,告诉上层这次事件上报完成; - 按键松开,驱动检测到电平变化;
- 驱动调用
input_report_key()函数,上报 EV_KEY 事件,code=KEY_POWER,value=0(松开); - 驱动再次调用
input_sync()函数,上报同步事件,完成一次完整的按键操作。
安卓系统收到这些事件后,就会自动执行对应的亮屏 / 锁屏操作,完全不用我们自己写上层逻辑,这就是标准框架的好处!
4. input 子系统核心 API,小白必须掌握
Linux 内核给我们提供了一套极其简单的 API,不用我们处理底层逻辑,只需要调用这几个函数,就能完成 input 设备的开发:
表格
| API 函数 | 作用 |
|---|---|
input_allocate_device() |
分配一个 input_dev 结构体,也就是一个输入设备对象 |
set_bit() |
设置 input 设备支持的事件类型和事件码,告诉内核这个设备能上报什么事件 |
input_register_device() |
把分配好的 input 设备注册到内核里,注册成功后,会自动在 /dev/input/ 下生成 event 设备文件 |
input_report_key() |
上报按键事件,最常用 |
input_report_abs() |
上报绝对坐标事件,触摸屏用 |
input_report_rel() |
上报相对坐标事件,鼠标用 |
input_sync() |
上报同步事件,每次上报完一组事件,必须调用这个函数 |
input_unregister_device() |
注销 input 设备,驱动卸载的时候调用 |
input_free_device() |
释放 input 设备的内存,和 input_allocate_device 对应 |
二、实战一:基于 input 子系统的标准按键驱动开发
我们先从最简单的按键驱动入手,基于之前的硬件接线,用 GPIO0_A1 引脚接按键,实现一个标准的 input 按键驱动,让安卓系统原生识别。
1. 设备树配置
和之前的中断驱动一样,我们只需要在设备树里添加按键节点,配置 GPIO 引脚,这里我们用内核标准的gpio-keys驱动框架,不用自己写复杂的中断逻辑,内核已经帮我们做好了!
对,你没看错,Linux 内核已经自带了标准的 GPIO 按键驱动,我们只需要在设备树里配置好对应的引脚和按键码,内核会自动生成 input 设备,不用我们写一行驱动代码!这就是标准框架的魅力。
修改你的板级.dts 文件,添加下面的节点:
dts
/ {
// 标准GPIO按键节点,用内核自带的gpio-keys驱动
gpio_keys: gpio-keys {
compatible = "gpio-keys"; // 必须和内核驱动的compatible匹配
pinctrl-names = "default";
status = "okay";
autorepeat; // 支持长按重复上报
// 电源按键
key_power {
label = "power-key"; // 按键名称
gpios = <&gpio0 RK_PA1 GPIO_ACTIVE_LOW>; // 按键GPIO,低电平有效
linux,code = <KEY_POWER>; // 按键码,电源键,安卓原生识别
debounce-interval = <20>; // 防抖时间,20ms
wakeup-source; // 支持按键唤醒系统
};
// 音量加按键
key_volup {
label = "volume-up-key";
gpios = <&gpio0 RK_PA2 GPIO_ACTIVE_LOW>;
linux,code = <KEY_VOLUMEUP>;
debounce-interval = <20>;
};
// 音量减按键
key_voldown {
label = "volume-down-key";
gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;
linux,code = <KEY_VOLUMEDOWN>;
debounce-interval = <20>;
};
};
};
核心属性讲解
compatible = "gpio-keys":这个是核心,内核自带的 gpio-keys 驱动会匹配这个属性,自动加载驱动;linux,code = <KEY_POWER>:设置按键的标准键码,这些键码都是 Linux 内核和安卓系统约定好的,安卓系统收到 KEY_POWER 就会处理亮屏锁屏,KEY_VOLUMEUP 就会调节音量加,不用我们写任何上层代码;debounce-interval = <20>:内核自带的防抖功能,不用我们自己在驱动里写防抖逻辑;wakeup-source:标记这个按键可以唤醒休眠的系统,电源键必备。
2. 编译烧录验证
-
编译设备树,打包 boot.img,烧录到开发板,重启;
-
重启完成后,进入 ADB shell,验证 input 设备是否生成: bash
运行
adb shell su ls /dev/input/能看到
event0、event1这类设备文件,说明 input 设备已经生成了; -
查看我们的按键设备: bash
运行
cat /proc/bus/input/devices能看到我们配置的 gpio-keys 设备,里面有我们设置的 KEY_POWER、KEY_VOLUMEUP 等键码,说明设备注册成功了;
-
测试按键功能:用安卓自带的
getevent工具,实时查看按键事件:bash
运行
getevent按下按键,终端会实时打印对应的事件,松开也会打印,说明驱动工作正常!同时,按下音量加减键,安卓系统会自动调节音量,按下电源键会锁屏 / 亮屏,完全原生支持,不用我们写一行上层代码!
3. 自定义按键的驱动实现
如果你需要自定义按键,安卓系统没有原生对应的键码,我们可以自己写简单的 input 驱动,实现事件上报,核心代码极其简单:
c
运行
// 核心代码片段,完整驱动可以参考之前的中断驱动
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
int state = gpio_get_value(key_gpio);
// 上报按键事件:按下=1,松开=0
input_report_key(input_dev, KEY_USER1, !state);
// 必须上报同步事件
input_sync(input_dev);
return IRQ_HANDLED;
}
static int __init key_input_init(void)
{
// 1. 分配input设备
input_dev = input_allocate_device();
// 2. 设置设备名称
input_dev->name = "my-custom-key";
// 3. 设置支持的事件类型:按键事件+同步事件
set_bit(EV_KEY, input_dev->evbit);
set_bit(EV_SYN, input_dev->evbit);
// 4. 设置支持的按键码
set_bit(KEY_USER1, input_dev->keybit);
// 5. 注册input设备
input_register_device(input_dev);
// 6. 申请GPIO、注册中断,和之前的中断驱动一样
// ... 省略GPIO和中断注册代码
return 0;
}
三、实战二:GT911 触摸屏驱动开发,RK 平台最常用的触控芯片
搞定了按键,我们来看更复杂的触摸屏驱动,RK 平台的开发板,90% 用的都是汇顶的 GT911/GT9271 触控芯片,I2C 接口,我们基于 input 子系统,完成它的驱动适配。
1. GT911 核心原理
GT911 是一款 5 点触控芯片,通过 I2C 接口和主机通信,当有手指触摸屏幕的时候,芯片会把触摸点的 X/Y 坐标、触摸点数、压力值,通过 I2C 接口上报给主机,我们的驱动只需要通过 I2C 读取这些数据,然后通过 input 子系统上报给安卓系统就行。
2. 设备树配置
GT911 的驱动,内核已经自带了,我们只需要在设备树里添加对应的节点,配置好 I2C 引脚、中断引脚、复位引脚,就能直接用,不用自己写驱动代码!
修改你的板级.dts 文件,在 I2C1 节点里添加 GT911 设备节点:
dts
&i2c1 {
status = "okay";
clock-frequency = <400000>;
pinctrl-names = "default";
pinctrl-0 = <&i2c1_xfer>;
// GT911触摸屏节点
gt911: gt911@5d {
compatible = "goodix,gt911"; // 和内核驱动匹配
reg = <0x5d>; // GT911的I2C地址,一般是0x5d或者0x14
status = "okay";
// 中断引脚,触摸屏有触摸的时候,会触发中断
interrupt-parent = <&gpio0>;
interrupts = <RK_PB4 IRQ_TYPE_EDGE_FALLING>;
// 复位引脚
reset-gpios = <&gpio0 RK_PB5 GPIO_ACTIVE_LOW>;
// 触摸屏分辨率,根据你的屏幕修改
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;
// 最大支持的触摸点数
touchscreen-max-touch = <5>;
};
};
// 中断引脚的pinctrl配置
&pinctrl {
gt911 {
gt911_irq_pin: gt911-irq-pin {
rockchip,pins = <0 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};
3. 内核配置,开启 GT911 驱动
默认情况下,RK SDK 的内核已经开启了 GT911 驱动,如果没开启,我们只需要修改内核配置文件,开启对应的配置:
-
进入内核目录,执行菜单配置: bash
运行
cd kernel make ARCH=arm64 menuconfig -
进入以下路径,开启 GT911 驱动: plaintext
Device Drivers ---> Input device support ---> Touchscreens ---> <*> Goodix I2C touchscreen driver -
保存配置,重新编译内核,打包 boot.img,烧录到开发板。
4. 验证驱动是否正常工作
-
开发板重启后,进入 ADB shell,查看 input 设备: bash
运行
cat /proc/bus/input/devices能看到 Goodix GT911 的 input 设备,说明驱动加载成功了;
-
用 getevent 测试触摸事件: bash
运行
getevent用手指触摸屏幕,终端会实时打印 X/Y 坐标的 ABS 事件,说明驱动正常上报触摸坐标;
-
安卓系统验证:重启后,安卓系统会自动识别触摸屏,你可以直接用触摸操作安卓系统,完全不用做任何上层适配!
四、安卓系统 input 事件的处理流程
很多小白会问:为什么我们按标准上报事件,安卓系统就能自动处理?这里给大家简单讲一下安卓的 input 事件处理流程,让你知其然更知其所以然:
- 我们的驱动通过 input 子系统,把事件上报到
/dev/input/eventX设备文件; - 安卓系统的
EventHub服务,会监听所有的 input 设备文件,读取上报的事件; InputReader服务,会把读取到的原始事件,处理成安卓系统能识别的输入事件;InputDispatcher服务,把事件分发给当前焦点的 App,或者系统的按键处理服务;- 系统服务收到电源键、音量键的事件后,会自动执行对应的系统操作,App 收到触摸事件后,会更新 UI。
整个流程,安卓系统已经完全实现好了,我们只需要按 input 子系统的标准,正确上报事件就行,不用关心上层的处理逻辑,这就是标准化的好处。
五、小白 input 驱动必踩的坑,提前规避
- 坑 1:上报完事件,没有调用 input_sync () 同步 这是新手最常犯的错误,上报完按键 / 坐标事件后,没有上报同步事件,上层应用收不到事件,或者事件错乱。记住:每次上报完一组事件,必须调用
input_sync(),告诉内核这次事件上报完成了。 - 坑 2:没有设置对应的事件类型和事件码 比如你要上报按键事件,但是没有用
set_bit(EV_KEY, input_dev->evbit)设置支持按键事件,或者没有设置对应的按键码,事件上报了,上层也收不到。必须先告诉内核,你的设备支持什么事件,才能上报对应的事件。 - 坑 3:按键防抖没做好,多次触发事件 机械按键有抖动,会导致一次按下,多次上报事件。解决方法:要么用 gpio-keys 驱动自带的
debounce-interval属性,要么在中断里做防抖处理,20ms 以内的重复中断直接忽略。 - 坑 4:触摸屏坐标不对,触摸方向反了 设备树里的
touchscreen-size-x/touchscreen-size-y和屏幕实际分辨率不匹配,或者 X/Y 轴搞反了。解决方法:修改设备树里的分辨率,或者在驱动里调整 X/Y 坐标的交换、翻转。 - 坑 5:中断触发类型不对,触摸屏没反应GT911 触摸屏的中断是下降沿触发,如果你配置成了上升沿,就会导致有触摸的时候,中断不触发,驱动读不到坐标。解决方法:根据芯片 datasheet,配置正确的中断触发类型。
结尾说两句
这篇文章,我们彻底搞懂了 Linux input 子系统的核心原理,完成了标准按键驱动和 GT911 触摸屏驱动的开发,实现了安卓系统的原生识别和适配。掌握了 input 子系统,你就能适配所有的输入设备,不管是按键、触摸屏、键盘还是鼠标,都是一样的逻辑,一通百通。
下一篇,我们进入显示驱动的内容,手把手带你完成 MIPI/LVDS 屏的适配与调试,详解 DRM 显示框架,解决新手最头疼的 "屏点不亮、花屏、闪屏" 问题,教你怎么在 RK 平台上适配各种屏幕。
我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 input 驱动的问题,评论区留言,我都会一一回复。