4. I2C驱动框架设计:Linux内核I2C子系统架构

做嵌入式Linux驱动开发,I2C子系统是绕不开的坎。说实话,我刚接触这块时也被那套分层模型搞晕过——什么adapter、client、algorithm,一堆抽象概念。后来调了几个月I2C触摸屏驱动,才慢慢摸清门道。

今天咱们就把这套框架拆开揉碎,看看Linux内核到底怎么管理I2C设备的。

4.1 三层架构:从总线到设备

Linux内核的I2C子系统,说白了就是三层结构:

  • 核心层(Core):提供注册/注销接口,管理总线号,处理数据传输的通用逻辑
  • 适配器层(Adapter):对应具体的I2C控制器硬件,负责收发时序
  • 客户端层(Client):对应挂在总线上的具体设备,比如温度传感器、EEPROM

我习惯把这三层比作快递系统:核心层是快递公司总部,定规则;适配器是各地分拣中心,负责装卸货;客户端就是收件人,只管收包裹。

关键认知:驱动开发者通常只写client驱动,但不懂adapter层,遇到硬件时序问题就抓瞎。我建议至少把adapter的注册流程走一遍。

Linux I2C 子系统三层架构 核心层 (I2C Core) i2c-core.c / i2c-dev.c 适配器层 (Adapter) struct i2c_adapter 适配器层 (Adapter) struct i2c_adapter 客户端 (Client) 温度传感器 客户端 (Client) EEPROM 客户端 (Client) 触摸屏 一个Adapter可挂多个Client 共享SCL/SDA总线

4.2 Adapter驱动模型:控制器的软件抽象

每个I2C控制器在Linux里对应一个struct i2c_adapter。这个结构体就是控制器的软件代言人。

核心成员就这几个:

成员 类型 说明
owner struct module * 所属模块,防卸载
class unsigned int 适配器类型,I2C_CLASS_HWMON等
algo const struct i2c_algorithm * 通信算法,最关键的部分
algo_data void * 算法私有数据,通常指向控制器寄存器基址
nr int 总线编号,/dev/i2c-N 中的N
dev struct device 内核设备模型节点

注册一个adapter,核心就是填充这个结构体,然后调用i2c_add_adapter()或i2c_add_numbered_adapter()。

我的习惯:用i2c_add_numbered_adapter()指定总线号,避免设备树或用户空间依赖动态编号。曾经有个项目,就因为总线号重启后变了,导致应用层死活打不开设备。

4.3 i2c_algorithm:真正的时序执行者

Adapter的algo成员才是干活的。它定义了两个关键函数指针:

struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap,
                       struct i2c_msg *msgs, int num);
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
                      unsigned short flags, char read_write,
                      u8 command, int size,
                      union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *adap);
};

master_xfer是核心——它接收一个i2c_msg数组,每个msg描述一次传输的方向、地址、数据和长度。adapter驱动要做的就是按msg的配置,操作硬件寄存器完成时序。

我见过不少新手在这里踩坑:master_xfer返回的是成功传输的msg数量,不是字节数。如果返回负数,上层会认为传输失败。

注意:有些SoC的I2C控制器不支持自动发送stop信号,需要在最后一个msg后手动置位STOP位。否则总线会一直处于占用状态,其他设备无法通信。

4.4 Client驱动模型:设备端的视角

Client驱动就是咱们平时写的那些——比如温度传感器驱动、音频Codec驱动。它通过struct i2c_driver注册到内核:

static struct i2c_driver my_sensor_driver = {
    .driver = {
        .name = "my_temp_sensor",
        .of_match_table = my_sensor_of_match,
    },
    .probe = my_sensor_probe,
    .remove = my_sensor_remove,
    .id_table = my_sensor_id,
};

module_i2c_driver(my_sensor_driver);

匹配方式有三种:

  • 设备树匹配:通过compatible属性,最常用
  • ACPI匹配:x86平台常见
  • ID表匹配:传统方式,通过i2c_board_info静态注册

匹配成功后,内核调用probe函数。在probe里,你会拿到一个struct i2c_client指针,它包含了设备的地址、适配器指针等信息。之后就可以用i2c_smbus_read_byte()等API读写设备了。

4.5 数据传输的完整路径

从client驱动发起一次读操作,到硬件真正拉高拉低SCL,中间经历了什么?

  1. client驱动调用i2c_smbus_read_byte_data(client, reg)
  2. 核心层构造i2c_msg数组(先写寄存器地址,再读数据)
  3. 核心层调用adapter->algo->master_xfer()
  4. adapter驱动解析msg,操作I2C控制器的数据寄存器、控制寄存器
  5. 硬件产生SCL时钟,逐bit传输数据

这个链路里,每一步都可能出问题。我调试过一个案子,client驱动读数据总是返回0xFF,最后发现是adapter的master_xfer实现里,读方向时忘了设置控制器的主机接收模式。

调试要点:遇到I2C通信失败,先确认adapter层是否注册成功(检查/sys/bus/i2c/devices/i2c-N/),再用i2c-tools的i2cdetect扫描设备地址,确认硬件连接没问题。

4.6 设备树中的I2C节点

现在的主流做法是通过设备树描述I2C设备和拓扑。一个典型的I2C控制器节点长这样:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  /* 100kHz标准模式 */
    pinctrl-0 = <&i2c1_pins>;

    temp_sensor: temperature@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };

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

这里要注意reg属性——它表示设备的7位I2C地址。很多芯片手册给的是8位地址(左移了一位),需要手动右移。我吃过这个亏,把0x90写进设备树,结果怎么都匹配不上。

属性 含义 常见坑
compatible 驱动匹配字符串 必须与驱动中of_match_table一致
reg 设备I2C地址(7位) 手册地址通常左移1位,需转换
clock-frequency 总线速率 超过400kHz需确认硬件支持
status 使能状态 忘记写"okay"等于没接

4.7 避坑指南:我踩过的几个雷

做I2C驱动这些年,有些坑印象特别深:

  • 地址不匹配:设备树里写8位地址,驱动里用7位,probe永远进不去
  • 时钟拉伸没处理:有些从设备会拉低SCL,主控不支持的话直接超时
  • 中断上下文睡眠:在I2C传输函数里调用msleep(),内核直接报错
  • 忘记释放GPIO:复用为I2C的GPIO在系统休眠时被其他模块拉低,总线锁死

我曾经在一个项目里,I2C总线偶尔死锁,查了两天才发现是设备树里pinctrl-0配错了引脚,导致SCL和SDA短路。从那以后,我每次拿到新板子,第一件事就是用示波器看波形,确认时序正常再写驱动。

实用技巧:在probe函数里加dev_info()打印client的地址和adapter名称,确认匹配成功。这是最直接的验证手段。

好了,I2C驱动框架的核心就这些。理解了三层模型和数据结构的关系,写驱动时心里就有底了。下一层咱们要深入的是——怎么用这套框架写出一个真正能用的client驱动,包括probe、数据传输和错误处理。