ARTICLE DETAIL

资讯详情

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

OpenHarmony下I2C总线调试:协议、驱动与排障经验总结

OpenHarmony下I2C总线调试:协议、驱动与排障经验总结 做OpenHarmony开发绕不开I2C。作为接入外设最频繁的总线之一它负责连接传感器、触摸屏、EEPROM、PMIC这类低速设备在硬件形态上只有两根线一根时钟线SCL一根数据线SDA。物理结构足够简单但出问题的时候却往往很让人头疼——电平看着正常、设备地址没写错、代码逻辑也对读回来的数据就是不对。这篇文章把我调I2C这几年踩过的坑、用过的排查手段一次性整理出来讲清楚三件事协议本身是怎么运转的在OpenHarmony上怎么正确地把I2C用起来出了问题怎么一步步定位根因。无论你是刚接触总线的新手还是已经在驱动开发里挣扎了一段时间的工程师按这个思路去调能省掉大量反复试错的时间。1. 为什么说I2C是OpenHarmony设备开发里躲不开的必修课1.1 两根线如何挂起几十个设备I2C在同一时刻只需要两根线却可以挂接多个设备靠的是“地址机制”。可以把它理解成一个小区里的快递柜每个人有一个房间号投递员只要知道房间号就能把快递送到对应柜子。I2C总线上每个从机设备都有自己独立的7位地址部分设备支持10位地址主机发起通信时先发送地址帧总线上所有从机都会收到这个帧但只有地址匹配的那个设备才会应答。一个地址对应一个设备挂几十个设备从硬件连接角度看也就多几根支路而已。这在OpenHarmony的硬件设计上非常实际。很多开发板引脚是稀缺资源如果每个传感器都要单独占用好几根引脚根本不够用。I2C用极少的引脚换取了足意的设备接入能力所以传感器、触摸芯片、存储芯片、电源管理芯片在设计上普遍优先提供I2C接口。这也是为什么OpenHarmony适配一块新板子时驱动工程师最先要打通的往往就是I2C这一层。1.2 OpenHarmony设备开发里哪些地方藏着I2C平时做OpenHarmony设备开发I2C出现的场景比想象中多。最常见的是环境传感器温湿度、气压、光照、气体检测大部分都走I2C。触摸屏控制芯片也是典型I2C设备调试时经常能看到GT911、FT5x06这类IC挂在I2C总线上。还有系统基础功能比如读取EEPROM做参数存储访问PMIC的寄存器控制IO扩展器甚至部分显示屏的初始化序列也要通过I2C下发。OpenHarmony的系统服务里还包含一些I2C外设相关的框架层比如传感器服务、输入服务底层最终都会落到I2C控制器驱动上。在“万物智能”的背景下设备形态五花八门但底层通信的手段其实非常固定。I2C作为最通用的低速板级总线无论是小开发板还是商用设备都是一道绕不开的技术门槛。把这个总线搞明白了再去看SPI、UART、CAN这些总线会发现学习路径其实是相通的——适配电气特性、弄清时序、再处理驱动和排障。2. I2C协议核心机制总线上到底发生了什么事2.1 起始、停止、字节收发和ACK——这套时序怎么读先说最基础的一层I2C的时序规则。平时看芯片手册里的时序图第一眼觉得密密麻麻实际拆开就那么几个动作。空闲状态下SCL和SDA都处于高电平。主机要开始通信时先让SDA从高变低此时SCL还是高电平这个“SCL高电平期间SDA的下降沿”就是起始条件。对应的停止条件是SCL高电平期间SDA的上升沿。时序图里这两个动作非常显眼逻辑分析仪抓包时先找这两个跳变即可。数据字节的传输规则是SDA上的数据必须在SCL高电平期间保持稳定只能在SCL低电平时切换。也就是说从机采样的时刻是SCL上升沿附近。发送一个字节主机从最高位MSB开始逐位输出总共8个时钟脉冲第9个时钟脉冲用于应答位。应答位上发送方释放SDA线接收方如果正常接收会主动把SDA拉低主机在SCL高电平期间采样到这个低电平就是ACK。如果接收方没有拉低SDA保持高电平就是NACK说明设备不认这个地址或者不喜欢这次通信。地址字节本身也是8位由7位设备地址左移一位后拼上读写标志位得到。读写标志为0表示主机接下来要写数据为1表示要读数据。很多新手栽在地址这里芯片手册里给一个裸地址0x38写地址却是0x70、读地址是0x71就是左移一位加上读写位的结果。这个细节后面排障时特别重要。2.2 速率、仲裁和时钟拉伸为什么有时候要降速I2C有标准模式100kbps、快速模式400kbps、快速模式1Mbps、高速模式3.4Mbps。实际开发中100k和400k用得最多。总线上所有设备必须互相兼容速率如果有一个从机最高只能跑100k那总线整体就只能降速到100k强行跑400k就会出现数据错乱。仲裁机制是I2C一个很有特色的地方。多个主机同时发起通信时谁先发现总线上已经有别的设备把SDA拉低谁就自动退出。道理很简单I2C是线与逻辑谁能把信号拉得更低谁占优在SDA上发送1的主机如果检测到总线上实际是0就知道有人也在发数据应退出发送。这个机制在设计上保证了多主场景下不会出现数据冲突。时钟拉伸则是从机控制总线节奏的手段。从机处理不过来时会把SCL拉低让主机暂停发送等从机准备好再释放SCL主机检测到SCL释放后才能继续。硬件I2C控制器通常会自动处理时钟拉伸但GPIO软件模拟I2C时必须自己轮询SCL电平否则在高速传输时会丢失数据。还有一个容易被忽略的点上拉电阻决定总线的可靠程度。I2C是开漏结构总线高电平完全靠上拉电阻提供。上拉电阻太大上升沿变缓太小驱动能力又不够。同一条总线上挂的设备越多寄生电容越大上拉电阻越需要选小一点。板级设计通常用4.7k欧姆如果总线上设备很多2.2k甚至1k也常见。排障时如果波形上升沿明显变缓、数据不稳定先怀疑上拉电阻大小。2.3 I2C与SPI、UART、CAN怎么选型搞嵌入式的人都知道总线选型本质是性价比选择。把常见总线摆在一起对比一下思路会更清晰。总线线数速率范围多设备能力典型场景I2C2SCLSDA100k~3.4M多从机靠地址区分传感器、EEPROM、PMIC、触摸屏SPI3NMOSIMISOSCK片选可达数十M多从机靠片选区分屏幕、Flash、ADC、SD卡UART2TXRX通常115200或更低点对点调试口、GPS模块、蓝牙模块CAN2CANHCANL可达1M以上多节点靠ID仲裁车载、工控、分布式节点选型建议比较直接低速、多设备、引脚不够用的场景优先I2C需要高吞吐量的选SPI设备形态是点对点且协议简单的选UART需要长距离、多节点、抗干扰场景选CAN。I2C的优势不在速度而在它的“一总线多设备”能力。3. 在OpenHarmony里把I2C用起来3.1 OpenHarmony的I2C驱动栈跟Linux不完全一样OpenHarmony设备端的驱动框架叫HDFHardware Driver Foundation跟Linux kernel里的设备驱动模型不同。I2C控制器驱动在HDF里也是以节点方式组织但配置文件的格式是HCS类似设备树又不是设备树节点命名、属性字段、引脚复用方式都和Linux的DTS不一样。刚开始适配时很容易拿着Linux的思维去找设备树结果发现路径不对。在板级配置中I2C控制器通常有专门的节点通过配置选择控制器基地址、时钟、速率和引脚复用。一个常见的坑是HCS里I2C节点已经配置了但SCL和SDA引脚的复用功能没有打开或者被其他功能抢占导致总线上完全没有波形。引脚复用是板级开发里最容易出问题的环节排查I2C问题时要把它放在最前面。HDF框架下I2C提供了比较稳定的对外接口结构不算复杂。这里要说个调经验的感受与其在框架层反复纠缠不如先看懂驱动模型的调用关系实际操作中大部分问题集中在设备地址、速率匹配和引脚复用上很少真的卡在HDF本身。3.2 核心API与一个完整的读写流程OpenHarmony给开发者提供的I2C接口比较简洁核心就三件套I2cOpen(controllerId)打开指定编号的I2C控制器返回设备句柄I2cTransfer(handle, msgs, count)执行一次批量消息传输I2cClose(handle)关闭控制器其中最关键的是I2cTransfer它接收一个消息数组每个消息是一个I2cMsg结构体包含从机地址、读写标志、缓冲区、长度。一次可以把“先写寄存器地址、再读数据”这两步组合成一个批量操作协议上可以理解为连续传输而不被拆分这对很多传感器芯片是必须的。一个典型的读取传感器寄存器的代码骨架是这样的#include device/io/i2c_if.h #define SENSOR_ADDR 0x38 // 裸地址 #define I2C_BUS 0 // 控制器编号按板级配置 int sensor_read_reg(uint8_t reg, uint8_t *buf, uint32_t len) { DevHandle handle NULL; struct I2cMsg msgs[2]; uint8_t reg_buf reg; handle I2cOpen(I2C_BUS); if (handle NULL) { printf(I2cOpen failed\n); return -1; } /* 第一条消息写寄存器地址 */ msgs[0].addr (SENSOR_ADDR 1) | 0; msgs[0].flags 0; msgs[0].buf reg_buf; msgs[0].len 1; /* 第二条消息读数据 */ msgs[1].addr (SENSOR_ADDR 1) | 1; msgs[1].flags I2C_FLAG_READ; msgs[1].buf buf; msgs[1].len len; if (I2cTransfer(handle, msgs, 2) ! 2) { printf(I2cTransfer failed\n); I2cClose(handle); return -1; } I2cClose(handle); return 0; }I2cMsg里的addr字段注意一下它需要的是包含了读写位的完整8位地址。有的SDK实现里会自动左移有的不会究竟哪种得看具体版本的头文件注释。我建议初始化时统一用“裸地址左移一位再或读写标志”这种写法不容易踩坑。3.3 控制器不够用GPIO模拟I2C怎么保底做原型开发时经常遇到板卡上I2C控制器不够用或者引脚被某种高优先级功能占用的情况。这时可以用GPIO模拟I2C也就是软件bit-banging。做法很直接把任意两个GPIO配成输出开漏模式外部加上拉电阻用软件按照I2C时序拉高拉低。这里必须强调一个前提GPIO一定要开漏输出。如果配成推挽输出就不符合I2C的线与逻辑在起始条件阶段可能出现总线冲突。软件模拟I2C的核心就是自己实现起始条件、停止条件、字节收发和ACK检测。代码如下static void i2c_delay(void) { udelay(5); } /* 100k速率对应的延时 */ static void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); i2c_delay(); SDA_LOW(); /* SCL高期间SDA拉低 START */ i2c_delay(); SCL_LOW(); i2c_delay(); } static void i2c_stop(void) { SCL_LOW(); i2c_delay(); SDA_LOW(); i2c_delay(); SCL_HIGH(); /* SCL高期间SDA拉高 STOP */ i2c_delay(); SDA_HIGH(); i2c_delay(); } static int i2c_write_byte(uint8_t data) { int i; uint8_t bit; for (i 7; i 0; i--) { bit (data i) 0x01; if (bit) SDA_HIGH(); else SDA_LOW(); i2c_delay(); SCL_HIGH(); /* SCL高时SDA电平被采样 */ i2c_delay(); SCL_LOW(); i2c_delay(); } /* 第9个时钟读ACK */ SDA_INPUT(); /* 释放SDA */ SCL_HIGH(); i2c_delay(); if (SDA_READ() ! 0) { return -1; /* NACK */ } SCL_LOW(); i2c_delay(); return 0; }软件模拟的劣势是CPU占用率高且无法硬件自动处理时钟拉伸。但作为应急方案它可以让你绕过控制器资源不足的问题而且排障时还能临时改时序判断是不是硬件控制器时序跟外设不兼容。有一点体会模拟I2C的时候延时参数宁长勿短先跑通再压缩速度不要开局就追求400k。4. 实战OpenHarmony读取AHT20温湿度传感器4.1 接线和上电前的检查清单纸上谈兵再多不如完整跑一遍。这里用AHT20温湿度传感器作为示例它的I2C地址是0x38通信协议不复杂适合用来演示完整链路。接线前先确认几件事SCL、SDA是否有上拉电阻。有些模块板上自带4.7k上拉电阻有些则需要外接。如果没有上拉电阻SDA和SCL永远只能被拉低拉不高表现为设备从不响应或读回全0。检查上电时序AHT20上电后需要至少100ms稳定时间期间不要发I2C命令否则设备可能进入异常状态。引脚绑定确认板子上哪个控制器对应哪两个引脚HCS里是否已经配好复用功能这个没确认之前不要急着写代码。连接关系很简单模块的SCL接开发板I2C控制器的SCL引脚SDA接SDA引脚VCC接3.3VGND共地。OpenHarmony系统起来之后可以先用逻辑分析仪或者示波器量一下引脚上有没有上拉电平通常SCL、SDA在空闲时都应为3.3V左右。4.2 代码从读寄存器到拿原始数据AHT20的用法比较特殊它不是通过传统寄存器方式读取数据而是先发送触发测量命令等待测量完成后再一次性读取6个字节。完整流程如下初始化发送0xBE 0x08 0x00触发测量发送0xAC 0x33 0x00等待至少80ms或轮询状态字节的bit3忙标志读取6字节数据用位运算拆出湿度和温度原始值这里直接在OpenHarmony上用I2C接口实现写了完整的注释#include device/io/i2c_if.h #include stdio.h #include unistd.h #define AHT20_ADDR 0x38 #define I2C_BUS 0 static int aht20_write_cmd(uint8_t *cmd, uint32_t len) { DevHandle handle I2cOpen(I2C_BUS); struct I2cMsg msg; msg.addr (AHT20_ADDR 1) | 0; msg.flags 0; msg.buf cmd; msg.len len; if (I2cTransfer(handle, msg, 1) ! 1) { I2cClose(handle); return -1; } I2cClose(handle); return 0; } static int aht20_read_data(uint8_t *buf, uint32_t len) { DevHandle handle I2cOpen(I2C_BUS); struct I2cMsg msg; msg.addr (AHT20_ADDR 1) | 1; msg.flags I2C_FLAG_READ; msg.buf buf; msg.len len; if (I2cTransfer(handle, msg, 1) ! 1) { I2cClose(handle); return -1; } I2cClose(handle); return 0; } int aht20_init(void) { uint8_t init_cmd[3] {0xBE, 0x08, 0x00}; return aht20_write_cmd(init_cmd, sizeof(init_cmd)); } int aht20_read(float *humidity, float *temperature) { uint8_t cmd[3] {0xAC, 0x33, 0x00}; uint8_t data[6] {0}; uint32_t hum_raw, temp_raw; if (aht20_write_cmd(cmd, sizeof(cmd)) ! 0) { return -1; } usleep(100 * 1000); /* 等待测量完成至少80ms */ if (aht20_read_data(data, sizeof(data)) ! 0) { return -1; } /* 验证状态字节bit0是否为0如果为1说明传感器忙 */ if (data[0] 0x80) { return -1; } /* 湿度占20bitdata[1]全部 data[2]高4bit data[3]低4bit具体拼接见下 */ hum_raw (data[1] 12) | (data[2] 4) | (data[3] 4); temp_raw ((data[3] 0x0F) 16) | (data[4] 8) | data[5]; *humidity (float)hum_raw / 1048576.0f * 100.0f; *temperature (float)temp_raw / 1048576.0f * 200.0f - 50.0f; return 0; }AHT20的原始数据要重点解释一下。读回来的6个字节里第一个字节是状态字节bit7如果为1表示芯片正忙。后面5个字节按位组合成两个20位的数据湿度是前20位温度是后20位。上面的拼接公式一旦写错一位读出来的数值就会非常离谱。我在实际调试时经常踩到这种位错位问题最简单的方法就是把原始6字节打印出来人工拼一下再用已知温湿度环境对照一下是否合理。4.3 原始数据怎么解析怎么验证拿到原始6字节后经常有人困惑为什么公式是这样拆的。本质上很简单AHT20把湿度数据和温度数据按二进制位连续排列没有对齐全字节边界。也就是说湿度不是刚好占第1、第2字节而是跨了3个字节。数据手册给出的寄存器布局是湿度20bit从data[1]开始占data[1]全部8位、data[2]的高4位、然后跨到data[3]的高半字节温度20bit接在湿度后面占data[3]的低4位、data[4]全部8位、data[5]全部8位我用代码里的拼接方式统一处理湿度取data[1]左移12位、data[2]左移4位、data[3]右移4位温度则取data[3]低4位左移16位、data[4]左移8位、data[5]。这样算出来后再除以2的20次方再乘以量程得到实际物理值。验证方法很直接用手捏住传感器温度数值应该在几秒内明显上升对传感器哈气湿度数值应快速增加。如果数值纹丝不动或者跳变得离谱多半是位运算拼接错了回来检查位移和掩码。5. I2C排障实战把那些“玄学”问题逐个拆开5.1 总线挂死SDA一直被拉低怎么救排障里最常见、也最让人抓狂的是“SDA被拉死”。现象是不管发什么地址总线都不响应用万用表量SDA引脚对地电压发现一直是低电平SCL倒是正常。这种情况通常是某个从机设备进入了非正常状态把SDA钳位住了。I2C标准里规定SCL高电平期间SDA应该允许被释放但如果某个从机内部状态机错乱它可能在错误时刻拉住了SDA导致整个总线瘫痪。急救方法我实测有效的是“SCL脉冲解锁法”在SCL上连续给出9个以上的时钟脉冲同时让SDA保持高电平这样从机状态机能接收到完整的字节时钟从锁定状态中走出来。实现上可以写个循环拉高拉低SCL不用管SDA上的电平。很多情况下9个脉冲就够如果还不够可以多给一些同时考虑掉电重启。根治的方向还得查根因。SDA被拉死往往是设备上电时序、地址冲突或者供电异常导致的。比如有两个设备配置成了相同地址同时应答就会互相干扰。又比如某个传感器芯片VDD比总线先上电芯片还没初始化完成就收到了I2C帧容易卡死。排查时要先确认每个设备的供电和复位时序别再盲目多发命令。5.2 设备不应答地址、上拉、时序三板斧I2C通信失败的另一半场景是“有波形但是收不到ACK”。收到NACK说明总线上没有设备认领这个地址按三个方向排查基本能覆盖所有情况。第一是地址算错。把7位裸地址0x38直接填进I2cMsg.addr而SDK又不会自动左移实际发送的地址字节就成了0x38但芯片期望的是0x70。反过来也一样给的是0x70SDK又左移了一次发送成0xE0就更对不上了。我的建议是每次拿到新芯片先看手册里地址字节的完整写法再对照代码里I2cMsg.addr的类型定义不要凭感觉。第二是上拉电阻或者接线问题。用万用表量总线空闲电平如果SCL高电平远低于供电电压比如只有1V左右说明上拉电阻太小或者根本没有上拉电阻从机没法把SDA拉高到被识别为高电平的程度。这时给SCL和SDA各加一个4.7k欧姆到VCC的上拉电阻重新测一次。第三是设备没从复位状态出来。很多传感器芯片有多种地址选择引脚需要外部拉高拉低来选地址引脚悬空时默认地址可能不是你预期的那一个。确认芯片的地址引脚接法再对照手册确认实际地址。还有一个容易忽略的是部分设备需要初始化命令之后才进入正常模式比如AHT20冷启动后要先发初始化指令才能响应后续读取。5.3 有波形但读不到数据速率失配和组合读写问题排障到后面会出现一个更隐蔽的问题波形逻辑上看起来对时钟也有地址也有但读回来的数据完全是乱的。这类问题大概率是速率失配以及未正确处理“写读”的组合消息。速率失配的典型表现是在100k下一切正常一旦改成400k就出乱码。原因是总线上的上拉电阻、寄生电容、从机最高速率共同决定了实际可跑的速率。上拉电阻偏大时400k下上升沿很缓在从机采样时刻数据还没有稳定到正确电平。解决思路有两个把总线速率降回100k或者减小上拉电阻值。排查时可以用逻辑分析仪抓取时间参数对比协议规范看SCL高电平时间是否满足要求。组合读写的问题则出在“设备需要先写寄存器地址再读数据”的场景。如果代码里把写寄存器地址和读数据分成了两个独立的I2cTransfer调用中间总线会插入一个停止条件再重新发起起始条件。很多传感器芯片在这种场景下不会保持内部寄存器指针状态导致第二次读操作读出来的并不是你想读的那个寄存器。解决办法是像前面代码里那样用一个I2cMsg数组把写和读组合成一次批量传输让硬件在一个事务里完成重复起始条件。5.4 排障利器逻辑分析仪、示波器和扫描脚本代码层面的问题看日志能定位总线层面问题还是得靠物理抓信号。我的习惯是手边常备一个USB逻辑分析仪十几块钱那种就够用配合开源的sigrok/PulseView软件把SCL、SDA、GND三根线接上采样率设到4M以上抓取起始条件附近的数据。抓下来之后最直观的效果是能直接看到地址字节、数据字节、ACK位是否都符合协议规范。我排障时几乎每次都能靠抓到的波形把问题锁定到具体一个时钟周期上。示波器在信号完整性排查上更有用。看波形上升沿是不是太缓、有没有明显过冲、SDA低电平能不能落到0.3V以下。逻辑分析仪看协议对错示波器看信号质量两者互补。如果手头没有仪器还有一个软件手段写一个I2C地址扫描小程序循环发送所有可能的7位地址检测哪些地址有ACK响应。这个思路类似Linux的i2cdetect工具在OpenHarmony下如果系统不带这个命令代码自己写一个也很简单不过几十行的量。运行一圈就能知道总线上实际有哪些设备在响应排查地址冲突非常高效。5.5 常见问题速查表问题现象大概率原因排查方法与处理手段总线上完全没有波形引脚复用没配置检查HCS板级配置确认SCL/SDA已切到I2C模式SDA始终为低总线挂死从机异常状态机锁死或地址冲突用9个以上SCL脉冲解锁排查供电时序和地址冲突发地址收不到ACK地址字节算错、设备未上电、上拉缺失按手册重新核对地址量空闲电平和供电100k正常400k乱码上拉电阻偏大、总线电容过大降到100k验证或换2.2k欧姆上拉单独写没问题连续读写读到错误数据组合读写被拆分成了两个事务改用一次I2cTransfer传两个消息的写法读回全是0xFFSDA没有上拉、设备未响应检查上拉电阻和设备供电读回全是0x00地址匹配了但设备没准备好检查复位、初始化序列、等待时间这张表看起来是七种情况实际对应两大根源物理层电平不对协议层地址时序不对。排障时建议先把物理层扫清楚再抓协议最后才是怀疑代码逻辑。顺序反过来效率会低很多。5.6 热词里提到的几个I2C相关场景也顺便聊聊在收集资料时看到几个高频搜索词比如“I2C自由数据模式”“GT911 I2C通信失败”“DS18B20挂总线”“I2C从机主动更新主机寄存器”都是实际项目里很具体的问题完全可以当作后续排障的补充方向简单交代一下。“I2C自由数据模式”是某些传感器芯片在I2C之外提供的一种简化数据流模式主要用于不需要标准命令结构的场景实际应用中较少遇到但个别光照传感器和触摸屏会用。遇到这类芯片时手册里如果没有明确的寄存器定义就不要再按普通I2C去猜回到芯片厂家的驱动Demo里找答案。“GT911 I2C通信失败”这个高频问题核心往往不在I2C时序本身而在触摸屏芯片的中断引脚和复位时序。GT911这类芯片上电后需要正确配置复位和中断引脚否则I2C地址和通信状态都会异常。调试这类设备建议先检查复位引脚是否保持正确电平中断引脚是否被错误地上拉或下拉。“DS18B20挂总线”是一个经典场景不过DS18B20本身走的是单总线协议不是I2C。所谓“挂总线”通常是指用IO模拟方式把单总线器件接到系统里。这里要注意的是单总线的开漏结构和时序要求比I2C更严格必须使用开漏输出加外部上拉电阻上拉电阻一般4.7k即可采集时序强烈依赖延时精度建议关闭中断或者用定时器补偿否则时间参数漂移会导致温度数据跳变。“I2C从机主动更新主机寄存器”这个需求在标准I2C协议里从机无法主动发起通信只能通过中断引脚通知主机来读。很多人想实现“从机推数据”效果本质是做一个“从机状态变化后主机主动轮询”的机制。硬件上给从机接一个中断引脚数据变化时拉低中断脚主机检测到中断后立刻发起I2C读操作这样就能实现近乎实时的数据更新这是标准做法不要试图让从机在总线上主动说话。6. 排障之外我调I2C攒下的几条土办法6.1 我排障时固定会问自己的三个问题调I2C时间长了我慢慢形成一套固定的自查逻辑。无论问题多玄学先问自己三个问题总线电平对不对地址字节对不对消息组合对不对三个问题依次过一遍十有八九能找到根因。电平对不对是用万用表量SCL和SDA空闲电压再抓一下波形确认有没有正常翻转。地址对不对是把手册上的地址字节完整格式写出来跟代码里的实际发送值对比。消息组合对不对是确认“先写寄存器地址再读写数据”这类操作有没有被拆开成两个事务。这个自查顺序帮我省了很多时间如果你也经常被I2C折腾不妨试试。长线排查还有一个心得调I2C千万不要一次性改太多变量。把速率降下来、把上拉电阻换掉、把代码逻辑改掉三个动作同时做完如果问题解决了你根本不知道是哪个动作起效了。正确的做法是一次只动一个变量每动一次都重新验证一遍。排障最怕的就是改了一堆之后问题“莫名其妙”好了这种好印象维持不了太久换个板子又复发。6.2 分享一个稳定好用的“PS总线复位小函数”最后分享一个我平时用于应急的总线复位小函数。很多传感器在异常情况下需要总线级别的复位直接断电重启往往不满足快速恢复的需求。我的做法是写一个函数把SCL和SDA两个引脚先配置为GPIO输出高然后连续拉9个时钟周期最后发一个停止条件。核心代码如下void i2c_bus_reset(void) { int i; /* 把SCL和SDA都设为输出高 */ pin_set_mode(I2C_SCL_PIN, PIN_OUTPUT); pin_set_mode(I2C_SDA_PIN, PIN_OUTPUT); pin_write(I2C_SCL_PIN, 1); pin_write(I2C_SDA_PIN, 1); udelay(5); for (i 0; i 9; i) { pin_write(I2C_SCL_PIN, 0); udelay(5); pin_write(I2C_SCL_PIN, 1); udelay(5); } /* 发停止条件SCL高时SDA拉高 */ pin_write(I2C_SDA_PIN, 0); udelay(5); pin_write(I2C_SCL_PIN, 1); udelay(5); pin_write(I2C_SDA_PIN, 1); udelay(5); }这个函数在传感器卡死、总线占用时非常好用把它放在系统启动早期甚至可以作为预防性措施执行一次。注意它依赖GPIO引脚有几个前提引脚复用支持GPIO模式且驱动初始化的接口能正常切换。如果HCS里已经把引脚锁死在I2C功能上那就要先把复用模式改回GPIO再执行复位操作完之后再切回I2C模式。这个函数内部逻辑不算复杂核心价值在于帮你培养一个“总线被卡住时先复位再查因”的习惯比直接热复位整个系统要温和得多也不会打断正在运行的其他设备事务。到了这一步I2C基本已经跑通了。剩下那些读错数、时序抖动、设备间相互干扰的问题本质上都可以回归到电平、时序、地址、组合消息这四件事上去。总线的底层原理不会变变的是每个芯片手册里细微的地址和寄存器定义。吃透一次通用逻辑后面换任何I2C外设都是拿这套方法去匹配新手册而已。
返回列表