/oop_for_i2c_dev

step by step samples to make I2C device in OOP style

MIT LicenseMIT

I²Cデバイスをクラス化するまで:ステップ・バイ・ステップ

これはなに?

MicroPythonを使ってマイコンに接続したLM75B互換のI²C温度センサ(LM75B,PCT2075,P3T1085など)から値を読んでくる例を使って,オブジェクト指向でハードを抽象化する方法を段階的に説明します.
このドキュメント自体がこのリポジトリのメイン部分で,samplesフォルダ以下のコードは各ステップで示したコードの例です.

最初にマイコン基板のIMXRT1050-EVKBを用いて,まず基板上のLEDの点滅を確認.そのあとMicroPythonのごく基本的な動作を確認します.
そのあとでI²Cで接続されたデバイスのアクセス試してからコードを徐々に変更,クラス化するまでを説明します.

Note
Qiita向けのノート : この記事はgithubで「I²Cデバイスをクラス化するまで:ステップ・バイ・ステップ」として公開している内容に変更を加えて転載したものです. すべてのコード例は上記リンク・サイト内のsamplesフォルダから入手可能です.

Note
Github向けのノート : この記事はQiitaでも公開されています.
すべてのコード例はこのサイト内のsamplesフォルダから入手可能です.

動かしてみる:MicroPythonでのLチカ

この記事での例は,マイコン基板にIMXRT1050-EVKBを使用しています. この基板でのMicroPythonの動かし方は,下の動画(日本語)が参考になります.

https://youtu.be/KHRxZc4m0Vc

ステップ0:マイコン基板動作の確認

IMXRT1050-EVKBには"D4"ピンにLEDが接続されています.これを対話式の環境(REPL)で動かしてみます.
サンプルコードstep00_LED_by_manual_operation.pyを手で打ってみます.
import machineでMicroPythonに組み込まれたハードウェア制御用ライブラリを使用可能にします.
pin=machine.Pin("D4",machine.Pin.OUT)はそのライブラリのPinクラスを使って"D4"ピンを出力として使えるようにし,pinという名のインスタンスを作ります.
pin.value(1)pin.value(0)はpinインスタンスのメソッド(インスタンスに属する関数)を,1または0の引数を与えて呼び出しています.
1行ごとに入力するたびにリターンキーを押して入力,pin.value(1)pin.value(0)を実行するたびにLEDが消灯,点灯します.

step00_LED_by_manual_operation.py

import machine
pin=machine.Pin("D4",machine.Pin.OUT)
pin.value(1)
pin.value(0)

Note
このmachine.Pinのようにハードウェアを使いやすく見せるのが,ここから先で説明する「デバイスのクラス化」という話です.
コード例の中のpinインスタンスはmachine.Pinクラスによって作られます.
インスタンスが作られる時に属性が与えられ(ここでは"D4"ピン指定や出力設定),それ以降,メソッド(インスタンスに属する関数)によって操作を行えるようにします.
I²C接続の温度センサから簡単に現在の温度を読めるように,このクラスを作る手順を示します.

ステップ1:自動化してみる

ステップ0の作業をスクリプトとして実行します.
上記の組み込みライブラリの他に,もう一つutimeを使えるようにします. これは時間に関連したライブラリです.

while True:で,それ以下のインデントされた行をまとめて繰り返し実行するループを作っています.ここには必ず最後に:が必要です.これをつけることにより,ここから下のインデントされた部分が,ひとまとまりのコードであることを示すことができます.
machine.sleep( 0.1 )で「0.1秒待ちます.pin.value( 1 )を実行して0.1秒待ち,pin.value( 0 )を実行して0.1秒待ち,そのあとはループの最初に戻ってこの動作を繰り返します.

step01_LED_by_script.py

import machine
import utime

pin = machine.Pin( "D4", machine.Pin.OUT )

while True:
    pin.value( 1 )
    utime.sleep(0.1)

    pin.value( 0 )
    utime.sleep(0.1)

ステップ2:変数を使って操作

