2. 触摸屏技术基础
做触摸屏驱动,绕不开Linux输入子系统。说白了,它就是内核里一套标准化的“输入事件处理管道”。我最早接触它是在一个工控平板项目上,当时想把一个串口触摸屏接入系统,翻了不少资料才搞明白这套机制。今天咱们就把这块掰开揉碎了讲清楚。
2.1 输入子系统整体架构
输入子系统分三层:设备驱动层、核心层和事件处理层。你可以把它想象成一个工厂流水线——驱动层负责“采集原料”,核心层负责“分拣传送”,事件处理层负责“包装出货”。
嗯,这张图我建议你多看两眼。很多新手写驱动时只盯着硬件操作,忽略了核心层和事件处理层的配合,结果设备注册成功了,用户空间就是读不到事件——我当年就踩过这个坑。
2.2 input_dev 结构体深度解析
每个输入设备在内核里都对应一个 struct input_dev。它定义在 linux/input.h 里,字段很多,但常用的就这几类:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
const char * |
设备名称,用户空间通过它识别设备 |
phys |
const char * |
物理路径,比如 USB 的 bus/port 信息 |
id |
struct input_id |
总线类型、厂商 ID、产品 ID、版本号 |
evbit |
unsigned long [] |
支持的事件类型位图(EV_KEY、EV_ABS 等) |
keybit |
unsigned long [] |
支持的按键码位图(BTN_TOUCH、KEY_A 等) |
absbit |
unsigned long [] |
支持的绝对轴位图(ABS_X、ABS_Y、ABS_PRESSURE) |
absinfo |
struct input_absinfo [] |
每个绝对轴的最小值、最大值、分辨率等参数 |
evbit 一定要设置 EV_ABS 和 EV_KEY,keybit 要包含 BTN_TOUCH,absbit 要包含 ABS_X、ABS_Y、ABS_PRESSURE。少一个,用户空间的触摸框架就可能识别不到设备。
来看一个典型的触摸屏 input_dev 初始化代码:
struct input_dev *touch_dev;
touch_dev = input_allocate_device();
if (!touch_dev) {
pr_err("Failed to allocate input device\n");
return -ENOMEM;
}
touch_dev->name = "My Touch Screen";
touch_dev->phys = "i2c/touch@1a";
touch_dev->id.bustype = BUS_I2C;
/* 设置支持的事件类型 */
__set_bit(EV_ABS, touch_dev->evbit);
__set_bit(EV_KEY, touch_dev->evbit);
/* 设置支持按键 */
__set_bit(BTN_TOUCH, touch_dev->keybit);
/* 设置绝对轴参数 */
input_set_abs_params(touch_dev, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(touch_dev, ABS_Y, 0, 768, 0, 0);
input_set_abs_params(touch_dev, ABS_PRESSURE, 0, 255, 0, 0);
这里要注意 input_set_abs_params 的第五个参数是 fuzz(抖动容忍值),第六个是 flat(平坦区)。我做过一个项目,触摸屏坐标总在跳动,后来把 fuzz 设成 4,问题就解决了。
2.3 事件上报机制
驱动采集到硬件数据后,通过 input_event() 上报。核心函数就三个:
input_report_key(dev, code, value)— 上报按键事件input_report_abs(dev, axis, value)— 上报绝对轴事件input_sync(dev)— 同步事件,告诉核心层“这一批数据发完了”
input_sync()。结果用户空间收不到任何事件,因为核心层认为数据还没准备好。这个 sync 就像快递的“确认发货”按钮——不点它,包裹永远在仓库里。
触摸屏典型的上报流程:
/* 触摸按下 */
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_X, x);
input_report_abs(dev, ABS_Y, y);
input_report_abs(dev, ABS_PRESSURE, pressure);
input_sync(dev);
/* 触摸移动 */
input_report_abs(dev, ABS_X, new_x);
input_report_abs(dev, ABS_Y, new_y);
input_report_abs(dev, ABS_PRESSURE, new_pressure);
input_sync(dev);
/* 触摸抬起 */
input_report_key(dev, BTN_TOUCH, 0);
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);
你想想看,每次触摸操作至少产生三个事件包:按下、移动(可能多个)、抬起。每个包都必须以 input_sync() 结尾。这是协议的一部分,不能省。
2.4 与用户空间的交互
事件经过核心层分发后,由 evdev 处理器写入 /dev/input/eventX 设备节点。用户空间程序通过标准的文件操作接口读取:
#include <linux/input.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
struct input_event ev;
int fd = open("/dev/input/event0", O_RDONLY);
if (fd < 0) {
perror("open");
return -1;
}
while (1) {
read(fd, &ev, sizeof(ev));
if (ev.type == EV_ABS) {
printf("Axis %d = %d\n", ev.code, ev.value);
} else if (ev.type == EV_KEY) {
printf("Key %d = %d\n", ev.code, ev.value);
} else if (ev.type == EV_SYN) {
printf("--- Sync ---\n");
}
}
close(fd);
return 0;
}
struct input_event 的结构很简单:
| 字段 | 类型 | 说明 |
|---|---|---|
time |
struct timeval |
事件发生的时间戳,精度微秒 |
type |
__u16 |
事件类型(EV_KEY、EV_ABS、EV_SYN 等) |
code |
__u16 |
事件码(BTN_TOUCH、ABS_X、SYN_REPORT 等) |
value |
__s32 |
事件值(按键 0/1、坐标值、压力值) |
关键点:用户空间每次 read() 返回一个 struct input_event,大小固定为 24 字节(32位系统)或 32 字节(64位系统)。触摸屏的每次 sync 之间可能包含多个事件,用户空间需要循环读取直到收到 EV_SYN。
在实际的图形框架中(比如 Qt 的 libinput 或 Android 的 EventHub),它们会通过 select() 或 epoll() 监听多个 event 节点,一旦有数据可读就批量处理。嗯,这里有个性能细节:如果驱动上报频率太高,用户空间来不及消费,内核的输入缓冲区会溢出——我遇到过触摸卡顿,最后发现是中断处理里上报太频繁,加了限速就好了。
2.5 小结
输入子系统的设计思路很清晰:驱动只管采集和上报,核心层负责路由,事件处理器负责和用户空间对接。你写触摸屏驱动时,核心工作就是:
- 分配并初始化
input_dev,设置好evbit、keybit、absbit和absinfo - 在中断或轮询中采集硬件数据,调用
input_report_xxx()上报 - 每次上报完一组数据,记得
input_sync() - 注册设备,让核心层把它挂到输入设备链表里