cargo make で contains multiple actions のエラーに遭遇
ブログさぼりがちなので、リハビリがてら軽いやつから書いていきます。
いまさらながら cargo make はじめました。
(一応) 自分でメンテしている echonet-lite-rs のテストを cargo make で作ってみるかー、と考えたのが始まりです。
contains multiple actions
Makefile.toml
に次のようなタスクを定義して、実行したところ、contains multiple actions
のエラーに遭遇しました。
[tasks.test] description = "test mra-reader" script = "cargo run data/MRA_V1.1.1/mraData/devices/0x0287.json" dependencies = ["fetch-test-data", "build"]
$ cargo make test [cargo-make] INFO - cargo make 0.36.3 [cargo-make] INFO - Project: mra-reader [cargo-make] INFO - Build File: Makefile.toml [cargo-make] INFO - Task: test [cargo-make] INFO - Profile: development [cargo-make] INFO - Running Task: legacy-migration [cargo-make] INFO - Skipping Task: fetch-test-data [cargo-make] INFO - Execute Command: "cargo" "build" Finished dev [unoptimized + debuginfo] target(s) in 0.01s [cargo-make] INFO - Running Task: test [cargo-make] ERROR - Invalid task: test, contains multiple actions.
cargo make にはビルトインのタスクがあって、test
もその1つのようです。
https://github.com/sagiegurari/cargo-make#disabling-predefined-tasksflows
今回の私がやったように、ビルトインタスクを自分で定義すると、置き換えではなく、拡張という扱いになるようです。
このようなビルトインタスクは --list-all-steps
するとずらずらとたくさん出てきます。
test
もありますね。
$ cargo make --list-all-steps Build ---------- build - build MRA reader binary build-flow - Full sanity testing flow. # 中略 Test ---------- bench - Runs all available bench files. bench-compile - Compiles all available bench files. # 中略 pre-test - No Description. test - Runs all available tests.
解決方法
toml ファイルに次のように書くことでビルトインタスクをロードしないようにできます。
[config] skip_core_tasks = true
またはタスク名をビルトインタスクとかぶらないようにすれば良いです。
[tasks.test-mra-parser] description = "test mra-reader" script = "cargo run data/MRA_V1.1.1/mraData/devices/0x0287.json" dependencies = ["fetch-test-data", "build"]
ビルトインタスクを眺めていると、よく使いそうなやつが多いのですが、多すぎる気もするので、どちらの方が使いやすそうか、試しながらやっていきたいですね。
結局 Rust の embedded-hal v1.0.0 ってどうなってるの?
この記事は、モダン言語によるベアメタル組込み開発 Advent Calendar 2022の12日目として書きました。
モダン言語によるベアメタル組込み開発、と言われても私のような人間は、何の言語ネタで書くか困ってしまいますね。 困った時の神だより。 神と言えば、Rust というのが、人類の共通認識になって久しく思われるため、Rust について書くことにしました。
あらまし
embedded-hal について言うことは全て「基礎から学ぶ組込みRust」で書いたので、何も書きません! 二度と同じような内容は書かない、という強い気持ちのもと書ききったので誰も文句ないね? embedded-hal を知らない人は本を買ってね。 本にはめちゃくちゃ丁寧に書いたから!
そんな embedded-hal ですが、基礎から学ぶ組込みRust執筆前から、v1.0.0 のリリースについての issue が作られています。
本を書き始めた当初は、本を出すのは embedded-hal v1.0.0 が出るのを待ちたい、とか言ってましたね。 待たなくてよかったね?
そんな長らく時間のかかっている embedded-hal v1.0.0 のリリースですが、一体全体どうなっているのでしょう? わたし、気になります! ということで軽く現状を調べてみました。
本当に軽く調べただけなので間違ってる部分もあるかもしれません。 ほんまかぁ?と思ったら自分でも確かめて見てください。
継続的に v1.0.0 alpha リリースがなされている
ちょいちょい v1.0.0-alpha.x がリリースされています。
現在の最新は alpha.9
です。
リポジトリのトップにも注意書きがなされていますが、alpha リリースごとに非互換の修正が入っています。
単純な一ユーザーとして考えると、毎バージョン、変更に対応しようとするとコストが高そうです。
また、世の中の embedded-hal のトレイトを使って実装されているドライバーは、stable version の 0.2.x
で止まっているものもあれば、最新の alpha リリースを追いかけているものもあります。
そのような状況なので、やりたいアプリケーションを書こうとしたときに、両方の embedded-hal が必要になったりして、中々にカオスな状況です
そんな状況なので、なんと、v0.2.x 系と v1.0.0-alpha 系との互換性維持のための crate が生まれちゃっていたりします。うーん!
async / await どうすんねん問題
Rust と embedded-hal をある程度触ったことあると共通の疑問に辿りつきます。 それが「async どうすんねん問題」です。
元々、embedded-hal の API はすべて nb という non-blocking API の Result
を返す設計になっていました。
そして、nb のドキュメントにさも async / await と関係ありそうなことが書いてありました。
Furthermore those APIs are not tied to a particular asynchronous model and can be adapted to work with the futures model or with the async / await model.
結論から言うと、nb と Rust の言語機能としての async / await は何も関係がありません。
nb が提供できるのは、WouldBlock
のエラーを返す API をポーリングして non-blocking な処理がかける、というだけなのです。
ということで、ワーキンググループの中で、v1.0.0 を出す前に async ちゃんと考えないといけないよね、という話になったようです。 これはめちゃくちゃ良い議論 & 判断な印象を受けました。
We had a brief chat about 1.0 in the meeting today and two interesting points were raised:
- Should we remove the nb traits entirely for 1.0? It's not clear that they're especially useful or widely used, many users report confusion with them, and ultimately the async traits should fully replace nb. > Plus, we could always add them back, but we can't ever delete them later if they're included in 1.0.
- Could we keep embedded-hal-async a separate crate forever, with embedded-hal just containing the common types/errors and the blocking traits? It would mean we could remove the ::blocking sub-modules which simplifies the crate, but we couldn't easily merge async traits back in the future.
Both worth resolving before we release 1.0.
あるべき方向に向かっている気がする
さて、エンターテイメントでもあるので、若干おもしろおかしく書いている部分はありますが、私個人の意見としてはあるべき方向に向かっている、という印象で、多少時間がかかってでも v1.0.0 の完成度を上げるのは今後の組込み Rust の普及に、非常に重要なことだと感じています。
例えば、async 系は、embedded-hal の一部として出すことを想定して、今は試行錯誤のフェーズとして独立した crate で開発されています。 でもちゃんと async が embedded-hal の一部として存在するようにする、というのはめちゃくちゃ価値があることだと思います。
その他、以前の embedded-hal では SPI や I2C など、マスターが複数のデバイスと通信できるペリフェラルをうまく表現できていない問題がありました。 今はこれを Bus として切り出して embedded-hal に加えていこうとする動きが見て取れます。 これも非常に良い話ですね。
まとめ
非互換修正が入りまくる中で開発するのはユーザーとしてしんどい部分もありますが、気長にあるべき姿の embedded-hal v1.0.0 のリリースを待ちたいと思います。 なんと言っても書籍を執筆していないので焦る必要がありませんしね!
来年も組込み Rust を頑張っていこうと思います。 良いお年を。
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 シリアルに出力することができます。
そして今週、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 を使用しています。
以下、動くようになるまでの技術メモです。
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.defaults
に JTAG シリアルコンソールを有効化するオプションを追加して、ビルドしてみます。
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/out
で idf.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
にちょっと細工を追加します。
次のコンパイルオプションを追加します。
.cargo/config.toml
[target.riscv32imc-esp-espidf] rustflags = ["-C", "default-linker-libraries"]
これで無事、動くようになりました。
2021年 まとめ
年末なので何したのか思い出しておく!
大雑把に言うと「基礎から学ぶ 組込みRust」の執筆と引っ越しでプライベートはほとんど燃え尽きてました。 仕事の面では Nature に転職してから1年経過し、できることも少しずつ増えてきた感じです。
次男が2歳になって動きが激しくなるにつれ、個人の作業時間を取る難易度が上がっていますが、できる範囲で活動は続けたいですね。
基礎から学ぶ 組込みRust
本を発売した時のエントリはこちら。 今年の前半は執筆、構成、GitHub の準備などでいっぱいいっぱいになっていました。
tomo-wait-for-it-yuki.hatenablog.com
最近、文章を書くモチベーションが回復してきたので、またちょいちょいブログなど書いていきたいです。
組込みRust関連
SWEST23
SWEST23で発表しました。 70分のセッションは大変だった!
その他
自分でがっつり何かを作る時間が取れないこともあり、knurling-rs や ESP-IDF で std 使える環境を軽く触ってキャッチアップに努めるくらいしかできてなかったです。 来年はもう少し自分で何か作ることを意識してやりたいですね。
引っ越し
家買いました。そして引っ越ししました。大変だった…。 家買いましたエントリも用意しているので、好ご期待。
読んだ本など
時間がなかったこともあり、あまり読めてないです。 購入した本自体がめちゃくちゃ少ない…。 もっと徳 (もとい本) を積まなければ。
仕事関係で OKR やら SQL やら Web 関係の雑誌やら読んでました。
仕事
Firmware の開発はだいぶこなれてきて、たまに Go でちょっと Server 書いたり書かなかったり。ただ Web の技術はまだまだわからないことばかりですね。SQL の本を2冊くらい読んで、色々楽になりました。 Firmware で Rust 使う空気感をだいぶ醸成できてきたので、来年もうひと頑張りしたいですね。
良いお年を。
M5Stamp-C3 Mate で始める組込み「std」Rust プログラミング
このエントリは Rust Advent Calendar 2021 3 の5日目として書きました。 qiita.com
はじめに
みなさん組込み Rust やっていますか?はい、やっていますね。息を吸うように日常的にやっているはずです。今年はなんと組込み Rust にフィーチャーした「基礎から学ぶ組込みRust」 という書籍も出版されており、もはや組込み Rust は人類の嗜みと言っても過言ではない状況です (嘘です。
そんな人類の嗜みであるところの組込み Rust ですが、いざ初めて見ると std が使えない、という現実が重くのしかかってきます。core
や alloc
、no_std
対応の crate をかき集めてもやりたいことがすんなりできず、歯がゆい思いをすることもしばしばあります。std::io
/ std::error
/ std::net
を始め、std
を前提とした基本的な機能さえ使わせてもらえず、std
の世界で生きている天上人と、no_std
の民の間には深い谷があります (適当言ってます。これを std ハラスメント
(エスティーディーハラスメント) として取り締まっていく所存です。
では、我々組込み Rust で遊びたい no_std
の民には救いはないのでしょうか?
救いはあります!2019年には組込みシステム向けリアルタイム OS の vxworks が、2021年には ESP32 で有名な Espressif の IoT 開発フレームワークの ESP-IDF および京都マイクロコンピュータ株式会社のリアルタイム OS ベースの開発プラットフォーム SOLID の std 対応が Rust に merge されています。ESP-IDF ターゲットは 1.56.0 で、SOLID ターゲットはリリースされたばかりの 1.57.0 で stable 化されています。
つまり、これらの環境では std
を使った組込み開発ができるのです!もう no_std
の民として我慢する必要はありません!std
を使って存分に組込み Rust 開発を謳歌できるのです!
本エントリでは、今年の10月から販売開始された 6 ドルの M5Stamp-C3 Mate を使って組込み std Rust プログラミングをやっていきます。
M5Stamp-C3 Mate では ESP32-C3 という SoC を搭載しています。命令セットが xtensa の ESP32 と異なり、ESP32-C3 では命令セットに RSIC-V を採用しています。RISC-V は Rust コンパイラでも対応されているため、xtensa と違って fork 版の Rust コンパイラなしに開発を始めることができます。
今回のブログエントリ執筆にあっては、次の rust-esp32-std-demo
をベースとしています。
動かすプログラム
みんな大好き The Rust Programming Language
の Hello, World! の次に作るプログラム「Guessing Game」を M5Stamp-C3 Mate で動かします。
この約30行のプログラムですね。
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..101); loop { println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
「え?それだけ?」と思ったそこのあなた!それ、エスティーディーハラスメントですよ!この 30 行程度の簡単なプログラムですら、no_std
では動かすことができません。ポイントは3つです。
no_std
ではヒープアロケーションの伴うString
が使えないno_std
には標準入出力 (std::io::stdin
) が存在しない (println
やstd::io
すら使えない)no_std
ではrand::thread_rng()
が使えない (rand
crate はno_std
では一部機能しか使えない
このうち、1
はアロケータを実装するだけで解決できるので大したハードルではありません。2
と 3
は std
に対応したプラットフォームをターゲットにする、以外の解決が難しいです。
今回使用する M5Stamp-C3 Mate では、ESP-IDF をベースとした std
が使えるのでこの程度は5分で動かせるはずです!やったぜ!
早速、rust-esp32-std-demo
をちょちょいのちょいっと下のように修正して動かしていきます。
esp_idf_sys::link_patches();
は、rwlock と atomic operation がリンクされない問題が現行 version の ESP-IDF だと発生するので、そのハックです。
use esp_idf_sys; // 追加 use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { esp_idf_sys::link_patches(); // 追加 println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..101); loop { println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
細かいビルド方法はrust-esp32-std-demo の buildを参照して下さい。基本的には、ESP-IDF の環境構築を行って、toolchain を nightly にするだけです。
5分で動…ビルドが通らない
満を持してビルドします。
$ cargo build # ログがいっぱい出る = note: Running ldproxy Error: Linker /home/tomoyuki/.platformio/packages/toolchain-riscv-esp/bin/riscv32-esp-elf-gcc failed: exit status: 1 STDERR OUTPUT: /home/tomoyuki/.platformio/packages/toolchain-riscv-esp/bin/../lib/gcc/riscv32-esp-elf/8.4.0/../../../../riscv32-esp-elf/bin/ld: /home/tomoyuki/repos/rust-esp32-c3-guessing-game/target/riscv32imc-esp-espidf/debug/deps/librand-0f1a4f54aa91172c.rlib(rand-0f1a4f54aa91172c.rand.c9ff3571-cgu.7.rcgu.o): in function `rand::rngs::adapter::reseeding::fork::register_fork_handler::{{closure}}': /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.4/src/rngs/adapter/reseeding.rs:314: undefined reference to `pthread_atfork' /home/tomoyuki/.platformio/packages/toolchain-riscv-esp/bin/../lib/gcc/riscv32-esp-elf/8.4.0/../../../../riscv32-esp-elf/bin/ld: /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.4/src/rngs/adapter/reseeding.rs:314: undefined reference to `pthread_atfork' collect2: error: ld returned 1 exit status = help: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified = note: use the `-l` flag to specify native libraries to link = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-libkindname) error: could not compile `esp32-c3-std` due to previous error
うん?pthread_atfork
がない、ということでリンクエラーになっています。そもそも pthread_atfork とはなんぞや?と思って調べたところ、fork するときにハンドラを登録するための関数でした。
fork するときのハンドラも何も、ESP-IDF ではそもそも fork を提供しないので、そんなものがあるはずがありません。エラーメッセージから rand
crate がこの pthread_atfork
を呼び出していることがわかります。
rand-0.8.4/src/rngs/adapter/reseeding.rs:314: undefined reference to `pthread_atfork'
該当のコードを確認してみるとすぐに原因がわかりました。さて、どこでしょうか?
#[cfg(all(unix, not(target_os = "emscripten")))] mod fork { use core::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Once; // 中略 pub fn register_fork_handler() { static REGISTER: Once = Once::new(); REGISTER.call_once(|| unsafe { libc::pthread_atfork(None, None, Some(fork_handler)); }); } } #[cfg(not(all(unix, not(target_os = "emscripten"))))] mod fork { pub fn get_fork_counter() -> usize { 0 } pub fn register_fork_handler() {} }
正解はこの1行です。ESP-IDF は unix ベースのプラットフォームとして実装されているので、この条件では libc::pthread_atfork
の呼び出しを含む mod fork
がビルド対象になってしまいます。ESP-IDF では空実装になっている方の mod fork
をビルド対象としたいです。
#[cfg(all(unix, not(target_os = "emscripten")))]
そこで、この条件を下のようにハックします。
- #[cfg(all(unix, not(target_os = "emscripten")))] + #[cfg(all(unix, not(any(target_os = "emscripten", target_os = "espidf"))))]
空実装の方も同様です。
- #[cfg(not(all(unix, not(target_os = "emscripten"))))] + #[cfg(not(all(unix, not(any(target_os = "emscripten", target_os = "espidf")))))]
これで、ビルドが通ります!
$ cargo build # 中略 Compiling esp-idf-sys v0.27.0 Compiling esp32-c3-std v0.1.0 (/home/tomoyuki/repos/rust-esp32-c3-guessing-game) Finished dev [optimized + debuginfo] target(s) in 2m 03s
参考にした rust-esp32-std-demo
の手順でインストールした flash / monitor ツールを使って書き込み / ログ出力します。
$ espflash /dev/ttyACM0 target/riscv32imc-esp-espidf/debug/rust-esp32-c3-guessing-game [00:00:01] ######################################## 12/12 segment 0x0 [00:00:00] ######################################## 1/1 segment 0x8000 [00:00:14] ######################################## 124/124 segment 0x10000 $ espmonitor /dev/ttyACM0
5分で動…かない
引き続き espmonitor
でログを確認していきます。やった起動し…
$ espmonitor /dev/ttyACM0 ESPMonitor 0.6.0 Commands: CTRL+R Reset chip CTRL+C Exit Opening /dev/ttyACM0 with speed 115200 Resetting device... done p configuration I (287) cpu_start: Starting sc�ESP-ROM:esp32c3-api1-20210207 Build:Feb 7 2021 rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT) SPIWP:0xee mode:DIO, clock div:1 load:0x3fcd6100,len:0x172c load:0x403ce000,len:0x928 # 中略 Guess the number! Please input your guess.
なんかエラーで再起動繰り返すやん!
Guru Meditation Error: Core 0 panic'ed (Illegal instruction). Exception was unhandled. Core 0 register dump: MEPC : 0x4200be86 RA : 0x42000212 SP : 0x3fc91550 GP : 0x3fc8b600 TP : 0x3fc84d8c T0 : 0xc742dfd5 T1 : 0x40389bb6 T2 : 0x00000000 S0/FP : 0x3c030130 S1 : 0x00000001 A0 : 0x3c030178 A1 : 0x00000013 A2 : 0x3fc91580 A3 : 0x3c030120 A4 : 0x3c030198 A5 : 0x00000001 A6 : 0x4200f938 A7 : 0x2e4f49de S2 : 0x4200a582 S3 : 0x00000019 S4 : 0x00000001 S5 : 0x3c030170 S6 : 0x3c0301b8 S7 : 0x00000002 S8 : 0x3fc91580 S9 : 0x3c0301d4 S10 : 0x000000ff S11 : 0x3c0301fc T3 : 0x00000000 T4 : 0x3fc91398 T5 : 0x3fc91358 T6 : 0x00000004 MSTATUS : 0x00001881 MTVEC : 0x40380001 MCAUSE : 0x00000002 MTVAL : 0x00000000 MHARTID : 0x00000000 # 中略 Rebooting...
どうも、標準入力からの入力受け付けで panic している様子です。
io::stdin() .read_line(&mut guess) .expect("Failed to read line");
返ってきているエラーを確認してみます。
let err = io::stdin() .read_line(&mut guess); dbg!(err);
WouldBlock
!!!
つまり、入力データがない、ということでエラーが返ってきているようです。なるほどね。
[src/main.rs:39] err = Err( Os { code: 11, kind: WouldBlock, message: "No more processes", }, )
ESP-IDF では UART0 を stdio / stdout / stderr として使います。この時、出力は blocking (書き込み完了するまで待つ) ですが、入力は non-blocking になっています。そのため C 言語の scanf
のような標準入力からの入力を blocking して待つことを前提としている関数がうまく動きません。
Rust の std::io::read_line()
も同様で、下位層が non-blocking になっていると、入力がない時に WouldBlock
のエラーを即時に返します。
このあたりのことは、ESP-IDF Programming Guid に書いてあります。
ESP-IDF Programming Guid Standard IO streams (stdin, stdout, stderr)
Due to this non-blocking read behavior, higher level C library calls, such as fscanf("%d\n", &var);, might not have desired results.
原因がわかれば解決は簡単!今回の場合、C 言語では次の2行を追加します。これで UART ドライバ (正確には virtual filesystem) が blocking で動作するようになります。
ESP_ERROR_CHECK(uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0)); esp_vfs_dev_uart_use_driver(CONFIG_ESP_CONSOLE_UART_NUM);
Rust からどうすれば良いのでしょうか? そう!C FFI を使います。
esp_idf_sys::esp!(unsafe { esp_idf_sys::uart_driver_install(esp_idf_sys::CONFIG_ESP_CONSOLE_UART_NUM.try_into().unwrap(), 256, 0, 0, std::ptr::null_mut(), 0) }).unwrap(); unsafe { esp_vfs_dev_uart_use_driver(esp_idf_sys::CONFIG_ESP_CONSOLE_UART_NUM.try_into().unwrap()) };
esp_idf_sys::esp!
マクロは、ESP-IDF の関数呼び出しのエラーコードを Result に変換するマクロです。uart_driver_install()
は ESP-IDF の C binding crate である esp-idf-sys
crate に binding があったのでそちらを使っています。その他、esp-idf-sys
にはマクロ定義やら型定義やらいろいろあります。
esp_vfs_dev_uart_use_driver()
の方は esp-idf-sys
に binding がなくて、このままだとそんな関数ないぞこらぁ!ということでコンパイルエラーになります。
error[E0425]: cannot find function `esp_vfs_dev_uart_use_driver` in this scope --> src/main.rs:20:9 | 20 | esp_vfs_dev_uart_use_driver(esp_idf_sys::CONFIG_ESP_CONSOLE_UART_NUM.try_into().unwrap()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
今回省略しますが、この ESP-IDF で Rust の std を使うプロジェクトのビルド方法については事前に調査してあったので、「どうせリンクするオブジェクト / アーカイブのどこかには esp_vfs_dev_uart_use_driver()
があるやろ」みたいなあてずっぽうな気持ちで、binding を手書きします。
extern "C" { pub fn esp_vfs_dev_uart_use_driver( uart_num: esp_idf_sys::uart_port_t ) -> (); }
勝ちました。
$ cargo build Finished dev [optimized + debuginfo] target(s) in 0.08s
5分で動…まだ動かない!
ここまでで完璧なバイナリが作れたはずです。意気揚々と flash / monitor します。
$ espflash /dev/ttyACM0 target/riscv32imc-esp-espidf/debug/rust-esp32-c3-guessing-game [00:00:01] ######################################## 12/12 segment 0x0 [00:00:00] ######################################## 1/1 segment 0x8000 [00:00:15] ######################################## 131/131 segment 0x10000 $ espmonitor /dev/ttyACM0 ESPMonitor 0.6.0 # 中略 I (283) cpu_start: Starting scheduler. Guess the number! Please input your guess.
ふふふ、大人しく私のゲスな入力を待ってやがります。ここで数字を入力すれば「あなたのゲスな入力はこれですね?」というメッセージが出力されるはずです!
println!("You guessed: {}", guess);
早速ゲスな入力を行っていきます。1 Enter
!!!
???おかしいですね?何も出力されません。なんやかんや調査した結果、この espmonitor
が入力に対応していないっぽいことがわかります。
5分で動いた、ってことにしようよ!
ということで、ESP-IDF に付属している monitor ツールを使います。適当な ESP-IDF のプロジェクト下で次のコマンドを実行します。
$ idf.py monitor # 中略 Guess the number! Please input your guess. You guessed: 50 Too small! Please input your guess. You guessed: 75 Too big! Please input your guess. You guessed: 67 Too big! Please input your guess. You guessed: 58 Too big! Please input your guess. You guessed: 55 Too small! Please input your guess. You guessed: 56 You win!
はい!組込み std Rust でも5分で guessing game が動きましたね!すごい!やればできる!
5分で動かせるコード
5分で guessing game が動かせるであろう成果物を用意しました (ESP-IDF のインストールからやると厳しいかもしれませんが)!
みなさま、ぜひ年の瀬には、組込み std Rust で guessing game を5分で動かす RTA をお楽しみ下さい。
Wio Terminal で Knurling Sessions 2020Q4
はじめに
ここ数日、Knurling Sessions 2020Q4 をリファレンスボード (nRF52840-DK) でやっていました。
tomo-wait-for-it-yuki.hatenablog.com
今回は、Wio Terminal を使って同じ内容 (CO2 センサから読み取った値をディスプレイに表示) をやってみます。
配線
I2C1_SDA / I2C1_SCL / 3.3V / GND をスルーホール用テストワイヤで接続します。 probe-run と defmt を使うために、JLink を接続しています。
実行結果
余談
実装時に1回ピンアサインを間違って、panic しましたが、probe-run (panic-probe) のおかげで、バックトレースが出力されたので、一瞬でデバッグできました。 やったね。
おわりに
大体半日もかからずに移植できました。 CO2 センサがちょっと高価ですが、基礎から学ぶ 組込みRust 後のお楽しみとしてどうでしょうか。
Wio Terminal で probe-run / defmt
はじめに
『基礎から学ぶ 組込みRust』では追加機材が不要な cargo hf2 を使ってファームウェアを書き込み、UART で文字を出力しました。 組込み Rust ではその他にも便利なツールがあります。 今回はそのうちの、probe-run と defmt を Wio Terminal で使ってみます (両方 knurling-rs の成果物です) 。
Runs embedded programs just like native ones
defmt ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers.
注意
本記事の内容を試すには、JLink か DAPLink かのデバッグアダプタを Wio Terminal に接続している必要があります。 Seeeduino XIAO を用いた DAPLink デバッグアダプタの環境構築は、基礎から学ぶ 組込みRust のサポートサイトに手順を掲載しています。
サンプルプロジェクトと実行方法
下に Wio Terminal で probe-run / defmt を試すためのサンプルプロジェクトを用意しました。
事前準備
probe-run をインストールします。
$ cargo install probe-run
Linux の場合は、libudev と libusb もインストールします。
sudo apt install libudev libusb
実行方法
Wio Terminal をデバッグアダプタと接続している状態で、プロジェクトを clone して、cargo run するだけです。
$ git clone https://github.com/tomoyuki-nakabayashi/wio-terminal-probe-run.git $ cd wio-terminal-probe-run $ cargo run
実行結果は次のようになります。
Running `probe-run --chip ATSAMD51P19A --speed 100 target/thumbv7em-none-eabihf/debug/wio-terminal-probe-run` (HOST) INFO flashing program (12.92 KiB) (HOST) INFO success! ──────────────────────────────────────────────────────────────────────────────── 0 INFO Hello, world! └─ wio_terminal_probe_run::__cortex_m_rt_main @ src/main.rs:11 (HOST) WARN program has used at least 195528 bytes of stack space, data segments may be corrupted due to stack overflow stack backtrace: 0: lib::inline::__bkpt at ./asm/inline.rs:13 1: __bkpt at ./asm/lib.rs:49 2: wio_terminal_probe_run::exit at src/lib.rs:29 3: wio_terminal_probe_run::__cortex_m_rt_main at src/main.rs:12 4: main at src/main.rs:10 5: ResetTrampoline at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547 6: Reset at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550 7: __DEFMT_MARKER_TIMESTAMP_WAS_DEFINED 8: Reset at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:497 error: the stack appears to be corrupted beyond this point
実行時のログは、タイムスタンプ (サンプル実装は1ずつインクリメントされるただのカウンタ)、ログレベル、ソースコード上の位置付きで出力されます。
0 INFO Hello, world! └─ wio_terminal_probe_run::__cortex_m_rt_main @ src/main.rs:11
bkpt 命令を呼び出したり、プログラムが panic するとバックトレースを出力します。
stack backtrace: 0: lib::inline::__bkpt at ./asm/inline.rs:13 1: __bkpt at ./asm/lib.rs:49 2: wio_terminal_probe_run::exit at src/lib.rs:29 3: wio_terminal_probe_run::__cortex_m_rt_main at src/main.rs:12 4: main at src/main.rs:10 5: ResetTrampoline at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547 6: Reset at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550 7: __DEFMT_MARKER_TIMESTAMP_WAS_DEFINED 8: Reset at $HOME/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:497 error: the stack appears to be corrupted beyond this point
少し解説
Cargo.toml
panic 時に probe-run 経由でバックトレースを出力する panic-probe
を使うことができます。
panic-probe = "0.2.0"
defmt は、Cargo.toml
に次の設定を追加します。features はこういうものだ、と思って下さい。
defmt = "0.2.0" defmt-rtt = "0.2.0" [features] default = [ "defmt-default", ] defmt-default = [] defmt-trace = [] defmt-debug = [] defmt-info = [] defmt-warn = [] defmt-error = []
.cargo/config
probe-run と defmt を使うためにリンクオプションを2つ追加します。
[target.thumbv7em-none-eabihf] rustflags = [ "-C", "link-arg=-Tlink.x", "-C", "link-arg=--nmagic", # 追加 "-C", "link-arg=-Tdefmt.x", # 追加 ] # runner を probe run に runner = "probe-run --chip ATSAMD51P19A --speed 100"
src/lib.rs
アプリケーションから使えるように、lib.rs
に部品をまとめておきます。
#![no_std] use core::sync::atomic::{AtomicUsize, Ordering}; use defmt_rtt as _; // global logger use panic_probe as _; use wio_terminal as _; use panic_probe as _; // same panicking *behavior* as `panic-probe` but doesn't print a panic message // this prevents the panic message being printed *twice* when `defmt::panic` is invoked #[defmt::panic_handler] fn panic() -> ! { cortex_m::asm::udf() } // defmt のタイムスタンプを実装します // タイマを使えば、起動からの時間を表示したりできます static COUNT: AtomicUsize = AtomicUsize::new(0); defmt::timestamp!("{=usize}", { // NOTE(no-CAS) `timestamps` runs with interrupts disabled let n = COUNT.load(Ordering::Relaxed); COUNT.store(n + 1, Ordering::Relaxed); n }); /// Terminates the application and makes `probe-run` exit with exit-code = 0 pub fn exit() -> ! { loop { cortex_m::asm::bkpt(); } }
src/main.rs
後は使うだけです。
#![no_std] #![no_main] use wio_terminal_probe_run; use wio_terminal as wio; use wio::entry; #[entry] fn main() -> ! { defmt::info!("Hello, world!"); wio_terminal_probe_run::exit() }
制限
次の issue に挙げられている通り、現在は speed を 100 khz にしないとうまく動きません。 そのため、少し動作が遅く感じます。
おまけ
probe-run は probe-rs を活かして実装されています。 probe-rs ではオプションの機能として、FTDI のデバッグアダプタをプローブする機能が実装されています。 そのうち、probe-run でも使えるようになるかもしれませんね。