見出し画像

Z80アセンブリ言語の書き方(自己流)


前回の記事ではソースコードをもとにアセンブラでマシン語にアセンブルしてWebMSXで動作させるところまでを実施しました。今回はそのソースコードをサンプルとしてアセンブリ言語の書き方について説明します。途中細かいところもありますが、アセンブリ言語ってこうなんだ。ということが理解できれば良いです。MSXというよりかはZ80の説明寄りの記事になっています。あんまり面白くないかも・・(?)
まずは前回記事のサンプルコードはこちらです。

なお、今回のサンプルコードは、ニャオニャオ21世紀様と@suzukiplan様のサイトも参考させていただいております。本当に感謝感謝であります。

; 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秒ごとにインクリメントされる値が格納されているアドレス

;--------------------------------------------
; 初期処理(お約束コード)
;--------------------------------------------
; プログラムの開始位置アドレスは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

    ;--------------------------------------------
    ; 初期処理(お約束コード)ここまで
    ;--------------------------------------------

    ; VRAMを使って画面に文字を表示する
    ; HLレジスタにメモリ(RAM)のアドレス
    ; DEレジスタにVRAMの先頭アドレス
    ; BCレジスタに転送サイズ(バイト)
    ; VRAMにメモリのデータを転送するにはBIOSのLDIRVMを使う

    ld hl, MESSAGE
    ld de, $1800    ; 数値の先頭に$をつけると16進として解釈する
    ld bc, 26       ; メッセージは26バイト
    call LDIRVM
    
End:
    jr End

MESSAGE:
    defm "Let's make an arcade game!"

BIOSルーチン

この箇所ではプログラムが使用するBIOSのサブルーチンのアドレスをひたすら名前付け(ラベル化)しています。

(書式)
LABEL名: EQU アドレス

LDIRVM:equ $005C はBIOSのLDIRVMというサブルーチンのアドレス(005CH)をソースコード内だけで通用するLDIRVMという名称で定義しています。

EQUというのはアセンブリ言語の疑似命令というやつで、疑似命令はそれ自体がマシン語に変換されるものではありません。コードを読みやすくするためのなんちゃって命令みたいなものと理解してください。ちなみにEQUはEQUALの略だと思っています。

LDIRVM:equ $005C と疑似命令で定義付けされているとアセンブラはソースコード中のLDIRVMの文字列箇所を005CHに変換します。

BIOSというのはBasic Input Output Systemのことでコンピュータの基本的な操作を制御しているサブルーチンがたくさんあります。

MSXのBIOSのサブルーチン名は4文字だったり6文字だったりとまちまちなのですが私は常に大文字6文字のラベル名を付けて定義するようにしています。「大文字6文字だからこれはBIOSのサブルーチンなんだな」と読みやすいようにしているだけです。サンプルコードに記載されている以外のBIOSもありますが、そちらはテクハンWikiを参照してください。

とりあえずこのサンプルコードのBIOSの定義だけでも「ゲームっぽい何か」は作れます。足りないのはサウンド関連(PSG)のBIOSくらいです。

ワークエリア

MSXにはワークエリアと呼ばれるエリアがRAMのメモリ空間に存在しています。ここにはMSX-BASICのWIDTHとかやCOLOR、SCREENなどの値が格納されています。また、値を格納することでWIDTHの幅が変わったり、色が変わったりします。ワークエリアはアドレスが決まっていて、F380Hからワークエリアとして使用されます。プログラムでこのワークエリアのアドレス内の値を破壊すると予期しない結果になったりします。とりあえず「F380H以降はめったには触らない!」と理解しておけばそれで大丈夫です。

プログラムの説明

ORG $4000 は「このプログラムは4000H番地からスタートしていますよ」とアセンブラに指示している箇所です。MSXのROMカートリッジの場合、4000Hもしくは8000Hからスタートしなければならない。と規定されており今回の場合は4000Hとしています。ROMをMSXのスロットに挿入して電源を入れるとシステムの初期化が行われたあとに4000H番地にジャンプしてきて、そこから処理が行われるイメージです。

    ; MSX の ROM ヘッダ (16 bytes)
    ; プログラムの先頭位置は0x4010
    defb 'A', 'B', $10, $40, $00, $00, $00, $00
    defb $00, $00, $00, $00, $00, $00, $00, $00

これはMSXのシステムにROMとして解釈させるためのお約束コードみたいなものです。MSXではROMカートリッジにするためのお約束として「ヘッダー情報(16バイト)を埋めること」と決められています。
ヘッダー情報は以下のとおりです。