次は変数を使ってみます.
先頭の行は#で始まっています.この行は「コメント」となり,実際のプログラム実行時には無視される部分になります.

pinインスタンスを作った次の行に変数があります.
stateという名の変数で.これに0が代入されています.(Pythonではあらかじめ変数を宣言する必要はありません)

ループの中ではifが使われています.これによってstateの値でプログラムの動作が変わります.if state:stateが「True」(ゼロではない)ことを確認した時に,次のインデントされた部分を実行します.
else以下には,上記のifの条件に合わなかった時に実行されるコードを書いておきます.
この例ではstateが「0」でなかった時にはstateに0を代入,stateが「0」だった時にはstateに1を代入します.

pin.value( state )では10を引数として与える代わりに変数を与えています.これにより変数の値によってLEDがON/OFFします.

step02_variable.py

### this code has a bug. need to fix to run :)

import machine
import utime

pin   = machine.Pin( "D4", machine.Pin.OUT )
state = 0

while True:
    if state:
        state = 0
    else
        state = 1

    pin.value( state )
    utime.sleep(0.1)

Warning
ちなみに,このコード例にはバグがあります.elseは最後にコロンをつけてelse:としておかなければ動作しません.

ステップ3:一本締め

この解説の最後というわけではありませんが,一本締めのパターンでLEDを点滅させます.
ここでいう一本締めは「♪♪♪・♪♪♪・♪♪♪・♪」です.

このパターンを作るのに「リスト」を使います.リストはデータを保存するための一種の配列です. 上記の音符のパターンを1と0にして,[]で囲って,patternという名をつけておきます.

このパターンを実行するのにforを使っています.
for v in pattern:patternから各要素を1個ずつ取り出してvに代入.それに続くインデントされた部分(ブロック)ではこのvを使ってプログラムが実行されます.

step03_list.py

import machine
import utime

pin     = machine.Pin( "D4", machine.Pin.OUT )
pattern = [1,1,1,0,1,1,1,0,1,1,1,0,1]

for v in pattern:
    pin.value( v )
    utime.sleep(0.1)

    pin.value( 0 )
    utime.sleep(0.1)

ステップ4:printとリスト

変数の中身はprint()で表示させることができます.
リストをprintすると,その中身が表示されます.
リストはC/C++のような単一の型の配列ではなく,様々な型(オブジェクト)を入れておくことができます.

step04_print.py

import machine
import utime

pin     = machine.Pin( "D4", machine.Pin.OUT )
pattern = [ 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1 ]

print( "Hello, world!" )
print( pattern )

pat2    = [ 1, 2**32, 3.14, "Strawberry", "fields", True, False ]
print( pat2 )


for v in pattern:
    pin.value( v )
    utime.sleep(0.1)

    pin.value( 0 )
    utime.sleep(0.1)

実行結果

Hello, world!
[1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1]
[1, 4294967296, 3.14, 'Strawberry', 'fields', True, False]

I²Cデバイスの動作確認

ステップ5:I²C

I²Cは,簡単に動作させることができます.
IMXRT1050-EVKBにPCT2075の評価基板「PCT2075DP-ARD」を接続しておきます.

i2c = machine.I2C( 0 )はmachineライブラリのI2Cクラスを使ってi2cインスタンスを作っています.引数に与えた0はハードウェアを指定するための数値で,これを指定することにより"A4"と"A5"ピンをI²Cとして使用できるようになります.

i2c.scan()はI2Cのメソッドで,このバスに接続されているデバイスのアドレスのリストを返してきます.

IMXRT1050-EVKBには基板上にあらかじめ2個のデバイスが搭載,接続されています. このためスキャン結果には合計3個のデバイスが現れます.PCT2075のアドレスは72(16進右詰表現で0x48,左詰表現で0x90)です.

step05_I2C.py

import machine

i2c      = machine.I2C( 0 )
dev_list = i2c.scan()
print( dev_list )

実行結果

[26, 31, 72]

ステップ6:温度データを読む

