ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony I2C总线驱动适配与通信故障排查实战

OpenHarmony I2C总线驱动适配与通信故障排查实战 I2C 这东西刚接触嵌入式的人往往觉得它简单——两根线一根时钟一根数据挂几个从设备就完事了。但真到了 OpenHarmony 这种多设备、多驱动、多任务并发的系统里你会发现 I2C 的问题从来不是能不能通而是为什么昨天能通今天不通为什么扫描到了地址却读不出数据为什么换个板子同样的代码就挂死。我在 RK3568、Hi3861、ESP32 这几套平台上都趟过 I2C 的坑从设备树配错引脚复用到上拉电阻选型导致波形塌陷再到 HDI 层时序参数没对齐导致从机 NACK几乎每一种失败模式都踩过一遍。这篇就围绕 OpenHarmony 系统下 I2C 总线的实际使用和排障展开把怎么用和怎么排这两件事讲透适合正在做驱动适配、外设接入、或者被 I2C 通信失败卡住的开发者参考。1. 先搞清楚 OpenHarmony 里 I2C 到底是怎么被管起来的很多人上手就去找i2c_write这种函数结果在 OpenHarmony 源码里翻半天找不到原因是没有先理解它的分层模型。OpenHarmony 的 I2C 不是裸机那种直接操作寄存器的玩法它有一套从内核到用户态的完整链路每一层出问题表现都不一样排障时必须先定位是哪一层。1.1 从硬件控制器到 HDI 接口的四层结构OpenHarmony 的 I2C 体系大致分四层。最底层是 SoC 的 I2C 控制器硬件比如 RK3568 有 6 组 I2CHi3861 有若干组每组控制器的寄存器基地址、时钟频率、FIFO 深度都不同。往上一层是内核态的 I2C 适配层Linux 内核用i2c_adapter和i2c_client来描述控制器和从设备OpenHarmony 在标准内核基础上做了适配。再往上是 HDIHardware Driver Interface层这是 OpenHarmony 特有的硬件驱动接口把 I2C 操作抽象成统一的接口给上层调用。最上面是用户态的服务和业务代码通过 HDI 接口发起读写。这个分层带来的直接后果是你在应用层调一个读接口失败可能是应用传参错、HDI 服务没起来、内核驱动没匹配、设备树没配、甚至硬件引脚没接对。所以排障第一步永远是确认问题出在哪一层而不是盲目改代码。1.2 设备树是 I2C 设备能否被识别的第一道关在 RK3568 这类平台上I2C 从设备能不能被系统识别几乎完全取决于设备树写得对不对。设备树里要描述三件事控制器本身使能、引脚复用pinctrl配置、从设备节点挂载。我见过太多人只写了从设备节点忘了配 pinctrl结果控制器时钟有了但 SDA/SCL 引脚还处于默认功能波形根本出不来。一个典型的 I2C 从设备节点长这样i2c3 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c3m0_xfer; gt911: gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 RK_PB6 GPIO_ACTIVE_HIGH; }; };这里clock-frequency是总线速率reg是从设备地址pinctrl-0引用引脚配置。注意reg里的地址是 7 位地址不是左移后的 8 位写地址这个坑后面会专门讲。1.3 HDI 接口的调用逻辑和常见误用OpenHarmony 的 HDI 层把 I2C 操作封装成I2cOpen、I2cTransfer、I2cClose这类接口。核心是I2cTransfer它接收一个I2cMsg数组每个 Msg 描述一次读或写。很多人第一次用会犯的错是把读和写分成两次 Transfer 调用中间没有保持总线占用导致某些从设备比如需要写寄存器地址后立即读的 EEPROM时序断裂。正确的做法是把写寄存器地址和读数据组合成一个 Msg 数组一次 Transfer 完成struct I2cMsg msgs[2]; msgs[0].addr 0x50; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg_addr; msgs[1].addr 0x50; msgs[1].flags I2C_FLAG_READ; msgs[1].len 4; msgs[1].buf read_buf; I2cTransfer(handle, msgs, 2);这样内核会在一次总线事务里完成写地址重复起始读数据符合 EEPROM 的随机读时序要求。如果拆成两次调用中间总线可能被其他任务抢走读出来的就是垃圾数据。2. I2C 通信失败的排查链路从现象反推到根因I2C 排障最忌讳的就是猜。我总结了一套从现象出发的排查链路基本能覆盖 90% 以上的失败场景。核心思路是先确认硬件层有没有波形再确认内核层有没有识别设备最后确认应用层调用有没有问题。2.1 用逻辑分析仪抓波形是最高效的第一步不管问题多复杂先上逻辑分析仪抓 SDA 和 SCL 两根线。这一步能直接排除掉一大半软件问题的误判。抓波形重点看四件事有没有起始条件SCL 高时 SDA 下降沿、有没有从机 ACK、时钟频率对不对、电平幅度够不够。我遇到过一个典型案例设备树配了 400kHz但从机是 100kHz 的老器件抓波形发现 SCL 频率确实是 400kHz但从机在第 9 个时钟周期没有拉低 SDANACK。这就是速率不匹配把clock-frequency改成 100000 就好了。如果不抓波形光看代码永远想不到是速率问题。还有一种情况是波形塌陷——SCL 上升沿变成圆弧而不是陡峭的方波。这通常是上拉电阻太大或者总线电容太大导致的。I2C 标准要求上升时间在 100kHz 下不超过 1000ns400kHz 下不超过 300ns。用示波器量一下上升时间如果超标把上拉电阻从 10k 换成 4.7k 甚至 2.2k 试试。2.2 内核日志里藏着设备匹配的全部线索抓完波形确认硬件没问题下一步看内核日志。OpenHarmony 启动时 I2C 控制器和从设备的注册信息都会打到dmesg里。重点搜几个关键词i2c、adapter、client、probe。如果看到i2c i2c-3: Failed to register i2c client这类信息说明从设备节点有问题通常是reg地址冲突或者compatible字符串没匹配上驱动。如果看到i2c-adapter i2c-3: bus not busy反复出现说明控制器使能了但总线上没有从设备响应可能是引脚复用没配对。我印象最深的一次是 GT911 触摸屏死活不识别日志里probe函数根本没被调用。查了半天发现是compatible字符串写成了goodix,gt911但驱动里匹配的是goodix,gt9xx一个字符的差异导致驱动完全没加载。这种问题不看日志根本发现不了。2.3 地址扫描工具能快速定位从设备是否在线Linux 下有i2cdetect工具OpenHarmony 上不一定自带但可以自己写一个简单的扫描程序。原理就是遍历 0x03 到 0x77 这 7 位地址空间对每个地址发一个空的写操作看有没有 ACK。for (addr 0x03; addr 0x77; addr) { msgs[0].addr addr; msgs[0].flags 0; msgs[0].len 0; msgs[0].buf NULL; ret I2cTransfer(handle, msgs, 1); if (ret 0) { printf(Device found at 0x%02x\n, addr); } }扫描能扫到地址说明从设备供电正常、地址配置正确、总线物理连接没问题。扫不到就要往硬件方向查了。注意有些从设备地址是可以通过引脚电平改变的比如某些传感器 ADDR 引脚拉高拉低对应不同地址扫描前先确认硬件上的地址配置。2.4 从机 NACK 的几种典型原因对照NACK 是 I2C 排障里最常见的现象但原因五花八门。我整理了一张对照表方便快速定位现象可能原因验证方法地址阶段就 NACK从设备地址错、供电异常、器件损坏换地址扫描、量供电电压地址 ACK 但数据阶段 NACK寄存器地址越界、从设备忙、写保护查数据手册确认寄存器范围读操作最后字节 NACK主机未发 NACK 表示结束检查驱动读时序实现随机 NACK总线干扰、上拉不足、多主机冲突抓波形看信号质量上电首次 NACK 后续正常从设备初始化未完成加延时或读状态寄存器轮询这张表是我在实际项目中反复验证过的基本能覆盖大部分场景。遇到 NACK 先对照这张表比盲目改代码效率高得多。3. 设备树配置里那些容易翻车的细节设备树是 OpenHarmony 在 RK3568 这类平台上绕不开的一环配错一个字段就可能让整个 I2C 总线不可用。这一节专门讲设备树里最容易出问题的几个点。3.1 引脚复用配置pinctrl 不配等于白干RK3568 的引脚是多功能复用的同一个物理引脚可以作 GPIO、I2C、UART、SPI 等。I2C 控制器要使能必须把对应引脚的功能切换到 I2C 模式这就是 pinctrl 的作用。pinctrl { i2c3 { i2c3m0_xfer: i2c3m0-xfer { rockchip,pins 3 RK_PA1 3 pcfg_pull_none_smt, 3 RK_PA2 3 pcfg_pull_none_smt; }; }; };这里的3 RK_PA1 3 ...表示第 3 组第 A1 号引脚功能选择 3即 I2C3 功能。如果功能号写错引脚就不会切到 I2C 模式波形出不来。我见过有人把功能号写成 2结果引脚切到了 UART怎么调都通不了。另外pcfg_pull_none_smt表示不启用内部上下拉、启用施密特触发器。I2C 总线通常需要外部上拉电阻所以内部下拉要关掉否则会和外部上拉打架导致电平拉不上去。3.2 从设备地址的 7 位与 8 位之争这是新手最容易踩的坑。I2C 从设备地址有 7 位和 8 位两种表示法。7 位地址是纯地址8 位地址是 7 位地址左移一位后加上读写位。设备树里的reg属性用的是 7 位地址而有些数据手册给的是 8 位地址。比如 GT911 的地址数据手册可能写 0xBA8 位写地址那 7 位地址就是 0xBA 1 0x5D。设备树里要写reg 0x5d。如果直接把 0xBA 填进去内核会把它当成 7 位地址 0xBA但 0xBA 超过了 7 位地址范围最大 0x7F直接报错。我建议养成习惯拿到任何 I2C 器件先确认数据手册给的是 7 位还是 8 位地址统一转换成 7 位再填设备树。3.3 中断引脚和复位引脚的 GPIO 配置很多 I2C 从设备触摸屏、传感器除了 I2C 通信还需要中断引脚和复位引脚。这两个引脚在设备树里要单独配置而且要注意 GPIO 编号和中断触发方式。gt911: gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 RK_PB6 GPIO_ACTIVE_HIGH; irq-gpios gpio0 RK_PB5 GPIO_ACTIVE_HIGH; };interrupts里的触发方式要和硬件设计匹配。GT911 的中断是低电平有效还是下降沿触发取决于芯片配置。如果配错了要么中断一直触发CPU 跑满要么永远不触发触摸无响应。我遇到过一次中断配成IRQ_TYPE_LEVEL_LOW结果触摸屏中断线一直拉低系统中断风暴直接卡死改成IRQ_TYPE_EDGE_FALLING就正常了。3.4 时钟频率设置与从设备能力的匹配clock-frequency这个属性看似简单但设错了问题很隐蔽。标准模式是 100kHz快速模式是 400kHz快速模式是 1MHz。大部分传感器支持 400kHz但有些老器件只支持 100kHz。更隐蔽的问题是即使从设备标称支持 400kHz实际布线电容大、上拉电阻大的情况下400kHz 的波形可能已经畸变导致偶发 NACK。这种情况下把频率降到 100kHz 往往能稳定运行。我的经验是调试阶段先用 100kHz 确保功能正常功能验证通过后再尝试提到 400kHz如果出现偶发错误就退回 100kHz。4. 实战中那些文档不会告诉你的经验前面讲的都是标准动作但实际项目里真正耗时间的往往是那些文档里不会写的细节。这一节分享几个我在多个项目里反复验证过的经验。4.1 上拉电阻选型不是随便找个 10k 就行I2C 总线的上拉电阻选型是有计算的。电阻太大上升沿太慢高速通信时波形来不及拉高电阻太小功耗大而且从设备可能拉不低灌电流能力不足。粗略的计算公式是R_min (VDD - VOL) / IOLR_max tr / (0.8473 × Cb)。其中 VOL 是从设备低电平输出电压IOL 是灌电流能力tr 是允许的最大上升时间Cb 是总线电容。实际项目中3.3V 系统、总线电容 100pF 左右、100kHz 速率下4.7k 到 10k 都能工作。但如果是 400kHz建议用 2.2k 到 4.7k。我遇到过一块板子用了 10k 上拉跑 400kHz波形上升沿接近 1us远超 300ns 的要求通信成功率只有 70% 左右换成 3.3k 后直接稳定。4.2 多从设备共存时的地址冲突和总线负载一条 I2C 总线上挂多个从设备很常见但要注意两点地址不能冲突总线电容不能超标。地址冲突好理解两个设备地址一样就没法区分。但有些设备地址可以通过引脚配置改变布线时就要规划好。我见过一块板子上两个传感器地址都是 0x68结果只能改硬件把其中一个的 ADDR 引脚拉高。总线电容是容易被忽略的。I2C 标准规定总线电容不超过 400pF。每个从设备的引脚有输入电容典型 10pF 左右PCB 走线也有电容约 1pF/cm。挂 10 个设备加上走线很容易超过 400pF。超了之后波形上升沿变缓高速通信就会出错。解决办法是降低速率、减小上拉电阻或者用 I2C 多路复用器比如 TCA9548A把总线分成多段。4.3 软件 I2C 和硬件 I2C 的取舍OpenHarmony 在部分平台上支持软件模拟 I2Cbit-banging就是用 GPIO 手动翻转电平来模拟 I2C 时序。什么时候用软件 I2C通常是硬件 I2C 控制器不够用或者某个设备的时序有特殊要求比如时钟拉伸特别长硬件控制器不支持。但软件 I2C 的缺点很明显占用 CPU、速率低、时序精度受调度影响。在 OpenHarmony 这种多任务系统里软件 I2C 如果没做好临界区保护时序很容易被其他任务打断导致通信失败。我的建议是能用硬件 I2C 就用硬件软件 I2C 只作为最后手段而且要做好关中断或高优先级任务保护。4.4 从设备初始化时序里的延时陷阱很多 I2C 从设备上电后需要一段初始化时间才能响应通信。比如某些传感器上电后需要 10ms 到 100ms 的稳定时间这期间发任何命令都会 NACK。更麻烦的是复位引脚的操作时序。以 GT911 为例复位引脚拉低至少 10ms拉高后要等 50ms 以上才能通信而且复位引脚和中断引脚的配合还有讲究复位释放时中断引脚的电平决定了 I2C 地址。这些时序要求数据手册里都有但很容易被忽略。我的做法是在驱动 probe 函数里严格按照数据手册的时序加延时不要图快省掉。省掉几十毫秒的延时可能换来几个小时的调试时间。4.5 逻辑分析仪的触发设置技巧用逻辑分析仪抓 I2C 波形触发设置很关键。如果只是自由采集很可能抓不到出问题的那一帧。建议用协议触发设置触发条件为地址匹配 NACK这样只有出现 NACK 时才触发采集能精准抓到问题帧。另外采样率要足够高。I2C 400kHz 的话采样率至少 10MHz 以上才能看清时序细节。如果采样率不够波形会失真误判成信号问题。5. 从 HDI 到内核跨层问题的定位方法有些 I2C 问题不是单一层面的而是跨层引起的。比如应用层调用返回错误但内核日志正常波形也正常问题可能出在 HDI 服务层。这一节讲怎么定位这类跨层问题。5.1 HDI 服务是否正常运行的确认方法OpenHarmony 的 HDI 服务是独立进程如果服务没起来应用层调用会直接失败。确认方法是查进程列表里有没有对应的 HDI 服务进程或者看 HDI 服务的日志输出。如果服务没起来常见原因是 SELinux 策略限制、服务配置文件缺失、或者依赖的驱动没加载。这类问题在dmesg和 HDI 服务日志里通常有明确报错按报错信息逐个排查即可。5.2 权限问题导致的调用失败OpenHarmony 有比较严格的权限管理。应用层要访问 I2C 设备需要相应的权限声明。如果权限没配调用会被拒绝但错误码可能很模糊让人误以为是硬件问题。排查方法是先确认调用返回的错误码如果是权限相关比如ERR_ACCESS_DENIED之类就去检查应用的权限配置文件。这类问题在开发阶段容易被忽略因为调试版本可能默认放开了权限但发布版本会严格检查。5.3 并发访问导致的总线冲突OpenHarmony 是多任务系统多个任务同时访问同一条 I2C 总线是可能的。如果驱动层没有做好互斥保护就会出现任务 A 发了一半任务 B 插进来发的情况导致时序错乱。内核的 I2C 子系统本身有总线锁但 HDI 层如果实现不当可能在锁外面做了额外操作。我遇到过一次两个服务同时读同一个传感器读出来的数据偶尔会串。后来在 HDI 层加了互斥锁才解决。排查这类问题的方法是在 I2C 读写前后加日志看是否有交错的调用。如果有就要在合适的层级加锁。5.4 用 ftrace 追踪内核 I2C 调用链如果问题出在内核层ftrace是很好的工具。可以追踪i2c_transfer、i2c_smbus_xfer这些函数的调用看每次传输的参数和返回值。echo function /sys/kernel/debug/tracing/current_tracer echo i2c_transfer /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on # 执行操作后 cat /sys/kernel/debug/tracing/trace这样能看到每次 I2C 传输的调用栈和耗时对于定位传输超时传输被阻塞这类问题很有帮助。6. 几个真实案例的完整复盘理论讲再多不如看几个真实案例。这一节复盘三个我实际遇到过的 I2C 问题从现象到根因到解决完整走一遍排查链路。6.1 GT911 触摸屏 I2C 通信失败从波形到设备树的排查现象RK3568 板子上 GT911 触摸屏完全不工作内核日志里没有任何 GT911 相关的 probe 信息。第一步抓波形SDA/SCL 上电后有短暂的活动但很快就没了。说明系统尝试过通信但失败了。第二步看内核日志搜i2c-3发现控制器注册成功但没有任何 client 注册。说明设备树里的 GT911 节点没被解析。第三步查设备树发现compatible写的是goodix,gt911但内核驱动里匹配的是goodix,gt9xx。改过来之后probe 被调用了但 probe 里返回失败。第四步看 probe 失败原因驱动里读芯片 ID 失败。抓波形发现地址阶段就 NACK。查数据手册发现 GT911 的 I2C 地址取决于复位时中断引脚的电平硬件设计里中断引脚默认拉低对应地址是 0x5D但设备树里写的是 0x14。改成 0x5D 后一切正常。这个案例的教训是设备树配置要对照数据手册逐项确认尤其是地址和 compatible 字符串这种一个字符就能决定成败的地方。6.2 EEPROM 随机读返回全 0xFF时序断裂的定位现象通过 HDI 接口读 EEPROM读出来的数据全是 0xFF。第一步确认硬件用逻辑分析仪抓波形发现写寄存器地址后总线上出现了停止条件然后才是读操作。这就是问题所在——随机读要求写地址重复起始读数据是一个连续事务中间不能有停止条件。第二步查代码发现应用层把写地址和读数据分成了两次I2cTransfer调用。每次调用结束内核都会发停止条件导致时序断裂。第三步修复把两次操作合并成一个 Msg 数组一次 Transfer 完成。修复后读取正常。这个案例说明I2C 的很多器件对时序有严格要求API 调用方式必须匹配器件时序不能想当然地拆分操作。6.3 多传感器偶发 NACK总线电容超标的解决现象一条 I2C 总线上挂了 8 个传感器单独测试每个都正常但全部挂上后偶发 NACK概率大概 5%。第一步抓波形发现 NACK 出现时SCL 的上升沿明显变缓从正常的 200ns 变成了 800ns 左右。第二步算总线电容8 个传感器每个约 10pFPCB 走线约 30cm 算 30pF加上连接器和封装寄生电容总共约 150pF。看起来没超 400pF但实际测量发现走线电容比估算的大总电容接近 350pF。第三步分析350pF 加上 10k 上拉电阻上升时间约 2.4us远超 400kHz 要求的 300ns。这就是偶发 NACK 的根因。第四步解决把上拉电阻从 10k 换成 2.2k上升时间降到约 500ns虽然还是略超但已经能稳定工作。同时把速率从 400kHz 降到 100kHz彻底稳定。这个案例的教训是总线电容要留足余量不能按理论最小值估算。上拉电阻和速率要配合总线实际情况调整。7. 把 I2C 排障能力变成肌肉记忆I2C 排障这件事说到底是一个分层定位逐层排除的过程。硬件层看波形内核层看日志HDI 层看服务应用层看调用。每一层都有对应的工具和方法关键是要养成按顺序排查的习惯而不是一上来就改代码。我在多个项目里总结下来最高效的排查顺序是先抓波形确认硬件再看内核日志确认设备识别然后用扫描工具确认从设备在线最后才查应用层调用。这个顺序能保证每一步都建立在已确认的事实之上避免在错误的方向上浪费时间。另外设备树配置和时序参数这两块是最容易出问题的地方建议在项目初期就对照数据手册逐项核对把compatible、reg地址、clock-frequency、pinctrl 功能号、中断触发方式这几个关键字段确认清楚。这些字段一旦配错后面所有调试都是白费功夫。最后分享一个我自己的习惯每接入一个新的 I2C 器件先写一个最小的测试程序只做地址扫描和寄存器读写确认基本通信正常后再集成到完整系统里。这样能把问题隔离在最小范围内避免在复杂系统里排查简单问题。这个习惯帮我省下了大量调试时间推荐你也试试。
返回列表