先頭から2バイトが”AB"であればROMカートリッジを意味します(お約束)
先頭3バイト目からの2バイトはプログラムの開始アドレスを指します。今回の例では4010Hです。ヘッダ情報が16バイトあるので、4000H + 10H = 4010H という意味です。
先頭5バイト目以降はとりあえず00Hで埋めればOKです。

defb というのが出てきましたね。これもまたアセンブリ言語の疑似命令というやつで、一連のバイトの連続を表します。DBと略してもOKです。
アセンブルすると連続されるのがわかります。(後述します)

(書式)
DEFB 'A', 'B', $43, "DE" ; "ABCDE"という文字データの連続を表す。文字1文字はシングルクオートで囲む
DEFB $00, $10, $20 ; 00H, 10H, 20Hという数値データの連続を表す。

ROMヘッダの'A', 'B', の後ろが$10, $40となっています。「あれ?プログラムの開始位置は4010Hだから、$40, $10じゃないの?」って思われるかもしれません。Z80はリトルエンディアン方式でデータがアドレスに保持されます。リトルエンディアン方式はバイトの末尾から順にアドレスに格納します。リトルエンディアンで16bitのデータをアドレスに格納するとアドレスの箱の中を見ると下位8ビットが先にくる。そういうものなんだとだけ理解してください。
LD HL, 4010H と書くとHレジスタには40H、Lレジスタには10Hが格納されますし、上記、DEFBの内容を
LD HL, (4002H)と書いても同様の結果になりますので、アセンブリ言語でのプログラミング上ではあまり意識することはないと思います。

スタックポインタ

LD SP, $F380
という箇所があります。SPというのはスタックポインタと呼ばれるものでサブルーチンを呼び出して戻ってくるときの戻り先アドレスを保持しています。SPの使い方を誤ると暴走します。私もあまり使いません。しばらくはそっとしておきましょう・・。

初期化(ほとんどお約束)

以下の部分はほとんどお約束です。

    ; 画面構成の初期化
    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

特筆すべきはスプライトモードを16x16にしている箇所です。RG0SAVにはVDPレジスタ#0のアドレスが格納されています。RG0SAV+1は1バイト足していいるのでVDPレジスタ#1のアドレスを意味することになります。
MSX(というかTMS9918)の仕様でスプライトモードを16x16にするためにはVDPレジスタ#1の第1ビット目を1にする必要があります。下図中の「Bit 1」の部分です。

VDP Register #1

LD A, (RG0SAV+1) 
でVDPレジスタ#1の指し示すアドレスの中の値をAレジスタに格納し
OR 2
でAレジスタに2をOR演算した結果をAレジスタに再び格納しています。

OR 2 としているのは 2(10進の2は2進では、0010B)でORをとって1ビット目だけを1にしているのです。そうしないと他のビットの値を壊してしまうのです。OR、AND、NOTについては別の記事で説明します。

こういう指定方法もあるんだなーとだけ今は思ってもらえれば良いです。

それと2進数の各ビットを表現するときは、00000000Bの右端のビットから左に向けて、第0ビット(Bit 0)、第1ビット(Bit 1)、第2ビット(Bit 2).. 第7ビット(Bit 7)といった形式で表現します。マシン語では原則、なにもかもがゼロ始まりで表現されます。

サブルーチンコール(特定のアドレスにジャンプして戻ってくる)

コード中にCALLというのがありますが、これはサブルーチンコールと思ってください。特定のアドレスにジャンプして、ジャンプした先でRET命令が行われると処理が戻ってきます。さきほどそっとしておいたスタックポインタと密接に関連があります。

CHGMOD:equ $005F ; SCREENモードを変更する

(中略)

call CHGMOD

CALL CHGMOD
の箇所では、CHGMODという名前付けされたアドレスに対してジャンプしRETで戻ってきます。CHGMODは$005Fで定義されているので、BIOSの005FHを呼び出して戻ってくるまで待ちます。
アセンブリ言語では凝ったことはできませんのでサブルーチンとしていかに共通的な処理をまとめるか、そのセンスがかなり必要になります。

VRAMの操作(VRAM=画面にデータを表示する)

この箇所も単にBIOSのサブルーチンをCALLで呼び出しているだけにすぎません。

    ; VRAMを使って画面に文字を表示する
    ; HLレジスタにメモリ(RAM)のアドレス
    ; DEレジスタにVRAMの先頭アドレス
    ; BCレジスタに転送サイズ(バイト)
    ; VRAMにメモリのデータを転送するにはBIOSのLDIRVMを使う

    ld hl, MESSAGE
    ld de, $1800    ; 数値の先頭に$をつけると16進として解釈する
    ld bc, 26       ; メッセージは26バイト
    call LDIRVM
 
    (中略)
 
    MESSAGE:
        defm "Let's make an arcade game!"

