3. Linux I2C子系统架构:三层结构深度拆解

做嵌入式Linux驱动开发,I2C子系统是绕不开的坎。我记得刚入行那会儿,看着I2C的代码一头雾水——怎么一个简单的I2C读写,要绕这么多层?后来踩了几个坑才明白,这种分层设计其实很巧妙。

今天我就带你彻底搞懂Linux I2C子系统的三层架构:I2C核心层I2C总线驱动I2C设备驱动。说白了,这三层各司其职,把硬件细节和上层应用隔离开来。

Linux I2C 子系统三层架构 用户空间应用 I2C 设备驱动层 i2c_driver / i2c_client / 具体设备操作 I2C 核心层 (I2C Core) i2c-core.c / 算法注册 / 总线管理 / 设备模型 I2C 总线驱动层 (Adapter) i2c_adapter / i2c_algorithm / 硬件寄存器操作 软件层 硬件层 API 接口 数据 结构 硬件 抽象

3.1 为什么需要三层结构?

你想想看,一个嵌入式板子上可能挂了好几个I2C设备——温度传感器、触摸屏、音频Codec。每个设备的寄存器地址、操作时序都不一样。如果让每个驱动都直接操作I2C控制器的寄存器,那代码就乱套了。

Linux内核的设计哲学就是分层抽象。I2C子系统把驱动拆成三层:

  • I2C核心层:提供统一的API和数据结构,管理总线注册、设备枚举
  • I2C总线驱动:直接操作I2C控制器硬件,处理时序、中断、DMA
  • I2C设备驱动:只关心具体设备的功能,比如读温度、设置音量

这样做的好处很明显——换一个I2C控制器,设备驱动不用改;换一个I2C设备,总线驱动不用动。我在项目中遇到过好几次平台迁移,从IMX6换到RK3399,设备驱动几乎没改,只换了总线驱动就搞定了。

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

核心层的代码主要在 drivers/i2c/i2c-core.c 里。它不直接操作硬件,而是提供了一套框架。我个人习惯把核心层理解为「I2C子系统的大管家」。

核心层主要干这几件事:

功能模块 说明 关键函数/结构体
总线注册 管理I2C总线的添加和删除 i2c_add_adapter()
设备枚举 从设备树或ACPI解析设备信息 of_i2c_register_devices()
驱动匹配 将设备驱动绑定到对应设备 i2c_device_match()
数据传输 提供统一的读写接口 i2c_transfer()
核心数据结构:
struct i2c_adapter 代表一条I2C总线
struct i2c_client 代表一个I2C设备
struct i2c_driver 代表一个设备驱动
struct i2c_algorithm 代表总线通信方法

3.3 I2C总线驱动:和硬件打交道的人

总线驱动,说白了就是I2C控制器的驱动。它负责把核心层的请求翻译成硬件时序。每个I2C控制器(比如IMX6的I2C IP核)都需要一个对应的总线驱动。

总线驱动的核心是 struct i2c_algorithm,它定义了三个关键方法:

struct i2c_algorithm {
    /* 主模式发送 */
    int (*master_xfer)(struct i2c_adapter *adap, 
                       struct i2c_msg *msgs, int num);
    /* 从模式接收(较少用) */
    int (*slave_xfer)(struct i2c_adapter *adap, 
                      struct i2c_msg *msgs);
    /* 功能支持查询 */
    u32 (*functionality)(struct i2c_adapter *adap);
};

嗯,这里要注意——master_xfer 是总线驱动里最重要的函数。它接收一个 i2c_msg 数组,然后按照I2C协议在硬件上产生START、ACK、STOP等信号。

避坑指南:
我曾经在调试一个I2C触摸屏时,发现读数据总是少一个字节。查了两天才发现是总线驱动的 master_xfer 实现里,对重复起始位(Repeated Start)的处理有问题。很多新手容易忽略这个细节。

3.4 I2C设备驱动:只关心业务逻辑

设备驱动是最上层,也是我们平时写的最多的部分。它只关心「怎么操作这个设备」,不关心「怎么在I2C总线上发数据」。

一个典型的I2C设备驱动结构:

static const struct i2c_device_id temp_sensor_id[] = {
    { "lm75", 0 },
    { }
};

static struct i2c_driver temp_sensor_driver = {
    .driver = {
        .name = "lm75",
        .of_match_table = temp_sensor_of_match,
    },
    .probe    = temp_sensor_probe,
    .remove   = temp_sensor_remove,
    .id_table = temp_sensor_id,
};

module_i2c_driver(temp_sensor_driver);

你看,设备驱动只需要注册 i2c_driver,核心层会自动帮我们做设备匹配。匹配成功后调用 probe 函数,我们在里面初始化设备、注册字符设备或输入子系统。

读写设备时,设备驱动调用核心层提供的API:

/* 从I2C设备读取一个字节 */
int i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);

/* 向I2C设备写入一个字节 */
int i2c_smbus_write_byte_data(struct i2c_client *client, 
                               u8 command, u8 value);

/* 更通用的传输接口 */
int i2c_transfer(struct i2c_adapter *adap, 
                 struct i2c_msg *msgs, int num);
我的经验:
写设备驱动时,优先用 i2c_smbus_xxx 系列函数。它们封装了错误处理,代码更简洁。只有在需要自定义时序(比如某些传感器要求连续读取多个寄存器)时,才用 i2c_transfer

3.5 三层之间的数据流

搞清楚了每层的职责,我们再看看数据是怎么在三层之间流动的。假设用户空间程序要读取一个温度传感器的值:

  1. 用户空间:通过 read() 系统调用进入内核
  2. 设备驱动:调用 i2c_smbus_read_byte_data()
  3. 核心层:构造 i2c_msg,调用 i2c_transfer()
  4. 核心层:找到对应的 i2c_adapter,调用 master_xfer()
  5. 总线驱动:操作I2C控制器寄存器,产生硬件时序
  6. 硬件:SCL/SDA线上产生I2C协议波形

返回路径正好相反。整个过程对设备驱动是透明的——它根本不知道底层用的是哪个I2C控制器。

注意:
三层之间的调用都是同步的。如果总线驱动在 master_xfer 里加了延时或等待,整个调用链都会被阻塞。我在调试一个音频Codec时,就因为总线驱动里有个10ms的忙等,导致音频播放出现卡顿。后来改成了中断方式才解决。

3.6 设备树与三层架构的配合

现在的嵌入式Linux项目基本都用设备树(Device Tree)。设备树描述硬件拓扑,I2C子系统从中解析出总线和设备信息。

一个典型的I2C设备树节点:

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

    temp_sensor: lm75@48 {
        compatible = "national,lm75";
        reg = <0x48>;
    };

    touch: ft5x06@38 {
        compatible = "edt,edt-ft5x06";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};

核心层在初始化时,会解析 i2c1 节点下的子节点,为每个子节点创建一个 i2c_client。然后根据 compatible 属性匹配对应的 i2c_driver

我个人习惯在设备树里把 clock-frequency 显式写出来,虽然很多平台有默认值。这样做的好处是——换平台时一眼就能看到总线速度,不用去翻芯片手册。

3.7 小结

Linux I2C子系统的三层架构,说白了就是「分而治之」的思想。核心层做调度,总线驱动管硬件,设备驱动管功能。每一层只关心自己的事,接口清晰,耦合度低。

搞懂了这个架构,后面写I2C设备驱动或者移植I2C总线驱动,你就知道该往哪个文件里加代码了。嗯,下一节我们具体看看怎么编写一个I2C设备驱动,到时候我会拿一个真实的温度传感器驱动做例子。

公众号:蓝海资料掘金营,微信deep3321