Linux I2C驱动核心结构体、传输接口与probe排查
这篇接着上一篇的内容往下走。上一回把I2C子系统的分层和文件分布过了一遍这次专门啃硬骨头i2c_adapter、i2c_algorithm、i2c_client、i2c_driver、i2c_msg 这几个结构体以及 i2c_transfer、i2c_master_send、i2c_master_recv、i2c_smbus_这些接口*。说实话我接触过的绝大多数I2C驱动问题表面看是“时序不对”“设备没响应”往下挖两层基本都落在这些结构体的某个字段填错了或者某个接口的返回值没检查。所以这篇不打算写成内核文档的翻译稿而是按照我自己写驱动、调板子的顺序把这几个结构体拆开看把接口的坑一个个标出来。刚入门的读者能照着搭出一个能跑的驱动骨架有经验的朋友可以重点看参数计算、匹配逻辑和排查那几节里面的细节大多是文档里不会写、只在调试现场才会撞上的东西。1. 先把I2C子系统的“人物关系”理清楚1.1 四个核心结构体各自扮演什么角色我习惯把I2C子系统当成一家快递公司来理解。i2c_adapter是那条运输线路也就是控制器本身负责把包裹送到目的地i2c_algorithm是具体的运输方式是卡车还是飞机它决定了“包裹怎么送出去”这个最底层的动作i2c_client是收件人代表挂在这条线上的某一颗从设备芯片地址就是门牌号i2c_driver是派件员掌握着处理这类包裹的业务逻辑。包裹本身是i2c_msg里面装着地址、方向、数据和长度。这个类比不是玩文字游戏它直接对应代码里谁注册谁、谁引用谁。i2c_adapter里有一个algo指针指向i2c_algorithmi2c_client里有一个adapter指针指回它挂靠的那条线路i2c_driver的probe回调收到的是i2c_client *和i2c_device_id *。写驱动的时候如果你清楚自己此刻在操作哪一层就不会出现“在probe里直接写寄存器地址去操作控制器”这种越界操作。再补一层关系内核启动后i2c_client的来源有两条路。老平台走板级文件里的i2c_board_info由i2c_register_board_info在启动早期登记等adapter注册时统一实例化新平台走设备树of_i2c_register_devices遍历adapter节点下的子节点为每个子节点调用i2c_new_client_device生成client。这两条路的终点是同一个函数所以无论你用的是哪种方式probe里看到的东西是一样的。结构体所在层级谁创建核心职责i2c_adapter控制器抽象控制器驱动持有锁、超时、重试、算法指针i2c_algorithm硬件操作控制器驱动实现 master_xfer / smbus_xferi2c_client从设备实例设备树或板级信息保存7位地址、所属adapter、irqi2c_driver设备驱动从设备驱动匹配表、probe、removei2c_msg单次传输描述调用者地址、标志、缓冲区、长度1.2 一条数据从用户态到SCL波形的完整路径很多人写驱动时只看自己那一层结果一出问题就不知道往上往下该找谁。把整条链路走一遍心里就有谱了。用户态用i2c-dev接口发起一次I2C_RDWRioctl内核里i2cdev_ioctl_rdwr把用户传进来的数组拷成内核态的i2c_msg数组然后调用i2c_transfer。i2c_transfer拿到adapter检查algo-master_xfer是否存在加bus_lock再调__i2c_transfer里面按需切换mux通道最后把消息数组交给master_xfer。控制器驱动的master_xfer才是真正碰寄存器的地方把每条msg拆成起始位、地址字节、读写位、数据字节、ACK位、停止位写进控制器的FIFO或者命令寄存器然后要么忙等要么等中断要么等DMA完成。所以当你说“I2C不通”的时候至少要问自己三个问题msg构造对了吗传输层返回什么错误波形到底出来了没有这三个问题分别对应驱动作者、i2c核心、硬件三个责任方排查时从中间往外扩最快。1.3 为什么Linux要把“设备”和“驱动”拆成两半这个问题我被问过不止一次。答案在总线模型的匹配机制上i2c_client描述“这里有个什么东西”i2c_driver描述“我会处理什么东西”两者通过匹配表对上号才调probe。这样同一份驱动代码可以服务多块板子板子上有没有这颗芯片、地址是多少全部由设备树或板级信息决定驱动本身不需要改动。代价是匹配失败时现象很隐蔽驱动加载了lsmod能看到但probe就是不进也没有报错。因为对内核来说“没匹配上”是正常状态总线上随时可能有它不认识的设备。这一点必须提前建立预期否则第一次遇到会怀疑人生。后面第6节会把匹配失败的几种典型原因列全。2. i2c_adapter与i2c_algorithm控制器侧怎么搭骨架2.1 i2c_adapter里哪些字段必须由你来填i2c_adapter是个大结构体字段多到第一次看会有点晕但真正需要驱动作者关心的就那么几个。owner填THIS_MODULE保证adapter在使用时模块不会被卸载algo指向你的算法实现algo_data是私有数据指针通常塞你的自定义结构体master_xfer里adap-algo_data取回来就能拿到寄存器基址、时钟、FIFO深度这些东西nr是总线编号-1表示让内核动态分配name会出现在/sys/bus/i2c/devices/i2c-N/name里起个能看懂的名字调试时省事timeout和retries决定单次传输的超时和重试次数。dev字段必须初始化i2c_add_adapter内部会调device_register如果dev.parent没设设备树里adapter节点的解析、电源管理、sysfs路径都会出问题。我见过有人手工分配adapter结构体却忘了adap-dev.parent pdev-dev结果probe能过但设备树子节点一个都没被实例化查了半天以为是of_i2c_register_devices没被调用实际上是parent没设导致of_node没继承下来。还有一个容易被忽略的字段是lock_ops。默认走i2c_adapter自带的忙等锁在原子上下文或者中断里发传输时可能需要换成i2c_lock_ops里基于raw spinlock的实现。是否要动它取决于你的master_xfer_atomic是否实现了普通场景不用管。2.2 master_xfer的返回值和实现要点master_xfer的签名是int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)。返回值语义必须记牢成功时返回实际处理的msg条数失败返回负的错误码。注意不是返回字节数也不是返回0。返回0会被上层当成“一条都没传”i2c_transfer会把它当错误处理。实现上有几个固定套路。先用i2c_check_functionality之类的逻辑判断本次传输是否被硬件支持比如10位地址、SMBus块传输。然后遍历msgs每条msg单独起一次传输判断msgs[i].flags I2C_M_RD决定方向把msgs[i].addr左移一位拼上读写位组成地址字节。数据长度放在msgs[i].len缓冲区在msgs[i].buf。中间的重复起始位要靠I2C_M_NOSTART标志判断但很多控制器硬件并不支持任意位置插入重复起始这时候就要在驱动里手动把多条msg合并处理。注意master_xfer里面不能睡眠等待太久也不能调用可能睡眠的接口去做与本次传输无关的事。控制器驱动可能被上层在持锁状态下调用把大块内存分配、文件操作之类的东西塞进来轻则触发调度告警重则死锁。忙等和中断的选择也要提前想清楚。传输长度短、频率高的场景比如读传感器一个寄存器用忙等加超时判断最省事几十微秒就过去了。传输长度长或者控制器FIFO很浅就得走中断加完成量。我一般会给忙等设一个上限超过就转成等待完成量避免在慢速设备上把CPU占死。2.3 注册adapter的两种方式与编号坑注册接口有两个i2c_add_adapter和i2c_add_numbered_adapter。前者让内核从空闲编号里挑一个后者要求你指定adap-nr如果这个号已经被占用就返回-EBUSY。什么时候必须用带编号的当设备树里有i2c-N这样的别名或者板级代码里写死了adapter_nr的时候。不带编号的注册在单控制器板子上没问题多控制器场景下编号顺序取决于probe顺序可能这次是i2c-0和i2c-1下次驱动加载顺序变了就互换用户态脚本里的i2cdetect -y 0就指错了设备。我现在的习惯是只要板子有多个I2C控制器一律走i2c_add_numbered_adapter用of_alias_get_id从设备树别名里取号稳定可预期。卸载路径对应i2c_del_adapter它会先标记adapter为删除状态再解绑所有client。这里有个时序问题如果client的remove回调里还要通过i2c_transfer访问设备必须在i2c_del_adapter之前完成否则传输会直接失败。比较稳妥的做法是把设备侧的清理放在remove里adapter的注销放在模块exit的最后一步。3. i2c_client与i2c_driver设备与驱动怎么配对3.1 i2c_client的地址到底是7位还是8位client-addr存的是7位地址不含读写位。这是新手最容易翻车的地方。数据手册上写0x50通常就是7位地址写0xA0是8位写地址写0xA1是8位读地址。设备树里reg 0x50填7位代码里client-addr也是7位拼地址字节的时候左移一位再或上方向位。判断方法很直接合法的7位地址范围是0x00到0x7F超过0x7F的值一定不是7位地址。设备树里写reg 0xA0会被内核当成非法地址通常会看到类似于“invalid reg”的报错或者设备创建失败。把0x50写成0xA0导致的另一个典型现象是i2cdetect能扫到0x50但驱动probe之后读写全失败因为实际发出的地址和芯片对不上。client-flags里还藏着不少信息比如I2C_CLIENT_TEN表示10位地址I2C_CLIENT_PEC表示SMBus包错误校验使能I2C_CLIENT_WAKE表示可以唤醒系统。irq字段由设备树里的interrupts属性或者板级信息填充client-irq是0表示没有中断线。3.2 i2c_driver的匹配表怎么写才不白忙活i2c_driver里放匹配信息的地方有两个id_table指向的i2c_device_id数组和driver.of_match_table指向的of_device_id数组。匹配优先级是of_match_table 优先其次是 id_table最后才是按 name 字段的模糊匹配。这个顺序决定了你只有一条表写对是不够的如果设备树写了compatible而驱动只提供了id_table但内核走的是设备树匹配路径那就可能匹配不上。实际写法我一般两个表都填i2c_device_id里的名字用来支持老平台和模块别名of_device_id里的compatible必须和设备树节点一字不差包括厂商前缀。字符串比较是精确匹配大小写敏感多一个减号少一个逗号都不行。static const struct i2c_device_id demo_id[] { { demo-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, demo_id); static const struct of_device_id demo_of_match[] { { .compatible vendor,demo-sensor }, { } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct i2c_driver demo_driver { .driver { .name demo-sensor, .of_match_table demo_of_match, .pm demo_pm_ops, }, .probe demo_probe, .remove demo_remove, .id_table demo_id, }; module_i2c_driver(demo_driver);module_i2c_driver这个宏把i2c_add_driver和i2c_del_driver包在一起省掉样板代码。用它的前提是初始化只在注册时做没有额外的全局资源要在模块加载时分阶段准备绝大多数传感器驱动都满足。3.3 probe时序里该做什么、不该做什么probe进来时client已经创建好、地址已经确定、设备树节点可以通过client-dev.of_node拿到但设备的供电、复位引脚、时钟这些外部条件不一定就绪。所以probe的第一件事通常是把这些资源拿全devm_regulator_get、devm_gpiod_get_optional、devm_clk_get_optional。用devm_前缀的好处是失败路径和remove路径都不用你手动释放内核会自动回收。第二件事是先做一次“存在性探测”。读一下chip id寄存器确认芯片真的在。这一步的价值在于把“接错地址”“芯片没焊好”“供电没上来”这类问题在probe阶段就暴露出来而不是等到用户态调用时才报一堆超时。读chip id失败的时候用dev_err_probe输出错误它会把-EPROBE_DEFER转成静默的延迟探测避免在依赖资源还没就绪时刷屏。注意probe里不要做长时间阻塞的操作比如轮询等待芯片内部自检完成。如果确实需要等待用msleep分段等并且设置一个合理的上限。probe卡住会拖慢整个总线的设备注册严重的时候影响启动。第三件事是注册字符设备、hwmon、input、iio 之类的对外接口以及申请中断。中断申请一般放在最后因为中断处理函数里可能马上要读寄存器此时设备初始化必须已经完成。中断处理里用i2c_transfer时要特别小心很多adapter在中断上下文里会退回忙等模式如果传输出错或者设备没响应超时时间会原样生效容易触发软锁。稳妥的写法是用IRQF_ONESHOT加线程化中断把I2C传输放到线程上下文里做。remove回调的顺序反过来先释放对外接口再关中断最后关电源同时记得pm_runtime的状态也要归位。用devm_管理之后这段代码能精简不少但跟硬件状态相关的、必须立刻生效的动作还是要显式写出来。4. i2c_msg与传输接口数据到底怎么发出去4.1 i2c_msg的四个字段和常见组合套路i2c_msg只有四个字段但组合方式决定了通信能不能成。addr是7位从机地址flags是标志位len是本次传输的字节数buf是数据缓冲区指针。flags里常用的有I2C_M_RD表示读方向I2C_M_NOSTART表示不发起始位直接续传I2C_M_IGNORE_NAK表示忽略从机不应答I2C_M_STOP表示传输结束后发停止位。后三个都属于“一般情况下别用”的选项用了就要清楚自己在绕开什么。最典型的组合是“写寄存器地址再读数据”这个操作在很多芯片上要求中间有重复起始位而不是停止位。写法是把两条i2c_msg放在一个数组里第一条写方向、长度为1、内容是从机寄存器地址第二条读方向、长度为N、缓冲区是接收区然后一次i2c_transfer传2条。控制器驱动如果支持重复起始两条msg之间就不会插入停止位如果不支持它可能会退化成“停止起始”的时序有些芯片因此拒绝响应。static int demo_read_regs(struct i2c_client *client, u8 reg, u8 *buf, int len) { int ret; struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, .len len, .buf buf, }, }; ret i2c_transfer(client-adapter, msgs, ARRAY_SIZE(msgs)); if (ret 0) return ret; if (ret ! ARRAY_SIZE(msgs)) return -EIO; return 0; }注意最后那个判断。i2c_transfer成功时返回处理的msg条数两条都成返回2。如果只返回1说明第一条成了第二条没成这在时序上有意义必须当错误处理不能只看“非负”。这段代码我几乎在每个驱动里都会写一遍虽然啰嗦但从没想过用更短的写法替代因为错误路径清晰比代码短重要。4.2 i2c_transfer的锁语义和不能踩的坑i2c_transfer内部会获取adapter-bus_lock把整组msg作为一个原子操作发出去。这意味着两件事第一不要把两段逻辑上无关的传输拼进同一个数组会白白占用总线锁第二在已经持有某些锁的情况下调用它要检查锁的嵌套关系避免形成环路。i2c_transfer不能在中断上下文里直接调用这一点经常被误解。虽然有些控制器驱动实现了master_xfer_atomic来支持原子上下文但前提是你明确知道自己的传输走的是这条路。稳妥做法永远是把传输挪到线程上下文或者工作队列里。返回值处理有几个固定的错误码要认-ENXIO通常表示地址没有应答或者从机不存在-ETIMEDOUT表示传输超时-EAGAIN在某些驱动里表示总线仲裁失败可重试-EOPNOTSUPP多半是功能不支持比如用了SMBus但控制器没实现。看到-ENXIO先去量波形确认地址发出去没有看到-ETIMEDOUT先查上拉电阻和时钟拉伸。4.3 封装接口与SMBus怎么选i2c_master_send和i2c_master_recv是最简单的封装前者发一段连续数据后者收一段连续数据返回值是实际发送或接收的字节数失败返回负数。它们内部就是构造一条i2c_msg调i2c_transfer适合“一次性发一包”的场景比如给显示屏刷一段缓冲区。SMBus接口是另一套体系包括i2c_smbus_read_byte_data、i2c_smbus_write_byte_data、i2c_smbus_read_i2c_block_data等。它们的便利之处在于把“写命令字节读数据”这个套路封装好了调用者不用自己拼msg数组。但代价是每次调用都会重新获取总线锁如果连续读多个寄存器开销比一次i2c_transfer传多条msg大得多。而且SMBus有自己的一套协议限制比如块读的最大长度通常是32字节超过就得分批。我的选择标准很简单寄存器级的小数据量访问用SMBus接口代码短、可读性好需要保证时序连续性的多段传输比如先写地址再读一长串数据或者反过来连续写多个寄存器不能被打断就用i2c_transfer自己拼msg。另外SMBus接口默认要求控制器实现smbus_xfer如果只有master_xfer内核会用软件模拟的方式实现SMBus功能没问题但效率会低一些functionality里是否置了I2C_FUNC_SMBUS_*位可以提前判断。接口适用场景返回值语义是否自行加锁i2c_transfer多条msg组合、需要原子性msg条数 / 负错误码是i2c_master_send连续写一段字节数 / 负错误码是i2c_master_recv连续读一段字节数 / 负错误码是i2c_smbus_read_byte_data读单寄存器数据或负错误码是每次调用i2c_smbus_read_i2c_block_data块读字节数 / 负错误码是每次调用5. 设备是怎么“长”出来的设备树与实例化路径5.1 设备树节点的最小可用写法设备树里I2C从设备节点挂在控制器节点下面reg填7位地址compatible和驱动里的of_device_id对应。一个能用的最小节点是这样的i2c1 { status okay; clock-frequency 400000; demo_sensor: sensor50 { compatible vendor,demo-sensor; reg 0x50; interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; vdd-supply reg_3v3; reset-gpios gpio1 13 GPIO_ACTIVE_LOW; }; };clock-frequency决定总线速率不填默认100kHz。400kHz是快速模式对布线和上拉电阻的要求比100kHz高不少板子上拉阻值偏大或者走线偏长的时候400kHz很容易出问题调试阶段建议先用100kHz跑通再往上提。节点名里的sensor50只是个标签真正起作用的是reg属性。后面的地址要和reg一致虽然内核不强制校验但写错了读代码的人会误解工具链也可能给出警告。5.2 i2c_new_client_device与i2c_new_device的区别用户态或者内核模块里想手工创建client会碰到两个函数。i2c_new_device是老接口失败时返回NULL需要看errno才知道原因。i2c_new_client_device是新接口失败时返回ERR_PTR可以直接用IS_ERR和PTR_ERR取错误码。新代码统一用后者错误处理更清晰。手工创建client的典型场景是设备树里没法描述某个设备的初始化顺序或者需要在运行时根据探测结果动态挂设备。这时候先填一个i2c_board_info结构体把type设成和驱动id_table里一致的名字addr设成7位地址of_node指向对应的设备树节点然后调i2c_new_client_device。注意创建出来的client要记得在不用的时候i2c_unregister_device否则会在总线上留下一个悬空实例。还有一个细节i2c_new_scanned_device用于地址探测它会先用i2c_default_probe去试探地址上有没有设备应答应答了才创建client。地址探测本身有风险某些芯片在被随意读一下之后会进入异常状态所以除非你确认目标芯片不会因为误读而出问题否则不推荐依赖扫描式探测。5.3 匹配优先级与多设备共存前面提过匹配顺序是of_match_table优先但具体流程还涉及i2c_device_match里的几段判断。设备树路径下of_driver_match_device先比对compatible匹配上就调probe没有设备树节点或者compatible不匹配才退到i2c_match_id用 id_table 比名字最后还有一层按driver-name的字符串比较。这意味着如果同一个驱动既要支持设备树又要支持老平台可以只写of_match_table加id_tablename那层基本用不上。同一条总线上挂多个同类设备时设备树里每个节点有自己的地址驱动probe会被调用多次每次拿到不同的client。驱动内部要避免用全局变量保存“设备状态”状态应该挂在i2c_set_clientdata里probe里set、remove里get各个实例互不干扰。这个坑我在早期代码里踩过两个相同传感器接在同一条总线上第二个设备的读数覆盖了第一个查了半天才发现是静态变量。6. 常见问题与排查技巧实录6.1 probe不进的排查顺序这是最高频的问题。我的排查顺序是固定的按代价从低到高走。第一步确认驱动真的加载了lsmod或者dmesg | grep 驱动名模块加载失败会在dmesg里留痕。第二步看compatible字符串有没有拼错直接diff设备树源文件和驱动里的字符串比肉眼比对可靠。第三步看总线上有没有这个clientls /sys/bus/i2c/devices/下面应该有1-0050这样的目录目录名格式是“总线号-地址”。如果没有说明设备树节点没被解析检查adapter节点的status是不是okay以及of_i2c_register_devices有没有被调用。第四步如果client存在但probe没进说明匹配失败。把驱动编译进内核而不是模块看启动日志里有没有匹配相关的提示或者打开I2C核心的调试输出加内核参数dyndbgfile drivers/i2c/* p能看到匹配过程的细节。第五步如果probe进了但返回错误dev_err_probe输出的错误码就是方向-EPROBE_DEFER表示依赖资源没就绪-ENODEV表示探测失败其他错误码按字面意思查。现象最可能原因快速验证模块没加载依赖未满足或符号冲突dmesg 看加载日志client 不存在设备树节点未使能或解析失败ls /sys/bus/i2c/devicesclient 存在但无probecompatible 不匹配对比设备树与驱动字符串probe 返回 -EPROBE_DEFER电源/时钟/GPIO 未就绪检查 regulator 与 pinctrlprobe 返回 -ENXIO地址无应答i2cdetect 扫地址6.2 返回-ENXIO但设备明明焊在上面遇到这个先别改代码去量波形。我最常遇到的原因有三个。第一上拉电阻没焊或者阻值太大总线空闲时电平拉不到高示波器一看就是缓坡上升。第二引脚复用没配对I2C的SCL和SDA被别的功能占用波形上根本看不到时钟。第三设备供电没上芯片处于未上电状态自然不会应答。软件侧还有一个隐蔽原因10位地址和7位地址混用。如果设备用的是10位地址client-flags必须置I2C_CLIENT_TEN否则发出去的地址格式不对。10位地址的传输会在地址字节前多发一个特殊的保留字节用7位方式发就完全对不上。再一个可能是地址冲突。同一条总线上两个设备地址相同i2c_new_client_device在创建第二个时会返回-EBUSY但如果两个设备是分两次、在不同时间创建的可能第一个已经正常工作了第二个创建失败而你只看到驱动没probe。i2cdetect -y 1能看到那条总线上所有应答的地址扫描结果和预期对比一下多出来的地址就要警惕。6.3 时钟拉伸、速率与上拉电阻的计算时钟拉伸是从机在SCL为低时拉住总线不放告诉主机“我还没准备好”。主机必须支持这个机制否则数据会错。i2c_adapter的timeout和retries在这里就有意义了超时设太短稍微慢一点的从机就被判失败设太长从机异常拉死总线时CPU会跟着卡很久。我的经验值是从机手册里给的最大响应时间乘以2到3倍再留一点余量100kHz总线上通常设几百毫秒到1秒。上拉电阻的取值有公式可算。上升时间约束给出上限Rp(max) tr / (0.8473 * Cb)其中tr是标准允许的最大上升时间100kHz标准模式是1000ns400kHz快速模式是300nsCb是总线电容包括走线、引脚和器件电容通常按100pF到200pF估算。灌电流约束给出下限Rp(min) (Vdd - Vol) / IolVol是低电平最大电压一般取0.4VIol是引脚能灌入的最小电流常见是3mA。举个实际例子Vdd取3.3VCb按150pF估400kHz模式下Rp(max) 300ns / (0.8473 * 150pF) ≈ 2360Ω也就是2.36kΩRp(min) (3.3 - 0.4) / 3mA ≈ 967Ω。所以阻值落在约1kΩ到2.2kΩ之间比较合适实际板子上用1.8kΩ或2.2kΩ都很常见。如果总线上挂了七八个设备Cb会变大Rp(max)相应变小这时候就得减小阻值或者降低总线速率。很多人把400kHz切回100kHz问题就消失了本质上是上升时间约束被放宽了不是问题真的解决了。6.4 并发访问与锁的处理同一条总线上多个设备、多个驱动并发访问是常态。i2c_transfer自己会加总线锁所以单次传输之间不会互相踩。但要注意“读-改-写”这种复合操作不是原子的先读一个寄存器判断某位再写回去中间可能被另一个设备的传输插进来虽然别的设备操作的是自己的地址不影响数据但如果两个线程操作同一个设备就会出竞态。解决办法是在驱动内部加自己的mutex把针对同一个client的复合操作保护起来。不要用adapter-bus_lock做这件事那是核心层的锁你手动去拿会破坏封装。用mutex_lock(client-dev... )也不对正确的做法是在自己的私有结构体里放一把mutexprobe里初始化remove里销毁。中断上下文和线程上下文都要访问设备时锁的选择更讲究。中断处理里不能睡但i2c_transfer可能睡所以中断处理里只能做标记、唤醒线程真正的传输放到线程里做。用spin_lock_irqsave保护共享标志位用mutex保护传输序列这两把锁分工明确不要混用。6.5 我调I2C时固定会用的几招i2cdetect是最快的存活检查-y跳过交互-r用读方式探测-q用SMBus快速命令探测。有的芯片用读方式不应答但用写方式能应答两种都试一下能省很多时间。i2cdump可以用来快速看一片寄存器区域确认芯片是否处于预期状态不过要小心别去dump那些读操作有副作用的芯片。内核侧的调试输出我一般开CONFIG_I2C_DEBUG_CORE和CONFIG_I2C_DEBUG_ALGO再配合动态调试在drivers/i2c/i2c-core-base.c上打开。这样做能看到每次传输的msg构成、地址、方向、返回码比自己加printk干净得多。如果怀疑时序问题逻辑分析仪接在SCL和SDA上触发条件设成起始位抓一次完整传输对照芯片手册的时序参数逐个看起始保持时间、数据建立时间、停止建立时间哪个不达标就改哪。这个手段看起来笨但定位硬件层面的问题时几乎没有替代品。最后分享一个我自己的习惯每写一个新驱动先不写probe逻辑只写最小骨架——匹配表、probe里读chip id并打印、remove里打印。确认这三点都通了再往里填功能。这个做法看着慢实际上省掉了大量“功能代码和匹配问题纠缠在一起”的调试时间。踩过几次“以为是寄存器写错、其实根本没进probe”的坑之后这套流程就再也改不掉了。