☰
OpenHarmony I2C 总线开发指南:设备树配置、HDF 驱动与排障实战
2026/9/30 6:15:09 网站建设 项目流程

1. I2C 总线在 OpenHarmony 里的定位与整体设计思路

I2C 这东西,搞嵌入式的人几乎天天见。它只有两根线,SDA 和 SCL,一根传数据,一根给时钟,主从设备都挂在这两条线上,靠地址区分谁是谁。便宜、省引脚、协议简单,所以传感器、EEPROM、触摸屏、小屏 OLED 这些外设,十有八九都走 I2C。但简单归简单,真到了 OpenHarmony 这种带 HDF 驱动框架、带设备树、带内核态用户态分层的系统里,I2C 的用法和裸机写寄存器完全是两码事。很多人从 STM32 转过来,第一反应是“我直接操作寄存器不就行了”,结果发现 OpenHarmony 里根本摸不到那一层,得顺着 HDF、设备树、内核驱动这条链路走。

这篇内容我打算把 I2C 在 OpenHarmony 上的完整使用路径讲透,从设备树怎么配、HDF 驱动怎么挂、用户态怎么调,到实际排障时怎么一步步定位问题。适合已经有一点嵌入式基础、正在往 OpenHarmony 上迁移的开发者,也适合那些被“I2C 通信失败”卡住、翻遍日志找不到头绪的人。核心关键词就几个:I2C、OpenHarmony、总线、排障、设备树。这几个词基本覆盖了从硬件到软件的全链路。

先说整体设计思路。OpenHarmony 的 I2C 访问不是单一入口,它分了几层:最底下是 SoC 的 I2C 控制器驱动,跑在内核态;中间是 HDF 的 I2C 核心层,负责把控制器抽象成统一接口;上面是具体外设的驱动,比如某个触摸屏或者传感器,它通过 HDF 提供的 I2C 接口去读写;再往上才是应用层通过 HDI 或者 NAPI 拿数据。这个分层的好处是,换一个 SoC,只要控制器驱动适配好,上层外设驱动基本不用动。坏处是,一旦某一层出问题,现象都一样——读不到数据,但原因可能天差地别。

为什么 OpenHarmony 要这么设计?因为它的目标场景是“万物互联”,设备形态太多,从几十块钱的模组到几百块的开发板都有。如果每个外设驱动都直接怼寄存器,移植成本会爆炸。HDF 这层抽象就是为了把硬件差异吃掉。所以你在 OpenHarmony 里写 I2C 外设驱动,第一件事不是看芯片手册的寄存器,而是看设备树里这个 I2C 控制器挂在哪、地址是多少、外设挂在哪个总线下。这个思路转变不过来,后面全是坑。

我见过太多人一上来就问“I2C 读不出来怎么办”,然后贴一段代码。其实这时候最该看的是设备树和内核日志,而不是代码。因为 OpenHarmony 的 I2C 链路里,设备树配错、控制器没使能、引脚复用没配对,这三个原因占了排障案例的一大半。代码层面的问题反而好查,因为 HDF 的接口返回值很明确,错就是错,不会给你模棱两可的结果。

还有一个设计上的点值得说:OpenHarmony 的 I2C 访问默认是同步阻塞的。也就是说,你调一次读,线程就卡在那儿等结果。这在单外设、低频访问的场景下没问题,但如果你挂了好几个 I2C 设备,或者某个设备响应特别慢,就会拖累整个线程。所以实际项目里,我一般会把 I2C 访问放到独立的任务或者工作队列里,避免阻塞主流程。这个不是 OpenHarmony 强制的,但属于实战经验,后面排障部分会展开。

2. 设备树配置:I2C 能不能通,一半看这里

2.1 I2C 控制器节点的关键属性

设备树是 OpenHarmony 里 I2C 的“户口本”。一个 I2C 控制器能不能用,先看它在设备树里有没有被正确描述。以常见的瑞芯微 RK3568 为例,I2C 控制器节点大概长这样:

