undefined

bokuweb.me

Rustから目覚めるぼくらのゲームボーイ!

Conway氏についてですが、公式にアナウンスがでたようです。ご冥福をお祈り申し上げます。

www.math.princeton.edu

2003年に発売された「Linuxから目覚めるぼくらのゲームボーイ!」というC言語でゲームボーイアドバンスで動作する自作ゲームを作成していく書籍があります。

ゲームボーイアドバンスはARM7TDMIというコアを使用しており、Rustで自作ゲームを作ることも可能となっています。

この記事では「Linuxから目覚めるぼくらのゲームボーイ!」のステップをRustで実施するための準備としてライフゲームが動くまでを書いてみます。

動機は今作っているWasmインタープリタをGBAで動かすことができないかの調査です。(たとえLチカレベルでも)AssemblyScriptとかでGBAのゲームかけたら面白くないですか。

成果物

github.com

環境のセットアップ

RustARMに対応しています。しかし、Coretexシリーズなど比較的新しめのアーキテクチャのみの対応となっており、古いARM7TDMIには対応していません。

幸いなことにLLVMはこのCPUに対応しているので直接LLVMにターゲット設定してやることでARM7TDMIに対応したバイナリを出力することが可能となります。

With Docker

Dockerイメージは用意したのでmakefileを書いておけば以下のように使えます

$ docker run --rm -v `pwd`:/code bokuweb/rust-gba make

Without Docker

Rust

Rust現時点での最新のnightlyを使用しています。組み込みでRustを使用する場合原則nightlyとなるようです。

rust version 1.44.0-nightly (94d346360 2020-04-09)

また、クロスビルド用にcargo-xbuildもインストールします。

$ rustup component add rust-src
$ cargo install cargo-xbuild

Toolchain

ARM7TDMI用のToolchainを用意します。ターゲットはarm-none-eabi

$ wget http://ftp.gnu.org/gnu/binutils/binutils-2.27.tar.gz
$ tar -zxvf binutils-2.27.tar.gz  
$ cd binutils-2.27
$ ./configure --target=arm-none-eabi
$ sudo make
$ sudo make install

Hello World!

画面にdotを打つのがGBAプログラミングにおけるHello Worldのようです。

crt0.S

ではmain.rsから書けるかと言うとそうではありません。まずはmain関数を呼ぶためのスタートアップルーチンをアセンブラで書く必要があります。

といっても最低限でよければ以下を書くだけです。mainを呼び出しているだけです。割り込みを使用する際にはここでいくつか設定が必要になると思いますがとりあえずは不要です。詳しくは「Linuxから目覚めるぼくらのゲームボーイ!」に記載があります。

    .arm
__start:
    ldr r0, =main
    bx r0

linker.ld

今回のようなケースでは、どのようなメモリ構成になっていて、どこにどのセクションをロードするのか。を教えてあげる必要があります。そのためにリンカースクリプトを用意します。

ENTRY(__start)

MEMORY {
    boot (w!x) : ORIGIN = 0x2000000, LENGTH = 256K
    wram (w!x) : ORIGIN = 0x3000000, LENGTH = 32K
    rom (rx)   : ORIGIN = 0x8000000, LENGTH = 32M
}

SECTIONS {
    .text : {
        KEEP(target/crt0.o(.text));
        *(.text .text.*);
        . = ALIGN(4);
    } >rom = 0xff
}

「プログラムを内蔵ROM(0x8000000)に配置する」よう記述しています

main.rs

ようやくmain.rsにとりかかります。以下は液晶ディスプレイの中央に青いドットを一つ表示するサンプルです。

#![no_std]
#![feature(start)]

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

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    unsafe {
        (0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3
        (0x600_0000 as *mut u16)
            .offset((80 * 240 + 120) as isize)
            .write_volatile(0x001F);
    }
    0
}
#![no_std]

