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

做嵌入式Linux驱动开发,I2C是绕不开的坎。很多初学者一上来就对着datasheet操作寄存器,结果发现内核里根本不让这么干。为什么?因为Linux内核已经帮你搭好了一套完整的I2C驱动框架。你只需要往这个框架里填东西就行。

我个人习惯把I2C子系统拆成四层来看:I2C核心层、I2C总线驱动、I2C设备驱动,以及设备树绑定。搞懂这四层的关系,你就能理解I2C设备在内核里是怎么被识别、怎么被控制的。

核心观点:Linux I2C驱动框架的本质是「分层解耦」。硬件相关的代码和业务逻辑分开,你写驱动时只需要关注设备本身的行为,不用管I2C控制器怎么发时序。

Linux I2C 子系统四层架构 用户空间应用 (i2c-tools / 用户驱动) I2C 核心层 (i2c-core) 设备注册/注销 · 总线管理 · 协议抽象 I2C 总线驱动 (i2c-adapter / i2c-algo) I2C控制器硬件操作 · 时序生成 · 中断处理 I2C 设备驱动 (i2c-client / i2c-driver) 传感器/EEPROM/ADC 等具体设备操作 设备树绑定 分层职责 核心层:搭桥 总线层:管硬件 设备层:管业务 设备树:管配置 各层通过标准 接口通信 互不依赖

2.1 I2C核心层:驱动框架的骨架

I2C核心层是内核帮你写好的基础设施。它不关心你的I2C控制器是哪个厂家的,也不关心你挂的是什么传感器。它只做三件事:管理总线、管理设备、提供统一的读写接口。

核心层暴露给驱动开发者的主要数据结构就两个:struct i2c_adapter 和 struct i2c_client。前者代表一条I2C总线,后者代表挂在总线上的一个设备。你想想看,整个I2C子系统其实就是围绕这两个结构体转的。

我的经验:刚开始看i2c-core.c源码时别硬啃。你只需要知道核心层帮你做了设备匹配、地址检测、读写超时处理这些脏活。我早期写驱动时总想自己实现地址检测逻辑,后来发现核心层已经做得很完善了,直接用就行。

2.2 I2C总线驱动:跟硬件打交道的那层

总线驱动,说白了就是I2C控制器的驱动。它负责把I2C协议里的START、STOP、ACK、NACK这些时序变成具体的寄存器操作。不同的芯片平台,这部分代码完全不同。

总线驱动需要实现两个关键东西:

  • i2c_algorithm:定义master_xfer函数,这是发送I2C消息的核心函数
  • i2c_adapter:注册到核心层,代表一条物理I2C总线

我记得有一次调试一个I2C触摸屏,发现读回来的数据总是错位。查了两天才发现是总线驱动的master_xfer里没处理好重复起始条件。嗯,这种坑只有踩过才知道疼。

组件 职责 关键函数/结构体
i2c_algorithm 定义I2C通信的底层操作 master_xfer(), smbus_xfer()
i2c_adapter 代表一条I2C总线 i2c_add_adapter(), i2c_del_adapter()
i2c_msg 描述一次I2C传输消息 addr, flags, len, buf

2.3 I2C设备驱动:你真正要写的那部分

如果你只是要驱动一个I2C传感器,那你只需要写设备驱动。设备驱动不关心I2C控制器是哪个IP核,它只通过核心层提供的API来读写数据。

设备驱动的核心是 struct i2c_driver。你需要实现:

  • probe函数:设备被检测到时调用,初始化设备、注册输入子系统或字符设备
  • remove函数:设备移除时清理资源
  • id_table:告诉内核你的驱动支持哪些设备

读写操作通过 i2c_transfer() 或 i2c_smbus_read_byte_data() 这类API完成。你不需要自己操作GPIO模拟时序,也不需要管I2C控制器的寄存器。

避坑指南:我曾经在probe函数里直接调用i2c_transfer去读设备ID,结果系统卡死了。为什么?因为probe执行时I2C总线可能还没准备好。正确的做法是在probe里只做资源分配,真正的通信放到设备打开之后。

2.4 设备树绑定:让驱动和设备对上号

设备树是Linux内核用来描述硬件配置的机制。对于I2C设备,你需要在设备树里告诉内核:

  • 这个设备挂在哪条I2C总线上
  • 它的I2C地址是多少
  • 它需要哪些额外的配置(中断引脚、频率等)

一个典型的I2C设备节点长这样:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    temperature_sensor: tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
    };
};

这里要注意几点:

  • compatible:必须和驱动里id_table中的字符串匹配
  • reg:设备的I2C地址,7位地址模式
  • status:不写"okay"的话内核不会去枚举这个设备

重要提醒:设备树里的reg地址写错了,驱动probe根本不会调用。我见过有人把7位地址左移一位填进去,结果内核报"no device at address"。I2C设备树里填的是原始7位地址,不是8位读写地址。

2.5 驱动匹配流程:从设备树到probe

搞清楚了各层的职责,我们再看看整个流程是怎么串起来的:

  1. 内核启动时,I2C总线驱动先注册,调用 i2c_add_adapter()
  2. 设备树解析器扫描I2C子节点,为每个节点创建 i2c_client
  3. 核心层把 i2c_client 挂到对应的 i2c_adapter 上
  4. 设备驱动注册时,核心层遍历所有 i2c_client,匹配compatible字段
  5. 匹配成功,调用驱动的probe函数

这个流程你理解了,I2C驱动开发的基本功就算打牢了。后面写具体设备驱动时,你只需要关注probe里怎么初始化设备、怎么通过核心层API读写数据。

个人建议:刚开始学的时候,别急着写驱动。先在开发板上用i2c-tools工具手动探测一下设备地址,确认硬件没问题了再动手写代码。我每次都是这么干的,能省掉一半的调试时间。