# SPDX-License-Identifier: MIT
"""
ST VL53L1X laser time-of-flight distance sensor.
Successor to the VL53L0X with 4 m range (vs 2 m on the L0X) and a
different register map. I2C default address 0x29 (same as L0X — XSHUT
strapping needed if both share a bus).
The chip-ID lives at 16-bit register 0x010F and reads back 0xEACC for
a VL53L1X (or 0xEBAA for VL53L4CD, an L1X-pin-compatible variant).
Like the L0X driver this module ships the minimum register sequence
to get usable single-shot ranging on a power-on-default chip. ST's
reference API does an extensive calibration / VHV / SPAD-array
selection pass for tuned operation; that lives behind a separate
``calibrate()`` method we'll add when we have hardware to validate
against.
Measurement cycle:
1. Write 0x40 to ``SYSTEM_INTERRUPT_CLEAR`` (16-bit reg 0x0086).
2. Write 0x40 to ``SYSTEM_MODE_START`` (16-bit reg 0x0087) — kicks
a single-shot ranging.
3. Poll ``GPIO_TIO_HV_STATUS`` (16-bit reg 0x0031) bit 0 until clear.
4. Read ``RESULT_FINAL_CROSSTALK_CORRECTED_RANGE_MM_SD0``
(16-bit reg 0x0096) as a 16-bit big-endian value — distance in mm.
5. Write 0x01 to ``SYSTEM_INTERRUPT_CLEAR`` (16-bit reg 0x0086).
VL53L1X registers are 16-bit (vs L0X's 8-bit) — addresses go through
two byte-swaps on the I2C wire. The driver wraps that in
``_read_u8_16`` / ``_write_u8_16`` helpers.
"""
import time
from openbricks.distance import DistanceSensor
_DEFAULT_ADDR = 0x29
# 16-bit register addresses.
_REG_IDENTIFICATION_MODEL_ID = 0x010F
_REG_SYSTEM_INTERRUPT_CLEAR = 0x0086
_REG_SYSTEM_MODE_START = 0x0087
_REG_GPIO_TIO_HV_STATUS = 0x0031
_REG_RESULT_FINAL_RANGE_MM = 0x0096
_EXPECTED_MODEL_ID = 0xEACC # VL53L1X
_EXPECTED_MODEL_ID_ALT = 0xEBAA # VL53L4CD pin-compatible variant
[docs]
class VL53L1X(DistanceSensor):
"""ST VL53L1X laser time-of-flight distance sensor (4 m range).
Args:
i2c: a ``machine.I2C`` (or compatible) instance.
address: 7-bit I2C address (default 0x29).
timeout_ms: how long to poll for a measurement to finish.
Default 200 ms; the chip is typically done in ~50 ms.
"""
def __init__(self, i2c, address=_DEFAULT_ADDR, timeout_ms=200):
self._i2c = i2c
self._addr = address
self._timeout_ms = int(timeout_ms)
chip_id = self._read_u16_16(_REG_IDENTIFICATION_MODEL_ID)
if chip_id not in (_EXPECTED_MODEL_ID, _EXPECTED_MODEL_ID_ALT):
raise OSError(
"VL53L1X not found at 0x%02x (id 0x%04x)" %
(address, chip_id))
[docs]
def distance_mm(self):
# Clear any leftover interrupt + start a single-shot range.
self._write_u8_16(_REG_SYSTEM_INTERRUPT_CLEAR, 0x01)
self._write_u8_16(_REG_SYSTEM_MODE_START, 0x40)
# Poll until measurement done (GPIO_TIO_HV_STATUS bit 0
# CLEAR — opposite polarity from the L0X).
polled_ms = 0
while polled_ms < self._timeout_ms:
status = self._read_u8_16(_REG_GPIO_TIO_HV_STATUS)
if (status & 0x01) == 0:
break
time.sleep_ms(2)
polled_ms += 2
else:
return -1
# Read final-range register (16-bit big-endian distance in mm).
mm = self._read_u16_16(_REG_RESULT_FINAL_RANGE_MM)
# Clear the data-ready interrupt for the next cycle.
self._write_u8_16(_REG_SYSTEM_INTERRUPT_CLEAR, 0x01)
if mm <= 0:
return -1
return mm
# ---- low-level I2C helpers (16-bit register addresses) ----
def _read_u8_16(self, reg):
# 16-bit reg → two-byte address pushed MSB-first; one-byte read.
self._i2c.writeto(self._addr, bytes([(reg >> 8) & 0xFF, reg & 0xFF]))
buf = self._i2c.readfrom(self._addr, 1)
return buf[0] if buf else 0
def _read_u16_16(self, reg):
self._i2c.writeto(self._addr, bytes([(reg >> 8) & 0xFF, reg & 0xFF]))
buf = self._i2c.readfrom(self._addr, 2)
if len(buf) < 2:
return 0
return (buf[0] << 8) | buf[1]
def _write_u8_16(self, reg, value):
self._i2c.writeto(self._addr, bytes(
[(reg >> 8) & 0xFF, reg & 0xFF, value & 0xFF]))