稼働中

マイクロビット(e_55)VL53L0X レーザ距離センサー

VL53L0X モジュール

VL53L0XはTime-of-Flightレーザ距離センサです。距離測定ができます。 I2C通信で出力します。
VL53L0Xは発光に940 nm VCSEL (Vertical Cavity Surface-Emitting Laser)、受光に SPAD array (Single Photon Avalanche Diodes) を使ったセンサで、2mまでの距離が測れ、分解能は1mmらしいです。
※写真ではモジュール名(シルク)はUL53LDKになってます。記事内ではモジュール名もVL53L0Xと記載します。

外観

VL53L0Xモジュールの外観です。
外観 VL53L0X 外観 VL53L0X 

ちなみに以下はVL53L0Xが動作中でVCSELが発光している状態です。赤外光なので目視では見えません。
VCSEL発光

実体図

VL53L0Xは3.3Vで動作可能なのでmicro:bitから給電します。I2Cの端子SCL、SDAをそれぞれmicro:bit V2の19、20pinに接続するだけです。そのため接続図は省略しました。※micro:bit V1ではメモリー不足?で動かなかったです。
以下は実体図です。
実体図

スクリプト

VL53L0Xのデータシートを見てもレジスタマップなど記載がないのでよくわかりませんでした。
結局、参考になる数種のモジュールから素人の私にでも移植できそうなものからmicro:bit用に一部に変更を加えました。
参考にしたVL53L0Xのモジュールは「https://github.com/mcauser/deshipu-micropython-vl53l0x」にあるvl53l0x.pyです。
338行あります。他のサイトのモジュールは600行超でした。比較すると338行分は含まれているようです。
338行は必要最小限のスクリプトなのかも知れません。中身を見ても何をしているのか?よくわかりません。

I2Cのread、writeの部分をmicro:bitに合うように変更を加えて使いました。
スクリプトは以下のようになりました。※MicroPython(BBC micro:bit V2)を使っています。


VL53L0X_test_01.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from microbit import *
from micropython import const
import ustruct
import utime
_IO_TIMEOUT = 1000
_SYSRANGE_START = const(0x00)
_EXTSUP_HV = const(0x89)
_MSRC_CONFIG = const(0x60)
_FINAL_RATE_RTN_LIMIT = const(0x44)
_SYSTEM_SEQUENCE = const(0x01)
_SPAD_REF_START = const(0x4f)
_SPAD_ENABLES = const(0xb0)
_REF_EN_START_SELECT = const(0xb6)
_SPAD_NUM_REQUESTED = const(0x4e)
_INTERRUPT_GPIO = const(0x0a)
_INTERRUPT_CLEAR = const(0x0b)
_GPIO_MUX_ACTIVE_HIGH = const(0x84)
_RESULT_INTERRUPT_STATUS = const(0x13)
_RESULT_RANGE_STATUS = const(0x14)
_OSC_CALIBRATE = const(0xf8)
_MEASURE_PERIOD = const(0x04)

class TimeoutError(RuntimeError):
    pass

