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 でも使えるようになるかもしれませんね。
Knurling Sessions 2020Q4 をやる (4) 〜ディスプレイ〜
はじめに
knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。
knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!
現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。
2020 Q4 のレンダリングされているドキュメントはこちら。
session20q4.ferrous-systems.com
前回、CO2 センサドライバを作成して、defmt で出力しました。
tomo-wait-for-it-yuki.hatenablog.com
今回は、CO2センサから読み取った値をディスプレイに表示します。 Knurling Sessions では Waveshare 4.2 inch ePaper ディスプレイを使っていますが、とりあえず手元にあった SSD1331 でやってみることにしました。
embedded-graphics を使っているので、なんとかなるだろう、と踏みました。 ePaper ディスプレイが 300x200 のサイズに対して、SSD1331 は 96x64 なので、サイズは調整が必要です。 また、ePaper ディスプレイは白黒なのに対して、SSD1331 は RGB フルカラーなので色の使い方も調整します。
Hello, e-Paper Display!
ピンアサインだけ一応ドキュメントに合わせた以外はほぼ独自で書きました。
Cargo.toml
に ssd1331 crate と embedded-graphics crate を追加します。
ssd1331 = "0.2.3" embedded-graphics = "0.6.2"
初期化はこのような感じです。
use ssd1331::{DisplayRotation::Rotate0, Ssd1331}; #[cortex_m_rt::entry] fn main() -> ! { // ... let din = pins_1.p1_01.into_push_pull_output(Level::Low).degrade(); let clk = pins_1.p1_02.into_push_pull_output(Level::Low).degrade(); let _cs = pins_1.p1_03.into_push_pull_output(Level::Low); let dc = pins_1.p1_04.into_push_pull_output(Level::Low); let mut rst = pins_1.p1_05.into_push_pull_output(Level::Low); let spi_pins = spim::Pins { sck: clk, miso: None, mosi: Some(din), }; let spi = Spim::new(board.SPIM3, spi_pins, spim::Frequency::K500, spim::MODE_0, 0); let mut display = Ssd1331::new(spi, dc, Rotate0); display.reset(&mut rst, &mut timer).unwrap(); display.init().unwrap(); // ... }
とりあえず 1 pixel 書いてみます。
ssd1331 は flush()
メソッドを呼び出さないとディスプレイに描画が始まらないので注意です。
display.set_pixel(10, 20, 0xf00); display.flush().unwrap();
Display Sensor Data
さて、いよいよ大詰めです。 ここでは、動的に文字列をディスプレイに表示する方法が説明されています。
arrayvec::ArrayString
を使います。ArrayString
は長さが固定の文字列として使えます。
次のような感じで、SCD30 から読み取った浮動小数点数を文字に変換します。
let mut buf = ArrayString::<[_; 12]>::new(); write!(&mut buf, "{:.2} {}", value, unit).expect("Failed to write to buffer");
文字サイズと文字の表示位置を調整して…
さいごに
少々機材が高いですが、基礎から学ぶ組込みRust を読んだ方なら、すんなりできるのではないでしょうか。 probe-run や defmt といったツールのできがよく、良いディベロッパー経験を得られます。
Knurling Sessions 2020Q4 をやる (3) 〜CO2センサドライバ〜
はじめに
knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。
knurling-session では、3ヶ月ごとに1つのテーマを取り上げています。 最新のもの以外は、公開されています。スポンサーになると最新のものも見れます。 また、スポンサーが増えると、knurling-rs に割く時間が増やせるため、ツールの改善や教材の整備が増えることが期待できます。 興味を持ったらスポンサーしましょう!
現在公開されている knurling-session の 2020 Q4 をやっていきます。 2020 Q4 は、nRF52840-DK と CO2 センサーを使ったプロジェクトです。
2020 Q4 のレンダリングされているドキュメントはこちら。
session20q4.ferrous-systems.com
前回、Lチカ終わらせたので、いよいよ CO2 センサ (SCD30) ドライバを書いていきます。
tomo-wait-for-it-yuki.hatenablog.com
Hello, Sensor!
まずは、CO2 センサの配線です。スルーホール用のテストワイヤ使ってみました。
SCD30 は、I2C インタフェースで制御します。 I2C ドライバのオブジェクトを作成して、SCD30 ドライバのオブジェクトを初期化します。
let scl = pins.p0_30.degrade().into_floating_input(); let sda = pins.p0_31.degrade().into_floating_input(); let twi_pins = twim::Pins{ scl, sda }; let i2c = Twim::new(board.TWIM0, twi_pins, twim::Frequency::K100); let mut scd30 = SCD30::init(i2c);
SCD30 ドライバも自作します。 データシートを見て、firmware version を取得するコードを書きます。
pub struct SCD30<T: Instance>(Twim<T>); impl<T> SCD30<T> where T: Instance { pub fn init(i2c: Twim<T>) -> Self { SCD30(i2c) } pub fn get_firmware_version(&mut self) -> Result<[u8; 2], Error> { let command: [u8; 2] = [0xd1, 0x00]; let mut rd_buffer = [0u8; 2]; self.0.write(DEFAULT_ADDRESS, &command)?; self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?; let major = u8::from_be(rd_buffer[0]); let minor = u8::from_be(rd_buffer[1]); Ok([major, minor]) } }
これで firmware version が取得できるようになります。
0 INFO Firmware Version: 3.66
Start Measuring
データシートを見ながら、SCD30 ドライバにメソッドを追加していきます。 今回作成する SCD30 ドライバは、70 行くらいで、それほど複雑ではありません。
impl<T> SCD30<T> where T: Instance { pub fn start_continuous_measurement(&mut self, pressure: u16) -> Result<(), Error> { let mut command: [u8; 5] = [0x00, 0x10, 0x00, 0x00, 0x00]; let argument_bytes = &pressure.to_be_bytes(); command[2] = argument_bytes[0]; command[3] = argument_bytes[1]; let mut crc = Crc::<u8>::new(0x31, 8, 0xff, 0x00, false); crc.update(&pressure.to_be_bytes()); command[4] = crc.finish(); self.0.write(DEFAULT_ADDRESS, &command)?; Ok(()) } pub fn data_ready(&mut self) -> Result<bool, Error> { let command: [u8; 2] = [0x02, 0x02]; let mut rd_buffer = [0u8; 3]; self.0.write(DEFAULT_ADDRESS, &command)?; self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?; Ok(u16::from_be_bytes([rd_buffer[0], rd_buffer[1]]) == 1) } pub fn read_measurement(&mut self) -> Result<SensorData, Error> { let command: [u8; 2] = [0x03, 0x00]; let mut rd_buffer = [0u8; 18]; self.0.write(DEFAULT_ADDRESS, &command)?; self.0.read(DEFAULT_ADDRESS, &mut rd_buffer)?; let co2 = f32::from_bits(u32::from_be_bytes([rd_buffer[0], rd_buffer[1], rd_buffer[3], rd_buffer[4]])); let temperature = f32::from_bits(u32::from_be_bytes([rd_buffer[6], rd_buffer[7], rd_buffer[9], rd_buffer[10]])); let humidity = f32::from_bits(u32::from_be_bytes([rd_buffer[12], rd_buffer[13], rd_buffer[15], rd_buffer[16]])); let data = SensorData{ co2, temperature, humidity }; Ok(data) }
ここまでできると、defmt 経由で計測結果を出力します。
Red Alert!
CO2 センサ、RGB LED、ブザーを組み合わせてプログラムを作成します。