Linux内核I2C驱动核心结构体与数据通路详解

发布时间:2026/10/19 1:19:18
Linux内核I2C驱动核心结构体与数据通路详解
搞 Linux 内核驱动的人十有八九都给 I2C 外设写过驱动。这总线串两个电阻两根线看 data sheet 上时序图好像也不复杂可真在内核里走一遍数据通路很多人才发现 I2C 并不是一个简单的read()/write()函数就能带过的东西。如果你打算在 Linux 下深入搞 I2C 内核驱动不管是温度传感器、触摸屏控制器还是电源管理芯片第一件事不是急着复制示例代码而是把i2c_adapter、i2c_algorithm、i2c_client、i2c_driver这几个核心角色之间的关系理清。这篇文章算是我多年调 I2C 的经验沉淀适合想读懂 Linux I2C 内核驱动源码的开发者也适合准备自己写 adapter 驱动、却总被i2c-core绕晕的人。读完你至少会明白一次 I2C 读写在 Linux 里究竟经历了什么出问题该优先去哪个模块找原因。1. 这层框架到底在解决什么问题1.1 低层硬件到处都是但没人希望上层面对裸寄存器I2C 总线在物理上只有两条线SCL 和 SDA。嵌入式主控里通常不止一个 I2C 控制器每个控制器可能挂着温度传感器、EEPROM、PMIC、触摸屏等一堆设备。如果每写一个外设驱动就要求它直接操作控制器寄存器那必然是一场灾难每个 SoC 控制器寄存器布局不同每个外设的驱动都得为不同平台维护一套底层收发代码代码爆炸维护成本飙升。内核在这里选择的做法和 PCI、USB 一样加一层“中介抽象”也就是 I2C 核心模块。控制器驱动负责把自家控制器包装成统一的i2c_adapter实现标准的收发回调设备驱动只负责描述“我是什么设备、我要通过标准 API 和数据交换”。这样一来同一个外设驱动在甲板子上用寄存器映射的 I2C 控制器到乙板子上用 GPIO 模拟的 I2C 控制器驱动代码一行都不用改。这是整个框架存在的最大价值隔离变化统一语义。1.2 内核里哪些代码是干什么的看 Linux 源码I2C 相关代码主要集中在drivers/i2c/目录和include/linux/i2c.h。搞懂这几块比直接一头扎进某一家的 controller 驱动更有用。i2c-core-base.cI2C 核心框架负责 adapter/client/driver 的注册、匹配、注销和总线的加锁调度。i2c-core-smbus.cSMBus 协议的封装很多 I2C 设备走的是 SMBus 风格的读寄存器命令。i2c-dev.c把 adapter 暴露成/dev/i2c-N字符设备的桥方便用户空间用 i2c-tools 直接调试。algos/i2c-algo-bit.c纯软件 GPIO 模拟 I2C 时序的算法层如果你的主控没有硬件控制器就能挂这套 bit-bang 算法。各平台有些i2c-xxx.c文件这些是硬件控制器驱动负责把具体的寄存器操作封装成标准回调。我建议新手先看i2c-core-base.c里的i2c_transfer()和i2c_add_adapter()把一条主流程跑通之后再看具体控制器驱动会快很多。核心框架不理解看到 SoC 厂商的 BSP 驱动就会觉得满屏都是寄存器读写完全抓不住重点。1.3 一次 I2C 数据请求的完整旅行从设备驱动发起一次读操作到总线上串行出现波形完整路径大体是这样的设备驱动调用i2c_transfer()传入 adapter 指针、struct i2c_msg数组和消息数量。i2c-core获取总线锁防止多个设备驱动同时使用同一条 I2C 总线造成事务交叉。核心层把请求路由到 adapter 的i2c_algorithm-master_xfer()回调。控制器驱动或 bit-bang 算法根据 msg 数组中的地址、标志位、长度构造 start 条件、地址帧、数据帧、stop 条件。硬件把电平发送到 SCL/SDA 总线上控制器驱动等待传输完成或者通过中断/轮询读取结果。返回已经成功传输的 msg 数量或者负数错误码i2c-core释放总线锁错误向上层传播。很多人误以为i2c_transfer()是“直接操作硬件”的函数实际上它更像一个交通枢纽。真正蹲在底层搬电平的是master_xfer回调。理解了这条路径后面排查问题就知道该去哪层加日志。2. 绕不开的核心结构体每个字段都不是白给的2.1 i2c_adapter控制器的“接口卡”struct i2c_adapter在 Linux 里代表一个可以挂设备的 I2C 控制器实例。别把它想得太玄可以类比成电脑上的 PCI 控制器总线控制器本身是硬件内核用一个对象描述它并给上层提供统一操作入口。adapter 里几个关键字段owner通常是THIS_MODULE防止使用中模块被卸载。algo指向struct i2c_algorithm这是 adapter 的功能核心后面专门讲。algo_data算法层私有数据bit-bang 方式经常用它存放 GPIO 句柄和延时参数。class该 adapter 能支持哪类设备。这是老式非设备树枚举方式下用来“设备发现”的过滤条件比如I2C_CLASS_HWMON表示可以挂硬件监控类设备。dev.parent把 adapter 挂到对应的平台设备节点上sysfs 里就能看到从属关系。nr总线编号。-1表示让内核自动分配也可以指定一个编号。name总线的名字在/sys/bus/i2c/devices/下会显示成i2c-N的别名。注册 adapter 过程也不复杂分配结构体填好algo、owner、dev.parent然后调用i2c_add_adapter()。成功后内核会自动创建一条虚拟总线并在 sysfs 中创建目录。你在用户空间看到的i2c-0、i2c-1就是这么来的。2.2 i2c_algorithm控制器驱动要交作业的地方adapter 只是个壳真正决定数据怎么收发的是struct i2c_algorithm。里面最核心的是static const struct i2c_algorithm xxx_i2c_algo { .master_xfer xxx_i2c_xfer, .smbus_xfer xxx_i2c_smbus_xfer, .functionality xxx_i2c_functionality, };master_xfer是 I2C 数据通路的真正主角。它的签名大概是int xxx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);msgs数组可以包含一条或多条消息内核用一次调用表达一个完整的复合事务。例如“先写寄存器地址再读数据”这种操作通常就是两个i2c_msg第一个 msg 方向是写第二个 msg 方向是读。控制器驱动要把两条消息串成一个不被打断的 I2C 事务这就是“repeated START”的典型应用场景。functionality回调也很重要。它返回 adapter 支持的能力位比如I2C_FUNC_I2C、I2C_FUNC_SMBUS_READ_BYTE_DATA。上层和用户空间的 i2c-tools 都会通过它判断某些操作能否在这个总线上执行。如果你没实现 smbus_xfer又返回了 SMBUS 特性上层就会用通用的 SMBus 模拟流程最后也还是落到master_xfer。2.3 i2c_client 和 i2c_driver设备与驱动的“劳动合同”struct i2c_client代表“挂在某条 I2C 总线上的一个从设备”它记录了设备地址、名字、挂在哪个 adapter 上。而struct i2c_driver代表“能够驱动这类从设备的驱动代码”。二者之间的关系就是内核设备和驱动模型里的“匹配-绑定”。一般情况下i2c_client不是设备驱动自己创建的。如果你用设备树描述硬件内核会扫描 adapter 节点下的子节点读到 compatible 和 reg 后自动创建对应的i2c_client。然后设备驱动模型会拿着这个 client 的 compatible 去匹配所有已注册的i2c_driver匹配成功就调用驱动里的probe()函数。很多新手写了个i2c_driver插上驱动却发现probe()不执行绝大多数情况是 client 根本没有被创建或者 compatible 的属性值两边没对上。可以先查/sys/bus/i2c/devices/下有没有生成N-0048这样的目录有目录才说明这层“劳动合同”签了一半。2.4 i2c_msg 和 i2c_transfer真正搬数据的人struct i2c_msg是数据包的最小单位。常见字段包括addr目标设备地址通常是 7 位地址。写读操作时内核和底层会自己处理读写位和左移一位驱动里拿到的是 7 位地址。flags方向和控制标志最重要的是I2C_M_RD置位表示读否则表示写。len缓冲区长度。buf数据缓冲区。设备驱动发起一次完整事务通常这样构造struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg_addr, }, { .addr client-addr, .flags I2C_M_RD, .len 2, .buf data, }, }; ret i2c_transfer(client-adapter, msgs, 2);这里第一条 msg 写寄存器地址第二条 msg 读两个字节的数据。读者要区分清楚i2c_transfer负责安排总线锁和调度i2c_msg负责描述数据语义master_xfer里的硬件循环才真正按照这些描述把电平翻转出来。3. 从零写一个 I2C 控制器驱动关键不是抄结构体3.1 控制器驱动到底要干哪几件事写控制器驱动和写设备驱动的思路完全不同。控制器驱动面向的是 SoC 上的 I2C 硬件模块不是某个传感器。它要做的事情可以总结为四件初始化控制器时钟、引脚、中断。实现master_xfer把i2c_msg翻译成硬件时序。注册一个i2c_adapter让这个控制器被 I2C 核心感知。实现错误处理和超时机制。很多人一上来就纠结寄存器细节忽略了整体骨架。其实只要先把 adapter 注册起来再写一个最简单的master_xfer哪怕只支持“单字节读、单字节写”这套驱动已经可以配合 i2c-tools 做大量验证了。寄存器细节可以逐步填骨架错了改起来才痛苦。3.2 一个最小 adapter 初始化流程控制器驱动通常是一个平台驱动在probe()里注册 adapter。核心流程可以精简成下面这个模样static int xxx_i2c_probe(struct platform_device *pdev) { struct xxx_i2c_dev *i2c_dev; struct i2c_adapter *adap; struct resource *res; void __iomem *base; int irq, ret; i2c_dev devm_kzalloc(pdev-dev, sizeof(*i2c_dev), GFP_KERNEL); if (!i2c_dev) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); i2c_dev-base base; adap i2c_dev-adapter; i2c_set_adapdata(adap, i2c_dev); adap-owner THIS_MODULE; adap-class I2C_CLASS_HWMON; adap-algo xxx_i2c_algo; adap-dev.parent pdev-dev; adap-nr -1; strscpy(adap-name, xxx-i2c, sizeof(adap-name)); ret i2c_add_adapter(adap); if (ret) return ret; platform_set_drvdata(pdev, i2c_dev); return 0; }这里有几个看起来不起眼、但非常影响使用的细节i2c_set_adapdata()把私有结构体挂到 adapter 上master_xfer里通过i2c_get_adapdata()取回硬件寄存器基地址和状态锁。adap-nr -1表示让内核自动分配总线号避免你在多个控制器时手动分配造成冲突。i2c_add_adapter()一旦成功sysfs 里马上能看到这条总线设备树里引用的i2c-0、i2c-1编号也可能跟着变。3.3 master_xfer 怎么写才不容易翻车master_xfer的内部实现因硬件而异但思路是差不多的遍历每个i2c_msg根据flags I2C_M_RD决定发起读还是写然后把块数据拆成一个个字节写进控制器数据寄存器。核心框架负责总线互斥驱动内部仍要加一把私有的锁防止并发访问同一控制器时寄存器状态错乱。一个最小化的框架可以长这样static int xxx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct xxx_i2c_dev *i2c_dev i2c_get_adapdata(adap); int i, ret; mutex_lock(i2c_dev-bus_lock); for (i 0; i num; i) { if (msgs[i].flags I2C_M_RD) ret xxx_i2c_read_bytes(adap, msgs[i]); else ret xxx_i2c_write_bytes(adap, msgs[i]); if (ret 0) { mutex_unlock(i2c_dev-bus_lock); return ret; } } mutex_unlock(i2c_dev-bus_lock); return num; }最容易犯的错是在master_xfer里忘记区分“这次传输是否是多 msg 复合事务”。有的控制器对重复 START 需要特殊寄存器配置如果你只在每个 msg 之间做了 STOP 再 START严格来说已经是两个独立事务和上层要求的“先写地址再读数据”语义不一致。某些传感器能容忍这种宽松时序但很多芯片会出错。写之前一定要看控制器手册有没有“连续传输”或者“重复起始位”相关的位需要设置。另外master_xfer返回的必须是“成功处理的 msg 数量”不是字节数。例如两个 msg 都成功就应该返回2。返回num这个做法是最稳妥的。3.4 时序、时钟拉伸和硬件能力匹配I2C 速率常用 100kbps 标准模式和 400kbps 快速模式有些还支持 1Mbps。控制器驱动里的时钟分频配置要跟设备树里的clock-frequency一致。如果在快速模式总线上挂了只支持标准模式的慢速设备通信会随机失败。这种情况下两个选择要么把整条总线的频率降下来要么给慢速设备单独加适配逻辑后者成本很高绝大多数项目都会选择降频。时钟拉伸也是老生常谈的坑。从设备为了等待数据准备好会主动拉低 SCL这叫做 clock stretching。如果控制器不支持自动等待或者中断处理不及时就会出现偶尔超时、偶发 ACK 错误。遇到这种问题不要一上来怀疑驱动逻辑先把总线频率降到 100kbps 试试往往能快速区分是时序裕量问题还是协议 bug。我在某 ARM 平台上就遇到过类似情况速率设 400k 时偶尔 -EIO改成 100k 之后几天都没有复现最后定位到是传感器对停止条件后的总线释放时间要求特别严格。4. 写 I2C 设备驱动你只需要关心怎么描述设备和怎么收发数据4.1 设备驱动的基本框架I2C 设备驱动写起来比控制器驱动简单得多因为不需要面对寄存器时序只需要注册一个i2c_driver实现 probe 和读写函数。一个典型驱动框架如下static const struct i2c_device_id xxx_ids[] { { xxx-chip, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_ids); static const struct of_device_id xxx_of_match[] { { .compatible vendor,xxx-chip }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match); static struct i2c_driver xxx_driver { .probe xxx_probe, .remove xxx_remove, .id_table xxx_ids, .driver { .name xxx-chip, .of_match_table xxx_of_match, }, }; module_i2c_driver(xxx_driver);module_i2c_driver()是个封装宏展开后就是常规的 module_init/module_exit 加i2c_add_driver。在probe()里你会拿到struct i2c_client *client。这个结构体里有adapter、addr、name基本就是你的操作入口。一般会在probe()里用devm_kzalloc分配私有数据然后i2c_set_clientdata(client, chip)存起来后面读函数里再用i2c_get_clientdata()取出来。4.2 设备树里 compatible 和 reg 是如何影响匹配的设备树里描述一个 I2C 从设备通常是挂在某个 adapter 节点下i2c2 { status okay; clock-frequency 400000; xxx-chip48 { compatible vendor,xxx-chip; reg 0x48; interrupt-parent gpio1; interrupts 5 IRQ_TYPE_LEVEL_LOW; }; };compatible的值必须和驱动里of_match_table中的字符串完全一致一个逗号、一个字母都不能差。内核会把设备树节点解析成一个i2c_client然后用这个 compatible 去匹配i2c_driver。如果不一致probe 不会被调用。reg写的是 7 位地址。这里有个非常经典的坑很多传感器的 datasheet 上把地址写成一个字节例如“设备地址 0x50”实际上那是包含读写位在内的 8 位地址换算成 7 位地址就是0x50 1 0x28。如果你按 8 位地址写进设备树驱动会去访问错误地址通信表现为 ACK 异常或者设备找不到。我一开始也中过招之后每次看到寄存器地址都习惯先问一句这是 7 位还是 8 位4.3 常用 API 的选择策略设备驱动层常见的 API 有这几种i2c_master_send(client, buf, len)和i2c_master_recv(client, buf, len)适合单条写、单条读不需要自己填i2c_msg。i2c_transfer(client-adapter, msgs, num)适合复合事务比如“先写寄存器地址再读 N 字节”是读写传感器寄存器最常用的方式。i2c_smbus_read_byte_data(client, command)、i2c_smbus_write_byte_data()这类适合 SMBus 风格设备内部会帮你打包成标准 SMBus 时序。读一个传感器寄存器用i2c_transfer的典型写法就是前面展示过的两段 msg。用i2c_master_send先写寄存器号再用i2c_master_recv读数据虽然也是可行的但这两步是独立的 I2C 事务中间可能被其他设备抢占总线不够严谨。对于多字节寄存器或者对时序一致性要求高的器件建议优先用i2c_transfer一次搞定。4.4 先别急着写内核驱动i2c-dev 帮你验证链路很多开发者一上来就写设备驱动结果发现通信失败分不清到底是硬件问题、地址问题还是控制器问题。更快的路径是先开启内核的CONFIG_I2C_CHARDEV用 i2c-tools 直接操作/dev/i2c-N验证链路i2cdetect -l i2cdetect -y 0 i2cget -y 0 0x48 0x00 i2cset -y 0 0x48 0x01 0x02i2cdetect -l列出系统里所有 I2C adapteri2cdetect -y 0扫描总线 0 上的设备地址。如果能看到对应地址有响应说明控制器、上拉电阻、设备都被打通了。这个时候再写内核驱动心里就有底了。如果 i2c-tools 都通不过先不要怀疑设备驱动回头查硬件电路和设备树更实际。用 i2c-dev 验证只能证明链路通了不能替代真正的设备驱动。尤其是需要快速连续读写、中断触发、电源管理等场景最终还是要靠内核驱动来保证事务的一致性和系统行为的可控。5. 调试方法论先分清三层问题再决定动哪里5.1 把问题拆成硬件、设备驱动、控制器驱动三层I2C 出问题时最怕“头痛医头脚痛医脚”。我习惯把问题分成三层硬件层上拉电阻没接、接线错误、电平不匹配、地址线冲突。设备驱动层compatible 不匹配、probe 没执行、API 用错、数据格式不对。控制器驱动/算法层时序不对、中断和状态机没处理好、没有正确区分读写标志。大多数时候i2c-tools 可以直接帮你定位到第一层还是第三层。硬件层和控制器驱动层的问题在i2cdetect阶段就会暴露设备驱动层的问题通常是 i2c-tools 能访问但自己的驱动一跑就报错。5.2 内核日志和动态调试的正确打开方式I2C 核心和一部分控制器驱动里有调试日志。内核开启以下配置后在 dmesg 里能捞到不少信息CONFIG_I2C_DEBUG_COREI2C 核心消息。CONFIG_I2C_DEBUG_ALGO算法层消息能看到每次 master_xfer 的调用。CONFIG_I2C_DEBUG_BUS总线驱动日志也就是控制器驱动的日志。如果你不想重新编内核也可以用 dynamic debug 对指定文件开日志echo file i2c-core-base.c p /sys/kernel/debug/dynamic_debug/control echo file drivers/i2c/busses/i2c-xxx.c p /sys/kernel/debug/dynamic_debug/control前提是驱动代码里用了dev_dbg()之类的动态调试接口并且内核开启了CONFIG_DYNAMIC_DEBUG。这种方式比重新编译内核快很多尤其是现场排查问题时不必等一个小时的编译。5.3 总线挂死、ACK 错误、probe 不回调排查套路我整理了一个问题速查表基本覆盖了我工作中遇到的大多数情况现象主要原因第一步排查i2cdetect所有地址显示XXSDA 被拉低、总线挂死、上拉电阻缺失示波器量 SCL/SDA 静态电平确认总线有高电平空闲状态i2cdetect找不到目标地址地址写错、设备供电异常、从设备地址线配置不对查 device tree 的 reg以及 datasheet 的地址引脚电平指定地址响应但i2cget返回 -EIO设备 NACK、控制器状态机错误、时钟拉伸降低总线频率看示波器波形判断是 ACK 位异常还是数据时序错误内核驱动 probe 不执行compatible 不匹配、设备树节点没挂到 adapter 下检查/sys/bus/i2c/devices/有没有对应 client比对 compatible偶发超时降频后消失时序裕量不足、从设备 clock stretching 支持不好用慢速模式验证必要时换硬件或调整控制器时序参数复合事务中间被插入其他设备访问没用i2c_transfer一次传多个 msg拆成多次独立调用改成单次i2c_transfer不要拆成 send/recv这张表不能直接告诉你根因但能缩小排查范围。硬件层问题多数是静态电平不对软件层问题多数是地址和匹配不对剩下那种“时好时坏”的问题优先怀疑时序和锁。5.4 我踩过的几个坑希望你别再踩一遍有一次调试一个 EEPROM设备树 reg 写的是 datasheet 上 8 位地址0xA0也就是 7 位地址0x50左移后的结果。结果i2cdetect怎么也扫不到折腾了整整一个下午最后才发现地址位写多了一位。后来我给自己定了个规矩凡是 reg 地址一律先十进制转二进制看一眼如果最低位经常为 0几乎可以断定你写的是 8 位地址。还有一次是 bit-bang 控制器驱动里master_xfer每处理一个字节都udelay(5)在超高频 CPU 上看起来没问题但系统开启实时任务后调度延迟变大偶尔就丢 ACK。后来我改成根据 SCL 高电平和低电平时间分别控制延时问题就消失了。其实很多 I2C 问题本质上是时序裕量不够能硬件测波形就测波形不要全靠猜。另一个提醒不要在master_xfer里做过重的休眠或长时间忙等。I2C 事务可能被内核用在上电时序、关机流程里某些场景下调用上下文不允许长时间阻塞。如果你的控制器没有中断机制最少也要保证每次轮询超时可配并且循环里调用 cpu_relax()避免独占总线。最后说一个我自己的习惯新板子调 I2C我先不写任何内核设备驱动只用 i2c-tools 把每一个挂在总线上的外设都扫一遍、读写一下关键寄存器再把设备树 client 是否生成确认一遍最后才让设备驱动接管。这样每一层都是独立验证的出了问题不会三方面互相猜疑。相信我这个流程能帮你省下大量本来要用来抓头发的调试时间。