ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux I2C驱动核心结构体、传输接口与probe排查

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_algorithm,i2c_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_xfer
i2c_client从设备实例设备树或板级信息保存7位地址、所属adapter、irq
i2c_driver设备驱动从设备驱动匹配表、probe、remove
i2c_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: sensor@50 { 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跑通再往上提。

节点名里的sensor@50只是个标签,真正起作用的是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_table,name那层基本用不上。

同一条总线上挂多个同类设备时,设备树里每个节点有自己的地址,驱动probe会被调用多次,每次拿到不同的client。驱动内部要避免用全局变量保存“设备状态”,状态应该挂在i2c_set_clientdata里,probe里set、remove里get,各个实例互不干扰。这个坑我在早期代码里踩过,两个相同传感器接在同一条总线上,第二个设备的读数覆盖了第一个,查了半天才发现是静态变量。

6. 常见问题与排查技巧实录

6.1 probe不进的排查顺序

这是最高频的问题。我的排查顺序是固定的,按代价从低到高走。第一步,确认驱动真的加载了,lsmod或者dmesg | grep 驱动名,模块加载失败会在dmesg里留痕。第二步,看compatible字符串有没有拼错,直接diff设备树源文件和驱动里的字符串,比肉眼比对可靠。第三步,看总线上有没有这个client,ls /sys/bus/i2c/devices/下面应该有1-0050这样的目录,目录名格式是“总线号-地址”。如果没有,说明设备树节点没被解析,检查adapter节点的status是不是okay,以及of_i2c_register_devices有没有被调用。

第四步,如果client存在但probe没进,说明匹配失败。把驱动编译进内核而不是模块,看启动日志里有没有匹配相关的提示;或者打开I2C核心的调试输出,加内核参数dyndbg="file drivers/i2c/* +p",能看到匹配过程的细节。第五步,如果probe进了但返回错误,dev_err_probe输出的错误码就是方向,-EPROBE_DEFER表示依赖资源没就绪,-ENODEV表示探测失败,其他错误码按字面意思查。

现象最可能原因快速验证
模块没加载依赖未满足或符号冲突dmesg 看加载日志
client 不存在设备树节点未使能或解析失败ls /sys/bus/i2c/devices
client 存在但无probecompatible 不匹配对比设备树与驱动字符串
probe 返回 -EPROBE_DEFER电源/时钟/GPIO 未就绪检查 regulator 与 pinctrl
probe 返回 -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标准模式是1000ns,400kHz快速模式是300ns,Cb是总线电容,包括走线、引脚和器件电容,通常按100pF到200pF估算。灌电流约束给出下限:Rp(min) = (Vdd - Vol) / Iol,Vol是低电平最大电压,一般取0.4V,Iol是引脚能灌入的最小电流,常见是3mA。

举个实际例子:Vdd取3.3V,Cb按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... )也不对,正确的做法是在自己的私有结构体里放一把mutex,probe里初始化,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”的坑之后,这套流程就再也改不掉了。

返回列表