I2C驱动框架:Linux内核I2C子系统架构

说实话,I2C驱动框架是很多嵌入式工程师的入门第一道坎。我记得刚接触Linux驱动时,看着i2c_adapter、i2c_client这些结构体,脑袋都是懵的。后来在项目中调了几个月I2C外设,才慢慢摸清门道。

今天我就带你把这个框架拆开来看。说白了,Linux内核的I2C子系统就三层:I2C核心、I2C总线驱动、I2C设备驱动。你想想看,这跟硬件上的I2C总线是不是很像?

Linux I2C 子系统三层架构 用户空间 应用程序 / 用户态驱动 I2C 核心层 (I2C Core) i2c-core.c / i2c-dev.c / 设备模型抽象 注册/注销 数据收发 I2C 总线驱动 i2c_adapter + i2c_algorithm 硬件相关(如i2c-imx.c) I2C 设备驱动 i2c_driver + i2c_client 外设相关(如eeprom.c) 硬件层 I2C控制器 / SDA & SCL 物理总线

1. I2C核心层:承上启下的枢纽

I2C核心层,说白了就是内核帮你搭好的脚手架。它不直接操作硬件,而是提供一套统一的接口和数据结构。我个人习惯把核心层理解成一个「调度中心」——总线驱动告诉它怎么发数据,设备驱动告诉它要跟谁通信,核心层负责把两边撮合起来。

核心层主要干这几件事:

  • 设备模型管理:负责i2c_adapter和i2c_client的注册、注销、匹配
  • 数据传输调度:提供i2c_transfer()、i2c_master_send()等API
  • SMBus兼容:封装了SMBus协议的支持函数
  • 设备文件接口:通过i2c-dev.c提供/dev/i2c-N用户态访问
💡 核心要点: 核心层不关心你的I2C控制器是IMX6还是STM32MP1,它只关心你有没有实现i2c_algorithm里的master_xfer函数。

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);
};
🔧 实战经验: 我在移植I2C驱动时,曾经只实现了master_xfer,没实现smbus_xfer。结果上层用SMBus协议的设备(比如某些温度传感器)直接报错。后来老老实实把smbus_xfer也补上了,其实就是对master_xfer的封装。

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; // 检测地址列表
};
⚠️ 避坑指南: 我曾经在probe函数里申请了GPIO中断,但remove函数里忘了释放。结果驱动卸载再加载时,中断申请失败,整个系统卡死。记住:probe里申请的资源,remove里一定要释放。

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在总线上是原子操作的——不会被其他设备打断。

💡 性能提示: 如果你要连续读写大量数据,尽量把多个msg放在一次i2c_transfer调用里。我在项目中优化过一个摄像头驱动,把原来每次单独发start-stop的多次调用合并成一次多msg传输,吞吐量提升了30%以上。

6. 总结一下

I2C驱动框架其实不复杂。记住这个对应关系:

  • 硬件I2C控制器 → i2c_adapter(适配器)
  • 硬件收发逻辑 → i2c_algorithm(算法)
  • I2C从设备 → i2c_client(客户端)
  • 设备驱动代码 → i2c_driver(驱动)

写驱动时,你只需要关注两件事:总线驱动里实现i2c_algorithm的master_xfer,设备驱动里在probe中调用i2c_transfer。剩下的匹配、注册、卸载,内核核心层都帮你搞定了。

嗯,框架就是这样。下次你拿到一块新的I2C外设芯片,照着这个思路去写驱动,心里就有底了。


可可精品资料库