4. Linux I2C 子系统架构:核心层、总线驱动、设备驱动与适配器
好,我们进入第四章。这一章要聊的是 Linux I2C 子系统的整体架构。说白了,就是搞清楚内核里那套 I2C 代码到底是怎么组织起来的。
我记得刚接触 I2C 驱动时,翻到 drivers/i2c/ 目录,一堆文件,看得我头皮发麻。后来跟着一个老工程师调了块触摸屏,才慢慢理清脉络。其实没那么玄乎,内核把 I2C 分成了四个层次:核心层、适配器驱动、总线驱动 和 设备驱动。咱们一层层剥开看。
一句话总结:I2C 子系统 = 核心层(管理注册与通信协议)+ 适配器(硬件控制器抽象)+ 总线(设备挂接与匹配)+ 设备驱动(具体外设逻辑)。
4.1 整体架构:一张图看懂
先画个框架图,把四个层次的关系摆清楚。你想想看,数据从用户态应用发出来,到最终操作 I2C 控制器的寄存器,中间要经过多少层?
这张图我建议你存下来。每次写驱动时,对照着看,就知道自己当前在操作哪一层。
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 里:
- 拿到
struct i2c_client *client,这就是你的设备句柄。 - 调用
i2c_smbus_read_byte_data()等函数读写寄存器,初始化设备。 - 注册字符设备、输入设备或 IIO 设备,把数据上报给用户空间。
关键点:设备驱动不直接操作硬件寄存器。它通过核心层提供的 API 发送消息,核心层再调用适配器驱动的 master_xfer。这就是分层的好处——你的驱动可以在任何 SoC 上跑,只要适配器驱动写好了。
4.6 四层协作:一次完整的 I2C 读写
咱们走一遍流程,看看用户态调 read() 读传感器数据时,四层是怎么协作的:
- 用户态:应用调用
read(fd, buf, len)。 - 设备驱动:字符设备驱动的
.read回调被触发,调用i2c_master_recv(client, buf, len)。 - 核心层:
i2c_master_recv()构造一个struct i2c_msg,调用__i2c_transfer(),最终调用适配器的master_xfer()。 - 适配器驱动:
master_xfer()操作 I2C 控制器的数据寄存器和控制寄存器,在 SCL/SDA 上产生 START、ACK、STOP 等时序。 - 硬件:I2C 控制器通过 GPIO 或专用引脚,把数据从传感器读回来。
- 数据原路返回,最终到用户态的 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 目录)。适配器起来了,再挂设备驱动。这样排查问题,思路清晰很多。