Arduino Serial.print(), Serial.write(), Serial.flush() のはたらき
Arduino UNO(ATmega328P)のシリアル通信(UASRT)において「データを送信する」が意味するもの
スケッチ上でSerial.print() やSerial.write() にデータを渡すと、そのデータは送信バッファに書き込まれます。「送信バッファ」とはメモリ(SRAM)に割り当てられた領域のことで、Arduino UNO(ATmega328P)の場合、64バイトあります。これらの関数の役割はここまでです。この先、TXDピンから電気信号としてデータが出ていくまでは、バックグラウンドでハードが行ってくれます。
ATmega328PのUSR(USARTの状態を管理するステータスレジスタ )には、UDR( USART Data Register データの一時保存場所)の空き状況を示すフラグUDRE( USART Data Register Empty )があります。UDRに空きができると、UDREが1になり、_tx_udr_empty_irqという割り込み処理(ISR)が発動します。この割り込み処理は、送信バッファから1バイトを取り出し、UDRに書き込みます。UDRに書き込まれたデータは、いくつかの回路を経由して、最終的には電気信号としてTXDピンから出ていきます。
なお、送信バッファがデータで満杯の時は、Serial.print() やSerial.write() は、送信バッファへのデータの書き込みを保留し、送信バッファのデータが減って空きが生じた時、空いた分だけ送信バッファにデータを書き込みます。
(参考)
megaAVR Data Sheet 200ページ
「完全データシート」 124ページあたり
Serial.flush()のソースから読み取れること
void HardwareSerial::flush()
{
// If we have never written a byte, no need to flush. This special
// case is needed since there is no way to force the TXC (transmit
// complete) bit to 1 during initialization
if (!_written)
return;
while (bit_is_set(*_ucsrb, UDRIE0) || bit_is_clear(*_ucsra, TXC0)) {
if (bit_is_clear(SREG, SREG_I) && bit_is_set(*_ucsrb, UDRIE0))
// Interrupts are globally disabled, but the DR empty
// interrupt should be enabled, so poll the DR empty flag to
// prevent deadlock
if (bit_is_set(*_ucsra, UDRE0))
_tx_udr_empty_irq();
}
// If we get here, nothing is queued anymore (DRIE is disabled) and
// the hardware finished tranmission (TXC is set).
}
Serail.flush()は、while文の中で送信済みフラグ(TXC)とデータレジスタ(UDR)の空き状況を示すフラグ(UDRE)フラグをチェックし、送信が全て完了していれば処理を抜けるようになっています。言い換えれば、送信が完了するまでSerial.flush()を抜けられないので、実質的には送信が完了する時間だけ delay()しているのと同じ結果になります。
delay()との違いは、送信バッファに溜まったデータがすべて送信されるまでどれだけ時間がかかるのかを予め計算して、その値を引数として渡す必用がないことです。
Serial.flush()を使ってみる
送信バッファがいっぱいの状態でSerial.flush()を実行した場合、その直後のSerial.print()の処理速度に与える影響を検証してみました。
Serial.print()は送信バッファが空の状態だとあっという間に処理が終わりますが、送信バッファがいっぱいの状態だと、空きが生じるまで待機するのでその分処理が終わるのが遅くなります。しかし、Serial.flush()を使って送信バッファが空になるのを待てば、そのあとのSerial.print()は速くなるはずです。
下のスケッチは、いずれも64バイトのデータを書き込んで送信バッファをいっぱいにした状態で、もう一度同じ同じ64バイトのデータを書き込むものです。違いは、1回目の書き込みの後にSerial.flush()を挟むか挟まないかです。
結果は、Serial.flush()なし が 266556μs で ありが 380μs でした。
Serial.flush() なしの場合
//Serial.print()で64バイトを1回書き込み後、Serial.print()で64バイトをもう1回書き込み、
//2回目の書き込みの処理時間を計測
//結果 266556µs
char str[64];
long startTime = 0;
long elapsedTime = 0;
void setup() {
Serial.begin(2400);
for (int i = 0; i <63; i++) {
str[i] = '*';
}
str[63] = '\0';
// 書き込み1回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
Serial.println();
startTime = micros();
// 書き込み2回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
elapsedTime = micros() - startTime;
Serial.print(elapsedTime);
Serial.println();
}
void loop(){
}
Serial.flush() ありの場合
//Serial.print()で64バイトを書き込んだ後、Serial.flush()を実行し、さらにSerial.print()で64バイトをもう1回書き込み、
//2回目の書き込みの処理時間を計測
//結果 380µs
char str[64];
long startTime = 0;
long elapsedTime = 0;
void setup() {
Serial.begin(2400);
for (int i = 0; i <63; i++) {
str[i] = '*';
}
str[63] = '\0';
// 書き込み1回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
Serial.println();
Serial.flush();
startTime = micros();
// 書き込み2回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
elapsedTime = micros() - startTime;
Serial.print(elapsedTime);
Serial.println();
}
void loop(){
}
これはあくまで、Serial.flush()がSerial.print() に与える速度的な影響です。Serial.flush()そのものにはdelay()と同じようにプログラムの実行を遅延させる効果があるので、送信バッファに2回書き込むという一連の処理の総体的な時間はSerial.flush()ありの方が当然遅くなります。それを示すのが次の検証です。
Serial.flush()がおよぼす遅延
前述のスケッチの処理時間を計測するポイントを変更したものが下記のスケッチです。
前述のスケッチではSerial.flush()実行後の2回目の書き込みの処理速度だけを計測しましたが、今回は、1回目の書き込みと2回目の書き込みを合わせた処理速度を計測します。
結果は、Serial.flush()なし が 270756μs で ありが 275288μs でした。
Serial.flush()なし
//Serial.print()で送信バッファへ64バイトを書き込んだ後、
//再度、Serial.print()で送信バッファへ64バイトを書き込む
//結果 処理時間 270756
char str[64];
long startTime = 0;
long elapsedTime = 0;
void setup() {
Serial.begin(2400);
for (int i = 0; i <63; i++) {
str[i] = '*';
}
str[63] = '\0';
//測定開始
startTime = micros();
// 書き込み1回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
Serial.println();
// 書き込み2回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
//測定終了
elapsedTime = micros() - startTime;
Serial.print(elapsedTime);
Serial.println();
}
void loop(){
}
Serial.flush()あり
//Serial.print()で送信バッファへ64バイトを書き込んだ後、Serial.flush()を実行し、
//再度、Serial.print()で送信バッファへ64バイトを書き込む
//結果 処理時間 275288
char str[64];
long startTime = 0;
long elapsedTime = 0;
void setup() {
Serial.begin(2400);
for (int i = 0; i <63; i++) {
str[i] = '*';
}
str[63] = '\0';
//計測開始
startTime = micros();
// 書き込み1回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
Serial.println();
Serial.flush();
// 書き込み2回目
for (int i = 0; i <64; i++) {
Serial.print(str[i]);
}
//計測終了
elapsedTime = micros() - startTime;
Serial.print(elapsedTime);
Serial.println();
}
void loop(){
}
Serial.flush()の使いどころ
Serial.flush()を使わずとも、データの送信が完了するまでにかかる時間を計算で割り出してdelay()を実行すれば同じ結果を得られます。
データの送信が完了するまでの時間を算出します。
1バイトのデータはデータビットが8bit、スタートビットとストップビットがそれぞれ1bitなので合計10bit必要です。
例えば、ボーレートが 2400 bps の場合、
1秒間に送信できるデータ: 2400bps / 10bit = 240 byte
1byte を送信するのに必要な時間: 1/ 240 = 0.00416sec (= 4160 µs)
下のスケッチを実行して確認すると理論値に近い 4212µs になりました。
//Serial.print()で1バイトを書き込み、
//送信開始から送信完了までの処理時間を計測する
//結果 4212 µs
char str[64];
long startTime = 0;
long elapsedTime = 0;
void setup() {
Serial.begin(2400);
Serial.print('*');
startTime = micros();
Serial.flush();
elapsedTime = micros() - startTime;
Serial.print(elapsedTime);
Serial.println();
}
void loop(){
}
Serial.print()やSerial.write()を実行し、送信バッファにデータが書き込まれると、そこから先、データが電気信号としてTXDピンから出て行くまでは、ハードがバックグラウンドで処理してくれます。送信バッファへの書き込みが終わった時点で、プログラムは次の処理に進みます。もし、データの送信が完了してから次の処理に移りたい場合は、送信するデータの長さから必要な時間を計算して delay() に渡せば Serial.flush() と同じ結果になるはずです。
しかし、上記のスケッチの実行結果からわかる通り、理論上の処理時間と実際の処理時間には差異があります。この差異がプログラム全体の結果に影響を与える可能性がある場合は、Serial.flush()を使う方が無難かもしれません。