ARTICLE DETAIL

资讯详情

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

VL53L9多模块I2C地址冲突?XSHUT分时上电+软件改地址全攻略

VL53L9多模块I2C地址冲突?XSHUT分时上电+软件改地址全攻略 1. 为什么我非要去折腾 VL53L9 的 I2C 地址1.1 我在做的东西最近在搭一台多区域深度感知的小设备说白了就是低成本接近传感器阵列配合手势识别功能。手上正好有几片 ST 的 VL53L9 系列飞行时间ToF测距模块。这颗芯片跟我之前用过的 VL53L0X、VL53L1X 这类老型号最大的区别是区域感知能力更完整能直接输出不同空间位置的深度分段做避障、坐姿提醒、接近检测、手势方向判断这类应用都很合适。不过项目做到第二版时我需要在同一块主板上并两个 VL53L9 模块一个朝前做障碍物测距一个朝斜下做跌落检测。两个模块安装位置不同但 I2C 总线只有一条这就逼着我去认真处理“地址”这个话题。1.2 问题是怎么冒出来的第一版接线图我画得很随意两个 VL53L9 模块的 SDA、SCL 直接并到同一组引脚上VCC 统一供 3.3VGND 共地然后上电。最开始我用i2cdetect -y 1扫描总线屏幕上蹦出来一个 0x29接着又跳出一个看起来一模一样的 0x29。两个从机都对着同一个地址应答主机根本分不清谁是谁命令发下去它们同时响应甚至出现一个拉低 SDA 另一个也想拉低的情况通信直接乱掉。这个现象如果你也遇到过那基本就是 I2C 地址冲突也就是我写这篇内容的起点。2. I2C 地址的底层逻辑7 位、8 位、0x29 还是 0x522.1 先搞清楚设备地址在 I2C 总线里是什么角色I2C 是典型的“一主多从”总线。主机发一个起始位然后跟着一个地址字节所有挂在总线上的从机都会收到这个字节但只有地址匹配的那一个会回应 ACK其他从机保持沉默。问题可能出在地址的“位数”上。I2C 规范里设备地址有 7 位寻址和 10 位寻址两种模式绝大多数传感器用的都是 7 位模式。7 位地址不代表你在数据手册里看到的就是最终发送的字节因为主机会把这 7 位左移一位最低位空出来作读/写标志位。我见过不少新手在这上面踩坑数据手册写“I2C 地址 0x52”到代码里就填0x52结果扫描时看到的却是0x29。原因很简单手册上写 0x52 往往已经是左移后的 8 位地址对应到 7 位地址就是 0x29你直接拿 0x52 去Wire.beginTransmission()能通才怪。表示方式数值说明7 位地址0x29数据手册寄存器描述里最常见8 位地址左移一位0x52很多 I2C 驱动和 Wire 库要求填这个写操作地址字节0x527 位地址左移一位最低位写 0读操作地址字节0x537 位地址左移一位最低位写 1这里有个判断技巧如果你扫描总线看到 0x29而驱动里填的是 0x52 却一直找不到设备先别怀疑硬件把地址右移一位再填入。反过来驱动里填 0x29 但总线扫描结果是 0x52那就左移一位。2.2 VL53L9 的默认地址是多少ST 的 ToF 传感器产品线很长从 VL53L0X 到 VL53L1X再到 VL53L3、VL53L5、VL53L7、VL53L8以及 VL53L9 系列整体上默认 7 位地址都是 0x29换算成 8 位地址是 0x52。VL53L9 延续了同一套设计习惯所以我手上这批模块默认地址也是 0x29。但这里我要强调一句不同模块厂商可能通过硬件引脚或出厂配置改过地址所以“默认 0x29”只能作为第一猜测。拿到新模块第一步永远是上电后用 i2cdetect 或者逻辑分析仪扫一遍看到实际地址再往下走。2.3 为什么很多 ST 传感器老是共用 0x29同一家公司把默认地址定成同一个值当然不是设计偷懒而是为了保证生态兼容。对用户来说单颗传感器使用时越无脑越好大家默认都是 0x29同一份驱动代码就能跑。但这带来一个副作用你把几颗一模一样的传感器并到一条总线上不处理地址的话全军覆没。所以 VL53L9 这类芯片通常都会提供两种地址规避手段一是通过 XSHUT 引脚配合上电时序二是在软件里调用 API 改写地址寄存器。下一节就展开讲。3. VL53L9 更改 I2C 地址的几种常见方法3.1 方法一用 XSHUT 引脚分时上电最稳也最推荐XSHUT 是 ST ToF 传感器上的复位/使能引脚低电平有效。把它拉低传感器内部的 I2C 状态机完全不工作相当于从总线上“摘掉”拉高传感器才开始正常工作。多颗同型号传感器的标准做法是把每一颗的 XSHUT 单独接到主控的 GPIO然后按这个顺序操作全部 XSHUT 拉低所有传感器都离线。把第一颗的 XSHUT 拉高只让这一颗以默认地址 0x29 上线。主控用默认地址连接第一颗发送地址修改指令把它的地址改成 0x45 或其它不冲突的值。把第二颗的 XSHUT 拉高这时第二颗使用的仍然是 0x29但已经和第一颗的新地址 0x45 区分开了。这个方法的好处是逻辑简单、可重复性好。上电时序只要保证“同一时刻只有一颗使用默认地址”就行后面 SystemInit 阶段也很容易恢复。需要注意初始化完成后 XSHUT 都要保持高电平不能随意拉低否则传感器会复位地址可能恢复成默认值。不同型号的 VL53L9 系列在掉电后地址是否保持也有差异我会在后面单独讲。3.2 方法二调用 ST API 在软件里改地址ST 为每代 ToF 传感器都提供了官方驱动 APIVL53L9 系列也不例外。API 里通常会有一个设置设备地址的接口我印象中 VL53L0X 时代是VL53L0X_SetDeviceAddress()VL53L1X 时代是VL53L1X_SetDeviceAddress()VL53L9 这套 Ultra Lite 驱动的函数命名可能更长更严格具体以你拿到的 API 头文件为准。这类接口的原理其实是往传感器内部的 I2C 地址寄存器写一个新值。芯片内部会把寄存器里的值作为 7 位地址解析然后在后续通信中响应新的地址。用软件改地址有一个注意点地址寄存器往往不是普通数据寄存器写入后需要传感器内部固件完成重新初始化。所以你在代码里看到的常见流程是用默认地址发起通信。调用 SetDeviceAddress 接口传入新地址。等待至少几十毫秒。从默认地址切换到新地址继续通信或重新初始化。如果你自己读寄存器来写而不是用官方 API风险主要在于寄存器偏移、数据位宽、是否需要额外校验。同一个系列不同型号之间这些细节可能有差异。我在实际项目里吃过亏所以强烈建议能用 API 就用 API不要凭感觉直接往 i2c 寄存器里写地址。3.3 方法三硬件地址选择引脚这个不一定每款都有ST 在部分 ToF 传感器上保留了一个硬件地址选择引脚或者通过外部下拉电阻来改变默认启动地址。比如把某个引脚拉高默认地址就从 0x29 变成 0x528 位之类。VL53L9 系列我不确定每款变体都开放了这个引脚所以最靠谱的做法是看数据手册的 Pin Description 部分搜索 “I2C address” 或 “address select”。即便有硬件地址选择引脚它能选的组合也有限通常只能做到二选一或四选一。如果项目里要挂三颗以上最终还是得配合 XSHUT 或者软件改地址。4. 实操记录从接线到两个 VL53L9 同时工作4.1 接线和上电顺序我用的主控是 STM32F103两个 VL53L9 模块都接在 I2C1 上。接线如下VL53L9_A 的 XSHUT - PA0VL53L9_B 的 XSHUT - PA1两个模块 SDA - PB7两个模块 SCL - PB6VCC - 3.3VGND - GND这里有个很多人容易忽略的坑VL53L9 这类新的深度传感器对 VCSEL 发射的供电要求比老型号高启动瞬间电流比想象中大如果开发板上的 3.3V LDO 余量不足传感器会反复复位导致地址扫描时有时无。所以供电优先级是先保证电源余量再谈地址。上电顺序我在程序里是这么处理的系统上电后PA0 和 PA1 默认输出低电平两个模组都处于复位状态。初始化 I2C 外设。PA0 拉高只让模块 A 工作。延时 100ms让模块 A 完成内部固件启动。调用VL53L9X_Sensor_SetI2CAddress()把 A 的地址从 0x29 改成 0x45。再延时 20ms。PA1 拉高让模块 B 上线B 保持默认地址 0x29。最后分别读取两个地址的传感器 ID 寄存器确认通信正常。这一步的细节决定成败XSHUT 拉高后到传感器真正能响应 I2C 命令之间有一个上电初始化时间。不同型号时间长短不同我实测下来 VL53L9 系列大概需要几十毫秒到一百毫秒。如果延时太短你会看到总线扫描正常但一读寄存器就返回 NACK。4.2 STM32 / Arduino 代码实现下面这段代码我做了简化但保留了关键逻辑方便你移植到自己的工程里。这里我使用伪 API 名称实际开发时请替换成你工程里 ST 官方驱动提供的函数。#include vl53l9_api.h #define XSHUT_A_PIN 0 #define XSHUT_B_PIN 1 #define VL53L9_DEFAULT_ADDR 0x29 #define VL53L9_A_NEW_ADDR 0x45 VL53L9_Dev_t dev_a; VL53L9_Dev_t dev_b; void set_xshut(int pin, bool level) { digitalWrite(pin, level ? HIGH : LOW); } void setup() { pinMode(XSHUT_A_PIN, OUTPUT); pinMode(XSHUT_B_PIN, OUTPUT); // 初始让两个传感器都离线 set_xshut(XSHUT_A_PIN, LOW); set_xshut(XSHUT_B_PIN, LOW); delay(10); // 模块 A 上线改地址 set_xshut(XSHUT_A_PIN, HIGH); delay(100); dev_a.i2c_addr VL53L9_DEFAULT_ADDR; VL53L9X_Init(dev_a); VL53L9X_Sensor_SetI2CAddress(dev_a, VL53L9_A_NEW_ADDR); delay(20); // 模块 B 上线保持默认地址 set_xshut(XSHUT_B_PIN, HIGH); delay(100); dev_b.i2c_addr VL53L9_DEFAULT_ADDR; VL53L9X_Init(dev_b); } void loop() { VL53L9_Result_t result_a, result_b; // 两个地址独立轮询互不干扰 VL53L9X_StartRanging(dev_a); VL53L9X_GetResult(dev_a, result_a); VL53L9X_StartRanging(dev_b); VL53L9X_GetResult(dev_b, result_b); delay(50); }有几个细节我要单独拎出来说第一改地址前一定要让模块 A 完成初始化。我见过有人上来就直接写地址结果传感器还没准备好写入被忽略后面一遍遍找不到设备。第二改完地址后dev_a.i2c_addr也要同步改成新地址。这个很容易漏因为 API 驱动内部很可能用这个变量去拼接每次通信的地址字节。地址改了但驱动上下文里还是旧地址就会不断通信失败。第三有的 API 版本里VL53L9X_Sensor_SetI2CAddress()成功返回后不会改变驱动结构体内的地址字段你需要手动赋值。这种“半自动”设计很容易造成误导我建议改完地址后直接读一次传感器 ID 寄存器验证当前通信用的到底是哪个地址。4.3 Linux 环境下的处理方式如果你的项目跑在 Linux 上比如树莓派或者各种 ARM 开发板思路一样。我习惯先用 i2c-tools 扫一下总线i2cdetect -y 1正常情况下只给一个传感器上电时你会在 0x29 附近看到地址两个都上电就会变成同一个地址重复出现。这时候用 XSHUT 分时上电的方式先把其中一个改地址再拉高另一个。内核驱动里的设备树也要对应修改。比如我使用双传感器时设备树里会分别描述两个节点i2c1 { status okay; clock-frequency 400000; vl53l9_a: vl53l945 { compatible st,vl53l9; reg 0x45; xshut-gpios gpioa 0 GPIO_ACTIVE_LOW; }; vl53l9_b: vl53l929 { compatible st,vl53l9; reg 0x29; xshut-gpios gpioa 1 GPIO_ACTIVE_LOW; }; };这里有一个很关键的细节设备树里的reg用的是 7 位地址还是 8 位地址取决于内核 I2C 子系统的约定。一般情况下设备树里填的是 7 位地址也就是 0x45 和 0x29 这种。如果你填 0x52驱动会认为从机在另一个地址上导致 probe 失败。如果厂商驱动没有提供 set address 的功能你还可以在用户空间写一个辅助程序在驱动 probe 之前先通过默认地址完成改地址操作。不过这种做法比较 hack会跟内核驱动抢设备不如在驱动层面一次性解决。4.4 验证方式不要只看 i2cdetect很多人改完地址看到 i2cdetect 里出现两个不同地址就觉得大功告成其实不够。i2cdetect 只能证明“总线上有设备在应答”不能证明“这个设备真的是 VL53L9”。我习惯的做法是读取传感器 ID 寄存器做双重确认。VL53L9 系列有专门的 identification 寄存器里面存有模型 ID、模块 ID、硅版本号等信息。两个模块虽然地址不同但读到的 ID 应该在同一个范围内而且和具体型号对应得上。已验证的结果长这样地址模型 ID 寄存器读值状态0x290xEC举例正常0x450xEC举例正常如果两个地址读到的 ID 差别很大或者某个地址读不出来那就要回到硬件连接和 XSHUT 时序上排查不要继续调试应用层。5. 常见问题与排查笔记5.1 i2cdetect 扫描不到 VL53L9这个问题排第一因为太常见了。可能原因有供电不足传感器没有正常启动扫描时模块完全无应答。测一下 3.3V 的实际电压接了传感器后有没有被拉低。忘了共地。SDA、SCL 上有上拉但回不到同一个参考地信号根本不成形。地址不是 0x29被模块厂商改过。这时候不要猜把总线上的器件一个个摘掉每次只留一个模块看扫描结果。I2C 上拉电阻缺失。很多成品模块自带 4.7k 上拉但如果你用的是裸芯片或者自己画的底板记得在 SDA、SCL 上各加一个 4.7k 或 10k 的上拉电阻到 3.3V。排查顺序建议先量电压再示波器看 SCL、SDA 有没有波形最后再怀疑地址问题。不要一上来就想着改地址先把“能通信”这个基本盘稳住。5.2 改完地址后设备在总线上“消失”这个问题我也踩过。具体表现是第一次能通过默认地址通信执行完 SetDeviceAddress 后新地址扫描不到默认地址也扫描不到。一个常见原因是你选的“新地址”本身不合法。I2C 协议里有些地址是保留地址比如 0x00、0x01、0x08 到 0x0F 等不能用作普通设备地址。如果选到这些值从机不会正确响应。安全起见我只推荐在 0x08 到 0x77 这个范围内挑选地址并且避开 0x29、0x52 这些已经被占用的。另一个更隐蔽的原因是传感器改地址后需要重新初始化才能继续工作。有些模块的地址寄存器在设备复位后会丢电丢失。你就算当前改成功了只要 XSHUT 拉低或者断电再上电它就恢复成默认地址。如果程序里没有按这个预期处理上电初始化流程就可能把设备搞丢。我的建议是把“改地址”当成上电初始化的一部分。每个模块上电后都由主控主动检查它当前地址如果不是预期地址就改成预期地址。不要指望一次性烧进去就永远不变。5.3 主机端 I2C 控制器的问题也要排查还有一个跟 VL53L9 本身无关但经常把人带偏的问题主机端控制器没正常工作。比如在 Windows 设备管理器里看到“AMD I2C 控制器出现感叹号”这种是主机端驱动或 BIOS 设置的问题不是传感器的问题。遇到这种状况Linux 下看看 dmesg 里 i2c 控制器是否注册成功Windows 下先更新芯片组驱动确认控制器工作正常再回来调传感器地址。在嵌入式 Linux 里可以用i2cdetect -l查看系统里有哪些 I2C 总线。如果总线列表都是空的控制器可能没使能设备树里要把对应的status改成okay还要确认 pinmux 配置正确。只有总线先通了VL53L9 的地址才有意义。5.4 改了地址后 I2C 通信时序异常这种情况通常不是地址选错而是总线频率太高。VL53L9 系列支持 400kHz Fast Mode 甚至更高的 Fast Mode Plus但真实布局下线缆太长、上拉电阻太大时总线时序会被拖垮。表现为低频下一切正常改成 400kHz 后就频繁出错扫描到的地址跳来跳去。排查时可以先把 I2C 时钟降到 100kHz看问题是否消失。如果消失说明是物理层时序问题去优化 PCB 走线、缩短杜邦线、调整上拉电阻。6. 一点个人经验地址问题本质上是个设计问题VL53L9 的 I2C 地址这件事单个传感器用的时候几乎没有存在感但一旦你的系统里出现多颗同型号传感器它就变成最折磨人的那个环节。我现在的处理原则是在设计阶段就把地址分配方案定下来而不是等 PCB 打样回来再补救。具体来说我会在原理图阶段就规划好每颗 VL53L9 的 XSHUT 走线确保每颗都能独立控制同时在嵌入式固件里预留一个地址映射表后续增减传感器模块只需要改一张表不用改动驱动框架。如果让我给新手一句话建议那就是不要绕开 XSHUT不要嫌多一根线麻烦。XSHUT 分时上电配合软件地址修改是处理 ST ToF 系列多设备最实用的方式。这个方法我用了很多年从 VL53L0X 到 VL53L9 都适用。最后不管你是用 STM32、Arduino 还是 Linux记住一点改完地址之后一定要读设备 ID 验证不要只看 i2cdetect 那一行输出。地址能对上、ID 能读对、数据能出数值才算真正把传感器装进系统里了。
返回列表