2. I2C总线协议基础

I2C(Inter-Integrated Circuit)总线是一种由Philips公司(现NXP)开发的同步、多主从、串行通信总线。在Linux显示驱动中,I2C主要用于配置显示控制器的寄存器、读取EDID数据、控制触摸屏及背光调节等。理解I2C的物理层与协议细节,是正确编写驱动与设备树节点的前提。

2.1 I2C物理层

I2C总线仅使用两条双向开漏信号线:

  • SCL(Serial Clock Line):串行时钟线,由主设备驱动。
  • SDA(Serial Data Line):串行数据线,用于地址和数据的双向传输。

两条线均通过上拉电阻连接到电源电压(通常为3.3V或5V)。当总线空闲时,SCL和SDA均为高电平。由于采用开漏结构,多个设备可以并联在同一总线上,通过线与逻辑实现仲裁。

驱动开发提示: 在Linux设备树中,I2C控制器的clock-frequency属性通常设置为100000(标准模式)或400000(快速模式)。显示驱动中常用的EDID读取要求时钟频率不低于50kHz,否则可能导致读取超时。

2.2 起始条件与停止条件

I2C总线上数据传输的起始和停止由SCL与SDA的特定电平变化定义:

条件 SCL状态 SDA状态 说明
起始条件(S) 高电平 下降沿(高→低) 主设备在SCL高电平时将SDA拉低,表示传输开始
停止条件(P) 高电平 上升沿(低→高) 主设备在SCL高电平时将SDA释放为高,表示传输结束
重复起始条件(Sr) 高电平 下降沿 在未发送停止条件前再次发送起始条件,用于改变传输方向
常见错误: 在驱动代码中,如果I2C控制器在发送停止条件前发生超时,总线可能被锁死。此时需要调用i2c_recover_bus()或硬件复位来释放总线。
I2C起始与停止条件时序图 I2C起始/停止条件时序图 SCL SDA S P D7 D6 D5 D4 D3 D2 D1 D0 起始条件:SCL高电平时SDA下降沿 | 停止条件:SCL高电平时SDA上升沿

2.3 数据帧格式

I2C数据帧以字节为单位传输,每个字节后跟一个应答位(ACK/NACK)。完整的传输帧结构如下:

  1. 起始条件(S)
  2. 7位从设备地址 + 读/写位(R/W):共8位,R/W=0表示写,R/W=1表示读。
  3. 从设备应答位(ACK):从设备拉低SDA表示应答。
  4. 数据字节(8位) + 应答位:可重复多个数据字节。
  5. 停止条件(P)
关键帧格式: 在显示驱动中,写寄存器操作通常发送:起始条件 + 设备地址(写) + 寄存器地址 + 数据值 + 停止条件。读操作则需先发送设备地址(写)和寄存器地址,再发送重复起始条件 + 设备地址(读)来读取数据。
I2C数据帧格式 I2C数据帧格式(7位地址 + 写操作) S 起始 A6:A0 (7位地址) 从设备地址 R/W 0=写 ACK 应答 D7:D0 (数据) 数据字节 ACK 应答 D7:D0 (数据) 更多字节 ACK 应答 P 停止 每个数据字节后必须跟一个ACK位(从设备拉低SDA),否则主设备发送NACK并终止传输 读操作帧格式(重复起始条件) S 地址+W ACK 寄存器 ACK Sr 地址+R ACK 数据 NACK P

2.4 7位地址与10位地址

I2C支持两种地址格式,在设备树和驱动中需要正确配置:

