見出し画像

Z80アセンブリ言語の学習その3

ソースコードのモジュール化

前回の記事以降はかなりソースコードが長くなっていきます。前回記事の最後ではGRAPHIC2について今回説明しようかなと思っていましたが、少し説明対象を変えてソースコードをモジュール化してみました。
GitHub上にある、sample004.asm、initialize.asm、pcg_graphic1.asm、common.asm、data.asmをダウンロードしてください。

sample004.asm
https://github.com/sailorman-msx/games/blob/main/src/sample004.asm

initialize.asm
https://github.com/sailorman-msx/games/blob/main/src/initialize.asm

pcg_graphic1.asm
https://github.com/sailorman-msx/games/blob/main/src/pcg_graphic1.asm

common.asm
https://github.com/sailorman-msx/games/blob/main/src/common.asm

data.asm
https://github.com/sailorman-msx/games/blob/main/src/data.asm

INCLUDE 疑似命令

ダウンロードしたsample004.asmを見ると以下のようになっているのがわかると思います。

;--------------------------------------------
; INCLUDE
;--------------------------------------------
include "initialize.asm"

;--------------------------------------------
; メイン処理
;--------------------------------------------
Main:

    ;--------------------------------------------
    ; キャラクタパターンとカラーテーブルを
    ; 作成する
    ;--------------------------------------------
    call CreateCharacterPattern

    ;--------------------------------------------
    ; 文字コード80Hの文字を256バイトぶん
    ; VRAMのパターンネームテーブルに埋める
    ; (横32文字 x 縦8行=256バイト)
    ;--------------------------------------------
    ld hl, $1800
    ld  a, $80
    call PutVRAM256Bytes

    ;--------------------------------------------
    ; 文字コード88Hの文字を256バイトぶん
    ; VRAMのパターンネームテーブルに埋める
    ; (横32文字 x 縦8行=256バイト)
    ;--------------------------------------------
    ld hl, $1900
    ld  a, $88
    call PutVRAM256Bytes

    ;
    ; 16バイト分のデータを画面にDUMP出力する
    ;
    ld hl, CHRPTN       ; CHRPTNのアドレスの内容を
    ld de, WK_DUMPDATA  ; WK_DUMPDATAのアドレスに
    ld bc, 16           ; 16バイト転送する
    ldir

    call PrintHexaDump

    ;
    ; WK_DUMPCHARのアドレスの内容を
    ; 画面上の下段(1A00H)に32バイト転送する
    ;
    ld de, $1A00
    call HexaDumpToVRAM

MainEnd:
    jr MainEnd

;-----------------------------------------------
; INCLUDE
;-----------------------------------------------
include "pcg_graphic1.asm"
include "common.asm"
include "data.asm"

今まで書いてあったBIOSルーチンの定義や固定データの部分などがすべて include "XXXXX.asm" というコードに置き換わっていることを確認できます。このincludeは””で括られたファイルを、通常のソースファイルとしてアセンブリ時にその部分に記述されているものと認識され、アセンブルされることになります。inlude "initialize.asm" と書かれている行がまるっと、initialize.asmのソースコードに置換されたうえでアセンブルされるということです。
こうすることでソースコードはすっきりと読みやすくなりますし、分類ごとにソースファイルを分離できるので修正もやりやすくなります。
前回使ったsample003.asmのソースコード行数は284行。
モジュール化した結果、今回のsample004.asmのソースコード行数は55行です。スッキリ〜!!

このように、目的に応じてサブルーチンをわけたりソースコードをわけたりすることをモジュール化と呼びます。今回のモジュール化ではソースコードを以下のように分けました。

initialize.asm
  初期処理用のソースコード。変数を追加・変更・削除する場合はこのソースコードに記述する。
 
pcg_graphic1.asm
  GRAPHIC1モード用のキャラクタグラフィックを定義するためのソースコード。
  作成するキャラクタが増える場合は、data.asmにも記述が必要。
  ※PCGProgrammable Character Generator
      キャラクタのパターンをROMではなくRAMに保存して書き換え可能にした機能のこと
 
