🎄

ARMマイコンCortex-MでRTICに入門する

2022/12/14に公開

はじめに

本記事はCIST (公立千歳科学技術大学) Advent Calendar 2022への寄稿です。
https://qiita.com/advent-calendar/2022/cist

2023/1/2 改訂

RustでミニマムなリアルタイムOSを作る試みとして、以前少しだけAVRマイコン(ATmega328p)に手を出した のですが、AVR関係のリファレンスの少なさと、Nightly Rustfeature has been removedに泣きをみて、改めてリファレンスの豊富なArmマイコン向けの環境を整備しました。

crates.io などをみていると、Rustの組み込み開発では、STM32/ARM Cortex-Mアーキテクチャをターゲットとしたライブラリ(クレート)やフレームワークの整備が活発なようです。

今回はArmアーキテクチャで組み込みRust開発を始める方に向けて、備忘録をまとめたいと思います。

開発環境

  • ホストマシン
    Apple M1 Mac

  • エディタ
    VS Code

  • QEMUターゲット
    LM3S6965 / ARMCortex-M3搭載マイコン

  • 使用フレームワーク
    RTIC

https://rtic.rs/1/book/en/

RTICについて

RTIC(Real-Time Interrupt-driven Concurrency)は、リアルタイムシステムの構築に向いている並行処理フレームワークです。
すべてのCortex-Mデバイスをサポートしていて、

  • タスク間のメッセージ送受信
  • タイマキューによるタスクスケジューリング
  • タスクの優先順位付け(プリエンティブマルチタスク)

などの、リアルタイムOSに必要となる多くの機能を扱うことができます。

環境構築

The Embedded Rust Book(日本語版) を参考に、クロスビルド環境とデバッグ環境のセットアップを行います。

  • Rust
  • GDB(arm-none-eabi-gcc)
  • OpenOCD
  • cargo-binutils
  • cargo-generate
  • QEMU

Rustツールチェーン

公式のインストール手順 に従いインストールを行います。
次のコマンドで、rustc、cargo、rustup がインストールされます。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

次のコマンドから.cargo/env を読みに行くと、envの中身のexport PATH="$HOME/.cargo/bin:$PATH" がexportされます。

$ source $HOME/.cargo/env

うまくPATHが通らない場合は、
echo export PATH="$HOME/.cargo/bin:$PATH" >> <任意のファイル>
で .bashrcや.zshrcに直接パスを通しましょう。

次のコマンドから、最新バージョンへの更新が行えます。

$ rustup update

クロスビルド環境の用意

Rustのデフォルトインストールでは、ホストマシンのネイティブコンパイラのみがインストールされます。
今回はLM3S6965をターゲットとしたバイナリをビルドするため、ARM Cortex-M3ターゲット向けのツールチェーンが必要となります。
そのため、rustup コマンドからthumbv7m-none-eabi ターゲットを追加します。

$ rustup target add thumbv7m-none-eabi # Cortex-M3

Rustがサポートしているターゲットの一覧は、--print target-list オプションから確認できます。

$ rustc --print target-list

サブコマンドのインストール

crates.io から、開発に便利なサブコマンドをインストールします。

cargo-generate は、gitプロジェクトで管理されているrustのプロジェクトテンプレート を新規作成できるコマンドです。
https://zenn.dev/shinyay/articles/hello-rust-day025

cargo-binutils は、LLVMツールを簡単に扱う(rustcがサポートするすべてのアーキテクチャで、objdumpなどのツールを同じコマンドから扱えるようにする)ためのサブコマンドです。

$ cargo install cargo-generate
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview

クロスツールのインストール

ARM用のバイナリファイルを扱うため、クロスツールのインストールを行います。
windowsやlinux環境の方はこちら を参考にしてください。

$ brew tap armmbed/formulae
$ brew install armmbed/formulae/arm-none-eabi-gcc # GCCのクロスコンパイラ
$ brew install openocd # デバッガ

続いてQEMUのインストールを行います。
QEMUはオープンソースのCPUエミュレータです。
x86システム上でArmマイコンなどのバイナリを動作可能にします。

$ brew install qemu # エミュレータ

Rustのリリースチャンネルについて

Rustで組み込み開発を行う場合、現在は安定版のstable環境で開発を行うことができます が、以前まではツールチェーンをnightlyに切り替える必要がありました。

nightly はいわゆるbeta版で、[1]Unstable Features と呼ばれる試験的にリリースされた機能を用いることができます。

