目的
①組み込み環境でRustを使うにはどうすればいいかを知る。
(今回は実機がないのでエミュレータになってしまった)
②Rustを組み込み環境で使うメリットを把握する。
方法
自宅にRustで動く組み込み環境がなかったため、プロセッサエミュレータであるQEMUを使用した。
環境としては
CPUコア:Cortex-M4
命令セット:ARMv7E-M
チップ:STM32F407VGT6
をエミュレートする。
そもそもなぜ、Rustが組み込み環境で注目されているのか?
理由①ガベージコレクションがない。リアルタイム性を確保しやすい言語であるため。
理由②メモリ安全、スレッド安全
理由③C/C++同様の処理速度
という理由からRustは組み込み環境でも注目されている。
クロスコンパイル環境構築
コンパイルする環境と実行する環境が異なる環境向けのバイナリ生成をクロスコンパイルという。
Rustでのクロスコンパイル
ARMv7E-Mに対応したツールチェーンを追加します。
rustup target add thumbv7em-none-eabi
その他必要なツールをインストール
cargo install cargo-binutils
rudtup component add llvm-tools-preview
いざ、Hello World
いつものコマンドでRustのプロジェクトを作成。
cargo new hello
ターゲットの設定
どの環境をターゲットにするか設定する必要があります。デフォルトでターゲットを設定するにはプロジェクトディレクトリ直下に.cargo/configファイルを作成します。
その中で、targetとしてtarget = "thumbv7em-none-eabi"を設定します。
build.rsの作成
ビルド中の中間ファイルをどこに作成するか、メモリの配置を記述したmemory.xファイルを読み込んで、新たにoutフォルダに作成する。
build.rs中ではprintIn!()でCargoにオプションを伝えることができる。リンク時に参照するフォルダパスとmemory.xが変更されたときに際コンパイルされるように指示されている。
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main(){
let out=PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}",out.display());
println!("cargo:return-if-changed=memory.x");
}
memory.xファイルにはメモリの開始アドレスと大きさが記載している。
この情報がリンカーにも渡される。
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 1024k
RAM : ORIGIN = 0x20000000, LENGTH = 128k
}
confiファイル内でコンパイラに与えるフラグを指定する
link時にcortex-m-rtクレートが自動生成するlink.xをリンクするように追加している。
runnerでcargo run時に実行するファイルを指定している。
[build]
target = "thumbv7em-none-eabi"
[target.thumbv7em-none-eabi]
rustflags=[
"-C","link-arg=-Tlink.x",
]
runner = "./runner.bat"
mainの実装
![no_std]は
Rustの標準ライブラリを使用しないことを意味する。標準ライブラリはOSが存在しない状態では使用できない。
![no_main]はmain関数を使用しないことを意味する。
代わりに[entry]で開始箇所を伝えている。
# ![no_std]
# ![no_main]
extern crate panic_halt;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
# [entry]
fn main() -> !{
let _=hprintln!("Hello World!");
debug::exit(debug::EXIT_SUCCESS);
loop{}
}
cargo run時はbuild.rs内に記述したrunner.batが実行されdockerが起動されるdocker内からローカルの生成されたバイナリにアクセスできるようにマウントし、QEMUEを起動している。
@echo off
set "f=%1"
call set "f=%%f:\=/%%"
call set "f=%%f:C:=%%"
set "d=%CARGO_MANIFEST_DIR%"
call set "d=%%d\=/%%"
call set "d=%%d:C%%"
docker run --rm -v %CARGO_MANIFEST_DIR%:/%d% ^
qemu ^
sh -c ^"cd /%d%; ^
qemu-system-gnuarmeclipse ^
-cpu cortex-m4 ^
-machine STM32F4-Discovery ^
-nographic ^
-semihosting-config enable=on,target=native ^
-kernel %f%"
cargo runで実行すると以下のようにHello Worldが表示される
Rustの組み込み向けの機能
当初の目的②「Rustを組み込み環境で使うメリットを把握する」の観点からいえば、下手なミスはコンパイラが防いでくれるように言語的に設計されている点が今一番感じているメリットである。また、ハードウェアの抽象化やモジュールの抽象化が綺麗で使いやすところも挙げられる。まだ詳しく、触り切れていないので何とも言えないというのが正直なところ。
現段階で、上記のように思った理由を挙げていく
ハードウェアを抽象化したクレートembeded-hal
GPIOなどのアドレスを知らなくても、簡単にGPIOの操作ができるようになっている。
STM32F407VGT6チップであれあば、stm32f4xx-halクレートが対応している。
tomlファイルに具体的にどのチップを使うか指定すればチップに対応する操作が可能となる。
stm32::Peripherals::take()でPripheralのインスタンスを取得し、
let mut led = gpiod.pd15.into_push_pull_output();でGPIOの15番ピンのモードSetと使用が可能となる。
チップが変わっても、使用するクレートを変えればよいだけなので楽。
# ![no_std]
# ![no_main]
extern crate panic_halt;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
use stm32f4xx_hal::{delay::Delay, prelude::*, stm32};
# [entry]
fn main() -> ! {
if let (Some(dp), Some(cp)) = (stm32::Peripherals::take(), stm32::CorePeripherals::take()) {
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.sysclk(48.mhz()).freeze();
let gpiod = dp.GPIOD.split();
let mut led = gpiod.pd15.into_push_pull_output();
let mut delay = Delay::new(cp.SYST, clocks);
for _ in 0..5 {
led.set_high().unwrap();
delay.delay_ms(100_u32);
led.set_low().unwrap();
delay.delay_ms(100_u32);
}
}
debug::exit(debug::EXIT_SUCCESS);
loop {}
}
安全に対するRustの思想、embeded-halの思想
それぞれの機能の詳細は後述するが、全体として「設定が矛盾してしまう状態を発生させない」ような思想になっている。
この思想の基にインスタンスは常に1つしか存在しないような設計になっている箇所が多い。
具体的には、Copyはコンパイルエラー、moveでの所有権を常に譲渡するような設計になっている。
例を挙げていく。
CPUの周辺回路を設定するPeripheralのインスタンスは常に1つしか存在できない
GPIOの設定をするためのstm32::Peripherals::take()で取得したインスタンスの型はOption型(値がないかもしれないことを示す型)になっていて、一度しか取得できないようになっている。
・Copyやcloneはできない。
・スレッド間の転送は可能
→マルチスレッドであっても任意のタイミングでPripheralsが1つしかないことをコンパイラが保証してくれる。
# [entry]
fn main() -> ! {
if let (Some(dp)) = (stm32::Peripherals::take()) {
}
クロックを管理するRCC(Reset and clock control)のインスタンスも常に1つしか存在できない
クロックの周波数などを制御するRCCのインスタンスも1つしか存在しないことが保証されている。
また、周波数をSetするときはfreeze()で周波数が書き換えられないようにしないとクロック周波数のインスタンスは取り出せない。
このようにすることで、クロック周波数とそのクロック周波数を基に設定した周辺回路の設定に矛盾が起きないようにしてある。
# [entry]
fn main() -> ! {
if let (Some(dp)) = (stm32::Peripherals::take()) {
let rcc = dp.RCC.constrain();#複数生成するとエラーになる
let clocks = rcc.cfgr.sysclk(48.mhz()).freeze();
}
型制約されたピン設定
set_high()などの関数はPD15>にしか実装されていないため、OUTPUT用のメソッドgpiod.pd15.into_push_pull_output()でのインスタンス取得ではなく、into_floating_input()などでInput用のインスタンスを取得しInput用でset_high()をするとコンパイルエラーになる。
# [entry]
fn main() -> ! {
if let (Some(dp)) = (stm32::Peripherals::take()) {
let gpiod = dp.GPIOD.split();
let mut led = gpiod.pd15.into_push_pull_output();
for _ in 0..5 {
led.set_high().unwrap();
delay.delay_ms(100_u32);
led.set_low().unwrap();
delay.delay_ms(100_u32);
}
}
メモリ管理
詳しく把握できていない。アロケータやヒープ用のクレートがあることは確認したが、どのように安全使えるように設計されているか把握できていない。
以下リンクにある参考文献などを読んでから再度まとめたい。
https://tomoyuki-nakabayashi.github.io/embedded-rust-techniques/03-bare-metal/allocator.html
https://garasubo.github.io/embedded-book/misc/bib.html