SNSへはこちら

Rustで組み込みプログラミング for STM32(4) – やっぱりコード自作でLチカ

以前 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 で動かすことにメリットは感じないですが、一応できるんだということが分かりました。この記事を参考にして是非安全なベアメタルプログラミングを楽しんでくださいね。

Ad
Ad

SNSへはこちら

RSS等はこちら