BIOSを呼び出す場合には、どんなレジスタにどんな値を入れてから呼び出す。とか呼び出して戻ってきたときにはどんなレジスタにどんな値が入ってる。といった仕様がそれぞれあります。LDIRVMではコメントにあるようなレジスタの使いかたをします。
defm というのはこれまた疑似命令です。"文字列ですよ"という意味です。DEFBの場合はカンマで連続できますが、DEFMは1個だけです。

LD HL, MESSAGE
で、MESSAGE のラベルの付いたアドレスの文字列を
LD DE, $1800H
で、VRAMの1800Hのアドレスに
LD BC, 26
で、26バイト分転送する。

という意味になります。

ジャンプ(直接アドレッシング、間接アドレッシング)

以下は無限ループのコードです。

End:
    jr End

ジャンプ命令にもいろいろありますが、それはまた別の記事で説明するとして直接アドレッシング指定でのジャンプ(JP命令)と、間接アドレッシング指定でのジャンプ(JR命令)の2種類があります。直接アドレッシング指定だと、マシン語としては必ず3バイトは必要になります。JP命令で1バイト、アドレスで2バイト必要だということです。JR命令は2バイトで済みます。たかが1バイト違い。。。と思うかもしれませんがJPやJRは頻繁に使います。チリも積もればでプログラムの領域をどんどん費やしてしまうことになるため、容量を考えるとJRのほうが良い場合が出てきます。
MSXはプログラムに使えるメモリ容量がかなり少なく、限りがありますからね。
JPとJRの違いはジャンプできる距離(?)です。JPは直接アドレスを指定してジャンプするのでどこにでもジャンプできますがJRはその命令を実行したアドレスの-126バイト前から127バイト先のアドレスまでしかジャンプできません。そういう違いがあるんだ。ということとメモリ容量はだいじだよー。ということを理解してもらえれば今のところはそれでかまいません。

今回はここまでです。次回ももう少し自分でサブルーチンを作ってみるなどしてアセンブリ言語の勉強をする予定です。アセンブリ言語の説明ばかりだとつまらないので、次次回あたりからVRAMまわりの説明に戻れるといいなと考えています。

では、また!

マシン語講座:マシン語はかく語りき(疑似命令)

上記説明でのDEFB、DEFMは次のようにマシン語にアセンブルされます。

% hexdump -C sample001.bin
00000000  41 42 10 40 00 00 00 00  00 00 00 00 00 00 00 00  |AB.@............|
00000010  31 80 f3 3e 0f 32 e9 f3  3e 01 32 ea f3 32 eb f3  |1.?>.2??>.2??2??|
00000020  3a e0 f3 f6 02 32 e0 f3  3e 01 cd 5f 00 3e 20 32  |:???.2??>.?_.> 2|
00000030  af f3 cd cc 00 3e 00 32  db f3 21 48 40 11 00 18  |????.>.2??!H@...|
00000040  01 1a 00 cd 5c 00 18 fe  4c 65 74 27 73 20 6d 61  |...?\..?Let's ma|
00000050  6b 65 20 61 6e 20 61 72  63 61 64 65 20 67 61 6d  |ke an arcade gam|
00000060  65 21                                             |e!|
00000062

一番上の行の41 42 … 00 00 00 の16バイトの箇所がDEFBで指定した箇所のアセンブル結果。
下から4行目 4C 65 74 … 以降がDEFMで指定した箇所のアセンブル結果になっています。
連続していますね。41Hが’A'で、4CHが’L’です。ASCIIコード表をいちおう載せておきます。

ASCIIコード表

あわせて、LDIRVMは005CHのラベル名でしたが、下から4行目 CD 5C 00 というように LDIRVM の 005CH が数値としてアセンブルされていることが確認できます。リトルエンディアンなので5Cと00がひっくり返っていることをお忘れなく。なお、CD は CALL nn のアセンブル結果です。

LD A, n 
とか
CALL nn
など
命令セットに nn や n といった文字が出てきますがnn はアドレス(16bit)をさします。n は1バイト(8bit)の数値をさします。豆知識として。

Z80のアセンブリ言語としての疑似命令は他にもいくつもあるんですが、いまのところこれだけ覚えておけばまあなんとかなる(?)と思っています。

いいなと思ったら応援しよう!

MSXのZ80で何か作る
セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。