首页 / 资讯中心 / 文章详情

Linux I2C驱动核心结构体与i2c_transfer避坑指南

Linux I2C驱动核心结构体与i2c_transfer避坑指南 ★ FEATURED ARTICLE
Linux 平台的 I2C 驱动分层特别清晰清晰到有点“冷”——两个设备地址配错、地址位数写错、驱动返回码写错编译都能过但 probe 就是不进、i2c_transfer 就是返回 -ENXIO。我前段时间调一颗小传感器原理图对的、设备树 reg 对的、供电也对的愣是卡了半个多小时最后发现i2c_driver.id_table里的名字少了个字母。这件事之后我把 I2C 子系统里那几个核心结构体和接口重新撸了一遍发现绝大多数“玄学问题”其实都落在i2c_adapter、i2c_client、i2c_driver、i2c_msg这四类对象的边界上。这篇就围绕 Linux I2C 驱动里最重要的结构体与接口把它们的职责、字段语义、调用约定和真机上反复踩到的坑讲透适合已经在 Linux 下写过字符设备、想接手板级总线驱动或者被一个 -ENXIO 折磨到怀疑人生的人看。1. probe 进不去的那半小时I2C 驱动里到底有哪几类对象1.1 总线、控制器、设备、驱动四类角色缺一不可很多人第一次读 I2C 子系统代码会产生一个误解以为i2c_adapter代表“一条 I2C 总线”。这个理解方向没错但它描述的是软件视角下的抽象——一条可被上层寻址的通道。物理上真正存在的是一颗 SoC 内部的 I2C 控制器 IP它有寄存器、有时钟、有中断甚至还有 FIFO 和 DMA 通道。控制器驱动注册上去的i2c_adapter只是把这块硬件包装成一个“可以用 7 位地址、发送 msg 序列”的服务提供者。因此一条完整的链路上有四类对象它们的分工非常硬对象所属侧典型来源一句话职责i2c_adapter控制器SoC 控制器驱动platform_driver.probe提供 “发一串 i2c_msg” 的能力i2c_algorithm控制器控制器驱动内部静态定义规定这套硬件怎么真的把电信号打出去i2c_client从设备设备树、board_info、手动创建一条总线上的一个从设备实例携带 7 位地址i2c_driver从设备传感器/EEPROM/PMIC 驱动一组回调等某个 client 匹配上来后执行 probe这四者的关系并不复杂控制器驱动拿着i2c_adapter去i2c_add_adapter()总线就有了“路”从设备侧提交i2c_driver去注册同时某个i2c_client通过名字或 compatible 匹配上这个 driver内核才调用probe()。probe()拿到了i2c_client用i2c_transfer()或 SMBus 封装接口去收发数据。回到我那次卡壳的场景i2c_client是有的设备树里 comaptible 写对了i2c_transfer也能在外围程序里跑通i2c-tools 能读到寄存器说明 adapter 和硬件都没问题问题必然出在 driver 匹配这一环。后来我把id_table打印出来一看果然少了个字母。这也说明一个判断方法问题出在哪一层是可以通过现象定位的。i2c-tools 能读写但 insmod 后/sys/bus/i2c/devices/下没有新绑定 → 匹配环节问题。有绑定、但 probe 立刻返回错误 → probe 里的资源获取问题。probe 成功、但读写数据不对 → msg 组装或时序问题。这条判据能省掉大量无头苍蝇式的排查。1.2 结构体之间的挂接关系比想象中更直接i2c_adapter里有algo指针指向i2c_algorithm有nr字段表示总线号对应/dev/i2c-0这样的节点有dev内嵌 device。i2c_client里有adapter指针反向指回所属总线有addr字段存 7 位地址有dev内嵌 device。i2c_driver里有id_table、probe、remove本质就是一个device_driver的特化版本。最能说明问题的一点i2c_client.dev的 parent 是i2c_adapter.dev所以你在/sys/devices/.../i2c-1/1-0050/下能看到挂在总线目录底下的设备目录。这个层级关系不是装饰它决定了设备树解析、devm_*资源释放、电源管理回调的顺序也决定了你dev_err()打印出来的那条路径长什么样——从路径就能读出 bus 号和地址。i2c_transfer()的第一个参数是client-adapter这个设计也是在强调一点传输能力属于 adapterclient 只是“告诉你发给谁”。搞清这一点后面很多接口的参数含义就顺了。2. i2c_adapter 与 i2c_algorithm控制器这一侧的底座2.1 adapter 注册时到底发生了什么i2c_add_adapter()或i2c_add_numbered_adapter()是控制器驱动 probe 的最后一步。前者让内核动态分配总线号后者要求调用者自己指定adap-nr。要固定总线号比如板子上希望某个控制器永远是i2c-3就用带 numbered 的版本并把CONFIG_I2C_COMPAT之类历史遗留概念抛开只用nr和bus_lock_init。注册过程中内核会做几件容易被忽略的事创建/dev/i2c-N字符设备节点需要CONFIG_I2C_CHARDEV这是 i2c-tools 能用的前提。在 sysfs 下建立i2c-N目录并挂载设备树里已经描述好的子设备。调用adap-algo-functionality()拿一次能力位图决定 SMBus 哪些接口可以走。加锁保护总线列表。这段逻辑意味着顺序很重要。如果你在i2c_add_adapter()之前就去访问adap-dev的子设备那什么都拿不到反过来如果先注册 adapter 再初始化内部锁或时钟在设备树子节点立即触发 probe 的场景下就可能直接炸。一个真实的小坑控制器 probe 里时钟没prepare_enable就把 adapter 注册上去了设备树里的从设备立刻 probeprobe 里第一件事就是读 ID 寄存器——结果 SCL 上一点波形都没有读到全是 0。这个错很容易让人怀疑地址其实只是时钟还没开。2.2 master_xfer 的返回值语义决定了上层怎么判错i2c_algorithm里最重要的三个成员struct i2c_algorithm { int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); int (*master_xfer_atomic)(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的返回值有一个非常明确但常被写错的约定成功发送了num条 msg返回num。发了部分就失败了返回实际完成的条数。一条都没发出去返回负的错误码。写错这里的后果是链式的。上层i2c_transfer()拿到小于num的非负返回值时会返回这个数字而很多驱动写的是if (ret 0) return ret;于是“只发了一条”被当成了成功后面读到的数据全是上一次的残留。我在一次 EEPROM 写页操作里就遇到过页写明明没成功驱动却得意洋洋地返回 0用户态看到的全是旧数据。正确的写法是把负错误码透传上去把“部分完成”明确表达为“没完成”static int xxx_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct xxx_i2c_dev *i2c i2c_get_adapdata(adap); int i, ret; for (i 0; i num; i) { ret xxx_i2c_xfer_one(i2c, msgs[i]); if (ret 0) return ret; } return num; }functionality()也要认真写别一上来就return I2C_FUNC_I2C;了事。它决定了 i2c-tools 哪些命令可用、决定了 SMBus 走真实硬件还是走 emulated 路径static u32 xxx_i2c_func(struct i2c_adapter *adap) { return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_NOSTART | I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA; }如果你的硬件支持省略起始位组合写场景一定要报I2C_FUNC_NOSTART否则上层看到这个位没置位会禁用I2C_M_NOSTART相关的优化路径某些器件的“写命令读数据一气呵成”就没法实现。2.3 一个最小可用的 master_xfer 骨架该怎么搭真正实现master_xfer时最常见的结构是“逐 msg 处理内部的 xfer_one 负责拼装 START/ADDR/RW/ACK/STOP”。骨架大致长这样static int xxx_i2c_xfer_one(struct xxx_i2c_dev *i2c, struct i2c_msg *msg) { u32 ctrl 0; int ret; ret xxx_i2c_wait_bus_idle(i2c); if (ret) return ret; xxx_i2c_set_addr(i2c, msg-addr, msg-flags I2C_M_RD); if (!(msg-flags I2C_M_NOSTART)) ctrl | CTRL_GENERATE_START; if (msg-flags I2C_M_RD) ctrl | CTRL_READ_MODE; writel(ctrl, i2c-base REG_CTRL); ret xxx_i2c_xfer_bytes(i2c, msg-buf, msg-len); if (ret) return ret; if (!(msg-flags I2C_M_NO_RD_ACK) !(msg-flags I2C_M_IGNORE_NAK)) ret xxx_i2c_check_ack(i2c); return ret; }几个要点值得单独说。第一msg-addr永远是 7 位地址硬件寄存器里通常需要左移一位再填读写位这个左移是在控制器驱动里做的不是在上层驱动里做的。第二多 msg 之间的 STOP 由谁产生要看清控制器行为——有些控制器每条 msg 自动带 STOP有些需要手动控制写错了就会出现“读寄存器时莫名多出一次 STOP器件状态机复位”。第三I2C_M_IGNORE_NAK和I2C_M_NO_RD_ACK这两个标志是给特殊场景用的不要图省事默认忽略 ACK否则地址根本没人应你也会“成功”。3. i2c_client一条总线上那个具体设备的身份证3.1 client 的四种来源优先用设备树i2c_client从哪里来决定了你调试时该看哪里。常见的四种路径设备树子节点I2C 控制器节点下的reg 0x50子节点内核在注册 adapter 时自动实例化 client。这是现在最主流的做法。i2c_board_infoi2c_register_board_info()老的板级代码方式靠arch_initcall注册一张静态表。新板子基本不用了但老 BSP 里遍地都是。i2c_new_client_device()运行时手动创建典型场景是 CPU 控制的 GPIO 复用器、或者探测到某个条件后才挂上去的子设备。i2c_new_dummy_device()占位用的 dummy 设备通常是多地址器件的“另一个地址”你想占个位、或者为了给某个功能模组做地址仲裁。对写法上第 3 种在较新内核里是i2c_new_client_device()旧的是i2c_new_device()后者会让人误以为返回的 client 一定有效实际上它有可能是 error pointer必须用IS_ERR()判。老代码里if (!client)的写法在新内核下是错的。手动创建 client 的完整写法大概是这样struct i2c_board_info info { .type xxx-sensor, }; info.addr 0x48; client i2c_new_client_device(adap, info); if (IS_ERR(client)) return PTR_ERR(client);注意i2c_new_client_device()返回的 client 生命周期由 adapter 托管你不需要也不能随意i2c_unregister_device()之外的释放动作否则就是双重释放。如果这块是临时代码、只在 probe 里用一次用完必须i2c_unregister_device(client)否则 device 结构泄漏、下次加载可能撞地址。最省心的方式还是走设备树因为匹配、供电 regulator、复位 GPIO、中断这些都通过标准属性描述好驱动里只关心寄存器。3.2 地址写 7 位还是 8 位一个错就白忙一天这是 I2C 驱动里频率最高的错误没有之一。i2c_client.addr是7 位地址。设备树reg 0x50填的是7 位地址。struct i2c_msg.addr也填7 位地址。硬件手册上给的“写地址 0xA0 / 读地址 0xA1”是8 位地址等于7位 1 | 读写位。换算关系8位写地址 1 7位地址。0xA0 1 0x50对。0x50 1 | 1 0xA1读地址。就这么简单但人们在看手册时经常直接把 0xA0 填进reg然后得到的结果是总线上实际发出去的地址变成 0x40器件当然不应答i2c_transfer返回-ENXIO。-ENXIO基本等价于“地址发出去没人 ACK”。遇到它先别改代码用i2cdetect -y -r 1扫一遍确认目标地址上确实有设备。注意i2cdetect默认显示的就是 7 位地址跟设备树一致这个对应关系要建立起来。还有一种反向的坑把地址写对了但i2c_msg.len写成了 0某些控制器会直接返回成功而且总线上啥都不发看起来“读到了”但 buffer 里全是初始值。3.3 clientdata 和 drvdata 是一回事但别混着用i2c_set_clientdata(client, data)和i2c_get_clientdata(client)是驱动里存取私有数据的标准姿势内部就是把指针塞进dev_set_drvdata(client-dev, ...)。所以它和dev_set_drvdata()、dev_get_drvdata()是同一份存储。问题在于当一个驱动同时管理多种从设备、或者上层框架比如 hwmon、regmap也往devdata里塞东西时就会互相覆盖。我见过一个例子驱动在 probe 里i2c_set_clientdata(client, priv)后面又调了devm_regmap_init_i2c()某份中间层代码顺手dev_set_drvdata(dev, regmap)结果 suspend 回调里取出来的 priv 变成了 regmap 指针一解引用就直接崩。规避方式很朴素驱动里统一只用一个存取入口要么全走i2c_get/set_clientdata要么全走dev_get/set_drvdata不要两者混用。如果确实要和框架共享把私有结构里的字段设计成“能容纳两份指针”或者用dev_get_drvdata之外的方式比如从device的 parent 上取分离。另一个常见错误是在remove里忘了i2c_set_clientdata(client, NULL)。如果驱动被 rmmod 之后又重新 insmod而 client 是长期存在的设备树那种probe里可能因为某些分支提前返回remove拿到的 priv 已经是野指针。4. i2c_driver匹配链路、probe 边界与 remove 的收尾4.1 id_table、of_match_table、acpi_match_table 的优先级i2c_driver是个特化的device_driver匹配走内核标准的 bus match 机制。I2C 总线层的i2c_device_match()会依次尝试三种匹配方式of_driver_match_device()设备树 compatible 匹配最优先。acpi_driver_match_device()ACPI 匹配。i2c_match_id()字符串名字匹配即id_table[].name与client-name比较。正因为有第三条id_table里名字写错就变成了那种“编译不报错、运行不匹配”的隐形错误。而如果只写了of_match_table没写id_table在纯设备树平台上通常没问题但一旦有人用i2c_new_client_device()手动创建 client.type xxx匹配就会失败——因为手动创建的 client 没有 of_node。所以稳妥的做法是两手都写而且名字保持一致static const struct i2c_device_id xxx_id[] { { xxx-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, xxx_id); static const struct of_device_id xxx_of_match[] { { .compatible vendor,xxx-sensor }, { } }; MODULE_DEVICE_TABLE(of, xxx_of_match); static struct i2c_driver xxx_driver { .driver { .name xxx-sensor, .of_match_table xxx_of_match, .pm xxx_pm_ops, }, .probe xxx_probe, .remove xxx_remove, .id_table xxx_id, }; module_i2c_driver(xxx_driver);module_i2c_driver()这个宏替掉了module_init/module_exit里的一堆样板用它能少写二十行。注意它内部已经调用了i2c_add_driver和i2c_del_driver自己再写一遍就是重复注册。4.2 probe 里该做什么不该做什么probe(struct i2c_client *client, const struct i2c_device_id *id)这个签名在较新内核里id参数被去掉了只剩client。如果你看到编译警告说参数不匹配就是内核版本差异别硬扛用#if或者直接按新签名写。probe 里的顺序我一般固定成这样先取平台数据client-dev.of_node、device_property_read_*。申请 regulator、GPIO、时钟全部用devm_版本。确认通信正常读一次 chip id 或版本寄存器失败就return -ENODEV。这一步非常重要是把“硬件没焊好/地址配错”和“驱动逻辑错”分开的分水岭。初始化驱动的内部状态和锁。注册子系统接口hwmon、input、iio、misc 等。配置中断如果有注意中断处理函数里不能用会睡眠的i2c_transfer除非用i2c_transfer_atomic或者丢到 workqueue。不该做的事也明确不要在 probe 里做长时间的忙等不要在 probe 里调msleep(500)等器件上电稳定——上电稳定用 regulator 的startup-delay-us或者设备树的reset-gpiosusleep_range小延时解决不要在没有检查返回值的情况下连续发若干条i2c_smbus_write_*一旦中间某步失败后面全乱。第 3 步检查 chip id 这件事我还想再强调一次。很多驱动为了“通用”把这步省了结果板子上焊错料、或者同一 compatible 对应多个型号时驱动照样 probe 成功只是后面读写全是垃圾。加一行 ID 检查的成本几乎为零省下的调试时间按天算。4.3 remove 与 devm 的边界remove()里该释放的东西用devm_申请的regulator、gpio、时钟、regmap、iio device 等都会被自动释放不用手动写。但有两类必须手动处理你自己kzalloc的私有结构。你注册到全局或子系统里、且不是devm_版本的资源比如某些老框架的register_xxx。顺序上先注销对外接口再释放内部资源最后i2c_set_clientdata(client, NULL)。反过来的话注销过程中如果有回调触发会摸到已释放的内存。还有一个容易忽视的点remove里不该再发 I2C 传输去“把器件关掉”。此时总线可能已经要被拆了或者 adapter 处于 suspend 状态传输会超时。要关器件用pm_ops的suspend或者 regulator 的devm_add_action_or_reset来做。5. i2c_msg 与 i2c_transfer把一次读写拼装正确5.1 四个字段的填法没有模糊地带struct i2c_msg只有四个字段但每个都有讲究字段含义常见错误addr7 位从机地址误填 8 位地址flagsI2C_M_RD等标志位读操作忘了置I2C_M_RDlen本段数据字节数用sizeof(buf)代替实际长度buf数据缓冲区指向栈上临时变量但传输异步化后失效buf的生命周期是重点。i2c_transfer在绝大多数控制器上是同步的函数返回时传输已完成栈上 buf 没问题。但如果你用了 DMA 模式的控制器或者调用了i2c_transfer_atomic情况就变了。稳妥做法是把经常用的 buffer 放进驱动私有结构里复用避免每次都在栈上开。还有flags里几个特殊的I2C_M_RD读方向。I2C_M_NOSTART本段不发起始位紧接上一段。用于“写寄存器地址立即读数据”一气呵成的组合。I2C_M_IGNORE_NAK忽略从机 NAK。I2C_M_NO_RD_ACK读最后一个字节后不发 ACK。I2C_M_RECV_LENSMBus block read长度由从机返回的第一个字节给出。其中I2C_M_NOSTART需要在functionality()里报告I2C_FUNC_NOSTART才生效这一点前面提过。5.2 寄存器读的标准写法与它的边界读一个 8 位寄存器最标准的双 msg 写法static int xxx_read_reg(struct i2c_client *client, u8 reg, u8 *val) { struct i2c_msg msgs[2]; int ret; msgs[0].addr client-addr; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; msgs[1].len 1; msgs[1].buf val; ret i2c_transfer(client-adapter, msgs, 2); if (ret 0) return ret; if (ret ! 2) return -EIO; return 0; }这段代码有两个细节值得拆。第一msgs[0].buf reg是把reg的地址传进去所以reg必须是函数参数这种有独立存储的变量不能直接写(u8){reg}之类的复合字面量虽然 C 语法上可以但可读性和老编译器兼容性都不好。第二ret ! 2的判断不能省前面讲过部分完成会被当成成功。写寄存器则是单 msgstatic int xxx_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; int ret; ret i2c_transfer(client-adapter, (struct i2c_msg){ .addr client-addr, .flags 0, .len sizeof(buf), .buf buf, }, 1); return ret 1 ? 0 : (ret 0 ? ret : -EIO); }注意这里用了复合字面量构造 msg因为只需要一条、且buf在传输期间有效所以安全。如果这条写操作后面要紧接着读就得拆成两条 msg 并考虑用I2C_M_NOSTART合并中间不产生 STOP否则某些器件的“写地址-读数据”状态机会被打断。5.3 返回值别只看是不是负数i2c_transfer()的返回值有三种情况返回num全部成功。返回0 ret num部分成功。这在多 msg 场景下很要命意味着第一条发出去了、第二条没发。返回负值错误码。常见的有-ENXIO地址无应答、-ETIMEDOUT超时可能是时钟被拉低、-EIOACK 阶段出错、-EAGAIN总线忙、-EREMOTEIO。-ENXIO和-EREMOTEIO的区别值得记一下前者通常是地址阶段就没 ACK器件不在后者通常是数据阶段 ACK 出错器件在但拒绝写入比如 EEPROM 页边界或者写保护。这两个错误混在一起排查会很痛苦日志里最好打出来。还有一个隐蔽的坑i2c_transfer()内部会i2c_lock_bus()如果驱动在持有该锁的情况下再次调用传输就会死锁。正常路径不会但在i2c_smbus_xfer的自定义实现里、或者在functionality回调里调用传输就会踩上。6. SMBus 封装接口省事但要知道它下面做了什么6.1 常用接口速查与选型i2c_smbus_*系列函数是把常见的 I2C 读写模式封装成了固定形态省得自己拼 msg。最常用的几个接口对应传输典型用途i2c_smbus_read_byte_data写 1 字节命令 读 1 字节读 8 位寄存器i2c_smbus_write_byte_data写 1 字节命令 写 1 字节写 8 位寄存器i2c_smbus_read_word_data写 1 字节命令 读 2 字节小端读 16 位寄存器i2c_smbus_write_word_data写 1 字节命令 写 2 字节写 16 位寄存器i2c_smbus_read_i2c_block_data写 1 字节命令 读 N 字节读连续寄存器块i2c_smbus_write_i2c_block_data写 1 字节命令 写 N 字节写连续寄存器块i2c_smbus_read_block_data读长度字节 读数据真正的 SMBus block长度由从机给前两组占了实际使用的九成以上。i2c_smbus_read_i2c_block_data和i2c_smbus_read_block_data的差别是新手最容易混淆的地方前者长度由你传参决定适合普通 I2C 器件后者长度由从机返回的第一个字节决定真正的 SMBus 器件才这么干。用错了要么多读几个字节要么第一个字节被当成长度吃掉数据整体错位。6.2 走 emulated 还是走真实 smbus_xfer如果控制器的i2c_algorithm里smbus_xfer是NULL内核会退回到i2c_smbus_xfer_emulated()用master_xfer拼出等效的 msg 序列。这对绝大多数器件完全够用但有两个前提要满足functionality()里必须报告对应的I2C_FUNC_SMBUS_*位否则接口直接返回-EOPNOTSUPP。emulated 路径构造的是标准 msg中间会带 STOP对于某些“必须连续、不能有 STOP”的器件可能不适用这时候老实自己拼 msg。另一个现实问题是时序。SMBus 规范对 SCL 低电平时间、总线空闲时间有要求比如 SMBus 时脉 10 kHz~100 kHz而 I2C 快模式可以到 400 kHz 甚至 1 MHz。如果硬件实际上跑在 400 kHz 而你在代码里走 SMBus 语义某些严格按 SMBus 时序设计的器件可能会抗议。我的经验是能用i2c_transfer就说清楚需要 SMBus 语义的器件就用i2c_smbus_*别为了少写几行代码把语义搞混。7. 引用计数与生命周期i2c_get_adapter 之后的那几步7.1 get/put 必须成对且位置要讲究当你拿到的是总线号而不是现成的i2c_client典型场景一个驱动要给总线上某个器件建 client但自己并不属于那条总线会用i2c_get_adapter(bus_nr)。这个函数会增加 adapter 的引用计数防止你正在用它的时候它被注销掉。adap i2c_get_adapter(bus); if (!adap) return -ENODEV; client i2c_new_client_device(adap, info); i2c_put_adapter(adap); /* 注意位置 */关键在最后这次i2c_put_adapter()的位置。i2c_new_client_device()之后 client 已经独立持有 adapter 的引用所以你自己的这次可以立刻还掉不需要一直攥到驱动卸载。如果忘了 putadapter 永远无法被注销热插拔或者模块卸载时会卡住表现为rmmod挂死或者总线号无法复用。反过来如果在i2c_new_client_device()之前就 put 了那 client 创建过程可能访问已释放的 adapter属于经典 UAF。7.2 总线上设备消失之后I2C 本身没有热插拔检测除了 SMBus Alert 那一套实际用得很少。所以当器件物理上被拔掉时内核并不知道。表现就是下一次i2c_transfer返回-ENXIO然后驱动可能就一直在错误里打转。如果业务上确实需要处理这种情况可以做一个带退避的重试连续 N 次-ENXIO后标记设备离线通过sysfs或者 netlink 通知用户态并停止继续访问。别做的动作是“直接i2c_unregister_device(client)”——如果 client 是设备树带来的注销它会让下次总线扫描时不一定能重新创建出来状态会变得很诡异。还有一种情况是 adapter 在 suspend 期间被关闭此时任何传输都会失败。如果驱动在suspend回调之后还有定时器或 workqueue 在发 I2C必须在这个阶段把工作停掉否则日志里会冒出大量超时。这类问题在低功耗产品上极其常见排查时看时间戳就能对上是 suspend 之后开始刷屏的。8. 实机调试里反复出现的坑与对应手段8.1 ACK 阶段失败与总线被拉低-ENXIO、-EREMOTEIO之外最烦的是-ETIMEDOUT且总线“死了”SCL 或 SDA 被某个器件一直拉低控制器连 START 都发不出去。原因通常是主控在传输中途复位、或者从机在读取过程中被提前打断它的状态机卡在“等下一个时钟”的状态。应急手段是发 9 个时钟脉冲让从机把剩下的位吐完然后手动产生一个 STOP。这个动作在控制器驱动里叫 bus recovery内核有现成的i2c_generic_scl_recovery()配合设备树里的scl-gpios、sda-gpios、recovery-delay-us就能启用adap-bus_recovery_info xxx_recovery_info;很多 BSP 的控制器驱动没实现这一块结果就是一次总线卡死必须重启设备。如果你的板子有量产的打算这部分建议补上成本很低。日常排查总线状态的土办法是用万用表量 SCL/SDA 对地电压正常空闲应该是接近 VCC被上拉电阻拉高。如果某根一直是 0V说明被某个器件钳住了先断电、逐个摘器件定位。8.2 上拉电阻与总线电容这两个硬件因素软件调不通的时候回头看硬件往往能发现根因。I2C 是开漏总线上拉电阻的取值受总线电容限制经验公式是R_max ≈ 300ns / C_bus对应标准模式上升时间要求。一条挂了五六个器件的板子走线又长电容很容易超过 200pF这时候用 10k 上拉就会导致边沿过缓读数据在高频下随机出错。典型现象是单条命令跑得好好的一旦连续读写或者提高速率就开始出-EREMOTEIO而且换一块板子现象不同。这种“看起来像软件、其实是信号完整性”的问题最快的验证方式是把速率从 400 kHz 降到 100 kHz如果问题消失基本就能确定是电气问题。逻辑分析仪抓一次波形看上升沿是不是明显变缓、有没有台阶比什么都直接。8.3 用工具把“现象”和“代码”对上调试链路我一般按这个顺序走i2cdetect -y -r 1确认 7 位地址能扫到。扫不到就别往下走了先查硬件。i2cdump -y 1 0x50读整片寄存器看器件是否响应、数据是否合理。看/sys/bus/i2c/devices/下有没有1-0050这样的目录、有没有driver符号链接。没有 driver 链接说明匹配失败回看id_table和 compatible。dmesg里搜驱动名和i2c看 probe 返回了什么错误码。-ENODEV通常是 ID 检查没过-EPROBE_DEFER是依赖资源没就绪。用示波器或逻辑分析仪看实际波形核对地址字节、读写位、ACK 位置。这套流程的价值在于每一步都排除一个层面不会出现“改了半天代码其实是杜邦线松了”的尴尬。8.4 几个我踩过的小细节最后攒几条零散但真会用到的经验。i2c_transfer里 msgs 数组是栈上的话数组元素的buf指向的也必须保证在函数返回前有效。我见过把buf指向一个static全局缓冲区结果多个 client 并发访问时互相踩数据的例子。多设备场景下缓冲区要么放私有结构要么加锁别图省事用全局。devm_regmap_init_i2c()能省掉大量寄存器读写的样板但它默认的reg_bits/val_bits如果没配对会出现“读 16 位寄存器却只发了 1 字节命令”的诡异现象。用 regmap 时先把config.reg_bits 8、val_bits 8、max_register这几个明确写上别依赖默认值。模块卸载时如果dmesg出现 “remove returned 1”说明你的remove函数返回了正数内核会把非零返回值当警告。remove应该返回 0 或者 void具体看内核版本的函数原型。还有一个和probe相关的如果驱动依赖的 regulator 在 probe 时还没 ready应该返回-EPROBE_DEFER而不是-ENODEV。前者会让内核稍后重试后者会让这次 probe 直接判死。这个区别在带 PMIC 的板子上体现得特别明显——上电顺序不同regulator 可能比你晚几十毫秒才注册。of_property_read_u32读不到属性时不会清零输出变量如果属性是可选的记得先给默认值再读否则会拿到栈上的随机数然后这个随机数被当成总线速率写进控制器表现就是“同一份代码在有的板子上正常、有的板子上直接不通”。整个过程里我最深的体会是I2C 驱动的复杂度几乎全在边界上——地址位数的边界、返回值语义的边界、生命周期和引用计数的边界、软件时序和电气特性的边界。把i2c_adapter、i2c_client、i2c_driver、i2c_msg这四个结构体的字段含义和调用约定掰开揉碎剩下的都是熟练度问题。真遇到卡住的时候我一般会强迫自己从“猜测代码哪里错”切换到“先用工具确认总线上到底发生了什么”这个习惯比记住任何 API 都管用。
阅读完成 · 觉得有帮助?
咨询建站