资讯动态

DAYS 57【Linux 驱动深度】I2C 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战

发布时间:2026/10/15 4:09:12 来源:尧图企业网站定制
言在嵌入式 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.ci.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_clienti2c_driver通过匹配表与i2c_client绑定绑定成功后调用 probe 函数完成设备初始化。二、核心数据结构详解1. i2c_adapteri2c_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_clienti2c_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_driveri2c_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_transferI2C 子系统的数据传输基于消息模型由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快速模式 sensor48 { 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 reg; /* 第二个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 设备驱动。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