概要
ESP32でのシリアル送信速度を調査してみました。特にESP32-S3などはUSB CDCなどを利用しているので、設定している通信速度と実際の速度が違ったのでどれぐらいの速度相当なのかを確認してみました。
シリアル通信の概要
シリアル通信の速度の単位はbaud(ボー)が利用されています。9600baudだと1秒間に9600個の信号を送受信可能です。通常は1バイト(8ビット)単位で送信を行い、最初にスタートビットと最後とにストップビットをつけるので1バイトあたり、10信号が必要になります。つまり9600baudだとざっくり秒間960バイトの送信が可能です。1バイトあたり約1ミリ秒ぐらいになります。
ちなみに最近の高速通信は1個の信号がHIGHかLOWの2値ではなく、中間レベルの信号も使うようになってきてbaudがそのまま通信速度換算ではなくなっているものがあります。
通信速度別の実測値
//#include "BluetoothSerial.h"
//BluetoothSerial SerialBT;
void setup() {
Serial.begin(115200);
//SerialBT.begin("ESP32_SSP");
}
void loop() {
delay(2000);
Serial.println();
Serial.println("Serial Test");
delay(1000);
char text[101] = {};
for (int i = 0; i < 100; i++) {
text[i] = '0';
}
unsigned long startTime = millis();
for (int i = 0; i < 1000; i++) {
//Serial.println("0"); // 1
Serial.println(text); // 100
//SerialBT.println("0"); // 1
//SerialBT.println(text); // 100
//delay(1); // BT
}
unsigned long endTime = millis();
Serial.printf("TIME = %ld\n", endTime - startTime);
delay(5000);
}
上記のコードで実測をしてみました。1文字と100文字を改行コード付きで1000回送信して、時間計測をしてみました。
ESP32(ATOM)で3バイト送信(1文字+改行コード)
baud | 1000回の実測値(ms) | 1回あたり(ms) | 理論値(ms) |
---|---|---|---|
9,600 | 3,097 | 3.097 | 3.125 |
115,200 | 248 | 0.248 | 0.260 |
250,000 | 119 | 0.119 | 0.120 |
500,000 | 58 | 0.058 | 0.060 |
750,000 | 39 | 0.039 | 0.040 |
1,000,000 | – | – | 0.030 |
1,500,000 | 34 | 0.034 | 0.020 |
3,000,000 | – | – | 0.010 |
Serial.begin()の数値を変えながら実行してみました。利用したボードがM5Stack ATOMなので設定できる速度が1,500,000までとなり、設定可能な速度も制限されています。
またArduino IDEのシリアルモニタだと上記のように選択肢が限定されていますので注意が必要です。M5Stack ATOMだと500,000に設定することで、通常の115,200より4倍以上早くなっていることがわかります。
また、理論値より少し早く遅くなっていますが測定誤差な気もします。
ESP32(ATOM)で102バイト送信(100文字+改行コード)
baud | 1000回の実測値(ms) | 1回あたり(ms) | 理論値(ms) |
---|---|---|---|
9,600 | 106,157 | 106.157 | 106.250 |
115,200 | 8,791 | 8.791 | 8.854 |
250,000 | 4079 | 4.079 | 4.080 |
500,000 | 2,039 | 2.039 | 2.040 |
750,000 | 1,359 | 1.359 | 1.360 |
1,000,000 | – | – | 1.020 |
1,500,000 | 680 | 0.680 | 0.680 |
3,000,000 | – | – | 0.340 |
100文字単位で送信した場合もほぼ理論値がでています。1文字単位で送信処理するとやっぱり内部のオーバーヘットが大きいみたいですね。
ESP32(ATOM)のBluetoothSerial
送信バイト | 1000回の実測値(ms) | 1回あたり(ms) | 備考 |
---|---|---|---|
2 | 154 | 0.154 | 115,200相当 |
101 | 1,389 | 1.389 | 750,000相当 |
BluetoothSerialを利用した場合には、設定した通信速度は無視されて送受信されます。短い文字を送信するのは非常に遅く、ある程度まとめて送信すると早くなるようでした。
ESP32-S3(ATOMS3)のUSB Serial
送信バイト | 1000回の実測値(ms) | 1回あたり(ms) | 備考 |
---|---|---|---|
2 | 17 | 0.017 | 1,000,000相当 |
101 | 336 | 0.336 | 3,000,000相当 |
ESP32-S3はESP32と同じハードウエアシリアルの他にUSB経由でシリアル通信が可能です。開発ボードによってはUSB端子が2つついており、片方がUSBシリアル変換IC経由でESP32-S3のハードウエアシリアルに接続、もう片方がESP32-S3のUSB用GPIOに接続されています。
ESP32-S3のUSBを利用した通信の場合には非常に高速で通信が可能でした。ただESP32のハードウエアシリアルについても使っているUSBシリアル変換ICの性能によってかなりばらつきが多いです。ATOMはCH552というマイコンを利用しており、最大が1,500,000までとちょっと低速です。
上記のFT231Xだと3Mまで設定可能ですのでもう少し高速化はできるはずです。
送信バッファの検証(ATOM)
前回のコードは1000回連続で送信することで、送信バッファの影響があまりでないようにしていましたが、ウエイトを追加して送信バイトによってどれぐらい時間がかかるかを上記で調べてみました。
1文字単位で送信
void setup() {
//Serial.setTxBufferSize(256);
Serial.begin(115200);
delay(1000);
for (int cnt = 0; cnt <= 200; cnt += 10) {
delay(100);
unsigned long startTime = micros();
for (int i = 0; i < cnt; i++) {
Serial.print('.');
}
unsigned long endTime = micros();
Serial.printf("\t%d\t%ld\n", cnt, endTime - startTime);
}
}
void loop() {
delay(1);
}
Serial.printで1文字単位で送信しています。Serial.setTxBufferSize()を利用することで送信バッファを追加することが可能です。今回は最大200文字まで送信しているので送信バッファをデフォルトの0と256で実験しています。
文字数 | TxBuffer=0(us) | TxBuffer=256(us) |
---|---|---|
0 | 2 | 1 |
10 | 174 | 380 |
20 | 333 | 747 |
30 | 499 | 987 |
40 | 665 | 1,331 |
50 | 831 | 2,601 |
60 | 1,004 | 3,330 |
70 | 1,171 | 4,059 |
80 | 1,337 | 4,781 |
90 | 1,503 | 6,054 |
100 | 1,669 | 6,778 |
110 | 1,835 | 7,504 |
120 | 2,008 | 8,233 |
130 | 2,174 | 9,501 |
140 | 2,340 | 10,228 |
150 | 2,506 | 10,959 |
160 | 2,672 | 11,681 |
170 | 12,991 | 12,953 |
180 | 13,162 | 13,680 |
190 | 13,328 | 14,404 |
200 | 13,494 | 15,134 |
結果をみるとTxBufferを設定したほうが全般的に遅いです。またTxBufferがデフォルトの0でも170文字送信したところからガツッと送信時間が増えています。
一括で送信
void setup() {
//Serial.setTxBufferSize(256);
Serial.begin(115200);
delay(1000);
for (int cnt = 0; cnt <= 200; cnt += 10) {
delay(100);
char text[cnt + 1] = {};
for (int i = 0; i < cnt; i++) {
text[i] = '.';
}
unsigned long startTime = micros();
Serial.print(text);
unsigned long endTime = micros();
Serial.printf("\t%d\t%ld\n", cnt, endTime - startTime);
}
}
void loop() {
delay(1);
}
先程のコードは1文字単位で送信していたので、送信する文字数をあらかじめ準備して一括で送信するようにしました。
文字数 | TxBuffer=0(us) | TxBuffer=256(us) |
---|---|---|
0 | 10 | 12 |
10 | 18 | 39 |
20 | 19 | 37 |
30 | 20 | 38 |
40 | 21 | 39 |
50 | 22 | 39 |
60 | 23 | 40 |
70 | 24 | 44 |
80 | 24 | 44 |
90 | 25 | 45 |
100 | 26 | 45 |
110 | 27 | 45 |
120 | 28 | 45 |
130 | 10,089 | 49 |
140 | 10,083 | 49 |
150 | 10,085 | 50 |
160 | 10,086 | 50 |
170 | 10,087 | 50 |
180 | 10,087 | 50 |
190 | 10,088 | 54 |
200 | 10,089 | 54 |
一括で送信するとかなり結果が変わりました。TxBufferがあると文字数の差がかなり少なくなっています。TxBufferがない場合にはTxBufferありより最初は早いのですが、130文字からガツッと速度が落ちています。
上記にTxBufferのコードがあるのですが、ここにヒントが書いてありました。
if (new_size <= SOC_UART_FIFO_LEN) {
// ESP32, S2, S3 and C3 means higher than 128
log_w("TX Buffer set to minimum value: %d.", SOC_UART_FIFO_LEN);
// it will use just UART FIFO with SOC_UART_FIFO_LEN bytes (128 for most SoC)
_txBufferSize = 0;
return SOC_UART_FIFO_LEN;
}
若干改行等で整形していますが、上記の処理になります。ESP32シリーズのSoCには128バイト以上のFIFOが内蔵されているので、それ以上のTxBufferを指定してくださいとのことです。
SoCの設定値一覧は上記のページで調べられるのですが今のところSOC_UART_FIFO_LENはすべて128バイトで、SOC_UART_BITRATE_MAXが5M(ESP32-C2のみ2.5M)になっていました。
つまり、128バイト以上の文字を一度に送信しようとするとSoCのFIFOに入らないので、送信が終わるまで処理がブロックされて大幅に遅延します。1文字単位で送信する場合には、バックグラウンドで徐々に前の文字は送信されているので、170文字ぐらいまでFIFOが溢れていなかったようです。
まとめ
シリアル通信は設定した通信速度での物理的な転送速度があります。ただしBluetoothSerialや内蔵USBなどの場合には送信単位などのよってスループットが変わるので注意が必要です。
115,200baudの場合で、100文字を送信するのに8ミリ秒程度かかるので、結構遅い処理なのを念頭に置きましょう。つまり定期的に送信する場合には100文字前後のを10ミリ間隔ぐらいで送信するのが限界です。5ミリ間隔で送信した場合には50文字以下に減らさないと物理的に間に合いません。
通信速度を上げることで送信時間を短くすることができますが、利用しているボードによって設定可能な速度が違います。また、Arduino IDEなどのシリアルモニタで設定可能な速度にする必要もあります。ESP32で動いていたものをESP32-S3に移植するとシリアル速度が115,200baudの場合でも、ESP32-S3の内蔵USB経由の場合にはもっと早く通信が完了する可能性が高いです。
また、ESP32内部にもFIFOがあり128文字まではキューにセットしてバックグラウンドで送信してくれます。FIFOがいっぱいになった場合には送信できるまでブロックされるので急に遅くなります。
独自に確保した送信用のリングバッファを使うことでFIFOよりキューを拡張することが可能です。ただしSoCのFIFOに追加するより独自に確保したリングバッファに追加する方が処理が重いので、細かい文字を頻繁に追加するよりは、printfなどで1つの文字列に整形してからの方が早い場合があります。
SoCのFIFOであれ、独自に拡張した送信用のリングバッファであれキューに入れるところまでは早くなりますが、実際に送信する速度は物理的な上限があるので注意が必要です。
5秒に一度大量の文字出力をする場合などであればバッファを多めに確保して、バックグラウンドでリングバッファからゆっくり送信するなどの処理は可能だと思います。
コメント