common.asm
  共通処理を記述するためのソースコード。掛け算などはこちらに記述する。
 
data.asm
  固定データを記述するためのソースコード。表示メッセージやPCG用のデータ、スプライト用の
  データ、その他もろもろの固定的なデータはここに記述する。
 
sample004.bas
  プログラムのメイン処理。上記4つの外部ソースコードをINCLUDEしている。

ソースコードがすっきりするということは誤ってコードを書き換えるといったリスクも減ります。読みやすい・誤修正のリスクが少ない、といいことづくめです。モジュール化はプログラミングのセンスがすべてです。自分にとって読みやすいコードであるか、他者にとって読みやすいコードであるか、拡張性は高いか、似たような処理のサブルーチンが重複していないか、目的にそってモジュール分割できているか、モジュール分割しすぎていないか、などなど、筆者はあまりセンスがないほうですが、今後はこのような形でモジュール化してからソースコードの説明を行おうと思います。

メモリダンプ出力のサブルーチンを追加しました

マシン語プログラミングでは常にメモリとレジスタの状態を意識する必要があります。今後の記事ではさらに意識する機会が増えることが予想されているためメモリダンプ出力用のサブルーチンを追加しました。
メモリの内容も出力できますし、レジスタの値を変数にセットして呼び出すことでレジスタの値も出力できます。
common.asmにPrintHexaDumpというサブルーチンを追加しています。
関連してinitilize.asmにWK_DUMPDATAとWK_DUMPCHARという変数を追加しています。WK_DUMPDATAが指し示すアドレスの中に最大16バイト分のデータを詰めてPrintHexaDumpを呼び出すと、WK_DUMPCHAR変数にWK_DUMPDATAアドレス以降の値が16進表記で格納されるサブルーチンです(16バイトなので32文字出力ぶんされる)
PrintHexaDumpを呼び出したあとで、DEレジスタに表示したいVRAMのアドレス(前記事で説明した画面表示と1対1になっているアドレス)をセットしてHexaDumpToVRAMを呼び出すとWK_DUMPCHARの情報が指定した場所に画面表示されます。

initialize.asm からの抜粋>
 
;--------------------------------------------
; 変数領域
;--------------------------------------------
; DEBUG PRINT用
WK_DUMPDATA:equ $C000    ; 16バイト
WK_DUMPCHAR:equ $C010    ; 32バイト

; キャラクタデータ作成用
WK_CHARDATAADR:equ $D000 ; 2バイト
WK_CHARCODE:equ $D002    ; 1バイト



common.asm からの抜粋>
 
;--------------------------------------------
; SUB-ROUTINE: PrintHexaDump
; 16進数文字列変換サブルーチン
; WK_DUMPDATAのアドレスの内容を32バイトぶん
; 16進表記する(32 x 2=64文字使用する)
;--------------------------------------------
PrintHexaDump:

    push hl
    push de
    push bc
    push af

    ; 16進表記文字の格納エリアを
    ; 文字コード20H(スペース)で埋める

    ld ix, WK_DUMPCHAR
    ld b, 32
    
PrintHexaDumpLoop1:

    ld (ix + 0), $20
    inc ix
    
    djnz PrintHexaDumpLoop1

PrintHexaDumpLoopEnd:

    ;
    ; WK_DUMPDATAのアドレスの値を16進表記にして
    ; WK_DUMPCHARのアドレスに格納していく
    ; (16バイト分)
    ;

    ld ix, WK_DUMPDATA
    ld iy, WK_DUMPCHAR
    ld b, 16

