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