4、触控驱动初始化:probe函数流程、设备树解析、GPIO与中断资源申请

好,咱们今天来聊聊触控驱动的初始化流程。说实话,这部分是驱动开发的「地基」。地基没打好,后面跑起来全是坑。我当年第一次写触控驱动时,就因为在probe函数里少做了一步资源检查,结果板子一上电就崩,查了整整两天才找到原因。

嗯,咱们一步步来。触控驱动的初始化,核心就三件事:probe函数怎么跑、设备树怎么读、GPIO和中断怎么要。说白了,就是让内核认识你的触控芯片,给它分配好资源,然后等着它干活。

4.1 probe函数:驱动的入口点

probe函数是驱动和设备的「握手环节」。当内核发现设备树里有一个触控节点,并且这个节点的compatible属性跟驱动匹配上了,就会调用probe函数。

我个人习惯把probe函数分成三个阶段:

  1. 资源获取阶段:读设备树、拿GPIO、申请中断
  2. 硬件初始化阶段:复位芯片、配置寄存器、校准
  3. 注册阶段:注册输入设备、创建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;
}
注意:申请GPIO后,一定要检查返回值。IS_ERR()检查错误,IS_ERR_OR_NULL()检查错误或空指针。我见过有人只检查了NULL,结果GPIO申请失败返回的是ERR_PTR,程序直接崩溃。

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;
}
核心要点:中断处理函数里不要做耗时操作。读I2C数据、解析协议、上报事件,这些都应该在线程化中断里做。上半部只做最必要的事情,比如清中断标志。

4.5 避坑指南:我踩过的那些坑

做触控驱动这么多年,有些坑我印象特别深。分享给你,希望你别再走一遍。

  • 我曾经在probe函数里直接调用msleep(),结果内核报「scheduling while atomic」。后来才知道,probe函数可能在原子上下文被调用,不能休眠。解决方案是用usleep_range()或者mdelay()
  • 我曾经忘记在设备树里设置中断触发方式,结果中断要么不触发,要么一直触发。查了三天,最后发现是设备树里少写了IRQ_TYPE_EDGE_FALLING
  • 我曾经在中断处理函数里直接调用input_sync(),结果导致中断延迟过高。后来把input_sync()放到工作队列里,问题解决。

嗯,这些坑说起来都是泪。但踩过一次,你就记住了。

4.6 小结

触控驱动的初始化,说白了就是三板斧:probe函数搭框架、设备树读配置、GPIO和中断要资源。每一步都有细节,每一步都可能踩坑。

我个人建议,写驱动时多想想「如果这个资源拿不到怎么办」、「如果这个函数失败了怎么办」。做好错误处理,你的驱动才能在各种奇葩硬件上稳定运行。

下一章,咱们聊聊触控数据的读取和上报。那才是真正跟用户交互的部分,也是驱动里最有趣的地方。