4. Linux I2C 子系统架构:核心层、总线驱动、设备驱动与适配器

好,我们进入第四章。这一章要聊的是 Linux I2C 子系统的整体架构。说白了,就是搞清楚内核里那套 I2C 代码到底是怎么组织起来的。

我记得刚接触 I2C 驱动时,翻到 drivers/i2c/ 目录,一堆文件,看得我头皮发麻。后来跟着一个老工程师调了块触摸屏,才慢慢理清脉络。其实没那么玄乎,内核把 I2C 分成了四个层次:核心层、适配器驱动、总线驱动 和 设备驱动。咱们一层层剥开看。

一句话总结:I2C 子系统 = 核心层(管理注册与通信协议)+ 适配器(硬件控制器抽象)+ 总线(设备挂接与匹配)+ 设备驱动(具体外设逻辑)。

4.1 整体架构:一张图看懂

先画个框架图,把四个层次的关系摆清楚。你想想看,数据从用户态应用发出来,到最终操作 I2C 控制器的寄存器,中间要经过多少层?

Linux I2C 子系统四层架构 用户空间 (User Space) 应用程序 / i2c-tools / 传感器数据读取 内核空间 (Kernel Space) I2C 设备驱动 (I2C Device Driver) i2c_driver / i2c_client / 传感器驱动 (如 bmp280, mpu6050) I2C 核心层 (I2C Core) i2c-core.c / 设备与驱动注册 / 总线管理 / 通信协议 I2C 适配器驱动 (I2C Adapter Driver) i2c_adapter / algorithm / 硬件控制器操作 (如 i2c-imx, i2c-omap) 硬件层 (Hardware) I2C 控制器 (如 i.MX6, OMAP, STM32MP1) 内核空间

这张图我建议你存下来。每次写驱动时,对照着看,就知道自己当前在操作哪一层。

4.2 I2C 核心层:大管家

核心层在 drivers/i2c/i2c-core.c 里。它不直接操作硬件,而是提供一套框架。主要干三件事:

  • 设备与驱动的注册管理:维护一个全局链表,记录所有注册进来的 i2c_adapter、i2c_driver 和 i2c_client。
  • 总线通信协议:封装了 i2c_transfer()、i2c_master_send()、i2c_master_recv() 等接口。你写设备驱动时,调的就是这些函数。
  • 设备树解析:从 DTB 里解析出 i2c_client 节点,自动创建设备。

我的习惯:调试时,我经常在 i2c_transfer() 里加打印,看消息队列是怎么被拆包组包的。这招排查通信异常特别管用。

4.3 I2C 适配器驱动:硬件操作员

适配器驱动对应具体的 I2C 控制器硬件。每个 SoC 的 I2C 控制器都不一样,所以这部分代码是平台相关的。

适配器驱动的核心结构体是 struct i2c_adapter,里面最重要的成员是 struct i2c_algorithm *algo。这个 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_transfer() 时,最终会落到这个函数里。它负责操作控制器的寄存器,在 SCL/SDA 线上产生时序。

我曾经踩过的坑:有一次移植适配器驱动,忘了实现 functionality 回调,结果上层调用 i2c_smbus_read_byte() 时直接返回 -EOPNOTSUPP。查了两天才发现是这里没填。所以适配器驱动里,这个回调一定要老老实实把支持的功能位都设上。

4.4 I2C 总线驱动:看不见的纽带

严格来说,Linux 里没有单独的 "I2C 总线驱动" 文件。总线这个概念是由核心层通过 bus_register() 注册的。它定义在 drivers/i2c/i2c-core-base.c 里。

总线的作用是什么?说白了就是牵线搭桥:

  • 当一个新的 i2c_client 注册时,总线会遍历所有 i2c_driver,调用 .probe() 匹配。
  • 当一个新的 i2c_driver 注册时,总线会遍历所有 i2c_client,同样调用 .probe()。

匹配的依据是 i2c_device_id 表或者设备树里的 compatible 属性。这部分逻辑在 i2c_device_match() 里实现。

匹配方式 优先级 典型场景
设备树 compatible 匹配 高 ARM/ARM64 平台,几乎所有新项目
i2c_device_id 表匹配 中 x86 平台或旧式板级代码
ACPI 匹配 低 x86 平台,工业电脑常见

4.5 I2C 设备驱动:你的主战场

这部分是咱们写的最多的。设备驱动就是具体外设的驱动,比如温度传感器、加速度计、触摸屏控制器。

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

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

static struct i2c_driver bmp280_driver = {
    .driver = {
        .name = "bmp280",
        .of_match_table = bmp280_of_match,
    },
    .probe    = bmp280_probe,
    .remove   = bmp280_remove,
    .id_table = bmp280_id,
};

module_i2c_driver(bmp280_driver);

你写设备驱动时,主要实现 probe 和 remove。在 probe 里:

  1. 拿到 struct i2c_client *client,这就是你的设备句柄。
  2. 调用 i2c_smbus_read_byte_data() 等函数读写寄存器,初始化设备。
  3. 注册字符设备、输入设备或 IIO 设备,把数据上报给用户空间。

关键点:设备驱动不直接操作硬件寄存器。它通过核心层提供的 API 发送消息,核心层再调用适配器驱动的 master_xfer。这就是分层的好处——你的驱动可以在任何 SoC 上跑,只要适配器驱动写好了。

4.6 四层协作:一次完整的 I2C 读写

咱们走一遍流程,看看用户态调 read() 读传感器数据时,四层是怎么协作的:

  1. 用户态:应用调用 read(fd, buf, len)。
  2. 设备驱动:字符设备驱动的 .read 回调被触发,调用 i2c_master_recv(client, buf, len)。
  3. 核心层:i2c_master_recv() 构造一个 struct i2c_msg,调用 __i2c_transfer(),最终调用适配器的 master_xfer()。
  4. 适配器驱动:master_xfer() 操作 I2C 控制器的数据寄存器和控制寄存器,在 SCL/SDA 上产生 START、ACK、STOP 等时序。
  5. 硬件:I2C 控制器通过 GPIO 或专用引脚,把数据从传感器读回来。
  6. 数据原路返回,最终到用户态的 buf 里。

嗯,这里要注意:每一步都有锁保护。核心层用 i2c_lock_bus() 保证多线程安全。你在设备驱动里调 i2c_transfer() 时,不用自己加锁,核心层已经处理好了。

4.7 避坑指南

我曾经犯过的错:

  • 适配器驱动里忘了调用 i2c_add_adapter():结果设备树里挂的子设备永远 probe 不了。因为核心层根本不知道有这个适配器存在。
  • 设备驱动里用 i2c_transfer() 传了空指针:核心层不会帮你检查 msg 是否为 NULL,直接崩。所以传参前一定要判空。
  • 中断上下文里调 i2c_transfer():这个函数可能会睡眠,在中断里调直接内核 panic。如果非要在中断里读写,用 i2c_atomic_transfer(),但前提是适配器支持原子操作。

4.8 小结

这一章我们把 I2C 子系统的四层架构拆了一遍。核心层是框架,适配器驱动管硬件,总线驱动管匹配,设备驱动管外设逻辑。四层各司其职,耦合度很低。

我个人习惯,拿到一个新板子,先看适配器驱动有没有注册成功(/sys/bus/i2c/devices/ 下有没有 i2c-X 目录)。适配器起来了,再挂设备驱动。这样排查问题,思路清晰很多。