2023年2月13日月曜日

ESP32 C3やS3の内蔵USBのシリアル通信は最高10Mbit/s


背景

ESP32 C3やS3の内蔵USB回路でどの程度の速度で通信できるか気になったので試してみました。

1バイトずつ送信した結果としては、外付けのUSBシリアル通信を利用すると700Kbit/s台の通信速度となり、外付けUSBシリアルICを利用すると921.6Kbit/s設定で780Kbit/sになると分かりました。

備忘録を兼ねて試した内容を共有します。

10文字ずつ送信した場合10Mbit/s

10文字ずつ送信した場合にESP32のCDCの仕様(12Mbit/s)に近い10Mbit/sで送信可能と分かりました。
詳しくはコメント欄をご覧ください。

使ったもの

  • ESP32にプログラムを書き込むPC + USBケーブル
    開発環境はplatformioを利用しました。
  • M5StampC3
    USBシリアルICであるCH9102を介してUSB通信する方式のESP32C3開発ボードです。
  • M5StampC3U
    内蔵USBシリアルにUSBコネクタが繋がっているESP32C3開発ボードです。
  • M5AtomS3Lite
    内蔵USBシリアルにUSBコネクタが繋がっているESP32S3開発ボードです。
    M5StampS3でも同様です。

試したプログラム: 1,000文字(1KB)と10,000文字(10KB)の通信速度を表示するプログラム

「a」10,000文字をシリアル通信で送信する時間を計測するプログラムを利用して、通信速度を把握します。
通信速度はBAUDRATEというマクロでplatformio.iniで指定する想定です。
src/main.cpp
#include <Arduino.h>

void setup() { Serial.begin(BAUDRATE); }

void printKiloBytes(uint32_t kiloBytes) {
auto start = millis();
for (int i = 0; i < kiloBytes * 1000; ++i) {
Serial.print("a");
}
Serial.println();
Serial.flush();
Serial.println(String(kiloBytes) + "K for " + String(millis() - start) + "ms");
}

void loop() {
Serial.println("hello at " + String(millis()));
printKiloBytes(10);
delay(1000);
printKiloBytes(1);
delay(1000);
}
各ボードのplatformio.iniの設定はそれぞれの実験結果と共に共有します。

時間や速度の計算は1byte送信に10bit要する想定(1BytePerSec = 10bit/s)で実施

ArduinoのSerialの標準送信形式はdata 8bit、パリティ無し、1stop bitなので、1 start bit + 8 data bit + 1 stop bitの10bitを要します。

bit/sからbyteの送信時間を求める場合は
byte * 10 / bit/s = 送信時間
となります。

送信時間からbit/sを求める場合は上記の式を変換して
byte * 10 / 送信時間 = bit/s
です。

ESP32C3 外付けUSBシリアルICを使う: 設定速度に応じて速くなるものの921.6Kbit/s(実速度780Kbit/s)で頭打ち

下記の設定でmonitor_speedとBAUDRATEを変えて複数の通信速度を試しました。
シリアル通信で通信内容を確認するには通信速度に応じてmonitor_speedも同じ値にする必要があります。
platformio.ini
[env:stamp-c3]
platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
monitor_speed = 9600
build_flags =
-D BAUDRATE=9600

各通信速度での実験結果はこちらです。
通信速度
(bit/s)
送信文字数
(Kbyte)
理論時間
(ms)
実測値
(ms)
実測値の通信速度
(bit/s)
9,600 1 1042 1043 9588
9,600 10 10426 10435 9583
115,200 1 86.8 87 114,942
115,200 10 868 869 115,074
921,600 1 10.8 13 769,230
921,600 10 108 128 781,250
4,000,000 1 2.5
13
769,230
4,000,000 10 25
128
781,250
4,500,000 プログラムが動きませんでした
5,000,000 プログラムが動きませんでした


