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核心 → 适配器驱动 → 硬件控制器。
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,所有适配器和从设备都注册在这条总线上。设备与驱动的匹配过程如下:
- 适配器注册:调用
i2c_add_adapter(),分配总线编号,创建i2c_adapter设备并挂载到虚拟总线。 - 从设备注册:通过设备树(DT)或板级代码创建
i2c_client,指定适配器编号和从设备地址。 - 驱动匹配:当
i2c_driver注册时,核心遍历已注册的i2c_client,通过id_table或设备树compatible属性匹配。 - probe调用:匹配成功后调用驱动的
probe函数,完成设备初始化。
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标志,否则从设备可能无法正确响应。
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驱动将无法获取显示器的分辨率、刷新率等关键参数,导致无显示输出。