Rustの標準ライブラリはOSの機能を利用しているため今回のようなベアメタルなケースでは使用できません。そのため#![no_std]をつけてstdクレートではなくcoreクレートをリンクするよう示します。(アロケータも使用できなくなるため、この時点ではVecStringも使用不可能となります。)

#[panic_handler]

#[panic_handler]アトリビュートにて、パニック発生時の動作を定義しています

main
(0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3

0x400_0000番地はLCD制御を行うレジスタで0x0403を書き込むことで複数枚ある背景のなかから2枚目、描画モード3を指定しています。

GBAでもファミコンやゲームボーイのように8x8のスプライトを敷き詰めてゲームを描画するのが基本的な使用方法になるようなのですが、ピクセル単位で描画することができるモードがあり、モード3はそのうちの一つです。

スプライトの用意や設定をせずに直接描画できるので楽です。

write_volatileは最適化により省略されたり並べ替えられたりしないことを保証するメソッドです。

例えば組み込みなどでは「レジスタのあるビットが変化するまでloopの中で待ち続ける」といった処理を書くことがあるかと思いますが、その場合volatileがないと同じ番地にリードを繰り返す無意味な処理とみなされ省略されるケースがあると思います。xxxx_volatileを使用することでこれを防げるという認識です。

 (0x600_0000 as *mut u16)
            .offset((80 * 240 + 120) as isize)
            .write_volatile(0x001F);

0x0600_0000VRAM80 * 240 + 120にオフセットすることで真ん中にドットを打つことができますできます。これは表示領域が240 * 160のためです。

ここでは0x001Fを書き込んでいますが、これは赤色になります。GBAでこのモードの場合各ドット色は15bitで表現されます。bit0~4が赤、bit5~9が緑、bit10~14が青となっています。そのためこのサンプルは赤いドットになります。

arm-none-eabi.json

前述したようにARM7TDMI用のバイナリを吐くにはLLVMに対してターゲットを指定してやる必要があります

{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "thiscall",
    "win64",
    "sysv64"
  ],
  "arch": "arm",
  "cpu": "arm7tdmi",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "executables": true,
  "linker": "arm-none-eabi-ld",
  "linker-flavor": "ld",
  "linker-is-gnu": true,
  "llvm-target": "thumbv4-none-eabi",
  "os": "none",
  "panic-strategy": "abort",
  "pre-link-args-crt": {
    "ld": ["crt0.o"]
  },
  "pre-link-args": {
    "ld": ["-Tlinker.ld"]
  },
  "relocation-model": "static",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "32"
}

llvm-targetthumbv4-none-eabiを指定したりlinkerarm-none-eabi-ldに指定し、pre-link-args-crtcrt0.opre-links-argslinker.ldを指定することでGBA 用のバイナリを吐くようになります。

このjsonはいくつかのサンプルを参考にしており、その際data-layoutについて、初めて見たのでびっくりしたのですが、以下を見ると読み解くことができそうです。

llvm.org

どうやらターゲットのエンディアンがリトルエンディアンであることやポインタを32bitアライメントでレイアウトすること、64bit整数を64bitアライメントでレイアウトする等指定しているようです。

makefile

自分は以下のようなmakefileを用意しました。 arm-none-eabi-ascrt0.Sからcrt.oを作成しtarget配下に格納。

cargo xbuildでビルドした後、arm-none-eabi-objcopyelfbinに変換しています。

build:
  mkdir -p target
  arm-none-eabi-as src/crt0.S -o target/crt0.o
  cargo xbuild --target arm-none-eabi.json --release
  arm-none-eabi-objcopy -O binary target/arm-none-eabi/release/lifegameboy game.gba

ビルド

ここまででようやくビルドできそうです。

$ docker run --rm -v `pwd`:/code bokuweb/rust-gba make

でうまくいけばgame.gbaが生成されると思います。

実行

エミュレータで動作させてみます。自分はvisualboyadvance-mを使用しました。

$ visualboyadvance-m game.gba

うまく行けば以下のように赤いドットが表示されると思います。これでようやくHello World!が終了です。

f:id:bokuweb:20200414012224p:plain

