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 をお楽しみ下さい。