knurling-rs のツールお試し Get a grip on bare-metal Rust!

はじめに

この記事は Rust Advent Calendar 2020 12日目の記事です。

github.com

Our mission is to improve the embedded Rust experience. To achieve this, we build and improve tools and create learning materials.

knurling-rs は Ferrous Systems による組込み Rust の開発経験を向上するためのツールや教材を作成するプロジェクトです。 本記事では、knurling-rs で提供されているツールを紹介します。

ターゲットボードにはアプリケーションテンプレートの手順で説明されている nRF52840-DK を使います。

プロジェクト成果物一覧 (2020/12時点)

次の3つのツールが公開されています。

  • probe-run : 組込み Rust のプログラムをネイティブと同じように実行します
  • defmt : 効率的な文字列フォーマットを提供します
  • flip-link : ゼロコストなスタックオーバーフロー保護機能を提供します

probe-run

github.com

Cargo のカスタムランナーで、RTT 経由でプログラムの出力を表示します。 ブレイクポイントでスタックバックトレースを表示してファームウェア実行を終了することができます。

UART を接続して他ターミナルを立ち上げなくても、ファームウェアからの出力を見ることができて、とっても便利です。 バックトレース表示してくれるのも Good です。

defmt

github.com

defmt ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers.

ということで、可能な文字列フォーマットはホスト側で行う仕組みになっており、マイコン側の負担が軽減されています。

flip-link

github.com

スタックオーバーフローを検出してくれます。組込み開発では気づきにくいバグなので、ありがたいですね。 現在は ARM Cortex-M でのテストが実施されています。

GitHub Sponsor でスポンサーになると、公開前のツールや教材をいち早く試すことができます。 現在で言うと、組込み Rust の教材 knurling-books はスポンサーのみが見ることができます。

サンプルアプリケーションを動かしてみる

セットアップ

Linux では libusb と libudev が必要です。

Knurling のツールをセットアップします。

$ cargo install flip-link
$ cargo install probe-run
$ cargo install cargo-generate

sudo なしでデバイスにアクセスできるように、udev ルールを設定しておきます。

/etc/udev/rules.d/99-nrf.rules
# udev rules to allow access to USB devices as a non-root user

# nRF52840 Development Kit
ATTRS{idVendor}=="1366", ATTRS{idProduct}=="1015", TAG+="uaccess"

アプリケーションテンプレートの作成

$ cargo generate \
    --git https://github.com/knurling-rs/app-template \
    --branch main \
    --name my-app

アプリケーションテンプレートの修正

nRF52840-DK 用に修正します。 これも手順通りです。

.cargo/config.toml
-runner = "probe-run --chip $CHIP --defmt"
+runner = "probe-run --chip nRF52840_xxAA --defmt"

デフォルトだと Cortex-M0 のターゲットトリプルが指定されているので、Cortex-M4F をターゲットにする。

.cargo/config.toml
-target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
+# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
 # target = "thumbv7m-none-eabi"    # Cortex-M3
 # target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
-# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
+target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
Cargo.toml
-# some-hal = "1.2.3"
+nrf52840-hal = "0.11.0"
src/lib.rs
-// use some_hal as _; // memory layout
+use nrf52840_hal as _;

実行

nRF52840-DK を USB ケーブルでホスト PC に接続して、実行します。

