以前 Rust で組み込みプログラミングをやったときにあんまりいいやり方でなかったのですが、それではいかんということでイチからプロジェクトを作っていこうとおもいます。
なお「設定が面倒くさい」というあなたに完成品を用意しました。どうぞ。
参考サイト
動作環境
本記事は以下を前提とします。
arm-none-eabi-gcc
へのパスが通っていること- macOS での実行(Linux でも十分に可能だと思います)
- Zsh
- マイコンは STM32F303K8T6
- 書き込みは
openocd
また、前回までの記事で導入が雑だったので、Rust、そして Xargo 導入まで解説します。
Rust 環境のインストール
rustup
まずは brew
でインストール。その後適当に rustup
を打つと選択肢が出てきますので 1
を選択しておきましょう。
$ brew install rustup-init
$ rustup
Current installation options:
default host triple: x86_64-apple-darwin
default toolchain: stable
modify PATH variable: yes
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
> 1
その後パスの設定です。僕の場合は Zsh を使っているので source
コマンドを .zshrc
に追加しておきました。
$ echo 'source ~/.cargo/env' >> ~/.zshrc
rustc
続いて、クロスコンパイルツールである Xargo をインストールするための rustc
を入れましょう。正式版ではなく nightly が必要になっています。rust-src もクロスコンパイルに必要ですのでインストールしておきます。
$ rustup default nightly # ずらずらとインストールされる
$ rustup component add rust-src
xargo
Cargo から Xargo をインストールしましょう。コマンドを1つ打つだけです。
$ cargo install xargo # こちらもずらずらと
以上で開発環境の導入は完了となります。
プロジェクトの作成から STM32 用の仕様にする
最初にプロジェクトを作成しましょうか。
$ xargo new stm32OnRust --bin
$ cd stm32OnRust
以下、このプロジェクトルートにいるものとして話を進めていきます。
各種設定を加えていきます。と言ってもただのコピペです。
.cargo/config
魔法の呪文だと思ってください。まず mkdir .cargo
をしておくこと。
[target.thumbv7em-none-eabihf]
runner = 'arm-none-eabi-gdb'
rustflags = [
"-C", "linker=arm-none-eabi-gcc",
"-Z", "linker-flavor=gcc",
"-Z", "no-landing-pads",
"-C", "opt-level=2",
"-C", "link-arg=-mcpu=cortex-m4",
"-C", "link-arg=-mthumb",
"-C", "link-arg=-mfloat-abi=hard",
"-C", "link-arg=-mfpu=fpv4-sp-d16",
"-C", "link-arg=-specs=nosys.specs",
"-C", "link-arg=-specs=nano.specs",
"-C", "link-arg=-Wl,-T,src/layout.ld",
"-C", "link-arg=-Wl,--gc-sections"
]
[build]
target = "thumbv7em-none-eabihf"
Cargo.toml
dependencies
について追加します。
[dependencies]
r0="*"
この r0
はかの有名な japaric さんが作成したベアメタル用の関数群となっています。スタートアップルーチン自作の使うので崇め奉りましょう。
build.rs
てきとーにファイルを作ってくださいね。
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/layout.ld");
}
Makefile
こら、そこ。「Xargo 使っているのに何でわざわざ make なんて使うんだよ」とか言わない。
CARGO=xargo
TARGET=thumbv7em-none-eabihf
PROJECT=stm32OnRust
.PHONY: build clean flash
build:
$(CARGO) $@
clean:
$(CARGO) $@
flash:
echo target/$(TARGET)/debug/$(PROJECT) | xargs -I {} openocd -f interface/cmsis-dap.cfg -f target/stm32f3x.cfg -c 'init;program {};reset;exit'
src内
色々と説明が面倒なのでここから全てパクってください。
ビルド、実行
ビルドは単に make
するだけでいいです。終わったら STM32 ちゃんを SWD でつないで make flash
すれば書き込みから実行まで行われます。
ポイント
このプロジェクトを作成した際のポイントを説明します。
unsafe の管理
Rust では安全なコードを目指しているため、生ポインタ(ナマポ)への代入を伴う処理や、C言語等の他言語による関数の利用をする際は危険かもしれないということで unsafe
を付けることが必要となっています。例えば &RCC.AHBENR
という生ポインタへ volatile
な感じで値を代入する際は
unsafe {
volatile_store(&RCC.AHBENR, (1 << 22));
}
と囲う必要があるのです。うん、美しくない。
まあこれは Rust の思想通りにしておくのが一番なのですが、如何せん醜いのでマクロを使って無理やり unsafe
を殺しています。以下はデータストア用のマクロ st!
の定義の一部。
#[macro_export]
macro_rules! st {
($x:ident.$y:ident, $z:expr) => {
unsafe {
volatile_store(&mut reg!($x).$y, $z)
}
};
}
このように、内部に unsafe
を取り入れることで見かけ上 unsafe
をなくすことに成功しました。また、以下のデータロード用マクロ ld!
を御覧ください。
#[macro_export]
macro_rules! ld {
($x:ident.$y:ident) => {
(|| {
unsafe {
volatile_load(&mut reg!($x).$y)
}
})()
};
}
今度は値を返す必要があるのでわざわざクロージャを設けて、その中に unsafe
を取り込むことによって同様の解決をしています。敬虔な Rust 使いだと発狂しそうですが、すみません、許してください。
そのおかげでLチカコードがスッキリと書けます。以下のような感じで。
pub fn ms_wait(ms: u16) {
st!(SysTick.LOAD, 1000 - 1);
st!(SysTick.VAL, 0);
bset!(SysTick.CTRL, 0);
for _ in 0..ms {
while( bitIsClr!(SysTick.CTRL, 16) ){}
}
bclr!(SysTick.CTRL, 0);
}
#[no_mangle]
pub extern fn main() {
bset!(RCC.AHBENR, 22);
st!(GPIOF.MODER, 1);
loop{
ms_wait(100);
st!(GPIOF.ODR, 1);
ms_wait(500);
st!(GPIOF.ODR, 0);
}
}
C言語互換の関数の作成
Rust では通常関数を作成する際にマングリングが行われます。しかし割り込みハンドラ等でこのマングリングが起こってほしくない場合があるでしょう。その場合は関数の直前に
#[no_mangle]
をつければ良いのです。元に上の例では main
関数にこれを付けることで純粋な main
シンボルで生成がされるようになっています。
非常に簡単なスタートアップルーチン
このマイコンで言う Reset_Handler
です。通常は色々とループを回してやや面倒なコピーやゼロ埋めをしなければならないのですが、先程 dependencies
に入れた r0
のおかげでスッキリと書けています。以下がそのスタートアップルーチンの全貌。
extern "C" {
static mut _sidata: u32;
static mut _sdata: u32;
static mut _edata: u32;
static mut _sbss: u32;
static mut _ebss: u32;
}
#[no_mangle]
pub extern fn Reset_Handler() {
unsafe {
r0::init_data(&mut _sdata, &mut _edata, &mut _sidata);
r0::zero_bss(&mut _sbss, &mut _ebss);
}
main();
loop{}
}
最初にリンカスクリプト中で定義されている変数を static mut
で宣言しておきます。その後 init_data
でプログラムを RAM にコピー、zero_bss
で .bss 領域をゼロ埋めし、最後に main
関数を呼ぶことでプログラムの実行を可能にしています。
linkage のエラーを回避
割り込み関数は存在しないと実働に大きな影響が出てしまいます。よってユーザが割り込み関数を作成しない場合に備えて、「とりあえず作っておくね。でもユーザが作ったら僕は消えるよ」という Weak な属性を付してデフォルトの関数を作っておく必要があります。実際は単純に
#[linkage="weak"]
としておけば良いのですが、これだとクロスコンパイルには使っちゃだめだぞと怒られてしまいます。そこで以下の workaround を適用するわけですね。
#[cfg_attr(all(feature="weak"), linkage="weak")]
何故か、エラーが消えてコンパイルが通ります。
未実装
今の所、割り込み関係は未実装です。具体的に言うと
Hoge_IRQn
EnableIRQ()
関数
です。割り込みベクタテーブル自体は出来ていますので、割り込み自体起こそうと思えば可能です。
更に大多数のレジスタマップが未登録となっています。できる人はドシドシ追加していってくださいね(丸投げ)。
〆
というわけで丸1日かけて Rust でマイコンを動かしたい!という願いを叶えるために頑張ってきました。
正直そこまで Rust で動かすことにメリットは感じないですが、一応できるんだということが分かりました。この記事を参考にして是非安全なベアメタルプログラミングを楽しんでくださいね。