ARTICLE DETAIL

资讯详情

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

OpenHarmony I2C总线调试实战:从协议原理到排障全链路

OpenHarmony I2C总线调试实战:从协议原理到排障全链路 I2C 这东西说简单也简单两根线一挂读写寄存器就完事了说难也真难一旦总线上某个器件抽风或者时序差那么一点点你盯着逻辑分析仪能看一下午。我在 OpenHarmony 上做外设适配这几年I2C 相关的调试占了相当大一块精力——从传感器、EEPROM 到触摸屏、编码器几乎每个项目都要跟它打交道。这篇就围绕 OpenHarmony 系统下 I2C 总线到底怎么用、出了问题怎么一步步排把我踩过的坑和总结出来的排查链路完整讲一遍。不管你是刚接触嵌入式总线的新手还是已经在做驱动适配的老手应该都能从里面找到能直接抄作业的东西。1. 先把 I2C 的物理层和协议层捋清楚很多人排障排不明白根子在于对 I2C 的底层机制只有模糊印象。所以我先把物理层和协议层讲透后面排障时你才知道每一步在查什么。1.1 两根线到底在干什么I2C 只用两根信号线SCL串行时钟线和SDA串行数据线。这两根线都是开漏输出结构也就是说器件只能把线拉低不能主动拉高。线要变高靠的是上拉电阻把电平拉上去。这个结构决定了几个关键特性总线上任何一个器件把 SDA 拉低整条线就是低电平这就是线与逻辑。多主机仲裁、时钟同步全靠这个机制。上拉电阻的取值直接影响上升沿时间。阻值太大上升沿变缓高速率下波形就塌了阻值太小灌电流太大器件可能扛不住。常见取值 4.7kΩ、2.2kΩ、1.5kΩ速率越高阻值越小。总线空闲时SCL 和 SDA 都必须是高电平。如果你上电测到某根线一直是低那基本可以断定有器件在死拉总线或者短路了。我见过太多案例波形死活不对最后发现是上拉电阻没焊或者焊了个 10kΩ 还在跑 400kHz。这种物理层的问题软件怎么调都没用必须先用示波器或逻辑分析仪把电平确认了。1.2 起始、停止、应答三个必须刻进脑子里的时序I2C 的通信全靠几个关键时序信号来界定起始条件StartSCL 为高时SDA 由高变低。这个下降沿告诉所有器件注意要开始传输了。停止条件StopSCL 为高时SDA 由低变高。表示本次传输结束总线回到空闲。应答ACK/NACK每传输 8 位数据后第 9 个时钟周期接收方把 SDA 拉低表示 ACK保持高表示 NACK。主机读数据时发完最后一个字节通常回 NACK然后发停止条件。这里有个特别容易搞混的点起始和停止条件都发生在 SCL 为高电平期间而普通数据位的改变必须发生在 SCL 为低电平期间。如果你在逻辑分析仪上看到数据在 SCL 高电平期间跳变那要么是时序配置错了要么是总线被干扰了。1.3 数据帧格式与地址机制一次典型的 I2C 传输长这样主机发起始条件主机发 7 位从机地址 1 位读写方向位0 写1 读对应从机回 ACK数据传输每字节后跟一个 ACK/NACK主机发停止条件7 位地址里有些地址是保留的。比如 0x00 是通用呼叫地址0x78 到 0x7F 这一段留给 10 位地址的前缀等特殊用途。实际选器件地址时要对照数据手册确认别想当然。另外还有重复起始条件Repeated Start就是在不发停止条件的情况下再发一个起始条件。读寄存器操作经常用这个先写寄存器地址然后重复起始再读数据。这样能保证读操作和写操作之间总线不被其他主机抢走。提示很多初学者写 EEPROM 读写时写完地址直接发停止再发起始去读中间如果被其他任务插入操作就可能读到错误数据。用重复起始是更稳妥的做法。2. OpenHarmony 下 I2C 的软件栈长什么样搞清楚协议之后得知道 OpenHarmony 是怎么把 I2C 抽象出来的。这一层不理解你连设备树该改哪里都不知道。2.1 HDF 框架下的 I2C 驱动模型OpenHarmony 用的是HDFHardware Driver Foundation驱动框架。I2C 在 HDF 里被抽象成几个层次I2C 控制器驱动负责操作具体的 SoC I2C 控制器寄存器实现时序产生、中断处理等。这部分通常由芯片厂商提供。I2C 核心层提供统一的接口管理总线、设备、消息传输。I2C 设备驱动具体外设的驱动比如某个传感器、触摸屏通过核心层接口收发数据。应用层或上层服务通过 HDF 提供的 I2C 接口访问设备不需要关心底层是哪个 SoC 的控制器。这个分层的好处是换芯片时上层驱动基本不用动坏处是出问题时你得知道问题出在哪一层。2.2 设备树I2C 设备注册的关键在 OpenHarmony以及底层 Linux 内核里I2C 设备是通过设备树Device Tree描述的。一个典型的 I2C 设备节点长这样i2c1 { status okay; clock-frequency 400000; sensor48 { compatible vendor,sensor-name; reg 0x48; interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; }; };几个关键字段status必须是okay默认可能是disabled忘了改这个是最常见的设备不工作原因之一。clock-frequency总线速率标准模式 100kHz快速模式 400kHz快速模式 1MHz。reg从机地址注意这里是 7 位地址不带读写位。compatible用来匹配驱动格式一般是厂商,型号。我踩过的一个坑设备树里reg写成了 8 位地址比如把 0x48 写成 0x90结果驱动一直匹配不上或者通信失败。设备树里的 reg 永远是 7 位地址这个一定要记牢。2.3 从设备树到驱动匹配的完整链路设备树节点写好后内核启动时会解析这些节点为每个 I2C 设备创建i2c_client然后拿compatible属性去和驱动里注册的of_device_id表比对。匹配成功就调用驱动的probe函数。如果probe没被调用排查顺序是设备树节点有没有被正确编译进 dtbstatus是不是okaycompatible字符串和驱动里的是否完全一致大小写、连字符都不能错I2C 控制器本身有没有使能这套链路我在 RK3568 上验证过很多次基本按这个顺序查八九不离十。3. 手把手在 OpenHarmony 上挂一个 I2C 设备光讲理论没意思我拿一个实际场景走一遍完整流程。假设我们要在 OpenHarmony 上挂一颗 I2C 接口的 EEPROM比如 AT24C02从设备树配置到读写验证全部走通。3.1 确认硬件连接与地址第一步永远是硬件确认。AT24C02 的 7 位地址是1010加上 A2/A1/A0 三个引脚的电平。如果三个引脚都接地地址就是0x50。用万用表量一下这三个引脚的实际电平别只看原理图——板子上的上下拉可能和你想的不一样。接线确认SDA、SCL 分别接到 SoC 对应的 I2C 控制器引脚VCC 和 GND 接好上拉电阻确认在位。3.2 设备树节点的编写与验证在对应的 I2C 控制器节点下添加i2c3 { status okay; clock-frequency 100000; eeprom50 { compatible atmel,at24c02; reg 0x50; pagesize 8; }; };编译 dtb 后系统启动可以通过以下方式验证设备是否被识别ls /sys/bus/i2c/devices/如果看到3-0050这样的目录说明设备已经被内核识别并创建了i2c_client。如果没有回到上一节的排查顺序。3.3 用 i2c-tools 做快速读写验证在正式写驱动之前我强烈建议先用i2c-tools验证硬件通路。这是最省时间的做法# 扫描 i2c-3 总线上的所有设备 i2cdetect -y 3 # 往地址 0x50 的偏移 0x00 写入一个字节 0xAB i2cset -y 3 0x50 0x00 0xAB # 从地址 0x50 的偏移 0x00 读一个字节 i2cget -y 3 0x50 0x00如果i2cdetect能扫到0x50i2cset和i2cget能正常读写说明硬件和底层驱动都没问题接下来写业务驱动就是纯软件的事了。如果扫不到问题在硬件或控制器驱动层。注意有些器件的地址在i2cdetect里显示为UU表示该地址已被驱动占用这是正常的不是错误。3.4 在 HDF 驱动里调用 I2C 接口OpenHarmony 的 HDF 提供了 I2C 访问接口。一个典型的读写流程#include i2c_if.h DevHandle i2cHandle I2cOpen(3); // 打开 i2c-3 if (i2cHandle NULL) { // 打开失败处理 } // 构造消息先写寄存器地址再读数据 struct I2cMsg msgs[2]; uint8_t regAddr 0x00; uint8_t readBuf[8]; msgs[0].addr 0x50; msgs[0].buf regAddr; msgs[0].len 1; msgs[0].flags 0; // 写 msgs[1].addr 0x50; msgs[1].buf readBuf; msgs[1].len 8; msgs[1].flags I2C_FLAG_READ; // 读 int32_t ret I2cTransfer(i2cHandle, msgs, 2); if (ret ! 2) { // 传输失败处理 } I2cClose(i2cHandle);这里的关键是I2cTransfer一次性提交两个消息中间会自动插入重复起始条件保证读写的原子性。如果你分成两次I2cTransfer调用中间可能被其他任务打断。4. I2C 排障从现象到根因的完整链路这部分是重点。I2C 出问题的现象就那么几种但根因可能分布在硬件、设备树、驱动、时序各个层面。我按现象 → 排查步骤 → 常见根因的方式整理。4.1 现象一i2cdetect 扫不到任何设备这是最让人抓狂的情况总线上一片空白。排查链路第一步量电平。用示波器或万用表测 SCL 和 SDA 在空闲时是否为高。如果某根线是低说明有器件死拉总线或短路。逐个断开器件排查。第二步确认控制器使能。检查设备树里对应 I2C 控制器的status是否为okay以及时钟、引脚复用pinctrl配置是否正确。很多 SoC 的引脚默认是 GPIO 功能需要配置成 I2C 功能。第三步看时钟频率。如果clock-frequency设得太高而你的上拉电阻又偏大波形上升沿太缓器件可能根本识别不了。先把频率降到 100kHz 试试。第四步查地址冲突。虽然扫不到设备通常不是冲突但如果总线上有两个器件地址相同可能出现异常。用i2cdetect逐个确认。我遇到过一次特别隐蔽的pinctrl 配置里 SDA 和 SCL 的引脚编号写反了。这种问题看原理图对不出来只能靠仔细核对设备树和 SoC 手册。4.2 现象二能扫到设备但读写失败设备能被识别说明地址和基本通信没问题但读写数据出错。常见根因现象可能根因排查方法读全 0xFF器件未上电或未初始化量器件供电查初始化时序读全 0x00总线被拉低或器件未响应查 SDA 电平查器件状态数据偶尔错时序临界或干扰降速测试查上拉电阻特定寄存器读错寄存器地址或页边界问题对照数据手册确认地址EEPROM 这类器件还有个页写的问题。AT24C02 的页大小是 8 字节如果你一次写超过 8 字节且跨越页边界地址会回卷导致数据写错位置。这个坑我在早期项目里踩过数据莫名其妙被覆盖查了半天才发现是页边界问题。4.3 现象三通信一段时间后挂死这种间歇性故障最难查。典型表现是系统跑一段时间后 I2C 通信超时重启后又正常。根因通常是总线死锁某个从机在传输过程中被复位或掉电把 SDA 拉低不放主机无法产生起始条件。解决办法是发送 9 个时钟脉冲让从机把剩余数据移出释放 SDA// 总线恢复手动发送 9 个 SCL 脉冲 for (int i 0; i 9; i) { // SCL 拉低 // 延时 // SCL 拉高 // 延时 } // 然后发送停止条件OpenHarmony 的 I2C 框架里部分控制器驱动已经内置了总线恢复机制但不是所有都支持。如果你的平台没有可能需要在驱动层自己实现。另一个常见原因是中断上下文里做 I2C 传输。I2C 传输是可能睡眠的操作在中断上下文里调用会导致系统异常。如果你在中断处理函数里直接读传感器赶紧改成工作队列或线程化中断。4.4 现象四多设备共存时相互干扰一条总线上挂多个设备时问题会更复杂。常见情况地址冲突两个器件地址相同。解决方法是改硬件地址引脚或者用 I2C 多路复用器如 TCA9548A分路。速率不匹配一个器件只能跑 100kHz另一个能跑 400kHz。整条总线只能按最低速率跑。上拉电阻不匹配不同器件对上升沿要求不同需要折中。我一般建议如果一条总线上设备超过 3 个或者速率要求差异大直接上 I2C 多路复用器。虽然多花几毛钱但省下的调试时间远超这个成本。5. 几个容易被忽略的实战细节前面讲的是主干这里补充几个我在实际项目里总结的细节都是文档里不太会写、但实际很要命的东西。5.1 逻辑分析仪的抓包技巧排 I2C 问题逻辑分析仪是必备工具。几个使用要点采样率要够至少是总线速率的 10 倍以上。跑 400kHz 总线采样率建议 10MHz 起步。触发条件设好可以设成起始条件触发或特定地址触发避免抓到一堆无关数据。解码器选对逻辑分析仪软件里选 I2C 解码器设置好 SCL/SDA 通道和地址位宽能直接看到地址、数据、ACK/NACK。抓到波形后重点看几个地方起始条件是否干净、ACK 位是否正常拉低、数据在 SCL 高电平期间是否稳定。如果看到 ACK 位是高NACK说明从机没响应要么地址错要么器件没准备好。5.2 时钟延展从机也会拉低 SCL很多新手不知道I2C 协议允许从机在需要更多处理时间时主动把 SCL 拉低这叫时钟延展Clock Stretching。主机必须检测到 SCL 被拉低后等待直到从机释放。如果你的主机驱动不支持时钟延展遇到会做延展的从机比如某些传感器在转换期间通信就会失败。排查时如果发现 SCL 被意外拉低先确认是不是从机在做时钟延展而不是硬件故障。5.3 设备树里 interrupt 和 I2C 的配合很多 I2C 传感器用中断引脚通知主机数据就绪。设备树里要同时配置 I2C 节点和中断sensor48 { compatible vendor,sensor; reg 0x48; interrupt-parent gpio2; interrupts 5 IRQ_TYPE_EDGE_FALLING; };中断触发方式要和器件手册一致。有的器件是低电平触发有的是下降沿触发配错了要么收不到中断要么中断风暴。我见过配成电平触发但器件一直拉低导致系统卡在中断里的案例。5.4 电源域和上电时序有些 I2C 器件对上电时序有要求比如 VCC 稳定后需要等待若干毫秒才能通信。如果驱动 probe 时立刻去读器件可能读到错误值。稳妥的做法是在 probe 里加一段延时或者先做一次软复位。另外如果器件和 SoC 不在同一个电源域SoC 先上电、器件后上电的情况下SoC 可能在器件还没准备好时就去访问导致失败。这种问题在低功耗场景里特别常见。6. 从 I2C 到其他总线的排障思路迁移I2C 的排障方法论其实可以迁移到 SPI、UART 等其他总线上。核心思路是一样的先确认物理层再确认配置层最后查协议层和驱动层。物理层就是电平和连线配置层就是设备树和控制器寄存器协议层就是时序和帧格式驱动层就是软件逻辑。任何总线问题按这个顺序查基本都能定位。我在做 SPI 排障时用的就是同一套思路先量 CS、CLK、MOSI、MISO 的电平再查设备树里的 mode 和速率配置最后看时序图。I2C 因为有时钟延展和线与逻辑稍微复杂一点但框架是一样的。掌握了这套方法你面对任何总线都不会慌。工具就是示波器、逻辑分析仪、万用表加上对协议的理解和耐心。最后分享一个我自己的习惯每次调通一个 I2C 设备我都会把设备树配置、驱动关键代码、逻辑分析仪抓到的正常波形截图整理成一份笔记。下次遇到类似器件直接翻笔记能省掉大量重复劳动。这个习惯坚持几年下来排障速度比刚入行时快了好几倍。
返回列表