VL53L8CX SPI on ESP32-S3: vl53l8cx_init() always fails on a Chinese AliExpress module
- April 8, 2026
- 2 replies
- 220 views
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.