Conway's Game of Life

もう少し動きのあるものを動かしてみたいので以前Wasmの勉強用に作ったライフゲームを移植してみたいと思います。

github.com

Allocator

前述したように現時点ではVecStringが使用できません。が、上記のサンプルはVecを使用しています。組み込みなどでは動的メモリ確保を禁止するルールなども多く、Vecを利用しないよう書き換えることも可能ですがせっかくなのでVecを使えるようにします。

linnker.ldの修正

linker.ldを修正し、data,bssのセクションを追加し、各開始と終了アドレス、またwramの終了アドレスを取れるようにしました。

SECTIONS {
    .text : {
        KEEP(target/crt0.o(.text));
        *(.text .text.*);
        . = ALIGN(4);
    } >rom = 0xff

    .rodata : {
        *(.rodata .rodata.*);
        . = ALIGN(4);
    } >rom = 0xff

    .data : {
        __data_start = ABSOLUTE(.);
        *(.data .data.*);
        . = ALIGN(4);
        __data_end = ABSOLUTE(.);
    } >wram AT>rom = 0xff

    .bss : {
        __bss_start = ABSOLUTE(.);
        *(.bss .bss.*);
        . = ALIGN(4);
        __bss_end = ABSOLUTE(.);
    } >wram

    __sidata = LOADADDR(.data);
    __wram_end = ORIGIN(wram) + LENGTH(wram) -1 ;
}

main.rsの修正

main.rsを修正します。まずはMutexHeapを作成します。これはCoretex-M用のアロケータであるrust-embedded/alloc-cortex-mから移植してきています。

Heap / Mutexの作成
pub struct Heap {
    heap: Mutex<linked_list_allocator::Heap>,
}

impl Heap {
    pub const fn empty() -> Heap {
        Heap {
            heap: Mutex::new(linked_list_allocator::Heap::empty()),
        }
    }

    pub unsafe fn init(&self, start_addr: usize, size: usize) {
        self.heap.lock(|heap| heap.init(start_addr, size));
    }
}


pub struct Mutex<T> {
    inner: UnsafeCell<T>,
}

impl<T> Mutex<T> {
    pub const fn new(value: T) -> Self {
        Mutex {
            inner: UnsafeCell::new(value),
        }
    }
}

impl<T> Mutex<T> {
    pub fn lock<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&mut T) -> R,
    {

        unsafe {
            let ie = core::ptr::read_volatile(REG_IE as *const u16);
            (REG_IE as *mut u16).write_volatile(0x0000);
            let ret = f(&mut *self.inner.get());
            (REG_IE as *mut u16).write_volatile(ie);
            ret
        }
    }
}

unsafe impl<T> Sync for Mutex<T> {}

rust-embedded/alloc-cortex-mの中身を見たところ、linked_list_allocatorクレートのHeapMutexで包んだものを使用していました。no_stdなのでMutexも用意する必要がありますが、リソース取得前に割り込みを禁止、取得後に割り込みを再設定をしてやれば良さそうです。以下の箇所が該当の箇所です。

let ie = core::ptr::read_volatile(REG_IE as *const u16);
(REG_IE as *mut u16).write_volatile(0x0000);
let ret = f(&mut *self.inner.get());
(REG_IE as *mut u16).write_volatile(ie);

global_allocatorの設定

上記で作成したHeapglobal_allocatorとして設定し初期化することでヒープが使用できるようになりVecが使用可能となります。

#[global_allocator]
static ALLOCATOR: Heap = Heap::empty();

unsafe impl GlobalAlloc for Heap {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        self.heap
            .lock(|heap| heap.allocate_first_fit(layout))
            .ok()
            .map_or(0 as *mut u8, |allocation| allocation.as_ptr())
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        self.heap.lock(|heap| heap.deallocate(NonNull::new_unchecked(ptr), layout));
    }
}

Allocatorの初期化

以下がヒープの初期化コードです。liner.ldの設定を参照し、ヒープの開始アドレスとサイズを求め設定しています。