i2c1: i2c@fe5a0000 { compatible = "rockchip,rk3568-i2c"; reg = <0x0 0xfe5a0000 0x0 0x1000>; interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>; clock-names = "i2c", "pclk"; pinctrl-names = "default"; pinctrl-0 = <&i2c1_xfer>; #address-cells = <1>; #size-cells = <0>; status = "okay"; };

这里面几个属性必须对:reg是控制器寄存器的物理地址和长度,写错了直接访问不到;interrupts是中断号,配错会导致传输完成中断收不到,表现为超时;clocks和clock-names是时钟源,I2C 控制器没时钟就是死的;pinctrl-0是引脚复用配置,SDA 和 SCL 必须复用成 I2C 功能,如果被别的功能占了,波形都出不来;status必须是okay,默认很多 SoC 的 dtsi 里是disabled,得在板级 dts 里覆盖。

#address-cells = <1>和#size-cells = <0>这两个也重要。I2C 总线下挂设备,地址是 7 位或者 10 位,占一个 cell,所以 address-cells 是 1;I2C 设备没有内存映射,所以 size-cells 是 0。这两个写错,编译能过,但设备树解析会出问题,外设节点挂不上去。

2.2 外设节点怎么挂

外设节点是挂在 I2C 控制器下面的子节点。比如一个 EEPROM:

&i2c1 { status = "okay"; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; status = "okay"; }; };

reg = <0x50>就是 I2C 从机地址。注意这里是 7 位地址,不是 8 位。很多新手看手册上写“写地址 0xA0,读地址 0xA1”,就往设备树里填 0xA0,结果死活不通。因为手册给的是 8 位格式,最低位是读写位,真正的 7 位地址是 0xA0 右移一位,也就是 0x50。这个坑我踩过不止一次,后来养成习惯,看到 8 位地址先除以 2。

compatible属性决定用哪个驱动去匹配。如果这个字符串和驱动里的of_match_table对不上,驱动就不会 probe,外设节点等于摆设。所以排障的时候,先确认compatible和驱动是否匹配,再看reg地址对不对。

2.3 引脚复用与上拉电阻

I2C 的 SDA 和 SCL 是开漏输出,必须接上拉电阻。一般开发板上会有 4.7k 或者 10k 的上拉,但有些板子为了省成本,上拉电阻没焊,或者阻值太大,导致上升沿太慢,高速通信时波形畸变。这个用示波器一看就知道,但很多人没有示波器,就只能靠“换一块板子试试”来排除。

引脚复用这块,RK3568 的 I2C1 默认可能复用到别的功能上,比如 GPIO 或者 UART。设备树里的pinctrl-0 = <&i2c1_xfer>就是把它切到 I2C 功能。如果这个引用写错,或者 pinctrl 节点本身没定义,引脚就不会切换,波形出不来。我遇到过一种情况:pinctrl 配了,但被后面的节点覆盖了,因为设备树里同一个引脚被多个节点引用,最后生效的是最后一个。这种问题查起来很烦,得把整个 dts 里涉及这个引脚的地方都搜一遍。

提示:设备树改完必须重新编译并更新到板子上,只改源码不烧录等于没改。OpenHarmony 的编译流程里,dtb 是打包进镜像的,单独替换 dtb 有时候不生效,建议整包烧录。

3. HDF 驱动框架下的 I2C 读写实操

3.1 HDF I2C 接口长什么样

OpenHarmony 的 HDF 给 I2C 外设驱动提供了一套标准接口,核心是I2cOpen、I2cClose、I2cTransfer这几个。外设驱动在Bind或者Init阶段拿到I2cController句柄,然后通过I2cTransfer发消息。消息结构是I2cMsg,里面包含从机地址、读写标志、缓冲区指针、长度这些。

一个典型的读 EEPROM 的流程是这样的:先构造一个写消息,把要读的寄存器地址写进去;再构造一个读消息,把数据读出来;两个消息可以放在一个I2cMsg数组里,一次I2cTransfer完成。这叫“复合传输”,中间不发 STOP,保证原子性。如果分成两次 transfer,中间可能被其他线程插入,导致读到的地址不对。

