シリアル通信の回では、Serial1.write で1バイト(0〜255)を送りました。今回も同じシリアル通信で、255 を超える値を送ります。サッカーロボットでは、サブがジャイロの角度 0〜359 をメインへ送るときに、この方法を使っています。値を3バイトに分け、先頭にヘッダーという目印を付けます。配線はシリアル通信の回と同じ4本です。
値を7ビットずつに分け、先頭のバイトだけ 128 以上にする。この方法は、次の記事のアイデアをお借りしています。
ArduinoからProcessingへint型のデータを送る(Imaginary Code)1バイトでは足りない ― 角度は 0〜359
Serial1.write() が1回で送るのは1バイト、つまり 0〜255 です。ジャイロの角度 0〜359 は、1回では送れません。
そこで、値を何バイトかに分けて送ります。
ところが分けたとたん、別の問題が出ます。線の上を流れるのは、ただのバイトの列です。メインが途中から読みはじめると、どのバイトが先頭なのか分かりません。
だから先頭のバイトに目印を付けます。これがヘッダーです。
7ビットずつに分ける ― ヘッダーは 128 以上
値を8ビットずつに分けると、データのバイトは 0〜255 のどれにもなります。目印に使える数が残りません。
そこで7ビットずつに分けます。データのバイトは 0〜127 に収まるので、128 以上はヘッダー専用にできます。
| 送る順 | 中身 | とりうる値 |
|---|---|---|
| head | 残りの上のビット + 128 | 128〜255 |
| high | まん中の7ビット | 0〜127 |
| low | 下の7ビット | 0〜127 |
head には7ビットまで入ります。7ビット × 3 で21ビット。送れるのは 0〜2,097,151 です。3バイト送るのにかかる時間は、115200 で約0.26ms です。
送るプログラム ― 0 から 359 までくり返し送る
サブから 0、1、2 … 359 と順に送り、359 まで行ったら 0 に戻ります。
// サブ(Pico B)― 送る側
int angle = 0;
void setup() {
Serial1.setTX(0); // GP0 から送る
Serial1.setRX(1); // GP1 で受ける
Serial1.begin(115200); // 速さは送受で同じに
}
void loop() {
sendIntData(angle); // 3バイトに分けて送る
angle++; // つぎの角度へ
if (angle > 359) { angle = 0; } // 359 の次は 0 に戻す
delay(100); // 目で追えるようにゆっくり
}
// 値を3バイトに分けて送る
void sendIntData(int value) {
byte low = (byte)(value & 127); // 下の7ビット
byte high = (byte)((value >> 7) & 127); // まん中の7ビット
byte head = (byte)(((value >> 14) & 127) + 128); // 残りの上のビット+128
Serial1.write(head); // 先頭から順に送る
Serial1.write(high);
Serial1.write(low);
}
値を分けているのは sendIntData() の3行です。
| 書き方 | はたらき | 359 のとき |
|---|---|---|
| value & 127 | 下の7ビットだけを残す | 103 |
| value >> 7 | 7ビット右へずらす(128 で割る) | 2 |
| value >> 14 | 14ビット右へずらす | 0 |
| + 128 | 先頭のビットを 1 にする | 0 + 128 = 128 |
送る順は head → high → low です。メインはこの順で届くと決めて読みます。
受けるプログラム ― 128 以上を待って、3バイトそろえる
// メイン(Pico A)― 受ける側
void setup() {
Serial.begin(115200); // パソコン確認用(USB)
Serial1.setTX(0); // GP0 から送る
Serial1.setRX(1); // GP1 で受ける
Serial1.begin(115200); // サブとの通信用
}
void loop() {
while (Serial1.available() >= 3) { // 3バイトたまっているあいだ
int head = Serial1.read(); // 1バイト取り出す
if (head >= 128) { // ヘッダーを見つけた
int high = Serial1.read();
int low = Serial1.read();
if (high < 128 && low < 128) { // データは必ず 127 以下
int value = ((head - 128) << 14) | (high << 7) | low;
Serial.println(value); // シリアルモニタに出す
}
}
// 128 未満はヘッダーではない。捨てて次へ
}
}
メインがやっていることは3つです。
| 書き方 | やっていること |
|---|---|
| available() >= 3 | 3バイトたまるまで読まない。1組がそろう前に読みはじめないためです |
| head >= 128 | ヘッダーかどうかを見る。128 未満なら、その1バイトを捨てる |
| << と | | 左へずらして元の位置に戻し、3つをつなぐ |
359 なら (0 << 14) | (2 << 7) | 103。2 を7ビット左へずらすと 256、そこに 103 をつないで 359 に戻ります。
メインを USB でパソコンにつなぎ、シリアルモニタを 115200 で開きます。0, 1, 2 … 359, 0, 1 … と並べば成功です。
ずれても自分で戻る ― 128 未満は捨てる
メインが読みはじめたとき、バッファの先頭が1組の先頭とは限りません。途中の1バイトが落ちることもあります。
high < 128 && low < 128 の1行は、途中の1バイトが落ちたときのためにあります。そのときメインは、次の組のヘッダーを low として読みます。データは必ず 127 以下なので、128 以上がまざった組は捨てます。
最新の値を取る ― 関数にまとめる
ロボットが使うのはいちばん新しい角度だけです。受ける部分を関数にして、最後に読めた1組を返します。
// メイン ― 最新の角度を返す
int readAngle() {
static int result = 0; // 前回の値を覚えておく
while (Serial1.available() >= 3) {
int head = Serial1.read();
if (head >= 128) {
int high = Serial1.read();
int low = Serial1.read();
if (high < 128 && low < 128) {
result = ((head - 128) << 14) | (high << 7) | low; // 上書きしていく
}
}
}
return result; // 最後の1組が残る
}
void loop() {
int angle = readAngle();
// ここから先は angle を使う
}
static を付けた変数は、関数を抜けても値が消えません。新しい組が届いていない周は、前回の値をそのまま返します。
while で読み切るのは、シリアル通信の回の「最新の値を取る」と同じ考え方です。たまっている組を古い順に読み、最後の1組だけが result に残ります。
動かないときに見るところ ― 症状から原因を引く
| 症状 | 原因 | 対処 |
|---|---|---|
| 何も出ない | 配線かボーレートがちがう | シリアル通信の回の 1〜100 のプログラムに戻して、先に確かめる |
| まるでちがう値が出る | 送る順と読む順がちがう | 送る側も読む側も head → high → low にそろえる |
| 数が飛ぶ | バッファがあふれている | メインの loop() から delay() を外す |
| 値が変わらない | サブが止まっている | サブの loop() が回っているか、LED を点滅させて見る |
ロボカップジュニア2027 に向けて、レスキュー(災害救助)とサッカーの2種目に取り組みます。プログラミングと電子工作の経験は問いません。