PrintHexaDumpLoop2:

    ;
    ; 最初の2バイトを取得し、DEレジスタに格納する
    ;
    ld d, (ix + 0)
    ld e, (ix + 1)

    ; Dジスタの値をAレジスタに転送
    ld a, d

    call Hex1Byte

    ; Eレジスタの値をAレジスタに転送
    ld a, e

    call Hex1Byte

    inc ix ; IXレジスタを2バイトぶん進める
    inc ix

    djnz PrintHexaDumpLoop2

    pop af
    pop bc
    pop de
    pop hl

    ret

;
; 1バイトの数値を文字列に変換する
;
Hex1Byte:

    ld c, a ; Aレジスタの値をCレジスタに退避

    ; Aレジスタを右シフトして上位4ビットの内容を
    ; 下位4ビットの位置にずらす
    srl a
    srl a
    srl a
    srl a

    call PutOneChar ; 上位4ビットの情報を16進表記する

    ld a, c ; 退避した内容をAレジスタに戻す
    and $0F  ; Aレジスタの値を下の桁のみにする

    call PutOneChar

    ret

;
; 4ビットの数値を文字に変換して
; IYレジスタ(WK_DUMPCHARのインデックス)のアドレスに
; 格納する
;
PutOneChar:

    cp 10 ; Aレジスタから10を引いた結果のフラグレジスタを取得する

    ; CYフラグがたたない場合
    ; つまり、Aレジスタの値が10以上の場合は
    ; PutHexCharを呼び出し16進で表示する
    jr nc, PutHexChar
    
    add a, $30
    ld (iy), a

    inc iy

    ret

PutHexChar:

    add a, $37 ; 数値に文字コード37Hを足してAからFまでの文字列にする
    ld (iy), a

    inc iy

    ret

;--------------------------------------------
; SUB-ROUTINE: HexaDumpToVRAM
; WK_DUMPCHARのデータをVRAMの指定したアドレス
; に出力する
; WK_DUMPCHARのデータ長さは16バイトとする
;   DEレジスタ:VRAMの先頭アドレス
;               先頭アドレスは横0の位置にした
;               ほうがみやすい
;--------------------------------------------
HexaDumpToVRAM:

    ld hl, WK_DUMPCHAR
    ld bc, 32          ; 32バイト分指定したアドレスに転送
    call LDIRVM

    ret

使用例はsample004.asmに記載のとおりです。

sample004.asm抜粋
 
    ;
    ; 16バイト分のデータを画面にDUMP出力する
    ;
    ld hl, CHRPTN       ; CHRPTNのアドレスの内容を
    ld de, WK_DUMPDATA  ; WK_DUMPDATAのアドレスに
    ld bc, 16           ; 16バイト転送する
    ldir

    call PrintHexaDump

    ;
    ; WK_DUMPCHARのアドレスの内容を
    ; 画面上の下段(1A00H)に32バイト転送する
    ;
    ld de, $1A00
    call HexaDumpToVRAM

実際にはPrintHexaDumpを呼び出しただけでは画面には表示されないのでちょっと変なコメントですね。最初は画面に一気に表示しようかなと思ったのですが変数に詰める、画面に表示する。という機能は別々にしたほうが汎用性が高そう(のちのち、使い勝手が良さそう)だったので分けました。
今回のsample004.asmをアセンブルするときもいままで同様に
z80asm -b sample004.asm
だけでOKです。INCLUDE対象となるファイルを違うディレクトリには置かないでください。場所を変えるとアセンブルに失敗します。
アセンブルで出来上がったsample004.binを実行すると次のような画面になります。

実行結果

80FCFCFC00CFCF… と表示されている部分がメモリダンプの出力文字列になります。
今回のソースではdata.asmのCHRPTNのアドレスの先頭から16バイトの文字列を16進表記で画面に出力しています。ソースコードを確認してもらえれば理解が早いかと。

追記:z88dk-z80nm が便利(中級者以上向け?)

