Skip to content

VanBruce/vl53l9cx-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vl53l9cx-python

Pure-Python I2C driver library for the STMicroelectronics VL53L9CX (the first VL53L9-family part) — the 54×42-zone (2.3k-zone) 3D direct-time-of-flight lidar module, released mid-2026. No C compilation, no kernel module: smbus2 transactions and a ~10 KB firmware blob (packaged, BSD-3-Clause).

Runs on CPython 3.9+ on any Linux host with an I2C bus — developed and validated on Raspberry Pi 5, equally applicable to Pi 4/Zero and other SBCs. Not MicroPython (the I2C transport and firmware loader would need porting to machine.I2C; the register map and sequences here would carry over). Usage examples in examples/.

Hardware-validated on the STEVAL-VL53L9 evaluation board + Raspberry Pi 5 (Bookworm): full-resolution depth/amplitude/ambient frames at 0.3–6 m indoors, ~95–99% zone validity. Written against datasheet DS14879 rev 6 and ST's STM32Cube 53L9A1 BSP (core 1.0.0, fw patch 0.17).

54x42 depth portrait fixed near-scale render
Head-and-shoulders at ~55 cm, 54×42 zones, log scale fitted to the subject (48–95 cm) The same frame at the standard fixed 5–80 cm _depth_near.png scale

Real capture via examples/capture.py — red = near, blue = far, black = no return. Every frame's raw depth/amplitude/ambient CSVs are saved, so any capture can be re-rendered at any scale after the fact. (Fun 940 nm facts visible here: hair ranges solidly — melanin barely absorbs in NIR — while eyeglasses are completely invisible: lenses transmit, frames are sub-zone.)

Quickstart

pip install "vl53l9cx[examples]"      # or: pip install .  from a clone
python examples/capture.py --bus 1 --xshut-gpio 22 --intr-gpio 17
from vl53l9cx import VL53L9CX, default_firmware

dev = VL53L9CX(bus=1)          # xshut=/wait_intr= callables optional
dev.power_on()                 # XSHUT must be high (see hardware notes!)
dev.load_firmware(default_firmware())   # required after every power-up
dev.boot()
dev.configure(resolution="54x42")       # 24x20, 18x14, 12x10, 8x6, 4x4
dev.start()
frame = dev.capture()          # trigger + wait + read (manual sync)
print(frame.depth_mm[21][27], "mm at the center zone")
dev.stop()

Frame exposes depth_mm, depth_flag (1 = valid measurement), amplitude, ambient, dss as (rows, cols) arrays (numpy if installed), plus frame_counter and the raw 100-byte status line. Arrays are un-flipped to scene orientation (the module's lens inverts optically; verified upright against a real scene).

Hardware notes — read before wiring (each of these cost us bench time)

  1. This is a 1.2/1.8 V-IO part. 3.3 V on any pin can kill it. IOVDD absolute max is 1.98 V, and every digital pin — SDA, SCL, XSHUT, INTR, SYNC_IN, AP_CLK — lives in that domain. Level-shift everything when driving from a Pi. (The STEVAL-VL53L9 board has PI4ULS3V204 shifters on-board; check its Host-IO jumper is on 3.3 V.)

  2. No repeated-start reads — ever. The device does not support a repeated start between the register-index write and the data read (datasheet "known limitations"). Worse than not working: it latches the device into NAK-everything until a clean STOP escapes it, which looks exactly like a dead sensor. This driver does START/WRITE/STOP + START/READ/STOP; if you port it, keep that. Related: plain i2cdetect probes some address ranges with empty START+STOP transactions, which the device also doesn't support — a bus scan can wedge it. Probe with i2cdetect -y -r 1 0x29 0x29 (read-byte mode) instead.

  3. No clock, no ACK. The sensor needs AP_CLK (6–27 MHz at IOVDD level, ±100 ppm) running before it will even acknowledge its I2C address. A missing/disabled clock looks like a wiring fault. STEVAL-VL53L9 owners: the board ships strapped for host-supplied AP_CLK — the onboard 12 MHz oscillator is disabled by R25 (its OE strap, active-high enable) and the header AP_CLK routes in through a level shifter via R23. To use the onboard oscillator: remove R25 and remove R23. Leaving R23 in with the header pin floating feeds the sensor auto-direction-shifter noise instead of a clock — the sensor then boots occasionally, which is maximally confusing.

  4. XSHUT must be actively driven high (no internal pull-up), and the device NAKs for some milliseconds after the rising edge while its ROM boots — poll through the NAKs (this driver does). Every XSHUT cycle requires a full firmware reload (~10 KB, ~0.1–0.3 s). If your host GPIO framework releases lines on process exit (gpiod does), XSHUT drops and the sensor leaves the bus — that's expected, not a crash. On a Pi, pinctrl set <N> op dh holds it across processes for manual poking.

  5. Don't leave SYNC_IN floating — no internal pull; idle it high (low pulses trigger frames in external-sync mode). Floating inputs behind auto-direction level shifters chatter.

  6. Depth word format: bit 15 of each 16-bit depth value is a valid flag (1 = real measurement) — not documented in the datasheet, confirmed on hardware; the remaining 15 bits are millimeters. This driver splits them into depth_mm/depth_flag.

  7. Bus speed and sharing: I2C up to 1 MHz Fast-mode+. A full 54×42 frame is ~14.9 KB ≈ 150–170 ms at 1 MHz, ~400 ms at 400 kHz. Reads are chunked (default 2 KB) so other devices on the bus interleave between chunks. Binned resolutions are proportionally faster (18×14 ≈ 1.7 KB).

  8. Address change ordering matters: when re-addressing (default 0x29, shared by every ST FlightSense part), the dynamic-address flag must be read before writing the new address — once dynamic addressing is active the device moves the instant the address register is written. set_i2c_address() does this correctly.

What's implemented

  • Full boot flow: XSHUT sequencing, ext-clock/rail config, firmware patch upload, boot command, patch-revision check (strict by default).
  • configure(): resolution/binning (with the device-side crop the square-transmit modes need), short/long ranging context, exposure (1–30 ms), power mode, manual/autonomous/slave sync.
  • Manual-sync capture() (trigger → INTR or polled FRAME_READY → 3-part frame read with DSS LUT mapping → ACK) and autonomous streaming (start()/stop() + wait_frame()/read_frame()).
  • Warm-device recover() (adopt an already-booted sensor without XSHUT control or firmware reload).
  • I2C re-addressing for multi-sensor buses.
  • GPIO-agnostic: pass callables for XSHUT and INTR-wait (gpiod v2 example glue in examples/capture.py); INTR is active-low (falls on frame-ready, released by the read ACK).

Not implemented: MIPI CSI-2 / I3C output paths, ST's post-processing pipeline (confidence/reflectance maps, TNR), on-device calibration routines. Raw depth/amplitude/ambient/DSS is what you get.

Status & provenance

  • Validated against fw patch 0.17 (packaged; boot() fails loudly on a mismatch — pass strict_patch=False to experiment with newer ULD blobs at your own risk).
  • The firmware blob is © STMicroelectronics, redistributed under BSD-3-Clause; extraction provenance in vl53l9cx/FW_PROVENANCE.txt.
  • Register map and sequences ported from ST's BSP; the datasheet alone is not sufficient to drive this part (no register addresses are published in it).
  • Provided as-is; tested on one board, one host, one firmware revision. Issues and PRs welcome, response time not guaranteed.

License: BSD-3-Clause (see LICENSE).

About

Pure-Python I2C driver for the ST VL53L9 / VL53L9CX 3D dToF lidar (54x42 zones) — Raspberry Pi ready, hardware-validated

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages