4. Linux 输入子系统

好,我们进入第四章。前面几章我们把触摸屏的硬件接口、I2C协议时序聊透了。这一章,咱们要真正动手写代码了——把I2C驱动框架搭起来。

说白了,Linux内核里的I2C驱动,核心就三件事:注册驱动、匹配设备、读写寄存器。我刚开始接触这块时,总觉得内核框架很神秘,后来发现,其实套路非常固定。你只要掌握了这个套路,换任何一款I2C触摸芯片,都能快速上手。

4.1 i2c_driver的注册与注销

每个I2C设备驱动,都需要一个i2c_driver结构体。它就像你的身份证,告诉内核:“嘿,我能驱动哪款芯片,我的probe函数在哪。”

先看结构体定义:

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

static struct i2c_device_id ft5x06_idtable[] = {
    { "ft5x06", 0 },
    { }
};

MODULE_DEVICE_TABLE(i2c, ft5x06_idtable);

static int ft5x06_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    // 初始化逻辑
    return 0;
}

static int ft5x06_remove(struct i2c_client *client)
{
    // 清理逻辑
    return 0;
}

static struct i2c_driver ft5x06_driver = {
    .driver = {
        .name   = "ft5x06",
        .of_match_table = of_match_ptr(ft5x06_of_match),
    },
    .probe      = ft5x06_probe,
    .remove     = ft5x06_remove,
    .id_table   = ft5x06_idtable,
};

module_i2c_driver(ft5x06_driver);
MODULE_LICENSE("GPL");

嗯,这里要注意几个关键点:

  • id_table:这是I2C设备ID表,内核通过它来匹配设备和驱动。名字必须和设备树里compatible属性一致。
  • of_match_table:设备树匹配表,现在主流做法。我建议你两个都写上,兼容性更好。
  • module_i2c_driver:这是个宏,展开后就是module_init和module_exit,省得你手写。
个人习惯:我写驱动时,喜欢把i2c_driver结构体放在文件末尾,probe/remove函数放在前面。这样阅读代码时,从上到下,逻辑更顺。

4.2 probe函数——驱动的入口

probe函数什么时候被调用?当内核发现一个I2C设备,并且它的名字或兼容属性跟你注册的驱动匹配时,内核就会调用你的probe函数。

你可以把probe理解为驱动的构造函数。在这里,你要完成:

  • 分配并初始化设备私有数据结构
  • 获取硬件资源(中断号、GPIO、时钟等)
  • 初始化I2C寄存器(通过regmap或直接SMBus)
  • 注册输入设备(input_dev)
  • 注册中断处理函数

我见过不少新手,把probe写得又臭又长。其实probe应该只做必要的初始化,复杂的逻辑放到工作队列或线程里去做。为什么呢?因为probe执行时,系统可能还没完全准备好,你在这里做太多事,容易出问题。

static int ft5x06_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct ft5x06_data *data;
    int ret;

    // 1. 分配私有数据
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;
    i2c_set_clientdata(client, data);

    // 2. 初始化regmap
    data->regmap = devm_regmap_init_i2c(client, &ft5x06_regmap_config);
    if (IS_ERR(data->regmap)) {
        dev_err(&client->dev, "regmap init failed\n");
        return PTR_ERR(data->regmap);
    }

    // 3. 读取芯片ID,验证连接
    ret = regmap_read(data->regmap, FT5X06_REG_CHIP_ID, &data->chip_id);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read chip id\n");
        return ret;
    }

    // 4. 注册输入设备
    // ... 后续章节展开

    return 0;
}
我曾经踩过一个坑:在probe里直接调用request_irq注册中断,结果中断触发时,输入设备还没注册完,导致空指针访问。后来我改成在probe最后才注册中断,或者在中断处理函数里加个标志位判断。切记,probe里的顺序很重要!

4.3 regmap操作——优雅的寄存器读写

说到I2C读写寄存器,早期驱动都是直接用i2c_master_send和i2c_master_recv,或者i2c_smbus_read_byte_data这些函数。代码写起来很啰嗦,而且缓存管理、并发保护都得自己搞。

