DeaD_EyE icon

VL6180X for Micropython 1.20+

DeaD_EyE | PRO | 07/04/23 11:37:53 AM UTC (Edited) | 0 ⭐ | 1352 👁️ | Never ⏰ | []
Python |

15.17 KB

|

None

|

0 👍

/

0 👎

# 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)
  <https://www.adafruit.com/product/3316>`_ (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)
 
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