概要
MaixPyのドキュメントのI2CのExampleのスレーブモードのコードが間違っていて通信できなかったので、その解決方法を書きます。マスター側はM5StickCでUIFlowでプログラミング、スレーブ側はUnitVでMaixPyでプログラミングしました。
目的・経緯
M5StickCとUnitVでI2Cで通信したい。
UnitVと通信する方法の解説記事をインターネットで探すと、UARTが多いんですが、M5StickCの外部接続ポートが少ないので他のセンサも使おうと思うとUARTで埋めてしまうのは具合が悪い、I2Cなら複数のセンサでバスを共有できる。そうするとM5StickCがI2Cマスター、UnitVがI2Cスレーブで通信するようにしたい。
I2Cマスター側(M5StickC)のサンプルプログラム
マスター側のM5StickCは、UIFlowで次のようなテストプログラムを書きました。
portA(Groveコネクタ)に接続し、デバイスアドレス0x24のスレーブと400Kbpsで通信するように設定しています。この設定をスレーブ側と合わせる必要があります。
Aボタンを押すと、M5StickC側からレジスタアドレス0x02に2バイトのデータを送信し、Bボタンを押すとUnitVのレジスタ0x04から2バイトのデータを受け取ります。よくあるデバイスのレジスタからデータを取得、書き込みするようなオペレーションが、スレーブ側でどのように対応するのか確認するために、このようなコードにしました。
MaixPyのI2CのExampleコード
UnitVをスレーブ側にします。UnitVはMaixPyを使いました。このドキュメントにはI2Cのスレーブモードで通信するサンプルコードが乗っているので、これに従ってプログラムを書いてみました。
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をグローバル変数として宣言すると動きました。正しいテストプログラムは以下の通りです。
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個読み出したためです。
レジスタからデータを読み書きするプログラム
最後に、サンプルを改良して、レジスタのデータを読み書きするテストプログラムを作成しました。
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に入っていると想定し、マスターからの読み書きに備えてレジスターに値をセットしています。