最近在帮人调一块传感器的Linux驱动,绕来绕去发现问题的根子还是在I2C子系统上。很多人写I2C设备驱动,能写probe、能调i2c_transfer,但一遇到设备树匹配失败、寄存器读出来全是0xFF、或者明明枚举到了设备却进不了probe,就开始毫无头绪地乱试。原因很简单:对I2C内核驱动的整个框架理解停留在"会调API"的层面,没有建立起"数据怎么从应用层到硬件引脚"的完整链路。
这篇东西就基于我对Linux I2C内核驱动的理解和实际调板经验,把I2C子系统从分层架构、核心数据结构、设备驱动编写到适配器(adapter)实现,再到调试技巧和常见坑位,完整地捋一遍。适合刚接触内核驱动开发、写过几个platform驱动但没深入过I2C的工程师,也适合那些I2C通信老是出问题、想看透内核到底干了什么的同学。
1. I2C子系统为什么是"三层结构":从一次读写看完整调用链
I2C总线在硬件上就两根线(SCL时钟线、SDA数据线),协议上也是主从式通信,看起来很简单。但到了Linux内核里,I2C子系统的代码抽象却是出了名的绕。我第一次接触的时候也懵:为什么一个i2c_smbus_read_byte_data()调用,中间要经过那么多层?
先看一张逻辑上的分层图,脑子里有了这张图,后面所有代码都能对号入座:
- 应用层/用户空间:
/dev/i2c-N设备节点,或者i2c-dev模块提供的I2C_RDWRioctl,这是调试和验证最常用的一层。 - 核心层(i2c-core):管理适配器注册、客户端枚举、驱动匹配、总线通信的调度。它不直接操作硬件引脚,而是把请求转交给adapter。
- 适配器层(i2c-adapter + i2c-algorithm):真正和硬件寄存器打交道的地方。适配器代表一个I2C控制器(比如SoC内部的I2C外设),algorithm实现具体的收发时序,比如
i2c-imx、i2c-omap、i2c-bcm2835这些就是这一层的实现。 - 设备驱动层(i2c-driver + i2c-client):这是大部分人写代码的地方,针对挂在总线上的具体设备(传感器、EEPROM、触摸屏控制器),通过
i2c_transfer()或SMBus API发起通信,由核心层配合适配器层完成实际传输。
当你在设备驱动里调用i2c_transfer()时,内核实际干的事情至少包含这些:把i2c_msg数组交给核心层,核心层找到对应的adapter,调用adapter->algo->master_xfer(),由算法实现拼接出起始位、地址字节、数据字节、停止位,再通过控制器寄存器一条一条把时序搬上总线。等硬件中断或标志位告诉你传输完成,结果再原路返回到驱动。
这个链路的第一个关键点在于:设备驱动永远不直接碰控制器寄存器。你只要知道设备的从机地址和寄存器地址,用标准API发消息即可。寄存器配置、时序细节、错误重试,全在adapter那一层。所以写I2C设备驱动的时候,脑子里只需要想"我要发的数据是什么",而不用想"怎么把SCL拉低"。
反过来说,如果你的I2C总线时序有问题,比如ack收不到、时钟频率不对、地址被误解析,那问题大概率出在adapter或者硬件配置上,而不是你的设备驱动写错了。这个判断方向在实战里非常重要,能帮你第一时间缩小排查范围。
2. 四个核心结构体:adapter、algorithm、client、driver的关系与生命周期
很多人搞不清i2c_adapter、i2c_algorithm、i2c_client、i2c_driver这几个结构体到底谁和谁是一对,谁又注册在先。我直接用一张关系表拆开讲,再逐个说明它们在代码和运行时的真实存在方式。
| 结构体 | 代表对象 | 谁来创建 | 典型注册函数 | 生命周期的开始 |
|---|---|---|---|---|
i2c_adapter | 一个I2C控制器 | 平台驱动/ACPI/设备树节点解析 | i2c_add_adapter()/i2c_register_adapter() | 控制器驱动probe时 |
i2c_algorithm | 控制器的通信方法集 | adapter驱动的作者实现 | 赋给adapter->algo | 与adapter同时 |
i2c_client | 一个挂在总线上的从设备 | 设备树/ACPI枚举、板级代码、或手动attach | i2c_new_client_device() | adapter就绪后 |
i2c_driver | 从设备的驱动逻辑 | 设备驱动作者实现 | i2c_add_driver() | 模块加载时 |
先说i2c_adapter。在内核里每一个适配器对应一个编号,比如/dev/i2c-1里的数字1就是这个adapter的nr值。适配器内部最关键的两个字段是algo和algo_data,前者是函数指针集,后者是传给这些函数的私有数据(通常是控制器寄存器的基地址或自定义结构体)。控制器驱动在probe里做三件事:初始化硬件时钟和引脚、填充i2c_algorithm、调用i2c_add_adapter()注册自己。
i2c_algorithm就三个回调要关注:master_xfer()(原生I2C传输)、smbus_xfer()(SMBus协议传输,很多控制器不单独实现,内核会用i2c_smbus_xfer_emulated()做软件模拟)、以及functionality()(声明控制器支持的能力,比如是否支持10位地址、是否支持快速模式等)。这个能力位很重要,因为上层API会用它来做参数校验,不支持的能力直接返回EOPNOTSUPP。
i2c_client这个名字容易误导人,它不是通常意义的"客户端连接",而是代表一个物理从设备。它主要携带从机地址、地址位宽、使用的adapter指针、设备树兼容性信息。当设备树里某个I2C控制器节点下有子节点时,core会为每个子节点创建client。在运行时,client会挂在adapter的userspace_clients链表上,或者被i2c_driver的probe绑定。
i2c_driver就是我们写驱动时通过i2c_add_driver()注册的东西。它里面有id_table(匹配用的i2c_device_id列表)和driver子结构体(含of_match_table)。匹配成功后,core调用驱动的probe(),并且把对应的i2c_client指针传进来。至此,client和driver形成绑定,之后驱动就能用client->adapter发起通信。
很多人在这里会有一个误区:以为注册i2c_driver后,驱动会自动枚举到总线上的所有设备。其实不是,必须系统里先有描述这个从设备的i2c_client,然后i2c_driver才会去匹配它。i2c_client从哪里来?要么设备树写了子节点,要么板级代码调用i2c_new_client_device(),要么用户态用i2c-dev的I2C_SLAVEioctl绑定(这是创建不了内核client的,只是用户态直接通信)。这也是为什么你往设备树里加了一个子节点却怎么都不probe时,要先确认节点是否正确解析成了client。
3. 从"描述设备"到"匹配驱动":设备树与i2c_device_id的匹配机制
I2C设备驱动的绑定,常见的就两条路径:设备树描述和设备表描述。我强烈建议新项目全部走设备树,原因下面会讲。
设备树里一个典型的I2C从设备节点长这样:
&i2c2 { status = "okay"; clock-frequency = <400000>; temperature@48 { compatible = "ti,lm75"; reg = <0x48>; /* 某些设备需要指定reg的位宽,比如10位地址 */ reg-names = "temp"; }; };内核解析流程是这样的:I2C core在注册adapter时,会去扫描它的设备树节点下所有子节点,对每个子节点创建一个i2c_client;子节点的compatible属性会和i2c_driver里of_match_table逐一匹配;reg属性解析成从机地址。匹配成功后触发probe。
of_match_table和i2c_device_id的优先级关系也经常让人困惑。简单规则是:如果设备树匹配成功,拿到的匹配数据是of_device_id里的data;此时i2c_driver->id_table可以留空,probe照样会执行。但如果你同时用MODULE_DEVICE_TABLE(i2c, xxx_id)做模块自动加载,需要i2c_device_id不为空。从设备树匹配出来的client,其name字段取自compatible的第二个字符串(如果没有就取第一个),这个name在做i2c_get_clientdata之外的某些内核逻辑里会用到。
再说i2c_device_id这条路径。在没有设备树的老板子,或者用ACPI描述I2C设备的情况下,匹配依据是ID表。比如:
static const struct i2c_device_id xxx_id[] = { { "lm75", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_id);内核在i2c_match_id()里,会对比client->name和ID表里的名字,一致就绑定。但注意,ACPI环境下client的name来自ACPI的_HID或设备树compatible的转换,不一定和ID表完全一致,所以ACPI系统更依赖acpi_device_id的匹配。
匹配成功之后,probe()调用时机是在adapter完全注册成功、client创建完成之后。这里有个很常见的启动顺序问题:如果I2C控制器的platform驱动注册得晚,或者它的probe因为外部依赖延迟了,那么挂在它下面的client就不会创建,自然也不会触发设备驱动的probe。这种问题表现出来就是设备一直枚举不到,不是驱动没加载,而是adapter还没就绪。
4. 实战代码:一个完整I2C设备驱动的骨架、读写API与原子性陷阱
这章直接给一套能抄作业的I2C设备驱动骨架,然后重点讲API选型和失败重试策略。设备驱动虽然结构简单,但要写稳还是要花心思的。
4.1 驱动骨架和probe流程
#include <linux/i2c.h> #include <linux/module.h> #include <linux/of_device.h> struct my_sensor_data { struct i2c_client *client; int irq; }; static int my_sensor_probe(struct i2c_client *client) { struct my_sensor_data *data; int ret; data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >struct i2c_msg msgs[2]; msgs[0].addr = client->addr; msgs[0].flags = 0; /* 写 */ msgs[0].len = 1; msgs[0].buf = reg_addr_buf; msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; /* 读 */ msgs[1].len = data_len; msgs[1].buf = data_buf; ret = i2c_transfer(client->adapter, msgs, 2);这里有个新手标准错误:有人会用i2c_master_send()先发寄存器地址,再用i2c_master_recv()读数据。这种拆分在大多数控制器上不会出问题,因为两次调用之间有总线空闲窗口,器件一般也接受。但有的器件对"地址写完后必须立刻读"有时序要求,中间一旦有空闲时间,器件会认为访问结束。这种场景必须用上面的组合消息。更极端的情况,如果控制器不支持repeated start,组合消息也可能被拆成两次独立传输,那就得看数据手册的具体时序要求了。
4.3 传输失败的经典返回值和事务重试
i2c_transfer()返回的是成功传输的消息条数,不是字节数。传2条消息只有1条成功,返回1,你得自己判断哪条失败了。典型失败返回值有几个:-EAGAIN表示仲裁丢失或总线忙,-ETIMEDOUT是控制器超时,-EREMOTEIO是收到NACK,-EINVAL是参数非法(比如addr超过10位范围)。
收到-EREMOTEIO要特别警惕:它通常表示总线上没有设备回应这个地址,或者设备地址错误,或者设备没上电。半夜调板子时遇到这个错误,第一反应应该是用示波器抓SCL/SDA,确认有没有设备在总线上。
我用过一个比较稳的重试模板:对临时性错误重试3次,每次间隔1~2ms;对NACK这类确定性错误不重试,直接上报,否则设备真的不在总线上时,你会被日志刷屏,还掩盖了真实的硬件问题。
static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val) { int ret; int retries = 3; do { ret = i2c_smbus_read_byte_data(client, reg); if (ret >= 0) break; if (ret == -EREMOTEIO || ret == -EINVAL) break; /* 确定性错误,不重试 */ msleep(1); } while (--retries); ... }5. 适配器那边的活:i2c_algorithm实现与一次master_xfer的完整流程
设备驱动讲完,得翻到适配器侧看看。虽然大部分人不写适配器驱动,但理解这层能让你排查问题的心态完全不一样。如果碰巧你要移植一个新的I2C控制器驱动,下面这套流程直接照做能少走很多弯路。
5.1 控制器硬件如何抽象为algorithm
i2c_algorithm结构体里,master_xfer函数指针的签名是:
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);它的任务是把msgs数组逐条翻译成控制器能执行的硬件操作。对于绝大多数SoC内部I2C外设,硬件会提供一个状态机:你先往数据寄存器写从机地址和读写位,控制器自动发送起始位,然后你逐个写数据字节,每个字节发送后等硬件产生中断或标志位,读数据则类似,最后写停止位命令。
一个简化且又能说明问题的实现思路是:对每条message,先写从机地址到控制器地址寄存器,同时设置读写标志和消息长度;然后进入循环,逐字节填充FIFO或等待状态寄存器为空后再填充下一个字节;最后发停止位。中途如果收到NACK标志,直接中断并返回错误。
在master_xfer里一定要做的一件事是参数健康检查:检查adapter指针、检查msgs数组长度、检查I2C_M_RD标志的合法性、检查msgs[i].addr是否和控制器支持的地址模式一致。这些检查看似多余,实际能避免很多不可名状的硬件卡死。
5.2 如何选择一个靠谱的起始点和停止点
时序上最容易出问题的是起始位和停止位。控制器一般有独立的start/stop命令位,但你得按顺序来:先写地址和数据,再发起始位命令?还是先发起始位命令再写地址?不同控制器的微架构不一样。为了让代码在各种器件面前都表现得干净,我一般的做法是严格遵循数据手册时序图,并且每次传输完成后主动发出停止位,哪怕传输中途出错也要发停止位,保证总线不挂着锁定状态。
有些控制器还有自动的"总线释放"功能,超时后会自动发停止位。但如果寄存器里没配这个东西,错误路径上忘记发停止位,总线上的从设备可能就一直认为主设备还在通信中,之后的所有访问都混乱。这种问题在日志里很难看出来,示波器一看SDA被拉死在一个电平上就明白了。
functionality()这个回调相对简单,但别偷懒。I2C_FUNC_I2C表示支持原生I2C消息,I2C_FUNC_SMBUS_BYTE_DATA代表支持SMBus字节读,等等。i2c-core在i2c_transfer()时不会严格检查功能位,但SMBus模拟路径会查。功能位声明少了,上层调用一些标准API会直接拒绝;声明多了,API调用会进入控制器不支持的路径,行为就不可预测。所以这个回调要跟硬件特性一一对应,宁少勿多。
5.3 adapter注册时的锁和并发
adapter内部有一个bus_lock锁,i2c_transfer()的整个过程中,从拿到消息数组到master_xfer返回,这个锁一直被持有。这意味着同一时刻同一adapter上只能有一个传输在跑,不同adapter之间互不干扰。如果你在master_xfer实现里又去调用了另一个adapter的传输函数,就死锁了——实操中真有人这么干过。
此外,master_xfer运行在进程上下文,可以睡眠,所以控制器驱动里等中断、等标志位都合法。千万别在中断上下文调用i2c_transfer(),内核会有明确的BUG_ON等着你。真要发数据,要么靠threaded irq,要么把数据推到工作队列里。
6. 调试三板斧:i2c-dev用户态验证、内核日志、示波器实测
驱动写完不是马上就可以交付的,调试阶段往往比写代码花更长时间。这里分享我实际调I2C时从用户态到内核态再到硬件的逐层排查方法,这个顺序非常关键,能尽量避免盲目猜测。
6.1 先用i2c-tools排除硬件和枚举问题
任何I2C新板子回来,都先别急着写驱动,用i2c-tools把总线扫一遍。前提是内核配置了CONFIG_I2C_CHARDEV,并且adapter注册成功后出现了/dev/i2c-N。
i2cdetect -l # 列出系统里所有I2C adapter i2cdetect -y 1 # 扫描总线1上的设备地址 i2cdump -y 1 0x48 # dump从机0x48的所有寄存器 i2cget -y 1 0x48 0x0F # 读从机0x48的寄存器0x0Fi2cdetect -y 1这个命令能帮助确认设备树里的地址写没写对。设备在地址0x48,扫描列表的0x48位置如果显示UU,说明这个地址被内核client占用;如果显示48,说明内核里没有client但总线上的设备确实在响应;如果显示--,硬件上就找不到这个设备,这时候再查电源、上拉电阻、或者地址引脚配置。
这个步骤的核心价值在于它不经过你的设备驱动,直接验证"内核I2C核心层和适配器层能不能正常工作"。如果i2cdetect扫不到设备,那你的驱动probe就算被调用了,实际读写也大概率失败,问题根源在adapter、硬件连接或设备树地址,而不是你的驱动逻辑。
6.2 内核日志里那些看似无害的I2C错误
驱动里dev_err打出来的I2C错误,要结合返回值判断严重程度。我见过不少人报"我的驱动一加载就报错failed to read chip id: -121",其实-121是-EREMOTEIO,也就是NACK。这时候先别怀疑驱动,挂上示波器,或者先用i2cdetect看设备在不在。
另外kernel: i2c i2c-1: sendbytes: NAK bailout这种日志,是i2c-dev路径下的SMBus模拟打印的,意思是写首字节后从机不回ACK。常见于从机地址正确但寄存器地址超范围,或者器件要求先发设备地址再发寄存器地址而你只写了设备地址。看到这类日志,用i2cdump确认从机实际支持的寄存器范围是一个高效的排除方式。
还有一类是i2c_transfer返回-ETIMEDOUT,这种多数是控制器的传输超时机制触发了,要么是时钟没配置对导致SCL太低,要么是总线上有设备把SCL拉死。此时用示波器抓波形,比任何日志都有用。
6.3 示波器看波形,能解决九成"玄学问题"
I2C调起来最忌讳"觉得没问题"。有次一个板子读EEPROM,前100次都正常,偶尔读取出错,软件上怎么改都压不住。最后示波器挂上去,发现SDA上升沿特别缓,原因是上拉电阻阻值选得太大,加上板载电容,导致SDA在标准模式下上升时间超标。改小上拉电阻后问题彻底消失。
示波器抓I2C有个基本套路:触发方式设为SDA下降沿,时基先放到毫秒级找整包波形,再放大到微秒级看单字节。确认的事情依次是:起始位是否标准、从机地址和读写位是否和预期一致、是否有ACK位、时钟频率是否和clock-frequency属性配置的一致、停止位是否干净。
对于400kHz快速模式,一个bit约2.5微秒,示波器采样率不够高,上升沿细节是看不准的。有条件的话用至少100MHz带宽的差分探头,没条件时在SCL/SDA上引出测试点,尽量缩短探头地线长度,不然看到的全是振铃。
7. 我踩过的那些I2C深坑,每条都对应一个排查场景
最后这部分是真正的经验值。我按"症状→根因→解决"的形式列几条最典型的,这些都是在内核代码里翻过、示波器上验证过的真实情形,供你在现场对照。
坑1:probe正常但首次读寄存器就全0xFF
设备树地址没错、i2cdetect也能扫到,但读寄存器返回的数据全是0xFF。后来发现设备的工作模式需要先激活(比如某些传感器有个使能寄存器,默认是关断状态),上电后直接读芯片ID读到的是虚无。解决方法是probe里先按数据手册写使能序列,再读ID。这个坑警示的是:i2cdetect扫到设备只代表6个地址字节的ACK正确,不代表器件已经准备好业务通信。
坑2:i2c_transfer()组合消息在部分控制器上时序不对
在某款SoC的I2C控制器上,i2c_transfer()传2条消息(写寄存器地址+读数据),结果读到的数据和寄存器地址不呼应。查了控制器手册,发现它对第二个消息的起始条件处理有bug,需要在第一条消息结束后插入小延时。这种问题没有通用解法,只能结合控制器手册和实测波形,通过改用SMBus读函数(它内部先发地址再读)规避,或手动加udelay()。
坑3:设备树里reg地址写的是16进制但带了后缀
写过reg = <0x48>;的人都觉得没问题,但我在一个项目里见过reg = <0x48 4>的写法,第二个数字被解析成了地址长度标志,导致client地址被错误地截断。内核的i2c地址解析支持多字段表示地址位宽,比如reg = <0x48 0x0010>表示10位地址。这个语法不熟悉时,宁可直接写单值。
坑4:适配器编号漂移导致用户态程序连错总线
系统里有两个I2C控制器,但注册顺序受内核启动调度影响,有时设备是i2c-1,有时是i2c-2。用户态/dev/i2c-1程序可能在上电后连了错误总线,导致偶发读写失败。内核里的解决办法是用alias或者在设备树里给adapter指定i2c-bus-id,用户态也可以用/sys/bus/i2c/devices/i2c-1/name这种sysfs属性确认设备身份再操作。
坑5:i2c_driver的probe返回-EPROBE_DEFER却被core拒绝调度
有驱动依赖的资源和I2C设备不在一个设备树节点里,probe阶段拿不到时钟,就返回-EPROBE_DEFER等内核稍后重试。但有个同事在probe里把这个返回值和普通错误一样处理,直接return ret也没事——内核本来就能识别-EPROBE_DEFER并重新调度probe。真正的问题是有些驱动错误地把别的错误码也返回成了-EPROBE_DEFER,导致内核无限重试。probe里该用就用,不该用千万别滥用。
坑6:总线引脚复用(pinmux)配置在设备树里缺了
I2C控制器驱动本身probe成功了,i2cdetect -l也能看到adapter,但就是扫描不到任何设备。排查到最后,设备树里I2C控制器的pinctrl配置没写,SoC把引脚默认复用成了GPIO模式,I2C外设信号根本没接到引脚上。这类问题用示波器一看就明白,SCL/SDA上完全没有波形。
8. 写I2C驱动时,值得反复对照的几项自查清单
代码写完之后,交给review或上线之前,建议按下面的清单自己过一遍,每一条背后都是一个"现场翻车三小时"的教训:
- 设备树地址是否和硬件实际地址一致:传感器或EEPROM的地址引脚(A0/A1/A2)决定实际地址,不要只抄数据手册默认值。
- 上电稳定时间够不够:probe里读寄存器前,给器件几十毫秒的稳定时间,比读失败后再重试省心。
- 传输失败是否区分了确定性错误和临时错误:NACK不要重试,超时和仲裁丢失要重试,重试间隔别太短。
- SMBus API和原生I2C API是否选对:组合消息场景用
i2c_transfer(),简单寄存器读写用SMBus封装更稳。 of_match_table和id_table是否都配置了:设备树系统至少要配of_match_table,模块自动加载时id_table也别缺。- 是否在probe里验证了设备ID:任何一个真实器件驱动,probe里不读ID做校验,就是给后续调试埋雷。
- 中断上下文是否调用I2C传输:这里一旦踩到,轻则
BUG_ON白屏,重则总线挂死,务必用threaded irq或工作队列。
这套清单不是从任何手册上抄的,都是我在项目里被现场问题教育出来的。碰上一个,记下来,下次review的时候就能提前拦掉。
最后再说两句实际感受
Linux I2C内核驱动这个领域,说难是真不难:无非就是i2c_client、i2c_driver、i2c_adapter、i2c_algorithm四个结构体在那里转,看懂一次就能看懂所有控制器驱动。但说容易也真不容易,因为90%的时间消耗不在代码上,而在"排除一个又一个看起来不合理的可能性"上。我的体会是,调I2C最重要的能力不是背API,而是建立一条从应用层一路到引脚的完整因果链:哪一层出了问题,会在哪个环节以什么形式暴露。你用i2cdetect验证过总线,用示波器确认过时序,再回到代码里查逻辑,解决问题的速度会比盲目改参数快一个数量级。
如果你也正在被某个I2C设备折磨,不妨先别急着改驱动,按上面第6章的顺序,把"用户态扫描、内核日志、示波器波形"这三板斧挨个敲一遍。多数情况下,答案会在你放下代码、盯着物理波形的那几分钟里自己跳出来。