⛩️

RustでX68000のMemory Mapped IOにアクセス

2024/02/20に公開

今回はRustで書いたコードからX68000[1]のMemory Mapped IOにアクセスします。
前回記事[2]に引き続きwasm-pack[3]を使ってビルドしたwasmバイナリをwasm2cで変換して必要となるインタフェースを探りながら進めます。

リアルタイムクロックへのアクセス

まずはスーパーバイザモードへ入ってリアルタイムクロック(RTC)のレジスタを2回一致で呼び出すことを目指します[4]

Rust側のコーディング体験

下記のようなRustコードでIOアクセスができることを目指します。

mod utils;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn x68k_sv(f: &dyn Fn());
    fn x68k_sv_mut(f: &mut dyn FnMut());

    fn x68k_bpoke(address:u32, value: u8);
    fn x68k_wpoke(address:u32, value: u16);
    fn x68k_lpoke(address:u32, value: u32);
    fn x68k_bpeek(address:u32) -> u8;
    fn x68k_wpeek(address:u32) -> u16;
    fn x68k_lpeek(address:u32) -> u32;

    fn puts(s: &str);
}

#[wasm_bindgen]
pub fn main() {
    let mut test_value = 0;
    x68k_sv_mut(&mut ||{
        test_value = x68k_lpeek(0xE8A000) & 0x000F0007;
        loop {
            let value = x68k_lpeek(0xE8A000) & 0x000F0007;
            if value == test_value { break; }
            test_value = value;
        }
    });
    puts(&format!("{}", test_value));
}

x68k_sv, x68k_sv_mutは引数で渡した無名関数をスーパーバイザモードに切り替えた状態で実行する関数です。
このような設計にするのは下記4点のメリットがあると考えているからです。

  • スーパーバイザモードの開始と終了が必ず対になることを保証できる
  • スーパーバイザモードに切り替わっているブロックがコード上で明確にできる
  • スーパーバイザモードに入ることによるスタックの切り替えを隠蔽できる
  • 無名関数の作るクロージャに変数をキャプチャさせることでスーパーバイザブロック外の変数にアクセスできる

2種類の関数を用意するのはブロック内でクロージャにキャプチャされたローカル変数を書き換える場合と書き換えない場合でクロージャの型が異なるためです。
今回はリアルタイムクロックの内容をブロック外のローカル変数に保持するのでミュータブルなクロージャを受け付けるx68k_sv_mut関数を使います。

x68k_Xpeek, x68k_Xpokeは、指定したアドレスへの読み書きを行うために用意する関数群です。RustでMemory Mapped IOへのアクセスを行う際は通常core::ptr::read_volatile, write_volatileを使うようですが、今回はwasm2cでの変換中に明確にIOアクセスしている個所のコードをマークしたいのでこのような方式をとります。

つづいて、このコードを実行するためにC言語側でどのような準備が必要になるか見ていきます。

C言語側の関数

まずは無名関数をコールバックするのにC言語側でどのような処理を用意する必要があるのか検討するためにwasm-bindgenの生成したjavascriptのコードを確認します。
コールバック関連個所を抜粋すると下記のようになっています。
javascriptのクロージャを作るためにいろいろとやっていますが、要点だけ見るとRust側から関数の引数として渡された2つの値arg0, arg1を引数にしてwasm_bindgen__convert__closures__invoke0_mut__h48b58bec9d0cc22fを呼び出してあげればコールバックが実現できそうです。

function __wbg_adapter_2(arg0, arg1) {
    wasm.wasm_bindgen__convert__closures__invoke0_mut__h48b58bec9d0cc22f(arg0, arg1);
}
export function __wbg_x68ksvmut_96b8b8b7cd2a1ebf(arg0, arg1) {
    try {
        var state0 = {a: arg0, b: arg1};
        var cb0 = () => {
            const a = state0.a;
            state0.a = 0;
            try {
                return __wbg_adapter_2(a, state0.b, );
            } finally {
                state0.a = a;
            }
        };
        x68k_sv_mut(cb0);
    } finally {
        state0.a = state0.b = 0;
    }
};

スーパーバイザモードを開始してコールバックを行うためにC言語側は下記のコードとしました。

extern void wasm_wasm_bindgen__convert__closures__invoke0_mut__h48b58bec9d0cc22f(uint32_t, uint32_t);