921600bit/sまでは通信速度に比例して処理速度が上がりましたが、その後は変わりませんでした。
115,2Kbit/sまでは設定速度と実測値の違いは1%未満でしたが、921.6Kbit/sでは設定速度より15%ほど遅い実測値でした。
921.6Kbit/sより大きい設定値では40Mbit/s付近がが動作可能な設定値の上限でした。

M5StampC3U 内蔵USBシリアル: 設定速度に関係なく710Kbit/s前後

BAUDRATEを変えて処理時間を計測します。
ARDUINO_USB_CDC_ON_BOOTとARDUINO_USB_MODEを定義して内蔵USBでシリアル通信させます。
内蔵USBを使う場合はmonitor_speedを指定しなくてもログを受信可能です。
platformio.ini
[env:stamp-c3u]
platform = espressif32
framework = arduino
board = esp32-c3-devkitm-1
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT
-D ARDUINO_USB_MODE
-D BAUDRATE=9600

各通信速度での実験結果はこちらです。
通信速度
(bit/s)
送信文字数
(Kbyte)
理論時間
(ms)
実測値
(ms)
実測値の通信速度
(bit/s)
9,600 1 1042 13~18
769,231~555,555
9,600 10 10426 137~142 729,927~704,225
115,200 1 86.8 13~18
769,231~555,555
115,200 10 868 137~142
729,927~704,225
921,600 1 10.8 13~18 769,231~555,555
921,600 10 108 137~142 729,927~704,225
4,000,000 1 2.5
13~18
769,231~555,555
4,000,000 10 25
137~142
729,927~704,225
1,000,000,000 1 0.01
13~18
769,231~555,555
1,000,000,000 10 0.1
137~142
729,927~704,225

最小限にまとめるとこうです。
通信速度
(bit/s)
送信文字数
(Kbyte)
理論時間
(ms)
実測値
(ms)
実測値の通信速度
(bit/s)
意味なし 1 -
13~18
769,231~555,555
意味なし 10 - 137~142 729,927~704,225

通信速度の設定に関係なく710Kbit/s前後の通信速度になりました。

M5AtomS3 内蔵USBシリアル: 設定速度に関係なく750Kbit/s前後

BAUDRATEを変えて処理時間を計測します。
ARDUINO_USB_CDC_ON_BOOTとARDUINO_USB_MODEを定義して内蔵USBでシリアル通信させます。
内蔵USBを使う場合はmonitor_speedを指定しなくてもログを受信可能です。
platformio.ini
[env:atom-s3-lite]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
build_flags =
-D ARDUINO_USB_CDC_ON_BOOT
-D ARDUINO_USB_MODE
-D BAUDRATE=9600

こうなりました。
通信速度
(bit/s)
送信文字数
(Kbyte)
理論時間
(ms)
実測値
(ms)
実測値の通信速度
(bit/s)
意味なし 1 -
13~18
769,231~555,555
意味なし 10 - 131~136 763,359~735,294

通信速度の設定に関係なく750Kbit/s前後の通信速度になりました。

余談: 内蔵USBシリアル回路はまだ不安定?

この記事の動作確認を行っている時に、内蔵シリアルを利用するC3とS3は時々USBポートをシリアルモニタで開けなくなりプログラムも書き込めない状態になりました。
USBを抜き差ししたりリセットボタンを押せば復帰しましたが、物理的な操作が必要になるのが気になりました。
大量の情報をシリアル通信でやり取りしつつリセット処理容易に行えないような装置を作る場合は、まだ外付けシリアルICに頼る方が安定しそうです。

不具合の再現条件をつかめていないのが不可解ではあります。

おわり

内蔵のUSBシリアル通信する場合は設定速度に関係なく1byteずつ送信すると700Kbit/s台、10byteずつ送信すると1Mbit/s(コメント欄参照)の通信速度となり、外付けUSBシリアルICで1byteずつ送信すると921.6Kbit/s設定で最高780Kbit/sになると分かりました。

内蔵USBシリアルを利用して10文字以上を送信した場合10Mbit/sとなり外付けUSBシリアルで通信可能な900Kbit/sに比べて10倍以上速くて嬉しいです。

