cargo make で contains multiple actions のエラーに遭遇

ブログさぼりがちなので、リハビリがてら軽いやつから書いていきます。

いまさらながら cargo make はじめました。

github.com

(一応) 自分でメンテしている echonet-lite-rs のテストを cargo make で作ってみるかー、と考えたのが始まりです。

github.com

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日目として書きました。

qiita.com

モダン言語によるベアメタル組込み開発、と言われても私のような人間は、何の言語ネタで書くか困ってしまいますね。 困った時の神だより。 神と言えば、Rust というのが、人類の共通認識になって久しく思われるため、Rust について書くことにしました。

あらまし

github.com

embedded-hal について言うことは全て「基礎から学ぶ組込みRust」で書いたので、何も書きません! 二度と同じような内容は書かない、という強い気持ちのもと書ききったので誰も文句ないね? embedded-hal を知らない人は本を買ってね。 本にはめちゃくちゃ丁寧に書いたから!

そんな embedded-hal ですが、基礎から学ぶ組込みRust執筆前から、v1.0.0 のリリースについての issue が作られています。

github.com

本を書き始めた当初は、本を出すのは 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 が生まれちゃっていたりします。うーん!

github.com

async / await どうすんねん問題

Rust と embedded-hal をある程度触ったことあると共通の疑問に辿りつきます。 それが「async どうすんねん問題」です。

元々、embedded-hal の API はすべて nb という non-blocking APIResult を返す設計になっていました。 そして、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.

docs.rs

結論から言うと、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 の一部として存在するようにする、というのはめちゃくちゃ価値があることだと思います。

github.com

その他、以前の embedded-hal では SPI や I2C など、マスターが複数のデバイスと通信できるペリフェラルをうまく表現できていない問題がありました。 今はこれを Bus として切り出して embedded-hal に加えていこうとする動きが見て取れます。 これも非常に良い話ですね。

github.com

まとめ

非互換修正が入りまくる中で開発するのはユーザーとしてしんどい部分もありますが、気長にあるべき姿の 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 シリアルに出力することができます。

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"]

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

2021年 まとめ

年末なので何したのか思い出しておく!

大雑把に言うと「基礎から学ぶ 組込みRust」の執筆と引っ越しでプライベートはほとんど燃え尽きてました。 仕事の面では Nature に転職してから1年経過し、できることも少しずつ増えてきた感じです。

次男が2歳になって動きが激しくなるにつれ、個人の作業時間を取る難易度が上がっていますが、できる範囲で活動は続けたいですね。

基礎から学ぶ 組込みRust

本を発売した時のエントリはこちら。 今年の前半は執筆、構成、GitHub の準備などでいっぱいいっぱいになっていました。

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

最近、文章を書くモチベーションが回復してきたので、またちょいちょいブログなど書いていきたいです。

組込みRust関連

SWEST23

SWEST23で発表しました。 70分のセッションは大変だった!

swest.toppers.jp

その他

自分でがっつり何かを作る時間が取れないこともあり、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 が使えない、という現実が重くのしかかってきます。coreallocno_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 プログラミングをやっていきます。

www.switch-science.com

M5Stamp-C3 Mate では ESP32-C3 という SoC を搭載しています。命令セットが xtensa の ESP32 と異なり、ESP32-C3 では命令セットに RSIC-V を採用しています。RISC-V は Rust コンパイラでも対応されているため、xtensa と違って fork 版の Rust コンパイラなしに開発を始めることができます。

今回のブログエントリ執筆にあっては、次の rust-esp32-std-demo をベースとしています。

github.com

動かすプログラム

みんな大好き The Rust Programming Language の Hello, World! の次に作るプログラム「Guessing Game」を M5Stamp-C3 Mate で動かします。

doc.rust-lang.org

この約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つです。

  1. no_std ではヒープアロケーションの伴う String が使えない
  2. no_std には標準入出力 (std::io::stdin) が存在しない (printlnstd::io すら使えない)
  3. no_std では rand::thread_rng() が使えない (rand crate は no_std では一部機能しか使えない

このうち、1 はアロケータを実装するだけで解決できるので大したハードルではありません。23std に対応したプラットフォームをターゲットにする、以外の解決が難しいです。

今回使用する 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 するときにハンドラを登録するための関数でした。

Man page of PTHREAD_ATFORK

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 のインストールからやると厳しいかもしれませんが)!

github.com

みなさま、ぜひ年の瀬には、組込み 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 を接続しています。

実行結果

f:id:tomo-wait-for-it-yuki:20210503210005p:plain
奥にあるのが CO2 センサ

f:id:tomo-wait-for-it-yuki:20210503203548p:plain
defmt のログ

余談

実装時に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 の成果物です) 。

github.com

Runs embedded programs just like native ones

github.com

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 のサポートサイトに手順を掲載しています。

github.com

f:id:tomo-wait-for-it-yuki:20210503140523p:plain
JLink 接続

サンプルプロジェクトと実行方法

下に Wio Terminal で probe-run / defmt を試すためのサンプルプロジェクトを用意しました。

github.com

事前準備

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 にしないとうまく動きません。 そのため、少し動作が遅く感じます。

github.com

おまけ

probe-run は probe-rs を活かして実装されています。 probe-rs ではオプションの機能として、FTDI のデバッグアダプタをプローブする機能が実装されています。 そのうち、probe-run でも使えるようになるかもしれませんね。