目次 [ Contents ]
Arduinoでは、変数や状態の確認、あるいはPC側からデータを送信するためにSerial.printをよく使います。かっこよく言えば、デバッグってヤツです。簡単で使い勝手がいいんですが、実は弊害もあります。
それはSerial.printを実行することで時間を取ってしまうことです。シビアな反応を必要としない処理であれば全然問題ない話ですが、何かのコントロールをさせたいとか、リアルタイム性を重視したスケッチを書きたいとなるとかなり気になってきます。
ということでこの記事ではSerial.printの出す遅延について書いていきます。
Serial.printにかかる時間
どのくらいのロスか
可変抵抗器で読んだ値でサーボモータを動かすような処理をしながらSerial.printを挟んでいるとスゴくモサモサ感が出ます。Serial.printを外せば問題ないので明らかにそれが原因です。
どれぐらいの遅延かはストップウォッチで測れないので、とりあえず、どんなもんか測定できるようなスケッチを組みます。
unsigned long time_intval = micros(); unsigned long duration [2] = {}; boolean report = false; void setup() { Serial.begin(9600); } void loop() { duration [report] = micros() - time_intval; time_intval = micros(); if (report) { Serial.print("Ser:"); Serial.print(duration[0]); Serial.print(" Thr:"); Serial.print(duration[1]); Serial.println(); } report = !report; }
ループの中でSerial.printする回としない回を作り、それぞれにかかった時間をduration[]に収めます。そしてその時間を見比べると、
「Ser」がシリアル通信した場合、「Thr」はスルーした場合のマイクロ秒です。
結構違います。
普段、シリアルモニタに書き出す文字数によって、遅延の体感は変わります。そこで、Serial.printするけど文字を表示しない、つまり空回しと比較するとどうなるでしょうか?
if (report) { Serial.print("Ser:"); Serial.print(duration[0]); Serial.print(" Thr:"); Serial.print(duration[1]); Serial.println(); } else { Serial.print(""); Serial.print(""); Serial.print(""); Serial.print(""); } report = !report; }
やはり文字数がキーポイントです。
1文字あたりのロス
for文で文字数を増やしていき、その差を調べてみます。
unsigned long time_intval = micros(); unsigned long duration; byte repeat; void setup() { Serial.begin(9600); } void loop() { duration = micros() - time_intval; time_intval = micros(); for (byte i = 0 ; i < repeat ; i++) Serial.print("@"); Serial.print(":"); Serial.print(duration); Serial.println(); repeat++; }
10文字以降は大体1040マイクロ秒づつ増えていきます。
ボーレートでの比較
今までの検証では全て通信速度が9600bpsでした。単純計算だと1秒間に1200バイト。1バイト送るために0.83ミリ秒かかる計算です。あくまで理論値ということや、データ転送でのエラーロスとか考えると…まあ、なんとなく上記に近い数値な気がします。
逆に通信速度を上げれば、データ送信の時間が短縮されるので、遅延は短くなるハズ。今度はそれを調べてみます。
unsigned long time_intval = micros(); unsigned long duration [2] = {}; boolean report = false; void setup() { Serial.begin(9600); }
Sample 1スケッチをベースにボーレートを変更していき、比較します。
結果がこちら。
ボーレート(bps) | Duration(μs) | 文字数 | 1文字当たりの遅延(μs) |
4800 | 37516 | 16 | 2344 |
9600 | 18704 | 16 | 1169 |
19200 | 8824 | 15 | 588 |
38400 | 4404 | 15 | 293 |
57600 | 2872 | 15 | 191 |
74880 | 2280 | 15 | 152 |
115200 | 1432 | 15 | 95 |
結果
かなりざっくりな検証ですけど、それでもSerial.printには時間がかかるのがハッキリしました。速度や文字数も注意しつつ。まあ根本的に、通信はインターバルの電圧変化によるものなので、当たり前なんでしょうけど…やってみた価値はあったのではないでしょうか。
対処方法
Serial.printが必須のスケッチでは、こういった遅延を回避するにはクロックとボーレートを上げていくしかない思います…。ただ、プロトタイピング中の確認では欲しいけど、最終的にはいらないという事であれば方法があります。
変数で切り替え
要らなきゃ一個づつ削除って方法が一番確実ですが、それでは再度見たい時、特にスケッチが長くなってくるとエラいことになります。そこでそういった検証の類はひとつの変数か、define定数で一括で操作してしまうと楽になります。
仮にArduinoのD10ピンにスイッチを付けて、その状態をシリアルモニタに返すスケッチを書いたとします。
#define SW 10 const boolean print_go = 1; void setup() { Serial.begin(9600); pinMode(SW, INPUT_PULLUP); } void loop() { boolean sw_stat = !digitalRead(SW); if (print_go) { Serial.println(sw_stat); } }
2行目のprint_goをtrueか(1)、falseか(0)変更することで、15行目以降のSerial.printを実行するか否かを指定できます。こうすると確認の書き込みでは実行しておいて、最終的に書き込む時は省くことができます。
実は変数よりdefineで定義しておく方がスケッチ量を節約できるので、オススメです。
#define print_go 1
ただ、この方法だとfalse(0)にしていても、命令自体はスケッチへ組み込まれてしまうので、無駄なif判定が常に行われてしまいます。
それも回避する方法があります。
プリプロセッサで切り替え
よく使う#defineはプリプロセッサと言われる処理です。これはスケッチを書き込む処理をする前にスケッチの書式を整理してくれるものです。例えば、
#define アイツ 単車
とすれば、書き込む前に「アイツ」と書かれた場所は全部「単車」に変換してくれます。スケッチ上に「単車」と直接3回書いていたとすれば3つ分の記憶容量を使うけど、defineで「アイツ」=「単車」と定義しておけば、3箇所が「アイツ」1つ分の記憶容量で済むので節約になるわけです。
そんなプリプロセッサですが、#if() 〜 #endifというものもあります。これは普段のifとほぼ同じ使い方です。ただ、falseの場合、これに挟まれた命令はプログラム上には無かったことになります。
#define SW 10 #define print_go 1; void setup() { #if(print_go) Serial.begin(9600); #endif pinMode(SW, INPUT_PULLUP); } void loop() { boolean sw_stat = !digitalRead(SW); #if (print_go) Serial.println(sw_stat); #endif }
#の行には;(セミコロン)がないことに注意してください。
Arduinoへの書き込む前に、命令自体を抹消するのでスケッチ容量が節約できます。ただし、この#if〜#endの中へ通常必要な変数や、処理を入れてしまっていると、コンパイル時にエラーが出ます。「どこにあんの?」と。
便利でオススメですが、取り扱いも注意が必要なのでお気をつけ下さい。