4. 触摸驱动初始化:注册input设备、设置设备属性、中断申请与配置
触摸驱动的初始化,说白了就是让内核知道“有个触摸屏在这里,它长这样,它用这根中断线通知我”。
我刚开始写触摸驱动时,总觉得初始化就是填几个结构体,后来踩了坑才明白——顺序错了,整个设备可能都起不来。
这一节,我们一步步拆解初始化的三个核心动作:注册 input 设备、设置设备属性、中断申请与配置。
4.1 注册 input 设备——让内核认识你
触摸屏在 Linux 里属于 input 子系统。你要做的第一件事,就是分配并注册一个 input_dev 结构体。
我个人习惯在 probe 函数里这样写:
struct input_dev *input_dev;
int ret;
// 1. 分配 input 设备
input_dev = devm_input_allocate_device(&client->dev);
if (!input_dev) {
dev_err(&client->dev, "Failed to allocate input device\n");
return -ENOMEM;
}
// 2. 设置设备名称和物理路径
input_dev->name = "ft5x06_touch";
input_dev->phys = "i2c/ft5x06/input0";
input_dev->id.bustype = BUS_I2C;
// 3. 设置支持的事件类型
set_bit(EV_ABS, input_dev->evbit); // 绝对坐标事件
set_bit(EV_KEY, input_dev->evbit); // 按键事件(触摸按下/抬起)
// 4. 设置支持的按键类型
set_bit(BTN_TOUCH, input_dev->keybit); // 触摸按键
// 5. 设置绝对坐标参数
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 800, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 480, 0, 0);
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
// 6. 注册 input 设备
ret = input_register_device(input_dev);
if (ret) {
dev_err(&client->dev, "Failed to register input device: %d\n", ret);
return ret;
}
这里有个细节——devm_input_allocate_device 是带资源管理的版本。设备移除时,内核会自动帮你释放。我早期用手动分配,漏了一次 input_free_device,内存泄漏查了一整天。
ABS_MT_SLOT 和 input_mt_init_slots()。单点触摸可以跳过,但多点触摸必须初始化 slot。
4.2 设置设备属性——告诉上层怎么用
注册完 input 设备,你还要告诉上层:
- 你的触摸屏分辨率是多少?
- 支持几个手指?
- 上报的数据格式是什么?
这些信息通过 input_set_abs_params 和 input_mt_init_slots 来配置。
来看一个实际项目中我常用的配置:
// 设置多点触摸协议
input_mt_init_slots(input_dev, MAX_TOUCH_POINTS, 0);
// 设置 X/Y 轴范围
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, SCREEN_WIDTH - 1, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, SCREEN_HEIGHT - 1, 0, 0);
// 设置触摸压力范围(如果硬件支持)
input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 255, 0, 0);
// 设置触摸面积
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
input_set_abs_params(input_dev, ABS_MT_WIDTH_MAJOR, 0, 255, 0, 0);
input_set_abs_params 的五个参数分别是:
| 参数 | 含义 | 示例值 |
|---|---|---|
| axis | 坐标轴类型 | ABS_MT_POSITION_X |
| min | 最小值 | 0 |
| max | 最大值 | 799 |
| fuzz | 抖动容忍值 | 0(不滤波) |
| flat | 平坦区(死区) | 0 |
4.3 中断申请与配置——让硬件能叫你
触摸屏是中断驱动的。手指一碰,硬件拉低中断引脚,驱动就要去读数据。
申请中断的代码通常长这样:
struct device *dev = &client->dev;
struct touch_data *ts = dev_get_drvdata(dev);
// 获取中断号
int irq = client->irq;
if (!irq) {
dev_err(dev, "No IRQ specified\n");
return -EINVAL;
}
// 申请中断
ret = devm_request_threaded_irq(dev, irq,
NULL, // 顶半部,设为 NULL 表示全部在底半部处理
touch_irq_handler, // 底半部处理函数
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"ft5x06_touch", ts);
if (ret) {
dev_err(dev, "Failed to request IRQ %d: %d\n", irq, ret);
return ret;
}
这里我用了 devm_request_threaded_irq,而不是传统的 request_irq。为什么?
- 顶半部(hardirq):只做最少的处理,比如清中断标志。我直接传 NULL,全部交给底半部。
- 底半部(threaded handler):可以睡眠,可以调用 i2c 读写。触摸屏的坐标读取就在这里做。
- IRQF_ONESHOT:保证中断处理完成前,硬件不会再次触发中断。这对电平触发的中断特别重要。
4.4 中断处理函数的实现
底半部处理函数是触摸驱动的核心。它负责:
- 读取触摸点坐标
- 上报给 input 子系统
- 通知上层应用
一个典型的中断处理函数:
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
struct touch_data *ts = dev_id;
int i, touch_count;
u8 buf[TOUCH_DATA_LEN];
// 1. 读取触摸数据
if (i2c_master_recv(ts->client, buf, TOUCH_DATA_LEN) != TOUCH_DATA_LEN) {
dev_err_ratelimited(&ts->client->dev, "I2C read failed\n");
return IRQ_HANDLED;
}
// 2. 解析触摸点数
touch_count = buf[2] & 0x0F;
// 3. 上报每个触摸点
for (i = 0; i < touch_count; i++) {
int x = (buf[3 + i * 6] << 8) | buf[4 + i * 6];
int y = (buf[5 + i * 6] << 8) | buf[6 + i * 6];
input_mt_slot(ts->input_dev, i);
input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, true);
input_report_abs(ts->input_dev, ABS_MT_POSITION_X, x);
input_report_abs(ts->input_dev, ABS_MT_POSITION_Y, y);
}
// 4. 上报同步事件
input_mt_sync_frame(ts->input_dev);
input_sync(ts->input_dev);
return IRQ_HANDLED;
}
注意这里我用了 dev_err_ratelimited 而不是 dev_err。为什么?
我曾经在一个项目里,I2C 总线偶尔出错,结果 dev_err 每秒打印几千条日志,直接把内核 log 缓冲区撑爆了。从那以后,凡是中断里可能高频触发的错误打印,我都用 ratelimited 版本。
4.5 初始化流程总览
把上面三步串起来,完整的初始化流程是这样的:
static int touch_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct touch_data *ts;
int ret;
// 1. 分配私有数据结构
ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
if (!ts)
return -ENOMEM;
ts->client = client;
i2c_set_clientdata(client, ts);
// 2. 硬件初始化(复位、配置寄存器等)
ret = touch_hardware_init(client);
if (ret)
return ret;
// 3. 注册 input 设备
ret = touch_input_register(ts);
if (ret)
return ret;
// 4. 申请中断
ret = touch_request_irq(ts);
if (ret) {
input_unregister_device(ts->input_dev);
return ret;
}
dev_info(&client->dev, "Touch driver initialized successfully\n");
return 0;
}
这个顺序是固定的:硬件初始化 → input 注册 → 中断申请。调换了,中断来了但 input 设备还没注册好,数据就丢了。
dev_info 打印。调试时一眼就能看出驱动是否加载成功,不用去翻 dmesg 找错误码。
4.6 本章核心流程图
下面这张图展示了触摸驱动初始化的完整数据流和依赖关系:
这张图把初始化的六个步骤串起来了。你写驱动时,可以对着这个流程检查自己的 probe 函数——少一步都不行。
好了,这一节的内容就到这里。初始化是触摸驱动的基石,写扎实了,后面的数据上报和事件处理才能稳。