恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
OpenHarmony I2C驱动开发与排障实战:从协议原理到设备树配置
首页
资讯中心
/
OpenHarmony I2C驱动开发与排障实战:从协议原理到设备树配置
OpenHarmony I2C驱动开发与排障实战:从协议原理到设备树配置
发布时间:2026/9/29 2:53:33
1. I2C 总线到底是个什么东西I2C 这玩意儿搞嵌入式的基本绕不开。你可以把它想象成一条只有两根线的“小区内部道路”一根是 SCL 时钟线负责打节拍另一根是 SDA 数据线负责传消息。所有挂在总线上的设备——传感器、EEPROM、OLED 屏、触摸芯片——都像小区里的住户共用这两根线靠“门牌号”设备地址来区分谁在说话。它跟 SPI、UART 最大的区别在于I2C 是多主多从、地址寻址、带应答机制的总线。SPI 通常靠片选线一根一根拉低来选设备线多但速度快UART 是点对点两个设备之间直连。I2C 只用两根线就能挂几十个设备代价是速率相对低标准模式 100kHz快速模式 400kHz高速模式 3.4MHz而且总线上任何一个设备拉死 SDA 都会导致整条总线瘫痪。在 OpenHarmony 系统里I2C 是 HDFHardware Driver Foundation驱动框架重点支持的总线类型之一。你会在//drivers/hdf_core/framework/model/i2c这类路径下看到核心实现而具体 SoC 的 I2C 控制器驱动则放在//device/soc/厂商/芯片/hdf/i2c下面。应用层通过 HDF 提供的统一接口去读写 I2C 设备不用关心底层是瑞芯微 RK3568 还是其他芯片。这篇文章适合谁看如果你正在 OpenHarmony 上接传感器、调触摸屏、读 EEPROM或者被“I2C 通信失败”“设备找不到”这类问题卡住过那接下来的内容就是给你准备的。我会从总线原理讲到设备树配置再讲到实际排障尽量把踩过的坑都摊开说。2. I2C 通信协议的核心机制拆解2.1 起始、停止与应答三件事撑起整个协议I2C 的通信过程看起来复杂其实核心就三件事起始条件Start、停止条件Stop、应答ACK/NACK。起始条件是在 SCL 保持高电平的时候SDA 从高变低。停止条件反过来SCL 高电平时 SDA 从低变高。这两个信号是总线上的“标点符号”告诉所有设备“我要开始说话了”和“我说完了”。应答机制是 I2C 可靠性的关键。主机每发送 8 位数据1 字节后会释放 SDA 线然后在第 9 个时钟周期读取 SDA 电平。如果从机把 SDA 拉低就是 ACK表示“收到了”如果保持高就是 NACK表示“没收到”或者“别再发了”。很多排障场景里用逻辑分析仪抓到的“第 9 个时钟没有拉低”基本就是从机没响应原因可能是地址不对、从机没上电、或者从机被复位了。注意起始和停止条件必须由主机产生从机不能主动发起。如果从机想“主动更新主机寄存器”在标准 I2C 里是做不到的只能通过额外的中断线通知主机来读。2.2 数据帧格式地址、读写位、数据、应答一次典型的 I2C 传输帧格式是这样的起始条件7 位从机地址 1 位读写标志0 写1 读从机应答ACK数据字节一个或多个每个数据字节后跟一个应答位停止条件举个例子你要往地址为0x50的 EEPROM 写一个字节到内部地址0x00时序是Start → 0x50W → ACK → 0x00内部地址→ ACK → 数据 → ACK → Stop。读的时候要先写内部地址再 Restart → 0x50R → 读数据 → NACK → Stop。这个“写地址再读”的套路是很多 I2C 设备的标准操作新手容易在这里漏掉 Restart 或者搞错读写位。2.3 时钟同步与仲裁多主机场景下的隐形规则当总线上有多个主机时I2C 靠时钟同步和总线仲裁来避免冲突。时钟同步是指所有主机的 SCL 线是“线与”关系谁的低电平时间长总线就按谁的节奏走。仲裁是指多个主机同时发数据时谁先发出低电平谁就赢输的那个自动退出。实际项目中多主机场景很少见但如果你在 OpenHarmony 上做双核通信或者多主冗余就得注意仲裁失败的主机需要重新发起传输而且仲裁过程中数据不会丢失。这个机制在硬件 I2C 控制器里通常是自动处理的但软件模拟 I2C 就得自己实现复杂度不低。2.4 I2C 与 SMBus、PMBus 的区别很多人会把 I2C 和 SMBus、PMBus 混在一起。简单说SMBus 是 I2C 的一个子集加了超时机制和更严格的电气规范主要用于电源管理。PMBus 又是 SMBus 的扩展专门用于电源管理设备。在 OpenHarmony 里如果你接的是 PMIC 或者电源管理芯片设备树里可能会看到smbus或pmbus的兼容字符串但底层还是 I2C 时序。区别在于超时时间SMBus 要求 35ms 超时I2C 没有这个硬性要求。如果你用 I2C 控制器去驱动 SMBus 设备可能会因为超时机制不匹配而通信失败。3. OpenHarmony 下 I2C 的设备树配置与驱动框架3.1 设备树里 I2C 节点怎么写在 OpenHarmony 的 HDF 驱动框架里设备树Device Tree是描述硬件连接关系的关键文件。以瑞芯微 RK3568 为例I2C 控制器的节点通常在kernel/linux/patches/linux-5.10/arch/arm64/boot/dts/rockchip/rk3568.dtsi里定义而具体板级的 I2C 设备挂载则在rk3568-evb.dts这类文件里。一个典型的 I2C 控制器节点长这样i2c1: i2cfe5a0000 { 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; };然后在板级文件里挂设备i2c1 { status okay; clock-frequency 100000; 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; }; };这里有几个关键点reg属性就是 I2C 从机地址clock-frequency是总线速率interrupts和reset-gpios是触摸芯片的中断和复位引脚。如果这些配错了设备根本不会出现在/dev下面或者驱动 probe 直接失败。3.2 HDF 驱动框架里的 I2C 接口OpenHarmony 的 HDF 框架给 I2C 提供了统一的接口核心头文件在//drivers/hdf_core/framework/include/platform/i2c_if.h。常用的几个函数I2cOpen(int16_t busNum)打开指定编号的 I2C 总线返回句柄。I2cClose(int16_t busNum)关闭总线。I2cTransfer(int16_t busNum, struct I2cMsg *msgs, int16_t count)执行一次传输支持多个消息连续发送。I2cTransferWithLock带锁的传输多线程场景下用。struct I2cMsg的结构大概是struct I2cMsg { uint16_t addr; // 从机地址 uint16_t flags; // 读写标志0 写1 读 uint16_t len; // 数据长度 uint8_t *buf; // 数据缓冲区 };实际写驱动的时候你通常会先I2cOpen然后构造I2cMsg数组调用I2cTransfer最后I2cClose。如果是连续读写比如先写寄存器地址再读数据可以把两个I2cMsg放在一个数组里一次I2cTransfer完成中间会自动产生 Restart 而不是 Stop。3.3 设备树与驱动匹配的底层逻辑设备树里的compatible字符串和驱动里的of_match_table是一一对应的。比如goodix,gt911会匹配到 GT911 触摸驱动。如果匹配不上驱动不会 probe设备也不会注册到 HDF 设备管理里。在 OpenHarmony 里I2C 设备的驱动通常分为两层平台驱动I2C 控制器驱动和器件驱动具体传感器/触摸芯片驱动。平台驱动负责初始化控制器、配置时钟和引脚器件驱动负责通过I2cTransfer读写寄存器。两层通过 HDF 的设备节点关联起来。实操心得如果你在设备树里改了 I2C 节点但驱动没反应先检查status是不是okay再检查compatible是否和驱动匹配最后看reg地址是否和硬件原理图一致。这三步能解决 80% 的“设备不识别”问题。4. I2C 排障实战从现象到根因的完整链路4.1 常见故障现象与初步定位I2C 出问题现象通常就几种设备不识别、读写超时、数据错乱、总线死锁。下面这张表是我在实际项目中整理出来的速查表现象可能原因排查手段设备不识别地址错误、上电时序不对、设备树未使能逻辑分析仪抓起始帧看地址是否匹配读写超时从机没应答、SCL 被拉低、时钟频率过高示波器看 SCL/SDA 波形降低速率测试数据错乱时序抖动、上拉电阻过大、干扰检查上拉电阻通常 4.7kΩ缩短走线总线死锁从机拉死 SDA、主机复位不完整手动发送 9 个时钟脉冲解锁概率性失败电源纹波、地弹、EMI加滤波电容检查地平面逻辑分析仪是排障利器。我通常用 Saleae 或者便宜的逻辑分析仪抓 I2C 波形重点看起始条件是否干净、地址帧是否匹配、第 9 个时钟是否有 ACK、停止条件是否完整。如果地址帧发出去没有 ACK基本就是从机没响应。4.2 用逻辑分析仪抓 I2C 波形的实操步骤第一步把逻辑分析仪的通道 0 接 SCL通道 1 接 SDA地线接板子地。第二步在软件里设置 I2C 解码选择正确的时钟通道和数据通道。第三步触发方式设为 SDA 下降沿起始条件采样率至少 1MHz 以上最好 4MHz。第四步让程序发起一次 I2C 传输抓取波形。抓到的波形里你可以直接看到地址、读写位、数据、ACK/NACK。如果地址是0x5D但波形里显示0x5C那就是地址左移了一位或者右移了一位的问题。I2C 的 7 位地址在传输时是左移一位最低位是读写位所以0x5D写操作实际发送的是0x5A读操作是0x5B。这个细节很容易搞错。注意有些逻辑分析仪的 I2C 解码器会把 7 位地址和读写位合并显示有些会分开显示。看波形的时候先确认软件的解码设置别被显示格式误导。4.3 总线死锁的解锁方法总线死锁是 I2C 最恶心的问题之一。现象是 SDA 被某个从机拉低主机发不了起始条件整个总线瘫痪。原因通常是从机在传输过程中被复位或者电源抖动导致状态机卡死。解锁方法有两种。硬件方法是给从机断电重启简单粗暴但有效。软件方法是在 SCL 上手动发送 9 个时钟脉冲让从机把剩余的数据位发完然后发一个停止条件。在 OpenHarmony 里你可以通过 GPIO 模拟 SCL 来实现// 伪代码手动发送 9 个时钟脉冲 gpio_set_direction(SCL_PIN, OUTPUT); gpio_set_direction(SDA_PIN, INPUT); for (int i 0; i 9; i) { gpio_set_value(SCL_PIN, 0); udelay(5); gpio_set_value(SCL_PIN, 1); udelay(5); } // 然后发送停止条件 gpio_set_direction(SDA_PIN, OUTPUT); gpio_set_value(SDA_PIN, 0); udelay(5); gpio_set_value(SCL_PIN, 1); udelay(5); gpio_set_value(SDA_PIN, 1);这个方法我实测过多次对大多数从机有效。但如果从机彻底挂死还是得断电。4.4 设备树配置错误的典型表现设备树配错表现往往很隐蔽。比如clock-frequency设成了 400kHz但从机只支持 100kHz结果就是概率性读写失败。或者reg地址写成了 8 位地址比如0x5D写成了0xBA驱动 probe 时读不到设备 ID直接返回失败。还有一种情况是引脚复用没配对。RK3568 的 I2C 引脚可能和 GPIO、UART 复用如果pinctrl-0没指向正确的引脚组SCL/SDA 根本没有波形。这时候用示波器看引脚会发现一直是高电平或者低电平没有时钟信号。实操心得改设备树之后一定要重新编译 dtb 并确认烧录成功。我遇到过好几次改了 dts 但没重新打包 boot.img结果调试半天发现用的还是旧设备树。确认方法是在系统起来后去/proc/device-tree下面看对应节点或者用dmesg | grep i2c看内核打印。5. I2C 读写 EEPROM 的完整代码示例与参数计算5.1 硬件连接与上拉电阻选择以 AT24C02 为例容量 2Kbit256 字节7 位地址是1010xxx其中 xxx 由 A0/A1/A2 引脚决定。如果全部接地地址就是0x50。SCL 和 SDA 各接一个 4.7kΩ 上拉电阻到 3.3V。上拉电阻的选择有讲究。阻值太大上升沿变缓高速通信时波形失真阻值太小功耗增加从机可能拉不低。标准模式 100kHz 下4.7kΩ 是经验值快速模式 400kHz 下可以用 2.2kΩ 到 4.7kΩ。计算公式是R t_r / (0.8473 * C)其中t_r是允许的上升时间标准模式 1000ns快速模式 300nsC是总线电容包括引脚电容和走线电容通常 100pF 到 200pF。按 100kHz、200pF 算R 1000ns / (0.8473 * 200pF) ≈ 5.9kΩ所以 4.7kΩ 是合理的。5.2 写 EEPROM 的时序与代码写 AT24C02 一个字节的流程Start → 0x50W → ACK → 内部地址 → ACK → 数据 → ACK → Stop。写完之后需要等待 5ms 左右的内部写周期期间从机不响应任何请求。在 OpenHarmony 的 HDF 框架下代码大概是这样int32_t EepromWriteByte(int16_t busNum, uint8_t innerAddr, uint8_t data) { int32_t ret; uint8_t buf[2] {innerAddr, data}; struct I2cMsg msg { .addr 0x50, .flags 0, // 写 .len 2, .buf buf, }; ret I2cTransfer(busNum, msg, 1); if (ret ! 1) { HDF_LOGE(eeprom write failed, ret%d, ret); return HDF_FAILURE; } OsalMSleep(10); // 等待内部写周期 return HDF_SUCCESS; }注意I2cTransfer的返回值是成功传输的消息数量返回 1 表示成功返回负数表示失败。OsalMSleep(10)是保险起见等 10ms实际 AT24C02 的写周期最大 5ms。5.3 读 EEPROM 的时序与代码读操作稍微复杂一点要先写内部地址再 Restart 读数据int32_t EepromReadByte(int16_t busNum, uint8_t innerAddr, uint8_t *data) { int32_t ret; uint8_t addrBuf innerAddr; uint8_t dataBuf 0; struct I2cMsg msg[2] { { .addr 0x50, .flags 0, // 写 .len 1, .buf addrBuf, }, { .addr 0x50, .flags 1, // 读 .len 1, .buf dataBuf, }, }; ret I2cTransfer(busNum, msg, 2); if (ret ! 2) { HDF_LOGE(eeprom read failed, ret%d, ret); return HDF_FAILURE; } *data dataBuf; return HDF_SUCCESS; }两个I2cMsg放在一个数组里I2cTransfer会自动在第一个消息后产生 Restart而不是 Stop。这是 I2C 协议里“复合传输”的标准做法很多新手会分成两次I2cTransfer调用中间产生 Stop导致读出来的数据不对。5.4 页写与连续读的边界处理AT24C02 支持页写一页 8 字节。如果你写超过 8 字节地址会回卷到页首覆盖之前的数据。所以写多字节时要分页处理int32_t EepromWritePage(int16_t busNum, uint8_t innerAddr, uint8_t *data, uint16_t len) { uint16_t written 0; while (written len) { uint8_t pageOffset innerAddr % 8; uint8_t pageRemain 8 - pageOffset; uint8_t chunk (len - written) pageRemain ? (len - written) : pageRemain; // 构造 buf写入 chunk 字节 // ... written chunk; innerAddr chunk; OsalMSleep(10); } return HDF_SUCCESS; }连续读没有页边界问题因为读操作是顺序递增地址的读完最后一个字节发 NACK 再 Stop 就行。6. 那些年我踩过的 I2C 坑与独家避坑技巧6.1 地址左移一位的经典错误I2C 的 7 位地址在传输时是左移一位最低位是读写位。比如0x5D写操作实际发送的是0x5A读操作是0x5B。很多驱动代码里直接写addr 0x5D然后flags 0底层会自动左移。但如果你用软件模拟 I2C就得自己处理这个移位。我见过有人把0x5D直接当 8 位地址发结果从机根本不响应。避坑技巧在逻辑分析仪里看地址时先确认软件显示的是 7 位地址还是 8 位地址。如果是 8 位最低位就是读写位去掉最低位再右移一位才是真正的 7 位地址。6.2 上拉电阻缺失导致波形畸变有些开发板为了省成本I2C 引脚没有焊上拉电阻靠芯片内部弱上拉。内部上拉通常几十 kΩ上升沿非常缓100kHz 下可能勉强能用400kHz 直接失败。现象是逻辑分析仪抓到的波形上升沿是圆弧形不是方波。解决办法是在 SCL 和 SDA 上各焊一个 4.7kΩ 电阻到 3.3V。如果板子已经打样了可以在排线中间接一个电阻或者用飞线。我试过在 SDA 上飞一个 2.2kΩ波形立刻变干净。6.3 电源时序不对导致设备不响应有些传感器要求 I2C 上电之前VDD 必须先稳定。如果 VDD 和 I2C 同时上电传感器内部状态机可能没初始化完导致前几次通信失败。GT911 触摸芯片就有这个问题复位引脚和 I2C 上电时序有严格要求。解决办法是在设备树里配置reset-gpios驱动 probe 时先拉低复位延时 10ms再拉高再延时 50ms然后才开始 I2C 通信。这个时序在 GT911 的数据手册里有明确要求但很多人不看手册直接上电就通信结果概率性失败。6.4 多设备挂载时的地址冲突I2C 总线上每个设备地址必须唯一。如果你挂了两个相同型号的传感器而它们的地址引脚都接地就会冲突。解决办法是通过地址引脚配置不同地址或者用 I2C 多路复用器如 TCA9548A扩展总线。TCA9548A 是一个 8 通道 I2C 多路复用器主机通过写它的寄存器来选择哪个通道导通。在 OpenHarmony 里你需要先写 TCA9548A 的地址通常是0x70选择通道然后再和通道上的设备通信。这个芯片解决了我很多“多个相同传感器”的场景。6.5 中断与轮询的取舍很多 I2C 设备支持中断输出比如触摸芯片、加速度计。用中断还是轮询取决于实时性要求和功耗。中断方式下设备有数据时拉低中断线主机在中断处理里读 I2C。轮询方式下主机定时读设备寄存器。在 OpenHarmony 里中断方式需要配置interrupt-parent和interrupts驱动里注册中断处理函数。轮询方式简单但功耗高。我一般建议触摸屏用中断传感器用轮询除非低功耗要求极高。实操心得中断方式下中断处理函数里不要做太耗时的 I2C 读写最好用工作队列或者线程去处理。I2C 传输本身可能睡眠在中断上下文里直接调用会出问题。7. 从 I2C 到其他总线的扩展思考I2C 只是嵌入式总线家族的一员。SPI 速度更快适合 OLED 屏、Flash 存储UART 简单直接适合调试串口、GPS 模块CAN 总线抗干扰强适合汽车电子I2S 专门传音频。选型的时候速率、距离、设备数量、功耗都是考量因素。在 OpenHarmony 的 HDF 框架里这些总线都有对应的统一接口。你学会了 I2C 的设备树配置和 HDF 调用方式再去看 SPI 或者 UART会发现套路是一样的设备树里配节点驱动里用SpiTransfer或UartWrite。底层逻辑相通只是时序和协议不同。我个人在实际操作中的体会是I2C 排障最核心的能力不是看代码而是看波形。逻辑分析仪抓一次波形比读十遍数据手册都管用。另外设备树配置一定要和硬件原理图对照地址、引脚、上拉电阻一个都不能错。最后再分享一个小技巧如果你怀疑是 I2C 总线本身的问题可以先把所有从机断开只留一个用最简单的读写测试。如果单个设备能通再逐个加设备这样能快速定位是哪个设备拉死了总线。