見出し画像

FCS80のC言語クロスコンパイル対応(SDCC)

以下の記事の続きです。

FCS80(自作の架空ゲーム機)のプログラミングを、Z80アセンブリ言語だけではなくC言語でもできるようにしてみました。

Z80クロスコンパイラの選定

Z80用のCクロスコンパイラとしては、z88dkとSDCCの2つが有名です。

どちらかといえばz88dkの方が情報量が多いので主流だと思われますが、今回は敢えてSDCCを採用しました。

z88dkを採用しなかった理由

z88dkはコンパイル(zccコマンド実行)時にターゲットオプションを指定する必要があります。

例えば、MSXであれば +msx、SEGA Master Systemであれば +smsというターゲットオプションを指定します。MSX+z88dkの使い方については以下の記事が分かりやすいです。

つまり、z88dkはMSX、PC-88、SEGA Master Systemなどの既知のZ80プラットフォーム向けのクロスコンパイラです。

一方、SDCCはプラットフォームフリーなZ80向けのプログラムを記述できます。(そのため、z88dkより利用難度が少し高い→z88dkを使うユーザが多いということかなと思われます)

FCS80向けのターゲットがz88dkにはまだ無いので、よりプリミティブなクロスコンパイラであるSDCCの方が簡単に使うことができます。

z88dkにFCS80のターゲットを追加するには色々と揃える必要がある(参考)ので、将来的には対応しようと思いつつ、先ずはSDCCで揉んでみることにしました。

SDCCのインストール

ソースコードをダウンロードしてビルドすることもできますが、ものすごく時間が掛かるのでbrewやapt-getでインストールすることをオススメします。

# macOS の場合
brew install sdcc
# Ubuntu の場合
sudo apt-get -y install sdcc

Hello, World!

前回記事でも示しましたが、アセンブリ言語で記述したHello, World!のコードは次の通りです。

org $0000
.main
    ; 割り込み関連の初期化
    IM 1
    DI

    ; VBLANKを待機
    call wait_vblank

    ; パレットを初期化
    ld bc, 8
    ld hl, palette0_data
    ld de, $9400
    ldir

    ; Bank 1 を Character Pattern Table ($A000) に転送 (DMA)
    ld a, $01
    out ($c0), a

    ; 画面中央付近 (10,12) に "HELLO,WORLD!" を描画
    ld bc, 12
    ld hl, hello_text
    ld de, 394 + $8000 ; 394 = 12 * 32 + 10
    ldir

    ; メインループ
mainloop:
    ; VBLANKを待機
    call wait_vblank

    ; ジョイパッド(1P)の入力を読み取る
    ld a, $0E
    out ($A0), a
    in a, ($A2)
    ld b, a

    ; 左カーソルが押されているかチェック(押されている場合は左スクロール)
    ld hl, $9602
    and %00100000
    jp nz, mainloop_check_right
    inc (hl)
    jmp mainloop_check_up
mainloop_check_right:
    ; 右カーソルが押されているかチェック(押されている場合は右スクロール)
    ld a, b
    and %00010000
    jp nz, mainloop_check_up
    dec (hl)
mainloop_check_up:
    ; 上カーソルが押されているかチェック(押されている場合は上スクロール)
    ld hl, $9603
    ld a, b
    and %10000000
    jp nz, mainloop_check_down
    inc (hl)
    jmp mainloop_check_end
mainloop_check_down:
    ; 下カーソルが押されているかチェック(押されている場合は下スクロール)
    ld a, b
    and %01000000
    jp nz, mainloop_check_end
    dec (hl)
mainloop_check_end:
    jmp mainloop

; VBLANKになるまで待機
.wait_vblank
    ld hl, $9607
wait_vblank_loop:
    ld a, (hl)
    and $80
    jp z, wait_vblank_loop
    ret

palette0_data: defw %0000000000000000, %0001110011100111, %0110001100011000, %0111111111111111
hello_text: defb "HELLO,WORLD!"

これと全く同じ動作をするコードをC言語で記述すると以下のようになります。

#include "fcs80.h"

void main(void)
{
    uint8_t scrollX = 0;
    uint8_t scrollY = 0;
    uint8_t pad;

    // パレットを初期化
    fcs80_palette_set(0, 0, 0, 0, 0);    // black
    fcs80_palette_set(0, 1, 7, 7, 7);    // dark gray
    fcs80_palette_set(0, 2, 24, 24, 24); // light gray
    fcs80_palette_set(0, 3, 31, 31, 31); // white

    // Bank 1 を Character Pattern Table ($A000) に転送 (DMA)
    fcs80_dma(1);

    // 画面中央付近 (10,12) に "HELLO,WORLD!" を描画
    fcs80_bg_putstr(10, 12, 0x80, "HELLO,WORLD!");

    while (1) {
        fcs80_wait_vsync();
        pad = fcs80_joypad_get();
        if (pad & FCS80_JOYPAD_LE) {
            scrollX++;
            fcs80_bg_scroll_x(scrollX);
        } else if (pad & FCS80_JOYPAD_RI) {
            scrollX--;
            fcs80_bg_scroll_x(scrollX);
        }
        if (pad & FCS80_JOYPAD_UP) {
            scrollY++;
            fcs80_bg_scroll_y(scrollY);
        } else if (pad & FCS80_JOYPAD_DW) {
            scrollY--;
            fcs80_bg_scroll_y(scrollY);
        }
    }
}

