尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

PIC18F16Q41硬件I2C读VL53L1X ID寄存器返回0x00的排查指南

PIC18F16Q41硬件I2C读VL53L1X ID寄存器返回0x00的排查指南 把VL53L1X接到PIC18F16Q41的硬件I2C上我做的第一件事就是读0x010F这个ID寄存器代码逻辑很简单发地址、发寄存器号、重启动、读数据。我心里预期很清楚这颗激光测距芯片的ID就得是0xEA结果串口打印出来的是0x00。这个结果让我在调试器前面坐了很久。0x00不是0xFF也不是总线卡死后的超时错误它看起来就像“传感器稳稳当当地回了一个0”但VL53L1X的ID寄存器明明应该返回0xEA。坏消息是这个问题有好几种可能的原因好消息是它们都能通过一步步排查来定位。这篇文章就把我这次完整排错链路写出来包括硬件接线、MCC生成代码、I2C时序细节以及最终怎么确认问题出在哪一环。如果你也在PIC18F16Q41这类新内核单片机上用硬件I2C驱动VL53L1X或者遇到类似“读寄存器总是0x00”的情况这篇能帮你省下好几个晚上的调试时间。1. 0xEA变成了0x00这个失败信号到底暴露了什么1.1 VL53L1X的ID寄存器与正确启动流程先确认目标。VL53L1X的IDENTIFICATION_MODEL_ID寄存器的地址是0x010F上电且boot完成之后读它正常的返回值就是0xEA。这个值是ST固化在芯片里的不会因为供电电压、环境光线或者测量状态改变它是验证I2C物理链路和寄存器读取时序是否正确的“金标准”。问题来了。0x010F是个16位寄存器地址很多从Arduino转过来的朋友可能没意识到自己以前用Wire库时repeated start和16位地址都是库帮忙处理的换成PIC的MCC硬件I2C后这些动作拆成了一个个底层API少调一个函数时序就不完整。我这次就是在MCC生成的驱动基础上手写读取流程结果第一步就卡住了。VL53L1X上电后并不是立刻就能访问I2C。芯片内部需要完成boot典型时间在1.2毫秒左右但如果你用XSHUT引脚控制芯片的复位那么XSHUT从低拉高之后同样需要等待这段时间。这个时间窗口内去读任何寄存器芯片都处于“我还没准备好”的状态总线上要么没应答要么应答了但内部寄存器全是默认的0。很多0x00就是这么来的不是I2C配置错了是芯片还没醒来。1.2 为什么是“0x00”而不是“0xFF”或总线卡死这是整件事最迷惑的地方。如果总线上一片安静从机完全没有应答读回来的数据通常是0xFF因为SDA线被上拉电阻拉高释放状态下读到的就是全1。我在其他项目里遇到过目标设备没供电的情况读出来就是0xFF。但VL53L1X返回0x00这释放了一个关键信号SDA线上有电平变化而且最终被拉低了。要么是从机确实在用低电平回传0要么是总线上的某些条件让主机在读取时误判了电平。顺着这个思路排查0x00的来源基本可以分成三类传感器确实正常应答但寄存器地址写错了读到的是某个值为0的地址。传感器没有应答代码又没检查ACK状态硬件外设的接收缓冲器是空的MCC驱动返回了一个默认的0。SDA被外部因素拉死比如电平不匹配导致总线锁住读到的数据恒为0。这三种可能分别对应软件时序、驱动配置、硬件设计三类问题后面我会逐个拆开看。1.3 PIC18F16Q41与常见Arduino环境的差别问题的温床PIC18F16Q41是Microchip新一代内核的单片机硬件I2C外设和传统PIC18系列上的MSSP模块已经不一样了。老的MSSP操作模式是设置SEN、RSEN、PCON这些位来触发start、repeated start、stop而新的I2C外设使用独立的CON0/CON1/CON2寄存器组配合MCC自动生成的API函数来调度收发状态。寄存器地址、中断标志、错误标志的位置全都变了。这个变化本身是好事它比老MSSP更接近现代I2C控制器的思路但代价是网上的历史资料和经验帖几乎都是针对老MSSP的照搬过来根本对不上号。MCC生成的新驱动里I2C1_WriteByte、I2C1_ReadByte这类函数都需要配合其它状态查询API使用很多坑就藏在这里你调用了函数但没检查上一步是否真正完成硬件就已经开始执行下一步了。另外PIC18F16Q41的I2C引脚是通过PPS外设引脚选择映射的不是硬件固定的。在MCC里如果只启用了I2C模块却没有把具体的SDA/SCL功能映射到目标引脚上外设等于在“盲操作”——引脚没有输出I2C波形当然读不到数据。这个和单片机型号强相关也是很多人换了芯片后I2C突然不工作的典型原因。2. 排查第一关先分清是“收不到应答”还是“读到了假数据”2.1 用最小化代码快速验证从机是否在线拿到0x00之后不要马上钻进寄存器时序里先用一段最小化代码验证VL53L1X到底在不在总线上地址对不对。VL53L1X的7位I2C地址是0x29转换成8位写地址是0x52读地址是0x53。很多第一次接触芯片的朋友查datasheet看到0x52头也不回就把这个值当7位地址用结果从机地址压根匹配不上。我建议写一个最简单的地址扫描函数for (uint8_t addr 0x08; addr 0x78; addr) { I2C1_Start(); I2C1_WriteByte((addr 1) | 0x00); while (!I2C1_IsDone()); if (I2C1_IsAckReceived()) { // addr对应从机在线 } I2C1_Stop(); }注意MCC各版本API名称略有差异有的用I2C1_IsAckNotReceived有的用I2C1_AckStatus具体看生成的i2c1.h头文件。关键点是显式检查ACK标志位而不是只看I2C1_WriteByte有没有返回。如果地址扫描能找到0x29这个从机说明物理连接和基础地址都是对的问题出在寄存器读取时序上。如果扫描了0x08到0x77全都没有应答那就要回头检查硬件VL53L1X有没有供电、XSHUT是不是被拉低了、SDA和SCL有没有接上拉电阻。2.2 读回寄存器时检查ACK标志MCC驱动会吞掉错误地址扫描通过之后我继续读0x010F寄存器返回值依然是0x00。这时候一个很容易被忽略的点浮出水面MCC生成的库函数在出错时并不会返回一个类似“-1”的错误码它把错误状态存在内部标志里。如果你这段代码只执行WriteByte、ReadByte不查错误标志即使从机压根没应答驱动也会“装作成功”最终把接收缓冲器里的默认值返回给你。这个默认值在很多MCC版本里就是0。所以你看到0x00不一定代表从机回了0而是驱动在“从机没回数据”的情况下给了你一个0。修正办法是每完成一个I2C动作都检查错误标志I2C1_Start(); I2C1_WriteByte(0x52); while (!I2C1_IsDone()); if (I2C1_IsError()) { // 打印错误不再继续 }如果你把所有动作的错误标志都打印出来很容易就能定位到是哪一步NACK了。比如写从机地址0x52这一步就NACK那是物理层或地址不对如果这一步ACK但写寄存器地址时NACK说明芯片虽然在线但正忙于boot或者总线时序不满足它的要求。2.3 接线、供电与上拉电阻的三个低级但高频的坑在深入软件之前还是先聊聊硬件排查这个问题我吃过大亏。VL53L1X的AVDD供电范围是2.6V到3.5V典型工作电压是2.8V或者3.3VI2C引脚不是5V耐受的。PIC18F16Q41工作电压范围很宽如果你直接给它供5V传感器用3.3V两边I2C直连那么VL53L1X的SDA引脚会被5V高电平反复冲击轻则通信异常重则损坏芯片。我在这次调试中用了3.3V给VL53L1X供电PIC18F16Q41也是3.3V单电源所以电平域是一致的。如果你的系统是5V单片机配3.3V传感器老老实实加I2C电平转换模块或者用两个MOS管搭一个双向电平转换电路别指望“运气好能跑起来”。上拉电阻也容易踩坑。I2C总线需要上拉电阻把SDA和SCL拉高VL53L1X的datasheet建议上拉电阻根据总线电容和通信速率选择400kHz快速模式下通常用2.2kΩ到4.7kΩ。有些开发板上已经自带了这个电阻但如果你是把VL53L1X模块单独拉线连到单片机模块上又没焊上拉那么总线永远出不来完整波形。这里有个简单的判断方法模块到手后测一下SDA和SCL的对地电压。正常待机状态下SCL和SDA都应该是高电平接近供电电压。如果测量发现SDA或SCL是0V先补上拉电阻再说。提示如果使用PIC18F16Q41内部的WPU弱上拉来凑合可能能点亮但极不稳定特别是总线电容较大或者线长超过10cm时弱上拉的驱动能力不够波形上升沿会变得很缓导致数据采样错误。老老实实外部上拉。3. VL53L1X的16位寄存器地址是这次最大的“暗坑”3.1 为什么要先写地址再读数据不是所有I2C器件都一样很多常用的I2C传感器比如MPU6050寄存器地址是8位的读操作流程是START、写从机地址写、写一个寄存器地址、重启动、写从机地址读、读数据。但VL53L1X不是它的寄存器地址空间是16位的比如0x010F这个ID寄存器高字节是0x01低字节是0x0F。读取时必须先写两个字节的寄存器地址先写高字节0x01再写低字节0x0F。只发一个0x0F的话芯片会把它当成寄存器地址的高字节根本找不到0x010F这个寄存器读出来的数据完全是另一回事。我在排查时先确认了地址扫描能找到0x29然后在代码里反复检查重启动和读地址逻辑差点往错误的方向钻牛角尖。最后逐个比对波形才想起来VL53L1X读取流程和8位寄存器地址器件不一样需要写两个字节的寄存器地址。回归到根本这类芯片不是“读寄存器时自动把地址递增或处理16位地址”它把所有I2C操作都视为“总线操作”你给它什么地址它就寻址什么寄存器区别只在于写几个地址字节。ST的驱动程序里封装了这个过程但裸代码就得自己处理。3.2 完整时序拆解从START到STOP每一步的波形与状态以400kHz快速模式为例读0x010F寄存器的完整流程如下START信号。发送从机写地址0x52末尾是写位0。等待芯片ACK。发送寄存器地址高字节0x01。等待芯片ACK。发送寄存器地址低字节0x0F。等待芯片ACK。发送RESTART信号不是简单的START必须是重复起始。发送从机读地址0x53末尾是读位1。等待芯片ACK。读取第一个字节数据主机在接收完成后发送NACK表示“只读一个字节别再发了”。STOP信号。用逻辑分析仪看第8步比第1步多了一个“在SCL和SDA都是高电平时SDA再拉低”的过程从波形上看和START几乎一样但位置上必须发生在两个写字节之后、读地址之前。如果这步写成普通的STOPSTART某些从机也能容忍但VL53L1X对时序的容错没那么高我实测过这种情况它偶尔能读出正确数据偶尔返回0非常不稳定。第11步的NACK尤其关键。很多新接触硬件I2C的人以为读一个字节就是“等它把数据发出来就完事”实际上主机这边的驱动器需要显式控制ACK位。如果你读最后一个字节时发了ACK而不是NACKVL53L1X会认为主机还想继续读接着把下一个寄存器的内容推上总线。这时你再发STOP芯片内部的状态机可能还没退出下一次通信就乱了。MCC生成的I2C1_ReadByte函数一般会自动处理最后一个字节的NACK但前提是你用的是“ReadByte”而不是“GenericRead”并且调用顺序正确。顺带一提如果你用示波器看波形可以重点观察SCL的第九个脉冲期间SDA的电平。在主机发送的每个字节之后SDA会被从机拉低这是ACK。在主机读取字节后SDA由主机控制第九个时钟周期应该保持高这是NACK。波形图上这一眼就能分辨整个流程对不对。3.3 可以直接抄的PIC18F16Q41硬件I2C读取代码下面这段代码是我在MCC生成硬件I2C驱动的基础上写的VL53L1X ID读取函数。不同的MCC版本生成的API名称可能不同关键是理解每一步在做什么然后对应到你自己的驱动函数上。#include mcc_generated_files/mcc.h #define VL53L1X_I2C_ADDR_W 0x52 #define VL53L1X_I2C_ADDR_R 0x53 uint8_t VL53L1X_ReadID(void) { uint8_t id 0; // 1. START I2C1_Start(); while (!I2C1_IsDone()); // 2. 写从机地址 W I2C1_WriteByte(VL53L1X_I2C_ADDR_W); while (!I2C1_IsDone()); if (I2C1_IsError()) { I2C1_Stop(); return 0; } // 3. 写寄存器地址高字节 0x01 I2C1_WriteByte(0x01); while (!I2C1_IsDone()); if (I2C1_IsError()) { I2C1_Stop(); return 0; } // 4. 写寄存器地址低字节 0x0F I2C1_WriteByte(0x0F); while (!I2C1_IsDone()); if (I2C1_IsError()) { I2C1_Stop(); return 0; } // 5. REPEATED START I2C1_RepeatedStart(); while (!I2C1_IsDone()); // 6. 写从机地址 R I2C1_WriteByte(VL53L1X_I2C_ADDR_R); while (!I2C1_IsDone()); if (I2C1_IsError()) { I2C1_Stop(); return 0; } // 7. 读取一个字节硬件自动在末尾发NACK I2C1_ReadByte(); while (!I2C1_IsDone()); id I2C1_GetByte(); // 8. STOP I2C1_Stop(); return id; }如果你的MCC版本里I2C1_ReadByte函数直接返回读取到的值那第4行就写成id I2C1_ReadByte();。这个差异源于Microchip MCC不同版本对I2C外设驱动API的调整所以拿到新版本MCC生成的代码第一步先打开i2c1.h看一眼函数原型别想当然。实测下来这段代码在PIC18F16Q41上读到的id就是0xEA。如果读到的还是0x00那就往下看第4章电源和复位时序的问题。4. 电源域和XSHUT复位时序隐蔽的0x00制造机4.1 3.3V传感器与5V单片机混合供电的陷阱第2章里提到过电平不匹配问题这里展开讲讲。PIC18F16Q41的I/O和电源电压是配套的你给它供5V它的I/O高电平就是5V。VL53L1X的I2C引脚绝对最大额定值不能超过供电电压0.3V以上而它的供电又必须在3.5V以内。如果你用5V单片机直连3.3V VL53L1X传感器一脚踩着3.3V一脚被拉到5V长久工作必出问题。我遇到过特别典型的故障现象刚上电时能读到0xEA但工作几秒后读出来就变成0x00。量一下SDA引脚电压发现它一直低在0.8V左右明显是被某一边钳住了。后来确认就是5V的高电平持续灌入3.3V器件造成内部保护二极管导通总线被“锁死”。如果你实在不想加电平转换芯片至少保证两边供电都是3.3V或者用两个4.7k电阻做分压把单片机输出的5V高电平降到传感器能容忍的范围同时把传感器输出的3.3V高电平直接送给5V单片机。不过这种做法只能单向匹配I2C是双向的最稳妥的还是专用电平转换方案。4.2 XSHUT拉高后必须等多久才能访问I2CVL53L1X的XSHUT引脚是芯片的硬件复位引脚低电平有效。很多模块设计上电默认让XSHUT低等单片机初始化完成后拉高这样做的好处是芯片能在一个确定的时间开始boot。但问题是有些人拉高XSHUT后紧接着就发I2C读命令芯片boot都没完成根本不会正常应答。在我的板子上XSHUT由PIC18F16Q41的一个GPIO控制。最开始我把XSHUT拉高后只等了1毫秒就去读ID结果读数就是0x00。后来我改成拉高后延时10毫秒再读第一帧就是0xEA。虽然datasheet上写的boot时间是1.2毫秒但考虑到供电上升时间、板上电容充电、传感器内部稳压器稳定等因素实际等待时间往大了放比较稳妥。c VL53L1X_XSHUT_SetHigh(); __delay_ms(10); // 实测10ms足够稳定 uint8_t id VL53L1X_ReadID();还要注意一个问题如果你用MCC的引脚配置把XSHUT所在的引脚初始化为低电平并且这个初始化发生在系统上电瞬间那VL53L1X会被一直按在复位状态直到你把该引脚拉高。有些模块上还把XSHUT接了上拉电阻这时候GPIO输出低会造成外部上拉和单片机引脚对拉电流可能偏大。正确做法是确认模块原理图如果板上已有上拉初始化时把引脚设为高阻输入或者直接拉高输出别让它输出低。4.3 用逻辑分析仪看启动阶段的I2C波形排查到这一步我建议上逻辑分析仪。不是等到最后才用而是从发现问题开始就挂着看波形。I2C调试如果没有示波器或逻辑分析仪等于摸黑走路。用逻辑分析仪抓启动阶段波形时重点看三个东西第一个START之后从机有没有在第9个时钟周期拉低SDA表示ACK。如果没有任何ACK脉冲说明从机压根没醒重点查XSHUT和电源。主机发送寄存器地址时SDA上的数据是不是0x01、0x0F。如果发成了0x0F、0x01高低字节顺序反了读出来就是别的值。重复起始后读到的第一个字节数据是不是0xEA。如果第一个字节是0xEA但程序里最终打印的是0x00说明软件里读回数据的容器不对比如读取完没有等I2C1_IsDone就调I2C1_GetByte拿到的是旧值或者空值。我用逻辑分析仪看到过一种很有意思的故障SCL上只有前几拍的时钟后面几拍时钟幅度断崖式下跌看起来像信号被拉死。后来排查发现是SDA和SCL两根线在杜邦线连接时顺序接反了SDA接到SCL、SCL接到SDA但芯片没坏I2C主站还能发送然而从机的地址永远对不上读到数据自然全乱。这种问题靠万用表量通断比看代码容易发现得多。5. 硬件I2C新外设配置自查清单与压箱底的验证手段5.1 MCC配置中容易漏掉的引脚与时钟选项如果你排查完上面所有步骤还是没有0xEA那就回到MCC配置界面逐项检查这几个容易忽略的设置。引脚映射。PIC18F16Q41的I2C引脚是可配置的在MCC的Pin Module里你需要手动把SCL和SDA分别映射到具体的引脚上同时确认这两个引脚没有被其它外设占用。映射完成后还要把引脚对应的ANSEL位关闭否则它默认是模拟功能数字I2C信号根本进不去。I2C时钟配置。MCC的I2C外设配置界面会让你填写目标时钟频率比如400kHz但实际总线频率取决于单片机主频和分频器的组合。如果主频配置和实际晶振不一致MCC计算出来的分频数就不准总线频率可能只有几十kHz甚至更低VL53L1X在这种频率下虽然能工作但如果太慢导致内部超时也可能出现异常。中断使能。MCC生成的I2C驱动有的依赖中断有的用轮询。如果你启用了中断但没把I2C中断优先级配好或者在中断服务程序里没有清除标志那么I2C1_IsDone会永远等不到完成状态程序卡死或一路跳过。我建议初次调试先用轮询模式跑通之后再考虑中断处理。还有一个容易忽略的选项是I2C外设的时钟拉伸和超时控制。VL53L1X有自动时钟拉伸能力如果外设配置把时钟拉伸关闭了或者超时时间设置太短遇到慢速响应时就会判定为总线错误。MCC里通常有Clock Stretching相关的选项保持默认允许状态就好。5.2 如果前述检查都通过用软件I2C交叉验证当所有硬件和MCC配置都检查过依旧读不到0xEA时我建议写一个软件I2CGPIO模拟I2C的读取函数来交叉验证。这不是退步而是一种有效的二分定位法如果软件I2C能读到0xEA说明VL53L1X本身正常问题锁定在单片机硬件I2C外设配置上如果软件I2C也读不到0xEA那基本可以确定问题在传感器一侧的接线、供电或者芯片本身。软件I2C实现不复杂核心是手动翻转SCL、按位收发SDA、并实现START、STOP、ACK检查。我通常会写得尽量精简配合一个10毫秒的XSHUT上电等待确认传感器基本状态。#define I2C_SDA_SetHigh() LATBbits.LATB0 1 #define I2C_SDA_SetLow() LATBbits.LATB0 0 #define I2C_SDA_Read() PORTBbits.RB0 #define I2C_SCL_SetHigh() LATBbits.LATB1 1 #define I2C_SCL_SetLow() LATBbits.LATB1 0 void Soft_I2C_Start(void) { I2C_SDA_SetHigh(); I2C_SCL_SetHigh(); I2C_SDA_SetLow(); I2C_SCL_SetLow(); } void Soft_I2C_Stop(void) { I2C_SDA_SetLow(); I2C_SCL_SetHigh(); I2C_SDA_SetHigh(); } uint8_t Soft_I2C_WriteByte(uint8_t data) { for (uint8_t i 0; i 8; i) { if (data 0x80) I2C_SDA_SetHigh(); else I2C_SDA_SetLow(); data 1; I2C_SCL_SetHigh(); I2C_SCL_SetLow(); } // release SDA and read ACK TRISBbits.TRISB0 1; I2C_SCL_SetHigh(); uint8_t ack !I2C_SDA_Read(); I2C_SCL_SetLow(); TRISBbits.TRISB0 0; return ack; }实际应用中把SDA方向切换处理好再补充读字节函数即可。整个软件I2C代码量不大但它给硬件外设排查提供了一个很干净的对照实验环境。我记得当时软件I2C第一次就读到0xEA我心里反而踏实了因为至少VL53L1X是好的最后在硬件I2C配置里找到了丢掉的错误检查逻辑。5.3 ST官方ULD API在验证阶段的价值ST官方为VL53L1X提供了ULD APIUltra Lite Driver这是一套跨平台的驱动代码目标就是方便用户在不同单片机上快速跑通测距功能。它的代码可以从ST官网或者GitHub仓库获取里面包含了完整的寄存器定义、初始化序列、测距函数和I2C读写接口。这里的价值在于如果你已经排查到“不确定是不是寄存器初始化顺序的问题”可以直接把ULD API里的platform层替换成自己的I2C读写函数例如uint8_t VL53L1_WriteMulti(VL53L1_Dev_t *dev, uint16_t index, uint8_t *pdata, uint32_t count); uint8_t VL53L1_ReadMulti(VL53L1_Dev_t *dev, uint16_t index, uint8_t *pdata, uint32_t count);只要这两个函数能正确完成16位寄存器地址的读写ULD API内部的所有初始化、测量、状态读取都能跑起来。我最后就是用这种方式确认了硬件I2C外设本身没有大问题——把这两个函数填好后ULD API初始化返回成功测距数据也能正常出来。所以如果你只需要读个ID看看通信是否正常完全不需要移植整套ULD API自己写个函数读0x010F就够了。但如果目标是做完整的测距功能直接用ST官方驱动会比从零开始抠寄存器省力得多至少不用在数据手册里一页页翻初始化序列。说到最后我还是想强调一下这次排错过程对我最大的启发0x00这个返回值本身不可怕可怕的是“它看起来像个正常读取结果”。如果你不主动检查ACK状态、不看波形、不打log很容易被这个温柔的错误带偏。I2C调试一定要带着“每个步骤都验证”的心态去做先确认从机在线再确认每个字节的ACK最后再确认读取的数据。这一步一个脚印排查下来往往比随机改参数更早看到0xEA那一幕。
返回列表