ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenHarmony I2C实战:从设备树配置到排障优化

OpenHarmony I2C实战:从设备树配置到排障优化 1. 从一根线说起I2C 在 OpenHarmony 里到底扮演什么角色搞 OpenHarmony 设备开发的朋友尤其是做瑞芯微 RK3568、Hi3861、RK3399 这类板子的绕不开的一个东西就是 I2C。你接个温湿度传感器、挂个 OLED 屏、读个 EEPROM、驱动个触摸芯片 GT911甚至接个 AS5600 磁编码器底层跑的基本都是 I2C。它不像 SPI 那样要一堆片选线也不像 UART 那样只能点对点两根线——SDA 和 SCL——就能挂一串设备这就是它最香的地方。但香归香I2C 也是实际开发里最容易翻车的总线之一。我见过太多人代码写得没问题设备树也配了结果 i2c 工具一敲返回个Remote I/O error然后就开始怀疑人生。问题往往出在几个地方设备地址搞错了、上拉电阻没焊、时钟频率太高、设备树节点没使能、或者总线上挂了两个地址冲突的器件。这些坑官方文档不会一条条告诉你得自己踩过才知道疼。这篇内容就是围绕 OpenHarmony 系统下 I2C 的实战使用和排障来展开的。我会从总线的基本原理讲起然后落到 OpenHarmony 的 HDI 接口怎么调、设备树怎么配、用户态怎么用 i2c-tools 验证最后重点讲排障——因为这才是真正花时间的地方。不管你是刚接触 OpenHarmony 的新手还是从 Linux 驱动转过来的老手这里面的东西应该都能直接用上。先说清楚适用场景你手里有一块跑 OpenHarmony 的开发板需要接一个 I2C 外设或者你已经在接但通信不正常需要排查。文章里的命令和配置以 RK3568 平台为例但思路和大部分操作在其他平台上也是通用的。2. I2C 通信协议核心机制拆解2.1 两根线怎么就把数据传明白了I2C 的物理层特别简单SDA 是数据线SCL 是时钟线两根线都是开漏输出所以必须接上拉电阻。这个上拉电阻不是可选项是必须的。很多模块出厂时自带上拉但有些没有你得自己补。典型值是 4.7kΩ如果总线电容大或者速率高可能要降到 2.2kΩ 甚至 1kΩ。通信过程是这样的主机拉低 SDA 同时 SCL 保持高电平这叫起始条件Start。然后主机在 SCL 低电平期间把数据位放到 SDA 上在 SCL 高电平期间从机采样。每传 8 位数据第 9 个时钟周期从机拉低 SDA 表示应答ACK不拉低就是非应答NACK。最后主机在 SCL 高电平期间把 SDA 从低拉到高这叫停止条件Stop。这里有个细节很多人忽略起始和停止条件之间的 SDA 变化必须在 SCL 低电平期间完成。如果你在 SCL 高电平的时候动了 SDA从机会把它当成起始或停止条件通信直接乱掉。这就是为什么 I2C 的时序图看起来那么“规矩”——它不是随便画的是硬件逻辑决定的。2.2 7 位地址和 10 位地址别搞混了标准 I2C 用 7 位地址所以理论上一条总线最多挂 112 个设备去掉保留地址。但实际用的时候很多传感器的地址是固定的比如 SSD1306 OLED 通常是 0x3C 或 0x3DGT911 触摸芯片常见的是 0x5D 或 0x14。地址冲突是排障时第一个要查的东西。10 位地址在 OpenHarmony 的常规外设开发里很少见大部分传感器和模块都是 7 位的。但你要知道有这回事万一遇到一个用 10 位地址的器件别以为是通信坏了。还有一个容易踩的坑数据手册上给的地址有时候是 8 位的包含了读写位比如写地址 0x78、读地址 0x79实际 7 位地址是 0x3C。你在代码里填地址的时候要填 7 位的那个不然怎么调都不通。这个坑我至少见过五六个人踩过。2.3 时钟频率不是越高越好I2C 标准模式 100kHz快速模式 400kHz高速模式 3.4MHz。OpenHarmony 里默认一般是 100kHz 或 400kHz。很多人为了追求读取速度直接把频率拉到 400kHz 甚至更高结果通信不稳定偶尔丢数据。频率能不能拉高取决于几个因素总线电容、上拉电阻阻值、走线长度、从机支持的最高频率。总线电容越大上升沿越缓高频下波形还没到高电平就被拉低了数据自然出错。经验值是如果走线超过 10cm或者挂了 3 个以上设备老老实实跑 100kHz。稳定比快重要。如果你用逻辑分析仪抓过波形会发现 400kHz 下的上升沿明显比 100kHz 更“斜”。这不是示波器的问题是 RC 充电的自然结果。上拉电阻 R 和总线电容 C 决定了上升时间τRC上升时间大约是 2.2RC。算一下就知道4.7kΩ 配上 200pF 的电容上升时间大概 2μs在 400kHz 下周期 2.5μs已经占了一大半留给高电平的时间非常紧张。3. OpenHarmony 下 I2C 的设备树配置与 HDI 接口3.1 设备树里怎么描述一个 I2C 设备OpenHarmony 的设备树沿用 Linux 的风格I2C 控制器节点下面挂设备子节点。以 RK3568 为例I2C1 控制器的设备树大概长这样i2c1 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c1m0_xfer; ssd1306: ssd13063c { compatible solomon,ssd1306; reg 0x3c; status okay; }; };几个关键点status必须是okay不然控制器根本不工作clock-frequency决定总线速率reg填的是 7 位地址compatible要和驱动里的匹配表对上。pinctrl 配置也很重要如果引脚复用没配对SDA 和 SCL 根本没有波形输出。我遇到过一种情况设备树里 I2C 节点配了但引脚被别的功能占用了比如被配成了 GPIO 或者 UART。这时候 i2c 工具能检测到控制器但发数据没反应。排查方法是在设备树里搜一下这个引脚有没有被其他节点引用或者直接看 pinctrl 的复用配置。3.2 HDI 接口OpenHarmony 的 I2C 用户态入口OpenHarmony 的 HDIHardware Device Interface层给用户态提供了统一的 I2C 访问接口。核心接口在i2c_if.h里定义主要用到这几个函数int32_t I2cOpen(int16_t number); int32_t I2cClose(int16_t number); int32_t I2cTransfer(int16_t number, struct I2cMsg *msgs, int16_t count);I2cOpen打开指定编号的 I2C 控制器返回一个句柄。I2cTransfer是核心它接受一个I2cMsg数组每个 msg 包含设备地址、读写标志、数据缓冲区和长度。一次 transfer 可以包含多个 msg实现“写寄存器地址读数据”这种组合操作中间不会插入停止条件这对很多传感器来说是必须的。struct I2cMsg { uint16_t addr; uint16_t flags; uint16_t len; uint8_t *buf; };flags里I2C_FLAG_READ表示读I2C_FLAG_NO_STOP表示这个 msg 结束后不发停止条件。比如读一个寄存器通常先发写 msg寄存器地址再发读 msg两个 msg 之间用 NO_STOP 连起来。3.3 用户态直接操作i2c-tools 在 OpenHarmony 上的使用OpenHarmony 标准系统里一般会带 i2c-tools如果没有可以自己交叉编译一个。常用命令# 列出所有 I2C 控制器 i2cdetect -l # 扫描 I2C-1 上的设备 i2cdetect -y 1 # 读寄存器 i2cget -y 1 0x3c 0x00 # 写寄存器 i2cset -y 1 0x3c 0x00 0x01 # 批量读 i2cdump -y 1 0x3ci2cdetect -y 1是最常用的排障命令。如果扫描出来地址位置显示UU说明这个地址被驱动占用了显示具体地址值说明设备在线显示--说明没检测到设备。如果整行都是--那就要查硬件了。注意i2cdetect 的扫描原理是发地址然后看有没有 ACK。有些设备不支持这种快速扫描可能会返回 NACK但实际上它是正常工作的。所以扫描不到不一定代表设备坏了要结合具体器件的数据手册判断。4. 完整实操从零接一个 I2C 传感器并读数据4.1 硬件连接与上拉电阻确认假设我们要接一个常见的温湿度传感器7 位地址 0x44SDA 接开发板的 I2C1_SDASCL 接 I2C1_SCLVCC 接 3.3VGND 共地。第一步不是写代码是拿万用表量上拉电阻。把传感器拔掉量 SDA 对 VCC 的阻值量 SCL 对 VCC 的阻值。如果都是 4.7kΩ 左右说明上拉没问题。如果量出来是无穷大说明没有上拉需要自己加。如果量出来是几百欧姆可能上拉太强了高频下功耗会比较大。我实际遇到过一块板子SDA 和 SCL 的上拉电阻焊反了位置导致 SDA 上拉到了 5VSCL 上拉到了 3.3V。虽然大部分时候能通信但偶尔会出错。这种硬件问题软件怎么调都没用必须先排除。4.2 设备树添加节点并编译在对应的 dts 文件里找到 i2c1 节点添加子节点i2c1 { status okay; clock-frequency 100000; sht30: sht3044 { compatible sensirion,sht30; reg 0x44; status okay; }; };编译设备树烧录重启。然后进系统用i2cdetect -y 1看 0x44 位置有没有显示。如果显示44说明设备树配置生效了设备也被识别到了。如果显示UU说明有驱动占用了这个地址但你还没加载对应的驱动这时候需要检查内核配置里有没有使能这个传感器的驱动。如果显示--回到硬件排查。4.3 用 HDI 接口写一个读取程序下面是一个简化的读取流程基于 HDI 接口#include i2c_if.h #define SHT30_ADDR 0x44 #define CMD_MEASURE 0x2C06 int read_sht30(void) { int16_t i2c_num 1; int32_t ret; uint8_t cmd_buf[2] {0x2C, 0x06}; uint8_t data_buf[6] {0}; struct I2cMsg msgs[2]; int32_t handle I2cOpen(i2c_num); if (handle 0) { return -1; } // 发送测量命令 msgs[0].addr SHT30_ADDR; msgs[0].flags 0; msgs[0].len 2; msgs[0].buf cmd_buf; ret I2cTransfer(handle, msgs, 1); if (ret ! 1) { I2cClose(handle); return -1; } // 等待测量完成 usleep(20000); // 读取 6 字节数据 msgs[0].addr SHT30_ADDR; msgs[0].flags I2C_FLAG_READ; msgs[0].len 6; msgs[0].buf data_buf; ret I2cTransfer(handle, msgs, 1); I2cClose(handle); if (ret ! 1) { return -1; } // 解析温湿度 uint16_t temp_raw (data_buf[0] 8) | data_buf[1]; uint16_t humi_raw (data_buf[3] 8) | data_buf[4]; float temperature -45.0f 175.0f * temp_raw / 65535.0f; float humidity 100.0f * humi_raw / 65535.0f; return 0; }这段代码的关键在于先发命令延时等待转换完成再读数据。不同传感器的时序要求不一样有的需要 15ms有的需要 50ms具体看数据手册。延时不够就读到旧数据或者全 0。4.4 验证数据正确性读出来的数据要验证。最简单的办法是拿手捂住传感器湿度应该上升吹口气湿度也会变化。如果读出来一直是 0 或者 65535说明通信有问题。还可以用 i2cget 手动读寄存器对比i2cget -y 1 0x44 0x00 w如果 i2cget 能读到合理数据但你的程序读不到那问题就在程序逻辑上不在硬件或设备树上。5. I2C 排障实战常见问题与排查思路5.1 通信完全没反应怎么查这是最常见的情况i2cdetect 扫描全是--或者程序返回-1。排查顺序应该是从硬件到软件从底层到上层。先量电压。SDA 和 SCL 在空闲状态下应该是高电平接近 VCC。如果量出来是 0V说明线被拉死了可能是某个设备把总线拉低了或者引脚配置错了。如果量出来是 1.8V 而 VCC 是 3.3V说明电平不匹配需要电平转换。再量上拉电阻。前面说过了不重复。然后看引脚复用。在设备树里确认 I2C 控制器的 pinctrl 配置是否正确引脚有没有被其他功能占用。RK3568 的引脚复用比较灵活一个引脚可以配成 I2C、UART、SPI、GPIO 等配错了就没波形。最后看设备树状态。status是不是okayclock-frequency是不是合理子节点的reg地址对不对。5.2 能扫描到设备但读写失败这种情况通常是时序或者协议层面的问题。i2cdetect 只发地址看 ACK不涉及具体的数据读写所以能扫描到只说明设备在线不代表通信完全正常。常见原因有几个一是寄存器地址不对写错了寄存器自然读不到正确数据二是读写标志搞反了把读操作写成了写操作三是没有正确处理 ACK/NACK有些设备在忙的时候会返回 NACK需要重试四是时钟频率太高数据手册标称支持 400kHz但实际模块上的滤波电容导致波形畸变。我遇到过一个 GT911 触摸芯片的案例i2cdetect 能扫到 0x5D但读坐标一直失败。后来用逻辑分析仪抓波形发现读操作后面没有停止条件导致下一次通信时总线状态不对。加上停止条件后就正常了。这种问题不看波形很难定位。5.3 多设备共存时的地址冲突与总线仲裁一条 I2C 总线上挂了多个设备如果两个设备地址相同就会冲突。比如你接了两个 SSD1306 OLED默认地址都是 0x3C那就只能有一个工作。解决办法是改其中一个的地址很多模块上有地址选择焊盘或者跳线。还有一种情况是地址不冲突但通信互相干扰。比如一个设备在通信过程中被另一个设备的中断拉低了 SCL导致时钟异常。这种问题比较隐蔽需要抓波形分析。总线仲裁是 I2C 协议自带的功能多主机同时发起通信时谁先拉低 SDA 谁获得总线控制权。但在实际应用中OpenHarmony 通常只有一个主机所以仲裁问题很少遇到。不过如果你用了 I2C 扩展芯片或者多路复用器就要注意通道切换的时序。5.4 排障速查表现象可能原因排查方法i2cdetect 全--硬件未连接、上拉缺失、引脚复用错误量电压、量上拉、查 pinctrli2cdetect 显示UU地址被驱动占用检查内核驱动加载情况能扫描到但读写失败寄存器地址错、时序不对、频率过高抓波形、降频率、查数据手册数据偶尔出错总线电容大、上拉太弱、干扰降低频率、减小上拉阻值、缩短走线读出来全是 0 或 0xFF设备未就绪、延时不够、地址错误增加延时、确认地址、检查电源通信一段时间后死掉总线被拉死、缺少停止条件抓波形、检查代码中的 STOP 处理提示逻辑分析仪是 I2C 排障的利器。几百块的那种就够了能解码 I2C 协议直接看到地址、数据、ACK/NACK比万用表和示波器直观得多。强烈建议搞一个。6. 几个容易被忽略的细节和实操心得6.1 上拉电阻不是随便选的前面提了 4.7kΩ 是典型值但实际选的时候要算一下。总线电容 C 包括走线电容、引脚电容和器件电容一般 10cm 走线大概 10-20pF每个器件引脚 5-10pF。挂 3 个器件、走线 10cm总电容大概 50pF 左右。上升时间 tr ≈ 2.2 × R × C。如果 R4.7kΩC50pFtr ≈ 0.5μs。在 100kHz 下周期 10μs完全够用。在 400kHz 下周期 2.5μs也还能接受。但如果 C 到了 200pFtr 就变成 2μs400kHz 下就非常紧张了。所以如果你要跑 400kHz上拉电阻建议降到 2.2kΩ 甚至 1.5kΩ。但阻值越小功耗越大低电平时的灌电流也越大。标准模式下拉低时灌电流建议不超过 3mA快速模式不超过 6mA。3.3V 除以 1kΩ 就是 3.3mA已经接近标准模式的上限了。所以 1kΩ 是底线再小就不推荐了。6.2 设备树里的 clock-frequency 和实际波形的关系设备树里配的clock-frequency是控制器输出的 SCL 频率但实际波形可能和这个值有偏差。因为 I2C 控制器分频的时候是整数分频源时钟除以分频系数不一定能精确得到你想要的频率。比如源时钟 100MHz你要 100kHz分频系数 1000刚好。但如果你要 400kHz分频系数 250也刚好。但如果源时钟是 99MHz要 100kHz分频系数 990实际频率就是 100kHz没问题。但如果要 333kHz分频系数 297.3只能取 297 或 298实际频率就有偏差。这个偏差一般不大不影响通信。但如果你用逻辑分析仪测出来频率和设定值差很多比如设了 400kHz 实际只有 200kHz那就要查控制器的时钟源配置了。6.3 热插拔 I2C 设备的风险I2C 设计上不支持热插拔。设备在通信过程中被拔掉总线可能被拉死或者产生异常波形导致控制器锁死。如果你确实需要热插拔比如测试不同传感器建议先断电再插拔或者用带隔离的 I2C 缓冲器。我试过在系统运行的时候直接拔掉一个 OLED 模块结果 i2c-1 整个控制器都不工作了后面接什么设备都扫不到。重启才能恢复。后来查了一下是控制器检测到总线异常后进入了保护状态。所以热插拔这件事能不做就不做。6.4 软件 I2C 和硬件 I2C 的取舍OpenHarmony 里一般用的是硬件 I2C 控制器速度快、不占 CPU。但有时候硬件 I2C 的引脚被占了或者需要接很多路 I2C就可以考虑软件模拟 I2Cbit-banging。软件 I2C 的好处是引脚灵活任何 GPIO 都能模拟。坏处是速度慢、占 CPU、时序精度依赖延时函数的准确性。在 OpenHarmony 上做软件 I2C 需要自己实现时序或者用现成的 GPIO 模拟库。我的建议是能用硬件 I2C 就用硬件实在没办法再考虑软件模拟。软件 I2C 在低速下比如 10kHz比较稳高速下容易出问题。6.5 逻辑分析仪抓波形的几个技巧抓 I2C 波形的时候触发条件设成 SDA 下降沿起始条件采样率至少是 SCL 频率的 10 倍。比如 400kHz 的 SCL采样率至少 4MHz建议 10MHz 以上。解码的时候选 I2C 协议设置好 SDA 和 SCL 对应的通道地址位数选 7 位。解码结果会直接显示地址、读写标志、数据和 ACK/NACK非常直观。如果波形上看到 SCL 被拉低很长时间不释放说明从机在忙或者总线被拉死了。如果看到 SDA 在 SCL 高电平期间变化说明有异常的起始或停止条件可能是干扰或者代码逻辑错误。7. 从能用到好用I2C 稳定性优化建议7.1 增加重试机制I2C 通信偶尔失败是正常的尤其是总线负载重或者环境干扰大的时候。在代码里加一个重试机制失败后重试 2-3 次能显著提高稳定性。int i2c_transfer_with_retry(int32_t handle, struct I2cMsg *msgs, int16_t count, int retry) { int ret; for (int i 0; i retry; i) { ret I2cTransfer(handle, msgs, count); if (ret count) { return ret; } usleep(1000); } return ret; }重试间隔不要太短给从机一点恢复时间。1ms 到 10ms 都可以看具体器件。7.2 总线恢复当 SDA 被拉死时怎么办有时候从机异常会把 SDA 一直拉低导致总线死锁。这时候主机发什么从机都不理因为起始条件都产生不了。恢复方法是把 SCL 配成 GPIO 输出手动发 9 个时钟脉冲然后发一个停止条件。这样从机在收到足够时钟后会把 SDA 释放。具体操作// 伪代码实际需要根据平台 GPIO 接口实现 gpio_set_direction(SCL, OUTPUT); gpio_set_direction(SDA, INPUT); for (int i 0; i 9; i) { gpio_set_value(SCL, 0); usleep(5); gpio_set_value(SCL, 1); usleep(5); } // 发停止条件 gpio_set_direction(SDA, OUTPUT); gpio_set_value(SDA, 0); usleep(5); gpio_set_value(SCL, 1); usleep(5); gpio_set_value(SDA, 1); usleep(5); // 恢复 I2C 功能这个操作在 OpenHarmony 里需要先释放 I2C 控制器的引脚复用配成 GPIO操作完再配回去。稍微麻烦一点但关键时刻能救命。7.3 电源和地的处理I2C 通信不稳定有时候不是总线的问题是电源的问题。传感器供电不稳内部逻辑乱掉自然不响应。建议在每个 I2C 设备的 VCC 引脚旁边放一个 0.1μF 的去耦电容离引脚越近越好。地线也要处理好。如果主机和从机距离远地线阻抗大信号参考电平会漂移。这种情况下考虑用 I2C 隔离器或者差分 I2C 扩展器。7.4 走线布局的注意事项SDA 和 SCL 尽量走在一起平行走线减少环路面积。不要和高速信号比如 SPI 时钟、USB 差分线长距离并行容易耦合干扰。如果必须交叉尽量垂直交叉。走线长度控制在 30cm 以内比较稳妥超过这个长度就要考虑降低频率或者加缓冲器。I2C 缓冲器比如 PCA9515 可以延长传输距离同时支持总线隔离。8. 关于 I2C 排障这件事我自己的体会I2C 这个东西入门容易用好难。协议本身不复杂两根线、几个状态半天就能看懂。但实际项目里遇到的问题千奇百怪硬件、设备树、驱动、应用层哪一层出问题都会表现为“通信失败”。我的经验是排障一定要有层次从硬件到软件从底层到上层不要跳步。先量电压和上拉再看引脚复用和设备树然后用 i2cdetect 确认设备在线再用 i2cget/i2cset 验证寄存器读写最后才去查应用程序的逻辑。大部分问题在前三步就能定位。逻辑分析仪真的值得买一个。很多问题看波形一目了然比猜来猜去效率高太多。我早期排障靠万用表和打印日志经常搞半天没头绪后来用了逻辑分析仪很多问题几分钟就定位了。最后说一个小心得每次接新传感器先别急着写代码先用 i2cdetect 扫一下确认地址对了再用 i2cget 读一下 WHO_AM_I 寄存器大部分传感器都有确认能读到正确的器件 ID。这一步花两分钟能省掉后面很多调试时间。
返回列表