4. FT5x06 驱动框架:驱动入口、probe 函数、设备树解析、I2C 读写函数封装

FT5x06 系列电容触摸控制器(FT5206、FT5306、FT5406、FT5506、FT5606)在嵌入式 Linux 中通常通过 I2C 总线与主控通信。本章节深入剖析其驱动框架的四个核心模块:驱动入口与出口、probe 探测函数、设备树匹配与解析、I2C 读写封装。理解这些模块是后续进行多点触控性能调优的基础。

学习提示: FT5x06 与 GT911 的驱动框架在 probe 流程和设备树解析上高度相似,但 FT5x06 的寄存器映射更简单,适合作为入门分析对象。掌握 FT5x06 的框架后,迁移到 GT911 只需调整寄存器地址和协议细节。

4.1 驱动入口与出口:module_init / module_exit

Linux 驱动通过 module_init 和 module_exit 宏向内核注册入口和出口函数。FT5x06 驱动采用标准的 I2C 驱动模型,入口函数执行 I2C 驱动注册,出口函数执行注销。

/* 驱动入口函数 */
static int __init ft5x06_ts_init(void)
{
    int ret;

    ret = i2c_add_driver(&ft5x06_ts_driver);
    if (ret)
        pr_err("FT5x06: Failed to register I2C driver, err=%d\n", ret);
    else
        pr_info("FT5x06: Touchscreen driver initialized\n");

    return ret;
}

/* 驱动出口函数 */
static void __exit ft5x06_ts_exit(void)
{
    i2c_del_driver(&ft5x06_ts_driver);
    pr_info("FT5x06: Touchscreen driver removed\n");
}

module_init(ft5x06_ts_init);
module_exit(ft5x06_ts_exit);

MODULE_AUTHOR("Embedded Touch Expert");
MODULE_DESCRIPTION("FT5x06 Capacitive Touchscreen Driver");
MODULE_LICENSE("GPL v2");
关键点: i2c_add_driver() 会将驱动挂接到 I2C 核心的总线驱动链表上。当 I2C 核心检测到与 id_table 匹配的设备时,自动调用 probe 函数。出口函数必须确保所有资源已释放,否则可能导致内核 panic。

4.2 I2C 驱动结构体与 id_table

驱动核心结构体 struct i2c_driver 包含 probe、remove、id_table 等关键成员。id_table 用于设备树或 ACPI 匹配,是驱动与硬件绑定的桥梁。

