Linux驱动之I2C子系统设计:从架构原理到代码实现 1. 引言IICInter-Integrated Circuit又称 I2C总线是嵌入式 Linux 开发中最常用的低速串行通信总线之一广泛用于连接传感器、EEPROM、RTC、触摸屏控制器等外设。本文围绕 Linux 下 IIC 子系统展开重点讲解主从设备概念、驱动结构分类、子系统架构并给出驱动模块与用户层代码实现帮助读者从整体到细节理解 IIC 驱动的设计思路。2. I2C主从设备概念IIC 总线采用主从Master/Slave通信模型由主设备发起通信并产生时钟信号从设备响应主设备的读写请求。理解主从关系是学习 IIC 驱动的前提。主设备Master负责产生 SCL 时钟、发起起始/停止条件、发送设备地址并控制数据传输方向。在 Linux 中主设备通常是 SoC 自带的 IIC 控制器对应内核中的 I2C Adapter。从设备Slave被主设备寻址并响应读写操作每个从设备在总线上拥有唯一的 7 位或 10 位地址。常见的从设备包括各类传感器、存储器芯片等对应内核中的 I2C Client。多主模式IIC 总线允许多个主设备共存通过仲裁机制避免冲突但在大多数嵌入式场景中采用单主多从结构。在设备树中主控制器节点下会挂载若干从设备子节点每个子节点通过 reg 属性指定从设备地址驱动框架据此完成设备与驱动的匹配。3. I2C驱动结构分类Linux IIC驱动体系从功能上可分为三类分别对应内核中不同的数据结构与注册接口。3.1 I2C控制器驱动控制器驱动负责管理 SoC 上的 IIC 硬件控制器实现底层时序、中断处理和数据传输。它向内核注册一个struct i2c_adapter并提供struct i2c_algorithm中的 master_xfer 回调来完成实际的总线读写。其中struct i2c_adapter是 I2C 控制器在内核中的抽象描述主要字段如下struct i2c_adapter { struct module *owner; unsigned int class; const struct i2c_algorithm *algo; void *algo_data; struct rt_mutex bus_lock; int timeout; int retries; struct device dev; int nr; char name[48]; struct completion dev_released; struct mutex userspace_clients_lock; struct list_head userspace_clients; struct i2c_bus_recovery_info *bus_recovery_info; const struct i2c_adapter_quirks *quirks; struct irq_domain *host_notify_domain; struct regulator *bus_regulator; };关键字段说明algo指向该控制器对应的算法实现nr表示总线编号对应 /dev/i2c-N 中的 Ntimeout和retries用于控制总线传输的超时与重试策略dev是内嵌的设备结构用于与设备模型关联。而struct i2c_algorithm定义了控制器驱动必须实现的总线操作回调其核心成员如下struct i2c_algorithm { int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); 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是核心回调负责执行一次完整的总线传输参数 msgs 指向待发送的消息数组num 表示消息数量smbus_xfer用于支持 SMBus 协议的传输functionality返回该控制器支持的功能标志位供上层查询能力。3.2 I2C从设备驱动从设备驱动面向具体的 IIC 外设芯片通过struct i2c_driver注册在 probe 函数中完成设备初始化、注册字符设备或输入设备等操作。它使用struct i2c_client描述挂在总线上的具体设备实例。其中struct i2c_driver是从设备驱动的核心结构用于向核心层注册驱动并声明匹配规则主要字段如下struct i2c_driver { unsigned int class; int (*probe)(struct i2c_client *client); void (*remove)(struct i2c_client *client); void (*shutdown)(struct i2c_client *client); int (*suspend)(struct i2c_client *client, pm_message_t mesg); int (*resume)(struct i2c_client *client); const struct i2c_device_id *id_table; struct device_driver driver; const struct of_device_id *of_match_table; int (*detect)(struct i2c_client *client, struct i2c_board_info *info); const unsigned short *address_list; struct list_head clients; };关键字段说明probe在设备与驱动匹配成功后调用完成设备初始化和资源申请remove在设备移除时调用负责释放资源id_table用于传统 ID 匹配of_match_table用于设备树 compatible 匹配driver是内嵌的通用设备驱动结构承载 name 等公共属性。而struct i2c_client描述挂在 I2C 总线上的具体从设备实例其核心成员如下struct i2c_client { unsigned short flags; unsigned short addr; char name[I2C_NAME_SIZE]; struct i2c_adapter *adapter; struct device dev; int irq; struct list_head detected; int (*unlock)(struct i2c_client *client); };关键字段说明addr是该从设备在总线上的 7 位或 10 位地址adapter指向其所属的控制器适配器name是设备名称用于日志和匹配dev是内嵌的设备结构用于与设备模型关联irq表示该设备对应的中断号若未使用则为 0。3.3 I2C核心层核心层是连接控制器驱动与从设备驱动的桥梁负责设备与驱动的匹配、总线管理、地址分配以及向用户空间提供访问接口。它实现了i2c_add_adapter、i2c_add_driver等核心 API并维护 /dev/i2c-N 设备节点。下面介绍核心层中几个最常用的 API 函数及其作用。i2c_add_adapter / i2c_del_adapteri2c_add_adapter用于向核心层注册一个 I2C 控制器适配器通常在控制器驱动的 probe 函数中调用。注册成功后核心层会为其分配总线编号并创建 /dev/i2c-N 节点。对应的i2c_del_adapter用于注销适配器在控制器驱动卸载时调用负责释放总线编号并移除设备节点。int i2c_add_adapter(struct i2c_adapter *adapter); void i2c_del_adapter(struct i2c_adapter *adapter);i2c_add_driver / i2c_del_driveri2c_add_driver用于向核心层注册一个从设备驱动通常在驱动模块的入口函数中调用。核心层会根据驱动的of_match_table或id_table与总线上已有的设备进行匹配匹配成功后调用驱动的 probe 函数。对应的i2c_del_driver用于注销驱动在模块卸载时调用。int i2c_add_driver(struct i2c_driver *driver); void i2c_del_driver(struct i2c_driver *driver);i2c_transferi2c_transfer是核心层提供给从设备驱动使用的最核心的数据传输接口。它接收一个struct i2c_msg消息数组核心层会将其路由到对应的控制器驱动由控制器驱动完成实际的总线时序操作。该函数返回成功传输的消息数量负数表示出错。int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);i2c_smbus_read_byte_data / i2c_smbus_write_byte_data这两个函数是基于 SMBus 协议的便捷读写接口内部封装了i2c_transfer适合对寄存器进行单字节读写操作。对于大多数传感器、EEPROM 等从设备使用这两个函数可以简化代码。s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command); s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value);i2c_new_client_device / i2c_unregister_devicei2c_new_client_device用于在运行时动态创建一个 I2C 从设备实例通常在设备树未描述设备或需要动态添加设备时使用。对应的i2c_unregister_device用于注销并释放该设备实例。struct i2c_client *i2c_new_client_device(struct i2c_adapter *adap, struct i2c_board_info *info); void i2c_unregister_device(struct i2c_client *client);以上是核心层中最常用的几个 API。在实际驱动开发中i2c_add_driver和i2c_transfer的使用频率最高前者负责驱动注册后者负责与底层控制器交互完成数据传输。4. I2C子系统架构Linux IIC 子系统采用分层设计自下而上依次为硬件层、控制器驱动层、核心层、从设备驱动层和用户空间接口层。flowchart TD A[用户空间应用] -- B[/dev/i2c-N 或 i2c-dev 接口/] B -- C[I2C 核心层 Core] C -- D[I2C 控制器驱动 Adapter] C -- E[I2C 从设备驱动 Client] D -- F[IIC 硬件控制器] E -- G[具体外设芯片] F -- H[SCL/SDA 总线] G -- H数据流向说明用户程序通过 open/read/write/ioctl 访问 /dev/i2c-N 节点核心层将请求路由到对应的控制器驱动由控制器驱动完成总线时序操作从设备驱动则通过核心层提供的 i2c_transfer 接口与底层控制器交互实现与具体芯片的通信。5. I2C从设备驱动下面以虚拟从设备为例演示一个完整的 I2C 从设备驱动模块的编写流程包括设备树匹配、probe、读写回调以及字符设备注册。5.1 设备树节点/ { i2c0: i2c13800000 { compatible samsung,s3c2440-i2c; reg 0x13800000 0x100; #address-cells 1; #size-cells 0; myi2c50 { compatible myvendor,myi2c-dev; reg 0x50; }; }; };5.2 从设备驱动模块#include linux/module.h #include linux/i2c.h #include linux/of.h #include linux/fs.h #include linux/uaccess.h #include linux/cdev.h #include linux/device.h #define MYI2C_MAJOR 0 #define MYI2C_NAME myi2c static struct i2c_client *g_client; static dev_t myi2c_devno; static struct cdev myi2c_cdev; static struct class *myi2c_class; static int myi2c_read_reg(struct i2c_client *client, u8 reg, u8 *val) { struct i2c_msg msg[2]; int ret; msg[0].addr client-addr; msg[0].flags 0; msg[0].len 1; msg[0].buf reg; msg[1].addr client-addr; msg[1].flags I2C_M_RD; msg[1].len 1; msg[1].buf val; ret i2c_transfer(client-adapter, msg, 2); if (ret 0) return ret; return 0; } static int myi2c_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; struct i2c_msg msg; msg.addr client-addr; msg.flags 0; msg.len 2; msg.buf buf; return i2c_transfer(client-adapter, msg, 1); } static ssize_t myi2c_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { u8 reg 0; u8 val 0; int ret; if (count 1) return -EINVAL; ret myi2c_read_reg(g_client, reg, val); if (ret 0) return ret; if (copy_to_user(buf, val, 1)) return -EFAULT; return 1; } static ssize_t myi2c_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { u8 data[2]; int ret; if (count 2) return -EINVAL; if (copy_from_user(data, buf, 2)) return -EFAULT; ret myi2c_write_reg(g_client, data[0], data[1]); if (ret 0) return ret; return count; } static const struct file_operations myi2c_fops { .owner THIS_MODULE, .read myi2c_read, .write myi2c_write, }; static int myi2c_probe(struct i2c_client *client) { int ret; g_client client; ret alloc_chrdev_region(myi2c_devno, 0, 1, MYI2C_NAME); if (ret 0) return ret; cdev_init(myi2c_cdev, myi2c_fops); ret cdev_add(myi2c_cdev, myi2c_devno, 1); if (ret 0) goto err_cdev; myi2c_class class_create(THIS_MODULE, MYI2C_NAME); if (IS_ERR(myi2c_class)) { ret PTR_ERR(myi2c_class); goto err_class; } device_create(myi2c_class, NULL, myi2c_devno, NULL, MYI2C_NAME); return 0; err_class: cdev_del(myi2c_cdev); err_cdev: unregister_chrdev_region(myi2c_devno, 1); return ret; } static void myi2c_remove(struct i2c_client *client) { device_destroy(myi2c_class, myi2c_devno); class_destroy(myi2c_class); cdev_del(myi2c_cdev); unregister_chrdev_region(myi2c_devno, 1); } static const struct of_device_id myi2c_of_match[] { { .compatible myvendor,myi2c-dev }, { } }; MODULE_DEVICE_TABLE(of, myi2c_of_match); static struct i2c_driver myi2c_driver { .probe myi2c_probe, .remove myi2c_remove, .driver { .name MYI2C_NAME, .of_match_table myi2c_of_match, }, }; module_i2c_driver(myi2c_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple I2C client driver example);代码说明probe 函数在设备树匹配成功后执行注册字符设备并创建设备节点read/write 回调通过 i2c_transfer 与底层控制器交互完成寄存器读写remove 函数负责释放资源。6. I2C用户层代码用户空间访问 I2C 设备有两种常见方式一是通过 /dev/i2c-N 节点配合 ioctl 直接操作二是通过驱动注册的字符设备节点访问。下面分别给出示例。6.1 /dev/i2c-N访问#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/i2c-dev.h #include linux/i2c.h #define I2C_DEV /dev/i2c-0 #define SLAVE_ADDR 0x50 int main(void) { int fd; int ret; unsigned char reg 0x00; unsigned char val 0x00; struct i2c_rdwr_ioctl_data packets; struct i2c_msg msgs[2]; fd open(I2C_DEV, O_RDWR); if (fd 0) { perror(open); return -1; } if (ioctl(fd, I2C_SLAVE, SLAVE_ADDR) 0) { perror(ioctl I2C_SLAVE); close(fd); return -1; } /* 写寄存器 */ msgs[0].addr SLAVE_ADDR; msgs[0].flags 0; msgs[0].len 2; msgs[0].buf (unsigned char[]){ reg, 0xAB }; packets.msgs msgs; packets.nmsgs 1; ret ioctl(fd, I2C_RDWR, packets); if (ret 0) { perror(ioctl I2C_RDWR write); close(fd); return -1; } /* 读寄存器 */ msgs[0].addr SLAVE_ADDR; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; msgs[1].addr SLAVE_ADDR; msgs[1].flags I2C_M_RD; msgs[1].len 1; msgs[1].buf val; packets.msgs msgs; packets.nmsgs 2; ret ioctl(fd, I2C_RDWR, packets); if (ret 0) { perror(ioctl I2C_RDWR read); close(fd); return -1; } printf(read reg 0x%02x 0x%02x\n, reg, val); close(fd); return 0; }6.2 驱动字符设备访问#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #define MYI2C_DEV /dev/myi2c int main(void) { int fd; unsigned char buf[2]; unsigned char val 0; fd open(MYI2C_DEV, O_RDWR); if (fd 0) { perror(open); return -1; } /* 写寄存器 0x00 0xAB */ buf[0] 0x00; buf[1] 0xAB; if (write(fd, buf, 2) ! 2) { perror(write); close(fd); return -1; } /* 读寄存器 0x00 */ if (read(fd, val, 1) ! 1) { perror(read); close(fd); return -1; } printf(reg 0x00 0x%02x\n, val); close(fd); return 0; }7. 总结Linux IIC 子系统通过分层架构将控制器驱动、核心层与从设备驱动解耦开发者只需关注自己负责的那一层。理解主从设备概念、掌握三类驱动结构以及熟悉 i2c_transfer 的用法是编写可靠 IIC 驱动的关键。实际项目中建议优先使用设备树描述硬件连接并在用户层通过 ioctl 或字符设备接口进行验证以降低调试成本。