struct I2cMsg msgs[2]; uint8_t reg_addr = 0x00; uint8_t data[16]; msgs[0].addr = 0x50; msgs[0].flags = 0; msgs[0].buf = &reg_addr; msgs[0].len = 1; msgs[1].addr = 0x50; msgs[1].flags = I2C_FLAG_READ; msgs[1].buf = data; msgs[1].len = 16; int32_t ret = I2cTransfer(controller, msgs, 2); if (ret != 2) { HDF_LOGE("i2c transfer failed, ret=%d", ret); }

I2cTransfer的返回值是成功传输的消息个数,正常应该是 2。如果返回小于 2,说明中间某条消息失败了。这时候要看具体是哪个消息失败,可以在每条消息后面单独打印,或者用逻辑分析仪抓波形。

3.2 地址对齐与缓冲区管理

I2cMsg里的buf指针指向的缓冲区,生命周期必须覆盖整个 transfer 过程。如果是在栈上定义的局部数组,transfer 还没返回就出了作用域,就会读到垃圾数据。这个在异步场景下尤其要注意,但 OpenHarmony 的 I2C 默认是同步的,所以栈上数组一般没问题。不过如果用了 DMA,就得保证缓冲区是物理连续且 cache 一致的,这个后面排障部分会讲。

len字段是消息长度,单位是字节。I2C 协议本身没有长度限制,但控制器 FIFO 一般有限制,比如 32 字节或者 64 字节。超过 FIFO 深度的传输,驱动会自动分包,但分包过程中如果从机不支持时钟拉伸,可能会丢数据。所以实际项目里,单次传输尽量别超过 FIFO 深度,大块数据分多次读。

3.3 用户态怎么访问 I2C

如果只是调试,不想写完整驱动,OpenHarmony 也提供了用户态的 I2C 访问接口,通过 HDI 或者直接操作/dev/i2c-x设备节点。不过 OpenHarmony 的/dev下不一定有标准 Linux 那种 i2c-dev 节点,得看具体版本和配置。有些版本是通过 HDI 的I2cHdiService来访问的,需要先拿到服务代理,再调Transfer方法。

用户态访问的好处是快,改一行编译一次就能试,不用重新烧内核。坏处是权限和稳定性问题,生产环境一般还是走内核驱动。我调试的时候喜欢先用用户态接口确认硬件没问题,再写正式驱动。这样能把硬件问题和驱动问题分开,省得混在一起查。

4. 排障实录:I2C 读不出来的七种典型情况

4.1 从机地址算错

这是最高频的问题。前面说过,7 位地址和 8 位地址差一位。还有一种情况是地址引脚接错,比如 EEPROM 的 A0/A1/A2 引脚决定地址低位,如果板子上拉或者下拉和预期不一致,实际地址就变了。排查方法是用i2cdetect类的工具扫一遍总线,看哪个地址有响应。OpenHarmony 里如果没有现成工具,可以写个简单的扫描程序,从 0x03 到 0x77 逐个发空消息,看哪个返回成功。

4.2 上拉电阻缺失或阻值不当

I2C 总线空闲时 SDA 和 SCL 都是高电平,靠上拉电阻维持。如果上拉电阻没焊,或者阻值太大(比如 100k),上升沿会非常慢,波形变成圆弧,高速通信时从机可能识别不到。阻值太小(比如 1k)则功耗大,而且有些从机驱动能力不够,拉不低。一般 4.7k 是折中值,3.3V 系统下 2.2k 到 10k 都常见。没有示波器的话,可以用万用表测空闲时 SDA 和 SCL 对地电压,正常应该接近 VCC,如果明显偏低,说明有器件在拉低或者上拉不够。

4.3 引脚复用冲突

SoC 的引脚往往有多个功能,I2C 只是其中之一。如果设备树里 pinctrl 没配对,或者被其他驱动抢先配置了,I2C 波形就出不来。排查方法是看 pinctrl 的 debugfs 节点,确认引脚当前功能。OpenHarmony 内核一般挂载了 debugfs 到/sys/kernel/debug,里面pinctrl目录能看到每个引脚的复用状态。如果发现 SDA 或 SCL 显示的是 GPIO 而不是 I2C,那就是复用没切过去。