成员 类型 说明
probe int (*probe)(struct i2c_client *, const struct i2c_device_id *) 设备匹配成功后调用,完成硬件初始化、中断注册、输入设备注册
remove int (*remove)(struct i2c_client *) 设备移除或驱动卸载时调用,释放资源
id_table const struct i2c_device_id * 传统 I2C 设备 ID 匹配表,与设备树 compatible 属性配合使用
driver struct device_driver 内嵌通用驱动结构,包含 name、of_match_table、pm 等
/* I2C 设备 ID 表 */
static const struct i2c_device_id ft5x06_ts_id[] = {
    { "ft5x06", 0 },
    { "ft5206", 0 },
    { "ft5306", 0 },
    { "ft5406", 0 },
    { "ft5506", 0 },
    { "ft5606", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_ts_id);

/* 设备树匹配表 */
static const struct of_device_id ft5x06_of_match[] = {
    { .compatible = "focaltech,ft5x06", },
    { .compatible = "focaltech,ft5206", },
    { .compatible = "focaltech,ft5306", },
    { .compatible = "focaltech,ft5406", },
    { .compatible = "focaltech,ft5506", },
    { .compatible = "focaltech,ft5606", },
    { }
};
MODULE_DEVICE_TABLE(of, ft5x06_of_match);

/* I2C 驱动结构体 */
static struct i2c_driver ft5x06_ts_driver = {
    .driver = {
        .name   = "ft5x06_ts",
        .of_match_table = ft5x06_of_match,
        .pm     = &ft5x06_ts_pm_ops,  /* 电源管理操作 */
    },
    .probe      = ft5x06_ts_probe,
    .remove     = ft5x06_ts_remove,
    .id_table   = ft5x06_ts_id,
};

4.3 probe 函数:驱动初始化的核心战场

probe 函数是驱动与硬件第一次握手的场所。FT5x06 的 probe 流程包含:设备树属性解析、I2C 客户端初始化、硬件复位、固件版本校验、中断申请、输入设备注册、多点触控协议配置。

常见陷阱: probe 失败时,必须确保已申请的资源全部回滚。例如,如果中断申请失败,需要先释放之前申请的输入设备。建议使用 goto 错误处理模式,确保每个错误标签释放对应层级的资源。
static int ft5x06_ts_probe(struct i2c_client *client,
                           const struct i2c_device_id *id)
{
    struct ft5x06_ts_data *ts;
    struct input_dev *input;
    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. 设备树解析:获取 GPIO、最大触摸点数、交换坐标等参数 */
    ret = ft5x06_parse_dt(&client->dev, ts);
    if (ret)
        return ret;

    /* 3. 硬件复位:拉低复位引脚,等待芯片稳定 */
    gpiod_set_value_cansleep(ts->reset_gpio, 0);
    msleep(5);
    gpiod_set_value_cansleep(ts->reset_gpio, 1);
    msleep(300);

    /* 4. 读取芯片版本,验证通信正常 */
    ret = ft5x06_read_version(ts);
    if (ret) {
        dev_err(&client->dev, "Failed to read chip version\n");
        return ret;
    }

    /* 5. 申请输入设备 */
    input = devm_input_allocate_device(&client->dev);
    if (!input)
        return -ENOMEM;

    input->name = "FT5x06 Touchscreen";
    input->id.bustype = BUS_I2C;
    input->dev.parent = &client->dev;

    /* 6. 设置多点触控协议:MT Protocol B */
    input_set_abs_params(input, ABS_MT_POSITION_X, 0, ts->max_x, 0, 0);
    input_set_abs_params(input, ABS_MT_POSITION_Y, 0, ts->max_y, 0, 0);
    input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
    input_set_abs_params(input, ABS_MT_WIDTH_MAJOR, 0, 255, 0, 0);
    input_set_abs_params(input, ABS_MT_TRACKING_ID, 0, ts->max_touch_num, 0, 0);

    ts->input_dev = input;

    /* 7. 申请中断 */
    ret = devm_request_threaded_irq(&client->dev, client->irq,
                                    NULL, ft5x06_ts_irq_handler,
                                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                    "ft5x06_ts", ts);
    if (ret) {
        dev_err(&client->dev, "Failed to request IRQ %d\n", client->irq);
        return ret;
    }

    /* 8. 注册输入设备 */
    ret = input_register_device(input);
    if (ret) {
        dev_err(&client->dev, "Failed to register input device\n");
        return ret;
    }

    dev_info(&client->dev, "FT5x06 probe success, ver=%d, max_touch=%d\n",
             ts->chip_ver, ts->max_touch_num);

    return 0;
}
性能调优视角: probe 中的硬件复位时序(5ms 低电平 + 300ms 高电平)直接影响芯片启动稳定性。如果复位时间不足,芯片可能无法正确响应 I2C 通信。实际调试时,建议用示波器抓取复位引脚波形,确保时序符合 FT5x06 数据手册要求。

4.4 设备树解析:从 DTS 到驱动参数

设备树(Device Tree)是嵌入式 Linux 描述硬件拓扑的标准方式。FT5x06 驱动通过 of_property_read_* 系列 API 从设备树节点中提取配置参数。典型的设备树节点如下:

/* 设备树节点示例 */
&i2c2 {
    ft5x06_ts@38 {
        compatible = "focaltech,ft5x06";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <800>;
        touchscreen-size-y = <480>;
        touchscreen-max-touch-points = <5>;
        focaltech,swap-xy;
        focaltech,invert-x;
        focaltech,invert-y;
    };
};

驱动解析函数实现:

static int ft5x06_parse_dt(struct device *dev, struct ft5x06_ts_data *ts)
{
    struct device_node *np = dev->of_node;
    u32 val;
    int ret;

    if (!np)
        return -ENODEV;

    /* 解析复位 GPIO */
    ts->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
    if (IS_ERR(ts->reset_gpio)) {
        dev_err(dev, "Failed to get reset GPIO\n");
        return PTR_ERR(ts->reset_gpio);
    }

    /* 解析触摸屏尺寸 */
    ret = of_property_read_u32(np, "touchscreen-size-x", &val);
    ts->max_x = ret ? DEFAULT_MAX_X : val;

    ret = of_property_read_u32(np, "touchscreen-size-y", &val);
    ts->max_y = ret ? DEFAULT_MAX_Y : val;

    /* 解析最大触摸点数 */
    ret = of_property_read_u32(np, "touchscreen-max-touch-points", &val);
    ts->max_touch_num = ret ? DEFAULT_MAX_TOUCH : val;
    if (ts->max_touch_num > FT5X06_MAX_TOUCH_SUPPORT)
        ts->max_touch_num = FT5X06_MAX_TOUCH_SUPPORT;

    /* 解析坐标变换标志 */
    ts->swap_xy  = of_property_read_bool(np, "focaltech,swap-xy");
    ts->invert_x = of_property_read_bool(np, "focaltech,invert-x");
    ts->invert_y = of_property_read_bool(np, "focaltech,invert-y");

    dev_info(dev, "DT parsed: %dx%d, max_touch=%d, swap=%d, inv_x=%d, inv_y=%d\n",
             ts->max_x, ts->max_y, ts->max_touch_num,
             ts->swap_xy, ts->invert_x, ts->invert_y);

    return 0;
}
设备树调试技巧: 使用 dtc -I dtb -O dts 反编译 DTB 文件,检查节点属性是否正确传递。在驱动中可以通过 dev_dbg 打印解析结果,配合 dyndbg 动态调试开关查看。

4.5 I2C 读写函数封装:可靠通信的基石

FT5x06 的寄存器访问采用标准的 I2C 读写序列。驱动需要封装可靠的读写函数,处理 I2C 传输错误、重试机制和寄存器地址对齐。FT5x06 的寄存器地址为 8 位宽,数据为 8 位或 16 位。

函数 功能 参数 返回值
ft5x06_i2c_read_reg 读取单个 8 位寄存器 client, reg_addr, *data 0 成功,负值错误码
ft5x06_i2c_write_reg 写入单个 8 位寄存器 client, reg_addr, data 0 成功,负值错误码
ft5x06_i2c_read_block 连续读取多个寄存器 client, reg_addr, len, *buf 0 成功,负值错误码
ft5x06_i2c_write_block 连续写入多个寄存器 client, reg_addr, len, *buf 0 成功,负值错误码
/* 单字节读:先写寄存器地址,再读数据 */
static int ft5x06_i2c_read_reg(struct i2c_client *client,
                               u8 reg_addr, u8 *data)
{
    struct i2c_msg msg[2];
    int ret;

    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = ®_addr;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = 1;
    msg[1].buf = data;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret == 2)
        return 0;
    else if (ret < 0)
        return ret;
    else
        return -EIO;
}

