
1. I2C 总线在 OpenHarmony 里的定位与整体设计思路I2C 这东西搞嵌入式的基本绕不开。它不像 SPI 那样追求速度也不像 UART 那样点对点简单粗暴它的价值在于两根线挂一串设备——一根 SCL 时钟线一根 SDA 数据线靠地址区分从机靠上拉电阻把总线拉高。在 OpenHarmony 的 HDF 驱动框架里I2C 被抽象成了一套标准的设备访问接口上层业务不需要关心底层是哪个 SoC 的 I2C 控制器只要拿到对应的控制器句柄就能对挂载的传感器、EEPROM、触摸屏等外设进行读写。我接触 OpenHarmony 的 I2C 开发最早是在一块 RK3568 的开发板上调 GT911 触摸屏。当时遇到的问题是触摸无响应日志里 I2C 传输直接超时。排查了一圈才发现设备树里 I2C 控制器的引脚复用没配对SCL 和 SDA 被别的功能占用了。这件事让我意识到I2C 排障的核心不在协议本身而在于从设备树到控制器寄存器再到外设地址这一整条链路的正确性。这套教程面向的是已经有一定嵌入式基础、准备在 OpenHarmony 上做外设驱动的开发者。如果你之前只在 STM32 上用 HAL 库调过 I2C那 OpenHarmony 的 HDF 框架会给你一套完全不同的编程模型。但底层时序、地址格式、ACK/NACK 机制这些东西是不变的理解了这些上层框架只是换了个壳。整体设计上我会按照“协议基础 → 设备树配置 → HDF 驱动编写 → 实际读写 → 排障方法论”这条线来展开。每个环节都会给出可复现的配置和代码同时解释为什么这么做。比如设备树里clock-frequency为什么通常设 100kHz 而不是 400kHz上拉电阻阻值怎么算这些细节在实际项目中经常被忽略但恰恰是问题的高发区。2. I2C 协议核心细节与 OpenHarmony 适配要点2.1 时序基础从起始条件到停止条件I2C 的时序说起来简单但真正用逻辑分析仪抓过波形的人才知道细节里全是坑。起始条件START是 SCL 高电平期间 SDA 从高变低停止条件STOP是 SCL 高电平期间 SDA 从低变高。这两个条件之间的每一个时钟脉冲SDA 上的数据必须在 SCL 低电平期间变化在 SCL 高电平期间保持稳定。数据帧格式是这样的起始条件之后主机发送 7 位从机地址加 1 位读写方向位0 写 1 读然后从机拉低 SDA 产生 ACK。如果是写操作接着发送寄存器地址再发数据如果是读操作重新发送起始条件Restart发送地址加读方向位然后从机返回数据主机每接收一个字节后发送 ACK最后一个字节发送 NACK最后发停止条件。注意很多初学者分不清“寄存器地址”和“从机地址”。从机地址是设备在总线上的身份标识比如 GT911 通常是 0x5D 或 0x14寄存器地址是设备内部存储单元的偏移比如触摸坐标存在 0x814E。两者是完全不同的概念。在 OpenHarmony 的 HDF 框架里这些时序细节被封装在 I2C 控制器驱动中你调用I2cTransfer时传入的消息结构体里包含了地址、缓冲区、长度和标志位。但封装不代表你可以忽略时序因为总线速率、上拉强度、线长这些物理层因素会直接影响时序质量。2.2 设备树中的 I2C 节点配置OpenHarmony 使用设备树来描述硬件资源I2C 控制器和挂载设备都需要在设备树里声明。以 RK3568 为例I2C 控制器的节点通常在rk3568.dtsi里定义板级设备树里只需要把状态改成okay并配置引脚。i2c3 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c3m0_xfer; gt911: touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 RK_PB6 GPIO_ACTIVE_LOW; irq-gpios gpio0 RK_PB5 GPIO_ACTIVE_HIGH; }; };这里有几个关键点。clock-frequency设成 100000 表示 100kHz这是 I2C 的标准模式。为什么不直接上 400kHz因为很多传感器和触摸屏在 400kHz 下时序余量不足尤其是线长超过 10cm 或者上拉电阻偏大的时候波形上升沿变缓高速下容易误码。我实测 GT911 在 400kHz 下偶尔丢中断降到 100kHz 后稳定运行。reg属性就是从机地址GT911 的地址由复位时的 INT 引脚电平决定拉低是 0x5D拉高是 0x14。这个细节在调试时经常被忽略导致地址对不上。pinctrl-0引用的引脚配置节点决定了 SCL 和 SDA 映射到哪两个物理引脚。RK3568 的 I2C3 有多组引脚可选如果配错了总线直接没波形。2.3 上拉电阻的计算与选型I2C 总线是开漏输出必须靠上拉电阻把线拉高。上拉电阻的阻值不是随便选的它和总线电容、上升时间、功耗都有关系。上升时间公式是tr 0.847 × R × C其中 R 是上拉电阻C 是总线总电容。标准模式 100kHz 下上升时间不能超过 1000ns。假设总线电容 200pF那么 R 最大约 5.9kΩ。同时R 太小会导致低电平时灌电流过大一般 I2C 引脚灌电流能力在 3mA 左右3.3V 供电下 R 最小约 1.1kΩ。所以常用范围是 2.2kΩ 到 10kΩ。我一般先用 4.7kΩ 打底如果波形上升沿太缓就降到 2.2kΩ如果功耗敏感就升到 10kΩ。很多开发板已经焊了上拉电阻外接模块时要注意不要重复上拉否则等效阻值变小灌电流可能超标。提示用示波器看 SCL 和 SDA 波形时如果上升沿明显变缓或者顶部有圆角说明上拉太弱或者总线电容太大。如果低电平下不去说明上拉太强或者有短路。3. OpenHarmony HDF 框架下的 I2C 驱动实操3.1 HDF I2C 接口的调用方式OpenHarmony 的 HDF 框架提供了两套 I2C 访问接口一套是内核态的I2cTransfer一套是用户态的I2cOpen/I2cRead/I2cWrite。内核态驱动通常用前者用户态测试工具用后者。内核态调用的典型流程是这样的struct I2cMsg msgs[2]; uint8_t regAddr 0x00; uint8_t data[2] {0}; msgs[0].addr 0x50; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf regAddr; msgs[1].addr 0x50; msgs[1].flags I2C_FLAG_READ; msgs[1].len 2; msgs[1].buf data; int32_t ret I2cTransfer(handle, msgs, 2); if (ret ! 2) { HDF_LOGE(I2cTransfer failed, ret %d, ret); }这里msgs数组包含两条消息第一条写寄存器地址第二条读数据。I2cTransfer返回成功传输的消息数量如果是 2 就表示两条都成功了。如果返回负数说明出错了需要根据错误码排查。flags字段里可以设置I2C_FLAG_READ表示读操作I2C_FLAG_NO_START表示不发起始条件用于连续读I2C_FLAG_10BIT_ADDR表示 10 位地址模式。大部分场景下用 7 位地址就够了。3.2 编写一个 EEPROM 读写驱动以 AT24C02 EEPROM 为例它的从机地址是 0x50每页 8 字节总容量 256 字节。写操作需要先发寄存器地址再发数据读操作需要先写寄存器地址再重启读。#define EEPROM_ADDR 0x50 static int32_t EepromWrite(I2cHandle *handle, uint8_t reg, uint8_t *buf, uint32_t len) { struct I2cMsg msgs[2]; msgs[0].addr EEPROM_ADDR; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; msgs[1].addr EEPROM_ADDR; msgs[1].flags 0; msgs[1].len len; msgs[1].buf buf; return I2cTransfer(handle, msgs, 2); } static int32_t EepromRead(I2cHandle *handle, uint8_t reg, uint8_t *buf, uint32_t len) { struct I2cMsg msgs[2]; msgs[0].addr EEPROM_ADDR; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; msgs[1].addr EEPROM_ADDR; msgs[1].flags I2C_FLAG_READ; msgs[1].len len; msgs[1].buf buf; return I2cTransfer(handle, msgs, 2); }写 EEPROM 时要注意页边界。AT24C02 每页 8 字节如果一次写入跨越页边界会回卷到页首覆盖之前的数据。所以写多字节时要按页拆分。另外每次写操作后 EEPROM 需要约 5ms 的内部写周期期间不响应总线连续写要加延时或者用 ACK 轮询。注意EEPROM 的写周期轮询是一种常见优化。发送起始条件加地址如果 EEPROM 返回 ACK 说明写周期结束可以继续下一次写如果 NACK 就继续等待。这样比固定延时更高效。3.3 用户态 I2C 调试工具的使用OpenHarmony 提供了i2c_tools命令行工具可以在用户态直接读写 I2C 设备。用法如下# 列出所有 I2C 控制器 i2c_tools list # 扫描 I2C-3 总线上的设备 i2c_tools detect 3 # 读取 I2C-3 上地址 0x50 的寄存器 0x00 i2c_tools read 3 0x50 0x00 2 # 向 I2C-3 上地址 0x50 的寄存器 0x00 写入 0xAA 0xBB i2c_tools write 3 0x50 0x00 0xAA 0xBB这个工具在调试阶段非常有用。比如你不确定触摸屏的地址是 0x5D 还是 0x14直接detect扫一遍就知道了。如果扫描不到任何设备说明硬件连接或者设备树配置有问题。我一般排障的顺序是先用detect确认设备在线再用read读设备 ID 寄存器验证通信正常最后才去查驱动逻辑。这样能把问题范围快速缩小到硬件、设备树还是驱动代码。4. I2C 排障方法论与常见问题速查4.1 从波形到日志的排查路径I2C 出问题表现无非几种传输超时、返回 NACK、数据错误、设备不响应。排查的时候我习惯按这个顺序走第一步确认设备树配置。检查 I2C 控制器状态是否okay引脚复用是否正确从机地址是否匹配。这一步能解决大部分“完全没反应”的问题。第二步用逻辑分析仪抓波形。看起始条件有没有发出来SCL 有没有时钟SDA 上有没有地址帧从机有没有拉低 ACK。如果起始条件都没有说明控制器驱动没工作如果有地址帧但没 ACK说明从机地址不对或者从机没上电。第三步看内核日志。OpenHarmony 的 HDF 框架会在传输失败时打印错误码比如-EIO表示总线错误-ETIMEDOUT表示超时-ENODEV表示设备不存在。根据错误码能快速定位是控制器问题还是设备问题。第四步检查上拉电阻和电源。用万用表量 SCL 和 SDA 的静态电平正常应该是高电平。如果量出来是低电平说明有设备把总线拉死了需要逐个断开设备排查。4.2 常见问题速查表现象可能原因排查方法解决方案传输超时总线被拉死、上拉缺失、引脚复用错误量 SCL/SDA 静态电平查设备树 pinctrl修复引脚配置检查上拉电阻返回 NACK从机地址错误、从机未上电、从机忙用 detect 扫描地址量从机电源修正地址等待从机就绪数据错误速率过高、线太长、干扰抓波形看时序余量降低 clock-frequency缩短线长设备时好时坏上拉太弱、电源纹波、接触不良看波形上升沿量电源纹波减小上拉阻值加滤波电容多设备冲突地址重复、总线电容过大逐个挂载测试修改地址减少挂载数量4.3 几个容易踩的坑第一个坑是地址左移。有些驱动代码里把 7 位地址左移一位再或上读写位有些则直接传 7 位地址由框架处理。OpenHarmony 的I2cMsg里addr字段填的是 7 位地址不需要手动左移。我见过有人在 HDF 里手动左移结果地址翻倍怎么都通不了。第二个坑是重复起始条件。读操作需要先写寄存器地址再重启读这两条消息之间的起始条件是 Restart 而不是 Stop。如果驱动实现成了 Stop 再 Start有些设备会复位内部地址指针导致读出来的数据不对。OpenHarmony 的I2cTransfer默认在消息之间发 Restart不需要额外配置。第三个坑是时钟拉伸。有些从机在处理数据时会拉低 SCL 来暂停主机时钟这叫时钟拉伸。如果主机不支持时钟拉伸就会误判为总线错误。大部分 SoC 的 I2C 控制器都支持但需要在设备树里确认没有禁用。第四个坑是电源域和引脚复用。有些 SoC 的 I2C 引脚默认复用成 GPIO 或其他功能必须在 pinctrl 里显式配置成 I2C 功能。RK3568 的 I2C3 就有多组引脚配错了就没波形。5. 从 I2C 扩展到其他总线的思路I2C 调通了再去看 SPI、UART、CAN 这些总线会发现很多概念是相通的。比如 SPI 也有主机从机、时钟极性相位、片选信号CAN 也有仲裁、ACK、错误帧。区别在于 I2C 是地址寻址、开漏输出、支持多主多从而 SPI 是片选寻址、推挽输出、通常单主多从。在 OpenHarmony 的 HDF 框架里每种总线都有对应的接口抽象。I2C 用I2cTransferSPI 用SpiTransferUART 用UartWrite/UartRead。设备树里的节点结构也类似都是控制器节点加设备子节点。学会了 I2C 的设备树配置和驱动编写迁移到其他总线时主要就是换接口函数和调整参数。我在实际项目里经常遇到 I2C 和 SPI 混用的场景比如触摸屏用 I2C显示屏用 SPI传感器用 I2CFlash 用 SPI。这时候设备树里要把每个控制器都配好引脚不能冲突电源域要独立。调试的时候一个一个来先确保每个总线单独能通再整合到一起。最后分享一个我常用的技巧在设备树里给每个 I2C 设备加一个status okay的同时也在驱动里加一行日志打印实际读到的设备 ID。这样每次系统启动时日志里就能看到哪些设备被正确识别了哪些没有。对于批量生产或者多配置的板子这个习惯能省下大量排查时间。