I2C驱动框架:Linux内核I2C子系统架构
说实话,I2C驱动框架是很多嵌入式工程师的入门第一道坎。我记得刚接触Linux驱动时,看着i2c_adapter、i2c_client这些结构体,脑袋都是懵的。后来在项目中调了几个月I2C外设,才慢慢摸清门道。
今天我就带你把这个框架拆开来看。说白了,Linux内核的I2C子系统就三层:I2C核心、I2C总线驱动、I2C设备驱动。你想想看,这跟硬件上的I2C总线是不是很像?
1. I2C核心层:承上启下的枢纽
I2C核心层,说白了就是内核帮你搭好的脚手架。它不直接操作硬件,而是提供一套统一的接口和数据结构。我个人习惯把核心层理解成一个「调度中心」——总线驱动告诉它怎么发数据,设备驱动告诉它要跟谁通信,核心层负责把两边撮合起来。
核心层主要干这几件事:
- 设备模型管理:负责i2c_adapter和i2c_client的注册、注销、匹配
- 数据传输调度:提供i2c_transfer()、i2c_master_send()等API
- SMBus兼容:封装了SMBus协议的支持函数
- 设备文件接口:通过i2c-dev.c提供/dev/i2c-N用户态访问
2. I2C总线驱动:跟硬件打交道的苦力
总线驱动是真正操作I2C控制器的代码。我在项目中调过好几款不同平台的I2C控制器,发现它们虽然寄存器千差万别,但最终都要抽象成两个关键结构体:i2c_adapter和i2c_algorithm。
2.1 i2c_adapter:适配器实体
每个I2C控制器对应一个i2c_adapter实例。你可以把它想象成硬件控制器的「软件代言人」。
struct i2c_adapter {
struct module *owner;
unsigned int class; // 适配器支持的设备类型
const struct i2c_algorithm *algo; // 算法指针,核心!
void *algo_data; // 算法私有数据
int nr; // 适配器编号,对应/dev/i2c-N
struct device dev; // 内核设备模型
struct list_head userspace_clients; // 用户态打开的设备列表
// ... 其他字段
};
嗯,这里要注意nr字段。我遇到过一个问题:板子上有两个I2C控制器,但设备树里顺序写反了,导致/dev/i2c-0和/dev/i2c-1对不上硬件。排查了半天才发现是nr分配的问题。
2.2 i2c_algorithm:通信算法
这个结构体定义了具体的收发操作。说白了,它就是一组函数指针。
struct i2c_algorithm {
// 主模式传输(最常用)
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
// SMBus传输(可选)
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);
};
3. I2C设备驱动:跟外设对话的翻译官
设备驱动负责跟具体的I2C外设通信。比如AT24C02 EEPROM、MPU6050陀螺仪,它们的驱动都属于这一层。关键数据结构有两个:i2c_client和i2c_driver。
3.1 i2c_client:设备的软件化身
每个I2C从设备对应一个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; // 用于设备检测链表
// ... 其他字段
};
addr字段是I2C通信的关键。我记得有一次调试,设备树里写的地址是0x50,但实际芯片是0x50左移一位后的0xA0。这种地址对齐问题,新手特别容易踩坑。
3.2 i2c_driver:驱动的注册模板
i2c_driver是设备驱动的「身份证」,内核通过它来匹配设备和驱动。
struct i2c_driver {
int (*probe)(struct i2c_client *client,
const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
struct device_driver driver; // 内核驱动模型
const struct i2c_device_id *id_table; // 设备ID表
// 设备检测回调(可选)
int (*detect)(struct i2c_client *client,
struct i2c_board_info *info);
const unsigned short *address_list; // 检测地址列表
};
4. 驱动匹配流程:设备与驱动的「相亲」过程
内核怎么把i2c_client和i2c_driver配对?说白了就两种方式:
| 匹配方式 | 原理 | 适用场景 |
|---|---|---|
| 设备树匹配 | 通过compatible属性匹配 | 主流方式,推荐使用 |
| ID表匹配 | 通过i2c_device_id.name匹配 | 传统方式,兼容旧代码 |
| ACPI匹配 | 通过ACPI表匹配 | x86平台常用 |
| 动态检测 | 调用detect回调扫描地址 | 热插拔或未知设备 |
匹配成功后,内核会调用i2c_driver的probe函数。你想想看,这时候i2c_client已经准备好了,adapter也初始化好了,probe里就可以放心地调用i2c_transfer来读写数据了。
5. 数据传输:i2c_transfer的核心机制
所有I2C数据收发,最终都汇聚到i2c_transfer()这个函数。它的参数是i2c_msg结构体数组,支持一次传输多个消息(比如先写寄存器地址,再读数据)。
struct i2c_msg {
__u16 addr; // 从机地址
__u16 flags; // 标志位(读/写、10位地址等)
__u16 len; // 数据长度
__u8 *buf; // 数据缓冲区
};
我个人习惯把i2c_msg理解成「I2C总线上的一个数据帧」。多个msg组合起来,就构成了一个完整的通信事务。内核保证这些msg在总线上是原子操作的——不会被其他设备打断。
6. 总结一下
I2C驱动框架其实不复杂。记住这个对应关系:
- 硬件I2C控制器 → i2c_adapter(适配器)
- 硬件收发逻辑 → i2c_algorithm(算法)
- I2C从设备 → i2c_client(客户端)
- 设备驱动代码 → i2c_driver(驱动)
写驱动时,你只需要关注两件事:总线驱动里实现i2c_algorithm的master_xfer,设备驱动里在probe中调用i2c_transfer。剩下的匹配、注册、卸载,内核核心层都帮你搞定了。
嗯,框架就是这样。下次你拿到一块新的I2C外设芯片,照着这个思路去写驱动,心里就有底了。
可可精品资料库