/* 单字节写:先写寄存器地址,再写数据 */
static int ft5x06_i2c_write_reg(struct i2c_client *client,
                                u8 reg_addr, u8 data)
{
    u8 buf[2] = { reg_addr, data };
    int ret;

    ret = i2c_master_send(client, buf, 2);
    if (ret == 2)
        return 0;
    else if (ret < 0)
        return ret;
    else
        return -EIO;
}

/* 块读:从 reg_addr 开始连续读取 len 字节 */
static int ft5x06_i2c_read_block(struct i2c_client *client,
                                 u8 reg_addr, int len, u8 *buf)
{
    struct i2c_msg msg[2];
    int ret;

    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = ®_addr;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = len;
    msg[1].buf = buf;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret == 2)
        return 0;
    else if (ret < 0)
        return ret;
    else
        return -EIO;
}

/* 带重试机制的读封装 */
static int ft5x06_i2c_read_retry(struct i2c_client *client,
                                 u8 reg_addr, u8 *data, int retries)
{
    int ret;
    int i;

    for (i = 0; i < retries; i++) {
        ret = ft5x06_i2c_read_reg(client, reg_addr, data);
        if (ret == 0)
            return 0;
        msleep(1);
    }

    dev_err(&client->dev, "I2C read retry failed after %d attempts\n", retries);
    return ret;
}
I2C 通信可靠性: FT5x06 在触摸扫描期间可能暂时拉低 SCL 时钟(时钟拉伸),导致 I2C 传输超时。建议在读写函数中加入重试机制(通常 3 次),并在每次重试前加入 1-5ms 延迟。对于批量读取触摸点数据(一次读取 30+ 字节),建议使用 i2c_transfer 而非多次单字节读,以减少总线开销。

