std Rust で M5Stamp C3U Mate の JTAG シリアルコンソールを使う

はじめに

昨年末、「M5Stamp-C3 Mate で始める組込み std Rust プログラミング」というエントリを書きました。

tomo-wait-for-it-yuki.hatenablog.com

年が明けてから、ファームウェアの書き込み、シリアルコンソール、GDB を使ったデバッグが USB Type-C ケーブル一本で可能な M5Stamp-C3U Mate が発売されました。ESP32-C3 には、USB Serial / JTAG コントローラが搭載されており、ESP-IDF の設定を変更することで、UART に出力されるコンソールログを JTAG シリアルに出力することができます。

www.switch-science.com

そして今週、Rust-jp のスラック で M5Stamp C3U Mate を使って、std Rust のコンソール出力を JTAG シリアルに出力できますか?という質問が Kawano Tatsuya さんから bare-metal チャネルに投稿されました。

なんという面白い燃料を…!ということで、std Rust で JTAG シリアルにコンソール出力を出せるようにしました。これで、Rust でも USB ケーブル1本で、ファームウェア書き込みから、コンソール出力、GDB デバッグができるようになりました。一旦 c3_jtag_serial_console ブランチに動く状態のものを用意しています。つい先程、ESP-IDF v4.3.2 を使う esp-idf-sys 0.30.2 がリリースされており、こちらを使えば特にトラブルなく使えると思います。下のブランチでは ESP-IDF v4.4 を使用しています。

github.com

以下、動くようになるまでの技術メモです。

sdkconfig の確認

ESP32-C3 の std Rust ですが、この環境は、esp-idf-sys によってベースが提供されています。esp-idf-sys は ESP-IDF をビルドして、その上に Rust bindings を構築します。このとき、ESP-IDF のコンフィグレーションは、アプリケーション crate の sdkconfig.defaults (など) の設定ファイルよって変更することができます。

例えば、sdkconfig.defaults に共通のコンフィグレーションを、ESP32-C3 特有のオプションは sdkconfig.defaults.esp32c3 に書く、といった具合です。

これを .cargo/config.toml あたりで環境変数を設定しておくと、ビルド時によしなにしてくれます。

[env]
ESP_IDF_SDKCONFIG_DEFAULTS = { value = "sdkconfig.defaults;sdkconfig.defaults.esp32c3", relative = true }

esp-idf-sys はデフォルトで、embuild という crate を通して、PlatformIO を使ってビルド環境を構築します。PlatformIOでも ESP-IDF を使うことは変わらないため、最終的に ESP-IDF のコンフィグレーションは sdkconfig.h というファイルに出力されることになります。

sdkconfig.defaultsJTAG シリアルコンソールを有効化するオプションを追加して、ビルドしてみます。

CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y

sdkconfig.h を探してみます。

$ find . -name sdkconfig.h
...
./target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-89db37652c4a27fa/out/esp-idf/.pio/build/debug/config/sdkconfig.h
...

だいぶ深いところにいますね。このsdkconfig.h の中身を確認したところ、CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG のマクロが定義されていませんでした。どうやら sdkconfig.defaults の設定が反映されていないようです。

features=native で試す

esp-idf-sys はデフォルトで PlatformIO を使ってビルドするのですが、個人的には ESP-IDF を直接使うほうが慣れているので、ESP-IDF を直接使う features=native を有効にして、問題を調査してみます。

$ cargo build --features=native

このコマンドを実行すると target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/out あたりに ESP-IDF を使ったビルドファイルが展開されています。

target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/out$ ls
bindings.rs  build  CMakeLists.txt  esp-idf-build.json  gen-sdkconfig.defaults  main.c  sdkconfig

ということで、ここにCONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y を書いた sdkconfig.defaults を置いて、ビルドして、上で辻褄が合うか試してみます。

esp-idf-sys で使用する ESP-IDF は .embuild/espressif/esp-idf-v4.3.1 下に展開されているので、source .embuild/espressif/esp-idf-v4.3.1/export.sh を実行して、環境を有効化します。

target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/outidf.py build を実行すると、なにやらコマンドが失敗します。

$ idf.py build
Executing action: all (aliases: build)
Running cmake in directory /home/tomoyuki/others/rust-esp32-c3-guessing-game/target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/out/build
Executing "cmake -G Ninja -DPYTHON_DEPS_CHECKED=1 -DESP_PLATFORM=1 -DIDF_TARGET=esp32c3 -DCCACHE_ENABLE=0 /home/tomoyuki/others/rust-esp32-c3-guessing-game/target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/out"...
// ...
  CMakeLists.txt:6 (idf_build_process)


-- Configuring incomplete, errors occurred!
See also "/home/tomoyuki/others/rust-esp32-c3-guessing-game/target/riscv32imc-esp-espidf/debug/build/esp-idf-sys-4abfefa4fc4e39f2/out/build/CMakeFiles/CMakeOutput.log".
cmake failed with exit code 1

CMakeLists.txt を確認したところ、ESP-IDF でプロジェクトのテンプレートに使う include($ENV{IDF_PATH}/tools/cmake/project.cmake) ではなく、include($ENV{IDF_PATH}/tools/cmake/idf.cmake) を使って、sdkconfig.defaults の与え方や、ビルド成果物をカスタムしていることがわかりました。

一旦、include($ENV{IDF_PATH}/tools/cmake/project.cmake) を使うように CMakeLists.txt を書き換えて、idf.py build でビルドできるようにしてみます。しかし、ビルドできるようになっても、CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG が sdkconfig.h の中に現れません。ここで、使っている ESP-IDF が v4.3.1 であることに気づきます。

同じプロジェクト構成で、ESP-IDF v4.3.2 でビルドすると、CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG が定義されることがわかりました。このことから、どうやら ESP-IDF の version 違いが問題であることが特定できました。

ここまでわかったところで、ESP-IDF v4.3.2 のリリースノート を見にいったところ、CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG は v4.3.2 で追加されたことがわかります。問題は、esp-idf-sys が ESP-IDF v4.3.1 を使っていることで、v4.3.2 以降の version を指定できればなんとかなりそう、という目星がつきました。

esp-idf-sys の feature branch を使う

あとは、esp-idf-sys に v4.4 を使う feature branch があることに気づいたため、こちらを使うように cargo.toml で crate にパッチを当てます。

cargo.toml
[patch.crates-io]
esp-idf-sys = { version = "0.30.1", git = "https://github.com/esp-rs/esp-idf-sys.git", branch = "feature/default-v4.4" }

ただ、どうも ESP32-C3 のときにリンクエラーになる問題があるので、.cargo/config.toml にちょっと細工を追加します。

github.com

次のコンパイルオプションを追加します。

.cargo/config.toml
[target.riscv32imc-esp-espidf]
rustflags = ["-C", "default-linker-libraries"]

これで無事、動くようになりました。