macOSの場合、sample004.binをhexdumpコマンドで出力するとアセンブル結果のダンプが出力されます(Windowsの場合は、certutils -encode-hex)
ですが、ダンプが出力されても例えば今回のソースのHexaDumpToRAMサブルーチンの先頭が一体どこなのかさっぱりわかりません。そこで便利なのがz88dk-z80nmコマンドです(Object and Library File Dumperと呼ばれています)
アセンブルすると必ず「.o」ファイルと「.bin」ファイルが出来ますよね?
いままでは「.bin」ファイルだけ対象にしていましたが、z88dk-z80nmはこの「.o」ファイルを入力元にしてどのサブルーチン名(シンボルともいう)がどのアドレスなのかを表示してくれます。
「.o」ファイルはObjectファイルといって「.asm」と「.bin」のあいのこみたいなファイルです。以下のようにコマンドを実行できます。

88dk-z80nm -a Objectファイル名

以下は、sample004.oをz88dk-z80nmコマンドで出力させた結果です。

% z88dk-z80nm -a sample004.o
Object  file sample004.o at $0000: Z80RMF16
  Name: sample004
  Section "": 322 bytes, ORG $4000
    C $0000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00
    C $0010: 31 80 F3 3E 0F 32 E9 F3 3E 01 32 EA F3 32 EB F3
    C $0020: 3A E0 F3 F6 02 32 E0 F3 3E 01 CD 5F 00 3E 20 32
    C $0030: AF F3 CD CC 00 3E 00 32 DB F3 CD 00 00 21 00 18
    C $0040: 3E 80 CD 00 00 21 00 19 3E 88 CD 00 00 21 00 00
    C $0050: 11 00 C0 01 10 00 ED B0 CD 00 00 11 00 1A CD 00
    C $0060: 00 18 FE 21 00 00 22 00 D0 2A 00 D0 7E 32 02 D0
    C $0070: FE 01 38 26 23 22 00 D0 3A 02 D0 67 1E 08 CD 00
    C $0080: 00 54 5D 2A 00 D0 01 08 00 CD 5C 00 2A 00 D0 D5
    C $0090: 11 08 00 19 D1 22 00 D0 18 CF 21 10 20 3E 81 CD
    C $00A0: 4D 00 21 11 20 3E 51 CD 4D 00 C9 F5 C5 06 00 CD
    C $00B0: 4D 00 23 10 FA C1 F1 C9 C5 D5 16 00 2E 00 06 08
    C $00C0: 29 30 01 19 10 FA D1 C1 C9 E5 D5 C5 F5 DD 21 10
    C $00D0: C0 06 20 DD 36 00 20 DD 23 10 F8 DD 21 00 C0 FD
    C $00E0: 21 10 C0 06 10 DD 56 00 DD 5E 01 7A CD 00 00 7B
    C $00F0: CD 00 00 DD 23 DD 23 10 EC F1 C1 D1 E1 C9 4F CB
    C $0100: 3F CB 3F CB 3F CB 3F CD 00 00 79 E6 0F CD 00 00
    C $0110: C9 FE 0A 30 08 C6 30 FD 77 00 FD 23 C9 C6 37 FD
    C $0120: 77 00 FD 23 C9 21 10 C0 01 20 00 CD 5C 00 C9 80
    C $0130: FC FC FC 00 CF CF CF 00 88 38 38 38 7C BA 38 6C
    C $0140: 00 00
  Symbols:
    L C $004D WRTVRM (section "") (file initialize.asm:8)
    L C $005C LDIRVM (section "") (file initialize.asm:13)
    L C $005F CHGMOD (section "") (file initialize.asm:14)
    L C $00CC ERAFNK (section "") (file initialize.asm:16)
    L C $F3AF LINWID (section "") (file initialize.asm:23)
    L C $F3DF RG0SAV (section "") (file initialize.asm:24)
    L C $F3E9 FORCLR (section "") (file initialize.asm:25)
    L C $F3EA BAKCLR (section "") (file initialize.asm:26)
    L C $F3EB BDRCLR (section "") (file initialize.asm:27)
    L C $F3DB CLIKSW (section "") (file initialize.asm:28)
    L C $C000 WK_DUMPDATA (section "") (file initialize.asm:35)
    L C $C010 WK_DUMPCHAR (section "") (file initialize.asm:36)
    L C $D000 WK_CHARDATAADR (section "") (file initialize.asm:39)
    L C $D002 WK_CHARCODE (section "") (file initialize.asm:40)
    L A $0000 Header (section "") (file initialize.asm:48)
    L A $0010 Start (section "") (file initialize.asm:55)
    L A $003A Main (section "") (file sample004.asm:9)
    L A $0063 CreateCharacterPattern (section "") (file pcg_graphic1.asm:13)
    L A $00AB PutVRAM256Bytes (section "") (file common.asm:11)
    L A $012F CHRPTN (section "") (file data.asm:6)
    L A $00C9 PrintHexaDump (section "") (file common.asm:93)
    L A $0125 HexaDumpToVRAM (section "") (file common.asm:217)
    L A $0061 MainEnd (section "") (file sample004.asm:52)
    L A $0069 CreateCharacterPatternLoop (section "") (file pcg_graphic1.asm:20)
    L A $009A CreateCharacterPatternEnd (section "") (file pcg_graphic1.asm:73)
    L A $00B8 CalcMulti (section "") (file common.asm:37)
    L A $00AF PutVRAM256BytesLoop (section "") (file common.asm:18)
    L A $00C0 CalcMulti1 (section "") (file common.asm:50)
    L A $00C4 CalcMulti2 (section "") (file common.asm:74)
    L A $00D3 PrintHexaDumpLoop1 (section "") (file common.asm:106)
    L A $00DB PrintHexaDumpLoopEnd (section "") (file common.asm:113)
    L A $00E5 PrintHexaDumpLoop2 (section "") (file common.asm:125)
    L A $00FE Hex1Byte (section "") (file common.asm:158)
    L A $0111 PutOneChar (section "") (file common.asm:183)
    L A $011D PutHexChar (section "") (file common.asm:199)
    L A $0141 CHRPTN_END (section "") (file data.asm:13)
  Expressions:
    E Cw $003A $003B: CreateCharacterPattern (section "") (file sample004.asm:15)
    E Cw $0042 $0043: PutVRAM256Bytes (section "") (file sample004.asm:24)
    E Cw $004A $004B: PutVRAM256Bytes (section "") (file sample004.asm:33)
    E Cw $004D $004E: CHRPTN (section "") (file sample004.asm:38)
    E Cw $0058 $0059: PrintHexaDump (section "") (file sample004.asm:43)
    E Cw $005E $005F: HexaDumpToVRAM (section "") (file sample004.asm:50)
    E Cw $0063 $0064: CHRPTN (section "") (file pcg_graphic1.asm:17)
    E Cw $007E $007F: CalcMulti (section "") (file pcg_graphic1.asm:55)
    E Cw $00EC $00ED: Hex1Byte (section "") (file common.asm:136)
    E Cw $00F0 $00F1: Hex1Byte (section "") (file common.asm:141)
    E Cw $0107 $0108: PutOneChar (section "") (file common.asm:169)
    E Cw $010D $010E: PutOneChar (section "") (file common.asm:174)