extern "C" {
    static mut __bss_start: u8;
    static mut __bss_end: u8;
    static mut __data_start: u8;
    static mut __data_end: u8;
    static __sidata: u8;
    static __wram_end: u8;
}

fn init_heap() {
    unsafe {
        let heap_start = &__bss_end as *const u8 as usize;
        let heap_end = &__wram_end as *const u8 as usize;
        let heap_size = heap_end - heap_start;

        ALLOCATOR.init(heap_start, heap_size);
    }
}

Vecを使用したmain

あとはinit_heapを呼べば、ヒープを使用できるようになります。

#[macro_use]
extern crate alloc;

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    init_heap();
    let mut v = vec!(0);
    v.push(1);
    0
}

ここで以下のようなリンクエラーがでてハマっていたのですが、lto = trueにすることで成功するようになりました。詳細は不明なのですが、リンクする必要のないものを探しにいき失敗していたところ、最適化により回避できるようになったように見えます。

error: linking with `arm-none-eabi-ld` failed: exit code: 1
  |
  = note: "arm-none-eabi-ld" "-Tlinker.ld" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.0.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.1.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.2.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.3.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.4.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.5.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.6.rcgu.o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.gba_sandbox.dymocybm-cgu.7.rcgu.o" "-o" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o" "--gc-sections" "-O1" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/release/deps" "-L" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib" "-Bstatic" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/liblinked_list_allocator-50c4d42fee3618a7.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/liballoc-65305f99407f1a06.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/librustc_std_workspace_core-3a5b75d78f842520.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/libcore-f024f2af9aa61b13.rlib" "/home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/sysroot/lib/rustlib/arm-none-eabi/lib/libcompiler_builtins-fc44ec67faac1c70.rlib" "-Bdynamic"
  = note: arm-none-eabi-ld: warning: cannot find entry symbol __start; defaulting to 0000000008000000
          /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_alloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0'
          /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_dealloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0'
          /home/bokuweb/ghq/github.com/bokuweb/gba-sandbox/target/arm-none-eabi/release/deps/gba_sandbox-6d2b8af1d4d70cc2.ivk3z60kvi15yiz.rcgu.o:(.ARM.exidx.text.__rust_realloc+0x0): undefined reference to `__aeabi_unwind_cpp_pr0'
          

error: aborting due to previous error

ゲームの描画

今回は4x4を1lifeとして作成しました。当初1pixel/lifeで実装しようとしたのですが、あっという間にヒープが枯渇したためです。Working RAM領域は32KiBなので当たり前感がありますね。メモリを節約するよう書き換えれば1pixel/lifeでもいけそうでしたが、あまりゲーム側に手を加えたくなかったので今回はこのようにしています。

wait_for_vsyncVBlankまでブロックし待つ関数です。VRAMを更新するのは基本的にはVBlankの間に行うことになっています。VBlankというのは描画を行っていない期間です。これは描画中にVRAMを変更すると意図しない描画になる可能性があるためです。

VBlank中であるかどうかは割り込みにて知ることも可能ですが、REG_VCOUNTすなわち0x0400_0006をリードすることで知ることもできます。REG_VCOUNTを読むことで現在描画中のy座標を知ることができるのでこの値が160以上であればVBlankということになります。

あとは特に特筆することはなく、lifeを計算し描画していくだけなので割愛します。

static REG_VCOUNT: usize = 0x0400_0006;

fn wait_for_vsync() {
    unsafe {
        while core::ptr::read_volatile(REG_VCOUNT as *const u32) >= 160 {}
        while core::ptr::read_volatile(REG_VCOUNT as *const u32) < 160 {}
    }
}

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    // ... ommited ...

    init_heap();
    let mut game: Game = Game::new(40, 60);
    unsafe {
        (0x400_0000 as *mut u16).write_volatile(0x0403); // BG2 Mode 3
        loop {
            let field = game.next();
            wait_for_vsync();
            for (i, cell) in field.iter().enumerate() {
                let col = i % 60;
                let row = i / 60;
                let color = if *cell { 0x7FFF } else { 0x0000 };
                for j in 0..4 {
                    (0x600_0000 as *mut u16)
                        .offset(((row * 4 + j) * 240 + col * 4) as isize)
                        .write_volatile(color);
                    (0x600_0000 as *mut u16)
                        .offset(((row * 4 + j) * 240 + col * 4 + 1) as isize)
                        .write_volatile(color);
                    (0x600_0000 as *mut u16)
                        .offset(((row * 4 + j) * 240 + col * 4 + 2) as isize)
                        .write_volatile(color);
                    (0x600_0000 as *mut u16)
                        .offset(((row * 4 + j) * 240 + col * 4 + 3) as isize)
                        .write_volatile(color);
                }
            }
        }
    }
}

正しく動作すれば以下のようにライフゲームが動作します。

f:id:bokuweb:20200414012508p:plain

実機への転送

「Linuxから目覚めるぼくらのゲームボーイ!」にはブートケーブルが付属しており、これを使うことで実機で動作させることが可能になります。

linker.ldの修正

ブートケーブル経由で動作させる場合リンカースクリプトの修正が必要になります。これはブートモード時はROM領域ではなく0x0200_0000の外部RAM領域から起動するためです。 romの箇所をbootに変更します。

SECTIONS {
    .text : {
        KEEP(target/crt0.o(.text));
        *(.text .text.*);
        . = ALIGN(4);
    } >boot = 0xff

    .rodata : {
        *(.rodata .rodata.*);
        . = ALIGN(4);
    } >boot = 0xff

    .data : {
        __data_start = ABSOLUTE(.);
        *(.data .data.*);
        . = ALIGN(4);
        __data_end = ABSOLUTE(.);
    } >wram AT>boot = 0xff

    .bss : {
        __bss_start = ABSOLUTE(.);
        *(.bss .bss.*);
        . = ALIGN(4);
        __bss_end = ABSOLUTE(.);
    } >wram

    __sidata = LOADADDR(.data);
    __wram_end = ORIGIN(wram) + LENGTH(wram) -1 ;
}

optusbのセットアップ

ブートケーブル経由で転送するためにoptusbというツールを使います。optusbに必要なlibusbをセットアップするのですが、optusbが使用しているAPIが古いためlibusb-compatが必要となります。macであればbrewで入るようです。

ちなみに記事ではUbuntu19.10ThinkPad X1 Carbon gen 6thで試しています。

$ wget http://downloads.sourceforge.net/libusb/libusb-compat-0.1.5.tar.bz2
$ tar -jxvf libusb-compat-0.1.5.tar.bz2 
$ sudo make 
$ sudo make install
$ sudo mount --bind /dev/bus /proc/bus
$ sudo ln -s /sys/kernel/debug/usb/devices /proc/bus/usb/devices

次にoptusbのコードをダウンロード、展開します。

$ wget http://www.skyfree.org/jpn/unixuser/optusb-1.01.tar.gz
$ tar -zxvf optusb-1.01.tar.gz 

その後Makefileを書き換えビルドすることでセットアップは完了です。

optusb: optusb.c usb.h libusb.a
    gcc -I `brew --prefix libusb-compat`/include/usb.h -L `brew --prefix libusb-compat`/lib -lusb -Wall -o optusb optusb.c

clean:
    rm -rf optusb *.o
make

以下のエントリが参考になりました。

yaplus.hatenablog.com

ncircuit.blog.fc2.com

データの転送

本体の電源を投入後、ケーブルを接続、以下のコマンドで転送ができます。

sudo optusb/optusb game.gba

動作すれば冒頭のtweetように動作します。

結果

動機であったWasmインタプリタを動かせるか、ですが、インタプリタ側をno_stdに対応させれば動くのではないかと思っていますが、あとはメモリ容量との勝負になりそう。できたとしてもドットをちょっと動かす程度のものにはなると思いますが、もし成功したらまた記事にします。