VL53L0X tutorial: lidar distance over i2c
The VL53L0X times a laser pulse to the target and back — real lidar for a few dollars, in a package smaller than your fingernail. Every one is hard-wired to address 0x29, which sounds like you can only ever use one. You can use as many as you have spare GPIOs.
Why lidar
VL53L0X vs an ultrasonic sensor
The HC-SR04 is cheaper. The VL53L0X is better at almost everything that matters, and the reason is physics: it measures time of flight of a 940 nm laser pulse, not an echo.
An ultrasonic sensor sprays a wide cone of sound and reports the first thing that bounces back. That makes it hopeless at narrow targets (it'll find the wall behind the pole), at soft ones (fabric and foam absorb the ping), and at angles (sound skates off a tilted surface). It's also physically big.
The VL53L0X sees a narrow spot, doesn't care whether the target is soft, works at close range where ultrasonics have a dead zone, and returns millimetres in a couple of milliseconds. It's the right sensor for gesture detection, a filament runout switch, liquid level, or a robot that shouldn't bump into chair legs.
Its own weaknesses are equally physical, and worth knowing before you build: bright sunlight floods it with infrared and cuts range; matte black surfaces reflect too little; and glass and mirrors defeat it entirely — the beam goes through or bounces away and you get whatever is behind. About 2 metres is the realistic ceiling indoors; the VL53L1X roughly doubles that.
Step 1
VL53L0X wiring and a first reading
Four wires for one sensor: VCC (3.3–5 V on a regulated breakout), GND, SDA, SCL. XSHUT and GPIO1 can stay unconnected until you want multiple sensors or interrupts.
One caution: the bare VL53L0X chip is a 2.8 V part. Mainstream breakouts (Adafruit, Pololu, most Qwiic/STEMMA boards) add a regulator and level shifting so 3.3 V or 5 V is fine — which is why our dataset lists it as 3.3–5 V. Bare-bones modules do exist. If you can't see a regulator on the board, level-shift it rather than hope.
Scan and expect 0x29. Then, with Adafruit's VL53L0X library:
#include "Adafruit_VL53L0X.h"
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
void setup() {
Serial.begin(115200);
if (!lox.begin()) { // defaults to 0x29
Serial.println("No VL53L0X at 0x29 — check wiring");
while (1);
}
}
void loop() {
VL53L0X_RangingMeasurementData_t m;
lox.rangingTest(&m, false);
// status 4 = out of range. The distance field is meaningless then —
// it commonly reads ~8190, which is NOT a measurement.
if (m.RangeStatus != 4) {
Serial.print(m.RangeMilliMeter); Serial.println(" mm");
} else {
Serial.println("out of range");
}
delay(100);
} That RangeStatus check is the difference between a working sensor and a confusing one. When nothing is in range — or the target is black, or it's glass, or the sun is behind it — the sensor doesn't error, it hands back a huge number (usually 8190 mm). Treat that as a distance and your robot confidently drives into a window.
The good part
Multiple VL53L0X sensors: the XSHUT trick
Every VL53L0X boots at 0x29. There are no address pins. Two on one bus both answer at once, and you read the bitwise AND of two sensors — a plausible number that's wrong.
But the address can be changed in software. The catch: you can't tell one apart from the other to address it, because they're both 0x29. Chicken and egg.
XSHUT breaks the loop. It's a hardware shutdown pin — hold it low and that sensor is completely off the bus, deaf and invisible. So:
- Hold every XSHUT low. The bus is empty.
- Release sensor 1 only. It's the sole device at 0x29, so it's unambiguous — give it a new address, say 0x30.
- Release sensor 2. It boots at 0x29, but sensor 1 has moved out of the way. Give it 0x31.
- Repeat. Each one is briefly alone at 0x29 while you rename it.
#include "Adafruit_VL53L0X.h"
#define XSHUT_1 2 // one host GPIO per sensor
#define XSHUT_2 3
Adafruit_VL53L0X lox1, lox2;
void setup() {
Serial.begin(115200);
pinMode(XSHUT_1, OUTPUT);
pinMode(XSHUT_2, OUTPUT);
// 1. everyone off the bus
digitalWrite(XSHUT_1, LOW);
digitalWrite(XSHUT_2, LOW);
delay(10);
// 2. wake sensor 1 alone, move it to 0x30
digitalWrite(XSHUT_1, HIGH);
delay(10);
if (!lox1.begin(0x30)) { Serial.println("sensor 1 failed"); while (1); }
// 3. now wake sensor 2 — 0x29 is free again
digitalWrite(XSHUT_2, HIGH);
delay(10);
if (!lox2.begin(0x31)) { Serial.println("sensor 2 failed"); while (1); }
}
void loop() {
VL53L0X_RangingMeasurementData_t a, b;
lox1.rangingTest(&a, false);
lox2.rangingTest(&b, false);
Serial.print(a.RangeStatus != 4 ? a.RangeMilliMeter : 0); Serial.print(" mm ");
Serial.println(b.RangeStatus != 4 ? b.RangeMilliMeter : 0);
delay(100);
} The gotcha that ruins this: the new address lives in RAM, not flash. Power-cycle the board and every sensor is back at 0x29 with no memory of the rename. That's not a bug to work around — it's why the XSHUT sequence must run in setup() every single boot. Miss it and you get a collision on the second power-up, which looks like intermittently broken hardware.
Cost: one GPIO per sensor. If you're short on pins, a TCA9548A multiplexer does the same job differently — each sensor stays at 0x29 on its own channel, and you spend zero host pins. Both are legitimate; the conflict guide compares them.
Which one
VL53L0X or VL53L1X?
They look identical and behave almost identically. The VL53L1X reaches about 4 m against the VL53L0X's 2, and adds a programmable region of interest — you can narrow its field of view in software to watch a specific patch rather than everything in front of it. Both are at 0x29 and both need the same XSHUT dance for multiples.
Because the boards are indistinguishable by eye, mixing them up is easy. Our free i2c tool tells them apart from their model-ID register — a 16-bit-index read that returns 0xEA on a VL53L1X. Handy when a drawer of sensors has lost its labels.
Get one
Questions
VL53L0X FAQ
What is the VL53L0X's i2c address?
0x29, and there are no address pins. Every VL53L0X boots at 0x29 — but unlike a DS3231, this one can be changed in software, which is what makes multiple sensors possible. The new address lives in RAM, so it's forgotten at every power cycle and has to be reassigned at every startup.
How do I use more than one VL53L0X on the same i2c bus?
The XSHUT trick. Wire each sensor's XSHUT pin to its own host GPIO, hold all of them in reset at startup, then bring them up one at a time — releasing one, giving it a new address, then releasing the next. Only one sensor is ever at the default 0x29 at a time, so there's no collision to resolve. It costs one GPIO per sensor and about ten lines of setup code.
What's the difference between the VL53L0X and the VL53L1X?
Range, mostly. The VL53L0X reaches about 2 metres; the VL53L1X roughly 4, and it adds a programmable region of interest so you can narrow its field of view in software. Both sit at 0x29 and both need the same XSHUT dance for multiples. Our i2c tool can tell them apart from their model-ID register, since the boards look identical.
Is the VL53L0X affected by ambient light or surface colour?
Both, yes. It's a time-of-flight laser, so it measures the actual flight time of a 940 nm pulse — that makes it far better than ultrasonic at narrow targets and soft materials, but bright sunlight adds infrared noise and cuts usable range, and a matte black surface reflects little enough to shorten it too. Glass and mirrors are worse: the beam passes through or bounces sideways, and you get a reading from whatever's behind.
Why does my VL53L0X read 8190 or a huge number?
That's the out-of-range signal, not a distance. The library reports a very large value (commonly 8190/8191 mm) when the sensor gets no usable return — nothing in range, a surface too dark, too much ambient IR, or the target is glass. Check the range status rather than treating the number as a measurement, or you'll log a wall 8 metres away that isn't there.
Do I need to level-shift a VL53L0X on a 5 V Arduino?
Check your specific breakout. The bare chip is 2.8 V and most modules include a regulator and level shifting so they tolerate 3.3 V or 5 V — that's why our dataset lists them at 3.3-5 V. But bare or minimal boards exist, and driving a 2.8 V part from a 5 V Uno kills it. If the module has no regulator visible, level-shift.