ARTICLE DETAIL

资讯详情

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

OpenHarmony I2C外设开发实战:从设备树配置到排障全链路

OpenHarmony I2C外设开发实战:从设备树配置到排障全链路 1. 从一个真实场景说起为什么I2C总让人又爱又恨搞OpenHarmony外设开发的朋友十个里有八个在I2C上栽过跟头。我自己第一次在RK3568开发板上接一颗GT911触摸屏的时候设备树配好了、驱动编译过了、系统也起来了结果触摸屏就是没反应。用逻辑分析仪一抓波形SCL有信号SDA一直是高电平——从机根本没应答。折腾了大半天才发现设备树里I2C控制器的时钟频率配成了400kHz而那颗GT911的上电初始化阶段只认100kHz。改完频率触摸立刻正常。这就是I2C的典型特征协议本身简单到用两根线就能通信但实际调试中涉及的东西一点都不少——设备树配置、时钟频率、上拉电阻、地址冲突、时序匹配、总线仲裁任何一个环节出问题都会导致通信失败。而且I2C的故障现象往往很“沉默”没有报错、没有崩溃就是读不到数据让你无从下手。这篇内容面向的是正在用OpenHarmony做外设开发的工程师不管你是在RK3568、Hi3861还是其他平台上工作只要你需要跟传感器、EEPROM、触摸屏、OLED这类I2C设备打交道这里的内容都能直接拿来参考。我会从I2C的协议原理讲起然后落到OpenHarmony下的设备树配置和驱动开发最后重点讲排障——毕竟这才是大家最头疼的部分。2. I2C协议核心机制两根线背后的门道2.1 物理层为什么只需要两根线I2CInter-Integrated Circuit的物理层极其精简一根SCLSerial Clock传时钟一根SDASerial Data传数据。所有设备都挂在这两条线上每条线通过一个上拉电阻接到电源正极。这个上拉电阻是整个I2C总线能工作的关键——I2C的输出级是开漏Open-Drain结构设备只能把线拉低不能主动拉高。线要变高靠的是上拉电阻把电平拉回去。这个设计带来的好处很直接多个设备可以同时挂在同一组总线上任何一个设备拉低总线其他设备都能感知到。这就是I2C支持多主多从和总线仲裁的物理基础。但代价是上拉电阻的选值很讲究。阻值太大上升沿变缓高速通信时波形还没到高电平就被拉低了阻值太小功耗增加而且某些设备的灌电流能力有限可能拉不低。经验值100kHz标准模式下用4.7kΩ400kHz快速模式下用2.2kΩ到4.7kΩ1MHz以上高速模式建议用1kΩ左右。但最终值要根据总线电容来算总线电容一般要求不超过400pF。2.2 协议层起始、停止、应答与数据帧I2C的通信流程可以用一句话概括主机发起起始条件发送从机地址和读写位从机应答然后双方按字节交换数据最后主机发停止条件。起始条件Start的定义是SCL为高电平时SDA从高变低。停止条件Stop则相反SCL为高电平时SDA从低变高。这两个条件由主机产生是每次通信的“书签”。数据帧格式上每次传输以字节为单位MSB先发。每发完一个字节接收方需要拉低SDA一个时钟周期作为应答ACK表示“收到了”。如果接收方不拉低NACK发送方就知道出了问题。一个典型的写操作时序是这样的Start → 从机地址(7bit) W(0) → ACK → 寄存器地址(8bit) → ACK → 数据(8bit) → ACK → ... → Stop读操作稍微复杂一点需要先写寄存器地址再重启读Start → 从机地址 W → ACK → 寄存器地址 → ACK → Restart → 从机地址 R → ACK → 数据 → NACK → Stop注意最后主机发的是NACK而不是ACK这是告诉从机“我读够了你可以释放SDA了”然后主机才能发出停止条件。2.3 时钟同步与仲裁多设备共存的关键当总线上有多个主机时I2C靠“线与”逻辑实现时钟同步和总线仲裁。时钟同步的意思是多个主机同时发时钟SCL的实际高电平时间是所有主机中高电平最短的那个决定的。仲裁的意思是多个主机同时发数据谁先发出与总线状态不一致的数据谁就退出。这套机制保证了I2C在多主场景下不会出现数据冲突但也意味着一旦总线上某个设备异常拉低SDA不放整个总线就挂了。这是排障中最常见的情况之一后面会详细讲。2.4 I2C与其他总线的对比特性I2CSPIUARTCAN线数2422速率100k-3.4M1M-100M通常1M最高1M多设备支持地址寻址支持片选点对点支持报文ID时钟同步同步异步异步典型场景传感器、EEPROMFlash、屏幕调试、模块汽车电子I2C的优势在于引脚少、支持多设备、有标准协议缺点是速率相对较低、总线电容有限制、排障不如SPI直观。选型的时候如果你需要挂多个低速外设且引脚紧张I2C是首选如果追求高速率SPI更合适。3. OpenHarmony下的I2C开发环境与设备树配置3.1 OpenHarmony的HDF驱动框架与I2C子系统OpenHarmony的外设驱动走的是HDFHardware Driver Foundation框架。跟传统Linux驱动不同HDF把驱动分成了驱动实现和驱动配置两部分配置部分通过HCSHDF Configuration Source文件描述最终编译成设备树信息供驱动读取。I2C子系统在HDF中的层次结构大致是最上层是I2C服务接口提供读写API中间是I2C核心层管理控制器和设备最下面是I2C控制器驱动跟具体SoC相关。你在写外设驱动的时候调用的是I2C服务接口不需要关心底层控制器的实现细节。3.2 设备树中I2C控制器的配置以RK3568为例I2C控制器的设备树节点通常在kernel/linux/patches/linux-5.10/arch/arm64/boot/dts/rockchip/rk3568.dtsi中定义。一个典型的I2C控制器节点长这样i2c1: i2cfe5a0000 { compatible rockchip,rk3568-i2c; reg 0x0 0xfe5a0000 0x0 0x1000; interrupts GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH; clocks cru CLK_I2C1, cru PCLK_I2C1; clock-names i2c, pclk; pinctrl-names default; pinctrl-0 i2c1_xfer; #address-cells 1; #size-cells 0; status disabled; };关键字段说明compatible匹配驱动用的字符串格式是“厂商,型号”。reg控制器的寄存器基地址和长度。clocks控制器需要的时钟源。pinctrl-0引脚复用配置指定SCL和SDA用哪组引脚。status默认是disabled需要在板级设备树中改为okay。在板级设备树比如rk3568-evb.dts中启用并配置i2c1 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c1_xfer; gt911: touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 RK_PB6 GPIO_ACTIVE_LOW; irq-gpios gpio0 RK_PB5 GPIO_ACTIVE_HIGH; status okay; }; };这里有几个容易踩坑的地方clock-frequency不写的话默认是100kHz但有些SoC的默认值可能不同建议显式指定。reg的值就是I2C从机地址的7位表示不包括读写位。GT911的地址是0x5D或0x14取决于上电时INT引脚的状态。pinctrl-0一定要确认引脚复用配置跟实际硬件一致否则SCL/SDA可能根本没输出。3.3 HCS配置文件与驱动绑定在OpenHarmony的HDF框架中除了Linux内核的设备树还需要在HCS文件中配置驱动信息。HCS文件通常放在vendor/厂商/产品/hdf_config/目录下。一个I2C设备的HCS配置示例device_i2c :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; moduleName HDF_I2C; serviceName HDF_I2C; deviceMatchAttr i2c_config; }; }然后在具体设备的HCS中引用device_gt911 :: device { device0 :: deviceNode { policy 2; priority 100; moduleName gt911_driver; serviceName gt911_service; deviceMatchAttr gt911_config; }; }deviceMatchAttr是驱动和配置之间的桥梁驱动代码中通过这个属性找到对应的配置节点。3.4 I2C读写API的使用OpenHarmony的I2C服务接口定义在drivers/hdf_core/framework/include/platform/i2c_if.h中。核心API有这几个int32_t I2cOpen(int16_t number); int32_t I2cClose(int16_t number); int32_t I2cTransfer(int16_t number, struct I2cMsg *msgs, int16_t count);I2cTransfer是最常用的它接受一个I2cMsg数组可以一次性完成“写寄存器地址读数据”的复合操作。I2cMsg的结构struct I2cMsg { uint16_t addr; // 从机地址 uint16_t flags; // 读写标志I2C_FLAG_READ或I2C_FLAG_WRITE uint16_t len; // 数据长度 uint8_t *buf; // 数据缓冲区 };读一个8位寄存器的典型用法struct I2cMsg msgs[2]; uint8_t regAddr 0x00; uint8_t readBuf[2] {0}; msgs[0].addr 0x5D; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf regAddr; msgs[1].addr 0x5D; msgs[1].flags I2C_FLAG_READ; msgs[1].len 2; msgs[1].buf readBuf; int32_t ret I2cTransfer(1, msgs, 2); if (ret ! 2) { // 处理错误 }注意I2cTransfer的返回值是成功传输的消息数量不是字节数。如果返回小于传入的count说明某条消息传输失败了。4. I2C排障实战从波形到代码的完整排查链路4.1 排障的基本思路先硬件后软件I2C通信失败的原因可以分成两大类硬件问题和软件问题。硬件问题包括上拉电阻缺失、线路断路/短路、从机供电异常、地址冲突等软件问题包括设备树配置错误、时钟频率不匹配、驱动逻辑bug、时序不满足从机要求等。排查的顺序应该是先确认硬件没问题再查软件。因为硬件问题用软件手段很难定位而软件问题在硬件正常的前提下更容易通过日志和波形分析。4.2 用逻辑分析仪抓波形最直接的诊断手段逻辑分析仪是I2C排障的利器。把探头接到SCL和SDA上设置好触发条件比如SDA下降沿触发就能抓到完整的通信波形。拿到波形后重点看这几个地方起始条件是否正常SCL高时SDA是否有下降沿。从机地址是否正确波形上能直接读出7位地址和读写位。从机是否应答第9个时钟周期SDA是否被拉低。数据是否符合预期跟从机手册对比。停止条件是否正常SCL高时SDA是否有上升沿。如果从机地址发出后没有ACK说明从机没有响应。可能的原因从机地址错了、从机没供电、从机复位引脚没释放、从机坏了。如果起始条件都发不出来说明主机端有问题。检查控制器是否使能、引脚复用是否正确、时钟是否配置。4.3 常见故障速查表故障现象可能原因排查方法SCL/SDA一直高控制器未使能、引脚未复用检查设备树status和pinctrlSDA一直低从机拉死总线、上拉电阻缺失断开从机看SDA是否恢复有起始无ACK地址错误、从机未供电用i2cdetect扫描地址读数据全0或全FF寄存器地址错误、从机未初始化对照手册确认寄存器映射偶发通信失败时钟频率过高、上拉电阻偏大降低频率、减小上拉电阻多设备时冲突地址重复、总线电容过大逐个挂载排查、缩短走线4.4 典型问题一GT911触摸屏I2C通信失败前面提到的GT911案例现象是系统启动后触摸无响应/dev/input/下没有event设备。排查过程第一步确认I2C控制器是否注册成功。查看/sys/bus/i2c/devices/目录下有没有1-005d这样的设备节点。如果没有说明设备树没生效或者驱动没匹配上。第二步用逻辑分析仪抓波形。发现主机发了起始条件和地址0x5D但从机没有ACK。这说明从机没响应。第三步检查GT911的供电和复位时序。GT911要求上电后RESET引脚拉低至少10ms再拉高然后INT引脚的状态决定I2C地址。如果复位时序不对芯片可能没进入正常工作状态。第四步检查I2C地址。GT911的地址由复位时INT引脚的电平决定INT为低时地址是0x5DINT为高时地址是0x14。如果设备树里写的是0x5D但实际硬件上INT被拉高了地址就对不上。最终发现是复位引脚的GPIO配置有问题在设备树中reset-gpios的极性写反了导致芯片一直处于复位状态。4.5 典型问题二SDA被从机拉死导致总线挂起这个问题的现象很有特点系统启动时I2C还能用运行一段时间后突然所有I2C设备都失联了。用逻辑分析仪看SCL正常但SDA一直是低电平。原因是某个从机在通信过程中异常复位或者进入错误状态把SDA拉低不放。因为I2C是线与逻辑只要有一个设备拉低SDA整条总线就都是低电平主机也无法发起新的通信。解决办法有两种第一种是硬件方案在SDA线上加一个总线恢复电路检测到SDA持续低电平时自动发送9个时钟脉冲让从机释放总线。第二种是软件方案在驱动中检测到通信超时后手动切换SCL/SDA为GPIO模式模拟时钟脉冲恢复总线然后重新初始化I2C控制器。OpenHarmony的I2C框架中可以在控制器驱动的transfer函数里加超时检测和恢复逻辑。static int32_t I2cHostTransfer(struct I2cCntlr *cntlr, struct I2cMsg *msgs, int16_t count) { int32_t ret HiI2cTransfer(cntlr, msgs, count); if (ret 0) { // 检测总线状态 if (I2cCheckBusBusy(cntlr)) { I2cBusRecovery(cntlr); ret HiI2cTransfer(cntlr, msgs, count); } } return ret; }4.6 典型问题三时钟频率不匹配导致通信不稳定有些I2C从机对时钟频率有严格要求。比如某些EEPROM在写操作时需要100kHz读操作可以到400kHz某些传感器在上电初始化阶段只支持100kHz初始化完成后才能切到400kHz。如果设备树里统一配了400kHz就可能出现“有时候能读有时候读不到”的现象。排查方法是先用100kHz测试如果100kHz稳定而400kHz不稳定基本可以确定是频率问题。解决方式有两种一是在驱动中动态切换频率初始化阶段用100kHz之后切到400kHz二是直接在设备树中把频率降到从机能稳定工作的值。OpenHarmony的I2C框架支持通过I2cCntlr的setFrequency接口动态调整频率。5. 进阶话题从设备树到驱动的完整链路5.1 设备树中的I2C从机节点如何被驱动匹配Linux内核的设备树匹配机制是这样的I2C从机节点的compatible属性会跟驱动中of_device_id表的compatible字段比对匹配成功后调用驱动的probe函数。在OpenHarmony的HDF框架中匹配逻辑类似但多了一层HCS配置的映射。具体来说设备树中的I2C从机节点会被Linux内核的I2C子系统识别创建一个i2c_client。如果这个设备同时有HDF驱动HDF框架会通过deviceMatchAttr找到对应的HCS配置然后加载HDF驱动。所以一个I2C设备可能同时被Linux原生驱动和HDF驱动管理这时候要注意避免冲突。5.2 设备树调试技巧如何确认配置生效调试设备树最直接的方法是看/proc/device-tree/目录。比如要确认i2c1控制器是否使能cat /proc/device-tree/i2cfe5a0000/status如果输出是“okay”说明使能了。要确认从机设备是否注册ls /sys/bus/i2c/devices/应该能看到类似1-005d的目录其中1是总线号005d是从机地址。还可以用i2cdetect工具扫描总线上的设备i2cdetect -y 1这个命令会扫描I2C总线1上的所有地址显示哪些地址有设备响应。注意这个工具需要内核支持i2c-dev接口并且在OpenHarmony上可能需要自己编译。5.3 驱动中的I2C读写封装在实际驱动开发中直接调用I2cTransfer会比较繁琐通常会封装一层。比如读一个16位寄存器的函数static int32_t ReadReg16(int16_t busNum, uint16_t devAddr, uint16_t regAddr, uint8_t *data, uint16_t len) { struct I2cMsg msgs[2]; uint8_t addrBuf[2]; addrBuf[0] (regAddr 8) 0xFF; addrBuf[1] regAddr 0xFF; msgs[0].addr devAddr; msgs[0].flags 0; msgs[0].len 2; msgs[0].buf addrBuf; msgs[1].addr devAddr; msgs[1].flags I2C_FLAG_READ; msgs[1].len len; msgs[1].buf data; return I2cTransfer(busNum, msgs, 2); }这个封装把“写寄存器地址读数据”的复合操作简化成一次调用驱动代码里用起来会清爽很多。5.4 中断与轮询的选择I2C设备的数据就绪通知通常有两种方式中断和轮询。中断方式下从机通过INT引脚通知主机数据就绪主机再去读轮询方式下主机定期去读从机的状态寄存器。中断方式的优点是实时性好、CPU占用低缺点是需要额外的GPIO引脚而且中断处理中不能做太耗时的I2C操作。轮询方式的优点是简单、不需要额外引脚缺点是实时性差、浪费CPU。在OpenHarmony中如果从机支持中断建议用中断方式。中断处理函数中不要直接做I2C读写而是发一个信号量或者消息给工作队列在工作队列中完成实际的I2C操作。这样避免在中断上下文中睡眠导致系统异常。6. 几个容易忽略的细节和实操心得6.1 上拉电阻不是随便选的很多人画板子的时候直接抄参考设计4.7kΩ上拉电阻一放就完事。但实际上拉电阻的值跟总线速率和总线电容直接相关。总线电容包括PCB走线电容、引脚电容和器件电容一般每根线在20pF到100pF之间。上升时间跟RC时间常数的关系是tr ≈ 0.847 × R × C。对于400kHz快速模式上升时间要求小于300ns。如果总线电容是100pF那么R最大是300ns / (0.847 × 100pF) ≈ 3.5kΩ。所以4.7kΩ在400kHz下可能偏大2.2kΩ更稳妥。6.2 设备树中的地址是7位不是8位I2C从机地址有7位和10位两种格式。7位地址在传输时会左移一位最低位补读写位凑成8位。设备树中的reg属性填的是7位地址不是左移后的8位值。比如GT911的地址是0x5D设备树里就写reg 0x5d不要写成0xBA。6.3 逻辑分析仪的采样率要够高用逻辑分析仪抓I2C波形时采样率至少要是总线频率的10倍以上。400kHz的I2C采样率建议在10MHz以上。采样率不够的话波形会失真可能把正常的ACK看成NACK。6.4 多个I2C设备共用中断引脚时的处理有些板子为了省GPIO把多个I2C设备的中断引脚接到同一个GPIO上。这时候中断处理函数需要遍历所有可能的中断源逐个读取状态寄存器确认是哪个设备触发了中断。这种设计会增加中断处理的复杂度但确实能省引脚。6.5 总线恢复的软件实现细节前面提到的总线恢复逻辑具体实现是这样的static void I2cBusRecovery(int16_t busNum) { // 1. 关闭I2C控制器 I2cClose(busNum); // 2. 将SCL和SDA切换为GPIO模式 GpioSetDir(SCL_PIN, GPIO_DIR_OUT); GpioSetDir(SDA_PIN, GPIO_DIR_OUT); // 3. 发送9个时钟脉冲 for (int i 0; i 9; i) { GpioWrite(SCL_PIN, 0); OsalUDelay(5); GpioWrite(SCL_PIN, 1); OsalUDelay(5); } // 4. 发送停止条件 GpioWrite(SDA_PIN, 0); OsalUDelay(5); GpioWrite(SCL_PIN, 1); OsalUDelay(5); GpioWrite(SDA_PIN, 1); OsalUDelay(5); // 5. 恢复I2C控制器 I2cOpen(busNum); }这段代码的核心思想是通过手动发送时钟脉冲让从机完成当前字节的传输并释放SDA然后发一个停止条件让总线回到空闲状态。6.6 调试I2C时先用低速再逐步提速这是我踩过多次坑之后总结的经验新板子第一次调I2C先把频率设到100kHz甚至更低比如50kHz确认通信正常后再逐步提高到目标频率。这样可以把频率相关的问题和其他问题分开避免一开始就被复杂的故障现象迷惑。6.7 注意I2C从机的上电时序很多I2C从机对电源和信号的时序有要求。比如某些传感器要求电源稳定后等待10ms才能通信某些EEPROM要求写操作后等待5ms才能读。如果驱动中没加这些延时就会出现“第一次读失败、第二次读成功”的奇怪现象。7. 关于I2C排障我个人的几条硬核建议搞I2C调试这些年我最大的体会是不要一上来就怀疑代码。I2C的问题十有六七出在硬件或者配置上代码逻辑本身出问题的概率反而小。所以排查顺序永远是先看供电和引脚、再看设备树配置、然后抓波形、最后才查驱动代码。另外逻辑分析仪的钱不能省。几百块的分析仪能帮你省下几十个小时的瞎猜时间。没有分析仪的时候用示波器看个大概也行但看不到协议层的细节。如果连示波器都没有至少用万用表量一下SCL和SDA的静态电平确认上拉电阻有没有起作用。还有一点养成记录波形和日志的习惯。每次调试成功后把正常的波形截图和关键日志保存下来。下次遇到类似问题拿出来对比一下很快就能定位差异。这个习惯帮我省了无数次重复排查的时间。最后说一个容易被忽略的点I2C总线的走线长度。I2C不是为长距离通信设计的走线一般不要超过30cm而且SCL和SDA要尽量靠近走必要时包地处理。如果确实需要长距离考虑用I2C缓冲器或者转成其他总线。
返回列表