先问大家一个问题:你在 OpenHarmony 上接 I2C 外设时,是不是也遇到过这种诡异场面——驱动编译进内核了,probe 也打印了,但读传感器寄存器就是全部返回 0xFF,或者写配置没反应,再严重点干脆整个总线卡死,所有挂在同一条总线上的设备全部失联。做了几年嵌入式系统开发,I2C 排障这种事我大概经历过几十次,从硬件波形到软件驱动再到框架配置,每个层面都踩过坑。这篇文章就把我在开源鸿蒙 OpenHarmony 上使用和排查 I2C 总线的方法完整整理出来,从物理层到底层驱动框架,再从框架到具体排障流程,希望能帮正在被 I2C 折腾的开发者少走点弯路。内容适配从入门新手到系统移植工程师,你不需要有很深的嵌入式底子,只要会看原理图、会写点 C 代码,就能跟着思路把问题定位到具体环节。
1. 我先交代一下I2C的物理层和协议层,排障离不开这些底账
很多人在 I2C 上出问题,第一反应是去翻驱动代码,这其实是本末倒置。我的经验是:I2C 的问题,十有五六出在物理层和协议层,而不在驱动代码本身。所以排障前,先把 I2C 的两根线、上拉电阻、ACK 机制、地址这些东西在脑子里过一遍,后面定位快很多。
1.1 两根线和上拉电阻就是全部硬件
I2C 总线的物理结构很简练,只有两条线:一条串行时钟线 SCL,一条串行数据线 SDA。所有设备都挂在这两根线上,主设备负责产生时钟,从设备根据地址被选中后响应数据。这里有个特别反直觉的点:I2C 设备并不是像 GPIO 那样靠强推挽输出驱动总线的,总线上的高低电平是靠上拉电阻拉起来、靠设备内部的开漏输出拉下去。也就是说,设备只能把总线拉低,拉不高的,高电平时所有设备都必须“放手”。
这个机制直接决定了几个排障要点:
- 如果上拉电阻缺失或者焊盘虚焊,SDA/SCL 的电平就悬空,电压可能一直飘在 1V 左右,时序逻辑全乱,设备表现不稳定。
- 如果上拉电阻太小,总线拉低的下降沿没问题,但释放时上升沿太慢,高速模式下面时序直接违规。
- 如果 SDA 被某个设备或者 PCB 漏电拉死到地,总线就一直处于 busy 状态,主设备发送起始条件时根本发不出去。
我调试过一块板子,上电后 I2C 偶尔正常偶尔卡死,后来用示波器看 SCL 和 SDA,发现 SCL 高电平时电压只有 1.8V,而芯片要求的 VIH 是 0.7×VDD,也就是 2.31V 左右。查了半天是 PCB 上 SCL 走线旁边有一条高频信号线,耦合噪声把高电平拉垮了。我当时第一反应是怀疑驱动,后来才意识到这是电气问题。所以千万记住,驱动只是照着协议发数据,真正干活的是电平。
1.2 协议交互里最容易出错的ACK与时钟拉伸
协议层大家常背的是起始条件、停止条件、设备地址、寄存器地址、数据位。但有两个点经常被忽略,恰恰是排障时最关键的:
第一个是ACK 应答位。I2C 规定每发一个字节后,接收方要在第 9 个时钟滴答拉低 SDA 表示“我收到了”。如果没有设备响应地址,SDA 在第 9 个时钟就是高电平,主设备收到的就是 NACK。很多开发者在读传感器 ID 时返回 0xFF,其实就是因为地址写错或者设备没上电,主设备发出了地址但没有任何设备 ACK,读回来的数据自然全 1。这时候别去检查寄存器位宽,先去确认设备到底有没有应答。
第二个是时钟拉伸。有些慢速从设备处理数据需要时间,它会主动把 SCL 拉低,要求主设备等着它。很多物联网传感器的驱动会适配这个特性,但如果你用的控制器主设备不支持时钟拉伸,而传感器又依赖这个机制,就会经常出现读写超时。OpenHarmony 里如果 I2C 驱动在等待传输完成时设置超时时间过短,慢速传感器就可能稳定复现超时问题。这个在选型阶段就要注意。
1.3 总线上的设备多了,你还要懂得仲裁与多主场景
I2C 支持多个主机,多个主机会通过总线仲裁来决定谁占用总线。仲裁的规则很简单:谁先释放总线谁退出,两个主机同时在 SDA 上发数据,谁发的信号先跟总线实际状态不一致,谁就无条件停止。听起来很智能,但仲裁意味着程序里必须处理总线冲突后的重试。
在 OpenHarmony 的板子上,如果多个主控核心或者多个进程同时操作同一个 I2C 控制器,而没有做好互斥,就会出现仲裁错误,表现就是读信号忽好忽坏,甚至 I2C 控制器状态挂了、总线上所有设备都失联。这种情况软件日志往往不会明确指出“仲裁失败”,而是显示 transfer timeout。
所以排障第一步,我建议先搞清楚硬件上挂了几条 I2C 总线,每根总线上有哪些设备,它们的地址分别是什么。不要一上来就看代码。硬件拓扑清楚了,后面每一步定位都有依据。我用的格式很简单,一张表列出来,比如:
| 总线 | 设备 | 7位地址 | 备注 |
|---|---|---|---|
| I2C-0 | 板载PMIC | 0x34 | 不能用,系统占用 |
| I2C-1 | 温湿度传感器 | 0x40 | 待调试 |
| I2C-1 | EEPROM | 0x50 | 待验证 |
| I2C-2 | 触控芯片 | 0x38 | 系统已正常 |
这张表在你的排障过程中就是地图。没有地图,排查容易变成见到哪块黑就挖哪里。
2. I2C和OpenHarmony的HDF是怎么互相找到的
从纯单片机视角转到 OpenHarmony,最大的不同是 I2C 不再是你直接在代码里操作寄存器那么直观,中间多了一层HDF(硬件驱动框架)。很多人第一次接触会觉得繁琐,但这个抽象是有意义的:系统里可能有多个 I2C 控制器,每个控制器上挂多个外设,HDF 负责把“哪个控制器、哪个设备、哪个驱动”三者关联起来。不理解这层关系,你连 probe 进不了都搞不清楚为什么。
2.1 HDF框架里的I2C平台驱动分管三件事
HDF 框架下,I2C 被拆成三层,你可以理解成:
- I2C 控制器驱动(Platform Driver):管具体芯片里的 I2C 硬件外设,包括时钟、中断、寄存器、DMA。
- I2C 外设驱动(Peripheral Driver):管理挂在总线上的具体器件,比如温度传感器、触控芯片。
- HDI 接口层:给上层提供统一的调用接口,屏蔽底层差异。
这三者的关系像物业和住户的关系:I2C 控制器驱动是物业,负责公共设施维护和电力供应;外设驱动是住户,只管自己屋里那点事;住户要通过物业提供的统一渠道才能用电用水,这个统一渠道就是 HDI 接口。
在 OpenHarmony 源码里,controller driver 通常和具体 SoC 绑定,代码放在drivers/peripheral/i2c对应适配层和平台目录。外设驱动则由你做产品功能时自行开发,也就是你自己写的那部分。如果你只是应用开发者,不涉及内核编译,那么你可能只跟 HDI 暴露的服务打交道。
2.2 HCS设备描述符:板上每根I2C控制器都要“报到”
OpenHarmony 不像旧式 Linux 那样只用设备树描述硬件,它有一套自己的配置描述语言 HCS(Hardware Configuration Source)。设备节点的挂载、驱动服务的发布、匹配规则,全都写在 HCS 文件里。我在刚上手时总觉得 HCS 不如设备树直观,后来发现它好处是分类特别清晰,配件配置、平台配置、外设策略分开管理,查起来反而快。
一个典型的 I2C 外设节点大致会声明这些字段:设备节点匹配属性、驱动服务名、私有数据,以及它使用的是哪条 I2C 控制器。控制器本身也会声明自己的编号、基地址、中断号和时钟。两个描述靠匹配属性绑定。
不写 HCS 行不行?如果你把驱动写成静态编译并且在代码里硬编码控制器号,在部分轻量系统上可以绕过去,但一旦涉及驱动服务需要被用户态通过 HDI 拉起,没有 HCS 节点和服务名,HDI 是找不到你的驱动服务的。所以标准做法还是老老实实改 HCS。
2.3 HDI是用户态拿到I2C服务的一条正规通道
这里顺带说说“openharmony hdi”这个热词。HDI 的全称是 Hardware Device Interface,它定义了一套向上屏蔽底层差异的接口。在你自己的 App 或者上层服务里,不可能直接像单片机上那样写寄存器操作 I2C,而是通过 HDI 接口库去调用驱动服务。
HDI 的意义在于:用户态程序不关心你用的是哪颗 SoC,也不关心 I2C 控制器驱动内部是怎么实现的。接口就那几个,打开设备、传输数据、关闭设备。底层的驱动实现可以随意替换,上层代码不用重新编译。
顺着 HDI 这条线继续往底层看,其实就是我在 2.1 节说的三层。这个架构也让 I2C 排障多了一个维度:同一个问题,既可能出在 HDI 调用层,也可能出在外设驱动层,还可能出在控制器驱动甚至硬件层。怎么快速切分层次,是后面的重点内容。
3. 实战:让一颗I2C温湿度传感器在OpenHarmony上跑起来
理论扯太多没用,直接走一遍完整流程。假设我手上有一块 OpenHarmony 开发板,板上有一条空闲的 I2C 控制器,编号是 I2C-1,我要挂一颗温湿度传感器,7 位地址是 0x44,它支持标准的 100kHz 模式,主要功能是触发测量后读一个 16 位的温度数据。
3.1 板级配置准备(HCS和dts差异说明)
如果你是从 Linux 转过来的,第一件事要接受一个事实:OpenHarmony 的板级配置以 HCS 为主,但底层内核里也可能有 device tree 参与。两者分工大致是:HCS 管设备节点、服务、驱动匹配,device tree 主要负责内核里寄存器基址和中断资源。不同版本和不同芯片适配方式有差异,但大方向是这个。
在 HCS 配置时,你需要确认控制器节点是否已经使能。以某个标准控制器的写法为例,核心字段包括:
| 字段 | 含义 | 举例 |
|---|---|---|
| matchAttr | 驱动匹配属性 | hisilicon_i2c_1 |
| serviceName | 提供给 HDI 的服务名 | hdf_i2c_1 |
| devId | 控制器的逻辑编号 | 1 |
| baseAddr | 寄存器基址 | 0x58100000 |
外设节点本身在 HCS 里的配置更像是个标记,告诉框架设备在哪条总线上,使用什么属性匹配到你的驱动。比如传感器节点会声明deviceMatchAttr = "ti_hdc1080_0x44",然后驱动侧通过HdfDeviceDesc拿到节点属性。
这段配置别小看,我一个同事把设备的devId配错了,把本来挂在 I2C-2 的设备配到了 I2C-1,结果驱动 probe 正常,但读数据总是超时。因为在 SoC 内部,I2C-1 和 I2C-2 对应不同的硬件引脚,引脚上根本没接传感器,地址自然没人 ACK。
3.2 驱动骨架和寄存器级读写
驱动代码的核心思路是拿到控制器句柄,然后组织寄存器地址和数据缓冲区调用传输函数。以 OpenHarmony HDF 平台框架的风格,代码大致长这样(不同 SDK 版本头文件名称可能有差异,以下示意):
#include "i2c_core.h" #include "device_resource_if.h" #include "hdf_log.h" #define TEMP_SENSOR_ADDR 0x44 #define REG_TEMP_H 0x00 #define REG_TEMP_L 0x01 static int32_t TempSensorReadTemp(DevHandle handle, uint16_t *temp) { uint8_t reg = REG_TEMP_H; uint8_t buf[2]; int32_t ret; // 写寄存器地址:告诉传感器接下来要读哪里 ret = I2cWrite(handle, TEMP_SENSOR_ADDR, ®, 1); if (ret != HDF_SUCCESS) { HDF_LOGE("write reg failed, ret=%d", ret); return ret; } // 读两个字节:温度高字节 + 温度低字节 ret = I2cRead(handle, TEMP_SENSOR_ADDR, buf, sizeof(buf)); if (ret != HDF_SUCCESS) { HDF_LOGE("read data failed, ret=%d", ret); return ret; } *temp = ((uint16_t)buf[0] << 8) | buf[1]; return HDF_SUCCESS; }看着简单,但有几个细节必须讲清楚,不然照抄必踩坑。
第一,I2cWrite 和 I2cRead 的形参语义在不同版本里有差异。有的封装会自动帮你加设备的 7 位地址,有的版本第二个参数确实是要传从机地址。如果你的 SDK 里封装好的 API 没有地址参数,那就直接传数据缓冲区。所以拿到代码第一件事是查头文件,确认函数签名,不要靠猜。
第二,寄存器的读写顺序要按传感器手册来。上面的示例是先写寄存器地址再读,但有些器件要求先写控制命令触发测量,然后等待一段时间才能读数据,中间可能还要 check 数据就绪位。如果你的传感器是后一种,直接照搬上面代码会稳定读出无效数据。
第三,I2cRead/I2cWrite 内部可能默认对事务做了拆包组合。有的实现会把写地址和读数据分开成两个 transaction,中间会产生一个 STOP 再重新 START;而有些传感器要求同一个 transaction 内连续完成“写寄存器地址 + 读数据”,这种情况你得用 combined message 的接口,而不是简单两个独立调用。
3.3 用户态通过HDI服务读取数据
如果你的项目只是在应用层要读个传感器数据,不想写内核驱动,OpenHarmony 也提供了 HDI 服务路径。用户态代码只需要通过 HDI 客户端拿到 I2C 服务代理,然后调用读接口。这类接口一般长这样:
#include "i2c_if.h" uint16_t ReadSensorTemp(void) { uint8_t data[2]; int16_t temp = 0; DevHandle handle = I2cOpen(I2C_BUS_1); if (handle == NULL) { return 0; } I2cWrite(handle, TEMP_SENSOR_ADDR, (uint8_t[]) { REG_TEMP_H }, 1); I2cRead(handle, TEMP_SENSOR_ADDR, data, 2); I2cClose(handle); return (uint16_t)((data[0] << 8) | data[1]); }用户态走 HDI 的好处是调试迭代快,不用每次改驱动都重新编译整个系统镜像。但注意:这条路径对同一个 I2C 控制器有并发保护要求。比如系统里如果有一个实时任务频繁访问同一条总线上的其它设备,你的用户态读操作可能迟迟等不到总线锁。这也是热词“openharmony hdi”背后大家真正关心的使用场景。
3.4 编译进系统并验证
配置好 HCS、写完驱动后,把驱动编译进内核或者编成独立的 ko 模块。在 OpenHarmony 构建系统里,可以单独编译,也可以跟随完整镜像编出来烧录。烧录后验证顺序建议:
- 先看 HDF 启动日志,确认设备节点有没有被框架找到,有没有调用你的 probe。
- 确认 i2c 控制器驱动 probe 成功,并且 device node 的 serviceName 正常挂载。
- 用一个最简单的 IOCTL 或者 HDI 测试命令,读设备 ID 寄存器。
- 如果设备 ID 能读出符合手册预期的值,再继续走业务功能验证。
这期间如果哪一步卡住了,就进入下一章的排障流程。
4. 排障路径:从软件日志、总线枚举到波形抓取
前面铺垫这么多,就是给排障做准备的。实战排查 I2C 问题,有一套我从实践中提炼出来的路径:先分层次,再选手段。层次没分清前,不要着急看示波器,也别瞎改驱动。
4.1 先分清是系统没设备,还是设备不应答
遇到 I2C 读数据失败,第一件事是确认 OpenHarmony 系统层面到底有没有注册这个 I2C 控制器节点。怎么确认?看日志或者设备节点列表。
我之前调试一块板子,驱动里调I2cOpen一直返回空指针。一开始以为是总线被占用,查了半天,最后发现是 HCS 文件里这个控制器的 status 字段被配成了 disabled,系统根本就没把它建立起来。所以第一个判断点就是:你打开的到底是存在但不工作的控制器,还是不存在的控制器。
还有一种情况:控制器存在了,open 也返回了,但外设不应答。这种问题进入下一步。
为了让大家能直接照着用,我给你一张我常用的排查表:
| 症状 | 初步结论 | 下一步动作 |
|---|---|---|
| open 返回 NULL | 控制器节点未注册 | 检查 HCS 节点和 status |
| open 正常,读写超时 | 总线物理不通或地址错误 | i2cdetect 扫从机地址 |
| 读回全 0xFF | 无 ACK,设备未上电或地址错 | 万用表测设备电源 |
| 读回全 0 | 设备 ACK 了但数据线异常 | 检查 SDA 波形和寄存器位宽 |
| 偶发超时 | 时钟拉伸/总线干扰/电源不稳 | 逻辑分析仪抓完整时序 |
| 总线卡死,所有设备失联 | SDA 被拉死或控制器状态机异常 | 复位设备、查上拉、查锁死 |
4.2 用i2cdetect判断设备有没有上线
在 OpenHarmony 根文件系统里,如果带了 i2c-tools 工具集,直接用i2cdetect -y <bus>扫一遍,几秒钟就能看到总线上哪些地址有响应。比如:
# 查看系统里有哪些 I2C 控制器 i2cdetect -l # 扫总线1上所有从机地址 i2cdetect -y 1扫描结果是表格,标着地址的格子说明那个地址有设备 ACK。看到 0x44 出现在表格里,说明这个传感器在物理层和协议层都被正确响应了,问题更大可能在驱动逻辑。如果扫描结果里根本没有 0x44,那问题就要回到硬件层:设备没上电、地址焊错了、SDA/SCL 接反了,或者上拉电阻没焊。
这个工具对 OpenHarmony 排障特别好用。我强烈建议把 i2c-tools 编进你的调试镜像,没有它的话,单靠写代码测试设备地址效率太低了。
如果目标系统没有 i2c-tools,还可以写一个小工具通过 HDI 接口枚举地址,逻辑就是依次对 0x01 到 0x77 发起单字节读,看哪个地址不返回 NACK。
4.3 逻辑分析仪看波形和时序
软件层排查如果没有结论,就该上逻辑分析仪了。注意这里有个优先级问题:不要一上电就挂逻辑分析仪,先把软件层扫完再挂波形工具。因为波形工具抓的是现象,不代表根因,串在里面看反而容易被带偏。
挂上逻辑分析仪后,抓 SCL 和 SDA 两根线。需要确认几件事:
- 起始条件是否正确:SCL 高电平期间,SDA 产生一个高到低的跳变。
- 设备地址字节是否正确:7 位地址第 1 位是地址,第 8 位是读写标志。你配置的地址和波形里实际出现的地址要完全对上。
- ACK 位是否存在:发送完 8 位数据后的第 9 个时钟,SDA 是否被从设备拉低。
- 数据个数的时序和寄存器长度的匹配。
我遇到过一种特别坑的情况:波形显示设备有 ACK,数据也读了,但读回来的数值不对。后来拿着逻辑分析仪一帧一帧数 bit,发现驱动里配置的寄存器地址是 16 位的,但传感器手册要求 8 位,导致后面所有数据都错位。这种问题不看波形根本查不出来,因为通信流程本身是通的。
4.4 一套可以直接照抄的排查流程
总结一下,我的 I2C 排障流程就五步:
- 软件层清点:
i2cdetect -l看控制器在不在,i2cdetect -y <bus>看设备在不在。 - 接口层自测:写最小读写命令,直接对固定寄存器读 ID,排除业务逻辑干扰。
- 波形层验证:逻辑分析仪抓地址字节、ACK、数据位,核对时序。
- 电气层复查:万用表量上拉电压、设备电源、SCL/SDA 空闲电平。
- 驱动层回查:确认 HCS 节点、driver 匹配属性、寄存器位宽、读写事务模式。
大部分 I2C 问题到前三步就能定位,到第四步的就是硬件故障,需要第五步的往往是软件逻辑和配置问题。顺序别乱,效率会高很多。
5. 总线级优化和电气适配:别等设备假死才后悔
把一个问题修好不是终点,总线稳定性设计也要重视。在 OpenHarmony 这类系统上跑 I2C,频率、上拉电阻、电压域、重试机制这些事,能做在前面就尽量做在前面。
5.1 上拉电阻计算、电平匹配
上拉电阻选多大,很多人觉得随便焊个 4.7k 就行。但如果总线速率高、挂的设备多,选错了确实会出问题。选型依据是 RC 时间常数:总线电容 C 大概是每米线缆几十 pF 到几百 pF,PCB 走线则小一些。电阻和电容一起决定上升沿时间,上升沿太慢会直接导致设备无法识别高电平。
I2C 规范里对上升沿时间有明确限制,标准模式和快速模式分别是 1000ns 和 300ns。粗略算的话,标准模式 100kHz 下用 4.7kΩ 配 100pF 总线电容,时间常数大约 470ns,能跑;到 400kHz 快速模式,就要换更小的电阻,比如 2.2kΩ 甚至 1kΩ。
还有电压匹配的问题。I2C 总线如果是 1.8V 电平的传感器,接到 3.3V 上拉的总线上,必须做电平转换,不能硬连。市面上有专门的双向电平转换芯片,原理也不复杂,就是两个 MOSFET 背靠背,一边拉低另一边跟着拉低。这个细节我在很多开发板原理图上见过坑,靠着上拉分别接两个电压域,看起来没问题,实际上在数据高速翻转时如果转换芯片驱动能力不够,波形边沿会很难看,导致偶发读错。
5.2 时钟频率和重试机制
OpenHarmony 的 I2C 控制器驱动一般支持动态设置传输速率。设置的时候要以总线上最慢设备的规格为准,而不是主控支持多快就设多快。举个例子,你的主控跑 1MHz 没问题,但总线上挂的一个老传感器只支持 100kHz,那整个总线配置得降到 100kHz,除非你有办法用两条独立总线把两类设备分开。
重试机制在排障中也特别重要。我的习惯是:写操作可以重试,读操作要看器件是否支持重复读取。有些传感器在触发测量后,如果还没测量完又收到读命令,会无视或者返回错误。这时重试需要带延时,给设备留出计算时间,然后重新发起读。在 OpenHarmony 的 HDI 应用中,这种机制直接写在上层逻辑里,千万别放在内核驱动里随意循环,否则一个慢速设备就能把整个系统的 I2C 总线占死。
5.3 高速/大批量传输的DMA考虑
最后一个性能点:大量数据的 I2C 传输是否要用 DMA。大多数 OpenHarmony 开发板上,I2C 单次传输几百字节的典型应用(比如读一个大容量的 EEPROM 或者传感器缓冲)用 CPU 搬运问题不大。但如果你要连续读几 kB 数据,并且系统里还有音频、显示等其他负载,CPU 中断式逐字节传输会明显拖慢系统。
此时控制器驱动可以配置 DMA 模式,把 buffer 地址交给控制器,让它自己搬运。启用 DMA 后要注意一致性映射,缓冲区内存需要是物理连续或者经过 IOMMU 映射的。这个在用户态 HDI 调用时尤其隐晦,因为上层传入的用户态指针如果要走 DMA,驱动内部需要做内存锁定和映射转换,处理不好会出现页错误或者传出来的数据是旧内容。我见过一个非常隐蔽的问题:同一段缓冲第一次读数据正常,第二次读就拿到上一次的旧数据,查到最后就是 DMA 缓存没有刷新。
6. 我把最常见的坑按优先级排了个序,照着避雷就行
文章标题点明“怎么排障”,最后我再把实战中最常见的问题集中列出来。这些坑每一个我都自己踩过,或者帮同事定位过,优先级按出现频率排。
6.1 地址位和寄存器位宽不对
这是发生率排第一的坑,没有之一。I2C 设备两种常见地址写法:7 位地址和 8 位地址。很多数据手册上写的地址是 8 位形式,比如 “0x90”,但实际 7 位地址是 0x48。写代码时如果直接把 0x90 传给驱动,驱动又没帮你左移一位,那访问的地址就错到 0x48 去了,结果就是你访问了一个根本不存在的设备,通信方误挂到别的地址上。反过来也一样。我的建议是,配置地址统一用 7 位形式,在代码里注释里写清楚来源。
寄存器位宽也是重灾区。传感器手册里寄存器地址有 8 位的、16 位的,总线传输时到底发 1 个字节地址还是 2 个字节地址,完全取决于器件。如果驱动写成 8 位地址而芯片内部寄存器是 16 位映射,数据错位是必然的。排查方法很简单,看 datasheet 的读时序图,数一下发送寄存器地址时波形有几个字节。
6.2 GPIO复用与电源时序
开鸿蒙系统里还会频繁遇到一个问题:引脚复用配置。芯片每个引脚往往有多个功能,I2C 的 SCL/SDA 可能和 GPIO、UART、PWM 复用。如果在板级配置里没有把引脚设置为 I2C 功能,你的控制器驱动初始化成功,但引脚实际根本不连 I2C 控制器。这时i2cdetect会扫不到任何设备,波形也全是乱的或者没有。
电源时序也容易掉坑。某些传感器要先供电再初始化,如果主控在传感器电源没稳定前就发起 I2C 通信,设备会一直不应答。排查时要看硬件上电流程,以及传感器复位引脚是否被异常拉低。这个在整机低功耗休眠唤醒的项目里尤其常见,休眠时把传感器电源切了,唤醒后软件没有等待电源稳定周期就发第一条 I2C 命令,设备直接不理你。
6.3 热插拔、总线死锁与恢复策略
最后聊一下总线死锁恢复。I2C 总线最怕 SDA 被拉死——从设备在半途发生异常复位,没有完成应有的释放,SDA 可能保持低电平。此时即使重新给主控上电,只要从设备还挂在那里,总线依然锁死。
解决这个问题有几个经典手段:
- 给总线设备单独做一个常电和时钟域,软件发现问题时强制把 I2C 设备的电源断掉再恢复。
- 在硬件上保留一路 GPIO 作为 I2C 总线的“重置钩子”,必要时给 SCL 发 9 到 16 个时钟脉冲,让卡死在中间状态的从设备完整接收完剩下的数据,完成释放。
- 驱动层在每次传输超时后,主动做一次软复位,把控制器的状态机清掉,再重置总线节奏。
OpenHarmony 里处理总线死锁时我最推荐的顺序是:先软复位控制器,再清总线,最后才考虑断电复位从设备。因为前两步不影响其它设备状态,第三步要动硬件,可能引入新的问题。
写到最后想说的是,I2C 这种总线技术不算新,但它在 OpenHarmony 这种现代系统上的用法确实需要重新适应。框架层的关键在于理解 HDF 的分层关系和 HCS 的配置方式,而排障层的关键在于建立“从现象到层次、从层次到手段”的排查顺序。我给你的建议是,在实际项目里提前把 i2c-tools、逻辑分析仪、万用表这三样东西准备好,把总线拓扑表贴在工位上。真出了问题,按本文的排查流程走一遍,你会发现绝大多数 I2C 的问题都不是玄学,而是地址、时序、配置里某个具体环节出了偏差。