1. I2C 总线在 OpenHarmony 里的定位与整体设计思路
I2C 这东西,搞嵌入式的人几乎绕不开。它不像 SPI 那样追求极致速度,也不像 UART 那样点对点简单粗暴,它的价值在于“两根线挂一堆设备”。在 OpenHarmony 系统实战开发里,I2C 是传感器、EEPROM、触摸屏、RTC、PMIC 这类外设最常用的接入方式之一。你拿到的标题是“I2C 总线怎么用?怎么排障?”,这其实问到了两个层面:一是怎么在 OpenHarmony 的 HDF 驱动框架下把 I2C 设备跑起来,二是当它不工作时,你怎么一步步定位问题。适合谁看?适合已经能编译 OpenHarmony、知道 HDF 驱动大概长什么样,但一遇到 I2C 读写失败就抓瞎的开发者。也适合从裸机或 Linux 驱动转过来的朋友,因为 OpenHarmony 的设备树和 HDF 配置有自己的脾气。
先把这个项目的核心思路说清楚。OpenHarmony 的 I2C 使用不是让你直接操作寄存器,而是通过 HDF(Hardware Driver Foundation)框架,把 I2C 控制器抽象成统一的总线服务。应用层或内核态驱动通过I2cOpen、I2cTransfer这类接口去读写。设备树负责描述“哪个 I2C 控制器、挂在哪、从机地址是多少、寄存器位宽多少”。HDF 驱动负责把设备树信息解析出来,生成对应的 I2C 客户端。整个链路是:设备树 -> HDF 配置 -> I2C 控制器驱动 -> 硬件时序 -> 从机设备。任何一环出问题,现象都是“读写失败”或“数据不对”。所以排障不能只盯着代码,得从硬件、设备树、驱动配置、时序、电源几个维度一起看。
为什么 OpenHarmony 要这么设计?因为 OpenHarmony 面向的是多芯片平台,瑞芯微 RK3568、海思、展锐等都可能跑。如果每个平台都写一套 I2C 操作代码,移植成本太高。HDF 把控制器驱动和设备驱动分离,控制器驱动由芯片原厂或社区提供,设备驱动由外设厂商或开发者编写。设备树则把硬件连接关系从代码里剥离出来。这样你换一个板子,只要改设备树和 HDF 配置,设备驱动代码基本不用动。这个设计思路和 Linux 的 I2C 子系统很像,但 OpenHarmony 的 HDF 更强调用户态驱动和内核态驱动的统一接口。实际开发中,你大部分时间是在写设备驱动和调设备树,而不是改控制器驱动。
还有一个关键点:I2C 是半双工、主从架构、开漏输出。这意味着总线上所有设备共享两根线,任何时刻只能有一个主机发起传输。从机不能主动说话,只能被叫到才应答。这就引出了地址冲突、总线仲裁、时钟拉伸、总线死锁这些经典问题。在 OpenHarmony 里,这些问题不会因为用了 HDF 就消失,反而因为多了一层抽象,排查起来更隐蔽。比如你写了一个 I2C 读取函数,返回失败,可能是从机没上电,可能是地址写错,可能是设备树里控制器编号不对,也可能是总线被某个设备拉死了。所以这篇内容我会把“怎么用”和“怎么排障”揉在一起讲,因为实际开发中这两件事根本分不开。
2. I2C 核心细节解析与 OpenHarmony 实操要点
2.1 I2C 时序基础:别急着写代码,先看懂这四张图
I2C 的时序其实不复杂,但很多人栽在细节上。起始条件(S):SCL 高电平期间,SDA 从高变低。停止条件(P):SCL 高电平期间,SDA 从低变高。数据位:SCL 低电平期间 SDA 可以变化,SCL 高电平期间 SDA 必须稳定。应答位(ACK):主机发完 8 位数据后释放 SDA,从机在第 9 个时钟周期把 SDA 拉低表示应答。非应答(NACK):从机不拉低 SDA,或者主机在读取最后一个字节后主动发 NACK。这些是基本功,但实际用逻辑分析仪抓波形时,你会发现很多问题就出在这些边沿上。
在 OpenHarmony 里,控制器驱动会帮你处理起始、停止、ACK 这些时序。你调用I2cTransfer时,传入一个I2cMsg数组,每个 msg 包含从机地址、读写标志、数据缓冲区、长度。控制器驱动会按顺序执行这些 msg。但注意,不是所有控制器都支持任意组合的 msg。有些控制器要求每个 msg 之间必须有停止条件,有些支持重复起始条件。如果你在一个 transfer 里放了多个 msg,中间没有停止条件,控制器可能会报错。这个细节在设备树里通常有i2c-scl-falling-time-ns、i2c-sda-falling-time-ns这类参数来调整时序,但很多开发者直接抄参考配置,结果换一个从机就挂了。
提示:用逻辑分析仪抓 I2C 波形时,一定要同时抓 SCL 和 SDA,并且把触发条件设在起始条件上。否则你看到的可能是一堆无意义的电平跳变。
2.2 OpenHarmony 设备树里的 I2C 节点怎么写才不踩坑
设备树是 OpenHarmony I2C 使用的第一道门槛。以 RK3568 为例,I2C 控制器节点通常在rk3568.dtsi里定义,比如i2c0: i2c@fdd40000。你要在板级设备树里覆盖它,设置status = "okay",然后添加从机子节点。从机节点必须包含reg属性,值是从机地址,比如reg = <0x50>。这个地址是 7 位地址,不是 8 位。很多人把写操作地址和读操作地址搞混,比如 EEPROM 的 7 位地址是 0x50,写是 0xA0,读是 0xA1。设备树里只写 7 位地址,HDF 驱动会自动处理读写位。
另一个坑是clock-frequency。默认可能是 100kHz,但有些从机支持 400kHz,有些只能 100kHz。如果你设了 400kHz 但从机跟不上,就会出现数据错乱或 NACK。RK3568 的 I2C 控制器还支持i2c-scl-rising-time-ns和i2c-scl-falling-time-ns,这些参数影响时序计算。如果你不填,驱动会用默认值,可能不匹配你的硬件。我一般会先用示波器测一下实际上升沿时间,再填进去。还有pinctrl配置,I2C 的 SCL 和 SDA 引脚必须正确复用为 I2C 功能,并且上拉电阻要接。很多板子 I2C 不通,最后发现是引脚复用没配,或者上拉电阻没焊。
在 OpenHarmony 的 HDF 配置里,你还需要在device_info.hcs里声明 I2C 设备。比如:
device_i2c :: device { device0 :: deviceNode { policy = 2; priority = 50; preload = 0; permission = 0666; moduleName = "HDF_I2C"; serviceName = "hdf_i2c"; deviceMatchAttr = "i2c_config"; }; }然后在i2c_config.hcs里配置总线号和从机信息。这里deviceMatchAttr要和设备树里的节点匹配。如果匹配不上,驱动加载了但找不到设备,读写就会返回-ENODEV。
2.3 HDF I2C 接口怎么调:从 Open 到 Transfer 的完整流程
OpenHarmony 的 I2C 用户态接口在base/hiviewdfx或drivers/hdf_core里,具体头文件是i2c_if.h。典型流程是:
I2cOpen(busNum)打开指定 I2C 总线,返回一个句柄。- 构造
I2cMsg数组,填充从机地址、缓冲区、长度、读写标志。 - 调用
I2cTransfer(handle, msgs, count)执行传输。 - 检查返回值,如果小于 0 说明失败。
I2cClose(handle)关闭总线。
看起来简单,但有几个细节。I2cMsg的addr字段是 7 位从机地址,不是 8 位。flags字段用I2C_FLAG_READ或I2C_FLAG_WRITE。如果你要读寄存器,通常需要两个 msg:第一个写寄存器地址,第二个读数据。这两个 msg 之间不能有停止条件,所以要用I2C_FLAG_NO_STOP标志。但有些控制器不支持这个标志,你就得分开两次 transfer,中间加停止条件。分开写的问题是,有些从机在停止条件后会复位内部地址指针,导致读不到正确数据。所以优先用组合 msg。
还有一个常见错误:缓冲区长度和实际数据长度不一致。比如你定义uint8_t buf[2],但len写了 4,驱动会读越界。或者你读 2 字节,但从机只返回 1 字节,第二个字节可能是 0xFF。这些在调试时都要用逻辑分析仪确认。
注意:
I2cTransfer的返回值是实际传输的 msg 数量,不是字节数。如果返回 1,说明第一个 msg 成功了,第二个可能失败了。要逐个检查。
3. I2C 排障实战:从现象到根因的完整链路
3.1 先分清楚:是总线不通,还是设备不应答
I2C 排障第一步是区分“总线级别故障”和“设备级别故障”。总线级别故障包括:SCL 或 SDA 被拉死、上拉电阻缺失、引脚复用错误、控制器未使能。设备级别故障包括:从机地址错误、从机未上电、从机内部寄存器地址错误、从机忙。怎么区分?用逻辑分析仪抓波形。如果发起始条件后,SCL 有 9 个时钟,但 SDA 一直是高,说明没有从机应答,这是设备级别问题。如果 SCL 一直被拉低,或者 SDA 一直被拉低,说明总线被某个设备拉死了,这是总线级别问题。
在 OpenHarmony 里,你可以先看内核日志。如果控制器驱动加载失败,日志里会有i2c-rk3x或类似字样。如果控制器加载成功但传输失败,日志里会有I2cTransfer failed或timeout。超时通常意味着从机没应答,或者总线被拉死。NACK 通常意味着地址不对,或者从机没准备好。你可以用hilog查看 HDF 的日志,过滤I2C关键字。
还有一个快速判断方法:用万用表测 SCL 和 SDA 对地电压。正常空闲时,两者都应该是高电平(比如 3.3V)。如果一个是 0V,说明被拉死了。如果两个都是 0V,可能是控制器没初始化,或者电源没上。如果电压在 1.5V 左右,可能是上拉电阻太大,或者总线电容太大。
3.2 设备树配置错误的五种典型表现
设备树配错是 I2C 不工作的最常见原因。我整理了几种典型表现:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 驱动加载但找不到设备 | deviceMatchAttr不匹配 | 检查 hcs 和 dts 里的属性名是否一致 |
| 读写返回 -ENODEV | 从机节点status不是okay | 确认设备树覆盖是否正确 |
| 读写返回 -EINVAL | reg地址超过 7 位范围 | 检查地址是否写成 8 位 |
| 读写超时 | clock-frequency太高 | 降到 100kHz 试试 |
| 数据错位 | pinctrl配错或上拉缺失 | 用示波器看波形质量 |
还有一个隐蔽问题:设备树里 I2C 控制器节点被其他驱动占用。比如某个 GPIO 驱动也用了同一组引脚,导致 I2C 无法复用。这时候你要检查pinctrl的引用计数,确保没有冲突。
3.3 用逻辑分析仪抓 I2C 波形的正确姿势
逻辑分析仪是 I2C 排障的核武器。但很多人抓不到有效波形,原因是采样率不够或触发条件不对。I2C 最快 400kHz,理论上 1MHz 采样率就够了,但为了看清毛刺,建议至少 10MHz。触发条件设在 SDA 下降沿且 SCL 高电平,这就是起始条件。抓到的波形要能看清起始、地址、ACK、数据、停止。如果波形上出现很多毛刺,说明上拉电阻太大或走线太长。如果 SCL 占空比严重不对称,说明时钟配置有问题。
在 OpenHarmony 里,你还可以用i2c-tools的i2cdetect来扫描总线。但注意,OpenHarmony 默认可能不带这个工具,你需要自己交叉编译。i2cdetect -y 0会扫描总线 0 上的所有地址。如果某个地址显示UU,说明该地址被驱动占用了。如果显示--,说明没有设备应答。这个工具能快速告诉你从机是否在线。
提示:
i2cdetect会发送大量探测包,有些从机不支持这种探测,可能会进入异常状态。所以扫描前最好确认从机手册。
4. 常见问题与排查技巧实录
4.1 I2C 读写 EEPROM 失败:地址和页写问题
EEPROM 是 I2C 最经典的从机,也是坑最多的。常见问题:写进去读出来不对,或者写一次成功第二次失败。原因通常是页写边界。比如 AT24C02 每页 8 字节,你从地址 0x07 开始写 4 字节,会跨页,导致数据回卷。正确做法是按页对齐写。另一个问题是写周期时间。EEPROM 写完后需要 5ms 左右的内部擦写时间,这期间不响应任何 I2C 命令。如果你连续写,第二次会 NACK。解决办法是写完后延时,或者用 ACK 轮询。
在 OpenHarmony 里,你可以封装一个eeprom_write函数,每次写一页,然后mdelay(10)。读的时候先写寄存器地址,再读数据。注意寄存器地址是 1 字节还是 2 字节,取决于 EEPROM 容量。AT24C02 是 1 字节,AT24C256 是 2 字节。设备树里不需要配这个,但驱动代码里要区分。
4.2 触摸屏 GT911 I2C 通信失败:复位和地址切换
GT911 是常见的 I2C 触摸屏控制器,它的坑在于地址不固定。上电时,如果 INT 引脚在复位释放时为低,地址是 0x5D;如果为高,地址是 0x14。很多开发者硬件设计时没注意这个,导致地址对不上。解决办法是在驱动初始化时,先控制复位和 INT 引脚,把地址切到期望值。然后在设备树里写对应的reg。
另一个问题是 GT911 需要固件配置。如果 I2C 能通但触摸没反应,可能是固件没加载。你需要通过 I2C 写入配置寄存器。这部分在 OpenHarmony 的 HDF 触摸屏驱动里有参考实现,但不同板子配置不同,要自己调。
4.3 总线死锁:一个设备拉死整条总线
I2C 总线死锁的典型现象是 SCL 或 SDA 被某个设备一直拉低。原因可能是从机在传输过程中复位,或者电源不稳导致状态机跑飞。解决办法是发送 9 个时钟脉冲,让从机把剩余数据移完,然后发停止条件。在 OpenHarmony 里,你可以在控制器驱动里实现一个i2c_recover_bus函数,用 GPIO 模拟时钟。但注意,这需要把 SCL 和 SDA 临时切回 GPIO 模式,发完脉冲再切回 I2C 模式。
预防死锁的方法:确保所有从机电源稳定,上电时序正确;在设备树里给 I2C 控制器加i2c-bus-recovery属性;避免在中断上下文里做大量 I2C 传输。
4.4 常见问题速查表
| 问题 | 现象 | 解决 |
|---|---|---|
| 地址错误 | NACK,无应答 | 确认 7 位地址,检查读写位 |
| 上拉缺失 | 波形上升沿缓慢 | 加 4.7k 上拉电阻 |
| 时钟太快 | 数据错乱 | 降到 100kHz |
| 页写跨页 | 数据回卷 | 按页对齐写 |
| 总线死锁 | SCL/SDA 被拉低 | 发 9 个时钟脉冲恢复 |
| 设备树不匹配 | 驱动加载失败 | 检查deviceMatchAttr |
| 引脚复用错误 | 无波形 | 检查pinctrl配置 |
| 电源未上 | 无应答 | 测从机 VCC |
5. 从裸机到 OpenHarmony:I2C 调试思维的转变
5.1 裸机 I2C 和 OpenHarmony I2C 的本质区别
裸机写 I2C,你直接操作寄存器,知道每一步在干什么。OpenHarmony 写 I2C,你面对的是 HDF 接口和设备树,中间隔了好几层。这带来的好处是移植方便,坏处是出问题时不好定位。我的经验是:先用裸机或 Linux 的i2c-tools确认硬件没问题,再上 OpenHarmony。如果裸机都读不到,那肯定是硬件或从机问题,跟 OpenHarmony 无关。如果裸机能读,OpenHarmony 读不到,那就是设备树或 HDF 配置问题。
另一个区别是并发。裸机里你通常在一个任务里用 I2C,不会有多线程竞争。OpenHarmony 里可能有多个驱动同时访问同一条 I2C 总线。HDF 的 I2C 接口内部有互斥锁,但如果你在用户态和内核态同时访问,还是可能出问题。所以尽量把 I2C 访问集中在一个驱动里。
5.2 用 HDF 日志定位 I2C 问题的技巧
OpenHarmony 的 HDF 日志可以通过hilog查看。你可以设置日志级别为 DEBUG,过滤I2C或HDF_I2C。关键日志包括:控制器初始化、设备匹配、传输开始、传输完成、错误码。如果传输超时,日志里会有timeout和当前寄存器值。如果 NACK,会有nack和从机地址。这些信息比你自己打印更准确。
还有一个技巧:在I2cTransfer前后加日志,打印msgs的地址、长度、标志。这样你能确认传给驱动的内容是否正确。很多时候问题就出在len或flags写错了。
5.3 个人实操心得:先软后硬,先简后繁
我调 I2C 的习惯是:先用一个最简单的从机(比如 EEPROM)验证总线通不通。如果 EEPROM 能读写,说明控制器、设备树、HDF 配置都没问题。然后再上复杂的从机(比如触摸屏、传感器)。如果 EEPROM 都不通,就别折腾复杂设备了,先查硬件。查硬件时,先测电压,再测波形,最后换从机。换从机是最快的排除法,如果换一个同型号的从机就好了,说明原来的坏了。
还有,设备树改完后一定要重新编译并更新。OpenHarmony 的设备树是打包在 boot 分区里的,不是单独的文件。你改了 dts 但没重新烧录,等于没改。这个坑我踩过好几次。
最后再分享一个小技巧:如果你怀疑是 I2C 时钟频率问题,可以在设备树里把clock-frequency改成 10000(10kHz),慢到极致。如果 10kHz 能通,说明是时序问题,再逐步提高。这个方法虽然笨,但很有效。