OpenHarmony I2C驱动调试实战:从协议原理到HDF应用

发布时间:2026/11/3 1:05:34
OpenHarmony I2C驱动调试实战:从协议原理到HDF应用

做OpenHarmony开发的朋友,尤其是刚接触外设驱动这部分的人,最常打交道的外设总线就是I2C了。温度传感器、加速度计、OLED屏、eeprom存储、触摸芯片甚至电池管理芯片,大半都是I2C接口。我接手过的项目里,外设通讯出问题的地方,十有八九也在I2C上。不是协议本身多难,而是当你对着一条黑屏的OLED或者读回一堆0xFF的数据时,往往不知道问题出在哪儿:是硬件没焊好?地址写错了?时序不对?还是驱动框架层没配对?

这篇内容就是把我实际调试OpenHarmony设备时和I2C相关的使用经验、踩坑过程、排查逻辑完整梳理一遍。不讲空泛的概念,直接聊到底怎么用、报错了怎么定位,文中代码和配置基于OpenHarmony标准系统的HDF驱动框架,同时也会涉及一些通用I2C协议层面的知识,对做嵌入式Linux或者RTOS开发的朋友同样有参考价值。

1. 先把I2C协议本身吃透:不理解时序,后面排障全是猜

排障的前提是知道“正常”长什么样。很多开发者遇到I2C问题第一反应是改代码,但改来改去没效果,原因就是脑子里没有一套清晰的协议模型。I2C这套协议其实非常老,上世纪八十年代就定下来了,但它的设计到现在依然够用,弄明白它的时序和帧格式,比背一百个API都有用。

1.1 物理层与电气特性:两条线和上拉电阻

I2C总线物理上只需要两根线:SDA(数据线)和SCL(时钟线)。这两根线都是开漏结构,也就是说设备只能把线拉低,不能主动拉高,拉高靠外部上拉电阻完成。这正好对应了“线与”的特性,多个设备可以并联在同一对线上,只要不通信的时候大家都释放总线(输出高阻),总线电平就由上拉电阻维持在高位。

上拉电阻的取值直接决定通讯稳定性。阻值太大,线的上升沿太缓,速率上不去;阻值太小,电流过大,设备可能拉不动。常规做法是4.7kΩ配100kbps的标准模式,400kbps快速模式用2.2kΩ左右比较稳。但这里有一个经常被忽略的点:总线上的设备数量、走线长度和分布电容会影响实际上升沿,所以如果你在一块传感器特别多的板子上遇到偶发通讯失败,先不要怀疑代码,试着把上拉电阻换小一个档位,比如10k换到4.7k,大概率能解决。

供电电平也要留意。I2C设备有3.3V的,也有5V的,如果你的MCU是3.3V而传感器模块是5V供电但没做电平转换,最好用MOSFET做的双向电平转换电路,否则时间长了容易把芯片IO口烧出暗病,初期还正常,跑几天就失灵,这种问题最难查。

1.2 一帧数据长什么样:起始条件、地址帧、读写位、ACK/NACK

I2C通讯的起点是主机在SCL保持高电平的时候,把SDA从高拉低,这叫起始条件。结束的时候反过来,SCL高电平期间SDA从低拉高,叫停止条件。这里有个容易出现误判的点:SDA和SCL都是高的时候是空闲态,不是数据。你在逻辑分析仪上看到一长串方波,想找到哪里是帧的开始,先找SCL高的时候SDA有没有跳变沿。

地址帧是9个时钟周期:前7位是设备地址,第8位是读写方向(0写,1读),第9个周期是从机回ACK(低电平表示应答)。值得注意的是所谓“设备地址”通常是7位地址,而很多芯片手册上写的是8位地址,例如写地址0x78其实是7位地址0x3C左移一位的结果。这个坑在SSD1306这类驱动芯片上特别常见,手册上写0x78,你直接填0x78去读地址就找不到了,得填0x3C。

数据帧跟在地址帧后面,每字节9个时钟周期,前8位是数据,第9位是应答位。主机写数据时,从机回ACK表示收下了;主机读数据时,主机回ACK表示还要继续读,主机回NACK就表示读完了。所以NACK本身不一定代表错误,读操作的最后一个字节就应该收到NACK,写操作中间收到NACK才是真正的异常。

1.3 为什么软件模拟I2C不总是靠谱

