3. 硬件接口与电气基础

好,我们进入触摸驱动实战的第一个硬核环节——I2C驱动。说实话,嵌入式Linux里90%的触摸屏都挂在I2C总线上。你搞懂了I2C驱动模型,就等于拿到了触摸驱动的入场券。

这一节,我会带着你把I2C适配器、设备驱动模型、probe函数、设备树匹配这几个核心概念串起来。不搞学院派那套,咱们直接说清楚:代码怎么写、设备树怎么配、probe怎么被调用。

3.1 I2C子系统三层架构

Linux的I2C子系统,说白了就是三层:

  • I2C核心层:提供总线注册、设备注册、数据传输的通用API。你不需要动它。
  • I2C适配器层:对应硬件上的I2C控制器。比如STM32MP1的I2C1、I2C2。这一层由SoC厂商写。
  • I2C设备驱动层:对应挂在总线上的具体外设。比如触摸芯片GT911、FT5x06。这一层,就是我们要写的。

我刚开始接触这个模型时,总觉得分层太多。后来在项目里调试一个I2C时钟芯片,发现适配器层和设备驱动层完全解耦,换一颗主控芯片,设备驱动一行代码都不用改。嗯,这时候才体会到分层的好处。

核心认知:你写触摸驱动,99%的工作量在设备驱动层。适配器层通常由BSP工程师或SoC厂商提供,你只需要在设备树里引用它。

下面这张图,把三层关系画清楚了:

Linux I2C 子系统三层架构 用户空间 (应用程序 / 触摸事件) I2C 设备驱动层 (触摸驱动) i2c_driver / probe / 数据传输 I2C 核心层 (通用API) i2c_add_driver / i2c_transfer / 总线管理 I2C 适配器层 (硬件控制器) i2c_adapter / 寄存器操作 / 时序控制 硬件 (I2C控制器 + 触摸芯片) 你写这一层 设备驱动层 内核已提供 几乎不用改 SoC厂商提供 设备树引用即可

3.2 设备驱动模型:i2c_driver 结构体

在Linux里,写一个I2C设备驱动,核心就是填充一个 i2c_driver 结构体。这个结构体定义在 include/linux/i2c.h 里。我挑几个关键字段说:

字段 类型 作用
driver.name const char * 驱动名称,用于和设备树中的 compatible 匹配
driver.of_match_table struct of_device_id * 设备树匹配表,核心中的核心
probe 函数指针 匹配成功后自动调用,初始化硬件
remove 函数指针 驱动卸载或设备拔出时调用
id_table struct i2c_device_id * 传统I2C设备ID表(非设备树方式)

来看一个实际的触摸驱动骨架。这是我之前做GT911驱动时的简化版:

#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/of.h>

/* 设备树匹配表 */
static const struct of_device_id gt911_of_match[] = {
    { .compatible = "goodix,gt911" },
    { /* 结束标志 */ }
};
MODULE_DEVICE_TABLE(of, gt911_of_match);

/* probe 函数 - 核心初始化 */
static int gt911_probe(struct i2c_client *client,
                       const struct i2c_device_id *id)
{
    struct device *dev = &client->dev;
    int ret;

    dev_info(dev, "GT911 probe start\n");

    /* 1. 检查设备是否存在 */
    ret = i2c_smbus_read_byte_data(client, 0x40);
    if (ret < 0) {
        dev_err(dev, "device not respond\n");
        return -ENODEV;
    }

    /* 2. 初始化触摸芯片 */
    /* ... 配置寄存器、申请中断、注册输入设备 ... */

    dev_info(dev, "GT911 probe success\n");
    return 0;
}

/* 驱动结构体 */
static struct i2c_driver gt911_driver = {
    .driver = {
        .name = "gt911",
        .of_match_table = gt911_of_match,
    },
    .probe = gt911_probe,
    .id_table = NULL,  /* 使用设备树,不需要传统ID表 */
};