「うわぁ・・わけわかんねえ・・」と思うかもしれませんね。
Symbols:と書かれている場所がポイントです。
HexaDumpToVRAMは

L A $0125 HexaDumpToVRAM (section "") (file common.asm:217)

と書かれていることがわかります。

Section "": 322 bytes, ORG $4000

と書かれている箇所の下がアセンブルした結果です。(sample004.binと同じ)
つまり、Section:配下の0125H番地がsample004.binでのHexaDumpToVRAMの先頭アドレスである。という意味になっています。太字箇所がHexDumpToVRAMサブルーチンの場所です(RET命令はマシン語ではC9)

C $0120: 77 00 FD 23 C9 21 10 C0 01 20 00 CD 5C 00 C9 80

この情報をもとにして、プログラムの実行中にダンプ出力したいアドレスを特定することが出来ます。
「うーん・・」という声が聞こえてきそうですが
こればっかりは「慣れ」です。
マシン語プログラミングは慣れるしかありません。。。

インデックスレジスタ(IX、IY)

今回のソースコードのcommon.asmではIXレジスタやIYレジスタが初めて登場しています。これらのレジスタはインデックスレジスタと呼ばれるもので、ざっくり言うと「アドレスを指定するためだけの16ビット専用レジスタ」です。
HLレジスタや、DEレジスタは16ビットレジスタですが、HとL、DとEといった感じで8ビットずつに分離して使うことが出来ますが、インデックスレジスタは分離できません。なぜならば「アドレス指定の専門レジスタだから」です。Z80で扱うアドレスは16ビットですから!以下に簡単に説明します。

