1. 引言
I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司(现 NXP)开发的两线式串行通信总线,广泛应用于嵌入式系统中连接低速外设,如传感器、EEPROM、RTC 时钟芯片、触摸控制器等。在 Linux 内核中,I2C 子系统提供了完整的驱动框架,屏蔽了底层硬件差异,让开发者可以专注于设备驱动的业务逻辑。本文记录笔者学习 Linux I2C 驱动开发的过程,从硬件协议基础到内核框架,再到实际驱动编写,逐步梳理关键知识点。
2. I2C 协议基础
在深入 Linux 驱动之前,先回顾 I2C 总线的硬件协议要点,这对理解内核框架的设计至关重要。
2.1 物理层特性
- 两线制:SCL(串行时钟线)和 SDA(串行数据线),均为开漏输出,需外接上拉电阻。
- 多主多从:总线上可挂载多个主机和从机,通过地址区分设备。
- 7 位或 10 位地址:常用 7 位地址,一个总线上最多挂载 112 个设备(保留地址除外)。
- 传输速率:标准模式 100 kbit/s,快速模式 400 kbit/s,高速模式 3.4 Mbit/s。
2.2 数据传输格式
一次完整的 I2C 传输包含起始条件(START)、地址帧、数据帧和停止条件(STOP)。读写操作的基本格式如下:
写操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP 读操作:START + 从机地址(写) + ACK + 寄存器地址 + ACK + START + 从机地址(读) + ACK + 数据 + NACK + STOP理解这个时序是编写驱动的基础,因为 Linux 内核的 I2C 传输接口本质上就是对这些时序的封装。
3. Linux I2C 子系统架构
Linux 内核将 I2C 子系统划分为三个层次:控制器驱动(Adapter)、核心层(Core)和设备驱动(Client Driver)。理解这三层的关系是学习 I2C 驱动的关键。
3.1 三层结构总览
- I2C Adapter(控制器驱动):直接操作硬件寄存器,负责在总线上产生时序,对应 SoC 上的 I2C 控制器外设。
- I2C Core(核心层):提供统一的设备模型、总线注册机制、数据传输接口,以及设备与驱动的匹配逻辑,对上层屏蔽控制器差异。
- I2C Client Driver(设备驱动):针对具体挂在总线上的设备(如某型号传感器)编写,通过核心层提供的 API 与设备通信,不直接操作硬件寄存器。
3.2 关键数据结构
I2C 子系统的核心数据结构主要有以下几个:
| 数据结构 | 作用 |
|---|---|
struct i2c_adapter | 描述一个 I2C 控制器,包含总线号、算法、锁等。 |
struct i2c_algorithm | 定义控制器收发数据的主函数(master_xfer)。 |
struct i2c_client | 描述一个挂在总线上的设备,包含地址、驱动名等。 |
struct i2c_driver | 描述一个设备驱动,包含 probe、remove 回调及 id 表。 |
struct i2c_msg | 描述一次传输的消息,包含地址、标志、缓冲区等。 |
4. 设备驱动开发流程
编写一个 I2C 设备驱动通常遵循固定的步骤。下面以读取一个虚拟传感器为例,梳理完整流程。
4.1 驱动框架搭建
首先定义一个i2c_driver结构体,并实现 probe 和 remove 回调:
#include <linux/i2c.h> #include <linux/module.h> #include <linux/init.h> static int my_sensor_probe(struct i2c_client *client) { pr_info("my_sensor: probe success, addr=0x%02x\n", client->addr); return 0; } static void my_sensor_remove(struct i2c_client *client) { pr_info("my_sensor: remove\n"); } static const struct i2c_device_id my_sensor_id[] = { { "my-sensor", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, my_sensor_id); static struct i2c_driver my_sensor_driver = { .driver = { .name = "my-sensor", }, .probe = my_sensor_probe, .remove = my_sensor_remove, .id_table = my_sensor_id, }; module_i2c_driver(my_sensor_driver); MODULE_LICENSE("GPL");4.2 数据传输接口
在 probe 中获取设备后,就可以通过i2c_transfer或更便捷的i2c_smbus_read_byte_data等接口读写数据。以读取传感器寄存器为例:
static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = ®, }, { .addr = client->addr, .flags = I2C_M_RD, .len = 1, .buf = val, }, }; ret = i2c_transfer(client->adapter, msgs, 2); if (ret != 2) { dev_err(&client->dev, "read reg 0x%02x failed, ret=%d\n", reg, ret); return ret < 0 ? ret : -EIO; } return 0; }4.3 设备树匹配
在现代内核中,通常通过设备树描述硬件。在设备树中声明设备节点后,驱动通过of_match_table完成匹配:
static const struct of_device_id my_sensor_of_match[] = { { .compatible = "vendor,my-sensor" }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match); static struct i2c_driver my_sensor_driver = { .driver = { .name = "my-sensor", .of_match_table = my_sensor_of_match, }, .probe = my_sensor_probe, .remove = my_sensor_remove, };对应的设备树节点示例:
&i2c1 { my_sensor@48 { compatible = "vendor,my-sensor"; reg = <0x48>; }; };5. SMBus 接口与辅助函数
对于大多数简单设备,不必每次都构造i2c_msg数组。内核提供了 SMBus 风格的封装函数,使用更加简洁。
5.1 常用 SMBus API
| 函数 | 功能 |
|---|---|
i2c_smbus_read_byte() | 读取一个字节,不带寄存器地址。 |
i2c_smbus_write_byte() | 写入一个字节,不带寄存器地址。 |
i2c_smbus_read_byte_data() | 读取指定寄存器的值。 |
i2c_smbus_write_byte_data() | 向指定寄存器写入值。 |
i2c_smbus_read_word_data() | 读取 16 位寄存器值。 |
i2c_smbus_write_word_data() | 写入 16 位寄存器值。 |
5.2 使用示例
使用 SMBus 接口后,前面的读寄存器函数可以简化为:
static int my_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; ret = i2c_smbus_read_byte_data(client, reg); if (ret < 0) { dev_err(&client->dev, "read reg 0x%02x failed, ret=%d\n", reg, ret); return ret; } *val = (u8)ret; return 0; }6. 用户空间访问
除了编写内核驱动,Linux 还提供了用户空间访问 I2C 设备的接口,方便调试和快速验证。通过/dev/i2c-N设备节点,配合i2c-dev内核模块,可以在应用层直接读写设备。
6.1 使用 i2c-tools
在开发板上安装i2c-tools后,可以快速扫描总线和读写设备:
# 扫描总线上的设备 i2cdetect -y 1 读取 0x48 设备 0x00 寄存器的值 i2cget -y 1 0x48 0x00 向 0x48 设备 0x01 寄存器写入 0xff i2cset -y 1 0x48 0x01 0xff6.2 应用层编程
在 C 程序中,通过ioctl系统调用与I2C_RDWR命令完成读写:
#include <stdio.h> #include <fcntl.h> #include <linux/i2c-dev.h> #include <sys/ioctl.h> int main(void) { int fd = open("/dev/i2c-1", O_RDWR); if (fd < 0) { perror("open"); return -1; } struct i2c_msg msgs[2]; unsigned char reg = 0x00; unsigned char val = 0; msgs[0].addr = 0x48; msgs[0].flags = 0; msgs[0].len = 1; msgs[0].buf = &reg; msgs[1].addr = 0x48; msgs[1].flags = I2C_M_RD; msgs[1].len = 1; msgs[1].buf = &val; struct i2c_rdwr_ioctl_data data = { .msgs = msgs, .nmsgs = 2, }; if (ioctl(fd, I2C_RDWR, &data) < 0) { perror("ioctl"); close(fd); return -1; } printf("reg 0x00 = 0x%02x\n", val); close(fd); return 0; }7. 调试与常见问题
I2C 驱动开发中遇到问题,可以从以下几个方向排查。
7.1 常用调试手段
- 开启内核调试日志:在
drivers/i2c目录下开启CONFIG_I2C_DEBUG_CORE和CONFIG_I2C_DEBUG_ALGO,观察传输细节。 - 逻辑分析仪:抓取 SCL/SDA 波形,确认时序是否符合协议。
- i2cdetect 扫描:确认设备地址是否正确,设备是否真的挂在总线上。
- 检查上拉电阻:SDA/SCL 必须有上拉电阻,否则通信不稳定。
7.2 常见错误码
| 错误码 | 含义 |
|---|---|
-ENXIO | 无此设备,通常是没有 ACK 应答,地址错误或设备未上电。 |
-ETIMEDOUT | 传输超时,总线可能被拉死或时钟异常。 |
-EBUSY | 总线忙,可能被其他主机占用。 |
-EINVAL | 参数无效,检查消息长度和标志位。 |
8. 总结
Linux I2C 驱动开发的核心在于理解三层架构:控制器驱动负责时序,核心层负责统一抽象,设备驱动负责业务逻辑。实际开发中,大部分工作集中在编写i2c_driver和利用 SMBus 辅助函数读写寄存器。建议初学者先在用户空间用 i2c-tools 验证硬件通路,再编写内核驱动,这样能有效缩小问题排查范围。后续可以进一步学习 I2C 的 10 位地址、多主机仲裁、DMA 传输等进阶内容。