MaixPy (micropython)でI2Cスレーブモード

# 概要
MaixPyのドキュメントのI2CのExampleのスレーブモードのコードが間違っていて通信できなかったので、その解決方法を書きます。マスター側はM5StickCでUIFlowでプログラミング、スレーブ側はUnitVでMaixPyでプログラミングしました。

# 目的・経緯
M5StickCとUnitVでI2Cで通信したい。
UnitVと通信する方法の解説記事をインターネットで探すと、UARTが多いんですが、M5StickCの外部接続ポートが少ないので他のセンサも使おうと思うとUARTで埋めてしまうのは具合が悪い、I2Cなら複数のセンサでバスを共有できる。そうするとM5StickCがI2Cマスター、UnitVがI2Cスレーブで通信するようにしたい。

# I2Cマスター側（M5StickC）のサンプルプログラム

マスター側のM5StickCは、UIFlowで次のようなテストプログラムを書きました。
![スクリーンショット 2020-09-04 12.37.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71087/b9946043-c522-a199-9d14-e44677d9f1c7.png)

portA(Groveコネクタ)に接続し、デバイスアドレス0x24のスレーブと400Kbpsで通信するように設定しています。この設定をスレーブ側と合わせる必要があります。

Aボタンを押すと、M5StickC側からレジスタアドレス0x02に2バイトのデータを送信し、Bボタンを押すとUnitVのレジスタ0x04から2バイトのデータを受け取ります。よくあるデバイスのレジスタからデータを取得、書き込みするようなオペレーションが、スレーブ側でどのように対応するのか確認するために、このようなコードにしました。

# MaixPyのI2CのExampleコード

