ARMマイコンCortex-MでRTICに入門する
はじめに
本記事はCIST (公立千歳科学技術大学) Advent Calendar 2022への寄稿です。
2023/1/2 改訂
RustでミニマムなリアルタイムOSを作る試みとして、以前少しだけAVRマイコン(ATmega328p)に手を出した のですが、AVR関係のリファレンスの少なさと、Nightly Rustのfeature has been removed
に泣きをみて、改めてリファレンスの豊富なArmマイコン向けの環境を整備しました。
crates.io などをみていると、Rustの組み込み開発では、STM32/ARM Cortex-Mアーキテクチャをターゲットとしたライブラリ(クレート)やフレームワークの整備が活発なようです。
今回はArmアーキテクチャで組み込みRust開発を始める方に向けて、備忘録をまとめたいと思います。
開発環境
-
ホストマシン
Apple M1 Mac
-
エディタ
VS Code
-
QEMUターゲット
LM3S6965 / ARMCortex-M3搭載マイコン
-
使用フレームワーク
RTIC
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のプロジェクトテンプレート を新規作成できるコマンドです。
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が使えるようになります。
Rustのツールチェーンのリリースは、nightly、beta、stableの3つのチャンネルに分類されています[2]。
Rustの開発はmaster
ブランチで行われており、master
ブランチの内容が毎晩nightly
ツールチェーンとしてリリースされます。
master
ブランチでバグ修正が行われ、6週間おきにmaster
を基にしたbata
ブランチが作成されます。
bata
ブランチで6週間のテストサイクルが行われた後、bata
ブランチを基にしたstable
ブランチが作成され、stable
のツールチェーンとしてリリースされます。
プロジェクトの作成
続いてプロジェクトを作成しましょう。
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 というファイルを作成し、メモリマップを以下のように設定します。
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
メモリマップの情報はマイコンごとに異なりますが、メーカー公式サイト などで入手できるデータシート から参照することができます。(本データシートでは72ページ以降に記載があります)
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を以下のように設定します。
[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上で次のコードを動かしてみます。
に掲載されているサンプルコードを基にしています。#![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::spawn
にfooを走らせるためのクロージャを渡してスレッドを立ち上げています。
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
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にまとめられています。
今回実装したサンプルコードは以下にあります。
参考文献
Discussion