module_i2c_driver(gt911_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

个人习惯:我写I2C驱动时,probe函数里第一件事一定是读一个寄存器确认设备在线。别问我为什么——有一次设备树地址写错了,probe照样被调用,结果后面所有操作都超时。加个寄存器校验,能省你半天调试时间。

3.3 probe函数:驱动与设备的握手时刻

probe函数什么时候被调用?简单说:当内核发现一个I2C设备和一个I2C驱动匹配成功时,就会调用驱动的probe函数。

匹配过程是这样的:

  1. 系统启动时,I2C核心层扫描设备树,为每个I2C子节点创建一个 i2c_client 对象。
  2. 当你的驱动调用 i2c_add_driver() 时,核心层遍历所有已注册的 i2c_client。
  3. 核心层拿驱动的 of_match_table 和每个 i2c_client 的 of_node 里的 compatible 属性做比较。
  4. 匹配上了?好,调用你的 probe 函数,把 i2c_client 指针传进去。

probe函数里,你通常要做这几件事:

  • 读取设备ID寄存器,确认硬件存在
  • 初始化设备(配置寄存器、设置工作模式)
  • 申请中断(触摸屏通常用中断通知CPU)
  • 注册输入子系统(把触摸事件上报给上层)
  • 初始化可能用到的定时器或工作队列

我曾经踩过的坑:probe函数里不要做耗时操作,比如msleep(1000)。内核在probe期间是持有锁的,你睡一秒,整个I2C总线都卡住。如果设备初始化确实需要等待,用异步方式或者放到工作队列里去做。

3.4 设备树匹配:compatible 属性的艺术

设备树匹配,说白了就是让内核知道:这个硬件节点,应该由哪个驱动来管。

匹配的核心是 compatible 属性。设备树里这样写:

&i2c2 {
    status = "okay";
    clock-frequency = <400000>;  /* 400KHz 快速模式 */

    gt911: touch@5d {
        compatible = "goodix,gt911";
        reg = <0x5d>;           /* I2C设备地址 */
        interrupt-parent = <&gpioa>;
        interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpiob 12 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <1024>;
        touchscreen-size-y = <600>;
    };
};

驱动里的 of_match_table 必须和设备树的 compatible 完全一致。我见过有人把"goodix,gt911"写成"goodix,gt911s",结果probe死活不调用——查了两天才发现是拼写错误。

匹配优先级是这样的:

匹配方式 优先级 说明
设备树 compatible 最高 现代内核首选,支持多平台
i2c_device_id 表 中等 传统方式,用于非设备树平台
ACPI 匹配 低 x86平台使用,嵌入式很少见

关键点:设备树里的 reg 属性就是I2C设备地址。触摸芯片的地址通常是7位,比如GT911默认是0x5d或0x14。注意:设备树里写的是7位地址,不是8位。我见过有人把0xBA(8位地址)写进去,结果内核怎么都扫描不到设备。

3.5 完整匹配流程:从设备树到probe

我把整个流程串起来,你感受一下:

  1. Bootloader加载设备树(.dtb文件)到内存。
  2. 内核解析设备树,为每个I2C控制器创建 i2c_adapter。
  3. 内核遍历I2C控制器下的子节点,为每个节点创建 i2c_client,并记录 compatible 和 reg。
  4. 你的驱动模块加载,调用 module_i2c_driver() 注册 i2c_driver。
  5. I2C核心层遍历所有 i2c_client,用驱动的 of_match_table 逐一匹配。
  6. 匹配成功?调用 probe,传入 i2c_client 指针。
  7. probe里,你通过 i2c_client->addr 拿到设备地址,开始通信。

嗯,流程不复杂,但每一步都有坑。我建议你刚开始做的时候,在probe函数里加一句 dev_info(dev, "client addr: 0x%02x\n", client->addr);,确认内核拿到的地址和你设备树里写的一致。

3.6 调试技巧:怎么看匹配有没有成功

匹配不成功,probe不调用,这是最常见的入门问题。我的排查步骤:

  • 检查设备树编译:dtc -I dtb -O dts xxx.dtb 反编译,确认节点存在。
  • 检查compatible字符串:驱动和设备树必须一字不差,包括大小写和逗号。
  • 检查I2C地址:reg 属性是否和芯片数据手册一致?7位地址别写成8位。
  • 检查I2C控制器状态:status = "okay" 写了吗?我忘过三次。
  • 看内核日志:dmesg | grep i2c 或 dmesg | grep gt911,看有没有错误信息。

我的调试利器:在驱动里加 #define DEBUG,然后打开 i2c-core 的调试日志。内核里 i2c-core.c 有很多 dev_dbg() 输出,能告诉你匹配过程的每一步。具体做法:echo 'file i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control。