Imaginary Code

from kougaku-navi.net

ArduinoからProcessingへint型のデータを送る

ArduinoでanalogRead()を使ってアナログ端子を読み取ると0〜1023の整数値(int型)を返します。Arduinoで扱われるint型は2バイトです。これに対し、シリアル通信では1バイトずつデータが送られるため、2バイト以上のデータを送受信するにはちょっと工夫がいります。

情報量が減ることを気にしないのであれば4で割って1バイト(0〜255)に収めてしまうというやり方もありますが、ここではint型のデータを上位バイトと下位バイトの2回に分けてデータを送るという方法を紹介します。

※逆バージョンはこちら。

Arduinoのコード

A0端子から読み取ったセンサ値をPCに送信します。ヘッダ('H')は、あるタイミングで受け取ったデータが上位バイトなのか下位バイトなのかを区別するための「見出し」として機能します。

void setup() {
  Serial.begin(9600);
}

void loop() {
  int value = analogRead(0);
  
  Serial.write('H');             // ヘッダの送信
  Serial.write(highByte(value)); // 上位バイトの送信
  Serial.write(lowByte(value));  // 下位バイトの送信
}
Processingのコード

Arduinoから送られてくるデータを受信し、コンソールに表示します。

import  processing.serial.*;

Serial  serial;

void setup() {  
  size(400, 250);
  serial = new Serial( this, Serial.list()[0], 9600 );
}

void draw() {
  background(10);
}

void serialEvent(Serial port) {  
  if ( port.available() >= 3 ) {  // ヘッダ + 上位バイト + 下位バイト で合計3バイト
    if ( port.read() == 'H' ) {  // ヘッダ文字を見つけたところから読み取る
      int high = port.read();   // 上位バイト読み込み
      int low = port.read();    // 下位バイト読み込み
      int recv_data = high*256 + low;  // 上位・下位を合体させる
      println(recv_data);  // 結果の表示
    }
  }
}
受信したデータをグラフとして表示する

センサのテストでよく使うのでこれも載せておきます。コードの簡単化のために、時系列データの更新のところで配列要素の全シフトを行うという手抜きをやっていますが、現実問題として計算効率がよくないので、実際はリングバッファにするなど工夫してください。


import  processing.serial.*;

Serial  serial;
int[]   data;

void setup() {  
  size(400, 250);
  data = new int [width];
  serial = new Serial( this, Serial.list()[0], 9600 );
}

void draw() {
  background(10);
  strokeWeight(2); 
  stroke(0, 255, 0);

  // グラフの描画
  for (int i=0; i<data.length-1; i++) {
    line( i, convToGraphPoint(data[i]), i+1, convToGraphPoint(data[i+1]) );
  }
}

void serialEvent(Serial port) {  
  if ( port.available() >= 3 ) {
    if ( port.read() == 'H' ) {
      int high = port.read();      
      int low = port.read();
      int recv_data = high*256 + low;
      
      // 時系列データを更新
      for (int i=0; i<data.length-1; i++) {
        data[i] = data[i+1];
      }
      data[data.length-1] = recv_data;
    }
  }
}

float convToGraphPoint(int value) {
  return (height - value*height/1024.0);
}
参考文献

上記の方法は以下の文献に解説があります。

Arduino Cookbook: Recipes to Begin, Expand, and Enhance Your Projects

Arduino Cookbook: Recipes to Begin, Expand, and Enhance Your Projects

【追記】 ヘッダと数値が同じ値になる問題

読み込みはじめのタイミングでたまたまヘッダではなく上位バイトあるいは下位バイトにあたる部分を読んでいて、なおかつその数値のなかにヘッダと同じ値('H' = 72)が含まれていた、ということが起こるとデータがズレて解釈されてしまいます。

アプリケーションによっては確率的にこの問題を無視してしまう方針も採り得ますが、実験データ採取などで正確性を要するような場合にはそうもいきません。というわけで、よりよい方法を以下で解説します。

ヘッダと数値が同じ値にならないようにしたバージョン

2バイトのデータを8ビットずつ分けるのではなく、2ビット・7ビット・7ビットという分け方をします。それぞれ1バイトのデータとして送るのですが、2ビットのデータには128を足します。こうすることで128以上の値であれば先頭データであると判別でき、それ以降のデータは7ビットで表現される値(0〜127)のため、先頭と同じ値になることは絶対にありません。


実装例を以下に示します。

Arduinoのコード
void setup() {
  Serial.begin(9600);
}

void loop() {
  int value = analogRead(0);  

  byte low  = value & 127;
  byte high = (value >> 7) & 127;
  byte head = ((value >> 14) & 127) + 128;

  Serial.write(head);
  Serial.write(high);
  Serial.write(low);
}
Processingのコード
import  processing.serial.*;

Serial  serial;

void setup() {  
  size(400, 250);
  serial = new Serial( this, Serial.list()[0], 9600 );
}

void draw() {
  background(10);
}

void serialEvent(Serial port) {  
  if ( port.available() >= 3 ) {    
    int head = port.read();
    if ( head >= 128 ) { // 128以上であれば先頭データなので、それに続くデータを読み取る
      int high = port.read(); // 2番目のデータ
      int low  = port.read(); // 3番目のデータ

      // データを復元する
      int recv_data = ((head-128) << 14) + (high << 7) + low;      
      println( recv_data );
    }
  }
}
グラフを表示するProcessingのコード
import  processing.serial.*;

Serial  serial;
int[]   data;

void setup() {  
  size(400, 250);
  data = new int [width];
  serial = new Serial( this, Serial.list()[0], 9600 );
}

void draw() {
  background(10);
  strokeWeight(2); 
  stroke(0, 255, 0);

  // グラフの描画
  for (int i=0; i<data.length-1; i++) {
    line( i, convToGraphPoint(data[i]), i+1, convToGraphPoint(data[i+1]) );
  }
}

void serialEvent(Serial port) {  
  if ( port.available() >= 3 ) {    
    int head = port.read();
    if ( head >= 128 ) {   // 128以上であれば先頭データなので、それに続くデータを読み取る
      int high = port.read();  // 2番目のデータ
      int low  = port.read();  // 3番目のデータ

      // データを復元
      int recv_data = ((head-128) << 14) + (high << 7) + low;      

      // 時系列データを更新
      for (int i=0; i<data.length-1; i++) {
        data[i] = data[i+1];
      }
      data[data.length-1] = recv_data;
    }
  }
}

float convToGraphPoint(int value) {
  return (height - value*height/1024.0);
}


この方法を教えてくれた@y_betaさんに感謝。

【2015/1/16】 コードを微妙に修正。serialEvent()の中で使われているdataという変数の初期化をシリアル通信の初期化より前にした。serialEvent()はsetup()の終了を待たずに動き始めるらしく、そのタイミングでserialEvent()内で使われている変数が初期化されてないとエラーが起こる。