很多人觉得GPIO口自己翻转就能模拟I2C,干嘛还要硬件控制器。软件模拟的好处是任意两个GPIO都能用,代码直观,调试方便。但代价是时序完全靠CPU一条条指令去翻,速度上不去不说,还容易被打断。系统里如果有高优先级中断频繁触发,一次通讯中间被挡住几十微秒,总线上就出现了超长电平,从机可能已经判定超时把数据丢了。

硬件I2C控制器负责产生完整的时序时序,CPU只需要把数据放进寄存器,硬件就按设定时钟把每个字节的波形发送出去。OpenHarmony的I2C外设驱动,如I2C-S3C2440等,底层的platform驱动已经把这些细节封装好了,设备驱动不用关心时钟翻转,只需要正确调用transfer接口。但是硬件I2C也有自己的脾气,比如它依赖时钟源配置,初始化时选错时钟源或者分频配错,出来的SCL频率会歪得离谱,设备不认。另外一部分SoC的硬件I2C有FIFO,如果FIFO深度很小,一次大块读取会被拆成多次小事务,这在多寄存器连续读时需要额外小心,不能让从机中途把地址指针搞丢。

提示:判断一个I2C问题是不是时序引起的,最直接的办法是示波器看SCL频率。用100kbps去配置,实际量出来如果是80kbps甚至更低,优先查时钟树和分频系数,而不是查设备地址。

2. OpenHarmony里的I2C驱动框架与API调用

OpenHarmony的驱动框架走的是HDF(Hardware Driver Foundation)体系。它的设计思路跟Linux的driver model有点像,但实现和接口全是自己的。I2C部分分成两层:一层是跟SoC硬件强相关的platform驱动,一层是跟具体外设强相关的device驱动。搞清楚这两层的关系,写驱动时才不会搞混自己该改哪个文件。

2.1 HDF驱动框架:平台驱动与设备驱动各管什么

平台驱动面向I2C控制器本身,做的事情是注册控制器、配置时钟、处理中断、提供统一的transfer操作接口。对于开发者来说,通常不需要动它,除非你要移植一个新的芯片平台。device驱动才是我们日常写业务代码时接触最多的部分,它面向具体外设,比如一颗温湿度传感器SHT30,一块OLED屏,驱动里去填写设备地址、寄存器配置,然后调用平台驱动提供的接口来发起通讯。

HDF里设备驱动和平台驱动的绑定靠的是device tree或者hdf配置文件。你写了一个I2C设备驱动之后,需要在配置里声明这个设备挂在哪个I2C控制器上,设备地址是多少。如果声明错了控制器编号,比如芯片上I2C0被用作其他用途,你新加的传感器实际接在I2C1上,那么驱动加载后所有的传输都会挂在错的总线上,读回来的数据自然不对。

这里还有一个概念值得理解:OpenHarmony的HDF引入了service和device的管理方式,设备驱动可以把自己注册成一个service,上层可以通过hid来访问。具体到I2C场景,上面提到的对外开放的Wrapper API和HDI接口,都是为了让应用层或者系统服务层能够以统一的接口来访问不同的I2C设备,避免应用层去直接操作platform driver。

2.2 核心API:I2cTransfer和它的参数细节

HDF里I2C设备驱动发起一次通讯,核心函数是I2cTransfer。它的原型本质上是一个消息数组,把所有要发送的字节按段组织好,一次调用完成一整笔交易。