4.6 驱动框架整体架构图

下图展示了 FT5x06 驱动从内核模块加载到输入事件上报的完整架构流程:

FT5x06 驱动框架架构图 module_init / module_exit i2c_add_driver / i2c_del_driver 设备树匹配 (of_match_table) probe 函数:设备树解析 → 硬件复位 → 版本校验 → 中断申请 → 输入设备注册 I2C 读写封装 (带重试机制) 中断处理 → 读取触摸点数据 输入子系统:MT Protocol B → 上报触摸事件 用户空间 (evdev / 应用层) 图例说明 驱动入口/出口 设备匹配 probe 流程

4.7 驱动框架关键数据结构

私有数据结构体 ft5x06_ts_data 贯穿整个驱动生命周期,承载设备树参数、硬件状态、输入设备和中断上下文信息。

/* FT5x06 驱动私有数据结构体 */
struct ft5x06_ts_data {
    struct i2c_client *client;          /* I2C 客户端指针 */
    struct input_dev *input_dev;        /* 输入设备指针 */
    struct gpio_desc *reset_gpio;       /* 复位 GPIO 描述符 */
    struct gpio_desc *irq_gpio;         /* 中断 GPIO 描述符(可选) */

    u16 max_x;                          /* X 轴最大分辨率 */
    u16 max_y;                          /* Y 轴最大分辨率 */
    u8  max_touch_num;                  /* 最大同时触摸点数 */
    u8  chip_ver;                       /* 芯片版本号 */

    bool swap_xy;                       /* 是否交换 XY 坐标 */
    bool invert_x;                      /* 是否翻转 X 轴 */
    bool invert_y;                      /* 是否翻转 Y 轴 */

    /* 触摸点缓存,用于多点触控协议 */
    struct ft5x06_touch_point points[FT5X06_MAX_TOUCH_SUPPORT];

    /* 调试与统计 */
    atomic_t irq_count;                 /* 中断计数 */
    struct mutex lock;                  /* 并发访问锁 */
};
框架设计要点:
  • 分层清晰: 驱动入口 → I2C 核心 → probe → 设备树 → 硬件操作 → 输入子系统,每层职责单一。
  • 错误回滚: probe 中每个步骤失败时,必须释放之前已申请的所有资源,避免内存泄漏。
  • 可配置性: 设备树属性使驱动无需重新编译即可适配不同硬件设计(如屏幕旋转、分辨率变化)。
  • 鲁棒性: I2C 读写封装加入重试机制,应对触摸芯片在扫描期间的时钟拉伸或总线噪声。

4.8 本章小结

FT5x06 驱动框架遵循标准的 Linux I2C 驱动模型,核心流程为:module_init → i2c_add_driver → 设备树匹配 → probe → 设备树解析 → 硬件初始化 → 中断注册 → 输入设备注册。本章详细拆解了每个环节的实现要点,特别是设备树解析的灵活性和 I2C 读写封装的可靠性设计。这些基础框架代码将直接复用到后续的 GT911 驱动适配和性能调优章节中。