PCT2075では電源投入後,そのままデータを読み出せば,現在温度のデータが得られます.
温度データは2バイトのデータです.

value = i2c.readfrom( 72, 2 )はI²Cの読み出しを行うメソッドです.
2つの引数をとり,1つ目はターゲットのアドレス.2つ目はバイト数を指定します.このメソッドからの返り値をvalueに代入しています.

print( value )は読み出した内容を確認しています.下の例では「b'\x1a\x00'」が表示されました.このデータは実際に読めた温度によって異なります.
「b'\x1a\x00'」は文字列「「'\x1a\x00'」」の前に「b」が付いています.これはbytearrayと呼ばれるバイト列であることを示し,各バイトデータの値が0x1aと0x00であることを表しています.

step06_I2C_read_PCT2075.py

import machine

i2c   = machine.I2C( 0 )
value = i2c.readfrom( 72, 2 )
print( value )

実行結果

b'\x1a\x00'

ステップ7:バイト列を通常のリストに

読み出したデータはバイト列のままでは扱いにくいのでリストに変換します. (MicroPythonにはunpackのための方法も用意されていますが,邯鄲のためにこのような方法を紹介します)

v = list( value )はバイト列データであるvalueを,通常のリストに変換してvに代入しています.

step07_I2C_read_PCT2075.py

import machine

i2c   = machine.I2C( 0 )
value = i2c.readfrom( 72, 2 )
print( value )    # read value is in bytearray format

v = list( value )
print( v )

実行結果

b'\x1a\x00'
[26, 0]

ステップ8:摂氏温度に変換

リストに変換されたvの各要素へのアクセスはv[0]v[1]のような添え字を使う方法で行えます.
先頭バイトの値はv[0],2番目のバイトはv[1]です.温度はこの2バイトを連結した16ビットに変換して256で割った値で求めることができます.

temp16bit = (v[ 0 ] << 8) | v[ 1 ]は16ビット値への変換を行う式です.
1バイト目を8ビット左シフトし,2バイト目の値とビット・オアをとっています. この値を256で割れば,摂氏温度での値が得られます.

step08_I2C_read_PCT2075.py

import machine

i2c   = machine.I2C( 0 )
value = i2c.readfrom( 72, 2 )
print( value )    # read value is in bytearray format

v = list( value )
print( v )

temp16bit = (v[ 0 ] << 8) | v[ 1 ]
print( temp16bit )
print( temp16bit / 256 )

実行結果

b'\x1a\x00'
[26, 0]
6656
26.0

ステップ9:少しシンプルに書き換える

ステップ8でのコードは,計算途中の値の表示などがあり冗長です.
これをシンプルに書き換えました.
表示には摂氏温度の値のみが出てきます.

step09_I2C_read_PCT2075

import machine

i2c   = machine.I2C( 0 )

value = i2c.readfrom( 72, 2 )
v     = list( value )
temp  = ((v[ 0 ] << 8) | v[ 1 ]) / 256
print( temp )

ステップ10:毎秒の温度表示

ステップ9のコードをWhile True:を用いてループに.
毎秒の温度を表示

step10_I2C_read_PCT2075.py

import machine
import utime

i2c = machine.I2C( 0 )

# trying to get temp every second

while True:
    value = i2c.readfrom( 72, 2 )
    v     = list( value )
    temp  = ((v[ 0 ] << 8) | v[ 1 ]) / 256
    print( temp )
    
    utime.sleep( 1 )

実行結果

26.0
26.0
25.875
26.0

ステップ11:温度の読み出しを関数に

デバイスからの温度の読み出しと摂氏の値に変換するところまでを,「関数」としてまとめます. 関数は何かの値を与えると,何かを返してくるコードのまとまりです. ですが,この場合,関数に与える値は何もありません.関数を呼び出すと,デバイスからの読み出しと変換を行なって,その値を返します.

この例ではdef read_temp():という宣言で「read_temp」という関数を定義しています.コロン(:)以降の下のコードブロックにその関数の本体が記述されています.
関数最後のreturn tempで,温度の値を返します.

