4、触摸屏驱动核心:probe函数实现、中断处理、坐标上报、多点触控协议
驱动移植做到这一步,其实真正的硬仗才刚刚开始。前面那些设备树配置、I2C 适配,说白了都是搭台子。probe 函数、中断处理、坐标上报、多点触控协议——这四个东西才是驱动真正唱戏的部分。
我个人习惯把 probe 函数比作驱动的「构造函数」。它不成功,后面一切免谈。而中断处理和坐标上报,则是驱动的心脏跳动。至于多点触控协议,嗯,这里面的坑我踩过不少,待会慢慢聊。
4.1 probe 函数:驱动的入场券
probe 函数什么时候被调用?当内核发现设备树里有一个 compatible 字符串,跟驱动里注册的 of_match_table 匹配上了,就会调用 probe。说白了,就是内核说:「嘿,你的硬件我找到了,你来初始化它吧。」
一个典型的 probe 函数,我通常会做这几件事:
- 获取设备树资源——比如中断号、GPIO、供电电压。
- 初始化 I2C 客户端——设置设备的寄存器、工作模式。
- 分配输入设备结构体——也就是
struct input_dev。 - 设置设备属性和事件类型——告诉内核这个设备能上报什么。
- 注册输入设备——调用
input_register_device。 - 申请中断——注册中断处理函数。
给你看一段我实际项目里用过的 probe 骨架代码:
static int touch_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct touch_data *ts;
struct input_dev *input;
int ret;
// 1. 分配私有数据结构
ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
if (!ts)
return -ENOMEM;
// 2. 获取中断号
ts->irq = client->irq;
if (!ts->irq) {
dev_err(&client->dev, "no IRQ specified\n");
return -EINVAL;
}
// 3. 初始化输入设备
input = devm_input_allocate_device(&client->dev);
if (!input)
return -ENOMEM;
input->name = "Touchscreen";
input->id.bustype = BUS_I2C;
input->dev.parent = &client->dev;
// 4. 设置支持的事件类型
__set_bit(EV_ABS, input->evbit);
__set_bit(EV_KEY, input->evbit);
__set_bit(BTN_TOUCH, input->keybit);
// 5. 设置触摸坐标范围
input_set_abs_params(input, ABS_MT_POSITION_X, 0, X_MAX, 0, 0);
input_set_abs_params(input, ABS_MT_POSITION_Y, 0, Y_MAX, 0, 0);
input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
// 6. 注册输入设备
ret = input_register_device(input);
if (ret) {
dev_err(&client->dev, "failed to register input device\n");
return ret;
}
// 7. 申请中断
ret = devm_request_threaded_irq(&client->dev, ts->irq, NULL,
touch_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"touch", ts);
if (ret) {
dev_err(&client->dev, "failed to request IRQ\n");
return ret;
}
ts->input = input;
i2c_set_clientdata(client, ts);
return 0;
}
devm_ 系列 API。这样一旦驱动卸载或者 probe 失败,资源会自动释放,省得你手动 cleanup 写到手软。我曾经在一个项目里忘了释放中断,结果每次 insmod/rmmod 循环三次,系统就崩了——排查了两天才发现是资源泄漏。
4.2 中断处理:别让 CPU 空转
触摸屏的中断处理,我一般用线程化中断。为什么?因为触摸屏的 I2C 读取操作可能会阻塞,如果在顶半部做,会拖死整个系统。
线程化中断的好处很明显:
- 顶半部只做最轻量的事——比如唤醒一个等待队列。
- 底半部(线程)可以安心地读 I2C、解析数据、上报坐标。
- 不会阻塞其他高优先级中断。
来看一个实际的中断处理函数:
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
struct touch_data *ts = dev_id;
// 读取触摸数据
touch_read_data(ts);
// 解析并上报
touch_report_data(ts);
return IRQ_HANDLED;
}
你可能会问:「为什么不在顶半部直接读 I2C?」
嗯,I2C 通信本身是同步的,如果总线上有其他设备在传输,你的中断处理函数就会被阻塞。在顶半部里阻塞,相当于把整个系统的中断响应都卡住了。我曾经见过一个产品,触摸屏中断处理里直接调了 i2c_master_send,结果系统音频出现爆音——因为音频 DMA 的中断被堵了。
4.3 坐标上报:从裸数据到用户空间
坐标上报,说白了就是把触摸屏芯片返回的原始数据,转换成用户空间能理解的坐标值。
原始数据通常长这样:
// 假设从芯片读到的 5 字节数据
byte[0]: 状态标志 (触摸/释放)
byte[1]: X 坐标高字节
byte[2]: X 坐标低字节
byte[3]: Y 坐标高字节
byte[4]: Y 坐标低字节
解析之后,我们需要调用 input 子系统提供的 API 来上报:
static void touch_report_data(struct touch_data *ts)
{
int x, y;
u8 buf[5];
// 读取原始数据
touch_i2c_read(ts->client, buf, 5);
// 解析坐标
x = (buf[1] << 8) | buf[2];
y = (buf[3] << 8) | buf[4];
// 上报触摸事件
input_report_key(ts->input, BTN_TOUCH, 1);
input_report_abs(ts->input, ABS_MT_POSITION_X, x);
input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
input_mt_sync(ts->input);
input_sync(ts->input);
}
这里有个细节:input_sync 必须调用。它告诉内核:「这一批事件发完了,你可以打包提交给上层了。」如果不调,用户空间永远收不到数据。
input_sync。漏掉它,触摸屏就会「卡死」——不是真的死,而是数据积压在缓冲区里出不去。
4.4 多点触控协议:Type A 还是 Type B?
多点触控协议,Linux 内核提供了两种:Type A 和 Type B。
| 特性 | Type A | Type B |
|---|---|---|
| 适用场景 | 不支持硬件追踪的触摸屏 | 支持硬件追踪(有 Slot ID) |
| 上报方式 | 每次上报所有触摸点 | 按 Slot 分别上报,只更新变化的点 |
| API 调用 | input_mt_sync |
input_mt_slot + input_mt_report_slot_state |
| 典型芯片 | 较老的电阻屏、部分电容屏 | 现代电容屏(如 Goodix、FocalTech) |
我个人建议:能用 Type B 就用 Type B。为什么?
- Type B 更高效——它只上报变化的触摸点,而不是全量刷新。
- Type B 支持 Slot 概念,可以追踪每个手指的连续移动。
- Type B 与 MTK(多点触控内核)的集成更自然,手势识别、防抖处理都更方便。
Type B 的典型实现长这样:
static void touch_report_mt(struct touch_data *ts, int slot, int x, int y, bool active)
{
// 设置当前操作的 Slot
input_mt_slot(ts->input, slot);
// 报告触摸状态
input_mt_report_slot_state(ts->input, MT_TOOL_FINGER, active);
if (active) {
// 上报坐标
input_report_abs(ts->input, ABS_MT_POSITION_X, x);
input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
}
// 别忘了 sync
input_sync(ts->input);
}
4.5 本章知识体系
下面这张图,是我自己整理的多点触控驱动核心流程。你看一眼,应该能对整个脉络有个直观印象:
从 probe 开始,一路走到坐标上报,中间每一步都有坑。但只要你把设备树资源拿对了、中断处理用线程化、坐标上报别漏 input_sync、多点触控选 Type B——这套组合拳打下来,驱动基本就稳了。
嗯,这一章的内容就到这里。代码你可以直接拿去用,但记得根据你手里的芯片手册调整寄存器地址和数据格式。每个芯片的协议都有细微差别,这是移植工作里最磨人的地方。