struct I2cMsg { uint32_t addr; // 7位设备地址 uint32_t len; // 本段数据长度 uint8_t *buf; // 数据缓冲区 uint16_t flags; // 读写标志: I2C_FLAG_READ 表示读 };

这里的flags字段很关键。I2C总线上的读操作和写操作在地址帧就分开了,一个address后面如果跟的是写方向,那后面传的是数据;如果是读方向,后面传的是从机吐出的数据。跨段操作时OpenHarmony会在段与段之间自动处理start和stop条件,但你是否需要重复起始条件,会受到具体实现影响,某些平台上连续读写之间会自动插入stop再重新start,这对一些需要连续寻址的设备来说会有问题。所以当一个设备要求“先写寄存器地址再连续读数据”时,不要简单认为把两个msg放一个数组就万事大吉,要看你的平台对多段msg的处理策略是不是保持了总线占用。

一个典型的读eeprom操作,通常要先把目标存储地址写进去,然后重新发start(不是stop之后重新来),再发起读方向。这个场景里addr字段是一样的,但前面一段flags为0,后面一段flags为I2C_FLAG_READ。如果你的平台不支持在一次transfer调用里连续完成,你可以拆成两次独立调用,但第二次调用之前要先再发送一次设备地址,相当于重新寻址,这里时序上会浪费一点时间,但兼容性最好。

2.3 HDI层封装与上层应用怎么调用

在OpenHarmony的系统服务架构里,直接操作I2cTransfer的是内核态的HDF驱动,而应用层或系统服务层需要访问I2C设备时,通常走HDI接口。HDI的全称是Hardware Device Interface,它就是一套进程间通信的接口定义,把内核态的能力打包成上层可以调用的服务。

这中间有几个概念容易把人绕晕。你在社区看到有人说“通过HDI接口控制I2C”,实际上他可能是说在他的驱动里调用了平台的HDI接口,也可能是说他的应用通过某个系统服务间接访问了I2C。实操层面,我的建议是:如果只是给某个设备写驱动,老老实实在HDF device驱动里干,不要去动上层HDI;如果是做一个通用的测试工具,比如想从串口命令行直接读某个寄存器,那可以基于HDI写一个小的服务封装,把设备的读写操作暴露到系统层。

代码上,HDF的I2C设备驱动初始化时要先获取一个控制器句柄:

#include "hdf_platform.h" #include "i2c_if.h" static int32_t SensorInit(struct DeviceResourceIface *iface) { struct I2cController *controller = I2cGetController(CONTROLLER_ID); // CONTROLLER_ID 需要在配置文件中指定,比如I2C1对应1 // 拿到controller之后再设置设备地址和传输参数 }

这个controller的获取流程实际上是HDF管理平台驱动和设备驱动绑定关系的一种体现。设备驱动不直接操作寄存器,它通过I2cGetController拿到一个已经由platform驱动初始化好的控制器对象,之后所有通讯都由这个对象来完成。如果获取失败,优先检查你的设备驱动有没有在配置里声明正确的控制器编号,以及平台驱动有没有加载成功。

3. 实战:从零驱动一块SSD1306 OLED屏

理论说太多容易飘,我用一块常见的0.96寸OLED屏SSD1306来走一遍完整流程。SSD1306是128x64分辨率的OLED驱动芯片,走I2C接口时地址是0x3C,帧格式比较简单,几乎没花里胡哨的功能,适合当教学案例,也适合用来验证你的I2C通路是否正常——毕竟屏幕亮没亮,一眼就能看出来。

3.1 硬件连接与设备地址判断

SSD1306的I2C接口有四个引脚:GND、VCC、SCL、SDA。模组的I2C地址由SA0引脚决定,多数模块上SA0是直接接地的,对应7位地址0x3C;如果SA0通过电阻接到了VCC,地址就是0x3D。我写过不少模块,遇到过标称0.96寸的屏幕默认地址是0x3C,结果焊上去怎么都ACK不了,最后一量SA0电平才发现模块出厂时是0x3D。所以拿到一个新模块,第一件事不是写代码,而是拿万用表量一下SA0的电平,顺便查一下模组原理图。

连接线路时还有一个小细节:SSD1306模组上很多是自带I2C上拉电阻的,如果MCU那边的I2C总线上已经有其他设备共享,要算一下多个上拉电阻并联后的总阻值。2kΩ以下时SCL拉低时电流会明显增大,可能超出引脚的灌电流能力,表现为SCL低电平降不下去,通讯全乱。

3.2 初始化序列与读写流程

SSD1306上电后需要一段初始化序列,这串命令基本是固定的:

static const uint8_t ssd1306_init_cmds[] = { 0xAE, // display off 0xD5, 0x80, // clock div 0xA8, 0x3F, // multiplex ratio 63 0xD3, 0x00, // display offset 0x40, // start line 0 0x8D, 0x14, // charge pump on 0x20, 0x00, // memory addressing mode: horizontal 0xA1, // segment remap 0xC8, // com scan dec 0xDA, 0x12, // com pins 0x81, 0xCF, // contrast 0xD9, 0xF1, // pre-charge 0xDB, 0x40, // vcom detect 0xA4, // display on resume 0xA6, // normal display 0xAF // display on };

这里面有一个SSD1306特有的机制,也是新手最容易出错的地方:它的I2C数据流里,每个字节前面都要加一个控制字节。0x00表示紧跟的字节是命令,0x40表示紧跟的字节是显示数据。所以上面那串命令在实际发送时,每个命令字节前面都要插一个0x00。很多人直接把这串数组作为纯数据发给屏幕,结果屏幕什么反应都没有。

用HDF的I2cTransfer实现命令发送和写显示数据,逻辑是不同的:命令走的是一条带标志的msg,数据走的是另一条msg,两者的flags都是写方向(0)。显示数据的大块写入则要单独发一个0x40控制字节,然后把一帧图像数据作为后续数据发出。一些驱动把控制字节和数据合并在一个数组里,这没问题,但要注意拆分方式。

static int32_t Ssd1306WriteCmd(struct I2cController *controller, uint8_t cmd) { struct I2cMsg msgs[2] = {0}; uint8_t control_byte = 0x00; msgs[0].addr = SSD1306_I2C_ADDR; msgs[0].len = 1; msgs[0].buf = &control_byte; msgs[0].flags = 0; msgs[1].addr = SSD1306_I2C_ADDR; msgs[1].len = 1; msgs[1].buf = &cmd; msgs[1].flags = 0; return I2cTransfer(controller, msgs, 2); }

3.3 0.9寸OLED与0.96寸OLED的兼容坑

这个坑我踩得印象深刻。0.96寸OLED用的是SSD1306,分辨率128x64;而0.9寸OLED模组很多用的是SSD1306的128x32版本,也有一部分用的是SH1106。把针对SSD1306 128x64的初始化序列跑在0.9寸屏幕上,现象是屏幕能亮但显示位置错乱,画面只出现在上半部分,或者是出现残影、滚动错位。

根因在于两者显存页地址和列地址的组织方式不同。SSD1306 128x64有8页(每页8像素高)、128列;SSD1306 128x32只有4页、128列,如果仍然按8页去写页地址,超过4页的部分实际上会绕回或丢弃,导致数据错乱。SH1106的差异更大,它的列地址不是SSD1306那种连续的128列映射,而是带偏移的132列——内部显存是132列,但只有128列映射到屏幕,所以写显存时如果按SSD1306的方式从第0列开始,实际显示会整体偏移几列。

处理办法是:拿到一块屏幕先确认驱动芯片型号,0.9寸的屏幕如果有明显偏移,给它发送0xDA0x12这种列映射指令可能无效,得换SH1106的驱动序列。在实际项目里,我把屏的驱动型号和分辨率做进配套驱动配置,让上层根据硬件版本动态选择初始化序列,避免兼容性问题堆到业务代码里。

4. I2C排障方法论:从现象到根因

这一节是重点中的重点。I2C排障难就难在,现象千奇百怪,但根因往往就那么几类。学会把现象归类和定位,比盲目改配置要高效得多。

4.1 先分清问题在哪一层:物理/协议/驱动/应用

接到一个“I2C设备读不到数据”的bug,我通常按这个顺序排查。先不管代码,用万用表量一遍SCL和SDA上的静态电平。正常情况下空闲时两条线都应该是高电平,上拉到VDD。如果SDA一直是低,说明某个设备把总线拉死了,多半是这个设备损坏或者它的I2C状态机卡死了,优先级最高的是断电重启那个可疑设备。如果SCL一直是低,问题多半在主机控制器没释放时钟,或者某根线被错误地配置成了输出低。

层级判断有一个快速手段:把同一个设备接到一块Arduino或者树莓派的I2C接口上去读写,如果那边工作正常,说明设备本身是好的,问题出在你的板子或驱动上;如果那边也读不到,问题就在设备和接线这一侧。这一招很土,但能快速切断至少一半的可能性。

OpenHarmony场景下,还有一个特殊层级的排查点:HDF配置。我曾经遇到过驱动加载报错,日志里是“I2C device init fail”,但硬件是完全没问题的,最后发现是hdf配置文件里match_attr的名字和设备驱动里注册的名字不一致,导致设备驱动没有被正常匹配,I2C控制器句柄一直是NULL。所以当你的上层驱动调I2cTransfer时老是返回空指针或者错误码,先看HDF有没有真正把这个设备驱动attach上,不要一头扎进线序问题。

4.2 波形说话:逻辑分析仪与示波器的使用

排I2C问题,逻辑分析仪比示波器好用得多。示波器适合看电气质量,比如上升沿过缓、毛刺、电平幅度不够,但要解码协议内容,逻辑分析仪方便得多。市面上几十块钱的8通道逻辑分析仪加上sigrok或Saleae软件就能解出I2C的地址、方向、数据和ACK状态。

拿到波形之后,先看起始条件:SCL高时SDA有没有下降沿。再看地址帧,解出来的是不是你要的设备地址。如果解出来是0x3C但设备手册说是0x3C,此时从机没有回ACK,说明总线上根本没有设备响应这个地址,要检查SA0引脚的电平以及接线。如果ACK有了但数据不对,比如读出来的寄存器数值和预期差很大,可能是时序问题,设备在时钟速度太快时超时了,或者是不同页地址下的读写顺序没对齐。

有一个细节很多教程不会提:逻辑分析仪采样率不要设太低。I2C标准模式下SCL是100kHz,分析仪至少用1MHz采样率才有足够的波形密度来准确解码。你设成100kHz去采样,抓到的波形会失真,解出来的地址都可能错。400kbps快速模式下建议2MHz以上采样率比较稳。

4.3 经典错误代码与常见坑:资源不足、地址冲突、NACK

开发过程中最常见的报错形态之一是“资源不足”或者类似错误码,比如Windows设备管理器里有个“该设备找不到足够资源可以使用。 (代码 12)”的问题,在OpenHarmony里也有类似的味道,表现为驱动加载失败,日志提示无法申请资源。

造成这类问题的根因,第一种情况是I2C控制器编号冲突。比如你在配置里把device驱动绑到了I2C0上,但内核启动时I2C0已经被某个系统功能占了,资源分配时就失败了,此时要把设备绑定到空闲的I2C控制器上,或者复用已有控制器但不冲突地使用调度策略。第二种情况是GPIO复用冲突,SoC的引脚功能通常是多路复用的,同一个引脚在pinmux里先被配置成了PWM功能,后又被I2C驱动申请,资源冲突就报出来了。这时候要去板级dts或者hdf配置里把pinmux对应的模式改到I2C功能。

地址冲突是另一个高频梗。I2C总线上如果挂了两个设备用同一个地址,它们会同时回ACK,总线上出现竞争,读出来的数据是两边叠加的乱码。排查方法是逐一断开设备,看还能不能正常访问;项目上更干净的做法是在硬件设计阶段就注意地址选择引脚,尽量让每颗设备独立可寻址。如果实在改不了地址,再考虑总线分路或者使用I2C多路复用器。

还有一种容易忽视的NACK现象:地址应答正常,但写寄存器数据时从机回NACK。这个通常是设备进入了写保护状态或者命令字不对。比如eeprom在写完一页之后,写操作需要等待内部编程完成,这时候你立刻发下一个写命令,从机会回NACK表示busy。驱动里要加延时等待或者轮询设备是否空闲,否则连续写就失败。

排查速查表:把常见现象和可能原因列成一张表,运维时对号入座即可。

现象可能原因入手方向
无ACK响应地址错、设备没上电、SA0引脚不对、总线被拉死量电平、查地址、核对SA0
SDA一直为低某设备锁死总线逐个断开设备定位
读回全0xFF设备没应答、读时序不对、上拉没接看NACK位置、查上拉电阻
写命令无效控制字节弄错、命令序列不对检查0x00/0x40控制前缀
偶发通讯失败上拉阻值过大、SCL频率过高示波器看上升沿、降速
驱动加载失败/资源不足控制器编号冲突、pinmux冲突查HDF配置和板级引脚
休眠唤醒后通讯异常外设掉电、I2C控制器状态未重置重新初始化控制器和外设序列

5. 进阶扩展:总线不够用怎么办、低功耗场景下的I2C

项目做大了I2C总会遇到各种边界情况:总线上设备太多、地址不够用、休眠功耗压不下去、唤醒后设备状态错乱。这一节讲三个我实际处理过的场景。

5.1 用I2C多路复用器扩展总线规模

当总线上挂了超过一定数量的设备,分布式电容变大,通讯稳定性会明显下降。更麻烦的是I2C地址只有7位,去掉几个保留地址,去掉已经占用的,可用地址也就一百多个,但实际一条总线上同时挂二三十个设备时信号质量就已经很头疼了。I2C多路复用器,比如TCA9548A,可以解决这个问题。

TCA9548A本身占一个I2C地址,它内部有8个下游通道,每个通道都是独立的I2C总线。主机在访问任何下游通道设备前,先向TCA9548A写一个控制字节,比如0x01选通通道0,0x02选通通道1,之后主机的所有I2C通讯就会路由到对应的下游总线上。选通后上游总线上的普通操作不受影响,只是相当于把下游设备“挂”了上来。这样可以把几十个同地址的设备分别隔离到不同通道,避免地址冲突,也降低了每段总线上的等效电容。

使用多路复用器时有一个时序陷阱:切通道之后不是立刻就能访问下游设备的,TCA9548A内部有建立时间,通常也就几十微秒,但如果你在极端情况下没做任何延时,紧接着就发起传输,有概率在通道还没完全稳定时把数据发出去,导致首个ACk失败。稳妥起见,选通后加一个usleep(50)再操作下游设备。

5.2 休眠唤醒后的I2C复位问题

低功耗产品上,I2C的休眠问题特别典型。我们有块主板在进入休眠后会关掉外设的供电轨以省电,唤醒后再去读挂在I2C上的传感器,就发现通讯异常了。原因是传感器掉电后,它的SDA/SCL引脚处于未知状态,如果引脚在掉电状态下把总线拉低,唤醒后总线就被锁死了。

更隐蔽的一个问题是I2C控制器本身的状态机。主机在休眠前可能正在发一个字节发了一半,SCL停在低电平,唤醒后控制器如果没做复位,它会认为自己还在传输中,此时任何新的传输都会卡死或者产生错误的起始条件。解决办法是在唤醒流程里强制对I2C控制器做一次软复位,重新配置时钟和模式,同时对挂载的外设重新发送一遍初始化序列。

这里补充一个经验:不要把外设初始化序列放在系统启动时只跑一次,在低功耗唤醒场景里一定也得准备好重新初始化的入口。SSD1306这种屏幕,掉电再上电之后如果不重新发初始化序列,就算I2C总线正常,屏幕也只会显示雪花点。有的驱动工程师以为屏幕坏了,其实只是唤醒后少了初始化步骤。另外,在唤醒后重新初始化I2C设备之前,最好先给外设供电稳定后再留一段延时,比如等电源轨稳定后再给外设发命令,避免外设工作在半电压状态导致不可预期的行为。

5.3 I2C编码器与其他特殊器件的使用注意

最后顺便聊聊I2C编码器这类非典型I2C设备。常见旋转编码器一般是GPIO输出A/B相,但有些高精度磁编码器芯片,如AS5600,走I2C读绝对角度。这种设备的特点是I2C寄存器数据不是直接从外设拉回来的“状态”,而是需要先写寄存器地址,再重新发起读方向,并且读的过程中整个寄存器地址指针要连续。如果中途stop了再重新寻址,寄存器的地址指针也会复位,读取的位置就不对了。

AS5600初始状态默认在低功耗模式,如果你上电后立刻去I2C读它的角度值,可能读出来全0,不是寄存器没数据,而是芯片还在唤醒过程。此类芯片一般要求延时至少几百毫秒再开始访问。写驱动时不要把这个“漫长的初始化延时”优化掉,省下的那几百毫秒会在实际产线上全部变成花的成本,反而更亏。

还有一类器件是带I2C接口的编码器或者触摸IC,它们可能要求主机在传输中支持“重复起始条件”,因为同一个传输事务里要先写寄存器再连续读。OpenHarmony的HDF平台驱动在I2cTransfer里对不同msg的组合是否支持repeated start,我会专门查一下对应平台的实现,如果没有就自己拆成两段,在中间手动插入stop再start。虽然速度慢一点,但通用性最好,不会有兼容性坑。

结尾:从协议到底层,一次把I2C调通的经验

我自己调试I2C踩过太多坑,花在协议理解和波形观察上的时间远远多于写代码的时间。如果要说一句经验之谈,就是永远先把硬件和时序确认到底了再去怀疑驱动代码,别把“I2C读不到数据”归咎于框架bug。大多数情况下,问题出在地址和时序这些看似最基础的地方。另外,一块逻辑分析仪和一页记录打印日志的脚本,绝对是I2C工程师最好的朋友。设备挂上总线之前,先用I2C扫描把所有存在的设备地址都列出来,这也是一种好习惯,省得后面反复排查。OpenHarmony的HDF框架虽然封装多,但只要按平台驱动、设备驱动、HDI服务三层去理解,调试的时候就会非常有方向感。再有新项目拿到手里,花十分钟把I2C总线上挂了哪些设备、地址是什么、控制器的编号是否对得上这些问题确认清楚,后面能省掉一整天的排障时间。