やはり、高級言語だと(アセンブリ言語よりは)コードの可読性が高くて良いですね。

この他にもCRT0(mainをコールするアセンブリ言語の処理)とシステムコール(fcs80_)の実装もGitHubで公開しています。

https://github.com/suzukiplan/fcs80/tree/master/example/hello-sdcc

システムコールは一部処理(主にI/Oを使う処理)をインライン・アセンブラで記述していますが、FCS80だとVDPへのアクセスにI/Oが不要なのでC言語で記述可能なものも結構あり、サクサク作ることができそうです。

以下、hello.cで使っている範囲のシステムコールの実装(fcs80.c)です。

#include "fcs80.h"
#pragma disable_warning 59 // no check none return (return at inline-asm)
#pragma disable_warning 85 // no check unused args (check at inline-asm)

void fcs80_wait_vsync(void)
{
__asm
    ld hl, #0x9607
wait_vblank_loop:
    ld a, (hl)
    and #0x80
    jp z, wait_vblank_loop
__endasm;
}

void fcs80_palette_set(uint8_t pn, uint8_t pi, uint8_t r, uint8_t g, uint8_t b)
{
    uint16_t col;
    uint16_t addr;
    col = r & 0x1F;
    col <<= 5;
    col |= g & 0x1F;
    col <<= 5;
    col |= b & 0x1F;
    addr = 0x9400;
    addr += pn << 5;
    addr += pi << 1;
    *((uint16_t*)addr) = col;
}

void fcs80_dma(uint8_t prg)
{
__asm
    out (#0xC0), a
__endasm;
}

void fcs80_bg_putstr(uint8_t x, uint8_t y, uint8_t attr, const char* str)
{
    x &= 0x1F;
    y &= 0x1F;
    uint16_t addrC = 0x8000 + (y << 5) + x;
    uint16_t addrA = addrC + 0x400;
    while (*str) {
        *((uint8_t*)addrC) = *str;
        *((uint8_t*)addrA) = attr;
        addrC++;
        addrA++;
        str++;
    }
}

void fcs80_bg_scroll_x(uint8_t x)
{
    *((uint8_t*)0x9602) = x;
}

void fcs80_bg_scroll_y(uint8_t y)
{
    *((uint8_t*)0x9603) = y;
}

uint8_t fcs80_joypad_get(void)
{
__asm
    ld a, #0x0E
    out (#0xA0), a
    in a, (#0xA2)
    xor #0xFF
    ld l, a
    ret
__endasm;
}

インラインアセンブラが混じっているのでhello.cほどではないですが、そこそこ読みやすくて良い感じではないでしょうか。

コンパイル&アセンブル〜リンクまでの手続きは以下のような形です。

# crt0 をアセンブル → crt0.rel (obj file) を出力
sdasz80 -o crt0.s

# システムコール (fcs80.c) をコンパイル → fcs80.rel (obj file) を出力
# ※0x0000 〜 0x00FF は crt0 用にリザーブするので 0x0100 〜 にコードを展開
sdcc -mz80 --code-loc 0x0100 --no-std-crt0 -c fcs80.c

# hello.c をコンパイルして crt0.rel と fcs80.rel をリンク → hello.ihx を出力
# ※0x0000 〜 0x0FFF は crt0+システムコール用にリザーブして 0x1000 〜 にコードを展開
sdcc -mz80 --code-loc 0x1000 --no-std-crt0 -Wlcrt0.rel -Wlfcs80.rel hello.c

# ihx 形式をバイナリ形式に変換しつつ 8192 bytes でパディング → hello.bin を出力
makebin -s 8192 hello.ihx hello.bin

# 以下バンク構成の ROM ファイル作成 → hello.rom を出力
# - Bank 0: hello.bin
# - Bank 1: font.chr (フォント画像)
${MAKEROM} hello.rom hello.bin font.chr

現時点(2023.12.22時点)では、CRT0とシステムコールの実装もhello-sdccの中にすべて入っていますが、それらはライブラリに分離しようと考えています。

ただ、SDCCを使うための全体像の把握をするには現在のhello-sdccが良い感じかもしれないので、hello-sdcc はこのままの形で残しつつ、他の example を作る時は切り離されたライブラリモジュールを使う形にしようと思っているところです。

想定プログラムロケーション

hello-sdccは、ROM Bank #0 に全てのプログラムを突っ込んだ形になっていますが、恐らくシステムコールは膨らんでいくのでROM Bank #0をシステム専有(MSXのBIOSみたいなイメージ)にして、#1がユーザプログラムのメインコード、#2と#3をバンク切り替えして色々使うような感じが(C言語では)ベターな使い方かなと想定中です。

想定プログラムロケーション

もちろん、FCS80のハードウェアとしてはそういう制限はしません。

ひとまず、FCS80の全機能のシステムコール化を進めて様子を掴もうと思っているところです。(システムコールで8KB以上必要になることは無いと思いますが念のため)

この記事が気に入ったらサポートをしてみませんか?