4.4 时钟频率不匹配

I2C 标准模式 100kHz,快速模式 400kHz,高速模式 3.4MHz。从机支持哪种模式,主机就得配成哪种。如果主机跑 400kHz,从机只支持 100kHz,通信就会出错。设备树里一般通过clock-frequency属性配置,比如clock-frequency = <100000>。有些驱动不读这个属性,而是用默认值,那就得改驱动或者确认默认值是否匹配。我遇到过一颗传感器,手册写支持 400kHz,但实际只在 100kHz 下稳定,后来降到 100kHz 就好了。这种属于器件本身的坑,只能实测。

4.5 中断或 DMA 配置问题

如果 I2C 控制器用中断模式,中断号配错或者中断被屏蔽,传输会超时。如果用了 DMA,DMA 通道冲突或者 cache 不一致,会导致数据错乱。排查方法是先关掉 DMA,用纯中断或者轮询模式跑一遍,如果通了,说明问题在 DMA。OpenHarmony 的 I2C 驱动有些支持dmas属性,去掉这个属性就退回中断模式。

4.6 电源和地线问题

这个容易被忽略。从机没供电,或者供电电压不对,自然不响应。地线没共地,电平参考不一致,也会通信失败。尤其是多板子互联的场景,地线必须接。我见过一次,传感器单独供电,和主控没共地,波形看着有,但从机就是不ACK。后来把地线连上就好了。

4.7 总线死锁

I2C 总线可能因为某个从机异常拉低 SDA 而死锁。这时候主机发什么从机都不理,SCL 被拉低或者 SDA 一直低。解决办法是发 9 个时钟脉冲,让从机把剩余数据移完,然后发 STOP。OpenHarmony 的驱动一般有恢复机制,但不一定默认开启。可以在驱动里加一个i2c_recover_bus的调用,或者手动用 GPIO 模拟时钟脉冲来解锁。

现象可能原因排查手段
完全无波形引脚复用错、控制器未使能查 pinctrl debugfs、确认 status
有波形但从机不ACK地址错、从机没供电扫描地址、测从机电压
偶尔成功偶尔失败上拉不足、时钟太快测波形上升沿、降频
传输超时中断配错、从机拉低SCL查中断号、测SCL电平
数据错乱DMA cache不一致关DMA对比

5. 几个容易踩的坑和实操心得

第一个坑是设备树里 I2C 控制器和从机节点的status属性。控制器status = "okay"但从机节点忘了写,或者从机节点写了但控制器是disabled,都会导致设备不 probe。我习惯改完设备树后,用grep把相关节点的 status 都过一遍,确认没有遗漏。

第二个坑是 HDF 驱动的Bind和Init顺序。Bind阶段一般只做资源映射和句柄获取,Init阶段才做实际初始化。如果在Bind里就去读写 I2C,可能控制器还没准备好,直接失败。正确的做法是在Init里做第一次通信测试,失败就返回错误,让框架知道这个驱动没起来。

第三个坑是日志级别。OpenHarmony 的 HDF 日志默认可能只打 ERROR 以上,调试的时候要把级别调到 DEBUG,才能看到 I2C 传输的详细过程。调完记得改回去,不然日志刷屏影响性能。

第四个坑是热插拔。I2C 设备一般不支持热插拔,但有些场景下传感器是插拔式的。如果从机拔掉后主机还在轮询,会一直超时,拖慢系统。这时候要在驱动里加超时重试和错误恢复,连续失败几次就停止轮询,等下次打开设备时再试。

提示:排障时先软后硬。先确认设备树和驱动配置没问题,再动示波器和万用表。很多问题其实是配置问题,硬件是好的。

最后分享一个我常用的调试手法:在 I2C 传输前后各翻转一个 GPIO,用逻辑分析仪同时抓 I2C 波形和这个 GPIO,就能精确知道每次传输的起止时间,以及传输过程中有没有被其他中断打断。这个对分析时序问题特别有用,比单纯看日志直观得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询