4. I2C控制器驱动开发(Linux):I2C核心层、I2C总线驱动、I2C设备驱动、adapter注册、client注册

好,咱们今天聊聊I2C控制器驱动。这部分内容,说白了就是Linux内核怎么把I2C总线管起来的。很多初学者一上来就对着datasheet猛写寄存器,结果发现驱动加载了,设备就是不通。为什么?因为没搞懂内核那套分层机制。

我个人习惯,先把框架看清楚再动手。I2C子系统在内核里分了三层:核心层、总线驱动层、设备驱动层。这三层各司其职,你想想看,就像公司里的三个部门——核心层是行政,管注册、管匹配;总线驱动是硬件接口,负责跟I2C控制器寄存器打交道;设备驱动是业务部门,只管跟具体传感器、EEPROM通信。

4.1 I2C核心层:承上启下的枢纽

核心层代码主要在drivers/i2c/i2c-core.c里。它不直接操作硬件,但所有I2C驱动的注册、注销、设备枚举、总线匹配,都得经过它。

核心层提供了几个关键结构体:

结构体 作用 我常用的场景
i2c_adapter 代表一个I2C控制器 每个硬件I2C接口对应一个adapter
i2c_client 代表一个I2C从设备 挂在adapter上的具体芯片
i2c_algorithm 控制器的通信方法 实现master_xfer函数
i2c_driver 设备驱动的抽象 匹配client并实现probe

核心层还维护了一个全局的adapter链表和driver链表。每次新注册一个adapter,核心层就会遍历所有已注册的driver,看看有没有能匹配上的。反过来,注册driver时也会扫描所有adapter。这个匹配机制,我当年调试的时候踩过坑——设备树里地址写错了,核心层匹配不上,驱动probe死活不调用。

关键函数调用链:

i2c_add_adapter() → i2c_register_adapter() → of_i2c_register_devices() → 创建设备节点 → 触发driver匹配

4.2 I2C总线驱动:跟硬件打交道的人

总线驱动,也叫控制器驱动。它负责实现i2c_algorithm里的master_xfer()函数。这个函数就是I2C通信的底层实现——怎么发start信号、怎么传地址、怎么收ACK、怎么发stop信号。

我在项目中遇到过一个问题:某款芯片的I2C控制器,发送地址后硬件不自动检查ACK,需要软件轮询状态寄存器。当时我按标准写法实现了master_xfer,结果通信成功率只有70%。后来加了重试机制和超时处理,才算稳定。

总线驱动的注册流程大致如下:

  1. 分配i2c_adapter结构体
  2. 填充i2c_algorithm,主要是master_xfer和functionality
  3. 设置adapter的dev.of_node(如果使用设备树)
  4. 调用i2c_add_adapter()或i2c_add_numbered_adapter()

这里有个细节:i2c_add_numbered_adapter()可以指定总线编号,比如i2c-0、i2c-1。而i2c_add_adapter()让内核自动分配。我个人习惯用带编号的版本,方便用户空间工具定位。

避坑指南:我曾经在注册adapter时忘了设置class字段,结果用户空间的i2cdetect工具扫不到设备。后来查了内核文档才发现,adapter->class = I2C_CLASS_HWMON这类标志位必须显式设置,否则核心层不会把设备暴露给用户空间。

4.3 I2C设备驱动:只管业务逻辑

设备驱动就简单多了。它不需要知道I2C控制器怎么发start信号,只需要调用i2c_transfer()或i2c_smbus_read_byte_data()这类API。核心层和总线驱动会帮你搞定底层时序。

设备驱动的核心是i2c_driver结构体:

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

