LiDAR ranging · first working bring-up
First Bring-Up
Getting a real 8×8 distance frame out of the VL53L8CX on the nRF9151, sharing an I²C bus with the Grid-EYE. Two software bugs, a bus signal-integrity fix, and one real concurrency bug stood between wiring it up and a clean frame — the concurrency bug was the one that mattered.
- Part
- ST VL53L8CX, 8×8 zones, SATEL-VL53L8 breakout
- Device
- cali.EGG68 (nRF9151), shared I²C bus with Grid-EYE
- Driver
- ST ULD API, own Zephyr I²C platform port
- Result
cal tof init→cal tof raw, clean 8×8 frame
Summary
cal tof init then cal tof raw now run cleanly on
real hardware — firmware upload, resolution set, ranging start, and a
physically coherent 8×8 distance frame. Two real software bugs got in
the way (a missing device address, a driver buffer overflow), a bus
signal-integrity problem needed a hardware fix, and the failure that actually
took longest to find was a missing lock: nothing stopped the Grid-EYE and the
LiDAR from touching the shared bus at the same time.
Two software bugs
Both found by methodical isolation — a second, never-stressed board failed identically, which is what pointed at software rather than a damaged part.
- Missing device address.
lidar_is_alive()never set the platform's device address before calling into the ULD driver — that field is only set insidelidar_init(), and the device struct is a static global, zero-initialized at boot. Calling the standalone liveness check beforeinithad run silently addressed the I²C general-call address instead of the sensor, producing a NACK indistinguishable from a dead chip. - Driver write-buffer overflow. The ULD driver's multi-byte register write used a two-message I²C transfer with no stop between them, which hit the nRF TWIM driver's internal concatenation-buffer limit — 16 bytes by default, against write chunks up to 512 bytes. The driver's own error named the exact devicetree property at fault. Fixed by building the combined write buffer locally and sending it as one plain write instead of relying on driver-level message concatenation.
Bus signal integrity
Connecting the sensor to the shared bus produced a sustained ~14 MHz oscillation on both SDA and SCL, swinging well past the 1.8 V bus rail. The board's onboard level translator uses edge-triggered, auto-direction sensing — a known bad combination with an already RC-limited, weak-pullup open-drain bus, and the scope showed exactly that: SCL charging on a slow RC curve rather than switching cleanly. A 100 Ω series resistor between the shared bus and the translator settled it completely, with negligible voltage drop at these pullup values.
The real cause of the intermittent failures
The interesting bug wasn't electrical. Even with clean wiring, ranging failed intermittently in a way that survived a full reinit and a power cycle — both rule out corrupted device state, so the cause had to be elsewhere. It was a missing bus-arbitration lock: the Grid-EYE's periodic capture (main thread, on a wall-clock timer) and the LiDAR's own multi-step ranging sequence (shell thread) could interleave, because Zephyr only serializes individual low-level I²C transfers, not each device's operation as a whole. A capture landing mid-sequence in the LiDAR's start/wait/read/stop chain was enough to break it.
Fixed with one shared mutex, held for each device's entire I²C-touching operation rather than just around each transfer. Validated by computing the next collision window against the Grid-EYE's own schedule and firing continuous ranging calls straight through it: 50 of 50 succeeded, including samples landing right at the collision point itself. The general lesson carries forward: sharing a bus needs a whole-operation lock, not just per-transfer safety — the same requirement will apply when the SPI bus picks up a second device.
Method & caveats
- Zone row/column orientation versus the sensor's physical field of view is not yet confirmed — unlike Grid-EYE, where it was explicitly checked. The figure above labels the raw ULD zone index (row = index ÷ 8, col = index mod 8), not a verified up/down/left/right mapping. Deliberately parked, not forgotten.
target_statusread a uniform 6 across all 64 zones. The ULD header comments name 5 and 9 as “OK”, not 6 — not yet resolved against the fuller status-code table, though the data itself is too clean and spatially coherent to suggest bad targets.- The two-panel figure style (heatmap + 3D block plot) is now the standard for any LiDAR frame on this project — picked deliberately, not by default: the heatmap is the precise per-zone read, the block plot is the “what does the scene look like” intuition panel, and a real side-by-side check on this exact frame showed neither one alone is enough (the block plot's flat regions aren't individually readable; the heatmap alone doesn't give the geometric “step down to the shelf” read at a glance).
Bring-up data and figure-generation scripts held in the device project
repository (coap_GridEye/lidar_bringup/), not published here.
Full narrative, including the false leads ruled out along the way, in the
project's Journal.