尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Linux I2C驱动开发:五大结构体、传输接口与排错实战
写 linux i2c 驱动真正卡住新手的往往不是总线时序而是那几个结构体到底谁管谁以及接口的返回值到底代表什么。上一篇把 i2c 子系统的分层捋了一遍这一篇专门啃结构体与接口i2c_adapter、i2c_algorithm、i2c_client、i2c_driver、i2c_msg 这五个东西的字段含义、生命周期和相互引用关系还有 i2c_transfer、i2c_master_send、i2c_smbus_read_byte_data 这批接口在什么场景下用哪个、出错时该盯着哪个错误码看。如果你刚接上一个传感器或者 EEPROM能编译通过但 probe 不进来、能进来但读回 0xFF、读一次对读两次错那这篇基本就是给你写的。我会从结构体的字段讲起一路讲到控制器侧的 algorithm 怎么实现、上拉电阻怎么算、设备树和板级信息两条注册路径怎么选最后把我在实际项目里踩过的坑整理成一张对照表。代码基于较新的内核主线写法老版本内核的差异我会单独标出来。1. 五个结构体撑起一条 I2C 总线1.1 adapter 与 algorithm一个代表总线一个负责动手struct i2c_adapter是内核眼里的一条物理总线。你插上一条 I2C内核就有一个 adapter 实例用户空间i2cdetect -l列出来的每一行对应的就是一个 adapter。它的关键字段不多但每一个都值得记住struct i2c_adapter { struct module *owner; unsigned int class; const struct i2c_algorithm *algo; void *algo_data; int nr; /* 总线编号i2c-0、i2c-1 的那个数字 */ char name[48]; struct device dev; /* 内嵌设备parent 指向控制器 */ struct rt_mutex bus_lock; int timeout; /* 毫秒 */ int retries; struct i2c_bus_recovery_info *bus_recovery_info; const struct i2c_adapter_quirks *quirks; };nr是总线号它决定了/dev/i2c-N和/sys/bus/i2c/devices/i2c-N里的那个 N。dev.parent一般指向控制器自己的struct device这一层父子关系在电源管理里很要紧——控制器休眠挂在它下面的 adapter 和所有 client 都会跟着走一遍 suspend/resume。bus_lock是给设备驱动用的递归锁i2c_transfer内部会拿它lock是留给 algorithm 实现者自己用的两者别搞混混用会在多设备并发访问时出现死锁。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是核心参数是 msg 数组和数量返回成功处理的 msg 个数。master_xfer_atomic是给原子上下文用的版本——比如系统掉电重启的最后一刻你要给 PMIC 写一个关机序列那时候不能睡眠只能用轮询方式往寄存器里怼。绝大多数控制器驱动不会实现它不实现也没关系核心层会返回-EOPNOTSUPP调用方自己得判断。functionality返回一组I2C_FUNC_*位掩码用来说明这条总线支持什么I2C_FUNC_I2C表示支持裸传输I2C_FUNC_SMBUS_BYTE_DATA表示支持 SMBus 字节数据读写I2C_FUNC_10BIT_ADDR表示支持 10 位地址。设备驱动在 probe 里第一件事就该是i2c_check_functionality(adapter, I2C_FUNC_I2C)不满足直接-EOPNOTSUPP返回别等到传输时才炸。1.2 client 与 driver一次配对的两个人i2c_client代表挂在总线上的一个从设备i2c_driver是驱动这类的设备的代码。前者是硬件描述后者是行为逻辑内核的 driver core 负责把它们撮合到一起。struct i2c_client { unsigned short flags; /* I2C_CLIENT_TEN、I2C_CLIENT_PEC 等 */ unsigned short addr; /* 7 位地址左对齐前的那 7 位 */ char name[I2C_NAME_SIZE]; struct i2c_adapter *adapter; struct device dev; int irq; };addr这里必须强调一句它是 7 位地址不是 datasheet 上那个 8 位写地址。很多传感器手册给的地址形如0x90写/0x91读这是把 7 位地址左移一位后补了读写位。填进i2c_client.addr或者设备树reg属性里的应该是0x48。这个坑每年都要埋掉一批人因为填错的后果不是编译报错而是 probe 时静默超时或者读回全 0xFF。irq字段是可选的中断号很多传感器支持数据就绪中断走request_threaded_irq比轮询省电得多。flags里I2C_CLIENT_TEN表示 10 位地址I2C_CLIENT_PEC表示开启 SMBus 的包错误校验。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 (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol, unsigned int data); const struct i2c_device_id *id_table; struct device_driver driver; /* 内含 name、of_match_table、pm */ };probe的签名在新内核里已经统一成只接收struct i2c_client *。早几年还有probe_new和老的probe(client, id)两套并存现在老签名已经删干净了probe_new也改名回probe。remove的返回值同样在往void收敛中间有一段remove和remove_new并存的过渡期。所以拿一份老驱动往新内核上搬最常见的编译错误就是这两个回调签名对不上——这不是你代码写错了是内核接口在变。alert是 SMBus Alert 协议的钩子只有少数带 ALERT 引脚、支持该协议的器件用得上一般驱动留空。1.3 i2c_msg真正描述发什么的东西一次 I2C 传输在数据结构上就是一组 msg 顺序执行。i2c_msg只有四个字段但它承载的信息决定了总线上波形长什么样struct i2c_msg { __u16 addr; /* 从设备地址 */ __u16 flags; /* I2C_M_RD、I2C_M_TEN、I2C_M_NOSTART 等 */ __u16 len; /* 本段数据长度 */ __u8 *buf; /* 数据缓冲 */ };常用的 flags 得背下来I2C_M_RD这一段是读。没有它就是写。I2C_M_TEN10 位地址模式配合flags里 client 的I2C_CLIENT_TEN。I2C_M_NOSTART这一段不发 START直接接上一段的数据继续。拼接硬件地址、做块传输时会用到。I2C_M_RECV_LENSMBus 块读接收长度由从设备在首字节给出。I2C_M_IGNORE_NAK忽略从设备的 NACK继续发完。慎用一般只在扫描或某些特殊时序场景。理解 msg 的关键在于重复起始这件事。假设你要读一个带 16 位寄存器地址的器件标准动作是先写 2 字节地址不发 STOP再发一次 START然后是读。这个动作在内核里对应的是两个 msg 组成一次i2c_transfer第一个 msg 是写、第二个 msg 是读。如果你写成两次i2c_transfer那两次之间会插入 STOP很多器件就会把状态机复位读出来的只能是垃圾。这是我在调一颗光感时卡了整整一下午的问题后来抓波形才看出来 STOP 的位置不对。1.4 一张表把关系钉死结构体归属层谁创建关键作用常见误用i2c_adapter核心层控制器驱动代表一条物理总线把 adapter 当 client 用i2c_algorithm控制器驱动控制器驱动实现实际传输functionality 返回值乱填i2c_client从设备设备树/板级信息/手动创建描述从设备地址与中断addr 填成 8 位地址i2c_driver从设备驱动模块代码提供 probe/remove匹配表写漏导致不 probei2c_msg传输层调用方临时构造描述一段传输buf 放在无法 DMA 的内存i2c_board_info板级板级代码/设备树向核心注册 clientof_node 与 reg 冲突这张表我建议打印出来贴在显示器边上直到你能不假思索地说出这个字段在哪个结构体里为止。新手 debug 时最常见的一种描述是我发了但我不知道发到哪儿去了本质上就是没把 client发给谁和 msg发什么分清。2. 注册与匹配驱动是怎么被内核叫起来的2.1 加 adapter编号这件事值得单独说控制器驱动在 probe 末尾要把自己的 adapter 挂到子系统上两个接口i2c_add_adapter()和i2c_add_numbered_adapter()。前者让内核动态分配一个总线号后者要求你先把adap-nr指定好内核要么给你这个号要么返回-EBUSY。什么时候必须用带编号的当板子上有外部需求依赖固定总线号时——比如设备树里写死了i2c-2上的某个器件、用户空间脚本要打开/dev/i2c-2、或者有片选式的多路复用器依赖顺序。动态编号的问题在于总线号会随探测顺序变化同一个内核镜像在不同板子上可能给出不同的号这对产线测试脚本是灾难。/* 动态编号 */ ret i2c_add_adapter(adap); /* 固定编号nr 预先填好 */ adap-nr 2; ret i2c_add_numbered_adapter(adap);现在主流的做法是交给设备树控制器节点加上别名的形式核心会自己解析出来并完成编号绑定驱动里直接调devm_i2c_add_adapter()就够了。devm_前缀意味着你不需要在 remove 里手动i2c_del_adapter()设备卸载时自动清理少一处泄漏点。注意i2c_add_adapter失败时不要再调i2c_del_adapter核心内部已经回滚干净了。我见过有驱动在错误分支里补了一刀 delete结果触发了引用计数下溢。2.2 三张匹配表id_table、of_match_table、acpi驱动能不能被叫起来取决于匹配。i2c 子系统里存在三套匹配路径优先级大致是先设备树/ACPI再是i2c_device_id。第一张表是id_table传统的名字匹配static const struct i2c_device_id my_sensor_id[] { { my-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, my_sensor_id);第二张是of_match_table设备树匹配static const struct of_device_id my_sensor_of_match[] { { .compatible vendor,my-sensor }, { } }; MODULE_DEVICE_TABLE(of, my_sensor_of_match);第三张是 ACPI 的acpi_match_table只有做 ACPI 平台比如 x86 笔记本上的触控板、传感器中枢才需要。三张表要一起挂在i2c_driver上static struct i2c_driver my_sensor_driver { .driver { .name my-sensor, .of_match_table my_sensor_of_match, .pm my_sensor_pm_ops, }, .probe my_sensor_probe, .remove my_sensor_remove, .id_table my_sensor_id, }; module_i2c_driver(my_sensor_driver);一个很常见的困惑是设备树里写好了为什么还不 probe。除了compatible拼错这种低级错误最隐蔽的是驱动没编进内核——of_match_table匹配上了但驱动模块本身没被加载或者被编译成了模块而 initramfs 里没带。调这事的时候先看/sys/bus/i2c/devices/下有没有那个 client 目录有说明设备树解析成功、client 已创建问题在驱动侧没有说明设备树那一段本身就没生效。2.3 probe/remove 的签名变迁前面提过签名变化这里展开说清楚因为它是老代码移植时最容易卡的地方。老内核大约 5.x 之前的写法是static int my_probe(struct i2c_client *client, const struct i2c_device_id *id);驱动里要么用这个老签名要么用后来引入的probe_newstatic int my_probe_new(struct i2c_client *client);新内核里probe_new被改名回probe老签名被移除。所以你现在写驱动直接用int (*probe)(struct i2c_client *)就对。为什么要去掉 id 参数因为i2c_client里已经有name设备树匹配上之后i2c_device_id的driver_data也能通过i2c_get_device_id()或i2c_client_get_device_id()拿到多传一个参数意义不大反而让 probe 签名在不同总线上不一致platform、spi 也是类似的收敛过程。remove的变化同步进行方向是改成void (*remove)(struct i2c_client *)。返回int的 remove 在语义上本来就很别扭——驱动卸载失败内核也没法真的把模块留住返回错误只会让引用计数出问题。移植老驱动时的实操建议先看目标内核版本include/linux/i2c.h里i2c_driver的定义别信网上抄来的代码。这一步花两分钟能省掉半小时的编译报错排查。2.4 module_i2c_driver 宏省掉的四行module_i2c_driver(my_sensor_driver);这一行展开之后等价于static int __init my_sensor_init(void) { return i2c_add_driver(my_sensor_driver); } static void __exit my_sensor_exit(void) { i2c_del_driver(my_sensor_driver); } module_init(my_sensor_init); module_exit(my_sensor_exit);它省掉的不只是四行代码还统一了错误处理路径i2c_add_driver失败时宏内部直接把错误码返给模块加载器不用你手写return ret。同样的宏还有module_spi_driver、module_platform_driver思路一致。只有一个场景不能用它驱动需要在内核启动的某个特定阶段之前完成注册比如要做 early console 或者依赖某个 initcall 顺序。那种情况老老实实写subsys_initcall之类。3. 传输接口i2c_transfer 和它的几个语法糖3.1 i2c_transfer 的返回值语义i2c_transfer(adapter, msgs, num)是底层接口返回值只有两种解读方式大于 0成功处理的 msg 个数正常情况等于num。小于 0错误码。注意即使前面几个 msg 成功了、最后一个失败返回的也是那个负错误码不会返回成功了 1 个。这一点和read()那种部分成功的语义不同所以判断时只写if (ret 0)就够别写if (ret ! num)虽然更严格但很多控制器在特殊场景下会返回非 num 的正值比如带 NOSTART 优化时合并了 msg。追求严格的话可以这么写ret i2c_transfer(client-adapter, msgs, 2); if (ret 0) { dev_err(client-dev, transfer failed: %d\n, ret); return ret; } if (ret ! 2) { dev_warn(client-dev, partial transfer: %d/2\n, ret); return -EIO; }顺带说一个细节i2c_transfer内部会根据adapter-retries做重试默认值通常是 3。这个重试是针对 NACK 和超时的对协议错误比如控制器报的仲裁丢失不一定重试。调试时如果看到同一个错误码反复出现又突然成功很可能就是重试在起作用说明你的时序贴边了得回头看电气参数。i2c_transfer会睡眠因为它内部拿bus_lock控制器驱动里也经常msleep等硬件状态。在中断上下文、软中断、自旋锁保护区内调用它是错的。如果确实需要在原子上下文里发数据只能指望master_xfer_atomic或者干脆把工作丢到 workqueue 里延后做。3.2 master_send/recv 到底省了什么i2c_master_send(client, buf, count)和i2c_master_recv(client, buf, count)是两个语法糖内部帮你构造单 msg 然后调i2c_transfer。返回值是实际传输的字节数成功时等于 count失败返回负错误码。它们省掉的是这段样板struct i2c_msg msg; msg.addr client-addr; msg.flags 0; /* 读的话是 I2C_M_RD */ msg.len count; msg.buf buf; ret i2c_transfer(client-adapter, msg, 1);但省掉样板也是有代价的它们只能发单个 msg做不了写地址重复起始读数据这种两段式操作。所以你只要涉及带寄存器地址的器件最终还是要回到手写i2c_msg数组。还有个小差异值得知道i2c_master_send对 0 长度传输的处理在部分控制器上会返回-EOPNOTSUPP配合I2C_AQ_NO_ZERO_LENquirk某些器件用 0 长度写来当探测手段遇到这个 quirk 就得换成实际读一个寄存器来判断设备是否存在。3.3 smbus 接口的适用边界SMBus 是 I2C 的一个子集规定了更严格的时序和固定的数据格式。内核提供了一整套i2c_smbus_*接口s32 i2c_smbus_read_byte(const struct i2c_client *client); s32 i2c_smbus_write_byte(const struct i2c_client *client, u8 value); s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command); s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value); s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command); s32 i2c_smbus_read_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, u8 *values);用不用它们判断标准很实在你的器件是不是 SMBus 兼容的。如果它就是一个写寄存器地址 读一个字节的器件大量传感器、EEPROM、IO 扩展器都是那用i2c_smbus_read_byte_data最省事语义清晰返回值直接就是 8 位数据不用自己拼 msg。如果器件需要 16 位寄存器地址、或者需要原子性的多字节连读读的过程中不允许被打断那就得用裸i2c_transfer。SMBus 协议本身只定义了 8 位 command塞不下 16 位地址。有个隐藏机制要知道如果控制器的algo-smbus_xfer为 NULL核心会调用i2c_smbus_xfer_emulated()把 SMBus 操作翻译成等价的i2c_msg序列再走master_xfer。这意味着哪怕控制器没有硬件 SMBus 支持i2c_smbus_*也能用只是多一层软件模拟的开销而且模拟出来的时序在极少数挑剔的器件上可能有问题。3.4 一次完整的寄存器读写封装实际驱动里我习惯先封两个内部函数把 msg 构造的细节收进去static int my_read_regs(struct i2c_client *client, u8 reg, u8 *buf, int len) { 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 len; msgs[1].buf buf; ret i2c_transfer(client-adapter, msgs, 2); if (ret 0) return ret; return ret 2 ? 0 : -EIO; } static int my_write_regs(struct i2c_client *client, u8 reg, const u8 *buf, int len) { struct i2c_msg msg; u8 *tmp; int ret; tmp kmalloc(len 1, GFP_KERNEL); if (!tmp) return -ENOMEM; tmp[0] reg; memcpy(tmp 1, buf, len); msg.addr client-addr; msg.flags 0; msg.len len 1; msg.buf tmp; ret i2c_transfer(client-adapter, msg, 1); kfree(tmp); if (ret 0) return ret; return ret 1 ? 0 : -EIO; }这里有几个刻意的设计。读函数用两个 msg 组成一次 transfer保证寄存器地址写入和数据读取之间是重复起始而不是 STOP绝大多数器件的状态机要求如此。写函数把寄存器和数据拼进一块连续的 buffer因为 I2C 写没有先写地址再写数据的停顿本来就是一串字节。这块 buffer 用kmalloc而不是栈上数组是为了避开 DMA 的安全问题——后面会细说。如果驱动里寄存器操作非常多别自己维护这套封装直接用 regmapstatic const struct regmap_config my_regmap_config { .reg_bits 8, .val_bits 8, .max_register 0x7F, .cache_type REGCACHE_RBTREE, }; regmap devm_regmap_init_i2c(client, my_regmap_config);regmap 的好处不止是省代码它自带寄存器缓存能减少总线访问次数它会把读写统一走一套接口换总线从 I2C 换到 SPI时驱动逻辑几乎不用动它还处理了位域操作regmap_update_bits读改写一条龙。代价是启动时多了一层间接调用对时序极敏感的场景要评估一下。4. 控制器侧algorithm 实现与电气参数4.1 master_xfer 的骨架该怎么搭写从设备驱动的人多写控制器驱动的人少但两者都得看懂master_xfer的骨架不然从设备出问题时你根本不知道该怀疑哪一层。一个最小可用的master_xfer大致是这个形状static int my_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct my_i2c *i2c i2c_get_adapdata(adap); int i, ret; if (!num) return 0; ret my_i2c_hw_init(i2c); if (ret) return ret; for (i 0; i num; i) { struct i2c_msg *msg msgs[i]; if (msg-flags I2C_M_TEN) return -EOPNOTSUPP; if (msg-len 0 (adap-quirks adap-quirks-flags I2C_AQ_NO_ZERO_LEN)) return -EOPNOTSUPP; ret (msg-flags I2C_M_RD) ? my_i2c_read_msg(i2c, msg) : my_i2c_write_msg(i2c, msg); if (ret) return ret; } return num; }几个要点。第一参数检查要放在最前面10 位地址、0 长度这类不支持的情况早点返回-EOPNOTSUPP比传到硬件里再报莫名错误强。第二每个 msg 之间是否发 STOP 由硬件和I2C_M_NOSTART决定简单实现可以每个 msg 都发 STOP但那样就破坏了重复起始的语义带寄存器的器件会读错——所以正经实现必须支持把多个 msg 串成一次连续传输。第三返回num表示全部成功中途失败返回负错误码不要返回成功了 i 个。quirks那个字段值得单独看一眼它用来描述控制器的能力缺陷比如I2C_AQ_NO_ZERO_LEN不支持 0 长度传输、I2C_AQ_NO_REP_START不支持重复起始、I2C_AQ_COMB_WRITE_THEN_READ限制组合传输的字节数。核心层依赖这些标志来提前拒绝不合法的请求或者用软件方式拆解。如果你的控制器有这些毛病却没声明上层驱动会遇到很多看起来毫无道理的失败。4.2 上拉电阻和总线电容怎么算这一节是硬件内容但软件工程师也得会算因为大量能通信但偶发失败的问题根源在这里。I2C 是开漏总线SDA/SCL 靠外部上拉电阻拉高。上拉电阻的取值被两个约束夹住太大则上升沿太慢太小的则灌电流超限。上限由上升时间决定Rp(max) tr / (0.8473 × Cb)tr是允许的最大上升时间标准模式100kHz是 1000ns快速模式400kHz是 300ns。Cb是总线总电容包括 PCB 走线大约 1pF/cm、每个器件的引脚电容典型 5~10pF和连接器。假设一条板子上挂 4 个器件、走线 10cm粗算 Cb ≈ 4×8pF 10×1pF ≈ 42pF留点余量按 100pF 算。在快速模式下Rp(max) 300ns / (0.8473 × 100pF) ≈ 3.5kΩ下限由输出低电平时的灌电流决定Rp(min) (VDD - VOL(max)) / IOL3.3V 供电、VOL最大 0.4V、IOL取 3mA得到(3.3 - 0.4) / 0.003 ≈ 967Ω。所以取值区间是 1k ~ 3.5k工程上常用 1.8k 或 2.2k。很多开发板默认贴 4.7k这在中低速、短走线、少器件的情况下能用一旦把总线拉到 400kHz 并挂满设备上升沿就可能不够陡表现为通信偶尔 NACK 或读回数据错位。这时候先别怀疑驱动用示波器看上升时间超过规范值就减上拉。还有一个常被忽略的点总线上所有上拉是并联的。控制器板载一对 4.7k你外接模块上又带一对 4.7k并联后等效 2.35k灌电流翻倍。有些模块还带 10k并联之后变成 3.2k反而更接近理想值。所以接了模块之后通信变差第一件事是数一数板子上到底有几个上拉。4.3 设备树与 board_info 两条注册路径从设备的描述有两条路设备树和i2c_board_info。设备树是现在的主流在控制器节点下挂子节点i2c1 { clock-frequency 400000; status okay; sensor48 { compatible vendor,my-sensor; reg 0x48; interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; }; };reg就是 7 位地址。clock-frequency写在控制器节点上作用于整条总线所有挂在上面的器件共享同一个速率。板级信息是给没有设备树的老平台或者需要动态添加设备的场景用的static struct i2c_board_info my_sensor_info { I2C_BOARD_INFO(my-sensor, 0x48), .irq GPIO_TO_IRQ(12), }; i2c_register_board_info(1, my_sensor_info, 1);i2c_register_board_info的第一个参数是总线号。这个函数有个限制它只能在启动早期调用通常是板级初始化阶段运行期动态加设备得用i2c_new_client_device()struct i2c_client *cli; struct i2c_board_info info { I2C_BOARD_INFO(my-sensor, 0x48), }; cli i2c_new_client_device(adapter, info); if (IS_ERR(cli)) return PTR_ERR(cli);注意这个函数返回的是ERR_PTR风格的指针得用IS_ERR判断而且用完要i2c_unregister_device()。老代码里常见的i2c_new_device已经改成i2c_new_client_devicei2c_new_dummy改成了i2c_new_dummy_device——改名是为了统一错误返回风格老的i2c_new_device在失败时返回 NULL新接口返回 ERR_PTR混着用会漏判。5. 常见问题与排查实录5.1 错误码对照表I2C 驱动的错误码就那么几个但每个背后的原因差别很大。我整理了一张表按我遇到的频率排序错误码字面含义实际最常见原因排查动作-ENXIO设备或地址不存在从设备没应答地址填错器件没供电用 i2cdetect 扫量器件 VCC-EREMOTEIO远端 IO 错误传输中途 NACK多数是时序或上拉问题示波器看波形减上拉-ETIMEDOUT超时时钟被从设备拉低不放clock stretching或控制器没响应检查是否有器件卡住 SCL-EIO通用 IO 错误控制器报错常见于 FIFO 溢出或状态机异常看控制器寄存器状态-EOPNOTSUPP不支持调了控制器没实现的功能比如原子传输查 functionality 返回值-EBUSY忙总线号被占用或多路复用器未选中通道查 adapter-nr 冲突-EPROBE_DEFER延后探测依赖的资源时钟、GPIO、稳压器还没就绪检查 probe 里资源获取顺序-ENXIO和-EREMOTEIO这两个最容易混。前者是根本没听到应答后者是听到了应答但后面出了问题。如果你在 probe 里第一次读寄存器就拿到 -ENXIO八成是地址错或者器件没上电如果是通信一段时间之后偶发 -EREMOTEIO那基本是电气问题。5.2 用 sysfs 和 i2c-tools 定位定位 I2C 问题我基本按这个顺序走从软到硬逐层排除第一步看总线和设备有没有被枚举出来ls /sys/bus/i2c/devices/ cat /sys/bus/i2c/devices/i2c-1/name如果i2c-1存在但1-0048这样的 client 目录不存在说明设备树那段没生效问题在 DTS 或匹配表。如果 client 目录存在但没有绑定驱动看看driver符号链接是不是空的。第二步用 i2c-tools 做裸访问绕开驱动i2cdetect -l # 列出所有总线 i2cdetect -y -r 1 # 扫描总线 1-r 用读方式探测 i2cget -y 1 0x48 0x00 # 读 0x48 的 0x00 寄存器 i2cset -y 1 0x48 0x01 0x80 # 写 i2cdump -y 1 0x48 # 整片 dumpi2cdetect -y -r里的-r有讲究默认的探测方式是用SMBUS_QUICK写一个字节有些器件被写一下会进入异常状态用读方式-r更安全。扫描结果显示UU表示该地址已被驱动占用不代表器件不存在。第三步如果 i2c-tools 能通而驱动不通问题在你自己的驱动里如果 i2c-tools 也不通问题在硬件或控制器配置跟从设备驱动没关系。这条分界线非常重要能省掉大量在错误方向上瞎改的时间。想要更细的追踪可以打开内核的动态调试echo file i2c-core-base.c p /sys/kernel/debug/dynamic_debug/control dmesg -w或者直接抓 I2C 层的 tracepointecho 1 /sys/kernel/debug/tracing/events/i2c/enable cat /sys/kernel/debug/tracing/trace_pipetracepoint 会把每一次 msg 的地址、方向、长度都打出来一眼能看出来你发的是写还是读、地址对不对。5.3 我踩过的坑坑一buf 用栈上变量DMA 模式下读到垃圾。很多控制器走 DMA要求 buffer 在物理连续的内存里。栈上的数组虽然虚拟地址连续但物理页不保证DMA 引擎会读到错误位置。表现是单次读没问题连续读偶尔错特别难查。解决方式是kmalloc分配 buffer或者给 msg 打上I2C_M_DMA_SAFE标志前提是控制器支持并正确实现。更稳的做法是在 probe 里预分配一块驱动私有的 DMA 安全 buffer反复复用。坑二在 probe 里用devm_分配的 buffer 传给了异步任务。devm_kzalloc的生命周期绑定在设备上设备卸载时自动释放。如果你的 buffer 被传给了 workqueue 或者定时器而设备在任务执行前被卸载了就是 use-after-free。这类问题往往只在模块反复加载卸载的压力测试里才暴露。坑三把 adapter 当成长期持有的对象。用i2c_get_adapter(nr)拿到 adapter 之后必须配对的i2c_put_adapter()否则总线模块永远卸载不掉。我见过一个驱动在 probe 失败分支里漏了 put结果 rmmod 控制器模块时一直报-EBUSY查了半天以为是设备还在用。坑四寄存器地址的位宽搞错。有些器件的寄存器地址是 16 位大端手册上写0x0100实际传输时要发0x01, 0x00两个字节。如果你按 8 位发器件会把它当成两个独立的 8 位命令读出来的数据永远对不上。判断方法很朴素看手册的时序图里地址后面跟着几个字节。坑五多设备共总线时的并发。两个驱动同时访问同一条总线如果各自都用i2c_transfer核心的bus_lock会保证互斥不会串包。但如果你在驱动里做了先读 A 再读 B中间不允许插入的复合操作两次i2c_transfer之间是可能被别人插进来的器件的内部状态机就可能错乱。这种情况要用i2c_lock_bus(adapter, I2C_LOCK_SEGMENT)把一组操作整体锁住或者干脆合并成一次 transfer。坑六clock stretching 被控制器忽略。有些控制器默认不支持从设备拉低 SCL 来延长时钟或者超时设得太短。表现是低速器件比如某些温湿度传感器在做转换时会拉低 SCL在大批量读取时偶发-ETIMEDOUT。解决要么调大adapter-timeout要么在控制器驱动里打开 stretching 支持。坑七忽略了器件的上电时序。有些器件上电后需要几十毫秒的内部初始化这段时间内不应答任何访问。驱动 probe 时如果立刻去读 ID 寄存器就会拿到 -ENXIO。标准做法是在 probe 开头加一个msleep或者在设备树里加reset-gpios和reset-delays-us把复位动作做实。提示调 I2C 时先把CONFIG_I2C_DEBUG_CORE和CONFIG_I2C_DEBUG_ALGO打开日志会详细很多。调试完记得关掉这些选项在量产内核里会拖慢传输。最后分享一个我个人的习惯新建一个 i2c 驱动时probe 的第一版不写任何业务逻辑只做三件事——打印 client 的 addr、adapter 的 nr 和 name、调用一次i2c_check_functionality打印结果然后读一个已知的 ID 寄存器打印出来。这四行日志能把设备在不在、总线对不对、能力够不够、通信通不通四个问题一次性回答完。很多所谓的疑难问题卡在这个最基础的验证环节上先把它做实后面的事情都是顺的。
RELATED

相关推荐

ADHD式大脑自我管理指南:从注意力调节到任务启动的实操系统

ADHD式大脑自我管理指南:从注意力调节到任务启动的实操系统

1. 从“i-have-adhd”这个标题说起:一个被严重低估的自我管理切口第一次看到“i-have-adhd”这个标题,我脑子里蹦出来的不是医学名词,而是一类非常具体的人:他们能在半小时内把一个复杂方案想得天花乱坠,却常常在“把袜…

📅 2026/10/26 11:41:20
SaaS点餐平台高并发架构设计与实战优化

SaaS点餐平台高并发架构设计与实战优化

1. SaaS点餐平台行业现状与核心挑战中午12点,某连锁餐厅的前台又排起了长队。服务员小王手忙脚乱地记录着顾客点单,后厨的订单打印机不断吐出小票,厨师老李对着堆积如山的订单直摇头。这样的场景正在倒逼餐饮行业进行数字化转型,而…

📅 2026/10/26 11:41:20
机器人自主完成胆囊切除术的技术突破与应用前景

机器人自主完成胆囊切除术的技术突破与应用前景

1. 项目背景与核心突破在微创外科手术领域,胆囊切除术(cholecystectomy)是最常见的腹腔镜手术之一。传统手术中,医生需要通过小切口插入器械,在视频引导下完成胆囊管和胆囊动脉的夹闭、切割操作。这个被称为"clip…