特性 7位地址 10位地址
地址范围 0x00 ~ 0x7F(128个,保留16个) 0x000 ~ 0x3FF(1024个)
帧头字节数 1字节(地址+R/W) 2字节(11110xx + R/W + 低8位地址)
保留地址 0x00(通用呼叫)、0x01(起始字节)等 无特殊保留
显示驱动常见设备 HDMI EDID(0x50)、触摸控制器(0x38~0x3F) 部分高端显示控制器(如某些DP接口芯片)
设备树表示 reg = <0x50>; reg = <0x0300>;(需驱动支持)
设备树配置示例: 在设备树中,I2C从设备地址通过reg属性指定。对于7位地址,直接写入地址值;对于10位地址,需要将地址的高两位与标志位组合,具体实现依赖I2C控制器驱动。
// 7位地址设备树示例(HDMI EDID)
&i2c2 {
    status = "okay";
    clock-frequency = <100000>;

    hdmi-edid@50 {
        compatible = "hdmi-edid";
        reg = <0x50>;  // 7位地址 0x50
    };
};

// 10位地址设备树示例(假设设备地址为 0x02A3)
&i2c3 {
    status = "okay";
    clock-frequency = <400000>;

    display-controller@2a3 {
        compatible = "vendor,display-ctrl";
        reg = <0x02A3>;  // 10位地址,驱动需设置 I2C_TENBIT 标志
    };
};

2.5 时钟同步与仲裁

I2C总线的时钟同步与仲裁机制是多主设备共存的基础:

  • 时钟同步:多个主设备同时驱动SCL时,SCL的低电平时间由最长的低电平决定,高电平时间由最短的高电平决定。所有主设备同步到统一的时钟。
  • 总线仲裁:多个主设备同时发送数据时,通过SDA线的线与逻辑进行仲裁。发送低电平的设备会覆盖发送高电平的设备,失去仲裁的设备自动转为从模式。
I2C时钟同步与总线仲裁 I2C时钟同步与总线仲裁 主1 SCL 主2 SCL 同步SCL 时钟同步:SCL低电平取最长,高电平取最短,所有主设备同步到统一时钟 总线仲裁:SDA线与逻辑,低电平优先;失去仲裁的设备释放总线并转为从模式 仲裁示例:主1发送0,主2发送1,主1获胜 主1 SDA: 0 主2 SDA: 1 总线SDA: 0(主1获胜)

2.6 I2C在显示驱动中的典型应用

在Linux显示驱动开发中,I2C总线主要用于以下场景:

应用场景 典型设备地址 通信内容 驱动接口
EDID读取 0x50(DDC通道) 读取128字节显示器EDID数据 drm_edid_read() / i2c_transfer()
显示控制器配置 0x38~0x3F(取决于芯片) 写寄存器设置分辨率、时序、色彩格式 regmap_write() / i2c_smbus_write_byte_data()
触摸屏控制 0x38~0x3F(如FT5x06) 读取触摸坐标、配置灵敏度 input子系统 + i2c_client
背光调节 0x2C~0x2F(如LP8556) 写PWM占空比、亮度等级 backlight子系统 + i2c
HDMI/DP接口控制 0x60~0x6F(如TDA998x) 配置音频/视频输出、HDCP密钥 drm_bridge + i2c
驱动开发最佳实践: 在编写I2C显示驱动时,建议使用devm_i2c_new_dummy_device()或i2c_new_client_device()来注册从设备。对于EDID读取,务必处理-ENXIO(无应答)错误,并在设备树中正确设置ddc-i2c-bus属性。
// Linux内核中I2C消息传输示例(读取EDID)
#include <linux/i2c.h>

static int edid_read(struct i2c_client *client, u8 *buf, int len)
{
    struct i2c_msg msgs[2];
    u8 reg = 0x00;  // EDID起始偏移

    // 写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;  // 写
    msgs[0].len = 1;
    msgs[0].buf = &reg;

    // 读EDID数据
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;  // 读
    msgs[1].len = len;
    msgs[1].buf = buf;

    return i2c_transfer(client->adapter, msgs, 2);
}
注意: 在I2C传输中,如果从设备不支持重复起始条件(Sr),驱动需要先发送停止条件再重新发送起始条件。部分老旧显示控制器存在此限制,需要在驱动中通过I2C_M_STOP标志控制。