UnitVをスレーブ側にします。UnitVは[MaixPy](https://maixpy.sipeed.com/en/)を使いました。このドキュメントにはI2Cのスレーブモードで通信する[サンプルコード](https://maixpy.sipeed.com/en/libs/machine/i2c.html#example-3-slave-mode)が乗っているので、これに従ってプログラムを書いてみました。

```python
from machine import I2C
import utime

count = 0

def on_receive (data):
    print ("on_receive:", data)

def on_transmit ():
    count = count + 1
    print ("on_transmit, send:", count)
    return count

def on_event (event):
    print ("on_event:", event)

i2c = I2C (I2C.I2C0, mode = I2C.MODE_SLAVE, scl = 34, sda = 35, addr = 0x24, addr_size = 7, on_receive = on_receive, on_transmit = on_transmit, on_event = on_event)

while(True):
    utime.sleep_ms(100)
```
ほぼサンプルそのままです。変えたのはI2C()関数の引数のsclとsdaのオプションをUnitVのGroveコネクタの信号ピンに合わせたのと、最後にwhile(True)のループを加えてプログラムが終了しないようにしただけです。

# 通信結果
## マスターからの送信
Aボタンを押してマスターからの送信を行うと、スレーブ側（UnitV）では、MaixPy IDEのシリアルターミナルに次のような出力が出ました。（#以降は書き加えたもの）

```
>>> on_event: 0 #通信開始イベント
on_receive: 2   #レジスタアドレス(0x02)
on_receive: 16  #受信データ1バイト目
on_receive: 16  #受信データ2バイト目
on_event: 2     #通信終了イベント
```
データが受信できていることがわかります。通信開始直後にレジスタアドレスを受信してその後に受信データを1バイト受信するたびにコールバックが呼ばれている様子がわかります。

##マスターから読み出し
次にBボタンを押してみると、UnitVには全く反応がなく、スクリプトの実行が終了してシリアルターミナルの接続も切れてしまいます。

# 問題点
予想ではマスター側でデータ読み出し要求をすると、on_transmit()がコールバックされてその戻り値がマスターに返されるはずですが、on_transmit()がコールバックされた形跡もなく、イベントも検出されず全く反応がありません。
この原因がなかなかわからなくて、マスター側、スレーブ側いろいろ設定を変えてみたんですが、変わりません。

ふと気づいて、whileの直前にon_transmit()と書いて、直接関数を呼び出ししてみると「NameError: local variable referenced before assignment」というエラーが出ます。これでやっとわかりました。on_transmit()の中のcountがグローバル変数ではなくて別のローカル変数を定義しようとしているためでした。

# 解決方法

on_transmit()関数の最初にglobal countとしてcountをグローバル変数として宣言すると動きました。正しいテストプログラムは以下の通りです。

```python

from machine import I2C
import utime

count = 0

def on_receive (data):
    print ("on_receive:", data)

def on_transmit ():
    global count
    count = count + 1
    print ("on_transmit, send:", count)
    return count

def on_event (event):
    print ("on_event:", event)

i2c = I2C (I2C.I2C0, mode = I2C.MODE_SLAVE, scl = 34, sda = 35, addr = 0x24, addr_size = 7, on_receive = on_receive, on_transmit = on_transmit, on_event = on_event)

while(True):
    utime.sleep_ms(100)
```

## マスターから読み出し（改）
これでBボタンを押すと、スレーブ側では次のようなメッセージが出力されました。（#以降は書き加えたもの）

```
>>> on_event: 0        # 通信開始イベント
on_receive: 4          # レジスタアドレスの受信
on_event: 0            # データ受信の開始イベント
on_transmit, send: 1   # 1バイト目送信
on_transmit, send: 2   # 2バイト目送信
on_transmit, send: 3   # 3バイト目送信
on_transmit, send: 4   # 4バイト目送信
on_event: 2            # 通信開始イベント
```
まず最初にレジスタアドレスを受信して、その後1バイトずつon_transmit()がコールバックされてデータを送信している様子がわかります。全部で4バイト送信していますが、2バイトデータを2個読み出したためです。

# レジスタからデータを読み書きするプログラム

最後に、サンプルを改良して、レジスタのデータを読み書きするテストプログラムを作成しました。

```python
from machine import I2C
import utime

addr = 0
count = 0
state = 0
register = [0,0,0,0,0,0,0,0]
def on_receive (data):
    global addr,count,state
    print ("on_receive:", data)
    if state == 1:
        addr = data
        count = 0
        state = 2
    else:
        register[addr+count]=data
        count = count + 1

def on_transmit ():
    global addr,count,state
    state = 4
    ret = register[addr+count]
    print ("on_transmit, send:", ret, " at:",addr+count)
    count = count + 1
    return ret

def on_event (event):
    print ("on_event:", event)
    global state,addr,count
    if event == 0:
        if state == 0:
            state = 1
            addr = 0
            count = 0
        elif state == 2:
            state = 3
    elif event == 2:
        if state==2 and count == 0:
            register[0]=addr
            print("recieved data:", register[0])
        elif state==2:
            rec=0
            shift=1
            for i in range(count):
                rec = rec + register[addr+i]*shift
                shift = shift * 256
            print("recieved data:", rec)
        state = 0
        addr = 0
        count = 0

i2c = I2C (I2C.I2C0, mode = I2C.MODE_SLAVE, scl = 34, sda = 35, addr = 0x24, addr_size = 7, on_receive = on_receive, on_transmit = on_transmit, on_event = on_event)

s1=0
s2=1
while(True):
    x = s1.to_bytes(2,'big')
    register[4]=x[0]
    register[5]=x[1]
    x = s2.to_bytes(2,'big')
    register[6]=x[0]
    register[7]=x[1]
    s1=s1+1
    s2=s2*2
    
    utime.sleep_ms(1000)
```
8バイトのレジスタを想定して、reigsterの配列を用意しました。on_recieve, on_transmit, on_eventを改良して、マスター側から指定したレジスタアドレスから読み書きするようにしました。

最後の部分はテストコードで、スレーブ側に1個のセンサがあるとして、それぞれs1とs2に入っていると想定し、マスターからの読み書きに備えてレジスターに値をセットしています。