I2C通信でArduinoにファイル転送:I2Cでファイル転送ってどうよ?
ラズパイからArduinoにファイルを送りたい場合があるかと思います。例えば、音楽ファイルとか画像ファイルとか。
あらかじめSDカードに書き込んでオフラインで渡すというので用が足りる場合は良いのですが、動作中にデータを変えたい場合もあるかと思います。
方法としてはWifiやUARTのシリアル通信などがスマートなのかもしれませが、Wifiはネットワーク環境がないと使えないし、同一の筐体内に収まっている隣の基板間で通信をするには大げさな感じもします。また、UARTは使える数に限りがあるので、何かのときのためにできたら温存しておきたいという気持ちがあります。
なので、個人的には複数のデバイスを共通のバスでつなげることができるI2Cが使えると便利な感じがします。
ネットを検索するとファイルをI2Cで転送したいと考える人はいるようで、そういった質問がちらほらあるようですが、I2Cはそういったものを送るためのものではないのでシリアル通信を使え的なつれない回答が多いようです。自分も試してみたいと思ったのですが、適当なテスト用のプログラムを見つけることができませんでした。
なので、試しに作ってみました。
Arduinoで標準的に使われているI2CのライブラリであるWireライブラリでは、受信バッファが32バイトなので、このバッファ量内でデータを転送する必要がありそうです。その部分さえ工夫すれば、送信自体はできそうな感じです。
プログラムの使い方
下記がテスト用に作成した送信用プログラム(ラズパイ側)と、受信用プログラム(Arduino側)です。ラズパイ5からArduino UNO R4にI2C通信でファイルを転送し、ArduinoにつないだSDカードに保存します。
ラズパイ側ではi2c_file_slaveを実行し、指定したファイルをArduinoに転送します。Arduino側では受信したデータを、test.binのファイル名でSDカードに保管します。
i2c_file_slave.inoはArduinoIDEでコンパイルしてArduino UNO R4に書き込みます。受信したデータを書き込む先はSDカードにしてありますが、SDカードを使わずファイル転送だけを試したい場合には、ソースコード内の#define USE_SDの行をコメントアウトしてください。
i2c_file_master.cppはラズパイ側で下記のようにコンパイルします。
% g++ -o i2c_file_master i2c_file_master.cpp
ラズパイとArduinoはI2Cで接続します。ラズパイとArduinoは信号レベルが異なるのでレベル変換を間に入れる必要があります。接続の方法はこちらなどを参考にしてください。また、ラズパイ側ではI2Cが使えるようにしておいてください。I2Cのデバイス名は/dev/i2c-1を使っています。もし、デバイス名が異なる場合には、ソースコード内のopenしているデバイス名を書き換えてください。
実行方法ですが、ファイル名がtest.txtのファイルを転送する場合、ラズパイ側で下記のように実行します。
% ./i2c_file_master test.txt
それなりに転送時間がかかるので、転送するファイルは最初は小さめのファイルで試した方が良いでしょう。
ファイル転送速度の結果
下記のグラフは、10バイトから1Mバイトのファイルを転送したときの転送時間の測定結果です。
ファイルの大きさによって変わりますが、5Kバイトのファイルだと、SDカードへの書き込みが無ければ1.5Kbytes/secくらいの速度ですが、SDカードへの書き込みをすると250bytes/secくらいと1/6くらいになってしまうようです。
SDカードへの書き込みは遅かったり早かったりとあまり安定していなく、場合によってはもっと早く終わる場合もあります。
この速度が実際のアプリケーションに使えるかどうかは使い道よると思いますので、ご覧になっている方々の判断にお任せします。個人的には数10Kバイト以下のファイルの転送であれば、ぎりぎり我慢できる範囲かなと思います。
プログラムの概要
I2C通信のデータの書き込みは、1バイトのレジスタ番号のあとにデータが続く感じになります。
今回のファイル転送のプログラムでは、各レジスタ番号に対して次のような使い方分けをしています。
0x00:転送データの書き込み
0x01:保存ファイル名
0x02:データサイズ
0x03:SDカード上のファイルへの書き込みの実行
0x04:SDカード上のファイルのオープン
0x05:オープンしたファイルのクローズ
ファイルのデータを転送するに先立ち、SDカード上に保管するファイル名をレジスタ0x01に書き込んでおきます。ArduinoのSDライブラリではファイル名は、8文字+拡張子3文字に限定されているので、この文字数内のファイル名を指定します。
次に、0x01レジスタに書き込んだファイル名のファイルをオープンをするため、レジスタ0x04に1を書き込みます。Arduino側でファイルのオープンが完了すると、0x04レジスタのデータは0に変わります。
ファイルがオープンしたらファイルのデータを転送します。
転送するデータは、レジスタ番号0x00を含む32バイトのデータを、レジスタ番号0x00に繰り返し書き込みます。レジスタ番号1バイトの次の1バイトはオフセットのインデックス番号として利用し、残りの30バイトをファイルのデータを格納します。
Arduino UNO R4側にはデータ受信用のバッファとして30byte x 256=7,680バイトの領域を準備しています。
インデックス番号は、送信した30バイトのデータをこのArduino側のバッファのどの位置に書き込むかを示す物で、例えばインデックス番号が5の場合、データ受信用のバッファの先頭から5x30=150バイトの位置に受信した30バイトのデータを書き込みます。
30バイトデータを256個(送信データがそれより小さければ256以下でもかまわない)の送信が終わったら、ラズパイ側からArduinoにデータの書き込み要求を出します。書き込み要求はレジスタ番号0x04に行います。書き込むデータのサイズはレジスタ0x02にあらかじめ書き込んでおきます。なので、0x02に書き込まれるのは7,680以下の値です。
以上のファイルのデータの転送とSDカードへの書き込みを繰り返し行います。
データの転送がすべて完了したら、ファイルのクローズを行うためレジスタ0x05に1を書き込んで完了です。
SDカードへの書き込みの実行(0x03)、ファイルのオープン(0x04)、ファイルのクローズ(0x05)の各実行命令はデータとして1を各レジスタに書き込みますが、Arduino側で処理が完了するとレジスタの値が0に変わります。マスター側(ラズパイ側)では、実行命令を出したらそのレジスタの値が0になるのを確認してから次の動作を行うようにしています。
注意点
SDカードへの書き込みですが、SDライブラリのみでSDカードへの読み書きをテストすると安定していますが、I2Cのスレーブと一緒に動かすと、どうも速度を含めて動作が安定しない感じです。SdFatライブラリではさらに不安定となるため、現在はSDライブラリで動かしています。
Arduino UNO R4での利用を前提として、スレーブ側では7,680バイトのバッファを固定的に確保しています。メモリが少ないその他のArduinoで動かす場合は、30x256の256の数(MAX_CHUNK_NUM)を小さくした上で、データ転送時のインデックス番号をその値以内としてラズパイ側からファイルのデータを送れば動くのではないかと思います(試していませんが)。