后来内核引入了regmap,这东西太好用了。它把寄存器读写抽象成一套统一的API,底层可以是I2C、SPI、MMIO,上层调用完全一样。而且自带缓存和并发保护。

我个人强烈建议:新写的I2C驱动,都用regmap。除非你的芯片有非常特殊的时序要求,否则regmap能省你一半的代码量。

4.3.1 regmap配置

static const struct regmap_config ft5x06_regmap_config = {
    .reg_bits = 8,      // 寄存器地址宽度:8位
    .val_bits = 8,      // 寄存器值宽度:8位
    .max_register = 0xFF, // 最大寄存器地址
    .cache_type = REGCACHE_RBTREE, // 使用红黑树缓存
    .read_flag_mask = 0x00,  // 读操作掩码
    .write_flag_mask = 0x00, // 写操作掩码
};

这里有几个参数需要根据芯片手册来配:

参数 说明 常见值
reg_bits 寄存器地址的位数 8或16
val_bits 寄存器值的位数 8或16
max_register 最大有效寄存器地址 根据芯片定
cache_type 缓存策略 REGCACHE_NONE / REGCACHE_RBTREE
read_flag_mask 读操作时,地址是否需要特殊标记 一般为0

4.3.2 常用regmap API

初始化完成后,读写就非常简单了:

// 读寄存器
int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val);

// 写寄存器
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val);

// 更新寄存器某几位(读-改-写)
int regmap_update_bits(struct regmap *map, unsigned int reg,
                       unsigned int mask, unsigned int val);

// 批量读
int regmap_bulk_read(struct regmap *map, unsigned int reg,
                     void *val, size_t val_count);

// 批量写
int regmap_bulk_write(struct regmap *map, unsigned int reg,
                      const void *val, size_t val_count);

你看,是不是很清爽?regmap_update_bits尤其好用,比如你要设置某个寄存器的bit3为1,不用先读再改再写,一个函数搞定。

为什么用regmap? 说白了,它帮你做了三件事:
1. 自动处理I2C传输的细节(地址、数据格式)
2. 提供缓存,减少不必要的I2C访问(省电!)
3. 内置锁,多线程访问时不会乱

4.4 整体框架图

下面这张图,把I2C驱动的整个注册流程和probe里的关键步骤串起来了。你写驱动时,就照着这个流程走,基本不会错。

I2C驱动注册与probe流程 I2C驱动注册与probe函数执行流程 module_i2c_driver 注册 i2c_driver 到内核 匹配设备(id_table / of) 调用 probe 函数 probe 内部执行步骤 分配私有数据 初始化 regmap 读取芯片ID 注册输入设备 注册中断 初始化完成 注意:probe 执行顺序很重要,特别是中断注册必须在输入设备之后

4.5 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • regmap缓存问题:如果你用REGCACHE_RBTREE,第一次读寄存器时,regmap会从硬件读。但之后如果硬件寄存器被其他模块改了(比如固件升级),regmap可能还返回缓存的值。我遇到过触摸屏校准值读不对,查了两天才发现是缓存没刷新。解决方案:关键寄存器读操作前,调用regcache_mark_dirty强制刷新。
  • 设备树匹配:compatible属性一定要和驱动里of_match_table完全一致,大小写、标点都不能错。我曾经把"ft5x06"写成了"ft5x06",少了个下划线,结果驱动死活不probe。
  • I2C地址:设备树里写的reg是7位地址,而芯片手册里给的可能是8位地址(左移了一位)。这个坑我见得太多了,一定要确认清楚。
调试小技巧:在probe函数里加dev_info打印,把读取到的芯片ID、版本号打出来。这样驱动加载时,看一眼内核日志,就知道I2C通信是否正常。省得后面出问题了再回头查。

好了,这一章的内容就到这里。I2C驱动框架说白了就是三板斧:注册、probe、regmap。你把这章代码敲一遍,跑通,后面触摸数据的读取就水到渠成了。