Home

Awesome

VL53L5CX MicroPython and CircuitPython Package

This package provides a class that is (mostly) a straight port of the ST Ultra Lite Driver (ULD) C module. With this class, you can configure and acquire data from a VL53L5CX sensor in both 4x4 and 8x8 resolutions. You can find details on the sensor operation and API in the ST user manual UM2884. The figure below shows a graphical display of a color depth map for an 8x8 resolution sample, demo.py.

Range as colors demo

The code has been tested with the VL53L5CX breakout board from Pesky Products on Tindie using a Pimoroni Tiny 2040 with both MicroPython (v1.16) and CircuitPython (v7.0) and a LILYGO TTGO T-Display using MicroPython (v1.17).

Installation

Copy the vl53l5cx directory to your board's lib directory using the usual mechanism for your board. The firmware file, vl_fw_config.bin, is generated from the ULD source file vl53lcx_buffers.h using the fw_file.c utility.

MicroPython

mpremote mkdir :lib/vl53l5cx
mpremote cp vl53l5cx/__init__.py :lib/vl53l5cx/__init__.py
mpremote cp vl53l5cx/_config_file.py :lib/vl53l5cx/_config_file.py
mpremote cp vl53l5cx/mp.py :lib/vl53l5cx/mp.py
mpremote cp vl53l5cx/vl_fw_config.bin :lib/vl53l5cx/vl_fw_config.bin

You could also compile the source files first using mpy-cross and then copy the corresponding .mpy files.

CircuitPython

cp -r vl53l5cx path-to-cp-disk/lib

There are no external dependencies for the vl53l5cx package. Some examples may have external requirements.

Usage

The VL53L5CX class requires at least an I2C device to interact with the sensor. In addition, depending on your HW (board), you may require a Pin for the LPN pin. If your board pulls this pin low, you will need to connect a Pin to pull the LPN pin high. In addition, this Pin can be supplied to the VL53L5CX class to perform a soft reset.

The interface for the class is the same for both MicroPython and CircuitPython, but the objects used to create an instance are different. The sensor.py utility module encapsulates the differences for each platform and keeps from duplication in the examples. You can use this as-is or just copy the code for your platform into your applications.

MicroPython Create

from machine import I2C, Pin

from vl53l5cx.mp import VL53L5CXMP

scl_pin, sda_pin, lpn_pin, _ = (22, 21, 12, 13)
i2c = I2C(0, scl=Pin(scl_pin, Pin.OUT), sda=Pin(sda_pin), freq=1_000_000)

tof = VL53L5CXMP(i2c, lpn=Pin(lpn_pin, Pin.OUT, value=1))

CircuitPython Create

import busio
from microcontroller import pin
from digitalio import DigitalInOut, Direction

from vl53l5cx.cp import VL53L5CXCP

scl_pin, sda_pin, lpn_pin, _ = (pin.GPIO1, pin.GPIO0, pin.GPIO28, pin.GPIO5)
i2c = busio.I2C(scl_pin, sda_pin, frequency=1_000_000)

lpn = DigitalInOut(lpn_pin)
lpn.direction = Direction.OUTPUT
lpn.value = True

tof = VL53L5CXCP(i2c, lpn=lpn)

You will need to adjust the pin numbers/ids based on your board's connections to the VL53L5CX sensor.

Basic Example

Once you have an instance, the code to interact with the sensor is the same, so the examples below do not show the creation code. Instead, this code is factored out into a helper module, sensor.py, that provides a wrapper function, make_sensor.

from sensor import make_sensor

from vl53l5cx import DATA_TARGET_STATUS, DATA_DISTANCE_MM
from vl53l5cx import STATUS_VALID, RESOLUTION_4X4


def main():
    tof = make_sensor()
    tof.reset()

    if not tof.is_alive():
        raise ValueError("VL53L5CX not detected")

    tof.init()

    tof.resolution = RESOLUTION_4X4
    grid = 3

    tof.ranging_freq = 2

    tof.start_ranging({DATA_DISTANCE_MM, DATA_TARGET_STATUS})

    while True:
        if tof.check_data_ready():
            results = tof.get_ranging_data()
            distance = results.distance_mm
            status = results.target_status

            for i, d in enumerate(distance):
                if status[i] == STATUS_VALID:
                    print("{:4}".format(d), end=" ")
                else:
                    print("xxxx", end=" ")

                if (i & grid) == grid:
                    print("")

            print("")


main()

Run the example using the method appropriate for your platform.

linux$ mpremote run basic.py
xxxx 1564  671 1754
xxxx 1537 1834 1770
xxxx 1572 1665 1736
xxxx xxxx xxxx 1727

