ARTICLE DETAIL

资讯详情

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

OpenHarmony I2C外设适配与排障实战:从协议时序到设备树配置

OpenHarmony I2C外设适配与排障实战:从协议时序到设备树配置 I2C 这东西说简单也简单两根线一挂设备就能通信说难也难真到了板子上一跑波形出不来、地址扫不到、读回来的数据全是 0xFF能让人对着示波器坐一晚上。我在 OpenHarmony 上做外设适配的这几年I2C 相关的调试占了相当大一块时间从最早的 RK3568 开发板到后面各种传感器、EEPROM、触摸屏的接入踩过的坑基本能写一本小册子。这篇就把 I2C 在 OpenHarmony 下的使用和排障完整梳理一遍从协议本身的时序逻辑到设备树怎么配、HDI 接口怎么调、逻辑分析仪怎么抓再到那些让人抓狂的典型故障怎么一步步定位。不管你是刚接触嵌入式总线的新手还是已经在做驱动适配的老手应该都能从里面找到点有用的东西。1. 先把 I2C 的物理层和时序逻辑吃透很多人调 I2C 出问题根子不在代码而在对协议本身的理解有偏差。所以这一章我不急着讲 OpenHarmony 怎么配先把 I2C 到底怎么工作这件事讲清楚后面排障的时候你才知道该看哪里。1.1 两根线到底在干什么I2C 全称是 Inter-Integrated Circuit中文叫集成电路总线由 SDA串行数据线和 SCL串行时钟线两根线组成。这两根线都是开漏输出结构也就是说器件只能把线拉低不能主动拉高高电平是靠上拉电阻拉上去的。这个结构决定了几个关键特性第一总线上可以挂多个设备大家共用这两根线第二任何设备在空闲时都不驱动线路线路被上拉电阻维持在高电平第三多个设备同时驱动不会烧毁器件因为开漏结构下大家只能拉低不会出现一个推高一个拉低的短路冲突。上拉电阻的取值是个经常被忽略的细节。典型值在 4.7kΩ 到 10kΩ 之间但这个值不是随便选的。阻值太大上升沿变缓高速通信时波形还没到高电平就被拉低了通信直接失败阻值太小灌电流增大器件可能扛不住而且功耗上去了。经验公式是看总线的等效电容标准模式 100kHz 下上升时间要小于 1000ns快速模式 400kHz 下要小于 300ns。如果你板子上挂了七八个设备走线又长等效电容可能到 200pF 以上这时候 10kΩ 的上拉就明显偏大了得换成 2.2kΩ 甚至更小。我遇到过一块板子单挂一个 EEPROM 时 10kΩ 跑 400kHz 没问题后来加了三个传感器波形上升沿直接变成圆弧降到 100kHz 才勉强能用最后把上拉换成 3.3kΩ 才彻底解决。1.2 起始、停止和应答三个必须刻在脑子里的信号I2C 通信的所有动作都建立在三个基本信号之上。起始条件Start是 SCL 为高电平时SDA 从高变低停止条件Stop是 SCL 为高电平时SDA 从低变高。注意这里的关键是 SCL 保持高电平期间 SDA 发生跳变这是区分数据位和起止条件的唯一依据。数据位传输时SDA 必须在 SCL 低电平期间变化在 SCL 高电平期间保持稳定接收方在 SCL 高电平期间采样。应答信号ACK/NACK是第九个时钟周期的事。主机发完 8 位数据后释放 SDA从机如果正确接收就在第九个 SCL 高电平期间把 SDA 拉低这就是 ACK如果从机没响应或者处理不过来SDA 保持高电平就是 NACK。很多排障场景里用逻辑分析仪抓波形第一件事就是看第九个时钟有没有 ACK没有 ACK 说明从机根本没应答问题要么在地址不对要么在从机没上电要么在硬件连接断了。1.3 数据帧格式与读写流程一次完整的 I2C 传输帧格式是这样的起始条件 7 位从机地址 1 位读写方向位 应答位 数据字节 应答位 ... 停止条件。7 位地址是标准也有 10 位地址的扩展模式但实际项目里 7 位占绝大多数。读写方向位 0 表示写1 表示读。写流程相对简单主机发起始发地址加写位等 ACK发寄存器地址等 ACK发数据等 ACK发停止。读流程稍微绕一点主机先发起始发地址加写位等 ACK发要读的寄存器地址等 ACK然后重新发起始这叫 Repeated Start发地址加读位等 ACK然后释放 SDA 让从机驱动数据线主机提供时钟每收一个字节发一个 ACK最后一个字节发 NACK 然后发停止。这个 Repeated Start 是很多人容易搞混的地方。有些从机要求读操作必须用 Repeated Start中间不能插停止条件否则内部状态机复位读出来就是错的。我在调一颗温湿度传感器的时候就遇到过先写寄存器地址再发停止再发起始读数据死活不对改成 Repeated Start 就正常了。所以看数据手册的时候时序图那一页一定要仔细看确认它要求的是哪种流程。1.4 时钟频率与总线速率I2C 标准模式是 100kHz快速模式 400kHz快速模式 是 1MHz高速模式能到 3.4MHz。但实际能跑多快取决于总线电容、上拉电阻、走线长度和从机支持能力。OpenHarmony 下配置 I2C 控制器时频率是在设备树或者 HDI 接口里指定的但你要确认从机能不能跟上。有些传感器标称支持 400kHz但实际在 400kHz 下偶尔丢数据降到 100kHz 就稳如老狗。我的建议是调试阶段先用 100kHz 把功能跑通确认没问题再往上提出问题了也好定位是频率问题还是逻辑问题。2. OpenHarmony 下 I2C 的设备树配置与 HDI 接口协议层面清楚了接下来看 OpenHarmony 里怎么把 I2C 用起来。OpenHarmony 的外设访问走的是 HDIHardware Device Interface层底层还是通过 Linux 内核的 I2C 子系统所以设备树的配置逻辑和标准 Linux 是一脉相承的但上层接口有自己的封装。2.1 设备树里 I2C 控制器和从机怎么描述以 RK3568 为例SoC 内部有多个 I2C 控制器比如 i2c0 到 i2c5每个控制器在设备树里是一个节点。控制器节点本身要配置时钟频率、引脚复用pinctrl、状态等。从机设备则作为控制器节点的子节点存在每个从机有自己的地址和驱动匹配信息。一个典型的设备树片段长这样i2c3 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c3m0_xfer; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; sensor68 { compatible bosch,bmi160; reg 0x68; }; };这里有几个点值得展开说。clock-frequency是总线频率单位 Hz100000 就是 100kHz。reg是从机地址注意这里写的是 7 位地址不是左移一位后的 8 位值。我见过有人把数据手册上的 8 位地址直接填进去结果怎么都扫不到设备就是因为没做移位。数据手册上如果写的是 0xA0那是 8 位格式7 位地址是 0x50。compatible属性是驱动匹配的关键内核里对应的驱动会拿这个字符串去匹配。如果内核没有现成的驱动你就得自己写一个或者用 i2c-dev 通用接口从用户态直接操作。OpenHarmony 下很多传感器适配就是走 HDI 接口底层可能用的是 i2c-dev 或者自己封装的字符设备。2.2 pinctrl 配置引脚复用最容易翻车的地方RK3568 的引脚是多功能复用的一个物理引脚可能同时是 GPIO、UART、I2C、SPI 等功能。设备树里必须通过 pinctrl 把引脚正确配置成 I2C 功能否则控制器发出了信号引脚却没切过去波形根本出不来。pinctrl 的配置通常在 SoC 的 dtsi 文件里已经定义好了比如i2c3m0_xfer这样的节点你只需要在控制器节点里引用。但如果你用的引脚组和默认的不一样就得自己定义。这里有个坑有些板子的原理图上 I2C 的 SDA 和 SCL 接反了或者 pinctrl 引用的组和实际硬件不对应导致波形完全出不来。排查的时候先用万用表量一下引脚在空闲时是不是高电平如果不是要么是 pinctrl 没配对要么是硬件上拉没接。2.3 HDI 接口的调用逻辑OpenHarmony 的 HDI 层为 I2C 提供了标准接口主要包含在i2c_if.h里。核心接口有这么几个I2cOpen打开一个 I2C 控制器I2cClose关闭I2cTransfer执行一次传输I2cGetConfig和I2cSetConfig读写配置。I2cTransfer是最核心的它接收一个I2cMsg数组每个I2cMsg描述一次消息传输包含从机地址、读写标志、数据缓冲区、长度等。一次调用可以传多个消息实现 Repeated Start 的效果。比如先写寄存器地址再读数据就可以构造两个I2cMsg第一个是写第二个是读中间不加停止条件。struct I2cMsg msgs[2]; uint8_t regAddr 0x00; uint8_t readBuf[2]; 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 readBuf; int32_t ret I2cTransfer(i2cHandle, msgs, 2);这段代码就是典型的写寄存器地址再读数据的操作。注意flags字段读操作要带I2C_FLAG_READ写操作不带。有些实现里还有I2C_FLAG_NO_START和I2C_FLAG_STOP这样的标志用来精细控制起止条件的生成具体看平台实现。2.4 用户态直接操作 i2c-dev 的备选方案如果 HDI 接口满足不了需求或者你只是想快速验证一个传感器能不能通可以直接用 Linux 的 i2c-dev 接口从用户态操作。OpenHarmony 底层是 Linux 内核/dev/i2c-3这样的设备节点是存在的。用open打开设备ioctl配合I2C_RDWR就能发起传输。int fd open(/dev/i2c-3, O_RDWR); struct i2c_rdwr_ioctl_data data; struct i2c_msg msgs[2]; // 填充 msgs ... data.msgs msgs; data.nmsgs 2; ioctl(fd, I2C_RDWR, data);这个方式的好处是灵活不需要写内核驱动适合调试阶段快速验证。坏处是没有经过 HDI 抽象代码可移植性差而且需要权限。实际产品里还是建议走 HDI 接口保持架构统一。3. 从零跑通一个 I2C 设备的完整流程理论讲完了这一章用一个具体例子把整个流程串起来。假设我们要在 RK3568 的 OpenHarmony 板子上接一颗 EEPROMAT24C02从设备树配置到读写验证一步步走完。3.1 硬件连接检查动手之前先量三下拿到板子和模块先别急着上电写代码。第一步是确认硬件连接。I2C 的 SDA、SCL、GND 三根线必须接对VCC 电压要匹配。用万用表量一下模块的 VCC 和 GND 之间有没有短路量一下 SDA 和 SCL 对 GND 的阻抗正常应该是几十 kΩ 到几百 kΩ 的量级如果接近 0 就是短路了。上电之后先不通信用万用表量 SDA 和 SCL 对 GND 的电压。正常情况下总线空闲时两根线都应该是高电平电压值接近 VCC。如果量出来是 0V 或者中间某个奇怪的电压说明上拉有问题或者有器件把线拉死了。这一步能排除掉相当一部分硬件故障。3.2 设备树添加节点并编译验证确认硬件没问题后在设备树里添加 EEPROM 节点。找到对应的 I2C 控制器节点在下面加子节点i2c3 { status okay; clock-frequency 100000; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; };改完设备树重新编译内核和设备树烧录到板子上。启动后先看/sys/bus/i2c/devices/目录下有没有出现3-0050这样的设备节点。如果有说明设备树配置被正确解析了驱动也匹配上了。如果没有检查compatible字符串是不是和内核驱动匹配或者用i2cdetect工具扫一下总线。3.3 用 i2cdetect 和 i2cget/i2cset 快速验证i2cdetect是 I2C 调试的瑞士军刀。在板子上执行i2cdetect -y 3它会扫描 I2C-3 总线上所有可能的地址把有设备应答的地址显示出来。如果 EEPROM 在 0x50你应该能看到 0x50 那一格显示50。如果全是--说明总线上没有任何设备应答问题在硬件连接、地址配置或者上拉电阻。扫到设备之后用i2cget和i2cset读写验证# 读 EEPROM 地址 0x00 的一个字节 i2cget -y 3 0x50 0x00 # 向 EEPROM 地址 0x00 写入 0xAB i2cset -y 3 0x50 0x00 0xAB # 再读回来确认 i2cget -y 3 0x50 0x00如果写进去读出来一致说明 I2C 通信链路完全正常。这一步跑通之后再写 HDI 代码或者应用层代码就有底了。3.4 在 OpenHarmony 应用层封装读写接口验证通过后在 OpenHarmony 的 HDI 层封装一个 EEPROM 读写接口。核心就是前面提到的I2cTransfer调用。写操作构造一个消息包含寄存器地址和数据读操作构造两个消息先写地址再读数据。int32_t EepromWrite(uint16_t addr, const uint8_t *data, uint32_t len) { struct I2cMsg msgs[2]; uint8_t addrBuf[2]; addrBuf[0] (addr 8) 0xFF; addrBuf[1] addr 0xFF; msgs[0].addr EEPROM_ADDR; msgs[0].flags 0; msgs[0].len 2; msgs[0].buf addrBuf; msgs[1].addr EEPROM_ADDR; msgs[1].flags 0; msgs[1].len len; msgs[1].buf (uint8_t *)data; return I2cTransfer(g_i2cHandle, msgs, 2); }注意 EEPROM 的写操作有页写限制AT24C02 的页大小是 8 字节跨页写会回卷覆盖。所以写多字节数据时要按页拆分每页写完等 5ms 左右的内部写周期。这个细节在数据手册里有但很容易被忽略导致写进去的数据错乱。4. I2C 排障实战从波形到代码的完整排查链路这一章是重点。I2C 出问题的时候现象往往很模糊——读不到数据、数据不对、偶尔失败。怎么从这些现象一步步定位到根因需要一套系统的排查方法。4.1 第一层先确认总线上有没有波形不管什么问题第一步永远是抓波形。用逻辑分析仪或者示波器探头接 SDA 和 SCL触发条件设成 SCL 下降沿或者 SDA 下降沿。发起一次通信看有没有波形出来。如果完全没有波形问题在控制器侧。检查设备树里 I2C 控制器的status是不是okaypinctrl 有没有配对时钟有没有使能。RK3568 上还要确认 CRU时钟复位单元里 I2C 控制器的时钟门控有没有打开。有时候设备树配了但时钟没开控制器根本不工作。如果有 SCL 但没有 SDA 变化或者 SDA 一直低可能是 SDA 被某个器件拉死了。逐个断开总线上的从机看是哪个器件的问题。也检查一下 SDA 和 SCL 有没有接反这个低级错误在实际项目中出现的频率比想象中高。4.2 第二层看起始条件和地址字节有波形之后放大看起始条件。SCL 高电平期间 SDA 有没有一个干净的下降沿。如果起始条件都不对后面的都不用看了。起始条件正常的话看紧接着的 8 位地址字节加方向位以及第九个时钟的 ACK。如果地址字节发出去后没有 ACK说明从机没应答。可能的原因地址不对7 位和 8 位搞混了、从机没上电、从机复位引脚没释放、从机地址引脚配置错了。我遇到过一颗传感器数据手册上写默认地址 0x68但模块上有个地址选择引脚悬空了实际地址变成了 0x69扫了半天才扫到。如果地址有 ACK但后续数据字节没有 ACK可能是从机内部状态不对比如寄存器地址越界了或者从机还没准备好。有些器件上电后需要一段稳定时间才能通信数据手册里叫 Power-On Time典型值几毫秒到几十毫秒这段时间内发什么它都不理。4.3 第三层数据内容对不对地址有 ACK数据也有 ACK但读回来的值不对。这时候要对比数据手册的时序图看读写流程是不是符合要求。常见的坑包括该用 Repeated Start 的地方用了 Stop-Start该等内部写周期的地方没等寄存器地址位宽搞错了8 位还是 16 位。还有一种情况是读回来的全是 0xFF。0xFF 通常意味着 SDA 线一直保持高电平主机读到的每一位都是 1。这可能是从机根本没驱动 SDA或者从机在读操作时没有正确切换到输出模式。检查一下读操作的消息构造flags有没有设I2C_FLAG_READ从机地址有没有写错。4.4 第四层偶发性失败的排查思路最头疼的是偶发失败——跑一百次成功九十次失败十次而且失败没有规律。这种问题通常和时序余量、电源稳定性、总线电容有关。先看波形质量。上升沿是不是太缓有没有过冲或者振铃。上升沿太缓会导致高速通信时采样错误解决办法是减小上拉电阻。过冲和振铃通常是走线阻抗不匹配可以在靠近源端串一个小电阻22Ω 到 33Ω试试。再看电源。用示波器看从机的 VCC 有没有纹波或者跌落。有些传感器在通信瞬间电流增大如果电源去耦没做好电压会瞬间跌落导致从机复位。在从机的 VCC 和 GND 之间并一个 100nF 加一个 10uF 的电容通常能解决。还有总线电容的问题。挂的设备越多走线越长等效电容越大。I2C 规范规定总线电容不能超过 400pF。如果超了要么减少设备要么用 I2C 多路复用器比如 TCA9548A把总线分成多段每段单独挂设备。4.5 逻辑分析仪的高级用法协议解码现在的逻辑分析仪基本都带协议解码功能能把抓到的波形直接翻译成地址、数据、ACK/NACK。这个功能在排障时非常有用不用自己对着波形数位。设置好 I2C 解码指定 SDA 和 SCL 通道触发一次通信解码结果直接告诉你哪一步出了问题。我习惯在解码结果里重点看三个地方起始条件后的第一个地址字节、每个字节后的 ACK 位、停止条件。如果地址字节解码出来和预期不符检查地址配置如果 ACK 位显示 NACK检查从机状态如果停止条件缺失检查代码里有没有正确结束传输。5. 那些年踩过的 I2C 坑与经验总结前面讲了流程和方法这一章把一些零散但很典型的坑集中说一下都是实际项目中真金白银换来的经验。5.1 地址移位问题7 位和 8 位的永恒 confusion这个问题出现的频率高到离谱。数据手册上给的地址有的是 7 位格式比如 0x50有的是 8 位格式比如 0xA0。设备树和 i2c-dev 接口用的都是 7 位地址如果你把 8 位地址填进去实际访问的地址就变成了 0x500xA0 右移一位但方向位可能就乱了。判断方法很简单7 位地址范围是 0x08 到 0x778 位地址范围是 0x10 到 0xEE 且最低位是 0。如果数据手册给的地址大于 0x77那基本可以确定是 8 位格式需要右移一位。我现在的习惯是拿到任何一颗 I2C 器件先在数据手册里搜 slave address 或者 device address看清楚它给的是几位格式然后在代码里统一用 7 位。5.2 上拉电阻的取值不是拍脑袋前面提过上拉电阻的事这里再展开一下。上拉电阻的取值要同时满足上升时间和灌电流两个约束。上升时间约束是 R × C ≤ 上升时间上限灌电流约束是 VCC / R ≤ 器件的最大灌电流通常 3mA。假设总线电容 200pF标准模式上升时间上限 1000ns那么 R ≤ 1000ns / 200pF 5kΩ。同时灌电流 3.3V / 5kΩ 0.66mA远小于 3mA没问题。所以 4.7kΩ 是个比较通用的值。但如果总线电容到了 400pFR 就得降到 2.5kΩ 以下这时候灌电流 1.32mA还在安全范围内。实际选型的时候我一般先用 4.7kΩ 试波形上升沿太缓就降到 3.3kΩ 或者 2.2kΩ但不会低于 1kΩ否则灌电流太大。有些开发板自带上拉你外接模块的时候要注意模块上有没有再并一个上拉两个上拉并联阻值减半可能导致灌电流超标。5.3 时钟拉伸从机的等一下信号时钟拉伸Clock Stretching是 I2C 的一个特性从机可以通过把 SCL 拉低来强制主机等待直到从机准备好。这个机制在标准里有但不是所有主机都支持。有些主机的 I2C 控制器硬件不支持时钟拉伸从机拉低 SCL 后主机不理继续发时钟通信就乱了。如果你用的从机数据手册里提到 Clock Stretching而通信又偶尔失败检查一下主机的 I2C 控制器是否支持。RK3568 的 I2C 控制器是支持时钟拉伸的但有些低端 MCU 或者用 GPIO 模拟的 I2C 可能不支持。软件模拟 I2C 的时候读 SCL 引脚状态再决定要不要继续发时钟就能支持时钟拉伸。5.4 多主竞争与仲裁丢失I2C 支持多主模式总线上可以有多个主机。当两个主机同时发起通信时通过仲裁机制决定谁赢。仲裁的原则是谁发的数据先出现低电平谁赢输的那个主机自动退出转为从机模式。多主场景在实际项目中不多见但如果你的系统里有两个处理器共享一条 I2C 总线就要考虑仲裁问题。仲裁丢失通常表现为通信突然中断或者数据错乱。排查的时候看波形如果 SDA 上有两个主机同时驱动的痕迹波形异常基本就是仲裁问题。解决办法是加一个 I2C 多路复用器把两个主机隔离开或者用软件锁来避免同时访问。5.5 电源域和电平匹配不同器件的 I2C 电平可能不一样有的是 1.8V有的是 3.3V有的是 5V。如果直接把 3.3V 的器件和 1.8V 的器件挂在一起1.8V 器件可能被 3.3V 的上拉电阻灌电流长期工作会损坏。解决办法是用电平转换芯片比如 PCA9306 或者 TXS0102把两边的电平隔离开。或者用 I2C 多路复用器每段总线用各自的上拉电压。还有一种简单办法是把上拉电阻接到最低的那个电压域但这样高电压域的器件可能识别不了低电平要看器件的 VIH 和 VIL 参数。我在一个项目里遇到过 1.8V 的摄像头模组和 3.3V 的传感器挂同一条总线摄像头模组的 I2C 引脚被 3.3V 上拉灌电流工作半小时就发烫。后来加了电平转换芯片才解决。所以硬件设计阶段就要确认所有 I2C 器件的电平是否一致不一致的必须做隔离。5.6 设备树配置的常见错误设备树配置错误导致的 I2C 问题也很常见。几个典型错误reg属性填了 8 位地址、clock-frequency单位搞错填了 kHz 而不是 Hz、pinctrl 引用了错误的引脚组、status忘了改成okay。还有一个隐蔽的错误是 I2C 控制器节点的#address-cells和#size-cells配置不对。标准情况下#address-cells应该是 1#size-cells应该是 0因为 I2C 从机只有地址没有大小。如果配错了子节点的reg解析会出问题设备根本注册不上。排查设备树问题的时候启动后看/proc/device-tree/下对应的节点确认属性值是不是你期望的。也可以用dtc工具把编译后的 dtb 反编译回 dts看看实际生效的配置是什么。6. 进阶话题I2C 扩展与多路复用当项目规模变大I2C 总线上挂的设备越来越多或者需要隔离不同电压域的设备时就需要用到 I2C 扩展和多路复用技术。6.1 I2C 多路复用器的工作原理I2C 多路复用器比如 TCA9548A本质上是一个受控的开关矩阵它把上游的一条 I2C 总线切换到下游的八条总线中的某一条。主机先向多路复用器发命令选择要访问的下游通道然后再对目标设备通信。TCA9548A 的地址是 0x70 到 0x77 可配通过 A0、A1、A2 引脚选择。它的控制寄存器很简单写一个字节哪一位是 1 就打开哪个通道。比如写 0x01 打开通道 0写 0x02 打开通道 1写 0x03 同时打开通道 0 和 1一般不建议同时开多个除非你确定没有地址冲突。用多路复用器的好处是每条下游总线的电容独立计算不会累加不同电压域的设备可以挂在不同的下游总线上各自用各自的上拉电压地址冲突的设备可以挂在不同通道上用相同的地址也不会冲突。6.2 多路复用器在 OpenHarmony 下的适配在 OpenHarmony 下用多路复用器需要在设备树里把多路复用器本身作为一个 I2C 从机节点然后把下游设备作为多路复用器的子节点。Linux 内核有标准的 i2c-mux 框架设备树里用i2c-mux相关的 compatible 字符串来匹配。i2c3 { mux70 { compatible nxp,pca9548; reg 0x70; #address-cells 1; #size-cells 0; channel0: i2c0 { #address-cells 1; #size-cells 0; reg 0; sensor68 { compatible bosch,bmi160; reg 0x68; }; }; channel1: i2c1 { #address-cells 1; #size-cells 0; reg 1; eeprom50 { compatible atmel,24c02; reg 0x50; }; }; }; };这样配置之后内核会自动处理通道切换应用层访问sensor68的时候内核先切换到通道 0再发起通信。对上层来说就像访问一条普通总线一样不需要关心多路复用器的存在。6.3 用 GPIO 模拟 I2C 的适用场景有些情况下硬件 I2C 控制器不够用或者某个设备的时序要求比较特殊硬件控制器满足不了这时候可以用 GPIO 模拟 I2C。GPIO 模拟的好处是灵活时序完全由软件控制可以精确控制每个时钟的高低电平时间也支持时钟拉伸。坏处是占用 CPU 资源速率上不去通常只能跑到 100kHz 左右。在 OpenHarmony 下用 GPIO 模拟 I2C可以用内核的 i2c-gpio 驱动设备树里配置两个 GPIO 分别作为 SDA 和 SCLi2c_gpio: i2c-gpio { compatible i2c-gpio; sda-gpios gpio1 RK_PB0 GPIO_ACTIVE_HIGH; scl-gpios gpio1 RK_PB1 GPIO_ACTIVE_HIGH; i2c-gpio,delay-us 5; #address-cells 1; #size-cells 0; };i2c-gpio,delay-us控制时钟频率值越大频率越低。5us 的延时大概对应 100kHz 左右。GPIO 模拟 I2C 的时候SDA 和 SCL 都要用开漏模式或者至少 SDA 要开漏否则多设备挂载时会有冲突。6.4 I2C 与 SMBus 的区别和兼容性SMBus 是 I2C 的一个子集主要用于电源管理和系统监控。它在 I2C 的基础上加了一些限制时钟频率固定在 10kHz 到 100kHz有超时机制有特定的命令格式。大部分 I2C 设备可以兼容 SMBus但反过来不一定。实际项目中如果你用 SMBus 的主机去访问纯 I2C 设备通常没问题因为 SMBus 的时序是 I2C 的子集。但如果你用 I2C 主机去访问严格的 SMBus 设备可能会因为超时机制或者命令格式不匹配而出问题。看数据手册的时候注意区分如果器件标的是 SMBus确认一下它是否兼容 I2C。7. 调试工具链与效率提升工欲善其事必先利其器。I2C 调试有一套趁手的工具效率能提升好几倍。7.1 逻辑分析仪的选型和使用技巧逻辑分析仪是 I2C 调试的必备工具。入门级的比如 Saleae Logic 8八通道支持 I2C、SPI、UART 等常见协议解码采样率 100MS/s对付 I2C 绰绰有余。国产的也有不少性价比高的选择比如 DSLogic 系列采样深度更大适合抓长时间的通信。使用逻辑分析仪的时候采样率要至少是被测信号频率的 10 倍。I2C 跑 400kHz采样率至少 4MS/s实际建议 10MS/s 以上这样波形细节看得清楚。触发条件设成 SCL 下降沿或者协议触发只抓有用的那段避免数据量太大不好分析。探头连接也有讲究。地线要尽量短最好用探头自带的短地线弹簧不要用长长的鳄鱼夹否则引入的寄生电感会让波形失真。SDA 和 SCL 的探头要分别接不要用一根探头同时碰两根线。7.2 用 i2c-tools 做快速验证i2c-tools 是 Linux 下 I2C 调试的标准工具集包含i2cdetect、i2cget、i2cset、i2cdump等命令。OpenHarmony 的调试版本通常可以编译进去或者通过串口终端手动推上去。i2cdetect -l列出所有 I2C 总线i2cdetect -y 3扫描总线 3 上的设备i2cdump -y 3 0x50把 EEPROM 的全部内容 dump 出来。这些命令在快速验证硬件和地址的时候非常方便比写代码快多了。需要注意的是i2cdetect的扫描原理是向每个地址发起一次读操作看有没有 ACK。有些设备在收到不完整的读操作时会进入异常状态所以扫描之前最好确认设备支持这种探测方式。大部分标准 I2C 设备都没问题但个别器件可能需要避免扫描。7.3 内核日志和 ftrace 的辅助排查当问题出在内核驱动层面时dmesg 里的日志是第一手资料。I2C 控制器驱动在出错时会打印错误信息比如超时、仲裁丢失、NACK 等。dmesg | grep i2c能过滤出所有 I2C 相关的日志。如果日志不够详细可以打开 I2C 核心的动态调试。echo 1 /sys/module/i2c_core/parameters/debug打开调试输出然后dmesg就能看到每次传输的详细信息包括地址、方向、数据长度、返回值。这个在定位驱动层面的问题时非常有用。ftrace 也可以用来跟踪 I2C 传输的调用链看是哪个函数发起的传输耗时多少。配置function_graphtracer过滤i2c相关的函数就能看到完整的调用流程和每个函数的执行时间。7.4 自己写一个 I2C 扫描工具有时候板子上没有 i2c-tools或者你想在应用层集成扫描功能可以自己写一个简单的扫描工具。核心逻辑就是遍历所有可能的 7 位地址0x08 到 0x77对每个地址发起一次空的写操作或者读操作看有没有 ACK。for (uint8_t addr 0x08; addr 0x77; addr) { struct I2cMsg msg; uint8_t dummy 0; msg.addr addr; msg.flags 0; msg.len 0; msg.buf dummy; int32_t ret I2cTransfer(handle, msg, 1); if (ret 0) { printf(Device found at 0x%02X\n, addr); } }这个工具在调试新板子的时候特别有用能快速确认总线上有哪些设备、地址是多少。注意len设为 0 表示只发地址不传数据有些 I2C 控制器可能不支持长度为 0 的消息那就发一个字节的 dummy 数据。8. 从 I2C 到整体系统设计的思考单独看 I2C它只是一条总线。但放到整个嵌入式系统里I2C 的设计会影响到电源管理、系统启动、设备热插拔等多个方面。这一章聊几个系统层面的考量。8.1 I2C 在系统启动流程中的角色很多系统的启动流程依赖 I2C。比如电源管理芯片PMIC通常挂在 I2C 上处理器上电后第一件事就是通过 I2C 配置 PMIC 的输出电压给 CPU 核心和内存供电。如果 I2C 通信失败系统可能连启动都启动不了。这种场景下I2C 的初始化必须在非常早的阶段完成通常在 bootloader 里就要配好。OpenHarmony 的启动流程里内核起来之前bootloader 已经通过 I2C 配置了 PMIC。内核起来之后I2C 驱动重新初始化接管总线。这个交接过程如果出问题可能导致系统在启动过程中挂死。排查启动阶段的 I2C 问题串口日志是关键。bootloader 阶段的日志通常能看到 PMIC 的配置过程如果卡在某一步对照 PMIC 数据手册看是哪个寄存器配置失败。内核阶段的日志看 dmesgI2C 控制器注册失败或者从机探测失败都会有打印。8.2 热插拔与设备动态管理I2C 本身不支持热插拔设备必须在系统启动时就在总线上。但有些应用场景需要动态添加或移除设备比如可插拔的传感器模块。这种情况下要么用支持热插拔的接口比如 USB要么在软件层面做处理。软件层面的处理方式是设备插入时通过 GPIO 中断通知系统系统再扫描 I2C 总线发现新设备后动态注册。设备移除时通过中断通知系统系统注销对应的设备节点。这个过程需要驱动和应用层配合比较复杂而且 I2C 总线在设备插拔瞬间可能产生毛刺影响其他设备的通信。如果项目里确实需要热插拔建议用 I2C 多路复用器每个可插拔设备挂在一个独立的通道上。插拔时只影响对应的通道不会干扰其他设备。多路复用器的通道切换由软件控制可以在设备插入后先关闭通道等设备稳定后再打开。8.3 I2C 总线的功耗优化在电池供电的设备里I2C 总线的功耗也需要考虑。上拉电阻在总线空闲时会有持续的电流流过虽然每个电阻只有几百微安但多个总线加起来也不容忽视。降低功耗的方法有几个一是增大上拉电阻但前面说过阻值太大会影响上升时间需要在功耗和性能之间权衡二是在总线空闲时把上拉电阻断开用 GPIO 控制上拉的供电通信前再打开三是降低通信频率频率越低单位时间内消耗的能量越少。还有一种做法是用 I2C 开关或者多路复用器在不访问某段总线的时候把那段总线断开断开后那段总线的上拉电阻就不耗电了。这个方法在多设备系统里效果明显。8.4 与其他总线的对比和选型I2C 不是唯一的选择SPI、UART、CAN 各有各的适用场景。I2C 的优势是引脚少两根线、支持多设备、有标准协议劣势是速率相对低、总线电容有限制、没有硬件流控。选型的时候看几个维度速率要求、设备数量、通信距离、功耗、成本。速率要求高几 Mbps 以上用 SPI设备多但速率要求不高用 I2C点对点远距离通信用 UART 或者 CAN需要多主和错误检测用 CAN。实际项目里经常是多种总线混用。比如 RK3568 上PMIC 和传感器用 I2C显示屏用 MIPI DSI调试串口用 UART外部存储用 SPI。每种总线发挥各自的优势系统整体最优。9. 写在最后的几句实在话I2C 这个东西入门容易精通难。协议本身不复杂两根线、几个信号看一遍数据手册就能懂。但真到了实际项目里各种奇怪的问题层出不穷每一个都要花时间去定位。我自己的经验是遇到 I2C 问题不要慌按照先硬件后软件、先波形后代码的顺序排查大部分问题都能在半个小时内定位到。逻辑分析仪是必须的投资几百块钱的入门款就能解决 90% 的问题。i2c-tools 是必须掌握的工具几条命令就能验证硬件和地址。设备树的配置要仔细地址、频率、pinctrl 这三样最容易出错。上拉电阻的取值要算一下不要随便抓一个 10kΩ 就用。还有一点数据手册永远是最好的老师。遇到问题先翻数据手册的时序图对照波形看哪一步不对。大部分 I2C 器件的时序要求都写得很清楚只是我们经常懒得看。我踩过的坑里至少有一半是因为没仔细看数据手册的某个细节。最后调试 I2C 的时候保持耐心。有时候一个问题查了一下午最后发现是杜邦线接触不良。这种时候不要怀疑自己的技术换个线、重新插一下可能就好了。硬件调试就是这样三分靠技术七分靠细心。
返回列表