3. I2C子系统基础:Linux I2C核心架构、I2C总线、适配器与从设备驱动模型

在DRM显示驱动中,I2C总线是连接GPU/主控与显示设备(如HDMI/DVI/DP接口的EDID读取、DDC通信、触摸屏控制器)的核心物理通道。理解Linux I2C子系统的分层架构,是调试显示驱动通信问题的前提。本章从I2C核心架构出发,逐步拆解总线、适配器与从设备驱动模型。

3.1 Linux I2C子系统整体架构

Linux I2C子系统采用三层分离设计,将物理适配器(I2C控制器硬件)与具体从设备驱动解耦。核心层提供统一的设备注册、数据传输和总线管理接口。

架构要点: I2C核心层维护一个虚拟I2C总线,所有适配器注册到该总线,所有从设备挂接到对应适配器。数据传输路径为:从设备驱动 → I2C核心 → 适配器驱动 → 硬件控制器。
Linux I2C子系统三层架构 Linux I2C 子系统三层架构 用户空间 / 内核其他子系统(DRM/Input等) I2C 核心层 (i2c-core) 设备注册/注销 | 总线管理 | 数据传输协议 (i2c_transfer) | 地址解析 I2C 适配器驱动 struct i2c_algorithm master_xfer / smbus_xfer I2C 从设备驱动 struct i2c_driver probe / remove / id_table I2C 控制器硬件 (SoC/外设) I2C 从设备 (EDID/触摸屏/Sensor) DRM驱动通过I2C核心 读取EDID/DDC数据

3.2 I2C核心数据结构与总线模型

I2C子系统基于Linux设备驱动模型(LDM),核心数据结构包括:

数据结构 作用 关键字段
struct i2c_adapter 代表一个I2C控制器(主设备) nr(总线编号)、algo(算法指针)、dev(设备结构)
struct i2c_algorithm 定义适配器支持的传输方法 master_xfer、smbus_xfer、functionality
struct i2c_client 代表一个I2C从设备 addr(7位/10位地址)、adapter(所属适配器)、name
struct i2c_driver 从设备驱动实现 probe、remove、id_table(匹配表)
struct i2c_msg 一次传输的消息单元 addr、flags(读写方向)、len、buf
调试提示: 在/sys/bus/i2c/devices/目录下可以查看系统中所有I2C设备。使用i2cdetect -l列出所有适配器,i2cdetect -y <bus_number>扫描从设备地址。

3.3 I2C总线:虚拟总线与设备匹配

I2C子系统维护一条虚拟总线i2c_bus_type,所有适配器和从设备都注册在这条总线上。设备与驱动的匹配过程如下:

  1. 适配器注册:调用i2c_add_adapter(),分配总线编号,创建i2c_adapter设备并挂载到虚拟总线。
  2. 从设备注册:通过设备树(DT)或板级代码创建i2c_client,指定适配器编号和从设备地址。
  3. 驱动匹配:当i2c_driver注册时,核心遍历已注册的i2c_client,通过id_table或设备树compatible属性匹配。
  4. probe调用:匹配成功后调用驱动的probe函数,完成设备初始化。
I2C设备与驱动匹配流程 I2C 设备与驱动匹配流程 ① 适配器注册 i2c_add_adapter() ② 从设备注册 i2c_new_client_device() ③ 驱动注册 i2c_add_driver() ④ 匹配判断 id_table / of_match_table 匹配成功 ⑤ probe() 调用 设备初始化 等待后续驱动注册 设备树中定义 reg = <0x50> 等地址

3.4 适配器驱动:核心传输接口

每个I2C适配器驱动必须实现struct i2c_algorithm中的传输函数。这是与硬件控制器交互的底层接口:

// i2c_algorithm 核心结构
struct i2c_algorithm {
    // 标准I2C传输:发送/接收一组消息
    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);
};
关键实现细节: master_xfer接收一个i2c_msg数组,支持组合传输(combined transfer)。例如,写寄存器地址后读数据(典型的EDID读取序列)通过两个msg实现:第一个msg写偏移地址,第二个msg读数据。

