ここからは Z80 コードの解説です。 全てを解説すると長くなるので、特に注意が必要な部分に絞ります。
Z80 の命令を覚えていないと読むのが大変だと思いますが、 少しずつ覚えていってください。 マシン語プログラミングに一番必要なのは根気です。
PC-8001 のテキスト RAM は 1 行あたり 120 バイトで、N-BASIC では
$F300
から配置されています。
文字は横 40 文字モードの場合は 1 バイトおきに使用されます(奇数アドレスは未使用)。 アトリビュートは常に 40 バイト使用されます。
今回はマシン語側ではアトリビュートの操作は行いません (BASIC 側で初期化したまま)。もし色を付けたりしたい場合、ROM 内ルーチンを利用すると楽です(詳しくは外部リンクを参照)。
さて、まずは XY 座標からテキスト RAM
アドレスを求めるサブルーチン
CALC_VRAM_ADDR
を作ります。今回は横 40 文字モードなので、アドレスは
$F300+Y*120+X*2
となります。
Z80 には乗算命令が無いので、乗算は毎回ちょっと苦労します。 「120 倍=128 倍-8 倍」と考えて、次のコードになりました。
VRAM EQU $F300 ; in : H=X, L=Y ; out: HL=VRAM addr ; brk: BC,AF CALC_VRAM_ADDR: LD B,0 LD C,H SLA C ; BC=X*2 LD A,L LD H,L LD L,B ; HL=Y*256 SRL H RR L ; HL=Y*128 ADD HL,BC ADD A,A ADD A,A ADD A,A LD C,A ; BC=Y*8 SBC HL,BC ; 最後の ADD A,A で CF=0 LD BC,VRAM ADD HL,BC RET
ここで一つ注意があります。BASIC ではカラーモードにした場合に左端 1 文字が使えませんが、上記の計算では考慮していないので、X 座標が 1 ズレます。
今回は全ての X 座標にその都度 1 足しましたが、このサブルーチンで 1 足した方が良かったかもしれません。
テキスト RAM アドレスを求めることができたので、次は 1
文字表示ルーチン PUT_CHAR
と文字列表示ルーチン PUT_STR
を作ります。
; in : H=X, L=Y, A=code
; out: HL=VRAM addr
; brk: E,BC,AF
PUT_CHAR:
LD E,A
CALL CALC_VRAM_ADDR
LD (HL),E
RET
; in : H=X, L=Y, DE=string addr
; out: HL=next VRAM addr, DE=next string addr
; brk: BC,AF
PUT_STR:
CALL CALC_VRAM_ADDR
@@:
LD A,(DE)
INC DE
OR A
RET Z
LD (HL),A
INC L ; ※1
INC HL
JR @B
文字列は 0 を終端コードとしています。 改行などの特殊な動作はありません。
※1
は INC HL
でもいいのですが、偶数アドレスに 1
足して奇数アドレスになるため、上位バイト(H
)
が変わらないことを利用した些細な高速化です。
では枠などを描くサブルーチン
INIT_SCR
を作ります。
INIT_SCR: LD HL,1*$100+0 ; 座標(1,0) CALL CALC_VRAM_ADDR PUSH HL LD (HL),$98 ; "┌" INC L INC HL LD A,$95 ; "─" LD B,36 @@: LD (HL),A INC L INC HL DJNZ @B LD (HL),$99 ; "┐" POP HL
ここまでで最上部 1 ラインを描画します。
グラフィック文字は打ち込めないので面倒ですが、文字コード一覧を見ながら打ち込むか、EQU
でわかりやすい名前を定義するなど工夫してください。
コツコツと枠を描いた後、右下のステータス類を表示しています。ここで
PUT_TRE_COUNT
というサブルーチンを呼んでおり、さらにその中で
PUT_DECIMAL1
というサブルーチンを呼んでいます。これは 1 バイトの値を 10
進数で表示するルーチンです。
PUT_TRE_COUNT:
LD HL,32*$100+20
LD A,(TRE_COUNT)
CALL PUT_DECIMAL1
RET
; in : H=X, L=Y, A=value
; brk: IX,IY,HL,DE,BC,AF
PUT_DECIMAL1:
LD D,0
LD E,A
; in : H=X, L=Y, DE=value
; brk: IX,IY,HL,DE,BC,AF
PUT_DECIMAL2:
CALL CALC_VRAM_ADDR
PUSH HL
POP IX
EX DE,HL
LD IY,DEC_TBL
LD B,0
@1:
LD D,(IY+1)
LD A,D
ADD A,A
JR C,@2F
LD E,(IY)
INC IY
INC IY
LD C,$30 ; "0"
@@:
OR A ; 念のため CF=0 に
SBC HL,DE
JR C,@F
INC B
INC C
JR @B
@@:
ADD HL,DE
LD A,B
OR A
JR Z,@F
LD (IX),C
INC IX
INC IX
@@:
JR @1B
@2:
LD A,B
OR A
RET NZ
LD (IX),$30 ; "0"
RET
DEC_TBL:
DW 10000,1000,100,10,1,$FFFF
2 バイトの値を 10 進数で表示する
PUT_DECIMAL2
に続いています。上の桁から順に
DEC_TBL
の各値を引けるだけ引いて、引けた回数を表示しています。
上位桁の 0 を表示しないように B レジスタで管理しています(一度も引けていないうちは 0)。
なお、SBC HL,DE
の前に OR A
でキャリーフラグをクリアしていますが、
実はここに来た時点で必ずクリアされているので必要ありません。
ただ、そのことを慎重に確認しなければならないのと、
将来コードを書き替える可能性を考えて、無理せず確実にクリアしています。
BASIC 版で INSTR
を使用しているところがあります。
2010 A$=MID$(M$(Z*16+Y),X+1,1):A=INSTR(" *@-+#?$",A$):IF A=0 THEN A=ASC(A$)-32
このあたりをマシン語に置き換えたのが以下の部分です。
OPEN_MAP:
CALL CALC_MAP_ADDR
LD A,(HL)
OR A
RET NZ
PUSH HL
LD HL,MAP_DATA
ADD HL,DE
LD A,(HL)
LD HL,MAP_SRC_CHARS
LD BC,8
CPIR
POP HL
JR Z,@1F
SUB 32
JR @2F
@1:
LD A,C
INC A
@2:
(中略)
MAP_SRC_CHARS:
DB "$?#+-@* "
あまり使われない CPIR
命令ですが、このような使い方ができます。ただし見つかるまで BC
レジスタを減らしていくため、検索バイト列(ここでは
MAP_SRC_CHARS
)は逆順にする必要があります。
あと INSTR
では最初に見つかると 1
が返りますが、CPIR
では最後に見つかると 0
が返るという違いもあります。
大半の処理は BASIC からマシン語に置き換えただけですが、3D 描画に関しては一つ大きな違いがあります。
BASIC 版では直接 PRINT
していましたが、マシン語版では
19x19 バイトの REND_WORK
を用意し、そこに描画した後でテキスト RAM に丸ごと転送しています。
こうすることで中途半端な書き換えが見えなくなります。
描画ルーチンは
REND_3D
で、ソースファイルは 3dd_rend.z80s
に分けてあります。
テキスト RAM への転送ルーチンは
TRANS_SCR
ですが、ここで初めに
WAIT_VBLANK_END
と
WAIT_VBLANK
というサブルーチンを呼んでいます。
; brk: AF WAIT_VBLANK: IN A,($40) AND $20 JR Z,WAIT_VBLANK RET ; brk: AF WAIT_VBLANK_END: IN A,($40) AND $20 JR NZ,WAIT_VBLANK_END RET
I/O ポート $40
入力はビット 5 が
V-Blank(垂直帰線期間)フラグで、
ここを監視することで画面の表示タイミングと合わせることができます。
また、マシン語で実行が速すぎる場合に、速度調整のためにも使用できます。
なお、PasocomMini PC-8001 の垂直周波数は(HDMI 出力の都合上)60Hz ですが、PC-8001 実機では 25 行モードで 62.42Hz と、多少の違いがあります。ご注意ください。
BASIC 版では動作が遅かったので、キー入力を INP
で読むことで押しっぱなしでも動けるようにしましたが、
マシン語だと速すぎて思うように動けなくなるので、
キーを離すのを待つことにします(動作としては、BASIC で
INKEY$
を使うのと同様)。
WAIT_INPUT: @@: IN A,(0) INC A JR NZ,@B IN A,(1) INC A JR NZ,@B INPUT_LOOP: IN A,(9) BIT 0,A ; STOP キー JP Z,EXIT
I/O ポート $00
入力と $01
入力が両方 $FF
になるまで待っています。
さらに、STOP キーが押されたら終了しています(BASIC に戻って Break がかかります)。
効果音は適当にウェイトを入れて、だいたい BASIC 版と同じ音を出しています。ちなみにマシン語なら高速に BEEP 音を ON/OFF できるので、もっと凝った音を出すことも可能です。
BEEP 音は I/O ポート $40
出力のビット 5 で ON/OFF
します。ただしこのポートの他のビットは変えてはいけません
(他の用途に使われているので)が、$40
入力で最後に出力した値を読み取れるわけではありません。
ではどうするかというと、N-BASIC では最後に出力した値を
$EA67
に格納しているのを利用します。
OUT_40H EQU $EA67 ; brk: AF BEEP1: LD A,(OUT_40H) OR $20 OUT ($40),A RET ; brk: AF BEEP0: LD A,(OUT_40H) AND $DF OUT ($40),A RET
こうすることで、他のビットを変えずに BEEP 音の ON/OFF ができます。ROM 内ルーチンを呼び出す方法もありますが、 処理が単純なのでこの方が良いでしょう。
スタート直後、1 歩も進まずに A キーを押すと、オートプレイモードになります。 自力で解きたい方は押さないようにしましょう。
↓がその動画です。