📅 2026/10/26 11:41:20
MORE NEWS

更多资讯

📰

实测Claude中文能力跃升:从翻译腔到地道中文的技术解析

最近圈子里聊得最多的一个话题,就是Claude突然就会说中文了。这个"突然"当然不是毫无来由,但对普通用户来说,体感确实很直接:以前拿它处理中文内容,总带着一股"翻译腔",语法挑不出毛病…

📰

如何对LLM大型语言模型进行评估与基准测试:从性能指标到推理能力的落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

DeepSeek-R1模型选型与提示语工程实践指南

简介:这份PDF文档围绕国产开源通用人工智能推理模型DeepSeek展开,面向AI研发工程师、学者及希望提升效率的个人开发者,帮助读者建立从基础认知到进阶应用的完整路径。内容从DeepSeek与开源推理模型DeepSeek-R1的定位切入,梳理智能…

📰

AI赋能知识产权运营:四类智能体破解科技成果转化供需匹配难题

简介:围绕AI赋能的精准知识产权运营供需匹配平台,这份Word文档面向高校科研机构、技术转移从业者、企业创新负责人及政府科技管理部门,聚焦成果转化中信息不对称、资源分散、合作不畅等痛点。全文以单份docx交付,压缩包约38KB&…

📰

UG/NX二次开发实战弹药库:从环境搭建到UF函数调试

简介:本资源是面向UG(Unigraphics)初学者与进阶开发者的系统性二次开发学习合集,聚焦C API与GRIP脚本双路径实践,助力工程师在航空航天、汽车制造等工业领域实现定制化建模、自动化任务与界面扩展。压缩包共620个文件&…

📰

STP、RSTP、MSTP演进与实战:从环路告警到负载分担

1. 从一次环路告警说起:为什么需要MSTP很多刚接触交换网络的朋友,第一次看到机房里所有交换机的指示灯同时疯狂闪烁、CPU占用率飙到百分之百、业务大面积中断的场景,大概率是遇到了二层环路。我印象很深的一次,某办公区临时加了一…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