3.5 从设备驱动模型:probe与数据传输

从设备驱动通过struct i2c_driver注册,核心是probe函数和id_table匹配表。以DRM中常见的EDID读取为例:

// I2C从设备驱动模板
static const struct i2c_device_id edid_i2c_id[] = {
    { "edid_rom", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, edid_i2c_id);

static int edid_i2c_probe(struct i2c_client *client)
{
    struct edid_data *data;
    
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    i2c_set_clientdata(client, data);
    
    // 读取EDID数据
    // 使用 i2c_master_send / i2c_master_recv 或 i2c_transfer
    return 0;
}

static struct i2c_driver edid_i2c_driver = {
    .driver = {
        .name = "edid_rom",
        .of_match_table = of_match_ptr(edid_of_match),
    },
    .probe = edid_i2c_probe,
    .remove = edid_i2c_remove,
    .id_table = edid_i2c_id,
};
module_i2c_driver(edid_i2c_driver);

3.6 数据传输API:i2c_transfer与辅助函数

从设备驱动使用以下API与硬件通信:

API函数 说明 典型使用场景
i2c_transfer(adap, msgs, num) 底层传输接口,支持多消息组合 EDID读取(写偏移+读数据)
i2c_master_send(client, buf, len) 向从设备发送数据 配置寄存器、发送命令
i2c_master_recv(client, buf, len) 从从设备接收数据 读取状态、获取数据
i2c_smbus_read_byte_data(client, cmd) SMBus协议:读一个字节 读取传感器值
i2c_smbus_write_byte_data(client, cmd, val) SMBus协议:写一个字节 设置寄存器
调试陷阱: 使用i2c_master_send/recv时,驱动会自动添加START/STOP信号。但组合传输(如EDID读取)必须使用i2c_transfer并设置I2C_M_RD标志,否则从设备可能无法正确响应。
I2C数据传输:单次读写 vs 组合传输 I2C 数据传输:单次读写 vs 组合传输 单次写: START + 地址+W 数据字节 STOP 单次读: START + 地址+R 读取数据 STOP 组合传输(EDID读取): START + 地址+W 偏移地址 Sr START + 地址+R 读取EDID STOP Sr = 重复起始 无STOP信号

3.7 设备树中的I2C节点配置

在嵌入式系统中,I2C设备通常在设备树中描述。以下是一个典型配置:

// I2C适配器节点(SoC内部)
&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  // 100kHz标准模式
    pinctrl-names = "default";
    pinctrl-0 = <&i2c1_pins>;
    
    // 从设备:HDMI EDID ROM(地址0x50)
    edid@50 {
        compatible = "hdmi-edid-rom";
        reg = <0x50>;
    };
    
    // 从设备:触摸屏控制器
    touch@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};
调试技巧: 使用i2cdump -y <bus> <addr>可以dump从设备所有寄存器的值。对于EDID,地址通常为0x50,dump结果前8字节应为00 FF FF FF FF FF FF 00(EDID头标识)。

3.8 常见调试场景与排查方法

问题现象 可能原因 排查命令/方法
i2cdetect扫描不到设备 地址错误、硬件未上电、SCL/SDA线路问题 示波器测量波形;检查设备树reg值
probe函数未调用 compatible不匹配、驱动未编译进内核 检查/sys/bus/i2c/devices/目录
数据传输返回-EREMOTEIO 从设备NACK、时钟拉伸超时 增加i2c-gpio模拟适配器调试
EDID读取不全 组合传输msg顺序错误、偏移地址未写 使用i2c_transfer并检查flags
DRM调试关联: 在DRM驱动中,EDID读取通常通过drm_get_edid()函数完成,该函数内部调用I2C核心的i2c_transfer()。如果I2C通信失败,DRM驱动将无法获取显示器的分辨率、刷新率等关键参数,导致无显示输出。