4、触控驱动初始化:probe函数流程、设备树解析、GPIO与中断资源申请
好,咱们今天来聊聊触控驱动的初始化流程。说实话,这部分是驱动开发的「地基」。地基没打好,后面跑起来全是坑。我当年第一次写触控驱动时,就因为在probe函数里少做了一步资源检查,结果板子一上电就崩,查了整整两天才找到原因。
嗯,咱们一步步来。触控驱动的初始化,核心就三件事:probe函数怎么跑、设备树怎么读、GPIO和中断怎么要。说白了,就是让内核认识你的触控芯片,给它分配好资源,然后等着它干活。
4.1 probe函数:驱动的入口点
probe函数是驱动和设备的「握手环节」。当内核发现设备树里有一个触控节点,并且这个节点的compatible属性跟驱动匹配上了,就会调用probe函数。
我个人习惯把probe函数分成三个阶段:
- 资源获取阶段:读设备树、拿GPIO、申请中断
- 硬件初始化阶段:复位芯片、配置寄存器、校准
- 注册阶段:注册输入设备、创建sysfs接口
你想想看,这三个阶段顺序不能乱。资源都没拿到,你初始化个啥?
来看一个典型的probe函数骨架:
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;
// 2. 获取设备树资源
ret = touch_parse_dt(ts, &client->dev);
if (ret) {
dev_err(&client->dev, "设备树解析失败\n");
return ret;
}
// 3. 硬件初始化
ret = touch_hw_init(ts);
if (ret) {
dev_err(&client->dev, "硬件初始化失败\n");
return ret;
}
// 4. 申请中断
ret = devm_request_threaded_irq(&client->dev, client->irq,
NULL, touch_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"touch", ts);
if (ret) {
dev_err(&client->dev, "中断申请失败\n");
return ret;
}
// 5. 注册输入设备
ret = touch_input_register(ts, &client->dev);
if (ret)
return ret;
dev_info(&client->dev, "触控驱动初始化完成\n");
return 0;
}
这里有个细节:devm_系列函数。我建议你尽量用这些带设备资源管理的函数。为什么?因为probe失败或者驱动卸载时,内核会自动帮你释放资源。我曾经在项目里手动管理资源,结果卸载时漏了一个GPIO没释放,下次加载直接死机。嗯,从那以后我再也不敢手动管理了。
4.2 设备树解析:让驱动认识硬件
设备树是驱动和硬件之间的「翻译官」。触控芯片的I2C地址、中断引脚、复位引脚、电源电压,这些信息都写在设备树里。
一个典型的触控设备树节点长这样:
&i2c3 {
touch@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>;
irq-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
touchscreen-size-x = <1024>;
touchscreen-size-y = <768>;
touchscreen-max-fingers = <5>;
};
};
解析设备树时,我习惯用device_property_read_*系列函数。这些函数兼容了设备树和ACPI两种方式,代码移植性更好。
来看解析代码:
static int touch_parse_dt(struct touch_data *ts, struct device *dev)
{
struct device_node *np = dev->of_node;
int ret;
// 读取分辨率
ret = device_property_read_u32(dev, "touchscreen-size-x", &ts->max_x);
if (ret) {
dev_warn(dev, "未指定X分辨率,使用默认值\n");
ts->max_x = 1024;
}
ret = device_property_read_u32(dev, "touchscreen-size-y", &ts->max_y);
if (ret) {
dev_warn(dev, "未指定Y分辨率,使用默认值\n");
ts->max_y = 768;
}
// 读取最大触控点数
ret = device_property_read_u32(dev, "touchscreen-max-fingers", &ts->max_fingers);
if (ret) {
dev_warn(dev, "未指定最大触控点数,使用默认值5\n");
ts->max_fingers = 5;
}
// 获取复位GPIO
ts->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(ts->reset_gpio)) {
dev_err(dev, "获取复位GPIO失败\n");
return PTR_ERR(ts->reset_gpio);
}
// 获取中断GPIO
ts->irq_gpio = devm_gpiod_get_optional(dev, "irq", GPIOD_IN);
if (IS_ERR(ts->irq_gpio)) {
dev_err(dev, "获取中断GPIO失败\n");
return PTR_ERR(ts->irq_gpio);
}
return 0;
}
devm_gpiod_get_optional而不是devm_gpiod_get。前者允许GPIO不存在,驱动可以继续运行。有些板子可能没接复位引脚,你总不能因为这个就让驱动加载失败吧?
4.3 GPIO资源申请:别小看这根线
GPIO在触控驱动里主要干两件事:复位和中断检测。申请GPIO时,有几个坑你得注意。
第一,GPIO的默认状态。申请复位GPIO时,我建议初始化为高电平。为什么?因为很多触控芯片是高电平正常工作,低电平复位。如果你初始化为低电平,芯片一上电就被复位了,可能连I2C通信都建立不起来。
第二,中断GPIO的配置。中断GPIO要配置为输入,并且要设置内部上拉或下拉。我曾经遇到一个情况:中断GPIO浮空,结果芯片没触发中断,GPIO电平却在乱跳,导致中断风暴。嗯,查了半天才发现是内部上拉没开。
来看GPIO申请的完整流程:
// 复位GPIO:输出,初始高电平
ts->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(ts->reset_gpio)) {
dev_err(dev, "复位GPIO申请失败\n");
return PTR_ERR(ts->reset_gpio);
}
// 中断GPIO:输入,带上拉
ts->irq_gpio = devm_gpiod_get(dev, "irq", GPIOD_IN);
if (IS_ERR(ts->irq_gpio)) {
dev_err(dev, "中断GPIO申请失败\n");
return PTR_ERR(ts->irq_gpio);
}
// 设置内部上拉
gpiod_set_consumer_name(ts->irq_gpio, "touch_irq");
ret = gpiod_direction_input(ts->irq_gpio);
if (ret) {
dev_err(dev, "设置中断GPIO方向失败\n");
return ret;
}
4.4 中断资源申请:响应触控的关键
中断是触控驱动的「心脏」。没有中断,你就不知道用户什么时候点了屏幕。
申请中断时,有几个参数要特别注意:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| 中断触发方式 | 上升沿、下降沿、电平触发 | 看芯片手册,大部分触控芯片用下降沿 |
| 中断处理函数 | 上半部还是下半部 | 用线程化中断,省心 |
| 中断标志 | IRQF_ONESHOT等 | 线程化中断必须加IRQF_ONESHOT |
我个人强烈推荐使用线程化中断(threaded irq)。为什么?因为触控驱动的中断处理里,通常要读I2C数据,而I2C通信可能会休眠。如果你用普通中断,在中断上下文里调用休眠函数,内核会直接报错。
来看中断申请的代码:
// 获取中断号
ts->irq = gpiod_to_irq(ts->irq_gpio);
if (ts->irq < 0) {
dev_err(dev, "获取中断号失败\n");
return ts->irq;
}
// 申请线程化中断
ret = devm_request_threaded_irq(dev, ts->irq,
NULL, // 上半部设为NULL,全部交给下半部
touch_irq_handler, // 线程化处理函数
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"touch", ts);
if (ret) {
dev_err(dev, "中断申请失败,错误码:%d\n", ret);
return ret;
}
4.5 避坑指南:我踩过的那些坑
做触控驱动这么多年,有些坑我印象特别深。分享给你,希望你别再走一遍。
- 我曾经在probe函数里直接调用msleep(),结果内核报「scheduling while atomic」。后来才知道,probe函数可能在原子上下文被调用,不能休眠。解决方案是用
usleep_range()或者mdelay()。 - 我曾经忘记在设备树里设置中断触发方式,结果中断要么不触发,要么一直触发。查了三天,最后发现是设备树里少写了
IRQ_TYPE_EDGE_FALLING。 - 我曾经在中断处理函数里直接调用input_sync(),结果导致中断延迟过高。后来把input_sync()放到工作队列里,问题解决。
嗯,这些坑说起来都是泪。但踩过一次,你就记住了。
4.6 小结
触控驱动的初始化,说白了就是三板斧:probe函数搭框架、设备树读配置、GPIO和中断要资源。每一步都有细节,每一步都可能踩坑。
我个人建议,写驱动时多想想「如果这个资源拿不到怎么办」、「如果这个函数失败了怎么办」。做好错误处理,你的驱动才能在各种奇葩硬件上稳定运行。
下一章,咱们聊聊触控数据的读取和上报。那才是真正跟用户交互的部分,也是驱动里最有趣的地方。