PrintHexaDumpLoopEnd:

    ;
    ; WK_DUMPDATAのアドレスの値を16進表記にして
    ; WK_DUMPCHARのアドレスに格納していく
    ; (16バイト分)
    ;

    ld ix, WK_DUMPDATA
    ld iy, WK_DUMPCHAR
    ld b, 16

PrintHexaDumpLoop2:

    ;
    ; 最初の2バイトを取得し、DEレジスタに格納する
    ;
    ld d, (ix + 0)
    ld e, (ix + 1)

LD IX, WK_DUMPDATA
でIXレジスタに変数WK_DUMPDATAのアドレスをセットしています。
コードのうしろのほうの
LD D, (IX + 0)
でIXレジスタがさししめすアドレスの値をDレジスタに格納しています。
LD E, (IX + 1)
でIXレジスタがさししめすアドレス+1のアドレスの値をEレジスタに格納しています。
これで、IXレジスタがさすアドレスの16ビットのデータがDEレジスタに格納されたことと同じ意味になります。
ポイントは、(IX + n) の部分です。
インデックスレジスタはディスプレースメント指定といって範囲を指定することができます。-128(128バイト前)から127(127バイト先)までをnの値にすることができるのです。JRジャンプ命令の間接アドレッシング指定と似ていますね。
インデックスレジスタはけっこう便利で頻繁に使うことになります。
IYレジスタもIXレジスタと利用方法はまったく同じです。
今回のPrintHexaDumpではIXレジスタをWK_DUMPDATAのアドレス指定のインデックスとして、IYレジスタをWK_DUMPCHARのアドレス指定のインデックスとしてそれぞれ使っています。

シフト命令(SRL)

16進表記のサブルーチンの中にSRL命令が初めて登場しています。

    ; Aレジスタを右シフトして上位4ビットの内容を
    ; 下位4ビットの位置にずらす
    srl a
    srl a
    srl a
    srl a

SRL命令はレジスタの値を右に1ビットシフトします。シフトした結果はシフト後レジスタに格納されます。
レジスタの値を4回右にシフトさせるともとの値の上位4ビットが下位4ビットの位置に位置づきます。

SRL命令で4回右にシフトした結果

SLAという左にシフトさせる命令と、SRAというシフト後に7ビット目が変わらない右シフト命令もあります。シフト命令は頻繁に使います。ぜひ覚えてください。

今回はここまでです。
・INCLUDE疑似命令でソースコードをすっきりモジュール化できる
・インデックスレジスタ(IX、IY)が便利
・シフト命令というものがある

という学習になりました。

次回こそはGRAPHIC2に突入しますよ!

では、また!

マシン語講座:マシン語はかく語りき(MSXの文字コード)

ASCIIコードは00Hから7FHまでの128文字ですが、MSXの文字コードはさらに80HからFFHまでが存在しています。今回のソース修正ではメモリダンプを16進表示するために前回記事で形状を変えていた’0’と’8’を80Hと88Hに変更しています。変更しないと0がブロックになったりするのでそのように変更しました。MSXでの文字コード(MSX1-Japanの文字コード)についてはテクハンWikiを参照ください。

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

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