シリアル通信は、マイコンでよく使われる通信です。パソコンとのやりとりも、センサーとのやりとりも、この通信で動いています。今回はそれを Pico 2個のあいだで使い、センサーを読む担当とモーターを回す担当に分けます。1台のロボットの中のマイコン同士の話です。まずは 1 から 100 までを送って受け取り、つながったことを目で確かめます。
仕事を分担する ― サブが計算して、メインが動かす
ロボットがやっていることは、大きく2つに分かれます。センサーを読んで、どう動くかを計算すること。そしてその結果を判断し、モーターを回すことです。
この2つを1個のマイコンにやらせると、どちらかが待たされます。サッカーロボットなら、loop() の中でボールの向きを計算しているあいだ、モーターへの指示は止まっています。
そこで、Pico を2個使って分担します。分けるときは、どちらがメインでどちらがサブかをはっきり決めておきます。
| 担当 | |
|---|---|
| サブ(Pico B) | センサーを読んで計算し、結果をメインに送る |
| メイン(Pico A) | 受け取った結果をもとに、モーターを回す |
モーターを持っているほうをメインとします。最後に動きを決める係で、重い計算はサブが引き受けます。
サブが渡すのは結論だけです。「ボールは右30度」なら 30 だけ。センサーの生の値は送りません。この受け渡しがシリアル通信です。
つなぐ線は4本 ― TX・RX・GND・3.3V
使うのは UART というしくみです。ロボットで「シリアル通信」と言うとき、たいていこの UART を指します。
ピンの名前に出てくる TX は送る口、RX は受ける口です。Transmitter(送信)と Receiver(受信)の頭文字で、GP0 (TX) なら「GP0 から送り出す」という意味になります。
信号そのものは、送る線1本と GND があれば届きます。ただし今回はメインからサブへ信号と電源も通して、4本にします。
つなぐときの決まりは3つです。TX は相手の RX へつなぎます。TX どうしでは、データは伝わりません。GND は共通にします。電源はメインの 3V3(OUT) からサブの VSYS へ入れ、サブに電気を送ります。
メインもサブも Pico なので、信号は3.3V同士。そのままつないでかまいません。ただし5Vで動く相手の TX を直接つなぐと、Pico が壊れます。
サブに書き込むときは、3.3V の線を抜きます。つないだまま USB をさすと、USB の電気がメインの 3.3V 側へ逆に流れこみ、メインが壊れることがあります。
| 線 | 役割 |
|---|---|
| サブ TX → メイン RX | 計算した結果を送る(これが主役) |
| メイン TX → サブ RX | 問い合わせや指示を送る |
| GND | 0V の基準をそろえる |
| 3.3V | メインからサブへ、電源を配る |
どのピンが基板のどこにあるかは、公式の図で確かめられます。印刷して手元に置いておくと早いです。
Raspberry Pi Pico のピン配置(Raspberry Pi 公式)無線つきの Pico W は別の図です。同じページの少し下にあります。
実際にブレッドボードで組むと、こうなります。
VSS と出ます)。| 名前 | つながる先 | 相手 |
|---|---|---|
| Serial | USBケーブル | パソコンのシリアルモニタ |
| Serial1 | UART0(GP0=TX, GP1=RX) | もう1個の Pico |
| Serial2 | UART1(GP4=TX, GP5=RX) | さらに別の相手 |
Serial に出してもパソコンのシリアルモニタに出るだけで、相手の Pico には届きません。相手に送るのは Serial1 です。
UART0 と UART1 がどのピンに出ているかは、公式のピン配置図 ↗で確かめます。GP0・GP1、GP4・GP5 のほかにも割り当てられるピンがあります。
ボーレートを合わせる ― 競技では 115200
ボーレートは、1秒間に何回信号を切り替えるかを表す数字です。受ける側は、この速さだけを頼りに「どこまでが1ビットか」を数えています。
| ボーレート | 1バイトにかかる時間 | 使いどき |
|---|---|---|
| 9600 | 約1ms | 最初につながるか試すだけ |
| 115200 | 約0.09ms | 競技ではこちら |
9600 は遅すぎます。1バイト送るだけで 1ms かかり、その間 loop() が進みません。115200 を標準にします。
送る側と受ける側で必ず同じ数字にします。
Serial1.write で1バイト送る
送るときに使うのは Serial1.write() です。かっこの中の数値が、そのまま1バイト線に流れます。
| 書き方 | 線に流れるもの |
|---|---|
| Serial1.write(25) | 0x19 を1バイト |
| Serial1.write(0xFF) | 0xFF を1バイト |
| Serial1.write('A') | 0x41 を1バイト |
write('A') と write(65) は同じです。文字も数も、線の上では同じ1バイトです。
送れる数値は 0〜255 だけ。そのかわり速くて単純です。変換もなく、ただ1バイト流すだけ。サッカーのように1周期を短くしたい競技では、これが効きます。
送るプログラム ― 1から100までくり返し送る
サブからメインにデータが送れることを確認するために、サブから 1、2、3 … 100 と順に送り、100 まで行ったら 1 に戻ります。
// サブ(Pico B)― 送る側
byte value = 1;
void setup() {
Serial1.setTX(0); // GP0 から送る
Serial1.setRX(1); // GP1 で受ける
Serial1.begin(115200); // 速さは送受で同じに
}
void loop() {
Serial1.write(value); // 1バイト送る
value++; // つぎの番号へ
if (value > 100) { value = 1; } // 100 の次は 1 に戻す
delay(100); // 目で追えるようにゆっくり
}
setTX と setRX は、UART0 をどのピンで使うか設定します(GP0・GP1 であればデフォルトなので省略できます)。どのピンを使っているかがコードを見て分かるように、書いておきます。別のピンに変えるときも、ここの数字を直すだけです。
どちらも begin() より前に置きます。あとから呼んでも効きません。
delay(100) は、目で追えるようにわざと遅くしてあります。本番ではもっと短くします。
受けるプログラム ― 受け取ってシリアルモニタに出す
メインは、届いたバイトをそのままパソコンに流すだけです。
// メイン(Pico A)― 受ける側
void setup() {
Serial.begin(115200); // パソコン確認用(USB)
Serial1.setTX(0); // GP0 から送る
Serial1.setRX(1); // GP1 で受ける
Serial1.begin(115200); // サブとの通信用
}
void loop() {
while (Serial1.available()) { // 届いている分を読み切る
byte value = Serial1.read(); // 1バイト取り出す
Serial.println(value); // シリアルモニタに出す
}
}
メインを USB でパソコンにつなぎ、Arduino IDE のシリアルモニタを 115200 で開きます。
| シリアルモニタの出かた | 読みとれること |
|---|---|
| 1, 2, 3 … 100, 1, 2 … と並ぶ | 配線もボーレートも合っている |
| 何も出ない | TX と RX が逆、または GND がつながっていない |
| でたらめな値が出る | ボーレートがちがう |
| 数が飛ぶ(5 のつぎが 8 など) | どこかでバイトが落ちている |
連番にしておくのがコツです。同じ値だと1つ落ちても気づけませんが、1ずつ増やしておけば飛んだ瞬間に分かります。
ここで使った命令は3つだけです。
| 命令 | 役割 |
|---|---|
| Serial1.write(x) | 1バイト送る |
| Serial1.available() | いま何バイト届いているか |
| Serial1.read() | 1バイト取り出す(取り出した分は消える) |
郵便ポストで考える ― バッファはあふれる
受け側には郵便ポストがあります。届いたバイトはいったんそこにたまり、read() で取り出すと消えます。これをバッファといいます。
大事なのは順番です。read() が取り出すのは、いちばん古いバイト。新しく届いたものはうしろに並ぶので、追い越しは起きません。
つまり、ポストに3つたまっているとき、1回目の read() で出てくるのはいちばん最初に届いたものです。いまの値ではありません。
ポストの大きさには限りがあります。取りに行く前に次が来続けると、入らなかった分は捨てられます。
| あふれる原因 | 何が起きているか |
|---|---|
受け側の loop() に長い delay() がある | 取りに行く回数が減って、ポストが満杯になる |
| 送る側が速すぎる | 受け側が1周する前に、次が届いてしまう |
まず、送る側と受ける側の両方で手を打ちます。受け側の loop() に delay() は置きません。サブ側も送りっぱなしにせず、メインの周期に合わせて送ります。
そのうえで、取り出し方そのものにコツがあります。
最新の値を取る ― while で読み切る
ロボットが欲しいのはいまの値です。0.3秒前の値は要りません。
ところが if で1周に1つずつ取り出すと、たまったぶんだけ遅れていきます。通信は生きているのに、ロボットは少し前の世界を見て動くことになります。
そこで while で届いている分を読み切ります。ポストの手紙を古いものから順に全部取り出し、最後に手に取った1通が、いちばん新しい手紙。それだけを使います。
| if で1バイトずつ | while で読み切る | |
|---|---|---|
| 1周で取り出す数 | 1バイトだけ | 届いている分すべて |
| たまったとき | 遅れがたまっていく | その場で追いつく |
| 手に入る値 | 古い順に1バイトずつ | いちばん新しい1バイト |
// メイン ― 最新の値だけ残す
byte latest = 0;
void loop() {
while (Serial1.available()) {
latest = Serial1.read(); // 上書きしていく。最後の1つが残る
}
// ここから先は latest を使う
}
while を抜けた時点で、latest にはいちばん新しい値が入っています。古い値は上書きされて消えています。
ひとつだけ弱点があります。この書き方では、届かなくなったことに気づけません。サブが止まっても latest は最後の値のまま残るので、メインはそれを「いまの値」として使い続けます。気になるときは、受け取った時刻を millis() で覚えておき、しばらく来なければ止める、という手が使えます。
1バイトは10ビット ― スタート・データ8・ストップ
ここまで「1バイト送る」と書いてきましたが、線の上を流れているのは電圧の高い・低いだけです。0 と 1 が並んでいるだけで、「ここからが1バイト」とは書いてありません。
では、受け側はどうやって区切っているのか。ここを見ておくと、ボーレートをそろえる理由まで一度に分かります。
まず、送っていないときの線は 1(HIGH)です。ずっと 1 のまま待っています。
送るときは、いきなり 0 に落とします。これがスタートビットです。受け側が見張っているのは、この落ちた瞬間だけです。
| 順番 | 値 | 中身 |
|---|---|---|
| アイドル | 1 | 何も送っていないとき |
| スタートビット | 0 固定 | ここから始まる、の合図 |
| データ 8ビット | 0 か 1 | 下位のビットから順に |
| ストップビット | 1 固定 | 線を 1 に戻す |
データビットには「自分はデータです」という印がありません。落ちたところから何ビットめか、それだけで決まります。つまり値ではなく、時間の位置で見分けています。
受け側は、まず0.5ビット分だけ待ちます。スタートビットの真ん中に来るためです。そこでまだ 0 かを確かめ(1 に戻っていればノイズとして捨てます)、あとは1ビットずつ、ビットの真ん中をのぞいていきます。図の●がその場所です。
最後にもう1ビット進んで、1 に戻っているかを見ます。ここが 0 なら、どこかがおかしいと分かります。
ストップビットが 1 固定なのには理由があります。ここで線を 1 に戻しておかないと、次のバイトのスタートビットが「落ちる」ことができません。続けて送っても、1バイトごとに必ず「落ちる瞬間」が作れるようにしてあります。
ストップビットの長さは1ビット分ですが、これは最短です。次を送るまでは 1 のまま続きます。
ここでボーレートの話に戻ります。受け側は時間を物差しにして数えているだけなので、1ビットの長さの見積もりがちがうと、のぞく場所がどんどんずれていきます。
| ボーレート | 1ビットの長さ | 1バイト(10ビット) |
|---|---|---|
| 9600 | 104µs | 約1.04ms |
| 115200 | 8.68µs | 約0.087ms |
第3節で出した「1バイトにかかる時間」は、この10ビット分です。8ビットではありません。
ずれは後ろのビットほど積み重なります。最初の1〜2ビットは合っていても、最後のほうは完全に別の場所を読みます。結果、まるでちがう数が届きます。これが「ボーレートがちがうとでたらめな値になる」の正体です。
GND をつなぐ理由も、ここにあります。「高い・低い」は電圧の判定で、その基準の 0V が GND です。GND がつながっていないと、受け側はそもそも「落ちた」ことを正しく見られません。
最後に呼び名です。この形を 8N1 といいます。
| 意味 | ほかに選べるもの | |
|---|---|---|
| 8 | データ長 8ビット | 5〜8。ふつうは 8 |
| N | パリティなし | 偶数/奇数。1ビット足して誤りを見つける |
| 1 | ストップビット 1 | 2。相手が遅いとき用 |
Serial1.begin(115200) と書くと、この 8N1 になります。Pico 同士の短い配線なら、既定のままで困りません。
動かないときに見るところ ― 症状から原因を引く
| 症状 | 原因 | 対処 |
|---|---|---|
| 何も受信しない | TX と RX が逆 | 送る側の GP0 → 受ける側の GP1 になっているか見る |
| でたらめな値が届く | ボーレートがちがう | 両方の Serial1.begin() の数字をそろえる |
| 動いたり動かなかったりする | GND がつながっていない | GND同士を必ず結ぶ。線の半抜けも疑う |
| 数が飛ぶ | バッファがあふれている | 受け側の loop() から delay() を外す。送る間隔を広げる |
| 値が古いまま変わらない | サブが止まっている | サブ側の loop() が回っているか、LEDを点滅させて見る |
| 反応が遅れていく | if で1バイトずつ処理している | while で読み切る形にする |
| サブが起動しない | 3.3V か GND が届いていない | サブの VSYS と GND に電圧が来ているか、テスターで見る |
Serial1 でエラーが出る | Pico用のボードが入っていない | Arduino IDE のボードマネージャで Raspberry Pi Pico/RP2040 を入れる |
ここまでで、数値を1つ渡せるようになりました。ところが実際には、値1つでは足りません。ボールの向きも、距離も送りたくなります。
値が2つ以上になると、どこからが1組なのか分からなくなります。そこで先頭にヘッダーという目印を付けます。255 を超える値を2バイトに分けて送るのも、同じ回でやります。次はそこへ進みます。
ロボカップジュニア2027 に向けて、レスキュー(災害救助)とサッカーの2種目に取り組みます。プログラミングと電子工作の経験は問いません。