""" MicroPython for Grove Time Of Flight VL53L0X sensor (I2C). https://github.com/vittascience/stm32-libraries https://wiki.seeedstudio.com/Grove-Time_of_Flight_Distance_Sensor-VL53L0X/ MIT License Copyright (c) 2020 leomlr (Léo Meillier) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/Vittascience/stm32-libraries" from micropython import const import utime import math _VL53L0X_IIC_ADDR = const(0x29) # Configuration constants: _SYSRANGE_START = const(0x00) _SYSTEM_THRESH_HIGH = const(0x0C) _SYSTEM_THRESH_LOW = const(0x0E) _SYSTEM_SEQUENCE_CONFIG = const(0x01) _SYSTEM_RANGE_CONFIG = const(0x09) _SYSTEM_INTERMEASUREMENT_PERIOD = const(0x04) _SYSTEM_INTERRUPT_CONFIG_GPIO = const(0x0A) _GPIO_HV_MUX_ACTIVE_HIGH = const(0x84) _SYSTEM_INTERRUPT_CLEAR = const(0x0B) _RESULT_INTERRUPT_STATUS = const(0x13) _RESULT_RANGE_STATUS = const(0x14) _RESULT_CORE_AMBIENT_WINDOW_EVENTS_RTN = const(0xBC) _RESULT_CORE_RANGING_TOTAL_EVENTS_RTN = const(0xC0) _RESULT_CORE_AMBIENT_WINDOW_EVENTS_REF = const(0xD0) _RESULT_CORE_RANGING_TOTAL_EVENTS_REF = const(0xD4) _RESULT_PEAK_SIGNAL_RATE_REF = const(0xB6) _ALGO_PART_TO_PART_RANGE_OFFSET_MM = const(0x28) _I2C_SLAVE_DEVICE_ADDRESS = const(0x8A) _MSRC_CONFIG_CONTROL = const(0x60) _PRE_RANGE_CONFIG_MIN_SNR = const(0x27) _PRE_RANGE_CONFIG_VALID_PHASE_LOW = const(0x56) _PRE_RANGE_CONFIG_VALID_PHASE_HIGH = const(0x57) _PRE_RANGE_MIN_COUNT_RATE_RTN_LIMIT = const(0x64) _FINAL_RANGE_CONFIG_MIN_SNR = const(0x67) _FINAL_RANGE_CONFIG_VALID_PHASE_LOW = const(0x47) _FINAL_RANGE_CONFIG_VALID_PHASE_HIGH = const(0x48) _FINAL_RANGE_CONFIG_MIN_COUNT_RATE_RTN_LIMIT = const(0x44) _PRE_RANGE_CONFIG_SIGMA_THRESH_HI = const(0x61) _PRE_RANGE_CONFIG_SIGMA_THRESH_LO = const(0x62) _PRE_RANGE_CONFIG_VCSEL_PERIOD = const(0x50) _PRE_RANGE_CONFIG_TIMEOUT_MACROP_HI = const(0x51) _PRE_RANGE_CONFIG_TIMEOUT_MACROP_LO = const(0x52) _SYSTEM_HISTOGRAM_BIN = const(0x81) _HISTOGRAM_CONFIG_INITIAL_PHASE_SELECT = const(0x33) _HISTOGRAM_CONFIG_READOUT_CTRL = const(0x55) _FINAL_RANGE_CONFIG_VCSEL_PERIOD = const(0x70) _FINAL_RANGE_CONFIG_TIMEOUT_MACROP_HI = const(0x71) _FINAL_RANGE_CONFIG_TIMEOUT_MACROP_LO = const(0x72) _CROSSTALK_COMPENSATION_PEAK_RATE_MCPS = const(0x20) _MSRC_CONFIG_TIMEOUT_MACROP = const(0x46) _SOFT_RESET_GO2_SOFT_RESET_N = const(0xBF) _IDENTIFICATION_MODEL_ID = const(0xC0) _IDENTIFICATION_REVISION_ID = const(0xC2) _OSC_CALIBRATE_VAL = const(0xF8) _GLOBAL_CONFIG_VCSEL_WIDTH = const(0x32) _GLOBAL_CONFIG_SPAD_ENABLES_REF_0 = const(0xB0) _GLOBAL_CONFIG_SPAD_ENABLES_REF_1 = const(0xB1) _GLOBAL_CONFIG_SPAD_ENABLES_REF_2 = const(0xB2) _GLOBAL_CONFIG_SPAD_ENABLES_REF_3 = const(0xB3) _GLOBAL_CONFIG_SPAD_ENABLES_REF_4 = const(0xB4) _GLOBAL_CONFIG_SPAD_ENABLES_REF_5 = const(0xB5) _GLOBAL_CONFIG_REF_EN_START_SELECT = const(0xB6) _DYNAMIC_SPAD_NUM_REQUESTED_REF_SPAD = const(0x4E) _DYNAMIC_SPAD_REF_EN_START_OFFSET = const(0x4F) _POWER_MANAGEMENT_GO1_POWER_FORCE = const(0x80) _VHV_CONFIG_PAD_SCL_SDA__EXTSUP_HV = const(0x89) _ALGO_PHASECAL_LIM = const(0x30) _ALGO_PHASECAL_CONFIG_TIMEOUT = const(0x30) _VCSEL_PERIOD_PRE_RANGE = const(0) _VCSEL_PERIOD_FINAL_RANGE = const(1) def _decode_timeout(val): # format: "(LSByte * 2^MSByte) + 1" return float(val & 0xFF) * math.pow(2.0, ((val & 0xFF00) >> 8)) + 1 def _encode_timeout(timeout_mclks): # format: "(LSByte * 2^MSByte) + 1" timeout_mclks = int(timeout_mclks) & 0xFFFF ls_byte = 0 ms_byte = 0 if timeout_mclks > 0: ls_byte = timeout_mclks - 1 while ls_byte > 255: ls_byte >>= 1 ms_byte += 1 return ((ms_byte << 8) | (ls_byte & 0xFF)) & 0xFFFF return 0 def _timeout_mclks_to_microseconds(timeout_period_mclks, vcsel_period_pclks): macro_period_ns = ((2304 * (vcsel_period_pclks) * 1655) + 500) // 1000 return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns // 2)) // 1000 def _timeout_microseconds_to_mclks(timeout_period_us, vcsel_period_pclks): macro_period_ns = ((2304 * (vcsel_period_pclks) * 1655) + 500) // 1000 return ((timeout_period_us * 1000) + (macro_period_ns // 2)) // macro_period_ns class VL53L0X: """Driver for the VL53L0X distance sensor.""" def __init__(self, i2c, address=_VL53L0X_IIC_ADDR, io_timeout_s=0): # pylint: disable=too-many-statements self._i2c = i2c self._addr = address self.io_timeout_s = io_timeout_s # Check identification registers for expected values. # From section 3.2 of the datasheet. if ( self._read_u8(0xC0) is not 0xEE or self._read_u8(0xC1) is not 0xAA or self._read_u8(0xC2) is not 0x10 ): raise RuntimeError("Failed to find expected ID register values. Check wiring!") # Initialize access to the sensor. This is based on the logic from: # https://github.com/pololu/vl53l0x-arduino/blob/master/VL53L0X.cpp # Set I2C standard mode. for pair in ((0x88, 0x00), (0x80, 0x01), (0xFF, 0x01), (0x00, 0x00)): self._write_u8(pair[0], pair[1]) self._stop_variable = self._read_u8(0x91) for pair in ((0x00, 0x01), (0xFF, 0x00), (0x80, 0x00)): self._write_u8(pair[0], pair[1]) # disable SIGNAL_RATE_MSRC (bit 1) and SIGNAL_RATE_PRE_RANGE (bit 4) # limit checks config_control = self._read_u8(_MSRC_CONFIG_CONTROL) | 0x12 self._write_u8(_MSRC_CONFIG_CONTROL, config_control) # set final range signal rate limit to 0.25 MCPS (million counts per # second) self.signal_rate_limit = 0.25 self._write_u8(_SYSTEM_SEQUENCE_CONFIG, 0xFF) spad_count, spad_is_aperture = self._get_spad_info() # The SPAD map (RefGoodSpadMap) is read by # VL53L0X_get_info_from_device() in the API, but the same data seems to # be more easily readable from GLOBAL_CONFIG_SPAD_ENABLES_REF_0 through # _6, so read it from there. ref_spad_map = bytearray(1) ref_spad_map[0] = _GLOBAL_CONFIG_SPAD_ENABLES_REF_0 self._i2c.writeto(self._addr, ref_spad_map) buf = bytearray(6) self._i2c.readfrom_mem_into(self._addr, ref_spad_map[0], buf) ref_spad_map.extend(buf) for pair in ( (0xFF, 0x01), (_DYNAMIC_SPAD_REF_EN_START_OFFSET, 0x00), (_DYNAMIC_SPAD_NUM_REQUESTED_REF_SPAD, 0x2C), (0xFF, 0x00), (_GLOBAL_CONFIG_REF_EN_START_SELECT, 0xB4), ): self._write_u8(pair[0], pair[1]) first_spad_to_enable = 12 if spad_is_aperture else 0 spads_enabled = 0 for i in range(48): if i < first_spad_to_enable or spads_enabled == spad_count: # This bit is lower than the first one that should be enabled, # or (reference_spad_count) bits have already been enabled, so # zero this bit. ref_spad_map[1 + (i // 8)] &= ~(1 << (i % 8)) elif (ref_spad_map[1 + (i // 8)] >> (i % 8)) & 0x1 > 0: spads_enabled += 1 self._i2c.writeto(self._addr, ref_spad_map) for pair in ( (0xFF, 0x01), (0x00, 0x00), (0xFF, 0x00), (0x09, 0x00), (0x10, 0x00), (0x11, 0x00), (0x24, 0x01), (0x25, 0xFF), (0x75, 0x00), (0xFF, 0x01), (0x4E, 0x2C), (0x48, 0x00), (0x30, 0x20), (0xFF, 0x00), (0x30, 0x09), (0x54, 0x00), (0x31, 0x04), (0x32, 0x03), (0x40, 0x83), (0x46, 0x25), (0x60, 0x00), (0x27, 0x00), (0x50, 0x06), (0x51, 0x00), (0x52, 0x96), (0x56, 0x08), (0x57, 0x30), (0x61, 0x00), (0x62, 0x00), (0x64, 0x00), (0x65, 0x00), (0x66, 0xA0), (0xFF, 0x01), (0x22, 0x32), (0x47, 0x14), (0x49, 0xFF), (0x4A, 0x00), (0xFF, 0x00), (0x7A, 0x0A), (0x7B, 0x00), (0x78, 0x21), (0xFF, 0x01), (0x23, 0x34), (0x42, 0x00), (0x44, 0xFF), (0x45, 0x26), (0x46, 0x05), (0x40, 0x40), (0x0E, 0x06), (0x20, 0x1A), (0x43, 0x40), (0xFF, 0x00), (0x34, 0x03), (0x35, 0x44), (0xFF, 0x01), (0x31, 0x04), (0x4B, 0x09), (0x4C, 0x05), (0x4D, 0x04), (0xFF, 0x00), (0x44, 0x00), (0x45, 0x20), (0x47, 0x08), (0x48, 0x28), (0x67, 0x00), (0x70, 0x04), (0x71, 0x01), (0x72, 0xFE), (0x76, 0x00), (0x77, 0x00), (0xFF, 0x01), (0x0D, 0x01), (0xFF, 0x00), (0x80, 0x01), (0x01, 0xF8), (0xFF, 0x01), (0x8E, 0x01), (0x00, 0x01), (0xFF, 0x00), (0x80, 0x00), ): self._write_u8(pair[0], pair[1]) self._write_u8(_SYSTEM_INTERRUPT_CONFIG_GPIO, 0x04) gpio_hv_mux_active_high = self._read_u8(_GPIO_HV_MUX_ACTIVE_HIGH) self._write_u8( _GPIO_HV_MUX_ACTIVE_HIGH, gpio_hv_mux_active_high & ~0x10 ) # active low self._write_u8(_SYSTEM_INTERRUPT_CLEAR, 0x01) self._measurement_timing_budget_us = self.measurement_timing_budget self._write_u8(_SYSTEM_SEQUENCE_CONFIG, 0xE8) self.measurement_timing_budget = self._measurement_timing_budget_us self._write_u8(_SYSTEM_SEQUENCE_CONFIG, 0x01) self._perform_single_ref_calibration(0x40) self._write_u8(_SYSTEM_SEQUENCE_CONFIG, 0x02) self._perform_single_ref_calibration(0x00) # "restore the previous Sequence Config" self._write_u8(_SYSTEM_SEQUENCE_CONFIG, 0xE8) def _read_u8(self, address): # Read an 8-bit unsigned value from the specified 8-bit address. buf = self._i2c.readfrom_mem(self._addr, address, 1) return buf[0] def _read_u16(self, address): # Read a 16-bit BE unsigned value from the specified 8-bit address. buf = self._i2c.readfrom_mem(self._addr, address, 2) return (buf[0] << 8) | buf[1] def _write_u8(self, address, val): # Write an 8-bit unsigned value to the specified 8-bit address. self._i2c.writeto(self._addr, bytearray([address & 0xFF, val & 0xFF])) def _write_u16(self, address, val): # Write a 16-bit BE unsigned value to the specified 8-bit address. self._i2c.writeto(self._addr, bytearray([address & 0xFF, (val >> 8) & 0xFF, val & 0xFF])) def _get_spad_info(self): # Get reference SPAD count and type, returned as a 2-tuple of # count and boolean is_aperture. Based on code from: # https://github.com/pololu/vl53l0x-arduino/blob/master/VL53L0X.cpp for pair in ((0x80, 0x01), (0xFF, 0x01), (0x00, 0x00), (0xFF, 0x06)): self._write_u8(pair[0], pair[1]) self._write_u8(0x83, self._read_u8(0x83) | 0x04) for pair in ( (0xFF, 0x07), (0x81, 0x01), (0x80, 0x01), (0x94, 0x6B), (0x83, 0x00), ): self._write_u8(pair[0], pair[1]) start = utime.gmtime() while self._read_u8(0x83) == 0x00: if ( self.io_timeout_s > 0 and (utime.gmtime() - start) >= self.io_timeout_s ): raise RuntimeError("Timeout waiting for VL53L0X!") self._write_u8(0x83, 0x01) tmp = self._read_u8(0x92) count = tmp & 0x7F is_aperture = ((tmp >> 7) & 0x01) == 1 for pair in ((0x81, 0x00), (0xFF, 0x06)): self._write_u8(pair[0], pair[1]) self._write_u8(0x83, self._read_u8(0x83) & ~0x04) for pair in ((0xFF, 0x01), (0x00, 0x01), (0xFF, 0x00), (0x80, 0x00)): self._write_u8(pair[0], pair[1]) return (count, is_aperture) def _perform_single_ref_calibration(self, vhv_init_byte): # based on VL53L0X_perform_single_ref_calibration() from ST API. self._write_u8(_SYSRANGE_START, 0x01 | vhv_init_byte & 0xFF) start = utime.gmtime() while (self._read_u8(_RESULT_INTERRUPT_STATUS) & 0x07) == 0: if ( self.io_timeout_s > 0 and (utime.gmtime() - start) >= self.io_timeout_s ): raise RuntimeError("Timeout waiting for VL53L0X!") self._write_u8(_SYSTEM_INTERRUPT_CLEAR, 0x01) self._write_u8(_SYSRANGE_START, 0x00) def _get_vcsel_pulse_period(self, vcsel_period_type): # pylint: disable=no-else-return # Disable should be removed when refactor can be tested if vcsel_period_type == _VCSEL_PERIOD_PRE_RANGE: val = self._read_u8(_PRE_RANGE_CONFIG_VCSEL_PERIOD) return (((val) + 1) & 0xFF) << 1 elif vcsel_period_type == _VCSEL_PERIOD_FINAL_RANGE: val = self._read_u8(_FINAL_RANGE_CONFIG_VCSEL_PERIOD) return (((val) + 1) & 0xFF) << 1 return 255 def _get_sequence_step_enables(self): # based on VL53L0X_GetSequenceStepEnables() from ST API sequence_config = self._read_u8(_SYSTEM_SEQUENCE_CONFIG) tcc = (sequence_config >> 4) & 0x1 > 0 dss = (sequence_config >> 3) & 0x1 > 0 msrc = (sequence_config >> 2) & 0x1 > 0 pre_range = (sequence_config >> 6) & 0x1 > 0 final_range = (sequence_config >> 7) & 0x1 > 0 return (tcc, dss, msrc, pre_range, final_range) def _get_sequence_step_timeouts(self, pre_range): # based on get_sequence_step_timeout() from ST API but modified by # pololu here: # https://github.com/pololu/vl53l0x-arduino/blob/master/VL53L0X.cpp pre_range_vcsel_period_pclks = self._get_vcsel_pulse_period( _VCSEL_PERIOD_PRE_RANGE ) msrc_dss_tcc_mclks = (self._read_u8(_MSRC_CONFIG_TIMEOUT_MACROP) + 1) & 0xFF msrc_dss_tcc_us = _timeout_mclks_to_microseconds( msrc_dss_tcc_mclks, pre_range_vcsel_period_pclks ) pre_range_mclks = _decode_timeout( self._read_u16(_PRE_RANGE_CONFIG_TIMEOUT_MACROP_HI) ) pre_range_us = _timeout_mclks_to_microseconds( pre_range_mclks, pre_range_vcsel_period_pclks ) final_range_vcsel_period_pclks = self._get_vcsel_pulse_period( _VCSEL_PERIOD_FINAL_RANGE ) final_range_mclks = _decode_timeout( self._read_u16(_FINAL_RANGE_CONFIG_TIMEOUT_MACROP_HI) ) if pre_range: final_range_mclks -= pre_range_mclks final_range_us = _timeout_mclks_to_microseconds( final_range_mclks, final_range_vcsel_period_pclks ) return ( msrc_dss_tcc_us, pre_range_us, final_range_us, final_range_vcsel_period_pclks, pre_range_mclks, ) @property def signal_rate_limit(self): """The signal rate limit in mega counts per second.""" val = self._read_u16(_FINAL_RANGE_CONFIG_MIN_COUNT_RATE_RTN_LIMIT) # Return value converted from 16-bit 9.7 fixed point to float. return val / (1 << 7) @signal_rate_limit.setter def signal_rate_limit(self, val): assert 0.0 <= val <= 511.99 # Convert to 16-bit 9.7 fixed point value from a float. val = int(val * (1 << 7)) self._write_u16(_FINAL_RANGE_CONFIG_MIN_COUNT_RATE_RTN_LIMIT, val) @property def measurement_timing_budget(self): """The measurement timing budget in microseconds.""" budget_us = 1910 + 960 # Start overhead + end overhead. tcc, dss, msrc, pre_range, final_range = self._get_sequence_step_enables() step_timeouts = self._get_sequence_step_timeouts(pre_range) msrc_dss_tcc_us, pre_range_us, final_range_us, _, _ = step_timeouts if tcc: budget_us += msrc_dss_tcc_us + 590 if dss: budget_us += 2 * (msrc_dss_tcc_us + 690) elif msrc: budget_us += msrc_dss_tcc_us + 660 if pre_range: budget_us += pre_range_us + 660 if final_range: budget_us += final_range_us + 550 self._measurement_timing_budget_us = budget_us return budget_us @measurement_timing_budget.setter def measurement_timing_budget(self, budget_us): # pylint: disable=too-many-locals assert budget_us >= 20000 used_budget_us = 1320 + 960 # Start (diff from get) + end overhead tcc, dss, msrc, pre_range, final_range = self._get_sequence_step_enables() step_timeouts = self._get_sequence_step_timeouts(pre_range) msrc_dss_tcc_us, pre_range_us, _ = step_timeouts[:3] final_range_vcsel_period_pclks, pre_range_mclks = step_timeouts[3:] if tcc: used_budget_us += msrc_dss_tcc_us + 590 if dss: used_budget_us += 2 * (msrc_dss_tcc_us + 690) elif msrc: used_budget_us += msrc_dss_tcc_us + 660 if pre_range: used_budget_us += pre_range_us + 660 if final_range: used_budget_us += 550 # "Note that the final range timeout is determined by the timing # budget and the sum of all other timeouts within the sequence. # If there is no room for the final range timeout, then an error # will be set. Otherwise the remaining time will be applied to # the final range." if used_budget_us > budget_us: raise ValueError("Requested timeout too big.") final_range_timeout_us = budget_us - used_budget_us final_range_timeout_mclks = _timeout_microseconds_to_mclks( final_range_timeout_us, final_range_vcsel_period_pclks ) if pre_range: final_range_timeout_mclks += pre_range_mclks self._write_u16( _FINAL_RANGE_CONFIG_TIMEOUT_MACROP_HI, _encode_timeout(final_range_timeout_mclks), ) self._measurement_timing_budget_us = budget_us def getRangeMillimeters(self): """Perform a single reading of the range for an object in front of the sensor and return the distance in millimeters. """ # Adapted from readRangeSingleMillimeters & # readRangeContinuousMillimeters in pololu code at: # https://github.com/pololu/vl53l0x-arduino/blob/master/VL53L0X.cpp for pair in ( (0x80, 0x01), (0xFF, 0x01), (0x00, 0x00), (0x91, self._stop_variable), (0x00, 0x01), (0xFF, 0x00), (0x80, 0x00), (_SYSRANGE_START, 0x01), ): self._write_u8(pair[0], pair[1]) start = utime.gmtime() while (self._read_u8(_SYSRANGE_START) & 0x01) > 0: if (self.io_timeout_s > 0 and (utime.gmtime() - start) >= self.io_timeout_s): raise RuntimeError("Timeout waiting for VL53L0X!") start = utime.gmtime() while (self._read_u8(_RESULT_INTERRUPT_STATUS) & 0x07) == 0: if (self.io_timeout_s > 0 and (utime.gmtime() - start) >= self.io_timeout_s): raise RuntimeError("Timeout waiting for VL53L0X!") # assumptions: Linearity Corrective Gain is 1000 (default) # fractional ranging is not enabled range_mm = self._read_u16(_RESULT_RANGE_STATUS + 10) self._write_u8(_SYSTEM_INTERRUPT_CLEAR, 0x01) return range_mm def set_address(self, new_address): """Set a new I2C address to the instantaited object. This is only called when using multiple VL53L0X sensors on the same I2C bus (SDA & SCL pins). See also the `example `_ for proper usage. :param int new_address: The 7-bit `int` that is to be assigned to the VL53L0X sensor. The address that is assigned should NOT be already in use by another device on the I2C bus. .. important:: To properly set the address to an individual VL53L0X sensor, you must first ensure that all other VL53L0X sensors (using the default address of ``0x29``) on the same I2C bus are in their off state by pulling the "SHDN" pins LOW. When the "SHDN" pin is pulled HIGH again the default I2C address is ``0x29``. """ self._i2c.write(_I2C_SLAVE_DEVICE_ADDRESS, new_address & 0x7F)