ARTICLE DETAIL

资讯详情

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

OpenHarmony I2C总线开发实战:从驱动配置到排障手册

OpenHarmony I2C总线开发实战:从驱动配置到排障手册 做嵌入式这些年I2C 大概是我打交道最多的总线之一。如果你玩过开源鸿蒙 OpenHarmony在接传感器、屏幕、触摸、存储芯片的时候十有八九会碰到 I2C 总线。和其他总线相比I2C 就靠两根线——SCL 和 SDA能把一堆设备串在一条总线上地址用软件区分省针脚又方便。但两根线带来便利的同时也带来了独特的排障难度一个设备出问题可能把整条总线拖死。围绕 I2C 的用法和排障我把平时积累的东西整理一份希望能帮大家少踩几个坑。这篇文章主要回答两个问题I2C 总线在 OpenHarmony 上应该怎么用设备上电后通信不正常该按什么思路排查如果你正在做 OpenHarmony 外设驱动开发或者刚入手带 I2C 屏、触摸、传感器这类设备这篇内容应该能帮上忙。基础概念我会按需讲重点放在实操和踩坑尽量不写那些从协议规范里直接抄出来的大段理论。1. 先弄明白 I2C 到底是个什么东西1.1 一条总线、两根线、多个设备I2C 的全称是 Inter-Integrated Circuit最早是飞利浦搞出来的串行通信总线。它最吸引人的地方在于所有设备都挂在同一条 SDA 和 SCL 两根线上主机通过设备地址来选择跟谁通信。每个设备出厂时都有自己的地址有些设备还留了地址引脚比如 A0、A1、A2通过电平组合可以在一定范围内改地址从而在同一总线上挂多个同型号设备。这里有个新手特别容易混淆的点DS18B20 这类温度传感器用的是单总线OneWire不是 I2C。虽然它们都只有一根数据线但单总线靠时序区分位I2C 要靠 SCL 给时钟两者协议完全不同。我在 OpenHarmony 开发群里见过有人把 DS18B20 当 I2C 设备去配置折腾半天没反应后来才发现接口都选错了。I2C 通信本身是主从模式主机提供 SCL 时钟并发出起始、停止条件从机被动响应。通信以 9 个时钟为一个基本单位前 8 个时钟传一个字节数据第 9 个时钟用来回应答ACK。主机向从机写数据时如果从机正常收到会在第 9 个时钟把 SDA 拉低作为应答主机读数据时从机发送数据主机拉低 SDA 表示应答。如果流程中断在这个应答位上通常就是地址错误、设备未上电或者从机卡死。1.2 在 OpenHarmony 里I2C 是怎么被抽象出来的OpenHarmony 的驱动框架叫 HDFHardware Driver Foundation它把 I2C 控制器抽象成了一组标准接口驱动开发者不用直接去操作一个 I2C 控制器的寄存器而是通过 I2C API 来收发数据。这样做的好处是上层驱动和具体平台解耦同一份代码在 Hi3861、RK、海思等不同平台上复用性更强。在 OpenHarmony 上操作 I2C 的基本三步是打开控制器、传输数据、关闭控制器。对应的接口是 I2cOpen、I2cTransfer、I2cClose。实际写驱动时一般会在外设驱动代码里通过 HDF 的消息或接口调用拿到这些能力然后封装成自己的传感器、触摸屏、EEPROM 读写函数。我见过很多刚从单片机转过来的人以为 OpenHarmony 上还要像 STM32 HAL 那样管寄存器其实完全没必要HDF 已经把这些细节处理掉了。不过要注意的是HDF 的 I2C 接口虽然统一但不同版本之间传参结构细节可能有差异。我建议以你使用的那版 SDK 的i2c_if.h为准。下面代码里我用的是常见版本的结构I2cMsg包含addr、len、buf、flags四个关键字段其中addr是 7 位设备地址不是 8 位。这一点非常容易踩坑后面会细说。2. 关键参数与时序不懂这些没法排障2.1 常问的几个速度档位——100k、400k、1MI2C 的时钟速度分几个档位标准模式 100kbit/s快速模式 400kbit/s快速模式 1Mbit/s再往上还有高速模式。日常接触最多的是 100k 和 400k。在 OpenHarmony 里配置 I2C 控制器时一般会有一个总线频率字段需要根据外设支持的最高频率来设置。很多人想当然地以为速度越高越好其实不对。I2C 是开漏结构SCL 和 SDA 靠上拉电阻把电平拉高下降沿则是设备主动拉低。所以总线的上升时间由外部上拉电阻和总线上所有设备的寄生电容共同决定。如果上拉电阻太大或者总线电容太大上升沿就会变得很缓达到高电平阈值的时间不够时序就会出错。我曾经把一个加速度计的频率从 400k 降到 100k通信瞬间稳定原因就是布线太长、总线电容太大。如果需要估算上拉电阻的范围可以套一个很粗略的思路最小电阻要保证设备能拉低到可识别的低电平常用公式是Rmin (Vcc - VOL_max) / IOL_max比如 3.3V 系统、输出低电平最大 0.4V、灌电流能力 3mARmin 大约是 1kΩ。最大电阻要保证在规定的上升时间内把线上电容充到高电平阈值比如总线电容 100pF、上升时间 1μsRmax 可以算到 10kΩ 级别。实际工程里 4.7kΩ 用得最多如果你总线设备超过 5 个或者线比较长我会降到 2.2kΩ 甚至 1kΩ追求一个更快的上升沿。2.2 时序里最容易踩坑的三个位置I2C 的时序图看起来复杂但实际调板子绝大多数问题集中在三个位置。第一个是起始和停止条件。SCL 为高时SDA 从高变低算起始SDA 从低变高算停止。如果从机在上电初始化时错过起始条件后面所有数据都会错位。这种问题多发生在主机发送起始条件过快或者从机复位后还没准备好时。第二个是 ACK/NACK 位。主机发完一个字节第 9 个时钟就是应答窗口。如果从机不拉低 SDA就变成了 NACK。最常见的原因是设备地址不对。很多芯片手册里写的地址是 8 位地址比如 0xE0但实际上它在 I2C 总线上的 7 位地址是 0x700xE0 右移一位。在 OpenHarmony 的I2cMsg.addr里你应该填 0x70如果把 0xE0 直接填进去多半会收到 NACK。第三个是时钟拉伸clock stretching。有些从机处理数据不够快会在需要时间的时候把 SCL 拉低让主机暂停等待。大多数硬件 I2C 控制器支持时钟拉伸但用 GPIO 模拟 I2C 时更容易出问题代码里如果没检查 SCL 的低电平状态就直接拉高时序就乱了。我在 OpenHarmony 的 OLED 驱动里就遇到过从机需要一点刷新时间必须等待检查到 SCL 被释放才能继续发下一个字节。3. 在 OpenHarmony 上实操 I2C从打开到收发3.1 环境准备与设备配置要在 OpenHarmony 上用 I2C得先确认板子上的 I2C 控制器有没有被使能。这部分通常涉及设备树或者板级配置不同芯片平台差异挺大。有的平台直接在dts里配置一个 I2C 节点有的平台通过 HDF 的 vendor 配置加载驱动。我建议你拿到一块新板子时先看厂家提供的内核配置和示例代码。比如 RK 平台一般会看到类似i2c_csi、i2c_gpio之类的 dtb 节点里面设置了时钟频率、引脚复用。如果 I2C 控制器没有被正确使能后面调用I2cOpen返回的句柄就是空这个问题经常在刚开始就被误判成外设问题。一种更快的确认方法是在用户态写一个小 Demo调用 HDF 的 I2C API 读某个设备的寄存器如果返回成功说明控制器和线路基本通了。如果一上来就在复杂的传感器驱动里排查变量太多很难定位。比如我有一次调一个音频 CodecI2C 设备地址写错了但又以为是音频链路的问题最后绕了一大圈才发现是地址移位搞错。3.2 读写 API 调用示例这里给一个 OpenHarmony HDF I2C API 的读写示例。假设我们要读一个温度传感器寄存器地址是 0x007 位设备地址是 0x38。#include i2c_if.h #define I2C_BUS_NUMBER 0 #define SENSOR_ADDR 0x38 /* 7位地址 */ #define REG_ADDR 0x00 int ReadSensorReg(uint8_t reg, uint8_t *value) { DevHandle handle I2cOpen(I2C_BUS_NUMBER); if (handle NULL) { printf(I2cOpen failed\n); return -1; } struct I2cMsg msgs[2]; uint8_t regAddr reg; /* 第一段向从机写寄存器地址 */ msgs[0].addr SENSOR_ADDR; msgs[0].flags 0; /* 0 表示写 */ msgs[0].len 1; msgs[0].buf regAddr; /* 第二段从该寄存器读一个字节 */ msgs[1].addr SENSOR_ADDR; msgs[1].flags I2C_FLAG_READ; /* 读标志 */ msgs[1].len 1; msgs[1].buf value; int32_t ret I2cTransfer(handle, msgs, 2); if (ret 0) { printf(I2cTransfer failed, ret%d\n, ret); I2cClose(handle); return ret; } I2cClose(handle); return 0; }这段代码看起来简单但有几处很关键的细节。第一I2cTransfer的第二个参数是msgs数组第三个参数是数组个数。它支持一次传输多段消息内核会自动在段与段之间插入重复起始条件repeated START这在读取寄存器场景下非常有用。第二msgs[0].buf和msgs[1].buf必须是可读写的缓冲区有的平台还会要求地址对齐。我遇到过在用户态用栈变量没问题但在内核态驱动里要用kmalloc分配的情况直接传栈指针偶发失败。第三I2cTransfer的返回值代表什么要看具体实现。有的平台返回实际传输的字节数有的平台返回 0 表示成功返回负数表示错误。所以代码里最好加日志把返回值打出来方便定位。不要只判断等于 0应判断小于 0 才是失败。3.3 一次 I2C 传输的背后发生了什么很多人以为调I2cTransfer只是发几个字节其实 OpenHarmony 的 HDF 框架会把这个调用下发到 I2C 控制器驱动由控制器完成时序产生和中断处理。如果你的设备是 EEPROM比如 AT24C02想读某个地址的数据不能像普通寄存器那样直接写地址然后 read而是要组合成写地址、重复起始、读数据三段消息原因就是 EEPROM 内部有一个地址指针先写入地址后紧接着读取才会返回该地址的数据。在写驱动时如果一次收发需要超过一页或者超过缓冲长度要注意拆分。比如 EEPROM 页写通常一次最多写 8 字节或 16 字节超过页大小写入有些芯片会把地址自动回卷导致数据覆盖错误。我调 AT24C02 时就被这个坑过连续写入超过 8 字节后前面的数据被覆盖了后来改成每页一块、拆分传输才解决。另一个要留意的是超时。HDF 的 I2C 接口一般会给控制器驱动配超时时间但如果总线卡死了I2cTransfer可能会一直阻塞或者返回超时错误。代码层面建议对I2cTransfer设置合理的调用超时不要无限等。有些版本会把超时时间放在消息里有些则是控制器驱动内部配置你需要看一下自己版本的接口定义。4. 实战排障手册I2C 上板后不工作的七个常见原因4.1 从无应答NACK开始排查无应答是最常见的 I2C 故障现象。你发了很多字节但总线一直不出现低电平的 ACK读寄存器返回的全是 0xFF 或者错误码。先把设备的供电和地线量一遍别急着怀疑上拉。很多传感器是 1.8V 供电如果没供电SDA/SCL 的静态电平可能被微弱拉高看起来“正常”但设备根本不会响应。如果供电没问题重点查地址。前面提到的 7 位和 8 位地址转换是我见过最普遍的失误。设备手册如果写“Write Address 0xDC”那说明 8 位地址是 0xDC7 位地址就是 0x6E。I2C 总线上实际发送的字节是 7 位地址左移一位再加上最低位 R/W 位写是 0读是 1。所以在 OpenHarmony 的I2cMsg.addr中填 7 位地址 0x6E而不是 0xDC。还有一种情况是设备地址引脚没接对。比如 GT911 触摸屏的地址引脚是高电平还是低电平直接决定了 7 位地址是 0x5D 还是 0x14。如果原理图上地址引脚悬空默认电平可能不是你预期的那档导致 NACK。我修过一个 GT911 通信失败的 case最后就是发现复位时序里把 INT 引脚和地址引脚搞混电平不对地址变了。4.2 总线卡死SDA 一直被拉低这是一种比较恶心的现象用万用表量 SDA发现它一直是低电平SCL 还在跳但所有通信都失败。原因通常是某个从机设备进入了异常状态持续把 SDA 拉低。最常见的是通信过程中刚好遇到从机复位、看门狗复位或者电源闪断从机内部的 I2C 状态机没回到 IDLE就错误地认为当前还在某个数据位中。软件上最常用的恢复手段是让主机连续产生 9 个 SCL 时钟脉冲。因为 I2C 从机在异常状态下如果还在等待一个完整字节的第 9 个时钟这 9 个脉冲能帮它收完当前“半截”数据然后释放 SDA。很多平台驱动里已经把这种 recovery 逻辑留在 I2C 控制器的 probe 函数中但有些没有。我在 OpenHarmony 上遇到过每次系统休眠唤醒后 I2C 总线就锁死后来在唤醒回调里先做一次 GPIO 模拟的 9 时钟 recovery再重新打开 I2C 控制器问题就消失了。如果 9 个时钟还解决不了那就必须断电重启设备或者重新上电。在产线调试里这种总线锁死经常是设备初始化时序冲突导致的。排查思路是在通信前先检查 I2C 总线是否空闲即 SDA 和 SCL 都为高。有些控制器在 open 时会做这个检查但自己的驱动里也可以再确认一下。4.3 时序不满足导致的偶发错误有时候通信大部分时间正常但偶发读错值或者超时这种情况极可能是时序裕量不足。最典型的特征是把 I2C 时钟频率从 400k 降到 100k 后问题频率明显下降。原因不一定是控制器时钟不准而是上拉电阻太大或者总线电容太大导致上升沿太慢。排查方法是看逻辑分析仪抓出来的波形。重点看 SCL 和 SDA 的上升沿是否陡峭如果呈现明显的斜坡上升时间接近甚至超过协议允许的最大值就要考虑减小上拉电阻。 4.7k 换 2.2k或者换 1k通常立竿见影。另外如果总线上挂了很多个 I2C 设备注意它们的输入电容会叠加这时候总线电容可能轻松超过 200pF一根短线就足以成为瓶颈。另一个时序坑是 I2C 的中断优先级。如果你在实时性很强的系统里SCL 高电平期间来了高优先级中断导致主机没能在规定时间内拉低 SDA 或继续翻转 SCL就会破坏既有时序尤其用 GPIO 模拟 I2C 时更容易出现。 OpenHarmony 大多数平台用硬件控制器还好但如果是自己用 GPIO 模拟最好把中断改成线程或加锁避免在一位数据的中间被打断。我见过一个项目在启动阶段偶发传感器读到 0x7F排查到最后是调试串口中断频繁打断 GPIO 模拟时序把串口中断优先级调低后就好了。4.4 地址线与电平匹配的坑地址线不光是 NACK 的常见原因也是配置错误的温床。很多芯片有多根地址线比如 A0、A1、A2通过接高接低组合出多个地址。你在 OpenHarmony 驱动代码里写的地址必须和硬件实际电平均匹配。我建议在原理图阶段就把每个设备和它实际的 7 位地址做成一张表并在每个设备驱动里通过宏定义或配置节点传入避免后期到处改。电平匹配是另一个容易踩雷的点。现在很多传感器是 1.8V 接口而主控 I2C 上拉可能是 3.3V。直接连接很容易导致主控读不到正确电平甚至损坏设备。正确做法是加 I2C 电平转换芯片比如 PCA9306或者用支持混合电压的 MCU。有些板子把上拉电阻接到了错误的电源域导致总线上、下拉都不彻底波形呈“中间态”这种故障最隐蔽用示波器看波形会发现高低电平幅值都不够。我在一块板子上遇到过 I2C 上拉电阻被贴到了错误的电源域3.3V 的设备串到 1.8V 的上拉上读出来的寄存器值每次都不一样。后来用万用表量 SDA 对地电压发现只有 1.6V 左右果断把上拉电阻改成 3.3V 供电问题立刻消失。4.5 干扰导致的通信中断I2C 的布线如果太长或者和电源、PWM 线走在一起极易引入干扰。干扰导致的故障通常是偶发读写错误、CRC 错误或者总线检测到停止条件提前触发。降低通信速率、缩短链路、屏蔽线缆都是通用手段。如果板子上空间允许可以在 SDA/SCL 对地各加一个几十皮法的小电容来滤除毛刺但需要注意这会增加总线电容反过来拖慢上升沿所以别加太大。还有一种很容易忽略的干扰源是电源。I2C 设备电源如果纹波大可能造成设备内部状态机误判在总线上产生错误的起始/停止位。可以给传感器电源加一个 10μF 0.1μF 去耦电容。我排障时通常会先看示波器上的电源纹波再去看 I2C 波形顺序反过来的话容易被抓不到的随机毛刺带偏。4.6 排查工具优先级工欲善其事必先利其器。I2C 排障我强烈建议的顺序是万用表 → 逻辑分析仪 → 示波器。万用表最先用来量静态电平、供电和上拉电阻确认没有明显的短路、断路。然后上逻辑分析仪逻辑分析仪能抓取协议帧直接看出 ACK 位、起始/停止条件、地址字节价格也不贵几十上百块钱的就能满足日常调试。示波器主要用于看波形质量、上升沿时间和干扰当怀疑硬件信号完整性问题时再用。如果手头只有万用表可以尝试不断枚举地址来观察 SDA 在哪个地址位后保持高电平但效率太低。有一回我帮一个朋友远程排障他硬是没用工具从早猜到晚也没搞定后来让他接上逻辑分析仪立刻看到从机返回的 ACK 位后多了一个奇怪的时钟周期才发现是 GPIO 模拟驱动里发了一位数漏了时钟。所以别省工具时间。5. 一些想让你少走弯路的经验5.1 代码层面别忽略错误码和超时很多 OpenHarmony 上的 I2C 驱动样例只写了“打开、传送、关闭”没有仔细处理错误码。但真实项目中I2C 故障是常态而不是意外。建议每个I2cTransfer调用都保存返回值并打日志。另外HDF 的消息发送也经常需要等待完成如果你的驱动在某些线程上下文里调用会阻塞注意设置超时。日志也要有分级策略。系统正常运行时把 I2C 日志级别调到 DEBUG 会拖慢性能但调板的时候把 I2C 日志打开能帮你精确知道发了几条消息、哪些消息超时。我通常会在驱动里加一个#define DEBUG_I2C开关只有调试版开启。还有一点不要在一个 I2C 读写函数里写死太长的重试。比如连续重试 5 次每次间隔 10ms如果设备掉线整个调用栈就像卡住了一样。更好的方式是快速失败把错误上报到上层由业务层决定是降低频率、重置总线还是弹错误提示。5.2 硬件层面上拉电阻的取舍上拉电阻真的是 I2C 的“玄学”。选太大上升沿太慢选太小低电平可能识别不了。一个比较稳的起点是 4.7kΩ。如果总线设备很多或者线长我会换 2.2kΩ。对快速模式 400k这个范围一般都能跑。如果你用 1.8V 电平上拉电阻也要相应调整因为 1.8V 下驱动能力更弱通常直接选用 2.2k 或 1k 来确保足够的上升速度。还有一点如果总线上某个设备的 SDA/SCL 引脚有内置上拉它和外部上拉并联会减小等效电阻可能反而让拉低能力不足的边缘设备识别不了。所以我一般先看原理图外部上拉和内部上拉是否冲突。动手改上拉之前先量一下当前静态电平再决定电阻值。5.3 调试顺序先看硬件再抓波形再翻驱动最后分享一个我自己的调试顺序按这个顺序排查 I2C 问题通常效率最高。第一步看硬件连线供电有没有、地线是不是一根完整的地、SCL/SDA 有没有接反、上拉有没有焊好。第二步上电后用万用表量 SDA/SCL 静态电平正常应该是接近 VCC 的高电平。如果发现 SDA 被拉低基本可以判断有设备卡死或短路。第三步接逻辑分析仪看第一帧波形确认地址、ACK、数据字节是否符合预期。第四步才轮到翻驱动代码查地址、flags、字节序、寄存器映射。我见过太多人一上来就怀疑 OpenHarmony 驱动有问题然后改一堆 HDF 配置最后发现是排针接触不良。其实硬件引脚焊接不良引起的 I2C 故障比想象中多得多尤其是用杜邦线临时连接的时候。结语前的最后提醒我个人在实际操作中的体会是I2C 排障最怕的就是想当然。你以为地址没问题你以为上拉没问题你以为驱动没问题但总有一个你以为在坑里等着你。所以我现在每调一块新板子都先做一张 I2C 设备清单写清楚每个设备的总线号、7 位地址、供电电压、上拉策略然后在第一版样机上用逻辑分析仪把所有设备的正常波形截下来存档。后面再出问题直接拿波形对比比在代码里加一堆打印管用得多。希望这篇文章能帮你把 I2C 的坑提前填掉让 OpenHarmony 上的外设开发不再那么挠头。
返回列表