Optimizations

After simplifying a driver there are a few optimizations you might consider making to help reduce the memory usage of the driver.  In general memory usage is a struggle for small microcontrollers and as a driver write you want to ensure your driver uses as little memory as possible.  This allows your users to build more complex and interesting programs without running out of memory.  This page describes a few useful tips to reduce memory usage.

Internal Constant Values

One optimization is the use of const values for integers or numbers that don't ever change.  There's a special const function added to MicroPython & CircuitPython that allows you to tell the interpreter and other tools that a value will never change.  When tools know the value is constant they can optimize its usage, like inlining the value where it's used instead of creating a variable that takes precious memory.  As your saw in the driver code page it makes sense to set register and other fixed values as consts.

However one extra optimization is to make 'internal' constants marked as private module members with an underscore.  These are global values in a module that are prefixed with an underscore in their name and tell tools that nothing outside the module will reference them.  By creating an internal constant you can be absolutely sure the value will be inlined and use as little memory as possible.

The big gotcha with internal constant values is that they can only be used inside your driver or module.  Your users can never reference these values!  This means you typically only want to use internal constants for things like register addresses that only your driver code will read and write.  For example the constants in our driver look like:

VL6180X_DEFAULT_I2C_ADDR = const(0x29)

VL6180X_REG_IDENTIFICATION_MODEL_ID    = const(0x000)
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_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_ALS_VAL             = const(0x050)
VL6180X_REG_RESULT_RANGE_VAL           = const(0x062)
VL6180X_REG_RESULT_RANGE_STATUS        = const(0x04d)
VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO       = const(0x04f)

VL6180X_ALS_GAIN_1         = const(0x06)
VL6180X_ALS_GAIN_1_25      = const(0x05)
VL6180X_ALS_GAIN_1_67      = const(0x04)
VL6180X_ALS_GAIN_2_5       = const(0x03)
VL6180X_ALS_GAIN_5         = const(0x02)
VL6180X_ALS_GAIN_10        = const(0x01)
VL6180X_ALS_GAIN_20        = const(0x00)
VL6180X_ALS_GAIN_40        = const(0x07)

VL6180X_ERROR_NONE         = const(0)
VL6180X_ERROR_SYSERR_1     = const(1)
VL6180X_ERROR_SYSERR_5     = const(5)
VL6180X_ERROR_ECEFAIL      = const(6)
VL6180X_ERROR_NOCONVERGE   = const(7)
VL6180X_ERROR_RANGEIGNORE  = const(8)
VL6180X_ERROR_SNR          = const(11)
VL6180X_ERROR_RAWUFLOW     = const(12)
VL6180X_ERROR_RAWOFLOW     = const(13)
VL6180X_ERROR_RANGEUFLOW   = const(14)
VL6180X_ERROR_RANGEOFLOW   = const(15)

Some of these values are only used inside the driver and users of the driver are not expected to ever reference them.  For example the VL6180X_DEFAULT_I2C_ADDR, and VL6180X_REG_* values are addresses which the driver code references--users never need to specify these values.

Some of the values your driver user might need to reference.  For example the VL6180X_ALS_* and VL6180X_ERROR_* values are constants which users might need to send to the driver (like when setting gain interpreting an error).

Let's make the register and address values internal constants by adding an underscore in front of their name.  We'll keep the ALS and error constants as-is so they remain as module-level values that a user can access:

_VL6180X_DEFAULT_I2C_ADDR = const(0x29)

_VL6180X_REG_IDENTIFICATION_MODEL_ID    = const(0x000)
_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_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_ALS_VAL             = const(0x050)
_VL6180X_REG_RESULT_RANGE_VAL           = const(0x062)
_VL6180X_REG_RESULT_RANGE_STATUS        = const(0x04d)
_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO       = const(0x04f)

VL6180X_ALS_GAIN_1         = const(0x06)
VL6180X_ALS_GAIN_1_25      = const(0x05)
VL6180X_ALS_GAIN_1_67      = const(0x04)
VL6180X_ALS_GAIN_2_5       = const(0x03)
VL6180X_ALS_GAIN_5         = const(0x02)
VL6180X_ALS_GAIN_10        = const(0x01)
VL6180X_ALS_GAIN_20        = const(0x00)
VL6180X_ALS_GAIN_40        = const(0x07)

VL6180X_ERROR_NONE         = const(0)
VL6180X_ERROR_SYSERR_1     = const(1)
VL6180X_ERROR_SYSERR_5     = const(5)
VL6180X_ERROR_ECEFAIL      = const(6)
VL6180X_ERROR_NOCONVERGE   = const(7)
VL6180X_ERROR_RANGEIGNORE  = const(8)
VL6180X_ERROR_SNR          = const(11)
VL6180X_ERROR_RAWUFLOW     = const(12)
VL6180X_ERROR_RAWOFLOW     = const(13)
VL6180X_ERROR_RANGEUFLOW   = const(14)
VL6180X_ERROR_RANGEOFLOW   = const(15)

Then in your driver code be sure to update all the references to the registers to use the new underscore name.  The rest of the driver now looks like:

class Adafruit_VL6180X:

    def __init__(self, i2c, address=_VL6180X_DEFAULT_I2C_ADDR):
        self._device = I2CDevice(i2c, 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)

    @property
    def range(self):
        """Read the range of an object in front of sensor and return it in mm."""
        # wait for device to be ready for range measurement
        while not (self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) & 0x01):
            pass
        # Start a range measurement
        self._write_8(_VL6180X_REG_SYSRANGE_START, 0x01);
        # 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):
        """Read the lux (light value) from the sensor and return it."""
        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
        if gain > VL6180X_ALS_GAIN_40:
            gain = VL6180X_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 4 != ((self._read_8(_VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7):
            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 == VL6180X_ALS_GAIN_1:
            pass
        elif gain == VL6180X_ALS_GAIN_1_25:
            lux /= 1.25
        elif gain == VL6180X_ALS_GAIN_1_67:
            lux /= 1.76
        elif gain == VL6180X_ALS_GAIN_2_5:
            lux /= 2.5
        elif gain == VL6180X_ALS_GAIN_5:
            lux /= 5
        elif gain == VL6180X_ALS_GAIN_10:
            lux /= 10
        elif gain == VL6180X_ALS_GAIN_20:
            lux /= 20
        elif gain == VL6180X_ALS_GAIN_40:
            lux /= 20
        lux *= 100
        lux /= 100 # integration time in ms
        return lux

    @property
    def range_status(self):
        """Retrieve the status/error from a previous range read."""
        return self._read_8(_VL6180X_REG_RESULT_RANGE_STATUS) >> 4

    def _load_settings(self):
        # 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_8(self, address, data):
        # Write 1 byte of data from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                       data]))

    def _write_16(self, address, data):
        # Write a 16-bit big endian value to the specified 16-bit register
        # address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                      (data >> 8) & 0xFF,
                                       data & 0xFF]))

    def _read_8(self, address):
        # Read and return a byte from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(1)
            self._device.read_into(result)
            return result[0]

    def _read_16(self, address):
        # Read and return a 16-bit unsigned big endian value read from the
        # specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(2)
            self._device.read_into(result)
            return (result[0] << 8) | result[1]

Notice the register and other values use the underscore-prefixed names.  This ensures the internal constants will be as efficient as possible with memory usage and inlined in binary versions of the code where possible.

Remember you need to understand how your users will use the driver code before you change values to internal constants.  Once a value is an internal constant it can never be accessed by your driver users!  Internal constants are intended only for internal values that the driver uses--not values like errors or settings that a user might need to pass to a function.

Memory Usage

In Python you don't typically need to manage memory as the interpreter is smart about understanding when memory is used and unused.  However there is still a limited amount of memory available and this can cause problems for your driver code.  As mentioned before at some point (typically 200-300 lines of code) a pure text .py file becomes too big for your board to load and process in memory.  For this issue you typically want to convert the file to a binary .mpy version--this file will use less memory when loaded by the interpreter.

On Linux and OSX it's easy to compile mpy-cross and use it yourself as mentioned in the guide--this is because these operating systems support a POSIX build environment (once you install Xcode Command Line Tools on OSX).  Simply clone the CircuitPython repository (being careful to choose the right branch as the master branch might be on a later version of the MPY format than the released CircuitPython builds) and build inside the mpy-cross folder:

git clone --recursive https://github.com/adafruit/circuitpython.git
cd circuitpython/mpy-cross
make

For Windows it's a bit trickier and you can either use the Vagrant-based VM in the mentioned guide (this is recommended to ensure you get the latest mpy-cross built) or you can download a pre-built binary below.  NOTE: This binary is built for CircuitPython 2.x / MicroPython 1.9.2 only.  Earlier or later versions of either might not work with binaries built using this tool!  This tool was built using the mingw 32-bit toolchain and should run on all versions of Windows 7 and above, 32-bit and 64-bit (with WOW32 emulation layer available).

In addition you can find other binary builds of mpy-cross on the CircuitPython releases page, but be aware they can only be guaranteed to work with the associated release of CircuitPython and might not work with earlier or later versions!  In addition be careful on OSX and Linux or Raspbian builds as binary dependencies might differ between your system and the built version--if you run into trouble stick with manually building your own version of mpy-cross.

To use mpy-cross, as the guide mentions you just need to invoke it in a terminal and pass in the path to a python .py file.  The tool will generate a .mpy version (if there are no syntax errors or other problems with the input file) which you can copy to your board and use in place of the .py version.  The .mpy version will use much less memory when being loaded by CircuitPython.

For example to mpy-cross a file called test.py you might run:

mpy-cross test.py

Make sure mpy-cross.exe (on Windows) or other suitable executable is in the same directory or in your terminal path.  If the tool succeeds there will be no output generated and you can find the test.mpy file in the same location.

Run mpy-cross with the --help option to see more details on advanced usage!

Within your driver code you might run into memory issues too, like if you're creating very large buffers or lots of variables.  There's no easy fix for this--memory is finite and you typically can't add more.  If you need more memory you need to find ways to reduce other memory usage.  Sometimes you can remove unused variables, functions, etc.  Sometimes you might need to remove functionality entirely and strip a driver down to just the core features.  There's no easy fix but be aware memory can be a significant issue as you port complex drivers and code to Python!

Last updated on 2017-11-06 at 06.17.56 PM Published on 2017-10-16 at 05.31.56 PM