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()或硬件复位来释放总线。
2.3 数据帧格式
I2C数据帧以字节为单位传输,每个字节后跟一个应答位(ACK/NACK)。完整的传输帧结构如下:
- 起始条件(S)
- 7位从设备地址 + 读/写位(R/W):共8位,R/W=0表示写,R/W=1表示读。
- 从设备应答位(ACK):从设备拉低SDA表示应答。
- 数据字节(8位) + 应答位:可重复多个数据字节。
- 停止条件(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线的线与逻辑进行仲裁。发送低电平的设备会覆盖发送高电平的设备,失去仲裁的设备自动转为从模式。
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 = ®
// 读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标志控制。