static inline void svmode(void (*invoke)(uint32_t, uint32_t), uint32_t arg0, uint32_t arg1){
    asm volatile(
        "suba.l a1, a1\n"
        "IOCS $81\n"
        "move.l d0, -(sp)\n"
        "movea.l %0, a0\n"
        "move.l  %2, -(sp)\n"
        "move.l  %1, -(sp)\n"
        "jsr (a0)\n"
        "add.l #8,sp\n"
        "move.l (sp)+, d0\n"
        "bmi skip_%=\n"
        "movea.l d0, a1\n"
        "IOCS $81\n"
        "skip_%=:\n"
        :
        : "r"(invoke), "r"(arg0), "r"(arg1)
        : "d0", "d1", "d2", "a0", "a1", "a2"
    );
}

void __wbg_x68ksvmut_96b8b8b7cd2a1ebf(uint32_t arg0, uint32_t arg1) {
    svmode((void (*)(uint32_t, uint32_t))wasm_wasm_bindgen__convert__closures__invoke0_mut__h48b58bec9d0cc22f, arg0, arg1);
}

スーパーバイザモードの開始終了のコードはインラインアセンブラで実装しています。
インラインアセンブラへの値の受け渡しは利用するアドレッシングをイミディエイト、レジスタ、メモリ(irm)の制約で指定できます。今回はメモリでの受け渡しを許可してしまうとスタック渡しのコードが生成されてしまうことがあるのでこれを避けるためにレジスタrでの受け渡しにしています。関数ポインタに関してはイミディエイトiを使うと外部関数のラベルが正常に生成されないためこちらもレジスタ制約としています。

peekのコードは下記のようにvolatileで読み出すコードとしました。

uint32_t __wbg_x68klpeek_dc3197ee9e360209(uint32_t adr) {
    return *(volatile uint32_t*)adr;
}

最後にmain関数からwasm_mainを呼び出すようにして完成です。

int main(int argc, char **argv) {

    global_argc = argc;
    global_argv = argv;

    wasm_main();
}

なお、前回記事で説明したthrow関数なども同じように利用していますが、今回の記事では説明を省略させていただきます。

実行結果

XM6[5]上で実行すると下記のようになります。

実行するたびに違う値がとれてますね。

ちなみにスーパーバイザモードに入らずにIOに触ると下記のようにバスエラーが発生してしまいます。

ラスタースクロールに挑戦

IOアクセスができるようになったのでラスタースクロールに挑戦してみます。今回はスーパーバイザモードに切り替えてさらに割り込みを禁止にした状態でレジスタをポーリングして水平帰線期間を捕まえる方法をとります[6][7]

mod utils;

use core::f32::consts::PI;
use wasm_bindgen::prelude::*;

const X68K_MMIO_CRTC_GRAPH_X0: u32 = 0xE80018;
const X68K_MMIO_GPIP: u32 = 0xE88001;
const X68K_MMIO_GPIP_BIT_HSYNC: u8 = 0x80;
const X68K_MMIO_GPIP_BIT_VDISP: u8 = 0x10;

#[wasm_bindgen]
extern "C" {
    fn x68k_sv(f: &dyn Fn());
    fn x68k_sv_mut(f: &mut dyn FnMut());
    fn x68k_disable_intrupt(f: &dyn Fn());
    fn x68k_disable_intrupt_mut(f: &mut dyn FnMut());

    fn x68k_bpoke(address:u32, value: u8);
    fn x68k_wpoke(address:u32, value: u16);
    fn x68k_lpoke(address:u32, value: u32);
    fn x68k_bpeek(address:u32) -> u8;
    fn x68k_wpeek(address:u32) -> u16;
    fn x68k_lpeek(address:u32) -> u32;

    fn puts(s: &str);
}
fn build_sin_table() -> Vec<u16> {
    let mut sin_table = Vec::new();
    for i in 0..64 {
        unsafe {
            sin_table.push((16.0 + 16.0 * ((i as f32) / 32.0 * PI).sin()).round().to_int_unchecked());
        }
    }
    sin_table
}
#[wasm_bindgen]
pub fn main() {
    let wave_table = build_sin_table();
    x68k_sv(&mut ||{
        let mut offset = 0;
        loop {
            while (x68k_bpeek(X68K_MMIO_GPIP) & X68K_MMIO_GPIP_BIT_VDISP) != 0x00 {
            }
            while (x68k_bpeek(X68K_MMIO_GPIP) & X68K_MMIO_GPIP_BIT_VDISP) == 0x00 {
            }
            x68k_disable_intrupt(&mut ||{
                let mut y_posizion = 0;
                let mut phase = offset;
                loop {
                    while (x68k_bpeek(X68K_MMIO_GPIP) & X68K_MMIO_GPIP_BIT_HSYNC) == 0x00 {
                    }
                    x68k_wpoke(X68K_MMIO_CRTC_GRAPH_X0, wave_table[phase]);
                    y_posizion += 1;
                    if y_posizion >= 256 { break; }
                    phase = (phase + 1) & 0x3F;
                    while (x68k_bpeek(X68K_MMIO_GPIP) & X68K_MMIO_GPIP_BIT_HSYNC) != 0x00 {
                    }
                }
            });
            // offset = (offset + 1) & 0x3F;
        }
    });
}