$ cargo rb hello
    Finished dev [optimized + debuginfo] target(s) in 0.02s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/hello`
  (HOST) INFO  flashing program (8.14 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  Hello, world!
└─ hello::__cortex_m_rt_main @ src/bin/hello.rs:8
stack backtrace:
   0: __bkpt
   1: my_app::exit
        at src/lib.rs:29
   2: hello::__cortex_m_rt_main
        at src/bin/hello.rs:10
   3: main
        at src/bin/hello.rs:6
   4: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   5: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

Hello, world! が出力されてスタックトレースが表示されました。

サンプルコードを拝見

サンプルコードの中身を少し見てみます。

まず、src/lib.rs です。 panic-probedefmt で使うグローバルオブジェクトや関数が実装されているようです。

src/lib.rs
#![no_std]

use core::sync::atomic::{AtomicUsize, Ordering};

use defmt_rtt as _; // global logger
use nrf52840_hal 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::timestamp]
fn timestamp() -> u64 {
    static COUNT: AtomicUsize = AtomicUsize::new(0);
    // NOTE(no-CAS) `timestamps` runs with interrupts disabled
    let n = COUNT.load(Ordering::Relaxed);
    COUNT.store(n + 1, Ordering::Relaxed);
    n as u64
}

/// Terminates the application and makes `probe-run` exit with exit-code = 0
pub fn exit() -> ! {
    loop {
        cortex_m::asm::bkpt();
    }
}

hello.rs ではログレベル INFO で Hello, world を出力したあとに、my_app::exit() を呼んでいます。 cortex_m::asm::bkpt() の実行でバックトレースを吐き出して、プログラムが停止するようになっているみたいです。

src/bin/hello.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("Hello, world!");

    my_app::exit()
}

src/bin 下には他にも5つのサンプルがあります。

$  ls
bitfield.rs  format.rs  hello.rs  levels.rs  overflow.rs  panic.rs

panic.rs は、panic 発生時にバックトレースが出力されることが確認できます。

src/bin/panic.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::info!("main");

    defmt::panic!()
}

実行すると次のようになります。

$ cargo rb panic
   Compiling my-app v0.1.0 (/home/tomoyuki/rust/my-app)
    Finished dev [optimized + debuginfo] target(s) in 0.25s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/panic`
  (HOST) INFO  flashing program (8.22 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  main
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:8
0.000001 ERROR panicked at 'explicit panic'
└─ panic::__cortex_m_rt_main @ src/bin/panic.rs:10
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: _defmt_panic
        at src/lib.rs:15
   4: defmt::export::panic
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/defmt-0.1.3/src/export.rs:204
   5: panic::__cortex_m_rt_main
        at src/bin/panic.rs:10
   6: main
        at src/bin/panic.rs:6
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550

overflow.rsは、スタックオーバーフローを検出する flip-link crate の動作を確認するものです。 わざと大きなデータを ack 関数の入り口で確保して、ひたすら再帰呼び出しします。

src/bin/overflow.rs
#![no_main]
#![no_std]

use my_app as _; // global logger + panicking-behavior + memory layout

#[cortex_m_rt::entry]
fn main() -> ! {
    ack(10, 10);
    my_app::exit()
}

fn ack(m: u32, n: u32) -> u32 {
    defmt::info!("ack(m={:u32}, n={:u32})", m, n);
    let mut big = [2; 512];
    if m == 0 {
        n + 1
    } else {
        big[100] += 1;
        if n == 0 {
            ack(m - 1, 1)
        } else {
            ack(m - 1, ack(m, n - 1))
        }
    }
}

実行すると次のようになります。

$ cargo rb overflow
    Finished dev [optimized + debuginfo] target(s) in 0.76s
     Running `probe-run --chip nRF52840_xxAA --defmt target/thumbv7em-none-eabihf/debug/overflow`
  (HOST) INFO  flashing program (8.67 KiB)
  (HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0.000000 INFO  ack(m=10, n=10)
└─ overflow::ack @ src/bin/overflow.rs:13
0.000001 INFO  ack(m=10, n=9)
└─ overflow::ack @ src/bin/overflow.rs:13
// ...
0.004313 INFO  ack(m=1, n=1)
└─ overflow::ack @ src/bin/overflow.rs:13
0.004314 INFO  ack(m=1, n=0)
└─ overflow::ack @ src/bin/overflow.rs:13
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: {"package":"panic-probe","tag":"defmt_error","data":"{:str}{:str}","disambiguator":"18392481814486563737"}
error: the stack appears to be corrupted beyond this point
  (HOST) ERROR the program has overflowed its stack

スタックオーバーフローしたことを最後に教えてくれていますね。

テスト

testsuite 下にテストコードがあります。defmt の assert と assert_eq を使っていることと、#[defmt_test::tests]アトリビュートを tests モジュールに付与していること以外、普通のテストを同じような感じですね。

testsuite/tests/test.rs
#![no_std]
#![no_main]

use my_app as _; // memory layout + panic handler
use defmt::{assert, assert_eq};

// See https://crates.io/crates/defmt-test/0.1.0 for more documentation (e.g. about the 'state'
// feature)
#[defmt_test::tests]
mod tests {
    #[test]
    fn assert_true() {
        assert!(true)
    }

    #[test]
    fn assert_eq() {
        assert_eq!(24, 42, "TODO: write actual tests")
    }
}

次のコマンドで実行できます。ちゃんとアサーションが失敗したときにはバックトレースを出力されています。

$ cargo test -p testsuite
0.000000 INFO  running assert_true ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000001 INFO  .. assert_true ok
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000002 INFO  running assert_eq ..
└─ test::tests::__defmt_test_entry @ tests/test.rs:9
0.000003 ERROR panicked at 'assertion failed: `(left == right)`
  left: `24`,
 right: `42`: TODO: write actual tests', testsuite/tests/test.rs:…
└─ panic_probe::print_defmt::print @ /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:140
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: __udf
   2: cortex_m::asm::udf
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/asm.rs:104
   3: rust_begin_unwind
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.1.0/src/lib.rs:75
   4: core::panicking::panic_fmt
        at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libcore/panicking.rs:85
   5: test::tests::assert_eq
        at tests/test.rs:18
   6: main
        at tests/test.rs:9
   7: ResetTrampoline
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:547
   8: Reset
        at /home/tomoyuki/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:550
error: test failed, to rerun pass '-p testsuite --test test'

素晴らしい。お手軽にベアメタルでもテストが書けますね!

knurling って何?

www.weblio.jp

ハンドルの握り棒や検査具の丸棒など使用中に手や指が滑らないようにするため操作部分に付ける横、または斜めの凹凸をローレットといい、旋盤などで凹凸を付けることをローレット加工、その加工を行うときに使用する工具をローレット工具(またはローレット)という。また、転造法の一種でダイヤモンド形、七子目などの凹凸のあるロール(こま型エ具)を、回転しながら棒材の表面に押し付けてギザギザを付ける加工をいう。

へー!