# SPDX-FileCopyrightText: 2023 Andre Müller # SPDX-License-Identifier: MIT """ `adafruit_vl6180x` ==================================================== CircuitPython module for the VL6180X distance sensor. See examples/simpletest.py for a demo of the usage. * Author(s): Tony DiCola, Jonas Schatz Implementation Notes -------------------- **Hardware:** * Adafruit `VL6180X Time of Flight Distance Ranging Sensor (VL6180) `_ (Product ID: 3316) **Software and Dependencies:** * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: https://github.com/adafruit/circuitpython/releases * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice **Update for Micropython:** This module was updated and works with ESP32 and Micropython 1.20+ **Bugs** read_lux does not work with my VL6180X distance sensor. I guess it's a bug. """ import struct import time from micropython import const # Registers _VL6180X_REG_IDENTIFICATION_MODEL_ID = const(0x000) _VL6180X_REG_SYSTEM_HISTORY_CTRL = const(0x012) _VL6180X_REG_SYSTEM_INTERRUPT_CONFIG = const(0x014) _VL6180X_REG_SYSTEM_INTERRUPT_CLEAR = const(0x015) _VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET = const(0x016) _VL6180X_REG_SYSRANGE_START = const(0x018) _VL6180X_REG_SYSRANGE_INTERMEASUREMENT_PERIOD = const(0x01B) _VL6180X_REG_SYSRANGE_PART_TO_PART_RANGE_OFFSET = const(0x024) _VL6180X_REG_SYSALS_START = const(0x038) _VL6180X_REG_SYSALS_ANALOGUE_GAIN = const(0x03F) _VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI = const(0x040) _VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO = const(0x041) _VL6180X_REG_RESULT_RANGE_STATUS = const(0x04D) _VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO = const(0x04F) _VL6180X_REG_RESULT_ALS_VAL = const(0x050) _VL6180X_REG_RESULT_HISTORY_BUFFER_0 = const(0x052) _VL6180X_REG_RESULT_RANGE_VAL = const(0x062) # Internal constants: _VL6180X_DEFAULT_I2C_ADDR = const(0x29) # User-facing constants: ALS_GAIN_1 = const(0x06) ALS_GAIN_1_25 = const(0x05) ALS_GAIN_1_67 = const(0x04) ALS_GAIN_2_5 = const(0x03) ALS_GAIN_5 = const(0x02) ALS_GAIN_10 = const(0x01) ALS_GAIN_20 = const(0x00) ALS_GAIN_40 = const(0x07) ERROR_NONE = const(0) ERROR_SYSERR_1 = const(1) ERROR_SYSERR_5 = const(5) ERROR_ECEFAIL = const(6) ERROR_NOCONVERGE = const(7) ERROR_RANGEIGNORE = const(8) ERROR_SNR = const(11) ERROR_RAWUFLOW = const(12) ERROR_RAWOFLOW = const(13) ERROR_RANGEUFLOW = const(14) ERROR_RANGEOFLOW = const(15) class VL6180X: """ Create an instance of the VL6180X distance sensor. You must pass in the following parameters: :param ~I2C i2c: An instance of the I2C bus connected to the sensor. Optionally you can specify: :param int address: The I2C address of the sensor. If not specified the sensor's default value will be assumed. :param int offset: The offset to be applied to measurements, in mm """ def __init__( self, i2c: I2C, address: int = _VL6180X_DEFAULT_I2C_ADDR, offset: int = 0 ) -> None: self._device = i2c self._address = address if self._read_8(_VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4: raise RuntimeError("Could not find VL6180X, is it connected and powered?") self._load_settings() self._write_8(_VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00) self.offset = offset # Reset a sensor that crashed while in continuous mode if self.continuous_mode_enabled: self.stop_range_continuous() time.sleep(0.1) # Activate history buffer for range measurement self._write_8(_VL6180X_REG_SYSTEM_HISTORY_CTRL, 0x01) @property def range(self) -> int: """ Read the range of an object in front of sensor and return it in mm. """ if self.continuous_mode_enabled: return self._read_range_continuous() return self._read_range_single() @property def range_from_history(self) -> Optional[int]: """ Read the latest range data from history To do so, you don't have to wait for a complete measurement. """ if not self.range_history_enabled: return None return self._read_8(_VL6180X_REG_RESULT_HISTORY_BUFFER_0) @property def ranges_from_history(self) -> Optional[List[int]]: """ Read the last 16 range measurements from history """ if not self.range_history_enabled: return None return [ self._read_8(_VL6180X_REG_RESULT_HISTORY_BUFFER_0 + age) for age in range(16) ] @property def range_history_enabled(self) -> bool: """ Checks if history buffer stores range data """ history_ctrl: int = self._read_8(_VL6180X_REG_SYSTEM_HISTORY_CTRL) if history_ctrl & 0x0: print("History buffering not enabled") return False if (history_ctrl > 1) & 0x1: print("History buffer stores ALS data, not range") return False return True def start_range_continuous(self, period: int = 100) -> None: """ Start continuous range mode :param int period: Time delay between measurements, in milliseconds; the value you will be floored to the nearest 10 milliseconds (setting to 157 ms sets it to 150 ms). Range is 10 - 2550 ms. """ # Set range between measurements if not 10 <= period <= 2550: raise ValueError( "Delay must be in 10 millisecond increments between 10 and 2550 milliseconds" ) period_reg = (period // 10) - 1 self._write_8(_VL6180X_REG_SYSRANGE_INTERMEASUREMENT_PERIOD, period_reg) # Start continuous range measurement self._write_8(_VL6180X_REG_SYSRANGE_START, 0x03) def stop_range_continuous(self) -> None: """ Stop continuous range mode. It is advised to wait for about 0.3s afterwards to avoid issues with the interrupt flags """ if self.continuous_mode_enabled: self._write_8(_VL6180X_REG_SYSRANGE_START, 0x01) @property def continuous_mode_enabled(self) -> bool: """ Checks if continuous mode is enabled """ return self._read_8(_VL6180X_REG_SYSRANGE_START) > 1 & 0x1 @property def offset(self) -> int: """ Read and sets the manual offset for the sensor, in millimeters """ return self._offset @offset.setter def offset(self, offset: int) -> None: self._write_8( _VL6180X_REG_SYSRANGE_PART_TO_PART_RANGE_OFFSET, struct.pack("b", offset)[0] ) self._offset = offset def _read_range_single(self) -> int: """ Read the range when in single-shot mode """ while not self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) & 0x01: pass self._write_8(_VL6180X_REG_SYSRANGE_START, 0x01) return self._read_range_continuous() def _read_range_continuous(self) -> int: """ Read the range when in continuous mode """ # Poll until bit 2 is set while not self._read_8(_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04: pass # read range in mm range_ = self._read_8(_VL6180X_REG_RESULT_RANGE_VAL) # clear interrupt self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) return range_ def read_lux(self, gain: int) -> float: """ Read the lux (light value) from the sensor and return it. Must specify the gain value to use for the lux reading: ================= ===== Setting Value ================= ===== ``ALS_GAIN_1`` 1x ``ALS_GAIN_1_25`` 1.25x ``ALS_GAIN_1_67`` 1.67x ``ALS_GAIN_2_5`` 2.5x ``ALS_GAIN_5`` 5x ``ALS_GAIN_10`` 10x ``ALS_GAIN_20`` 20x ``ALS_GAIN_40`` 40x ================= ===== :param int gain: The gain value to use """ reg = self._read_8(_VL6180X_REG_SYSTEM_INTERRUPT_CONFIG) reg &= ~0x38 reg |= 0x4 << 3 # IRQ on ALS ready self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg) # 100 ms integration period self._write_8(_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0) self._write_8(_VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100) # analog gain gain = min(gain, ALS_GAIN_40) self._write_8(_VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain) # start ALS self._write_8(_VL6180X_REG_SYSALS_START, 0x1) # Poll until "New Sample Ready threshold event" is set while ( (self._read_8(_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7 ) != 4: pass # read lux! lux = self._read_16(_VL6180X_REG_RESULT_ALS_VAL) # clear interrupt self._write_8(_VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07) lux *= 0.32 # calibrated count/lux if gain == ALS_GAIN_1: pass elif gain == ALS_GAIN_1_25: lux /= 1.25 elif gain == ALS_GAIN_1_67: lux /= 1.67 elif gain == ALS_GAIN_2_5: lux /= 2.5 elif gain == ALS_GAIN_5: lux /= 5 elif gain == ALS_GAIN_10: lux /= 10 elif gain == ALS_GAIN_20: lux /= 20 elif gain == ALS_GAIN_40: lux /= 40 lux *= 100 lux /= 100 # integration time in ms return lux @property def range_status(self) -> int: """ Retrieve the status/error from a previous range read. This will return a constant value such as: ===================== ============================== Error Description ===================== ============================== ``ERROR_NONE`` No error ``ERROR_SYSERR_1`` System error 1 (see datasheet) ``ERROR_SYSERR_5`` System error 5 (see datasheet) ``ERROR_ECEFAIL`` ECE failure ``ERROR_NOCONVERGE`` No convergence ``ERROR_RANGEIGNORE`` Outside range ignored ``ERROR_SNR`` Too much noise ``ERROR_RAWUFLOW`` Raw value underflow ``ERROR_RAWOFLOW`` Raw value overflow ``ERROR_RANGEUFLOW`` Range underflow ``ERROR_RANGEOFLOW`` Range overflow ===================== ============================== """ return self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) >> 4 def _load_settings(self) -> None: """ private settings from page 24 of app note """ self._write_8(0x0207, 0x01) self._write_8(0x0208, 0x01) self._write_8(0x0096, 0x00) self._write_8(0x0097, 0xFD) self._write_8(0x00E3, 0x00) self._write_8(0x00E4, 0x04) self._write_8(0x00E5, 0x02) self._write_8(0x00E6, 0x01) self._write_8(0x00E7, 0x03) self._write_8(0x00F5, 0x02) self._write_8(0x00D9, 0x05) self._write_8(0x00DB, 0xCE) self._write_8(0x00DC, 0x03) self._write_8(0x00DD, 0xF8) self._write_8(0x009F, 0x00) self._write_8(0x00A3, 0x3C) self._write_8(0x00B7, 0x00) self._write_8(0x00BB, 0x3C) self._write_8(0x00B2, 0x09) self._write_8(0x00CA, 0x09) self._write_8(0x0198, 0x01) self._write_8(0x01B0, 0x17) self._write_8(0x01AD, 0x00) self._write_8(0x00FF, 0x05) self._write_8(0x0100, 0x05) self._write_8(0x0199, 0x05) self._write_8(0x01A6, 0x1B) self._write_8(0x01AC, 0x3E) self._write_8(0x01A7, 0x1F) self._write_8(0x0030, 0x00) # Recommended : Public registers - See data sheet for more detail self._write_8(0x0011, 0x10) # Enables polling for 'New Sample ready' # when measurement completes self._write_8(0x010A, 0x30) # Set the averaging sample period # (compromise between lower noise and # increased execution time) self._write_8(0x003F, 0x46) # Sets the light and dark gain (upper # nibble). Dark gain should not be # changed. self._write_8(0x0031, 0xFF) # sets the # of range measurements after # which auto calibration of system is # performed self._write_8(0x0040, 0x63) # Set ALS integration time to 100ms self._write_8(0x002E, 0x01) # perform a single temperature calibration # of the ranging sensor # Optional: Public registers - See data sheet for more detail self._write_8(0x001B, 0x09) # Set default ranging inter-measurement # period to 100ms self._write_8(0x003E, 0x31) # Set default ALS inter-measurement period # to 500ms self._write_8(0x0014, 0x24) # Configures interrupt on 'New Sample # Ready threshold event' def _write_register(self, register: int) -> None: """ Write 0 byte of data from the specified 16-bit register address. """ self._device.writeto( self._address, struct.pack(">H", register), ) def _write_8(self, register: int, data: int) -> None: """ Write 1 byte of data from the specified 16-bit register address. """ self._device.writeto( self._address, struct.pack(">HB", register, data), ) def _write_16(self, register: int, data: int) -> None: """ Write a 16-bit big endian value to the specified 16-bit register address. """ self._device.writeto( self._address, struct.pack(">HH", register, data), ) def _read_8(self, register: int) -> int: """ Read and return a byte from the specified 16-bit register address. """ result = bytearray(1) self._write_register(register) self._device.readfrom_into(self._address, result) return result[0] def _read_16(self, register: int) -> int: """ Read and return a 16-bit unsigned big endian value read from the specified 16-bit register address. """ result = bytearray(2) self._write_register(register) self._device.readfrom_into(self._address, result) return struct.unpack_from(">H", result)[0] if __name__ == "__main__": from machine import I2C, Pin, PWM led = PWM(Pin(13), duty_u16=0, freq=300) i2c = I2C(0, freq=400_000) sensor = VL6180X(i2c) # sensor.offset = 20 sensor.start_range_continuous(50) while True: if (srange := sensor.range) != 255: led.duty_u16(int((255 - srange) * 65535 / 255)) # time.sleep_ms(500) print(srange, "mm") else: led.duty_u16(0)