输入子系统:从input_event到多点触控的驱动层实现
一、场景痛点与技术挑战
Linux输入子系统是用户与设备交互的桥梁。
键盘、鼠标、触摸屏、游戏手柄都依赖它。
但驱动开发者在实践中遇到大量痛点。
多点触控协议理解困难。
内核定义了Type A和Type B两种上报协议。
Type A每次触摸独立上报完整数据包。
Type B用slot机制追踪持续触摸点。
两者切换逻辑不同,开发者常混淆。
input_event结构体字段含义模糊。
type、code、value三字段的组合规则复杂。
EV_ABS的code从ABS_X到ABS_MT_SLOT多达数十种。
触摸屏上报时ABS和EV_SYN的顺序有严格要求。
顺序错误导致上层收到残缺数据包。
触摸坐标映射和校准容易出错。
硬件坐标与屏幕坐标方向可能不一致。
旋转90度时X/Y轴需要互换。
多触点ID分配和回收逻辑容易出Bug。
触点离开后ID未回收,上层读到幽灵触点。
二、核心原理与架构设计
输入子系统分三层架构。
驱动层负责硬件交互。
从I2C或SPI总线读取触摸数据。
解析硬件寄存器中的坐标、压力、触点数。
将原始数据转换为input_event上报。
核心层负责事件路由。
input_core维护设备链表和handler链表。
设备注册时匹配对应handler。
事件从设备流经核心层到达handler。
Handler层负责事件分发。
evdev handler将事件写入字符设备节点。
mousedev handler模拟鼠标协议。
keyboard handler处理键盘事件到终端。
用户空间通过read()从/dev/input/eventX获取数据。
多点触控上报协议分两种。
Type A协议(无追踪)。
每次触摸完整上报所有触点。
SYN_REPORT之前连续上报多个ABS_MT_PACKET。
触点离开不做标记,靠缺失判断。
适合硬件不追踪触点ID的低端触摸屏。
Type B协议(有追踪)。
用ABS_MT_SLOT标记当前操作触点。
ABS_MT_TRACKING_ID分配和回收触点。
触点进入时分配新ID,离开时ID设-1。
剩余数据只在触点状态变化时上报。
适合硬件追踪触点的高端触摸屏。
三、生产级代码实现
多点触控驱动(Type B协议)
/* 基于Linux内核 drivers/input/touchscreen 简化实现 */ #include <linux/module.h> #include <linux/input.h> #include <linux/input/mt.h> #include <linux/i2c.h> #include <linux/interrupt.h> #include <linux/delay.h> #define MAX_TOUCH_POINTS 10 #define TOUCH_CHIP_REGS 0x00 struct ts_data { u16 x; u16 y; u8 pressure; u8 id; bool valid; }; struct mt_touch_dev { struct i2c_client *client; struct input_dev *input; struct ts_data points[MAX_TOUCH_POINTS]; int num_points; int max_slots; /* 触点ID到slot的映射表 */ int id_to_slot[MAX_TOUCH_POINTS]; /* slot使用状态跟踪 */ bool slot_used[MAX_TOUCH_POINTS]; }; /* 读取触摸芯片数据 */ static int mt_touch_read_data(struct mt_touch_dev *dev) { struct i2c_client *client = dev->client; u8 buf[MAX_TOUCH_POINTS * 6 + 2]; int ret; ret = i2c_smbus_read_i2c_block_data(client, TOUCH_CHIP_REGS, sizeof(buf), buf); if (ret < 0) { dev_err(&client->dev, "I2C read failed: %d\n", ret); return ret; } dev->num_points = buf[0] & 0x0F; if (dev->num_points > MAX_TOUCH_POINTS) dev->num_points = MAX_TOUCH_POINTS; for (int i = 0; i < dev->num_points; i++) { int offset = 2 + i * 6; dev->points[i].x = (buf[offset] << 8) | buf[offset + 1]; dev->points[i].y = (buf[offset + 2] << 8) | buf[offset + 3]; dev->points[i].pressure = buf[offset + 4]; dev->points[i].id = buf[offset + 5] & 0x0F; dev->points[i].valid = true; } return 0; } /* 查找或分配slot */ static int mt_touch_assign_slot(struct mt_touch_dev *dev, int tracking_id) { /* 已有映射则直接返回 */ if (dev->id_to_slot[tracking_id] >= 0) return dev->id_to_slot[tracking_id]; /* 查找空闲slot */ for (int slot = 0; slot < dev->max_slots; slot++) { if (!dev->slot_used[slot]) { dev->id_to_slot[tracking_id] = slot; dev->slot_used[slot] = true; return slot; } } /* 无空闲slot,丢弃该触点 */ dev_warn(&dev->client->dev, "No free slot for id %d\n", tracking_id); return -1; } /* 释放slot */ static void mt_touch_release_slot(struct mt_touch_dev *dev, int tracking_id) { int slot = dev->id_to_slot[tracking_id]; if (slot >= 0) { dev->slot_used[slot] = false; dev->id_to_slot[tracking_id] = -1; } } /* Type B协议上报触点数据 */ static void mt_touch_report_type_b(struct mt_touch_dev *dev) { struct input_dev *input = dev->input; int slot; /* 上报当前有效触点 */ for (int i = 0; i < dev->num_points; i++) { struct ts_data *pt = &dev->points[i]; slot = mt_touch_assign_slot(dev, pt->id); if (slot < 0) continue; input_mt_slot(input, slot); input_mt_report_slot_state(input, MT_TOOL_FINGER, true); input_report_abs(input, ABS_MT_POSITION_X, pt->x); input_report_abs(input, ABS_MT_POSITION_Y, pt->y); input_report_abs(input, ABS_MT_PRESSURE, pt->pressure); } /* 标记已离开的触点 */ for (int id = 0; id < MAX_TOUCH_POINTS; id++) { bool still_present = false; for (int i = 0; i < dev->num_points; i++) { if (dev->points[i].id == id) { still_present = true; break; } } if (!still_present && dev->id_to_slot[id] >= 0) { slot = dev->id_to_slot[id]; input_mt_slot(input, slot); input_mt_report_slot_state(input, MT_TOOL_FINGER, false); mt_touch_release_slot(dev, id); } } input_mt_sync_frame(input); input_sync(input); } /* 中断处理函数 */ static irqreturn_t mt_touch_irq(int irq, void *data) { struct mt_touch_dev *dev = data; if (mt_touch_read_data(dev) < 0) return IRQ_HANDLED; mt_touch_report_type_b(dev); return IRQ_HANDLED; } /* 设备初始化 */ static int mt_touch_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct mt_touch_dev *dev; struct input_dev *input; int ret; dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev->client = client; dev->max_slots = MAX_TOUCH_POINTS; /* 初始化ID映射表 */ for (int i = 0; i < MAX_TOUCH_POINTS; i++) { dev->id_to_slot[i] = -1; dev->slot_used[i] = false; } input = devm_input_allocate_device(&client->dev); if (!input) return -ENOMEM; input->name = "multi-touch-device"; input->id.bustype = BUS_I2C; /* 设置设备支持的事件类型 */ __set_bit(EV_ABS, input->evbit); __set_bit(EV_KEY, input->evbit); __set_bit(BTN_TOUCH, input->keybit); /* 设置ABS参数 */ input_set_abs_params(input, ABS_X, 0, 4096, 0, 0); input_set_abs_params(input, ABS_Y, 0, 4096, 0, 0); input_set_abs_params(input, ABS_MT_POSITION_X, 0, 4096, 0, 0); input_set_abs_params(input, ABS_MT_POSITION_Y, 0, 4096, 0, 0); input_set_abs_params(input, ABS_MT_PRESSURE, 0, 255, 0, 0); input_set_abs_params(input, ABS_MT_TRACKING_ID, 0, MAX_TOUCH_POINTS - 1, 0, 0); /* 初始化多点触控slot */ ret = input_mt_init_slots(input, MAX_TOUCH_POINTS, INPUT_MT_DIRECT); if (ret) return ret; dev->input = input; ret = input_register_device(input); if (ret) return ret; /* 注册中断 */ ret = devm_request_threaded_irq(&client->dev, client->irq, NULL, mt_touch_irq, IRQF_ONESHOT | IRQF_TRIGGER_FALLING, "mt-touch", dev); if (ret) { dev_err(&client->dev, "IRQ request failed: %d\n", ret); return ret; } i2c_set_clientdata(client, dev); return 0; } static const struct i2c_device_id mt_touch_id[] = { { "mt-touch-chip", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, mt_touch_id); static struct i2c_driver mt_touch_driver = { .driver = { .name = "mt-touch", }, .probe = mt_touch_probe, .id_table = mt_touch_id, }; module_i2c_driver(mt_touch_driver); MODULE_AUTHOR("zhongyiren"); MODULE_DESCRIPTION("Multi-touch driver with Type B protocol"); MODULE_LICENSE("GPL");触摸坐标校准与映射
"""触摸坐标校准工具""" import numpy as np class TouchCalibrator: """四点校准算法""" def __init__(self, screen_width: int, screen_height: int): self.screen_w = screen_width self.screen_h = screen_height def calibrate(self, raw_points: list, ref_points: list) -> dict: """ raw_points: 4个硬件坐标 [(x,y), ...] ref_points: 4个屏幕坐标 [(x,y), ...] 返回: 仿射变换矩阵参数 """ # 构建方程组 Ax = B # x' = a*x + b*y + c # y' = d*x + e*y + f A = [] B = [] for raw, ref in zip(raw_points, ref_points): rx, ry = raw sx, sy = ref A.append([rx, ry, 1, 0, 0, 0]) A.append([0, 0, 0, rx, ry, 1]) B.append(sx) B.append(sy) A = np.array(A, dtype=np.float64) B = np.array(B, dtype=np.float64) # 最小二乘求解 result, _, _, _ = np.linalg.lstsq(A, B, rcond=None) return { "a": round(result[0], 6), "b": round(result[1], 6), "c": round(result[2], 6), "d": round(result[3], 6), "e": round(result[4], 6), "f": round(result[5], 6), } def transform(self, raw_x: int, raw_y: int, params: dict) -> tuple: """将硬件坐标转换为屏幕坐标""" sx = params["a"] * raw_x + params["b"] * raw_y + params["c"] sy = params["d"] * raw_x + params["e"] * raw_y + params["f"] sx = max(0, min(self.screen_w, int(round(sx)))) sy = max(0, min(self.screen_h, int(round(sy)))) return (sx, sy)四、性能优化与工程实践
中断处理是触摸驱动的性能瓶颈。
触摸中断频率可达100-200Hz。
每帧数据包含5-10个触点的坐标。
中断处理耗时过长导致数据丢帧。
优化策略一:使用threaded_irq。
中断上半部仅唤醒线程。
数据解析和上报在下半部线程执行。
避免中断上下文中的长时间操作。
IRQF_ONESHOT标志保证中断线程完成前不重新触发。
优化策略二:I2C读取批量优化。
一次性读取整帧数据而非逐字节。
i2c_smbus_read_i2c_block_data一次传输60字节。
比逐寄存器读取减少90%的总线事务。
I2C时钟从100kHz提升到400kHz(Fast Mode)。
优化策略三:减少不必要的input_report。
Type B协议只在触点状态变化时上报。
稳定的触点只上报坐标变化。
大幅减少input_sync的调用频率。
触摸坐标校准实践。
四点校准算法是工业标准。
屏幕四角放置参考点。
硬件坐标与屏幕坐标的仿射变换矩阵。
校准参数存储在设备树或NVRAM中。
旋转0/90/180/270度各有独立校准参数。
Ghost触点防护。
触摸芯片的固件质量参差不齐。
偶发的虚假触点需要驱动层过滤。
连续3帧以上出现的触点才上报。
单帧触点直接丢弃,避免误触。
五、总结与技术提炼
输入子系统三层架构职责清晰。
驱动层读硬件数据,核心层路由事件,handler层分发到用户空间。Type B协议用slot追踪持续触点。
ABS_MT_SLOT标记操作触点,ABS_MT_TRACKING_ID分配和回收。
触点离开ID设-1,释放slot供新触点使用。id_to_slot映射表管理触点生命周期。
新触点分配空闲slot,离开时释放slot。
映射表避免ID和slot的错配。input_mt_init_slots初始化slot结构。
INPUT_MT_DIRECT模式直接映射坐标到屏幕。
input_mt_sync_frame替代旧的input_mt_sync。I2C批量读取优化数据传输。
一次block_read替代多次byte_read。
threaded_irq避免中断上下文长时间操作。四点仿射校准解决坐标映射。
硬件坐标与屏幕坐标的非线性偏差。
旋转场景需要独立校准参数集。
Ghost触点用帧数阈值过滤。