xxxx xxxx  671 1754
xxxx 1530 1857 1765
xxxx 1546 1665 1751
xxxx xxxx xxxx 1703

xxxx xxxx  671 1743
xxxx 1566 1857 1717
xxxx 1560 1665 1744
xxxx 1543 1616 1712

Selecting Results

You can select all or a subset of the nine possible results produced by the sensor when starting operation via start_ranging(). The Results object returned from get_ranging_data() will contain a property for each of the specified results. Note that the single argument to start_ranging() is a set, list, or other iterable.

from vl53l5cx import DATA_TARGET_STATUS, DATA_DISTANCE_MM

tof.start_ranging({DATA_TARGET_STATUS, DATA_DISTANCE_MM})

results = tof.get_ranging_data()

print(len(results.distance_mm), len(results.target_status))
start_ranging() optionResults property
DATA_AMBIENT_PER_SPADambient_per_spad
DATA_NB_SPADS_ENABLEDnb_spads_enabled
DATA_NB_TARGET_DETECTEDnb_target_detected
DATA_SIGNAL_PER_SPADsignal_per_spad
DATA_RANGE_SIGMA_MMrange_sigma_mm
DATA_DISTANCE_MMdistance_mm
DATA_REFLECTANCEreflectance
DATA_TARGET_STATUStarget_status
DATA_MOTION_INDICATORmotion_indicator

Interrupt Example

Instead of polling for data available via the check_data_ready method, you can use the interrupt pin from the VL53L5CX to signal data is ready. The examples below show how this can be used with the light sleep functionality in both MicroPython and CircuitPython.

# MicroPython
from machine import lightsleep, Pin
from esp32 import wake_on_ext1, WAKEUP_ALL_LOW

from sensor import make_sensor

from vl53l5cx import DATA_TARGET_STATUS, DATA_DISTANCE_MM
from vl53l5cx import RESOLUTION_4X4


def main():
    wake_on_ext1([Pin(13, Pin.IN)], WAKEUP_ALL_LOW)

    tof = make_sensor()
    tof.reset()
    tof.init()

    tof.resolution = RESOLUTION_4X4
    tof.ranging_freq = 30

    tof.start_ranging({DATA_DISTANCE_MM, DATA_TARGET_STATUS})

    while True:
        lightsleep()

        results = tof.get_ranging_data()
        print(min(results.distance_mm), max(results.distance_mm))


main()
# CircuitPython
import alarm
from microcontroller import pin

from sensor import make_sensor

from vl53l5cx import DATA_TARGET_STATUS, DATA_DISTANCE_MM
from vl53l5cx import RESOLUTION_4X4


def main():
    pin_alarm = alarm.pin.PinAlarm(pin=pin.GPIO5, value=False, pull=True)

    tof = make_sensor()
    tof.reset()
    tof.init()

    tof.resolution = RESOLUTION_4X4
    tof.ranging_freq = 30

    tof.start_ranging({DATA_DISTANCE_MM, DATA_TARGET_STATUS})

    while True:
        alarm.light_sleep_until_alarms(pin_alarm)

        results = tof.get_ranging_data()
        print(min(results.distance_mm), max(results.distance_mm))


main()

Power Example

The VL53L5CX can be operated in a "sleep" (LP idle) mode where it consumes less power when not ranging. The data sheet lists the current consumption in Section 4.4.

Current consumption

The example below shows how to switch between "wakeup" (HP idle) and "sleep" (LP idle) using the power_mode property. Measured current consumption (including SoC and board usage) is shown in the graph.

from time import sleep

from sensor import make_sensor

from vl53l5cx import POWER_MODE_WAKEUP, POWER_MODE_SLEEP


def main():
    tof = make_sensor()
    tof.reset()
    tof.init()

    assert(tof.power_mode == POWER_MODE_WAKEUP)
    sleep(0.5)

    tof.power_mode = POWER_MODE_SLEEP
    assert(tof.power_mode == POWER_MODE_SLEEP)
    sleep(0.5)

    tof.power_mode = POWER_MODE_WAKEUP
    assert(tof.power_mode == POWER_MODE_WAKEUP)
    sleep(0.5)

    tof.power_mode = POWER_MODE_SLEEP
    assert(tof.power_mode == POWER_MODE_SLEEP)
    sleep(0.5)

    tof.power_mode = POWER_MODE_WAKEUP
    assert(tof.power_mode == POWER_MODE_WAKEUP)
    sleep(0.5)


main()

Measured current consumption

TODO