ツールチェーンの変更はrustupコマンドから行います。
$ rustup default nightly
main.rsやlib.rsのトップに#![feature(featureの名前)] 属性を付与することで、プロジェクト全体でfeatureが使えるようになります。
https://qiita.com/quasardtm/items/91b5297bdf17ed92dbe6

Rustのツールチェーンのリリースは、nightly、beta、stableの3つのチャンネルに分類されています[2]

Rustの開発はmaster ブランチで行われており、masterブランチの内容が毎晩nightlyツールチェーンとしてリリースされます。
masterブランチでバグ修正が行われ、6週間おきにmasterを基にしたbataブランチが作成されます。
bataブランチで6週間のテストサイクルが行われた後、bataブランチを基にしたstableブランチが作成され、stableのツールチェーンとしてリリースされます。

regression fixes
http://solid.kmckk.com/doc/skit/current/solid_rust/ecosystem/rust-toolchain.html

プロジェクトの作成

続いてプロジェクトを作成しましょう。
https://tomoyuki-nakabayashi.github.io/book/start/qemu.html ではcortex-m-quickstart というプロジェクトテンプレートを使いますが、今回はrticを使うため、defmt-app-template
というテンプレートからプロジェクトを作成します。

$ cargo generate \
    --git https://github.com/rtic-rs/app-template \
    --branch main \
    --name プロジェクト名

git cloneや、curlからスナップショットを入手して展開することでもインストールできます。

プロジェクトのセットアップ

基本的にはREADME に従ってセットアップを行います。
今回はlm3s6965をターゲットデバイスにしているため、一部 https://github.com/rtic-rs/cortex-m-rtic を参考にしています。

メモリ情報の設定

プロジェクトのルートにmemory.x というファイルを作成し、メモリマップを以下のように設定します。

src/memory.x
{
  /* NOTE 1 K = 1 KiBi = 1024 bytes */
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

メモリマップの情報はマイコンごとに異なりますが、メーカー公式サイト などで入手できるデータシート から参照することができます。(本データシートでは72ページ以降に記載があります)

Cargo.tomlは以下のように設定します。

scr/Cargo.toml
[package]
authors = ["yud0uhu"]
name = "arm-rs-rtos"
edition = "2018"
version = "0.1.0"

[workspace]
members = [
  "macros",
  "testsuite",
]

[lib]
name = "rtic"

[dependencies]
cortex-m = "0.7.0"
cortex-m-rtic-macros = { path = "macros", version = "1.1.5" }
rtic-monotonic = "1.0.0"
rtic-core = "1.0.0"
heapless = "0.7.7"
bare-metal = "1.0.0"
nb = "1"
lm3s6965 = "0.1.3"
cortex-m-semihosting = "0.3.3"
systick-monotonic = "1.0.0"
panic-semihosting = "0.5.2"
defmt = "0.3.0"
defmt-rtt = "0.3.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"]}

# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true
incremental = false
opt-level = "s"
overflow-checks = true

# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true
incremental = false
opt-level = "s"
overflow-checks = true

[build-dependencies]
version_check = "0.9"

[target.x86_64-unknown-linux-gnu.dev-dependencies]
trybuild = "1"