割り込み禁止もスーパーバイザモードと同じような方式で実装します。x68k_disable_intruptに渡した無名関数内の処理が割り込み禁止で実行されます。
この関数のC言語側の実装は下記のようにしました。

static inline void dis_intr(void (*invoke)(uint32_t, uint32_t), uint32_t arg0, uint32_t arg1){
    asm volatile(
        "ori.w #$0700, sr\n"
    );
    invoke(arg0, arg1);
    asm volatile(
        "andi.w #$f8ff, sr\n"
    );
}

void __wbg_x68kdisableintrupt_3ec1858ba4c5dc6a(uint32_t arg0, uint32_t arg1) {
    dis_intr((void (*)(uint32_t, uint32_t))wasm_wasm_bindgen__convert__closures__invoke0__hf2795e69f4eae89b, arg0, arg1);
}

実行結果

XM6での実行結果を下記に掲載します。
縦に直線を引いたページをラスタースクロールしています。


64ドット周期のサイン波でスクロールさせているので縦256ドット表示では4周期分の波形が見えるのが正解ですがそのようにはなっていないようです。
違う周波数の波になっている感じですね。水平帰線の信号を取りこぼしているのが原因だろうと思います。

水平帰線をポーリングしている部分のアセンブリコードは下記のようなコードが生成されていました。
数マイクロセカンドの信号変化を捕まえるのはこのコードでは難しいのでしょうね。

	lea ___wbg_x68kbpeek_a51a25fcf8ed5e80,a4	*	lea __wbg_x68kbpeek_a51a25fcf8ed5e80,%a4	|, tmp70
_?L225:							*.L225:
							*| raster.c:4984:             l7 = __wbg_x68kbpeek_a51a25fcf8ed5e80(l7);
	move.l #15237121,-(sp)				*	move.l #15237121,-(%sp)	|,
	jbsr (a4)					*	jsr (%a4)		| tmp70
							*| raster.c:4988:             if (l7) {
	addq.l #4,sp					*	addq.l #4,%sp	|,
	tst.b d0					*	tst.b %d0	| tmp89
	jbpl _?L225					*	jpl .L225		|

まとめ

今回はRustのコードでスーパーバイザモードに切り替えてMemory Mapped IOにアクセスするコードに挑戦しました。スーパーバイザモードへの切り替えやIOへのアクセス自体は問題なく行うことができました。
また、IOをポーリングする方式のラスタースクロールにも挑戦しました。結果としては生成されるコードの効率が悪く水平同期を捕まえるのは厳しかったようです。

次回予告

次回はwasm2cをチューニングしてラスタースクロールがちゃんと動くことを目指します

脚注
  1. wikipedia:X68000 ↩︎

  2. wasm-packで作ったLife GameをレトロPC(X68000)で動かす ↩︎

  3. wasm-pack ↩︎

  4. xdev68kのスーパーバイザモードのサンプルコードを参考にさせていただきました ↩︎

  5. X680x0エミュレータ XM6 TypeG ↩︎

  6. MicroPython for X680x0のサンプルコードを参考にさせていただきました ↩︎

  7. レジスタの詳細はぷにぐらま~ずまにゅあるを参考にさせていただきました ↩︎

Discussion