第3章:I2C触摸屏驱动移植

好,咱们今天聊聊触摸屏驱动移植。说实话,I2C触摸屏是我在AOSP移植工作中打交道最多的外设之一。你想想看,手机、平板、车载中控,哪个设备离得开触摸?而这里面,I2C接口的触摸屏又占了绝大多数。

我个人习惯把触摸屏驱动移植分成三个层次:I2C设备驱动模型初始化序列寄存器读写与固件升级。咱们一层层剥开来看。

3.1 I2C设备驱动模型:Linux下的标准玩法

Linux内核里,I2C设备驱动有一套固定的框架。说白了,就是三步走:注册驱动匹配设备绑定操作

先看一个典型的驱动结构体:

static const struct i2c_device_id ft5x06_ts_id[] = {
    { "ft5x06", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ft5x06_ts_id);

static const struct of_device_id ft5x06_of_match[] = {
    { .compatible = "edt,ft5x06", },
    { }
};
MODULE_DEVICE_TABLE(of, ft5x06_of_match);

static struct i2c_driver ft5x06_ts_driver = {
    .driver = {
        .name   = "ft5x06",
        .of_match_table = ft5x06_of_match,
    },
    .probe      = ft5x06_ts_probe,
    .remove     = ft5x06_ts_remove,
    .id_table   = ft5x06_ts_id,
};
module_i2c_driver(ft5x06_ts_driver);

这里有两个关键点:

  • of_match_table:用于设备树匹配。你在dts里写compatible = "edt,ft5x06",内核就能找到这个驱动。
  • id_table:传统I2C设备ID匹配,用于非设备树场景。
我的经验:现在新平台基本都用设备树。我建议你优先把of_match_table配好。曾经有个项目,工程师只配了id_table,结果设备树死活匹配不上,查了两天才发现是这里漏了。

probe函数是驱动的入口。它负责:

  1. 分配并初始化触摸屏数据结构
  2. 设置I2C通信参数(速率、地址等)
  3. 注册输入设备(input_dev)
  4. 注册中断处理函数
  5. 初始化触摸屏硬件(调用初始化序列)

3.2 触摸屏初始化序列:上电后的第一件事

触摸屏上电后,不是马上就能用的。它需要一套初始化序列。我遇到过不少问题,都是初始化顺序不对导致的。

典型的初始化流程是这样的:

static int ft5x06_ts_init(struct ft5x06_ts_data *data)
{
    int ret;

    /* 1. 软复位 */
    ret = ft5x06_i2c_write_reg(data, REG_SOFT_RESET, 0x01);
    if (ret < 0)
        return ret;
    msleep(20);  // 等待复位完成

    /* 2. 读取芯片ID,确认通信正常 */
    ret = ft5x06_i2c_read_reg(data, REG_CHIP_ID, &chip_id);
    if (ret < 0 || chip_id != EXPECTED_ID) {
        dev_err(data->dev, "chip id mismatch\n");
        return -ENODEV;
    }

    /* 3. 配置工作模式 */
    ft5x06_i2c_write_reg(data, REG_MODE, MODE_ACTIVE);

    /* 4. 设置触摸阈值 */
    ft5x06_i2c_write_reg(data, REG_THRESHOLD, 0x28);

    /* 5. 使能中断 */
    ft5x06_i2c_write_reg(data, REG_INT_ENABLE, 0x01);

    return 0;
}

这里有几个坑,我一个个说:

  • 延时不能省:软复位后,芯片需要时间稳定。我见过有人把msleep(20)写成msleep(1),结果10次里有3次初始化失败。
  • 芯片ID校验:这是确认I2C通信是否正常的关键步骤。如果读不到正确的ID,要么是地址错了,要么是硬件没上电。
  • 阈值设置:不同尺寸的屏,触摸阈值不一样。7寸屏和10寸屏的参数完全不同。
注意:初始化序列里的寄存器地址和值,一定要参考芯片数据手册。我曾经图省事,直接复制了另一个型号的初始化代码,结果触摸屏完全不响应。后来发现是复位时序不一样。

3.3 寄存器读写:I2C通信的核心

I2C触摸屏的寄存器读写,说白了就是发两个字节:寄存器地址 + 数据。但不同厂商的协议略有差异。

以FT5x06为例,它的读写函数是这样的:

static int ft5x06_i2c_read_reg(struct ft5x06_ts_data *data,
                u8 reg, u8 *value)
{
    struct i2c_msg msg[2];
    int ret;

    /* 写寄存器地址 */
    msg[0].addr = data->client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = &reg;

    /* 读数据 */
    msg[1].addr = data->client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = 1;
    msg[1].buf = value;

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

static int ft5x06_i2c_write_reg(struct ft5x06_ts_data *data,
                 u8 reg, u8 value)
{
    u8 buf[2] = { reg, value };
    int ret;

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

这里有个细节:读操作用了重复起始条件(即msg[0]和msg[1]组合)。写操作则直接发送地址+数据。为什么?因为读操作需要先告诉芯片要读哪个寄存器,然后再接收数据。这两个操作之间不能有停止信号。

关键点:I2C通信的速率也很重要。标准模式100kHz,快速模式400kHz。触摸屏通常用400kHz。但有些老芯片只支持100kHz,速率设高了会丢数据。

3.4 固件升级:给触摸屏“刷系统”

触摸屏也有固件,而且需要升级。为什么?因为触摸算法在持续优化,或者出厂时固件有bug。

固件升级的流程大致如下:

  1. 进入升级模式(通常通过写特定寄存器)
  2. 擦除Flash
  3. 按块写入固件数据
  4. 校验CRC或MD5
  5. 复位芯片,退出升级模式

我写过一个简单的固件升级函数:

static int ft5x06_firmware_update(struct ft5x06_ts_data *data,
                   const u8 *fw_data, size_t fw_size)
{
    int ret, i;
    size_t block_size = 64;  // 每块64字节

    /* 1. 进入升级模式 */
    ft5x06_i2c_write_reg(data, REG_FW_MODE, 0xAA);

    /* 2. 擦除 */
    ft5x06_i2c_write_reg(data, REG_FW_ERASE, 0x01);
    msleep(500);  // 擦除需要时间

    /* 3. 逐块写入 */
    for (i = 0; i < fw_size; i += block_size) {
        ret = ft5x06_i2c_write_block(data,
                     REG_FW_ADDR + i,
                     &fw_data[i],
                     min(block_size, fw_size - i));
        if (ret < 0) {
            dev_err(data->dev, "fw write failed at %d\n", i);
            return ret;
        }
        msleep(10);
    }

    /* 4. 校验 */
    ret = ft5x06_verify_fw(data, fw_data, fw_size);
    if (ret < 0) {
        dev_err(data->dev, "fw verification failed\n");
        return ret;
    }

    /* 5. 退出升级模式 */
    ft5x06_i2c_write_reg(data, REG_FW_MODE, 0x00);

    return 0;
}
警告:固件升级过程中绝对不能断电!我有个同事在产线上测试,升级到一半拔了USB,结果触摸屏变砖了。后来只能拆机用烧录器救回来。所以,量产时一定要加掉电保护。

另外,固件文件通常以.bin.iic格式提供。我建议把固件放在/lib/firmware/目录下,通过request_firmware()接口加载。这样驱动和固件分离,升级固件时只需要替换文件,不用重新编译内核。

3.5 避坑指南:我踩过的那些雷

做触摸屏驱动移植这些年,我总结了几条血泪教训:

  • I2C地址别搞错:7位地址和8位地址的区别。很多芯片手册给的是7位地址,但Linux驱动里用的是8位(左移一位)。我见过有人直接拿7位地址去注册,结果设备都找不到。
  • 中断触发方式:触摸屏通常用边沿触发(上升沿或下降沿)。但有些芯片要求电平触发。配错了,要么中断风暴,要么没中断。
  • 电源管理:触摸屏的休眠和唤醒时序很重要。有些芯片休眠后需要重新初始化,有些则不需要。这个一定要看数据手册。
  • 多点触摸协议:Linux内核支持两种多点触摸协议:Type A(无状态)和Type B(有状态)。现在主流都用Type B,但有些老驱动还在用Type A。移植时要注意匹配。
小技巧:调试I2C通信时,可以用i2cdetect命令扫描总线,确认设备地址。再用i2cgeti2cset手动读写寄存器。这样能快速定位是硬件问题还是软件问题。

好了,这一章的内容就到这里。I2C触摸屏驱动移植,说难不难,说简单也不简单。关键是理解模型、吃透时序、注意细节。下一章咱们聊聊输入子系统的核心——input_dev的注册与事件上报。到时候见。