MicroPythonはプログラムを先頭行から読んでいきますが,関数定義部分はその時点では実行されず,あとからその関数が呼び出された際に実行されます.
この例ではループ内のt = read_temp()が関数の呼び出しで,返り値をtに代入しています.

実行結果はステップ9の例と同様です.

step11_function_PCT2075.py

import machine
import utime

def read_temp():
    value = i2c.readfrom( 72, 2 )
    v     = list( value )
    temp  = ((v[ 0 ] << 8) | v[ 1 ]) / 256

    return temp    

i2c = machine.I2C( 0 )

# trying to get temp every second

while True:
    t = read_temp()
    print( t )
    utime.sleep( 1 )

ステップ12:さらにシンプルに書き換え

コードを短く書き換えました. 各段階で一時的な変数に値を保存することなく,メソッドや関数をそのまま別の関数の引数として与えています.

step12_function_condensed_PCT2075.py

import machine
import utime

def read_temp():
    v = list( i2c.readfrom( 72, 2 ) )
    return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c = machine.I2C( 0 )

while True:
    print( read_temp() )
    utime.sleep( 1 )

ステップ13:複数の温度センサを扱えるようにしてみる

ステップ12までの例では,アドレス=72のデバイスからの温度を読むことしかできません.
これでは有用性が低いので,関数の引数にアドレスを与えるように書き換えました. def read_temp( address ):addressが読み出し対象デバイスのターゲット・アドレスです. 関数内でi2c.readfrom( address, 2 )のようにターゲット・アドレス指定をaddressに書き換えました.
関数の呼び出しではt = read_temp( 72 )のようにアドレスを指定します.

step13_function_flexible_PCT2075.py

import machine
import utime

def read_temp( address ):
    v = list( i2c.readfrom( address, 2 ) )
    return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c = machine.I2C( 0 )

while True:
    print( read_temp( 72 ) )
    utime.sleep( 1 )

ステップ14:複数の温度センサを読む

ステップ13の関数を使って,複数のセンサからの値を読んでくることができるようになります.
具体的にはこのようなコードを書くことになります.

この例ではアドレス72,73,74のデバイスにアクセスしてみています.

step14_multiple_dev_PCT2075.py

import machine
import utime

def read_temp( address ):
    v = list( i2c.readfrom( address, 2 ) )
    return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c = machine.I2C( 0 )

while True:
    print( read_temp( 72 ) )
    print( read_temp( 73 ) )
    print( read_temp( 74 ) )
    utime.sleep( 1 )

ステップ15:複数のバスに繋がった複数の温度センサを読む

大きなシステムの場合,マイコンに搭載された複数のI²Cバスを同時に使って,それぞれのバスに接続された複数のセンサの値を読んでくることもあります.
関数にバスのインスタンスも渡せるようにしておけば対応することが可能です.

この例ではi2c_0i2c_1のふたつのバスに繋がった6個の温度センサを読んでみる例です.

step15_multiple_bus_PCT2075.py

import machine
import utime

def read_temp( bus, address ):
    v = list( bus.readfrom( address, 2 ) )
    return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c_0 = machine.I2C( 0 )
i2c_1 = machine.I2C( 1 )

while True:
    print( read_temp( i2c_0, 72 ) )
    print( read_temp( i2c_0, 73 ) )
    print( read_temp( i2c_0, 74 ) )
    print( read_temp( i2c_1, 72 ) )
    print( read_temp( i2c_1, 73 ) )
    print( read_temp( i2c_1, 74 ) )
    utime.sleep( 1 )

I²Cデバイスのクラス化

ステップ16:クラス化

ステップ14の例では各デバイスを読む際に,「どのバス」の「どのアドレス」からという指定が必要でした.
これをコード内で管理するのは煩雑です.
この手間をクラス化によって省くことができます.

クラスはいくつかのデータと関数をまとめた入れ物と考えることができます.
このクラスを作っておけば,同種の処理(この場合は温度センサ・デバイスを)単純化できます.

