自作 RISC-V マイコンを Rust で動かす

\[ \newcommand{dn}[3]{\frac{\mathrm{d}^{#3} #1}{\mathrm{d} #2^{#3}}} \newcommand{\d}[2]{\frac{\mathrm{d} #1}{\mathrm{d} #2}} \newcommand{\dd}[2]{\frac{\mathrm{d}^2 #1}{\mathrm{d} {#2}^2}} \newcommand{\ddd}[2]{\frac{\mathrm{d}^3 #1}{\mathrm{d} {#2}^3}} \newcommand{\pdn}[3]{\frac{\partial^{#3} #1}{\partial {#2}^{#3}}} \newcommand{\pd}[2]{\frac{\partial #1}{\partial #2}} \newcommand{\pdd}[2]{\frac{\partial^2 #1}{\partial {#2}^2}} \newcommand{\pddd}[2]{\frac{\partial^3 #1}{\partial {#2}^3}} \newcommand{\p}{\partial} \newcommand{\D}[2]{\frac{\mathrm{D} #1}{\mathrm{D} #2}} \newcommand{\Re}{\mathrm{Re}} \newcommand{\Im}{\mathrm{Im}} \newcommand{\bra}[1]{\left\langle #1 \right|} \newcommand{\ket}[1]{\left|#1 \right\rangle} \newcommand{\braket}[2]{\left\langle #1 \middle|#2 \right\rangle} \newcommand{\inner}[2]{\left\langle #1 ,#2 \right\rangle} \newcommand{\l}{\left} \newcommand{\m}{\middle} \newcommand{\r}{\right} \newcommand{\f}[2]{\frac{#1}{#2}} \newcommand{\eps}{\varepsilon} \newcommand{\ra}{\rightarrow} \newcommand{\F}{\mathcal{F}} \newcommand{\L}{\mathcal{L}} \newcommand{\t}{\quad} \newcommand{\intinf}{\int_{-\infty}^{+\infty}} \newcommand{\R}{\mathcal{R}} \newcommand{\C}{\mathcal{C}} \newcommand{\Z}{\mathcal{Z}} \newcommand{\bm}[1]{\boldsymbol{#1}} \]

いままで C++ をメインの言語として使ってきたのですが、最近コンパイラを作るのに Rust を使ってみたところかなり良かったので、今後作るものは Rust で書いきたいと思っています。ということで今回は FPGA に実装した自作 RISC-V マイコンを Rust で動かす方法を調べて実装してみます。

ソースコードは [@tnakabayashi](https://twitter.com/tnakabayashi) さんの riscv-rust-hello を参考にしています。

また以下のドキュメントを参照しました。

Baremetal Rust for RISC-V

Embedded Rust Techniques

The Embedonomicon

The Embedded Rust Book

Rust 裏本 高度で危険な Rust Programming のための闇の技法

環境は以下の通り。

$ cat /etc/issue
Ubuntu 20.04.6 LTS \n \l
$ rustc -V
rustc 1.75.0 (82e1608df 2023-12-21)
$ cargo -V
cargo 1.75.0 (1d8b05cdd 2023-11-20)

環境構築

以下のコマンドで使いたいマシンが対応してるか調べることができます。

$ rustc --print target-list
> riscv32imc-unknown-none-elf

これを target triplet と言い、コンパイラがどの計算機環境での実行バイナリを吐けばいいのかを、以下の項目で指定します。

以下のコマンドでクロスコンパイラをインストールします。

$ rustup target add riscv32imc-unknown-none-elf

ツールチェーン

objdump や nm などのツールをインストールします。

$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview

最初のプログラム

main.rs を書きます。最小限のプログラムです。main すら存在しません。

#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

ターゲットを指定してコンパイルします。

$ rustc --target riscv32imc-unknown-none-elf main.rs

言語仕様上パニックハンドラだけは定義する必要があるようです。パニックハンドラを消すと、このようなエラーが出ます。

error: `#[panic_handler]` function required, but not found

吐かれたバイナリ中身を見てみます。

$ rust-objdump ./main -x

./main: file format elf32-littleriscv
architecture: riscv32
start address: 0x00000000

Program Header:
    PHDR off    0x00000034 vaddr 0x00010034 paddr 0x00010034 align 2**2
         filesz 0x00000080 memsz 0x00000080 flags r--
    LOAD off    0x00000000 vaddr 0x00010000 paddr 0x00010000 align 2**12
         filesz 0x000000b4 memsz 0x000000b4 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**64
         filesz 0x00000000 memsz 0x00000000 flags rw-
 UNKNOWN off    0x000000f4 vaddr 0x00000000 paddr 0x00000000 align 2**0
         filesz 0x00000026 memsz 0x00000026 flags r--

Dynamic Section:

Sections:
Idx Name              Size     VMA      Type
  0                   00000000 00000000
  1 .comment          00000040 00000000
  2 .riscv.attributes 00000026 00000000
  3 .symtab           00000020 00000000
  4 .shstrtab         00000036 00000000
  5 .strtab           0000001d 00000000

SYMBOL TABLE:
00000000 l    df *ABS*  00000000 main.7c41ea6d61ccd5c8-cgu.0

Cargo

Cargo を使えるようにします。 新しくディレクトリを作ります。

$ mkdir rvrs
$ cd rvrs
$ cargo init

設定ファイルを書きます。

# .cargo/config
[build]
target = "riscv32imc-unknown-none-elf"

src/main.rs を上記のコードで書き換えます。

以下のコマンドでコンパイルできます。

$ cargo build

以下のコマンドで逆アセンブルできます。

$ cargo objdump --bin rvrs -- -x
(省略)

謎のシンボルが増えていますが、cargo build がデフォルトでデバッグビルドをするためのようです。

--release オプションを付けると、リリースビルドをして、逆アセンブルできます。

$ cargo build --release
$ cargo objdump --bin rvrs --release -- -x

L チカ (仮)

0x0300_0000番地に LED に繋がったレジスタがあるとしましょう。 L チカのコードは以下のようになります。

#![no_main]
#![no_std]

#[no_mangle]
pub extern "C" fn __start_rust() -> ! {
    let led = 0x0300_0000 as *mut u8;
    loop {
        unsafe {
            *led = 0;
            *led = 1;
        }
    }
}

use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

__start_rust は Rust のエントリポイントです。 led というポインタを作って、それの指すアドレスに対して 0 と 1 を交互に書き込んで L チカしています。

これをビルドして中身を見てみます。

$ cargo build
$ cargo objdump --bin rvrs -- -d

rvrs:   file format elf32-littleriscv

Disassembly of section .text.__start_rust:

80000000 <_bss_start>:
80000000: 41 11         addi    sp, sp, -16
80000002: 37 05 00 03   lui     a0, 12288
80000006: 2a c6         sw      a0, 12(sp)
80000008: 09 a0         j       0x8000000a <_bss_start+0xa>
8000000a: b7 05 00 03   lui     a1, 12288
8000000e: 01 45         li      a0, 0
80000010: 23 80 a5 00   sb      a0, 0(a1)
80000014: 05 45         li      a0, 1
80000016: 23 80 a5 00   sb      a0, 0(a1)
8000001a: c5 bf         j       0x8000000a <_bss_start+0xa>

12288 = 0x3000 です。

最適化オプション

エントリポイントまで

実際に動作するバイナリを作っていきましょう!

リンカスクリプト

. は変数です!

マイコンが起動すると、まず ROM の 0 番地にある命令が実行されます。 これをリセットベクタと言ったりするのですが、

ABI

Rust には安定した ABI がない Define a Rust ABI #600 そうです。

C 的人間がこのような関数を見たら、引数は順番にスタックに push されると考えがちですが、Rust ではそこに取り決めはないようです。最適化の中でフィールドの順番が入れ替わっているかもしれません。

fn hoge(a: u8, b: u32, c: u8) -> u32 {
    a + b + c
}

TODO バイナリを見る

なので extern "C" で C の ABI を拝借します。

extern "C" {
}

また構造体も同じように、上から順番にメモリに配置されるわけではなく、入れ替えられる可能性があります。 repr(C) で C と同様に上から順番に配置できます。

#[repr(C)]
struct Hoge{
    a: u8,
    b: u32,
    c: u8
}

リンカ

リンカは GCC の ld を使用できるため、今まで通りのリンカスクリプトを使用できます。

MEMORY
{
    RAM   (rw) : ORIGIN = 0x00000000, LENGTH = 0x00002000 /* 8 KiB */
    FLASH (rx) : ORIGIN = 0x00050000, LENGTH = 0x00100000 /* entire flash, 1 MiB */
}

SECTIONS {
    /* 略 */
    .bss :
    {
        . = ALIGN(4);
        _sbss = .;
        *(.bss .bss.* .sbss .sbss.*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    } >RAM
    /* 略 */
}

リンカスクリプト中で定義したシンボルを Rust 中で使用するには static 変数として宣言します。

extern "C" {
    static mut _sbss: u32;
    static mut _ebss: u32;
}

たとえば BSS 領域を 0 で初期化するには ptr::write_bytes (C のmemset) を使用し以下のようにします。

let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
ptr::write_bytes(&mut _sbss as *mut u8, 0, count);

Attribute としてシンボル名とセッションを指定できます。

#[export_name = "foo"]

アセンブラ

外部ファイル

外部のアセンブラファイル .s を別途アセンブルし、rust とリンクします。 ここは通常の C と同じ方法でできます。

Rust は Makefile みたいなビルドスクリプトを Rust で記述できます。いいね。

TODO ビルドスクリプトを書く

インラインアセンブラ

インラインアセンブラは以下のように書きます。

asm!(
    "mov {tmp}, {x}",
    x = inout(reg) x,
    tmp = out(reg) _,
);

詳細はRust リファレンスを参照してください。

拡張命令

独自の拡張命令をラップしたアセンブラマクロ

#define my_opr(rs1,rs2,rd) .word(/* 略 */)

を使用するには global_asm! を使用します。

TODO 試す

スタートアップルーチン

ベアメタルのプログラムのエントリポイントは、割り込みベクタの先頭にあるリセットベクタになります。ここから、環境を整えて main 関数へと引き渡すまでの部分を、スタートアップルーチンと言います。

上で説明した extern "C"#[no_mangle] を使用しシンボル名を同じにすることで、 C で開発していたものと同じアセンブラを使用できます。

割り込みハンドラ

ペリフェラル

ここ The Embedded Rust Book | ペリフェラル にだいたいのことが書いてあります。

有名なハードウェアに対しては、それをラップするクレートを実装してくれています。 が、自作マイコンの場合、それらを自分で書く必要があります。

今回は CPU に PicoRV32 というそこそこ有名なものを使用しているので、CPU のクレートは見つかりました。

picorv32-rt

CPU 以外のペリフェラルの部分は独自なので、自分で実装する必要があります。

まず、機能ごとにレジスタをまとめた構造体を定義します。

#[repr(C)]
struct GPIO {
    pub iosel: u32,
    pub in: u32,
    pub out: u32,
}

メモリのアドレスを指定して書き換えるには、

L チカ

以上で自作マイコンのプログラミング言語を C++ から Rust に移行するための道具は揃ったと思うので、実際にやっていきます。

コンパイルスクリプト

シミュレータ

iverilog でマイコンを動かして main 関数に到達していることを確認します。

アセンブリの比較

最後に、C++ と Rust の吐いたアセンブリを比べてみます。 以下のコマンドでアセンブリを出力できます。

$ rustc --emit asm main.rs