2016年11月17日木曜日

Arduino の Serial.write とうまく付き合う方法 (BLESerial2 編)

概要

Serial.write はArduino の TX ピンから信号を送信することができる命令です
同じような命令に print や println もあります
Serial.write は (特に何も指定しない場合) 1 byte の信号を送信することができます
今回は 1 byte 以上のデータを送信する場合や受取側で考慮するべき点について考えてみました
タイトルに BLESerial2 があるのはデータの受信と送信のデバッグに Bluetooth を使ったからです

環境

  • Arduino IDE 1.6.12
  • Arduino Pro mini (5v, 16MHz)
  • Mac OS X 10.10.5
  • Ruby 2.3.1p112

基本的な使い方

まず、1 byte の情報を送信する方法を考えます

Serial.write(1)

は数字の「1」を 1 byte 分送信します
送信する際はバイト配列として 16 進数に変換して送られるので「0x01」となります

この方法で他のデータを送信すると以下のようになります

  • 「8」・・・ 0x08
  • 「10」・・・ 0x0A
  • 「a」・・・ 0x0A (Serial.write(0x0A)) として送信
  • 「255」・・・ 0xFF
  • 「65535」・・・0xFF

ちなみにテストスケッチは以下の通り

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

void loop() {
  Serial.write(32767);  // ここの値を変更
  delay(2000);
}

255 までは 1 byte で収まる範囲になります
ただ、65535 は 2byte 必要になる情報です
これを wirte すると受け取ったときの情報は 255 のときと同様に「0xFF」になってしまいます
これは冒頭でも説明した用に Serial.write がデフォルトだと 1 byte の情報しか送信できないためです

本来なら 65535 は 0xFFFF として受け取りたいです

int (2byte) の情報を送信する方法

では、次に 65535 を送信する方法を考えます
手順としては以下順番で送信します

  1. 2byte のバイト配列に格納
  2. Serial.write の 2 つ目の引数にバイト配列のサイズを指定
  3. 2byte のバイト配列を送信

になります
スケッチにすると以下の通り

byte data[2];
data[0] = byte(65535);
data[1] = byte(65535 >> 8);
Serial.write(data, 2);

これで 0xFFFF を送信することができるようになりました
が、非常に面倒くさいです

もう少し簡単な方法で記載してみます
int 型の配列を作成して、その配列を最終的にバイトに変換することで送信してみます

int val = 65535;
int data[1];
data[0] = val;
Serial.write((byte*) data, 2);

どうでしょうか、これでも 0xFFFF が送信できたと思います
で、Arduino というか C 言語にある union を使って共用体を作成することでもう少し見やすいコードにすることができます

typedef union {
  int val;
  byte binary[2];
} u;

として共用体を作成し以下のように送信します

u u1;
u1.val = 65535;
Serial.write(u1.binary, 2);

これでも「0xFFFF」を送信することができます
こちらのほうがコード的には読みやすく良い感じになります

で、肝心なのは Serial.write で 1byte 以上のデータを送信するためには

  1. バイト配列を用意する
  2. 第二引数に送信するそのバイト配列のバイト数を指定する

という方法を取る必要があるということになります

float (4byte) の情報を送信する方法

では、今度は float のデータを送信してみましょう
Arduino UNO や Duo では float は 4byte として扱われます
先程 2byte を送信したときのやり方とほぼ同じです

まず float 用の共用体を定義します
ポイントとしては float は 4byte なので先程の int のときとは異なり 4byte 分のバイト配列を定義する必要があります

typedef union {
  float val;
  byte binary[4];
} uf;

これで送信する側で

uf uf1;
uf1.val = 1.0;
Serial.write(uf1.binary, 4);
delay(2000);

とすれば OK です
ちなみに float 型の 1.0 は 16 進数に直すと「0x0000803F」になります
ちょっと説明が遅れましたが、Arduino の場合ビッグエンディアンとして扱われるので変換するときは逆から読む必要があります

で、union を使わないで送信することもできます
これも動作を理解していれば簡単で float 型の配列を用意して、それをバイト変換した上で配列の長さ x 4 のバイトサイズを Serial.write してあげれば OK です

float val = 1.0;
float data[1];
data[0] = val;
Serial.write((byte*) data, 4);

これでも目的のデータを送信することができます

受取側での考慮

受取側はバイト配列としてデータが受け取れることを考慮する必要があります
なので、そのデータが int なのか float なのかはたまた文字列なのかによってデータをコンバートする方法が異なります

例えば今回の例でいうと「65535」は int ではありますが、unsigned int になります
つまり符号なしの 2byte 情報になります
それを受信側で変更すると (Ruby を使います)

s='0xFFFF'
v=[s.to_i(16)].pack('n').unpack('S')[0]

で 65535 が取得できます
これが間違えて unpack するときに「s (符号あり 2byte)」として受信してしまうと「-1」が取得できてしまいます

なので、受信側では送信側が何の型で送信したのかをちゃんと把握してからコンバートする必要があるということです

ついでに float もコンバートしてみましょう
float は符号ありの 4byte 情報になります
Ruby の unpack にはちょうど「f」という単精度浮動小数点数があるのでこれを使って

s='0x0000803F'
v=[s.to_i(16)].pack('N').unpack('f')[0]

とすることで目的の「1.0」を取得することができます
ちなみに pack('n')pack('N') の違いは 16bit or 32bit の違いでどちらもビッグエンディアンの情報を扱うときに使用する Array クラスの関数になります

JavaScript 版

ブラウザで 4byte の float 配列をコンバートする方法です
nodejs だと結構簡単にできるのですが、ブラウザのみだと難しかったのでメモしておきます

function toFloat(data) {
  splited = data.match(/.{2}/g);
  a = new Float32Array(
    new Uint8ClampedArray(
      splited.map(function(a){
        return parseInt(a, 16);
      })
    ).buffer
  );
  return a;
}

で以下のように呼び出すことで「1.0」を取得することができます

val = '0x0000803F'.toString(16).replace(/0x/, "").match(/../g).join("");
type = toFloat(val);
console.log(type);

最後に

Arduino の Serial.write で複数のバイト配列を送信するときのポイントを紹介しました
バイト配列を作るということとバイト数を指定するというポイントを抑えておけば基本は大丈夫だと思います
あとは、union を使うと少しは簡単になるということを覚えておくとよいと思います

そして、実は送信側よりも受信側でバイト配列をコンバートするほうが実はかなり大変だったりします
型を考えなければいけないのはもちろんですが、配列の長さごとに異なるデータが格納させている場合もあるので、その場合は配列を区切って処理する必要があります
例えば先頭 2byte は「タイプ」でその後の 4byte が「値」とかはケースとしてよくあると思います
しかもタイプと値とで型が違うとかは更にコンバートするときにややこしくなると思います
なので、送信する側は少し受信側のことを考えてから送信してあげると良いかと思います

参考サイト

2 件のコメント:

  1. C言語での受け取りプログラムを記載して欲しいです。

    返信削除
  2. シリアル通信での受取りのことだと推測しますが、それであれば Serial.read という関数が標準であるのでそれを使ってもらえばシリアル情報を受け取れると思います

    返信削除