[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = "s"
overflow-checks = false

[profile.dev.build-override]
codegen-units = 16
debug = false
debug-assertions = false
opt-level = 0
overflow-checks = false

[profile.release.build-override]
codegen-units = 16
debug = false
debug-assertions = false
opt-level = 0
overflow-checks = false

[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = "s"
overflow-checks = false

[patch.crates-io]
lm3s6965 = { git = "https://github.com/japaric/lm3s6965" }

続いて、.cargp/config.tomlを以下のように設定します。

.cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
- runner = "probe-run --chip $CHIP"
+ runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" # lm3s6965をターゲットにしてqemuが立ち上がる
rustflags = [
  "-C", "link-arg=-Tlink.x", # リンカの設定
]

[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
- 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)

[alias]
rb = "run --bin"
rrb = "run --release --bin"
xtask = "run --package xtask --"

cortex-m-rtic-macros を使うため、ルートディレクトリにmacros を追加します。

RTICを動かしてみる

QEMU上で次のコードを動かしてみます。
https://github.com/rtic-rs/cortex-m-rtic/tree/master/examples
に掲載されているサンプルコードを基にしています。

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

use panic_semihosting as _;
use rtic::app;

#[app(device = lm3s6965, dispatchers = [SSI0, QEI0])]
mod app {
    use cortex_m_semihosting::{debug, hprintln};

    #[shared]
    struct Shared {}

    #[local]
    struct Local {}

    #[init]
    fn init(_: init::Context) -> (Shared, Local, init::Monotonics) {
        foo::spawn().unwrap();

        (Shared {}, Local {}, init::Monotonics())
    }

    #[task(priority = 1)]
    fn foo(_: foo::Context) {
        hprintln!("foo - start").unwrap();
        baz::spawn().unwrap();
        hprintln!("foo - end").unwrap();
        debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
    }

    #[task(priority = 2)]
    fn baz(_: baz::Context) {
        hprintln!(" baz - start").unwrap();
        hprintln!(" baz - end").unwrap();
    }

}

$ cargo run --bin preempt
   Compiling arm-rs-rtos v0.1.0 (/Users/denham/Documents/arm-rs-rtos)
    Finished dev [optimized + debuginfo] target(s) in 0.69s
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/examples/priority`
Timer with period zero, disabling
foo - start
 baz - start
 baz - end
foo - end

ソースコードについて

このコードで何をしているのか、簡単にみてみましょう。

並行処理の単位としてタスク(関数foo,bazを実行する)が定義されています。
init関数のfoo::spawn().unwrap(); では、foo::spawnfooを走らせるためのクロージャを渡してスレッドを立ち上げています。
https://doc.rust-jp.rs/book-ja/ch16-01-threads.html
unwrap は、Option<T>型やResult<T, E>型の値を返す関数です。unwrap()を呼ぶとErrのときはpanicを返します。

foo関数でも同様にbaz::spawn().unwrap(); でスレッドを立ち上げています。

結果的に、foo->baz->foo の順番でタスクが実行(テキストが出力)されています。

foo,barなどのタスクは、クレートで定義された定数の範囲(1..=(1 << NVIC_PRIO_BITS))で優先度を持つことができます。(#[task(priority = 1)])
RTICでは、数字が大きいほど優先度が高く設定されています。
複数のタスクの実行準備ができている場合、優先度の最も高いタスクが優先して実行されます。

以下はタスクの優先順位付けを示した図です。
優先度の低いタスクfooの実行中に優先度の高いタスクbarを生成すると、タスクbazの実行が割り込みで実行され、タスクbazが完了したタイミングで、タスクfooが実行を開始しています。

そのため、foo->bazの順ではなく、foo->baz->fooの順番でタスクが実行されていることがわかります。

Task Priority
  ┌────────────────────────────────────────────────────────┐
  │                                                        │
  │                                                        │
3 │                      Preempts                          │
2 │                    bar─────────►                         │
1 │          foo─────────► - - - - foo────────►                │
0 │Idle┌─────►                   Resumes  ┌──────────►     │
  ├────┴──────────────────────────────────┴────────────────┤
  │                                                        │
  └────────────────────────────────────────────────────────┘Time

https://rtic.rs/1/book/en/by-example/app_priorities.html

nostdとは
Rustのベアメタルプログラムには、#![no_std]という属性(アトリビュート)が必要です。
これにより、stdクレート(標準ライブラリ)ではなく、coreクレート(rust onlyで書かれたライブラリ)のみを扱えるようにしています。
cortex-m-rtクレートを始め、組み込み開発に関わるクレートはno_stdでのみ利用可能なものが多いです。

no_stdでのみ利用可能なクレートは数多く(2022/12/14現在4442件)、crates.ioの、crates.io No standard library カテゴリから参照できます。

また、組み込み開発に関わる主要なクレートはawesome-embedded-rustにまとめられています。

今回実装したサンプルコードは以下にあります。
https://github.com/yud0uhu/arm-rs-rtos/tree/qemu-lm3s6965

参考文献

https://rtic.rs/1/book/en/preface.html
https://tomoyuki-nakabayashi.github.io/book/
https://tomoyuki-nakabayashi.github.io/embedonomicon/
https://tomo-wait-for-it-yuki.hatenablog.com/archive/category/rtfm
https://qiita.com/tatsuya6502/items/7d8aaf3792bdb5b66f93
https://apollolabsblog.hashnode.dev/
https://dev.classmethod.jp/articles/rust-embedded-helloworld/

脚注
  1. https://doc.rust-lang.org/unstable-book/index.html ↩︎

  2. https://doc.rust-lang.org/1.64.0/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-train ↩︎

Discussion