2026-04-07 10:42 PM - last edited on 2026-04-08 7:39 AM by Andrew Neil
raw ID and is_alive() work
Hello,
I am using a Chinese VL53L8CX module from AliExpress, not the official ST evaluation board. I want to stress that in the first sentence because this may be important: this is a Chinese AliExpress breakout, not the original ST hardware. The same module works on my setup over I2C, but I am trying to bring it up over SPI on an ESP32-S3 and I am stuck at a very specific point.
What already works:
- The same sensor/module works over I2C on this board.
- In SPI mode I can repeatedly read the hardware ID correctly: `dev=0xF0`, `rev=0x0C`.
- `vl53l8cx_is_alive()` often returns `status=0, alive=1`.
What fails:
- `vl53l8cx_init()` never completes successfully.
- The failure happens around the software reboot / boot polling stage.
My hardware/software context:
- ESP32-S3
- custom firmware using Arduino + ESP-IDF SPI path
- Chinese AliExpress VL53L8CX module
- `SPI_I2C_N` forced HIGH for SPI mode
- level-shifted module, jumper wires about 20 cm
- SPI mode 3
- SPI clock currently conservative, around 1.2 MHz
This is the reset / startup sequence I currently use on the adapter side:
static void prepareSensorSpiPins(bool csAsserted) {
digitalWrite(SENSOR_SPI_I2C_N, HIGH);
pinMode(SENSOR_SPI_I2C_N, OUTPUT);
digitalWrite(SENSOR_SPI_CS, csAsserted ? LOW : HIGH);
pinMode(SENSOR_SPI_CS, OUTPUT);
digitalWrite(SENSOR_SPI_CLK, VIPLUS_L8CX_SPI_IDLE_LEVEL);
pinMode(SENSOR_SPI_CLK, OUTPUT);
digitalWrite(SENSOR_SPI_MOSI, LOW);
pinMode(SENSOR_SPI_MOSI, OUTPUT);
pinMode(SENSOR_SPI_MISO, INPUT_PULLUP);
enableSensorSpiIdlePullups();
}
static void prepareSensorSpiSelectedPins() {
// keep CS asserted while the sensor latches interface selection
prepareSensorSpiPins(true);
}
static void performStStyleSpiReset() {
prepareSensorSpiSelectedPins();
pinMode(activeSensorLpnPin(), OUTPUT);
digitalWrite(activeSensorLpnPin(), LOW);
delay(kL8cxResetLowDelayMs); // 100 ms
digitalWrite(activeSensorLpnPin(), HIGH);
delay(kL8cxPostResetDelayMs); // 100 ms
prepareSensorSpiSelectedPins();
}Then the actual init sequence in my adapter is roughly this:
if (strategyUsesManualReset(strategy)) {
performStStyleSpiReset();
} else {
digitalWrite(activeSensorLpnPin(), HIGH);
delay(10);
}
if (strategyUsesManualReset(strategy)) {
prepareSensorSpiSelectedPins();
} else {
prepareSensorSpiIdlePins();
}
logSensorGpioLevels("pre-begin");
const auto raw_id = readRawId();
printf("[L8CX] raw id status=%u dev=0x%02x rev=0x%02x\n", ...);
uint8_t alive = 0;
uint8_t status = _sensor->is_alive(&alive);
printf("[L8CX] is_alive status=%u alive=%u\n", ...);
probeGo2Status(...); // reads reg06/reg07 before init
printf("[L8CX] uld init begin\n");
status = _sensor->init();
printf("[L8CX] uld init status=%u\n", ...);My SPI read path is also very close to the ST example: transmit 2-byte address first, then receive data while keeping CS active.
spi_transaction_t addr_t = {};
addr_t.length = 16;
addr_t.tx_buffer = addr_tx;
addr_t.flags = SPI_TRANS_CS_KEEP_ACTIVE;
spi_transaction_t data_t = {};
data_t.length = 0;
data_t.rxlength = size * 8;
data_t.rx_buffer = rx;
err = spi_device_acquire_bus(dev, portMAX_DELAY);
if (err == ESP_OK) {
err = spi_device_polling_transmit(dev, &addr_t);
if (err == ESP_OK) {
err = spi_device_polling_transmit(dev, &data_t);
}
spi_device_release_bus(dev);
}Inside `vl53l8cx_init()` the relevant sequence is this:
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x7fff, 0x00);
status |= VL53L8CX_RdByte(&(p_dev->platform), 0x06, &pre_boot_status);
status |= VL53L8CX_RdByte(&(p_dev->platform), 0x07, &pre_boot_status_07);
printf("[L8CX-ULD] init: pre-sw-reboot reg06=0x%02x reg07=0x%02x\n",
pre_boot_status, pre_boot_status_07);
if (pre_boot_status != 0x01) {
printf("[L8CX-ULD] init: sw reboot begin\n");
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x0009, 0x04);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000F, 0x40); // I also tested 0x42
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000A, 0x03);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000C, 0x01);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x010A, 0x01);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x4002, 0x01);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x4002, 0x00);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x010A, 0x03);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x0103, 0x01);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000C, 0x00);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000F, 0x43);
status |= VL53L8CX_WaitMs(&(p_dev->platform), 1);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000F, 0x40);
status |= VL53L8CX_WrByte(&(p_dev->platform), 0x000A, 0x01);
status |= VL53L8CX_WaitMs(&(p_dev->platform), 100); // I also tested 500 ms
}
status |= _vl53l8cx_poll_for_answer(p_dev, 1, 0, 0x06, 0xff, 1);Typical log in the “better” runs looks like this:
[L8CX] raw id status=0 dev=0xf0 rev=0x0c
[L8CX] is_alive status=0 alive=1
[L8CX] pre-init go2 status=0 reg06=0x00 reg07=0x00
[L8CX-ULD] init: pre-sw-reboot reg06=0x00 reg07=0x00
[L8CX-ULD] init: sw reboot begin
[L8CX-ULD] init: poll boot fail status=1
The most interesting alternative state I have ever seen is this:
[L8CX] pre-init go2 status=0 reg06=0x80 reg07=0x05
[L8CX-ULD] init: pre-sw-reboot reg06=0x80 reg07=0x05
But even in that case, after the software reboot sequence the sensor falls back and boot polling still fails.
I also had one weaker case with:
reg06=0x00 reg07=0x02
Questions for the community:
1. Do `reg06=0x80` and `reg07=0x05` mean anything documented for VL53L8CX boot ROM / SPI bring-up?
2. Is it expected that raw SPI ID and even `is_alive()` can work while the device is still not in a valid post-boot SPI state?
3. Is there any known SPI-specific variation of the software reboot sequence for VL53L8CX, especially around registers `0x000F`, `0x000A`, and `0x4002`?
4. Has anyone successfully used a Chinese/AliExpress VL53L8CX breakout over SPI with ULD, not the official ST hardware?
5. Could onboard level shifting or non-official board design explain a case where SPI raw ID works but `vl53l8cx_init()` never reaches boot-ready state?
If useful, I can also share full logs and a larger code excerpt.
Edited to apply source code formatting - please see How to insert source code for future reference.
Solved! Go to Solution.
2026-04-08 6:20 AM
Hi!
I looked into this forum topic more carefully, and it turns out my module uses the wrong level shifter.
This is a Chinese VL53L8CX breakout, and it appears to be populated with an unsuitable translator chip for this application. So the problem is most likely not in my firmware, but in the hardware design of the module itself.
That explains why I could get partial communication but never stable full initialization.
Thanks to everyone who pointed me in that direction.
2026-04-08 6:20 AM
Hi!
I looked into this forum topic more carefully, and it turns out my module uses the wrong level shifter.
This is a Chinese VL53L8CX breakout, and it appears to be populated with an unsuitable translator chip for this application. So the problem is most likely not in my firmware, but in the hardware design of the module itself.
That explains why I could get partial communication but never stable full initialization.
Thanks to everyone who pointed me in that direction.
2026-04-08 8:58 PM
Hi
Glad to hear you have found the root cause. Congratulations~