Context
For 360° indoor coverage, the drone needs an array of 6 VL53L0X around the frame. All sensors have the default I²C address 0x29 → conflict. Two solutions: TCA9548A multiplexer or XSHUT-based re-addressing (ST AN4846).
Final recommendation: XSHUT + Adafruit CircuitPython continuous mode
Why:
- ST AN4846 — official approach; the only one without reliability caveats. TCA9548A in continuous mode occasionally blocks the I²C bus (Adafruit forum issue #27 + thread t=126621).
- Adafruit CircuitPython VL53L0X — actively maintained Python lib, RPi-ready, multi-sensor example for 2 sensors out of the box (extends to 6 by simply adding XSHUT pins).
- Realistic ~25-30 Hz total array on RPi 5 in Python (timing budget 20 ms, continuous mode, polling
data_ready). 3× headroom over 10 Hz mission FSM. - CPU < 10% on one core of RPi 5, event → publish latency < 25 ms.
Pattern: XSHUT + software re-addressing
- At start, all sensors in reset (XSHUT LOW via GPIO).
- Bring up XSHUT HIGH on one sensor at a time; write a unique address (0x30, 0x31, …) over I²C.
- Address change is not permanent — the procedure repeats on each power cycle.
- After init — all sensors on unique addresses, call
start_continuous()on each. - Polling cycle via
data_readyproperty — sequential read; sensors physically work in parallel.
GPIO budget: 6 GPIOs for the XSHUT lines (RPi 5 has 27 GPIOs — not a blocker).
Realistic Hz numbers (estimates)
| Scenario | Per-sensor | Total array | Comments |
|---|---|---|---|
| A — Naive (sleep 1s) | 1 Hz | ~1 Hz | unusable (Pololu forum) |
B — Continuous + polling data_ready (TB=20 ms) |
3-5 Hz | ~25-30 Hz | chosen — enough for 10 Hz FSM |
| C — GPIO interrupt + select() | 30-40 Hz | ~200-240 Hz | +12 GPIO, RT issues |
| D — C++ rclcpp + ioctl + interrupt | ~50 Hz | ~300 Hz | bandwidth limit |
Comparison of approaches
XSHUT (ST AN4846 recommended) ✅
- ✅ Native ST approach; references everywhere
- ✅ Continuous mode without bus blocking
- ✅ Sensor-failure isolation (cut a faulty one via XSHUT)
- ❌ N GPIOs for N sensors
- ❌ Software re-addressing on every power cycle
TCA9548A multiplexer ❌
- ✅ Minimum GPIO (only 2 for I²C + power to TCA itself)
- ✅ Built-in in Pimoroni VL53L0X-python
- ❌ Channel-switching overhead (~25-300 µs per switch)
- ❌ Reliability issues — Adafruit issue #27 (unresolved), forum 126621 — blocks the I²C bus in continuous mode
- ❌ Reset / fail recovery harder
Driver landscape (gap analysis)
| Driver | Lang | Multi-sensor | RPi 5 ready | Maintained | Verdict |
|---|---|---|---|---|---|
| Adafruit_CircuitPython_VL53L0X | Python | ✅ XSHUT | ✅ via blinka | ✅ active 2024+ | baseline pick |
| Gadgetoid/VL53L0X-python | Python ctypes | ✅ XSHUT + TCA9548A | ⚠ stale | ❌ since 2018 | fallback / reference |
| sebastiengemme/vl53l0x-ros | C++ | ❌ single | unspec | unspec | old, single-sensor |
| slaghuis/ROS2-VL53L1X | C++/Py | ❌ single | ✅ | ⚠ | for VL53L1X, not L0X |
| Custom rclpy wrapper on Adafruit | Python | ✅ | ✅ | self | our path |
Gap analysis: a production-grade ROS 2 driver for a VL53L0X array doesn’t exist in 2024-2025. All candidates are single-sensor / ROS1 / stale. → write our own researchbest_drone/sensors/vl53l0x_array_node.py over Adafruit.
Minimal code (skeleton)
import board, busio
from digitalio import DigitalInOut, Direction
import adafruit_vl53l0x
# 6 XSHUT pins on RPi 5 GPIO 5, 6, 12, 13, 16, 17
xshut_pins = [board.D5, board.D6, board.D12, board.D13, board.D16, board.D17]
xshuts = [DigitalInOut(p) for p in xshut_pins]
for x in xshuts:
x.direction = Direction.OUTPUT
x.value = False # all in reset
i2c = busio.I2C(board.SCL, board.SDA)
sensors = []
for idx, x in enumerate(xshuts):
x.value = True # bring up sensor N
time.sleep(0.05)
s = adafruit_vl53l0x.VL53L0X(i2c)
s.set_address(0x30 + idx) # unique address
s.measurement_timing_budget = 20000 # 20 ms = 50 Hz max
s.start_continuous()
sensors.append(s)
# In the ROS 2 node:
while rclpy.ok():
for idx, s in enumerate(sensors):
if s.data_ready:
rng = s.range # mm
self.publish_range(idx, rng / 1000.0)
time.sleep(0.005)
Wiring
- 6× VL53L0X: VCC + GND + SDA + SCL + XSHUT = 30 wires
- A custom PCB or ribbon cable is recommended — not spaghetti
- Common GND mandatory
- I²C pull-ups on 3.3 V (often built into the breakout, verify)
RPi 5 I²C @ 400 kHz config
/boot/config.txt:
dtparam=i2c_baudrate=400000
core_freq=250
I²C physical-layer baseline: a 10-sensor read at 400 kHz = ~2.5 ms.
Skip
- TCA9548A multiplexer — ~1.5× more channel-switch overhead + reliability issues. Minimum-GPIO advantage isn’t justified on RPi 5 (27 GPIOs).
- Naive single-shot mode with sleep between reads — ~1 Hz/sensor, bottleneck without cause.
- Pimoroni Gadgetoid/VL53L0X-python in production — stale since 2018, RPi 5 compatibility not guaranteed.
Risks
- VL53L0X 2 m max range may be too short for open spaces — fallback to VL53L1X (4 m) or a hybrid of forward-facing TF-Luna + VL53L0X×4-5 side/up/down.
- Adafruit Blinka on RPi 5 BCM2712 — not first-class supported. Risk mitigation: smbus2 fallback (lower-level).
- GPIO interrupt latency variance on non-RT Linux — Scenario C needs
chrtRR scheduling.
Effort estimate
- 4 hours: write
vl53l0x_array_node.py(rclpy + Adafruit lib wrap, 6 XSHUT init, continuous mode) - 2 hours: RPi 5 I²C @ 400 kHz config, wiring diagram
- 4 hours: bench measurement (when HW exists)
- 4 hours: integration test with
mission_fsmobstacle-avoidance loop
Total: ~14 hours to production-ready.
Sources
- ST AN4846 “Using multiple VL53L0X in a single design” — DM00280486
- Adafruit CircuitPython VL53L0X —
examples/vl53l0x_multiple_sensors_continuous.py - VL53L0X datasheet Rev 5+ — timing budget, GPIO1 interrupt
- Pololu forum t/15227 — TCA9548A median filter
- Pololu forum t/13852 — Fast VL53L0X readings (nathanb dev)
- Adafruit forum t=126621 — VL53L0X + TCA9548A blocks I²C