2. I2C驱动框架:Linux I2C子系统架构、设备树绑定
好,咱们进入第二章。这一章我打算把Linux下的I2C驱动框架给你拆开揉碎了讲清楚。很多初学者一上来就翻芯片手册、写寄存器,结果代码写得乱七八糟,根本没法维护。说白了,你得先理解这个框架是怎么设计的,才能写出符合规范的驱动。
我个人习惯,在讲任何子系统之前,先画一张全局架构图。这样你心里有个谱,知道每个模块在哪个位置、干什么活。
2.1 三层架构:核心、总线、设备
Linux I2C 子系统分三层。这个分层设计,我当年刚接触时觉得多此一举,后来被坑过几次才明白它的好处。
| 层级 | 核心文件/模块 | 职责 |
|---|---|---|
| I2C 核心层 | i2c-core.c, i2c-dev.c | 设备模型抽象、总线注册、驱动匹配、ioctl 接口 |
| I2C 总线驱动层 | i2c-imx.c, i2c-s3c2410.c | 适配器 (Adapter) 实现,操作硬件寄存器产生时序 |
| I2C 设备驱动层 | eeprom.c, bmp085.c | 客户端 (Client) 驱动,与具体外设交互 |
核心层 是骨架。它管理着总线的注册、设备的枚举、驱动的匹配。你写驱动时,大部分 API 都是从这层暴露出来的。比如 i2c_add_adapter()、i2c_new_device(),都是核心层的函数。
总线驱动层 是干活的人。它直接操作 I2C 控制器的寄存器,负责产生 SCL 时钟、控制 SDA 数据线。不同的 SoC 有不同的 I2C 控制器,所以这一层是平台相关的。我在项目里移植过 i2c-imx 驱动,当时为了调一个时钟延时的参数,折腾了两天——嗯,硬件上的坑,后面会专门讲。
设备驱动层 是具体业务。比如你接了一个温度传感器,你只需要调用 i2c_smbus_read_word_data() 这类接口,不用关心底层时序怎么产生。这就是分层的好处——写设备驱动的人,可以完全不懂硬件寄存器。
关键理解: 总线驱动负责「怎么传」,设备驱动负责「传什么」。两者通过核心层的 adapter 和 client 结构体连接。
2.2 核心数据结构:adapter 与 client
这两个结构体,是 I2C 子系统的灵魂。你理解了它们,就理解了整个框架。
struct i2c_adapter
代表一个 I2C 控制器。每个硬件 I2C 总线对应一个 adapter。
struct i2c_adapter {
struct module *owner;
unsigned int class; // 设备类,用于自动探测
const struct i2c_algorithm *algo; // 算法,核心!实现收发函数
void *algo_data;
struct device dev; // 内核设备模型
int nr; // 总线编号,如 i2c-0, i2c-1
struct list_head userspace_clients;
// ... 其他字段
};
这里最重要的就是 algo 字段。它是一个 i2c_algorithm 结构体,里面定义了 master_xfer() 函数指针。总线驱动必须实现这个函数,核心层通过它来发起 I2C 传输。
我的经验: 调试新板子的 I2C 时,我第一个查的就是 adapter 的 nr 对不对。有一次设备树配错了,adapter 注册成了 i2c-2,但应用层硬编码了 i2c-0,结果死活读写不了。这种低级错误,排查起来最浪费时间。
struct i2c_client
代表一个挂在 I2C 总线上的从设备。每个外设芯片对应一个 client。
struct i2c_client {
unsigned short addr; // 7位或10位从机地址
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; // 所属的总线
struct device dev; // 内核设备模型
int irq; // 中断号,如果有的话
struct list_head detected; // 用于设备探测链表
};
client 的 addr 就是你在芯片手册上看到的那个从机地址。注意,Linux 内核里用的是 7 位地址(左对齐),而数据手册上经常写 8 位地址(含读写位)。这个转换我吃过亏,后面会专门提醒。
2.3 设备树绑定:让硬件描述与驱动分离
设备树 (Device Tree) 是 ARM Linux 的标配。它把硬件信息从内核代码里抽出来,放到一个 .dts 文件里。这样同一个内核可以适配不同的板子。
我记得第一次接触设备树时,觉得这东西好麻烦。但后来维护过几个项目后,发现它确实香——换一个传感器,改几行 dts 就行,不用重新编译内核。
I2C 控制器节点
在设备树里,I2C 控制器通常这样描述:
&i2c1 {
status = "okay";
clock-frequency = <100000>; // 100kHz 标准模式
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
// 挂载在总线上的从设备
temperature_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
};
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
关键字段解释:
- compatible:驱动匹配的关键字符串。内核通过它找到对应的驱动。
- reg:从机地址。注意是 7 位地址,比如 0x48。
- clock-frequency:总线频率。标准模式 100kHz,快速模式 400kHz。
- interrupts:中断配置。如果设备支持中断,这里指定。
避坑指南: 我曾经在一个项目里,设备树里 reg 写了 0x90(8位地址),结果内核怎么都匹配不上。后来查了半天才发现,内核用的是 7 位地址 0x48。记住:reg 填 7 位地址,不要左移一位!
驱动中的匹配
设备驱动通过 of_match_table 来匹配设备树节点:
static const struct of_device_id tmp102_of_match[] = {
{ .compatible = "ti,tmp102" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);
static struct i2c_driver tmp102_driver = {
.driver = {
.name = "tmp102",
.of_match_table = tmp102_of_match,
},
.probe = tmp102_probe,
.id_table = tmp102_id, // 传统ID表,用于非设备树平台
};
当内核解析设备树时,发现 compatible = "ti,tmp102",就会去遍历已注册的 i2c_driver,找到匹配的驱动,然后调用它的 probe 函数。
2.4 驱动注册与 probe 流程
整个流程是这样的:
- 总线驱动注册:调用
i2c_add_adapter(),将 adapter 注册到核心层。 - 设备树解析:内核解析 dts,为每个 I2C 子节点创建
i2c_client。 - 驱动注册:调用
i2c_add_driver(),将 i2c_driver 注册到核心层。 - 匹配与 probe:核心层遍历已注册的 client,找到 compatible 匹配的 driver,调用 probe。
在 probe 函数里,你通常会做这些事情:
- 分配并初始化私有数据结构
- 读取设备树中的属性(如中断号、时钟频率)
- 注册中断处理函数
- 创建 sysfs 文件或输入设备
- 初始化硬件(如设置传感器模式)
核心原则: probe 函数不要做耗时操作。如果初始化需要时间,用工作队列或定时器延后处理。我见过有人在 probe 里用 msleep(1000),结果系统启动慢了 1 秒,被项目经理骂了一顿。
2.5 本章小结
这一章我们讲了 Linux I2C 子系统的三层架构:核心层、总线驱动层、设备驱动层。核心数据结构 adapter 和 client 是理解整个框架的关键。设备树让硬件描述与驱动代码分离,提高了代码的可移植性。