言
在嵌入式 Linux 驱动开发中,I2C 是应用最广泛的外设总线之一。从 EEPROM、温湿度传感器、触摸屏到音频 Codec、PMIC 电源管理芯片,几乎都离不开 I2C 接口。Linux 内核为了统一管理不同厂商的 I2C 控制器和各类外设,抽象出了一套完整的 I2C 子系统框架。
很多初学者写 I2C 驱动,只会照搬例程调用i2c_smbus_read_byte_data,却不理解内核 I2C 子系统的分层架构、核心数据结构之间的关系,遇到通信失败、设备不识别等问题很难排查。本文就从架构层面入手,系统拆解 Linux I2C 子系统,再结合完整代码示例,讲解 I2C 设备驱动的标准开发流程。
一、I2C 子系统整体架构
Linux I2C 子系统采用典型的分层架构,清晰地分离了硬件控制器与外设协议,是 Linux 总线驱动中设计非常经典的子系统之一。
1. 两层驱动模型
I2C 子系统将驱动分为两大类,分别对应硬件层和协议层:
表格
| 驱动类型 | 别称 | 核心职责 | 典型代表 |
|---|---|---|---|
| 适配器驱动 | 控制器驱动 | 抽象 I2C 主机硬件,实现底层时序,向上提供统一传输接口 | i2c-imx.c(i.MX 系列)、i2c-bcm2835.c(树莓派) |
| 设备驱动 | 协议驱动 | 针对具体 I2C 外设,实现业务逻辑,通过子系统 API 与外设通信 | AT24 EEPROM 驱动、BH1750 光感驱动 |
简单理解:适配器驱动管硬件时序,设备驱动管业务逻辑。适配器驱动一般由芯片厂商随内核一并提供,驱动开发者的主要工作是编写针对具体外设的设备驱动。
2. 三大核心数据结构
I2C 子系统围绕三个核心结构体运转,三者的关系构成了整个子系统的骨架:
struct i2c_adapter:代表一个 I2C 主机适配器,对应一条物理 I2C 总线struct i2c_client:代表总线上一个具体的 I2C 从设备struct i2c_driver:代表 I2C 设备驱动,对应具体外设的驱动逻辑
三者的关系:一个i2c_adapter下可以挂载多个i2c_client;i2c_driver通过匹配表与i2c_client绑定,绑定成功后调用 probe 函数完成设备初始化。
二、核心数据结构详解
1. i2c_adapter
i2c_adapter是 I2C 主机控制器的抽象,描述了一条 I2C 总线的硬件能力和操作方法。
struct i2c_adapter { struct module *owner; unsigned int class; // 总线类型 const struct i2c_algorithm *algo; // 总线传输算法,核心回调集合 void *algo_data; int nr; // 总线编号 char name[48]; // 总线名称 struct device dev; int timeout; // 传输超时时间 int retries; // 重试次数 // ... };其中最核心的是i2c_algorithm结构体,它定义了适配器驱动实现的底层传输回调,最重要的就是master_xfer函数,对应 SPI 子系统中的transfer回调。适配器驱动的核心工作就是分配初始化i2c_adapter,实现传输回调,最后调用i2c_add_adapter注册到内核。
2. i2c_client
i2c_client是 I2C 从设备的抽象,每个物理从设备对应一个实例,保存了设备的地址、所属总线等硬件信息。
struct i2c_client { unsigned short flags; // 设备特性标志 unsigned short addr; // 7位从机地址(内核自动处理读写位) struct i2c_adapter *adapter; // 所属的I2C适配器 struct device dev; int irq; // 设备中断号 char name[I2C_NAME_SIZE]; // ... };重要说明:
i2c_client不是设备驱动手动创建的,而是内核根据设备树(或 ACPI、板级信息)自动创建的。驱动只需要通过匹配表找到对应的 client 即可。
3. i2c_driver
i2c_driver是 I2C 设备驱动的抽象,遵循 Linux 标准的驱动 - 设备匹配模型。
struct i2c_driver { int (*probe)(struct i2c_client *client, const struct i2c_device_id *id); int (*remove)(struct i2c_client *client); const struct i2c_device_id *id_table; // 传统ID匹配表 struct device_driver driver; // 设备树匹配表 const struct of_device_id *of_match_table; // ... };驱动开发者的核心工作就是填充这个结构体,实现 probe、remove 等函数,通过i2c_add_driver注册到子系统。内核提供了module_i2c_driver宏,可以简化驱动注册注销的样板代码。
三、数据传输模型:i2c_msg 与 i2c_transfer
I2C 子系统的数据传输基于消息模型,由i2c_msg描述单次传输,由i2c_transfer执行批量原子传输。
1. i2c_msg:单次传输消息
i2c_msg描述一次独立的 I2C 传输,可以是读操作也可以是写操作。
struct i2c_msg { __u16 addr; // 从机7位地址 __u16 flags; // 传输方向与标志 #define I2C_M_RD 0x0001 // 读操作,写操作为0 #define I2C_M_WR 0x0000 __u16 len; // 数据长度 __u8 *buf; // 数据缓冲区 };注意:I2C 是半双工通信,一个 msg 只能是读或者写,不能同时读写。这和 SPI 全双工的 transfer 有本质区别。
2. i2c_transfer:批量原子传输
i2c_transfer可以一次执行多个i2c_msg,多个消息之间只发送重复起始信号(Repeated START),不发送停止信号,整个操作是原子的。
函数原型:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);这是 I2C 读寄存器最标准的实现方式:
- 第一个 msg:写操作,发送寄存器地址
- 第二个 msg:读操作,读取寄存器数据
- 两个 msg 之间用重复起始连接,中间不释放总线
这种 “先写后读” 的组合传输,是绝大多数 I2C 外设寄存器读取的标准时序。
3. SMBus:标准化的 I2C 子集
SMBus(系统管理总线)是 I2C 的一个子集,定义了标准的命令格式和时序规范。绝大多数 I2C 外设都兼容 SMBus 协议。内核提供了一套 SMBus 便捷函数,内部封装了i2c_msg,是开发中最常用的 API。
常用的 SMBus 函数:
i2c_smbus_read_byte_data:读指定寄存器i2c_smbus_write_byte_data:写指定寄存器i2c_smbus_read_word_data:读 16 位寄存器i2c_smbus_write_word_data:写 16 位寄存器
四、I2C 设备驱动完整实战
下面以一个虚拟的 I2C 温湿度传感器为例,完整演示 I2C 设备驱动的编写流程。
1. 设备树配置
首先在设备树对应 I2C 控制器节点下添加设备节点:
&i2c1 { status = "okay"; clock-frequency = <400000>; // 400kHz快速模式 sensor@48 { compatible = "myboard,hts20"; reg = <0x48>; // 7位从机地址 interrupt-parent = <&gpio1>; interrupts = <6 IRQ_TYPE_EDGE_FALLING>; }; };注意:设备树中
reg属性写的是7 位地址,内核会自动处理读写方向位,不需要手动左移。
2. 驱动代码实现
#include <linux/module.h> #include <linux/i2c.h> #include <linux/interrupt.h> #include <linux/pm.h> /* 设备ID匹配表 - 传统平台匹配 */ static const struct i2c_device_id hts20_id[] = { {"hts20", 0}, {} }; MODULE_DEVICE_TABLE(i2c, hts20_id); /* 设备树匹配表 */ static const struct of_device_id hts20_of_match[] = { { .compatible = "myboard,hts20" }, {} }; MODULE_DEVICE_TABLE(of, hts20_of_match); /* 设备私有数据结构 */ struct hts20_priv { struct i2c_client *client; int irq; int temp; int humidity; }; /* * @brief 读取传感器寄存器 * @param priv 私有数据 * @param reg 寄存器地址 * @retval 读取到的8位值 */ static u8 hts20_read_reg(struct hts20_priv *priv, u8 reg) { return i2c_smbus_read_byte_data(priv->client, reg); } /* * @brief 写入传感器寄存器 * @param priv 私有数据 * @param reg 寄存器地址 * @param val 待写入值 */ static void hts20_write_reg(struct hts20_priv *priv, u8 reg, u8 val) { i2c_smbus_write_byte_data(priv->client, reg, val); } /* * @brief 批量读取(原始i2c_msg方式) * @param priv 私有数据 * @param reg 起始寄存器 * @param buf 接收缓冲区 * @param len 读取长度 */ static int hts20_read_buf(struct hts20_priv *priv, u8 reg, u8 *buf, int len) { struct i2c_msg msgs[2]; /* 第一个msg:写寄存器地址 */ msgs[0].addr = priv->client->addr; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = ® /* 第二个msg:读数据 */ msgs[1].addr = priv->client->addr; msgs[1].flags = I2C_M_RD; msgs[1].len = len; msgs[1].buf = buf; return i2c_transfer(priv->client->adapter, msgs, 2); } /* 中断处理函数 */ static irqreturn_t hts20_irq_handler(int irq, void *dev_id) { struct hts20_priv *priv = dev_id; /* 读取传感器数据 */ priv->temp = hts20_read_reg(priv, 0x01); priv->humidity = hts20_read_reg(priv, 0x02); return IRQ_HANDLED; } /* probe函数:设备匹配成功后执行 */ static int hts20_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct hts20_priv *priv; int ret; /* 1. 分配私有数据结构 */ priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->client = client; i2c_set_clientdata(client, priv); /* 2. 检查适配器能力 */ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { dev_err(&client->dev, "Adapter does not support SMBus\n"); return -ENODEV; } /* 3. 获取中断 */ priv->irq = client->irq; ret = devm_request_threaded_irq(&client->dev, priv->irq, NULL, hts20_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "hts20", priv); if (ret) { dev_err(&client->dev, "Failed to request irq\n"); return ret; } /* 4. 初始化传感器硬件 */ hts20_write_reg(priv, 0x00, 0x01); // 启动连续测量 dev_info(&client->dev, "HTS20 sensor probed successfully\n"); return 0; } /* remove函数:设备移除时执行 */ static void hts20_remove(struct i2c_client *client) { struct hts20_priv *priv = i2c_get_clientdata(client); /* 传感器进入休眠模式 */ hts20_write_reg(priv, 0x00, 0x00); } static struct i2c_driver hts20_driver = { .driver = { .name = "hts20", .owner = THIS_MODULE, .of_match_table = hts20_of_match, }, .probe = hts20_probe, .remove = hts20_remove, .id_table = hts20_id, }; module_i2c_driver(hts20_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Developer"); MODULE_DESCRIPTION("HTS20 I2C temperature/humidity sensor driver");3. 代码要点说明
module_i2c_driver:内核提供的便捷宏,自动完成驱动的注册与注销。i2c_check_functionality:检查适配器是否支持所需的功能,是驱动健壮性的重要保证。- SMBus 函数:单字节读写优先使用 SMBus 函数,代码简洁且兼容性好。
i2c_transfer:适合批量读写、特殊时序场景,灵活性更高。devm_*系列函数:资源托管,设备移除时自动释放,减少内存泄漏风险。
五、常用 API 速查
表格
| API 函数 | 功能 | 典型使用场景 |
|---|---|---|
i2c_smbus_read_byte_data | 读指定 8 位寄存器 | 单寄存器读取,最常用 |
i2c_smbus_write_byte_data | 写指定 8 位寄存器 | 单寄存器写入,最常用 |
i2c_smbus_read_word_data | 读 16 位寄存器 | 16 位数据传感器 |
i2c_smbus_write_word_data | 写 16 位寄存器 | 16 位配置寄存器 |
i2c_master_send | 原始 I2C 写操作 | 连续写多个字节 |
i2c_master_recv | 原始 I2C 读操作 | 连续读多个字节 |
i2c_transfer | 批量消息传输 | 先写后读、复杂时序 |
i2c_check_functionality | 检查适配器功能 | probe 中做能力校验 |
i2c_set_clientdata | 设置设备私有数据 | probe 中保存私有指针 |
i2c_get_clientdata | 获取设备私有数据 | 各处接口获取上下文 |
六、开发调试常见踩坑
1. 设备地址错误
这是最高频的问题。I2C 地址有 7 位和 8 位两种表述:
- 设备树、
i2c_client->addr中使用7 位地址 - 逻辑分析仪抓到的是8 位地址(包含读写位)
很多初学者会把手册上的 8 位地址直接写入设备树,导致设备永远匹配不上。
2. 设备树匹配失败
- 检查
compatible字符串与驱动of_match_table是否完全一致 - 确认 I2C 控制器节点
status为 "okay" - 检查地址是否与总线上其他设备冲突
3. 通信失败、ACK 错误
- 硬件上优先检查上拉电阻,I2C 总线必须有上拉电阻才能正常工作
- 确认时钟频率是否超过外设支持的最大值
- 检查引脚复用配置是否正确
4. SMBus 与原始 I2C 不兼容
少数外设不遵循 SMBus 规范,使用 SMBus 函数会通信失败,此时改用i2c_transfer构造原始 I2C 消息即可解决。
5. 并发访问问题
I2C 总线是共享资源,内核内部有总线锁保护。不要在中断上下文(尤其是原子上下文)中执行长时间的 I2C 传输,可能导致死锁或系统卡顿。
6. 读操作时序错误
读寄存器必须遵循 “写地址→重复起始→读数据” 的流程。很多初学者分开调用i2c_master_send和i2c_master_recv,中间会产生停止信号,导致外设状态重置,读不出正确数据。这种场景必须使用i2c_transfer。
结语
Linux I2C 子系统是 Linux 总线驱动的经典范本,分层架构清晰地分离了硬件控制器与外设协议。作为驱动开发者,理解i2c_adapter、i2c_client、i2c_driver三者的关系,掌握i2c_msg的传输模型,就能快速开发各类 I2C 设备驱动。