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 でも使えるようになるかもしれませんね。

Knurling Sessions 2020Q4 をやる (4) 〜ディスプレイ〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

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 でやってみることにしました。

f:id:tomo-wait-for-it-yuki:20210501204734p:plain
Waveshare 4.2 inch b&w ePaper Display

f:id:tomo-wait-for-it-yuki:20210501204919p:plain
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");

文字サイズと文字の表示位置を調整して…

f:id:tomo-wait-for-it-yuki:20210501202831p:plain
SCD30 で読み取った値を SSD1331 に表示

f:id:tomo-wait-for-it-yuki:20210501202934p:plain
ちょっと小さいけど良い感じに表示できている

さいごに

少々機材が高いですが、基礎から学ぶ組込みRust を読んだ方なら、すんなりできるのではないでしょうか。 probe-run や defmt といったツールのできがよく、良いディベロッパー経験を得られます。

Knurling Sessions 2020Q4 をやる (3) 〜CO2センサドライバ〜

はじめに

knurling-rs では Ferrous Systems が、スポンサー向けに組込み Rust の教材 knurling-session を提供しています。

github.com

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 センサの配線です。スルーホール用のテストワイヤ使ってみました。

f:id:tomo-wait-for-it-yuki:20210501171508p:plain
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 経由で計測結果を出力します。

f:id:tomo-wait-for-it-yuki:20210501171053p:plain
CO2

Red Alert!

CO2 センサ、RGB LED、ブザーを組み合わせてプログラムを作成します。