先の記事でArduinoのリアルタイム性能の確認,およびシリアル通信におけるバイト通信のすすめについて述べました.最後に,実際にバイト通信をするための簡単なサンプルプログラムを挙げておきます.Arduinoで現在時刻,アナログ端子A0およびアナログ端子A1の値をそれぞれ取得し,シリアルでPCに送信するというものです.
基本動作:
- 現在時刻(マイクロ秒)をmicros()で取得すると unsigned long intすなわち4バイトの値が返ってきますが,そのうち下位の1バイトを無視して上位の3バイトだけ送信しています.2^32[usec]≒4295[sec]ですので,71分くらいで飽和します(注:計算間違ってたので直しました).
- アナログ信号をanalogRead()で取得するとintすなわち2バイトの値が返ってきますが,精度は10bit,つまり 0〜1023の整数値が返ってきます.
- unsigned と signed の挙動の違いを確認するため,data[1]の方は512を引いて -512〜511 の整数値を送信しています.
- 送信したいデータは時刻が3バイト,アナログ信号がそれぞれ2バイトで計7バイトになります.これを配列buffer[0]〜buffer[6]に格納します.
- サンプル周期4[msec]で回しています.端子D12から確認用のパルスを出しています.
終端バイトとエスケープの設定:
- 一回分の送信データの末尾に終端バイトDLMを付加することにします.ここでは0x80です.
- もし buffer[0]〜buffer[6] にDLMと同じバイトが含まれていたら,別のバイトESCに置き換えます.ここでは0xFFです.
- 置き換えを行ったら,buffer[7] の第0〜第6ビットの対応するところを1にして記録します.ここで,buffer[7]の第7ビットは使わないので,buffer[7]自身が DLM=0x80と一致することはあり得ません.
- buffer[0]〜buffer[6]に,エスケープ情報buffer[7]と終端バイトbuffer[8]を加えて,計9バイトを送信します.
Arduino側プログラム:
const int analog_0 = 0; const int analog_1 = 1; const int sample_out = 12; const long int dT = 4000; const byte DLM = 0x80; const byte ESC = 0xFF; boolean sample_flag; long int time_us; /* current time [usec] */ void setup() { pinMode(sample_out, OUTPUT); Serial.begin(57600); sample_flag = true; } void loop() { int data[2]; byte buffer[9]; time_us = micros(); /* data 0, 1 */ data[0] = analogRead(0); data[1] = analogRead(1); digitalWrite(sample_out, sample_flag?HIGH:LOW); sample_flag = !sample_flag; buffer[0] = byte((time_us>> 8) & 0xFF); buffer[1] = byte((time_us>>16) & 0xFF); buffer[2] = byte((time_us>>24) & 0xFF); buffer[3] = byte( data[0] & 0xFF); buffer[4] = byte((data[0]>> 8) & 0xFF); buffer[5] = byte( data[1] & 0xFF); buffer[6] = byte((data[1]>> 8) & 0xFF); buffer[7] = 0; for (int i=0; i<=6; i++) { if (buffer[i] == DLM) { /* if the delimiter appears*/ buffer[7] += 1<<i; buffer[i] = ESC; /* escape */ } } buffer[8] = DLM; /* send delimiter */ Serial.write(buffer,9); delayMicroseconds( max(time_us+dT-micros(), 0) ); }
Processing側では次のようにコーディングします.
- Serial.bufferUntil(DLM)を使用し,終端バイトDLMを受け取ったら割り込みをかけるように設定します.コールバック関数はserialEvent()です.
- Serial.readBytesUntil(DLM)を使用し,終端バイトDLMまでのデータを読み込みます.(余分なデータが残っていると負の数が返されます. )
- あとは基本的にArduino側のプログラムと逆の操作です.エスケープbitが立っていた場合は当該バイトをDLMに差し替えます.
- 【重要】以前にも書きましたが,Arduinoのint型が2バイトなのに対しProcessingのint型は4バイトです.元の2バイト整数が2の補数で正負表現されていたとしても,これを4バイト整数に代入すると正しく正負が表現されません.たとえば -1=0xFFFF が 0x0000FFFF = +65535 になってしまうわけです.そこで,0x8000以上の数値は負の数と解釈するように修正が必要です.
Processing側プログラム:
import processing.serial.*; byte DLM = byte(0x80); int[] data = new int[3]; void setup() { Serial myPort; size(320, 200); frameRate(30); println(Serial.list()); String portName = Serial.list()[0]; myPort = new Serial(this, portName, 57600); myPort.clear(); myPort.bufferUntil(DLM); } void draw() { background(0); fill(255); for (int i=0; i<data.length; i++) { textAlign(RIGHT); text( nfc(data[i],0), 150, 50*i+50 ); textAlign(LEFT); text( ": data(" + nf(i,1) + ")", 170, 50*i+50 ); } } void serialEvent(Serial port) { byte[] buffer = new byte[9]; if (port.readBytesUntil(byte(0x80),buffer) <0) { port.clear(); } else { for (int i=0; i<=6; i++) { /* check the escape flag */ if ( ((buffer[7]>>i) & 0x01) == 1) { buffer[i] = DLM; /* restore! */ } } data[0] = (((buffer[2]<<24)&0xFF000000) | ((buffer[1]<<16)&0xFF0000) | ((buffer[0]<<8)&0xFF00)); data[1] = ( ((buffer[4]<<8)&0xFF00) | (buffer[3] & 0xFF) ); data[2] = ( ((buffer[6]<<8)&0xFF00) | (buffer[5] & 0xFF) ); if (data[2] >= 0x8000) data[2] -= 0x10000; /* from unsigned to signed */ } }
以上,学生が通信でハマったときに「ここ見といて」で済ませられるようにTipsをまとめておきました.もっと洗練された書き方もできると思いますが,とりあえず挙動を理解するためのサンプルということでこれ以上は詰めていません.なにかお気づきの方がおられましたらメールでご指摘いただければ幸いです.
※メモ:Serial.print(val,BYTE)を使うとSerial.printでも生のバイトデータを送信できますが,送られるのは1バイトだけです.valがintやlongなどの多バイト型であっても,実際に送られるのは最下位の1バイトです.