static const struct of_device_id xxx_of_match[] = {
    { .compatible = "vendor,xxx-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, xxx_of_match);

static struct i2c_driver xxx_driver = {
    .driver = {
        .name = "xxx_sensor",
        .of_match_table = xxx_of_match,
    },
    .probe = xxx_probe,
    .remove = xxx_remove,
    .id_table = xxx_id,
};
module_i2c_driver(xxx_driver);

注意module_i2c_driver()这个宏,它把i2c_add_driver()和i2c_del_driver()封装好了。我见过有人手写module_init和module_exit,结果忘了调用i2c_add_driver,驱动加载了但probe不执行。

4.4 adapter注册:让内核认识你的控制器

adapter注册是总线驱动的最后一步。核心函数是i2c_register_adapter(),它做了几件事:

  • 分配总线编号(如果没指定)
  • 创建/dev/i2c-N设备节点
  • 扫描设备树,为每个子节点创建i2c_client
  • 触发driver匹配

设备树中adapter的典型写法:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    temp_sensor: temperature@48 {
        compatible = "vendor,temp-sensor";
        reg = <0x48>;
    };

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

内核解析设备树时,会为temp_sensor和eeprom各创建一个i2c_client。这里要注意reg地址必须跟硬件一致,我曾经把0x48写成0x49,结果驱动probe了但读写全是错误数据。

重要提醒:adapter注册成功后,i2c-core会调用of_i2c_register_devices()。如果你的设备树里没有子节点,或者status = "disabled",那么不会创建任何client。很多新手在设备树里写了节点但忘了加status = "okay",折腾半天找不到原因。

4.5 client注册:设备驱动的灵魂

i2c_client代表一个具体的I2C从设备。它通常在两种情况下被创建:

  1. 设备树解析:adapter注册时自动创建
  2. 板级代码:通过i2c_new_device()手动创建

client注册后,核心层会遍历所有i2c_driver,调用i2c_match_id()或of_match_node()进行匹配。匹配成功就调用driver的probe函数。

probe函数里,你拿到的是struct i2c_client *client,通过它你可以:

  • 读取设备地址:client->addr
  • 获取adapter:client->adapter
  • 发起通信:i2c_master_send()、i2c_master_recv()

我习惯在probe里先读一下设备的ID寄存器,确认硬件确实在线。如果读不到,就返回-ENODEV。这个习惯帮我避免了好几次硬件虚焊导致的问题。

4.6 整体框架图

下面这张图展示了I2C子系统的分层结构和注册流程。我画的时候特意把核心层放在中间,因为它确实是整个系统的枢纽。

I2C子系统分层结构与注册流程 I2C子系统分层结构与注册流程 用户空间层 i2cdetect / i2cget / i2cset / 应用程序 I2C核心层 (i2c-core) adapter注册/注销 | driver注册/注销 | 设备枚举 | 总线匹配 i2c_add_adapter / i2c_add_driver / of_i2c_register_devices I2C总线驱动层 i2c_adapter + i2c_algorithm master_xfer / 寄存器操作 I2C设备驱动层 i2c_driver + i2c_client probe / remove / i2c_transfer I2C控制器硬件 SDA / SCL 引脚 · 寄存器 I2C从设备硬件 传感器 / EEPROM / 触摸屏 注册流程: ① 总线驱动注册adapter → ② 核心层解析设备树 → ③ 创建i2c_client → ④ 匹配i2c_driver → ⑤ 调用probe

4.7 调试技巧与常见问题

最后分享几个我实际调试中总结的经验:

  • adapter注册失败:检查dmesg,看是不是中断号冲突或IO内存申请失败。我遇到过platform_get_resource返回NULL的情况,原因是设备树里reg属性写错了。
  • probe不调用:先确认i2cdetect -l能看到adapter,再用i2cdetect -y N扫描总线。如果设备地址显示UU,说明驱动已绑定;如果显示--,说明硬件没应答。
  • 通信超时:检查时钟频率是否匹配。有些从设备只支持100kHz,你设成400kHz就会超时。另外,上拉电阻阻值不对也会导致时序异常。

调试利器:我习惯在probe函数里加dev_info(&client->dev, "probe success, addr=0x%02x\n", client->addr);。别小看这一行,它能帮你快速定位是驱动没加载,还是probe失败了。