4. I2C驱动框架设计:Linux内核I2C子系统架构
做嵌入式Linux驱动开发,I2C子系统是绕不开的坎。说实话,我刚接触这块时也被那套分层模型搞晕过——什么adapter、client、algorithm,一堆抽象概念。后来调了几个月I2C触摸屏驱动,才慢慢摸清门道。
今天咱们就把这套框架拆开揉碎,看看Linux内核到底怎么管理I2C设备的。
4.1 三层架构:从总线到设备
Linux内核的I2C子系统,说白了就是三层结构:
- 核心层(Core):提供注册/注销接口,管理总线号,处理数据传输的通用逻辑
- 适配器层(Adapter):对应具体的I2C控制器硬件,负责收发时序
- 客户端层(Client):对应挂在总线上的具体设备,比如温度传感器、EEPROM
我习惯把这三层比作快递系统:核心层是快递公司总部,定规则;适配器是各地分拣中心,负责装卸货;客户端就是收件人,只管收包裹。
关键认知:驱动开发者通常只写client驱动,但不懂adapter层,遇到硬件时序问题就抓瞎。我建议至少把adapter的注册流程走一遍。
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,中间经历了什么?
- client驱动调用
i2c_smbus_read_byte_data(client, reg) - 核心层构造
i2c_msg数组(先写寄存器地址,再读数据) - 核心层调用
adapter->algo->master_xfer() - adapter驱动解析msg,操作I2C控制器的数据寄存器、控制寄存器
- 硬件产生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、数据传输和错误处理。