この例ではclass temp_sensor:によってtemp_sensorクラスを作成しています.
クラスの中にはふたつのメソッド(クラスで定義された関数)__init()__read()が定義されています.

def __init__( self, bus, address ):はインスタンスが作られるときに呼ばれる初期化のメソッドです.このメソッドでは与えられたbusaddressをインスタンス内の変数として保存しています.
selfはインスタンス変数へアクセスするためのメカニズムを提供しています.メソッド定義内では必ず第一引数として指定しておき,インスタンス変数はself.__busself.__adrのようにアクセスします.

インスタンス変数は同クラス内の他のメソッドから参照できます.他のメソッドでも必ず第一引数selfを用意しておき,上記と同様にself.__adrのようにアクセスします.

read()メソッド内では温度センサのアクセスに,__init()__で保存しておいたself.__busself.__adrを用いています.
self.__bus.readfrom( self.__adr, 2 )のようにself.__busに保存されたI2Cインスタンス・メソッドreadfromを呼び出し,その際にアドレスをself.__adrで指定しています.

インスタンスの作成はts = temp_sensor( i2c, 72 )で行なっています.I2Cインスタンスのi2cとアドレス値72を渡し,tsインスタンスが作られます.
温度の読み出しはインスタンス・メソッドreadで,インスタンス名を用いてts.read()のように行います.

step16_class_PCT2075.py

import machine
import utime

class temp_sensor:
    def __init__( self, bus, address ):
        self.__bus = bus
        self.__adr = address
    
    def read( self ):
        v = list( self.__bus.readfrom( self.__adr, 2 ) )
        return ((v[ 0 ] << 8) | v[ 1 ]) / 256
        
i2c = machine.I2C( 0 )
ts  = temp_sensor( i2c, 72 )

while True:
    print( ts.read() )
    utime.sleep( 1 )

ステップ17:

ステップ15の「複数のバスに繋がった複数の温度センサを読む」は,この例のように書き換えることができます.

step17_class_PCT2075.py

import machine
import utime

class temp_sensor:
    def __init__( self, bus, address ):
        self.__bus = bus
        self.__adr = address
    
    def read( self ):
        v = list( self.__bus.readfrom( self.__adr, 2 ) )
        return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c_0 = machine.I2C( 0 )
i2c_1 = machine.I2C( 1 )

ts0   = temp_sensor( i2c_0, 72 )
ts1   = temp_sensor( i2c_0, 73 )
ts2   = temp_sensor( i2c_0, 74 )
ts3   = temp_sensor( i2c_1, 72 )
ts4   = temp_sensor( i2c_1, 73 )
ts5   = temp_sensor( i2c_1, 74 )

while True:
    print( ts0.read() )
    print( ts1.read() )
    print( ts2.read() )
    print( ts3.read() )
    print( ts4.read() )
    print( ts5.read() )
    utime.sleep( 1 )

ステップ18:複数デバイスの管理をインスタンスのリストに

ステップ17の各インスタンスをリストとして用意しておけば,さらに管理が容易になります.
この例のts_listは温度センサのインスタンスがリストになっており,各デバイスからの読み出しはfor ts in ts_list:のループ内でts.read()を呼び出すだけで済みます.

step18_class_PCT2075.py

import machine
import utime

class temp_sensor:
    def __init__( self, bus, address ):
        self.__bus = bus
        self.__adr = address
    
    def read( self ):
        v = list( self.__bus.readfrom( self.__adr, 2 ) )
        return ((v[ 0 ] << 8) | v[ 1 ]) / 256

i2c_0 = machine.I2C( 0 )
i2c_1 = machine.I2C( 1 )

ts_list = [ temp_sensor( i2c_0, 72 ), 
            temp_sensor( i2c_0, 73 ),
            temp_sensor( i2c_0, 74 ),
            temp_sensor( i2c_1, 72 ),
            temp_sensor( i2c_1, 73 ),
            temp_sensor( i2c_1, 74 )
            ]
              
while True:
    for ts in ts_list:
        print( ts.read() )
        utime.sleep( 1 )