class VL53L0X:
    #def __init__(self, i2c, address=0x29):
    def __init__(self, address=0x29):
        #self.i2c = i2c
        self.address = address
        #self.init()
        i2c.init(freq=100000, sda=pin20, scl=pin19)
        #i2c.init()
        self._started = False

    def _registers(self, register, values=None, struct='B'):
        if values is None:
            size = ustruct.calcsize(struct)
            #data = self.i2c.readfrom_mem(self.address, register, size)
            buf=bytearray(1)
            buf[0]=register
            i2c.write(self.address,buf)
            data=i2c.read(self.address,size)                        
            values = ustruct.unpack(struct, data)
            return values
        data = ustruct.pack(struct, *values)
        #print('data',data,type(data))
        #data b'\x01' <class 'bytes'>
        #self.i2c.writeto_mem(self.address, register, data)
        buf=bytearray(1)
        buf[0]=register
        buf=buf+data
        #print('BUF',buf,type(buf))
        #BUF bytearray(b'\x89\x01') <class 'bytearray'>
        i2c.write(self.address,buf)
        #buf=bytearray(2) buf[1]=data >> can't convert bytes to int

    def _register(self, register, value=None, struct='B'):
            if value is None:
                return self._registers(register, struct=struct)[0]
            self._registers(register, (value,), struct=struct)

    def _flag(self, register=0x00, bit=0, value=None):
        data = self._register(register)
        mask = 1 << bit
        if value is None:
            return bool(data & mask)
        elif value:
            data |= mask
        else:
            data &= ~mask
        self._register(register, data)

    def _config(self, *config):
        for register, value in config:
            self._register(register, value)

    def init(self, power2v8=True):
        self._flag(_EXTSUP_HV, 0, power2v8)

        # I2C standard mode
        self._config(
            (0x88, 0x00),

            (0x80, 0x01),
            (0xff, 0x01),
            (0x00, 0x00),
        )
        self._stop_variable = self._register(0x91)       
        
        self._config(
            (0x00, 0x01),
            (0xff, 0x00),
            (0x80, 0x00),
        )

        # disable signal_rate_msrc and signal_rate_pre_range limit checks
        self._flag(_MSRC_CONFIG, 1, True)
        self._flag(_MSRC_CONFIG, 4, True)

        # rate_limit = 0.25
        self._register(_FINAL_RATE_RTN_LIMIT, int(0.25 * (1 << 7)), struct='>H')

        self._register(_SYSTEM_SEQUENCE, 0xff)

        spad_count, is_aperture = self._spad_info()
        spad_map = bytearray(self._registers(_SPAD_ENABLES, struct='6B'))

        # set reference spads
        self._config(
            (0xff, 0x01),
            (_SPAD_REF_START, 0x00),
            (_SPAD_NUM_REQUESTED, 0x2c),
            (0xff, 0x00),
            (_REF_EN_START_SELECT, 0xb4),
        )

        spads_enabled = 0
        for i in range(48):
            if i < 12 and is_aperture or spads_enabled >= spad_count:
                spad_map[i // 8] &= ~(1 << (i >> 2))
            elif spad_map[i // 8] & (1 << (i >> 2)):
                spads_enabled += 1

        self._registers(_SPAD_ENABLES, spad_map, struct='6B')

        self._config(
            (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._register(_INTERRUPT_GPIO, 0x04)
        self._flag(_GPIO_MUX_ACTIVE_HIGH, 4, False)
        self._register(_INTERRUPT_CLEAR, 0x01)

        # XXX Need to implement this.
        #budget = self._timing_budget()
        #self._register(_SYSTEM_SEQUENCE, 0xe8)
        #self._timing_budget(budget)

        self._register(_SYSTEM_SEQUENCE, 0x01)
        self._calibrate(0x40)
        self._register(_SYSTEM_SEQUENCE, 0x02)
        self._calibrate(0x00)

        self._register(_SYSTEM_SEQUENCE, 0xe8)

    def _spad_info(self):
        self._config(
            (0x80, 0x01),
            (0xff, 0x01),
            (0x00, 0x00),

            (0xff, 0x06),
        )
        self._flag(0x83, 3, True)
        self._config(
            (0xff, 0x07),
            (0x81, 0x01),

            (0x80, 0x01),

            (0x94, 0x6b),
            (0x83, 0x00),
        )
        for timeout in range(_IO_TIMEOUT):
            if self._register(0x83):
                break
            utime.sleep_ms(1)
        else:
            raise TimeoutError()
        self._config(
            (0x83, 0x01),
        )
        value = self._register(0x92)
        self._config(
            (0x81, 0x00),
            (0xff, 0x06),
        )
        self._flag(0x83, 3, False)
        self._config(
            (0xff, 0x01),
            (0x00, 0x01),

            (0xff, 0x00),
            (0x80, 0x00),
        )
        count = value & 0x7f
        is_aperture = bool(value & 0b10000000)
        return count, is_aperture

    def _calibrate(self, vhv_init_byte):
        self._register(_SYSRANGE_START, 0x01 | vhv_init_byte)
        for timeout in range(_IO_TIMEOUT):
            if self._register(_RESULT_INTERRUPT_STATUS) & 0x07:
                break
            utime.sleep_ms(1)
        else:
            raise TimeoutError()
        self._register(_INTERRUPT_CLEAR, 0x01)
        self._register(_SYSRANGE_START, 0x00)

    def start(self, period=0):
        self._config(
          (0x80, 0x01),
          (0xFF, 0x01),
          (0x00, 0x00),
          (0x91, self._stop_variable),
          (0x00, 0x01),
          (0xFF, 0x00),
          (0x80, 0x00),
        )
        if period:
            oscilator = self._register(_OSC_CALIBRATE, struct='>H')
            if oscilator:
                period *= oscilator
            self._register(_MEASURE_PERIOD, period, struct='>H')
            self._register(_SYSRANGE_START, 0x04)
        else:
            self._register(_SYSRANGE_START, 0x02)
        self._started = True

    def stop(self):
        self._register(_SYSRANGE_START, 0x01)
        self._config(
          (0xFF, 0x01),
          (0x00, 0x00),
          (0x91, self._stop_variable),
          (0x00, 0x01),
          (0xFF, 0x00),
        )
        self._started = False

    def read(self):
        if not self._started:
            self._config(
              (0x80, 0x01),
              (0xFF, 0x01),
              (0x00, 0x00),
              (0x91, self._stop_variable),
              (0x00, 0x01),
              (0xFF, 0x00),
              (0x80, 0x00),
              (_SYSRANGE_START, 0x01),
            )
            for timeout in range(_IO_TIMEOUT):
                if not self._register(_SYSRANGE_START) & 0x01:
                    break
                utime.sleep_ms(1)
            else:
                raise TimeoutError()
        for timeout in range(_IO_TIMEOUT):
            if self._register(_RESULT_INTERRUPT_STATUS) & 0x07:
                break
            utime.sleep_ms(1)
        else:
            raise TimeoutError()
        value = self._register(_RESULT_RANGE_STATUS + 10, struct='>H')
        self._register(_INTERRUPT_CLEAR, 0x01)
        return value

## MAIN
# Create a VL53L0X object
tof = VL53L0X()         # instance
tof.init()              # 初期化
tof.start()             # スタート
sleep(100)
while True:
    tof.read()          # 測定
    print(tof.read())
    #tof.stop()         # 停止
    sleep(500)          # 待機時間

実行結果

VL53L0Xセンサーと厚紙の距離を約10cmづつ階段状に増やしながら80cmまで測定しました。
測定結果はThonnyのシェルに表示されます。


VL53L0X_test_01.py
>>> %Run -c $EDITOR_CONTENT a
99
99
98
100
100
99
102
189
169
170
173
233
287
287
省略--

表計算ソフトで図示しました。
距離測定

測定値は約10cmくらい長くなってました。測定値から98mmを引くと良さそうです。スクリプトが600行以上あるモジュールを使えば何か違うのかも分かりませんが・・。
とりあえず測定は出来ていると思います。

ちなみに、待機時間をsleep(100)にして厚紙を移動させた測定値を測定しながらプロットしてみました。
メニューView>Plotterを選択して実行します。それなりに測定できているようです。

Plotter

まとめ

VL53L0Xモジュールをmicro:bit用に修正しました。どうにかVL53L0Xによる距離測定ができたと思います。

※今回でmicro:bitの使用例は終わりです。また追加できるものがあれば記載します。