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以上必要になることは無いと思いますが念のため)