はじめに
組込みソフトウェア向けプログラム言語というとC言語かC++言語を思い浮かべる人が多いのではないでしょうか。13年間組込みソフトウェアエンジニアとして働いてきましたが、C++言語を使った開発は1回だけで他は全部C言語で開発してきました。たまにアセンブリ言語を使用することもあります。組込み業界とはそういうところだと思います。Qiitaの記事は毎日チェックしていますが、WEB業界やゲーム業界はモダンなプログラミング言語が使えて羨ましいなと思っていました。
しかし近年組込み業界の救世主となるかもしれないRustというプログラミング言語が現れたようです。2015年頃からいたようなのですが、お恥ずかしながらチェックが遅れて半年前ぐらいに気付きました…この言語、ざっくりいうとC言語で好き勝手できていたことをできなくし、ビルドを通すハードルを上げる代わりにビルドが通ったらメチャクチャ安全なプログラムが出来上がるという、組込みにはうってつけの言語のようです。またC言語にはないモダンな言語仕様が実装されていたり、実行速度がC言語並みだったりとかなり魅力的です。今後組込み業界にジワジワと侵食してくるのではないかと個人的には予想しています。
前置きが長くなりましたが、思い立ったが吉日ということで私も組込みRustに入門してみることにしました。既にQiita等技術ブログでSTM Arduino ラズパイ等で組込みRustを扱った記事がありますので、私は国産ボードを動かしてみようと思いmbedボードであるGR-PEACHをチョイスしてみました。
環境
開発・動作環境は以下の通りです。
-
- OS
Windows 10 64bit
rustc・cargo
1.48.0-nightly
rustup
1.22.1
GNU Tools ARM Embedded
6 2017-q2-update
e2studio
2020-07
作戦
GR-PEACHはmbedボードです。PCとUSBで接続すればドライブとして認識し、そこにバイナリを放り込むだけで書込みが完了するという手軽さがあります。しかし詳しいことはわかっていないのですが、書込まれるバイナリがmbedのバイナリかどうかをチェックしているようで、適当なバイナリを書き込もうとしても書込まれません(試しに適当なファイルをドライブに放り込んでみてください)。またICEで書込もうにもJTAGの回路はありますがデフォルトではピンが立っていませんし、私のようなニワカはICEなんて持っていません。
そこでmain()が呼ばれるまではmbed(C++)に任せそこからRustのソースで用意したrust_main()関数を呼んであげることにしました。
こんなイメージです。
extern "C" void rust_main();
int main()
{
rust_main();
}
#[no_mangle]
pub extern "C" fn rust_main() {
// Lチカ
}
Rustのインストール
Rustのインストール方法に関する記事はたくさんありますので軽くにしておきます。
公式サイトからrustup-init.exeをダウンロード
実行すると選択肢が表示されるので1)を選ぶ
詳しくは「Windows 10 で Rust のインストール」辺りをご参考ください。この記事にかかれているパスの追加はインストーラが勝手にやってくれたので不要でした。
ターゲットの追加
以下のコマンドでターゲットを追加します。
GR-PEACHにはRZ/A1Hマイコンが載っています。RZ/A1HのコアはCortex-A9でアーキテクチャはv7-Aなのでarmv7a-none-eabiを追加します。
> rustup target add armv7a-none-eabi
もしerror: no override and no default toolchain setと表示されるようなら
> rustup update stable
を実行後再度試してください。
またツールチェイン追加中にerror: could not download file from …のようなエラーが出る場合はc:\Windows\System32\drivers\etc\hostsに
13.32.244.72 static.rust-lang.org
を追加してください。
e2studioのインストール
こちらもインストール方法に関する記事はたくさんありますので軽くにしておきます。
統合開発環境e2studioからインストーラをダウンロード
インストーラの指示に従いインストール
詳しくは「GR-PEACHオフライン開発環境の構築[On Windows 10]」辺りをご参考ください。
また本章とは別件ですがRustでクロスコンパイルするときにGCCのリンカを使用するため、このタイミングでC:\Program Files (x86)\GNU Tools ARM Embedded\6 2017-q2-update\binにパスを通しておきます。
プロジェクトを作成する
適当なディレクトリ(日本語は入っていないほうがいいかもです)で以下のコマンドでRustのプロジェクトを作成します(ついでに作成したディレクトリ内に入ります)。
> cargo new blinky --lib
> cd blinky
Created library `blinky` packageと表示されれば作成完了です。–libはライブラリ用テンプレートを用いてプロジェクトを作りますという意味です。後述しますが実行バイナリではなく静的ライブラリを作成します。
以降このパス(適当なディレクトリ\blinky)を作業ディレクトリとします。
Cargo.tomlを編集する
作業ディレクトリ直下にCargo.tomlというファイルがあると思います。勉強中のため断言できませんがプロジェクトの設定ファイルだと思っていただければと思います。
このファイルを以下のように書き換えます(authorsはご自分の名前で…)。
[package]
name = "blinky"
version = "0.1.0"
authors = ["yagisawa"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[lib]
crate-type = ["staticlib"]
追加したのは[lib]以降です。静的ライブラリを作成しますという意味になります。
コードを書く
いよいよLチカのコードを書きます。
srcディレクトリ内にlib.rsというファイルがありますので以下のように書き換えます。
#![feature(llvm_asm)]
#![no_std]
const PORTN_BASE: usize = 0xFCFE_3000;
const P6: *mut usize = (PORTN_BASE + 6 * 4) as *mut usize;
const PM6: *mut usize = (PORTN_BASE + 0x300 + 6 * 4) as *mut usize;
const PMC6: *mut usize = (PORTN_BASE + 0x400 + 6 * 4) as *mut usize;
#[no_mangle]
pub extern "C" fn rust_main() {
let mut flg: bool = false;
write(PM6, 0);
write(PMC6, 0);
loop {
if flg {
write(P6, 1 << 12);
}
else {
write(P6, 0);
}
flg = !flg;
for _ in 0..200_000_000 {
unsafe { llvm_asm!("" :::: "volatile")}
}
}
}
fn write( addr: *mut usize, data: usize ) {
unsafe {
core::ptr::write_volatile(addr, data);
}
}
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
Rustの知識は皆無なので詳しい説明はできませんが、要点をまとめると
#![feature(llvm_asm)]
llvm_asm!を使いますという意味
#![no_std]
標準ライブラリではなくコアライブラリをリンクしますという意味
PORTN_BASE P6 PM6 PMC6
GR-PEACHのLED_USER(P6_12)にアクセスするためのレジスタアドレス
#[no_mangle]
この関数はマングリングしませんという意味
マングリングとは名前空間や多重定義などで同じ名前の関数が定義された場合にコンパイラが区別できるようにシンボルに付加情報を埋め込むこと
pub extern “C” fn rust_main()
Rustの関数ではなくCの関数として定義しますという意味
for _ in 0..200_000_000
GR-PEACHのクロックは400Mhzで、空のfor文は大体2命令ぐらいになりそうなので2億回ぐらい回せば500msになるんじゃないかなーという適当なループ
unsafe { llvm_asm!(“” :::: “volatile”)}
最適化によりループが消えないようにする(命令の意味は…すみませんわかりません。何もしない命令か?)
core::ptr::write_volatile(addr, data)
最適化されないようにvolatlieアクセスにする
#[panic_handler] fn panic(_panic: &PanicInfo<‘_>)
プログラムが暴走したときにコールされる関数
となります。
リリースチャネルの変更
Rustには
-
- Nightly
-
- Beta
- Stable
の3つのリリースチャネルがあります。
先程のコードでllvm_asm!というマクロを使用したのですが、このマクロはNightlyでしか使用できないためリリースチャネルを変更する必要があります。
以下のコマンドで変更できます。
> rustup install nightly
> rustup override add nightly
以下のように表示されれば変更完了です。
info: using existing install for 'nightly-x86_64-pc-windows-msvc'
info: override toolchain for '適当なディレクトリ\blinky' set to 'nightly-x86_64-pc-windows-msvc'
nightly-x86_64-pc-windows-msvc unchanged - rustc 1.49.0-nightly (98edd1fbf 2020-10-06)
カスタムターゲットの作成
ターゲットの追加で既にターゲットは追加しましたが一点問題があります。mbed環境のライブラリはFloat ABIがhardで作成されているのですが、Rustターゲットのarmv7a-none-eabiはsoftで作成されています。hard-floatとsoft-floatの混在はできないようなのでこのままだとビルドが通りません。そこでhard-floatに設定したカスタムターゲットを作成します。
まず以下のコマンドでarmv7a-none-eabiのターゲット仕様を表示します。カスタムターゲット名はarmv7a-none-eabihfにしようと思うのでそのままjsonファイルとしてリダイレクトすると楽ちんです。
> rustc +nightly -Z unstable-options --print target-spec-json --target armv7a-none-eabi > armv7a-none-eabihf.json
ファイルをみてみると
"features": "+v7,+thumb2,+soft-float,-neon,+strict-align"
という行がありsoft-floatになっていることがわかります。
これを以下のように書き換えます。
{
"arch": "arm",
"data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64",
"disable-redzone": true,
"emit-debug-gdb-scripts": false,
"env": "",
"executables": true,
"features": "+v7,+thumb2,-neon,+strict-align",
"is-builtin": true,
"linker": "rust-lld",
"linker-flavor": "ld.lld",
"llvm-target": "armv7a-none-eabihf",
"max-atomic-width": 64,
"os": "none",
"panic-strategy": "abort",
"relocation-model": "static",
"target-c-int-width": "32",
"target-endian": "little",
"target-pointer-width": "32",
"unsupported-abis": [
"stdcall",
"fastcall",
"vectorcall",
"thiscall",
"win64",
"sysv64"
],
"vendor": ""
}
Xargoのインストール
XargoはCargoのサブコマンドで標準のクレートのバイナリリリースを持たないターゲットにおいて、Rustクレートを簡単にクロスコンパイルしてくれます。どういうことかというとRustでサポートされているarmv7a-none-eabi等のターゲットは追加したときに標準のクレート(libcore, liballoc等)がライブラリ(バイナリ)の状態でインストールされます。しかし先程作ったカスタムターゲットはJSONファイルを作っただけなのでそれがありません。そこでXargoはrust-src(上記ライブラリ等のソース)をビルドしてカスタムターゲット向けのバイナリを用意してくれちゃうわけです。
以下のコマンドでインストールできます。
> rustup component add rust-src
> cargo install xargo
ビルド
いよいよビルドです。初回は先程の標準クレートのビルドが一緒に行われます。
以下のコマンドでビルドできます。
> xargo build --release --target armv7a-none-eabihf
Finished release [optimized] target(s) in 0.01sと出ていればビルド完了です。–releaseはリリースビルドという意味です。–targetで先程作ったカスタムターゲットを指定します。
ビルドが完了するとblinky\target\armv7a-none-eabihf\releaseにlibblinky.aというファイルができていると思います。
GR-PEACHのオフライン開発環境構築
冒頭に書いた作戦の詳細ですがmbed環境をビルドするときに静的ライブラリにしたRustプログラムを一緒にリンクしようと考えています。
こちらは本題ではないので軽くにしておきます。
mbed compilerで新規プロジェクトを作成する(雛形はmbed-os-example-mbed5-blinkyで)
e2studioプロジェクトとしてエクスポートする
e2studioにインポートする
main関数を作戦で書いたコードに書き換える
libblinky.aをe2studioプロジェクトのルートディレクトリ辺りにコピーする
プロジェクトのプロパティ → C/C++ビルド → 設定 → ツールの設定 → Cross ARM C++ Linker → Librariesにblinkyライブラリを追加する(Library search pathの追加も忘れずに)
動作確認
失敗の数々
今までスムーズに作業手順を書いてきましたがここまで来るのに様々な失敗をしてきました。特にhard-floatのバイナリが作れなかった時は「もう無理か…」と諦めかけたこともありました。
ここではそんな失敗についてご紹介したいと思います。他のマイコン等で作業していて同じような現象に出くわした時等参考になればと思います。
ターゲットがない
Rustをインストールして一発目にWindowsのHello Worldを作ろうとしました。いざビルドしようとすると
error: no override and no default toolchain set
と出て出鼻をくじかれました。
> rustup update stable
を実行することで解決できました。
ちなみにこの現象が発生しているとバージョン確認すらできません。
ターゲットが追加できない
色々とクロスコンパイル環境の作り方がわかってきてさぁターゲットを追加するぞというところで
error: could not download file from ...
と出て追加できませんでした。
どうやらDL先の名前解決ができないみたいなので、c:\Windows\System32\drivers\etc\hostsに
13.32.244.72 static.rust-lang.org
を追加することで解決できました。
プロジェクトを作っていない
興味がある情報だけを調べていってcargo build –targetすればクロスコンパイルできる事がわかりました。そこで適当なディレクトリにmain.rsを作ってビルドしたところ
error: could not find `Cargo.toml`
となりました。
プロジェクトを作成するを参考にプロジェクトを作成しましょう、という話です。
armv7a-none-eabiでhard-floatビルドできない
カスタムターゲットの作成で書いた話です。soft-floatとhard-floatが混在しているとリンク時に
... /ld.exe: error: mbed-os-example-mbed5-blinky.elf uses VFP register arguments, ...
というエラーが出ます。
なんとかRustコンパイラに引数を渡そうといろいろ試しましたがうまく行かず、最終的にカスタムターゲットを作るに至りました。
プロジェクトを複製するとビルドが通らなくなる
試行錯誤のあのバージョン、このバージョンとプロジェクトを複製していたら
ERROR: the sysroot can't be built for the Stable channel. Switch to nightly.
と出てビルドできなくなりました。リリースチャネルの変更ですが今回の方法ではRust全体が変更されるのではなくディレクトリごとに変更されるようでrustupがそのディレクトリを管理しています。C:\Users\ユーザー名\.rustup\settings.tomlをみてみると
default_host_triple = "x86_64-pc-windows-msvc"
default_toolchain = "stable-x86_64-pc-windows-msvc"
profile = "default"
version = "12"
[overrides]
"\\\\?\\適当なディレクトリ\\blinky" = "nightly-x86_64-pc-windows-msvc"
こんな感じで管理されていました。
おわりに
なんとかLチカまでこぎつけました。
Rust人口はまだまだ少なく、組込み人口ともなればもっと少ないので情報があまり出回っておらずかなり苦労しました。この記事が組込みRustユーザのお役に立てれば光栄です。
今回は【とにかくやっている編】ということもあり
-
- ほとんどmbedじゃない?
-
- 全然組込みっぽくない(割込みを使用していない等)
- 全然Rustっぽくない(と、勝手に思ってる)
等課題がたくさんあります。
-
- mbed依存部をブートローダだけにする
-
- ICEを買ってスタートアップからスクラッチで書いてみる
-
- 点滅周期にタイマーを使う
- ペリフェラルのアクセスにrza1クレートを使ってみる
等色々挑戦していきたいと思います。
そのうちCで書いてるその実装、Rustならこう(簡潔に・安全に)書けるよ、的な記事も書けたらなと思っています。
参考
本記事を書くにあたり以下のサイトを参考にしました。
-
- Windows 10 で Rust のインストール
-
- GR-PEACHオフライン開発環境の構築[On Windows 10]
-
- RustでLチカする2種類の方法(Arduino編)
-
- rustup: the Rust toolchain installer
-
- Rust の crate_type をまとめてみた
-
- コンパイラサポートに関する覚書
-
- [Rust]インストールからHello world, Cargoを使ったプロジェクト管理入門
- How can I install Rust using rustup on Windows when behind a firewall?