Z80アセンブリ言語の学習その2
前回、アセンブリ言語についての説明を行いました。今回もその続きです。今回は自分でサブルーチンを作れるようになることを目的とします。
以下、サンプルコードです。エディタにコードをコピペするなどしてsample002.asmというファイル名で保存してください。できるだけコメントを付けて読みやすくしているつもりです。
https://github.com/sailorman-msx/games/blob/main/src/sample002.asm
; BIOSルーチン
REDVRM:equ $004A ; VRAMの内容をAレジスタに読み込む
WRTVRM:equ $004D ; VRAMのアドレスにAレジスタの値を書き込む
SETRED:equ $0050 ; VRAMからデータを読み込める状態にする
SETWRT:equ $0053 ; VRAMにデータを書き込める状態にする
FILVRM:equ $0056 ; VRAMの指定領域を同一のデータで埋める
LDIRMV:equ $0059 ; VRAMからRAMにブロック転送する
LDIRVM:equ $005C ; RAMからVRAMにブロック転送する
CHGMOD:equ $005F ; SCREENモードを変更する
SETGRP:equ $007E ; VDPのみをGRAPHIC2モードにする
ERAFNK:EQU $00CC ;ファンクションキーを非表示にする
GTSTCK:equ $00D5 ; JOY STICKの状態を調べる
GTTRIG:equ $00D8 ; トリガボタンの状態を返す
CHGCLR:equ $0111 ; 画面の色を変える
KILBUF:equ $0156 ; キーボードバッファをクリアする
; ワークエリア
LINWID:equ $F3AF ; WIDTHで設定する1行の幅が格納されているアドレス
RG0SAV:equ $F3DF ; VDPレジスタ#0の値が格納されているアドレス
FORCLR:equ $F3E9 ; 前景色が格納されているアドレス
BAKCLR:EQU $F3EA ;背景色のアドレス
BDRCLR:equ $F3EB ; 背景色が格納されているアドレス
CLIKSW:equ $F3DB ; キークリック音のON/OFFが格納されているアドレス
INTCNT:equ $FCA2 ; MSX BIOSにて1/60秒ごとにインクリメントされる値が格納されているアドレス
; 変数格納エリア
; 変数格納エリアをプログラム領域の近いところに設定すると暴走のもとになるため
; わりと遠いアドレスにしておくのが良い
MESSAGE_ADR:equ $D000
;---------------------------------------------------------
; 初期処理(お約束コード)
;---------------------------------------------------------
; プログラムの開始位置アドレスは0x4000
org $4000
Header:
; MSX の ROM ヘッダ (16 bytes)
; プログラムの先頭位置は0x4010
defb 'A', 'B', $10, $40, $00, $00, $00, $00
defb $00, $00, $00, $00, $00, $00, $00, $00
Start:
; スタックポインタを初期化
ld sp, $F380
; 画面構成の初期化
ld a, $0F
ld (FORCLR), a ;白色
ld a, $01
ld (BAKCLR), a ;黒色
ld (BDRCLR), a ;黒色
;SCREEN1,2
ld a,(RG0SAV+1)
or 2
ld (RG0SAV+1),a ;スプライトモードを16X16に
ld a, 1 ;SCREEN1
call CHGMOD ;スクリーンモード変更
ld a, 32 ;WIDTH=32
ld (LINWID), a
;ファンクションキー非表示
call ERAFNK
;カチカチ音を消す
ld a, 0
ld (CLIKSW), a
;---------------------------------------------------------
; 初期処理(お約束コード)ここまで
;---------------------------------------------------------
;---------------------------------------------------------
; MAIN-LOOP
;---------------------------------------------------------
MainLoop:
; 無駄ループを繰り返し遅延させる
; 遅延させないとマシン語は速すぎる!
; call でサブルーチンにジャンプする
call DelayLoop
call DelayLoop
call DelayLoop
; テキストメッセージを全て*にする
ld hl, MESSAGE_ASTERISK
ld de, MESSAGE_ADR
ld bc, 26
; LDIRはブロック転送命令
; LDIRはHLが指し示すアドレスの内容を
; DEレジスタが指し示すアドレスを先頭に
; BCレジスタが指し示すバイト数だけブロック転送する
; このサンプルだと以下のようになる
; MESSAGE_ASTERISKのアドレス以降26バイトぶんの文字を
; MESSAGE_ADRのアドレスの中にブロック転送する
ldir
; テキストメッセージを画面に表示する
call PrintMessage
; 無駄ループを繰り返し遅延させる
call DelayLoop
call DelayLoop
call DelayLoop
; テキストメッセージを文章にする
ld hl, MESSAGE
ld de, MESSAGE_ADR
ld bc, 26
ldir
; テキストメッセージを画面に表示する
call PrintMessage
; MainLoopに戻る
; JPは直接アドレッシング指定と呼ばれるジャンプ
jp MainLoop
;---------------------------------------------------------
; SUB-ROUTINE: DelayLoop
; 空ループを繰り返し処理遅延をおこす
;---------------------------------------------------------
DelayLoop:
ld b, 150 ; 150 * 255回空ループを繰り返す
DelayLoop1:
ld a, $FF ; Aレジスタに$FF(255)をセット
DelayLoop2:
dec a ; Aレジスタの値から1を減算する
; ZフラグがたっていなければDelayLoop2に戻る
; JRは間接アドレッシング指定と呼ばれるジャンプ
jr nz, DelayLoop2
; Bレジスタの値から1を減算しZフラグがたっていなければDelayLoop1に戻る
; DJNZも間接アドレッシング指定
djnz DelayLoop1
; ret で呼び出し元の次の命令アドレスに実行位置が戻る
ret
;---------------------------------------------------------
; SUB-ROUTINE: PrintMessage
;---------------------------------------------------------
PrintMessage:
; VRAMを使って画面に文字を表示する
; HLレジスタにメモリ(RAM)のアドレス
; DEレジスタにVRAMの先頭アドレス
; BCレジスタに転送サイズ(バイト)
; VRAMにメモリのデータを転送するにはBIOSのLDIRVMを使う
ld hl, MESSAGE_ADR
ld de, $1800 ; 数値の先頭に$をつけると16進として解釈する
ld bc, 26 ; メッセージは26バイト
call LDIRVM
ret
;---------------------------------------------------------
; 固定値領域
;---------------------------------------------------------
MESSAGE:
defm "Let's make an arcade game!"
MESSAGE_ASTERISK:
defm "**************************"
変数
いわゆる”プログラミング言語”には必ず変数定義ということが出来ますが、アセンブリ言語では変数という概念はありません。レジスタは変数っぽい感じですが数にも限りがありますし頻繁に値が変わるので変数として扱うにはとても貧弱です。ではどうするか?というと、変数に該当するアドレスを自分で作るのです。上記ソースでは以下の箇所で変数エリアを定義しています。この変数エリアは文字列を格納する場所として、MESSAGE_ADRという名称でアドレス定義しています。
; 変数格納エリア
; 変数格納エリアをプログラム領域の近いところに設定すると暴走のもとになるため
; わりと遠いアドレスにしておくのが良い
MESSAGE_ADR:equ $D000
ソース内のコメントにも書いていますが、変数エリアをプログラムのマシン語が格納される場所に近いところのアドレスに定義すると誤って値を変更したりなんたりとバグの元なので、遠いアドレスに定義するようにしましょう。
ブロック転送命令
今回のソースコードではLDIRという命令を書いています。これはブロック転送と呼ばれる命令でHLレジスタに格納されているアドレスの値をDEレジスタに格納されているアドレスに転送します。転送するバイト数はBCレジスタで指定します。下記コードではMESSAGE_ADR変数のアドレスにMESSAGE_ASTERISKアドレスの文字列を26バイトぶん転送しています。
; テキストメッセージを全て*にする
ld hl, MESSAGE_ASTERISK
ld de, MESSAGE_ADR
ld bc, 26
; LDIRはブロック転送命令
; LDIRはHLが指し示すアドレスの内容を
; DEレジスタが指し示すアドレスを先頭に
; BCレジスタが指し示すバイト数だけブロック転送する
; このサンプルだと以下のようになる
; MESSAGE_ASTERISKのアドレス以降26バイトぶんの文字を
; MESSAGE_ADRのアドレスの中にブロック転送する
ldir
サブルーチン
今回のソースコードでは、DelayLoopというサブルーチンとPrintMessageというサブルーチンを作っています。DelayLoopサブルーチンの中に新しく登場した命令を書いていますのでそれを説明します。
;---------------------------------------------------------
; SUB-ROUTINE: DelayLoop
; 空ループを繰り返し処理遅延をおこす
;---------------------------------------------------------
DelayLoop:
ld b, 150 ; 150 * 255回空ループを繰り返す
DelayLoop1:
ld a, $FF ; Aレジスタに$FF(255)をセット
DelayLoop2:
dec a ; Aレジスタの値から1を減算する
; ZフラグがたっていなければDelayLoop2に戻る
; JRは間接アドレッシング指定と呼ばれるジャンプ
jr nz, DelayLoop2
; Bレジスタの値から1を減算しZフラグがたっていなければDelayLoop1に戻る
; DJNZも間接アドレッシング指定
djnz DelayLoop1
; ret で呼び出し元の次の命令アドレスに実行位置が戻る
ret
DelayLoop2:の中に DEC A という命令があります。これはAレジスタに格納されている値を1減らしてAレジスタに格納します。1減らすことをデクリメントといいます。逆に1増やすことはインクリメントといいます。インクリメントの命令はINCです。
フラグレジスタと条件付きジャンプ命令
レジスタにはフラグレジスタ(Fレジスタ)というものがあります。フラグレジスタには値を転送することはできませんが、演算結果によってビットがたちます(1になる)。このフラグレジスタを使って条件分岐を行います。とはいってもフラグレジスタの値をAレジスタにLDしてうんぬん、、などとする必要はありません。
N(加減算フラグ)とH(ハーフキャリーフラグ)はプログラミングでは使いません。未使用と書かれているビット部分は未使用です。
CY(キャリーフラグ)は足し算や引き算などで繰り上がりや繰り下がりがおきたときに1がたちます。INC命令やDEC命令ではこのフラグには影響しません。かなりの頻度で使います。
P/V(パリティオーバーフローフラグ)はANDやORの論理演算の結果で1がたちます。筆者は使ったことがありません。
Z(ゼロフラグ)は演算の結果が0になった場合に1がたちます。かなりの頻度で使います。
S(サインフラグ、符号フラグ)は演算の結果がマイナスの値になった場合に1がたちます。筆者はあまり使いません。
上記のうちプログラミングにおいて重要なフラグは太字箇所のフラグです。
サンプルコードでは
JR NZ, DelayLoop2
と書いています。これは、DEC Aの結果としてゼロフラグがたっていなければDelayLoop2にジャンプする。という意味になります。
BASICだとこんな感じでしょうか。
100 A=255
110 A=A-1
120 IF A <> 0 THEN GOTO 110
フラグレジスタの条件付きジャンプを使うことで、いろんな条件分岐を行う。というのがマシン語の定石になります。当noteでは以降、いろんなところで条件付きジャンプのコードが登場しますので、つど説明しようと思います。
特殊な条件付きジャンプ(DJNZ)
サンプルコードに以下のような記述があります。これは条件付きジャンプでもBレジスタだけに限定したNon Zeroジャンプ命令です。
;---------------------------------------------------------
; SUB-ROUTINE: DelayLoop
; 空ループを繰り返し処理遅延をおこす
;---------------------------------------------------------
DelayLoop:
ld b, 150 ; 150 * 255回空ループを繰り返す
(中略)
; Bレジスタの値から1を減算しZフラグがたっていなければDelayLoop1に戻る
; DJNZも間接アドレッシング指定
djnz DelayLoop1
DJNZ命令は「Bレジスタをデクリメントしてその結果、ゼロフラグがたたなければ間接アドレッシングでジャンプ」という命令になります。
DEC B
JR NZ, DelayLoop1
だと、マシン語にしたときに
05 20 n
で3バイトになりますが
DJNZ DelayLoop1だとマシン語にしたときに
10 n
で2バイトで済みます。1バイト少なくて済むのです。
前回記事にも書きましたがたかが1バイト、されど1バイトです。
今回はここまでです。実際にz80asmコマンドを使ってサンプルコードをアセンブルして、sample002.binをWebMSXで動かしてみてください。
ソースコードはいつものようにGitHubにも置いてます。お好きにどうぞ。
ここまでの記事でおおよそ簡単なプログラムは読めるようにはなってきたんじゃないかなと思います。
次回はMSXのGRAPHIC2モードについて説明しようと思います。GRAPHIC2モードこそMSXの醍醐味といっても過言ではありません。いちばん楽しいところだと筆者は思っています。
では、また!
MSX余談:ROMにないサブルーチンをなぜ呼べる?MSXのメモリ空間
当noteでは常にROMイメージとしてアセンブルしています。4000Hを開始位置としたプログラムとしています。そのプログラムには005CHといったBIOSのサブルーチン(プログラム)はありません。でもなぜ呼び出しが出来るのでしょうか?
実はMSXではメモリ領域をスロットとかページとかっていう概念で切り分けて組み合わせてメモリ空間を実現しているようです。
筆者はこのへんがよくわかっていませんので厳密な説明がしにくいのですが、例えば0000H〜3FFFHまではMSX本体についているBIOSメモリ空間を使ったり、F380H以降もMSX本体についているメモリ空間を使ったり、そして4000HからはROMのメモリ空間を使うなどしているようです。組み合わせて使っているというイメージのようですね。
誰か詳しいひといたら教えてください(汗)
ちなみにVRAMはVDP側のメモリ空間になっていてMSX本体のメモリ空間でもROMのメモリ空間でもありません。