参考

Arduino Serial
Basic Options - esptoo.py
ESP32C3の内蔵jtagを利用してubuntu上のvscodeでデバッグ

変更履歴

2023.03.27
10byteずつ送信した場合に10Mbit/sを記録したことを追記しました。詳しくはコメント蘭をご覧ください。
単位表記bpsをbit/sに変更しました。

7 件のコメント :

辻田 さんのコメント...

ボーレートの設定って、普通、UARTの設定なので、USBが内蔵されているICでUARTを使用しない場合、設定値に関わらず変化しないというのは、感覚的に正しいと思います。
ただ、USBの通信レートだと思うと、750kbpsっていうのは遅いですよね。

内蔵USBのコアって、内部でUART通信してる?って思ってしまいます。

Asuki Kono さんのコメント...

コメントありがとうございます。
750kbpsは内蔵USBの割に遅いですよね。
teensy4.1やstm32のように10Mbps以上になって欲しいです。

辻田 さんのコメント...

Data sheetとか、Technical reference manualとか見る限り、12Mbps近くまで出てもおかしくないですね。USBアクセスの実装に問題があるのではないでしょうか?

Asuki Kono さんのコメント...

情報ありがとうございます。
ESP32S3 Technical Reference Manualの32 USB On-The-Go (USB)の冒頭で「The OTG_FS can operate as either a USB Host or Device and supports 12 Mbit/s
full-speed (FS) and 1.5 Mbit/s low-speed (LS) data rates of the USB 2.0 specification. 」とありますね。
LSだったとしても、1MBit/sは超えて欲しいところです。

esp-idfのリポジトリを見てみたところUSB_PHY_SPEED_FULLという設定値を見つけました。
しかしながら、arduino-esp32ではそれに関する設定を見つけられなかったので、SPEED_LOWなどSPEED_FULLではない値でビルド済みのものが使われて遅い通信になっているのかもしれません。

辻田 さんのコメント...

12MbpsのレートはPhyで決まるので普通に作ればこのレートは出るはずです。FSかLSかは、アクセスしてくるHostが決めていると思うので、普通のPCからアクセスするならFSで、非力なUSBホストマイコンとかからならLSになると思います。

オシロで波形を見てみればわかると思いますが、パルスの幅自体は十分に高速(おそらくFSのレート)だと予想します。

それより極端に遅いので、UARTのシリアルprintのルーチンをベースに作っているからそちらの制約に引っ張られているんじゃないかと思います。タイマー割り込みで1バイトずつ送信してるとか…

Asuki Kono さんのコメント...

情報ありがとうございます。
FSかLSはUSBホストによって変わるものなのですね。

arduino-esp32の実装を辿ったところHWCDCのwrite処理は一度実行されるとセマフォでロックしていると分かりました。
そこで、ロックの回数が少なくなるように予め長さ100の文字列を定義して送信する処理で速度を確認したところ10k送信時は5.88Mbps、1k送信時は1.67MbpsになるのをESP32C3で確認しました。
なお、長さ1000の文字列で試しても、長さ100利用時と速度は同じでした。

確認時のログの抜粋

print 10K for 17ms
588.24K byte/s
5.88M bit/s

print 1K for 6ms
166.67K byte/s
1.67M bit/s

10k送信時の10Mbps超えまであと4.12Mbpsです。

Asuki Kono さんのコメント...

長さ10の文字列の出力の繰り返しで10Mbit/s出せました。
長さ20でも結果は同じで、長さ4だと遅くなりました。

長さ10ずつ主力時のログの抜粋

print 10K for 14ms
714.29K byte/s
7.14M bit/s

print 1K for 1ms
1.00M byte/s
10.00M bit/s

出力が長さが100単位だとにリングバッファに乗り切らない文字列の扱いで処理が遅くなるのかもしれません。
長さ100ずつの時は10K出力が速かったですが、長さ10ずつの時は1K出力の方が速くなるのは不思議です。