MSXの開発環境を準備しよう
前回はいきなりVDPとVRAMの説明を行いましたが、多少はMSXの画面仕様について理解いただけたでしょうか?え?わからない?でも、それで良いんです。前提知識を頭に入れておくとプログラミング時に反復してイメージできるようになっていきます。ご安心ください。
さて、今回はMSXの開発環境の準備について説明します。少々長くなりますがおつきあいください。
さて、いちばん最初の記事でさらっと紹介していますが、MSXのマシン語プログラミングでは以下を必要とします。
Z88DK(Z80系マシンのCコンパイラとアセンブラ)
マシン語のプログラミングは以下のようなコードになります。代表例としてAレジスタに数値の10を代入(マシン語では転送と呼びます)するコードです。このように人間でもわかるようなコードをアセンブリ言語と呼びます。
アセンブリ言語でコードを書く→アセンブラでマシン語にする→動かす
という流れです。
; セミコロンから書き始めるとコメント欄となる
Main: ; ラベルをつけれる。ラベルはループやジャンプ、サブルーチン名として使用する
LD A,10 ; Aレジスタに数値の10を転送
Loop:
JR Loop ; ラベルLoopにジャンプするラベルを指定するときにはコロンは付けない
LD…という箇所が命令(オペコード)です。LD A, n という命令があります。もちろん命令は他にもたくさんあります。このコードをアセンブル(マシン語に変換)すると次のような数値の連続になります。数値は全て16進表記としています。
3E 0A 18 FE
最初の3EHは LD A, n をマシン語にした結果です。LDが命令ではなく"LD A, n "が命令と思ってください。n の部分は10なので3EHの次に0AHと変換されています。次の18HはコードのJRの部分です。コードとしてはラベルのLoopに戻っているので無限ループのコードです。18Hの次にFEHをつけると無限ループになります。
「はあ〜ん?」という声が聞こえてきそうですが、CPUは数値のON/OFFでしか物事を判別できないため、アセンブリ言語を数値に変換する必要があるのです。なお、命令セットは他にもたくさんあります。(山本様のサイトと渕田様のサイトをご紹介させていただきます)
命令セットがこんなに多岐に渡っていたらオペコードの数値を覚えるのはそれは大変な労力になります。また、ものによっては126バイト前のアドレスにしかジャンプ出来ないといった制約のある命令もあるため、いろんなことを考慮してプログラミングしなければいけません。その労力を減らすためにアセンブラが必要となるわけです。ちなみに私の少年時代は命令とそれに対応する数値をにらめっこしながら手作業でアセンブルしていました。そのしんどさとVRAMが理解できないこととがあいまって勉強を諦めてしまいました・・。
昔話は横において、それではさっそくZ88DKをダウンロードしてアセンブラを手に入れましょう!
Z88DKのインストール
以下のサイトから最新のZ88DKをダウンロードします。
Windowsの場合:
macOSの場合:
ダウンロードしたらZIPファイルを展開して適当なディレクトリに配置しましょう。WindowsであればC:¥z88dk、macOSなら ~/z88dk などといったディレクトリを作ってそこに展開後のファイル群をポイと置くだけで良いです。
ファイル群を置いたら環境変数を設定しましょう。
WindowsであればPATH環境変数に、C:¥z88dk¥bin を追加。
macOSであれば.zshrcに export PATH=${PATH}:${HOME}/z88dk/bin を追加。
といった感じです。筆者はmacOSですがインストール後はこんな感じになっています。より詳しくはz88dkのInstallationのページを参照してください(英語)
z88dk % ls
LICENSE changelog.txt libsrc testsuite
Makefile doc set_environment.sh vsbuild.cmd
README.md examples snap win32
azure-pipelines.yml ext src z88dk.Dockerfile
bin include support z88dk_prompt.bat
build.sh lib test
プログラムを書いてアセンブルする
といっても・・まだ全然説明していないのでいったんは以下のコードをコピペしてsample001.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秒ごとにインクリメントされる値が格納されているアドレス
;--------------------------------------------
; 初期処理(お約束コード)
;--------------------------------------------
; プログラムの開始位置アドレスは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!"
ファイルが保存できたら以下のコマンドでアセンブルします。sample001.asmというファイルをアセンブルするよー。という意味です。
アセンブルに成功するとsample001.binというバイナリファイルが作成されます。
z80asm -b sample001.asm
筆者の環境はmacOSなのですが、macOSにはhexdumpというコマンドが標準でついているのでsample001.binがどうなってるか確認できます。
% 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
正常にアセンブルできてるっぽいですね!
アセンブルに失敗するとエラーが出るのでそのエラーの内容を確認してはコードを修正してアセンブルを繰り返す作業になります。
Windowsの場合はcertutilコマンドで内容を確認できるかと思います。
(Windowsの場合)
> certutil -encodehex sample001.bin sample001.hex & type sample001.hex
WebMSXで動作を確認する
それでは上記の説明で作成したsample001.binを動かしてみましょう!
当noteではWebMSXを使って説明します。
以下のURLにブラウザでアクセスしてください。
次に画面下の「Cartridge 1」→「Load ROM Image」を選択します。
そうするとファイルを選択するダイアログが表示されるので先ほど作成したsample001.binを選択して「開く」を押してください。
当noteでは常にROMイメージとしてファイルを作成します。MSXカートリッジを作ってる気分でなんだかちょっとわくわくしますね。
動きましたか?
以下のような画面が表示されたら正常に動作しています。
なお、今回のソースコードは画面モードをSCREEN1にして、前景色・背景色を設定するなど初期処理をしたうえで、この文章を表示して無限ループしているだけです。ソースコード上にVRAMという文字があることを意識してください。VRAMのアドレスに文章のデータを転送することで画面に文章が表示されている。ということです。これ重要。
以上、かけあしでしたがMSXの開発環境はこれで準備完了です。
アセンブリ言語でプログラムを書いてアセンブラでマシン語にアセンブルしてMSXのROMイメージを作成し、WebMSXで動かす。ということができるようになったはずです。
sample001.asmはGitHubでも公開しているのでコピペするなりなんなりどうぞ。
https://github.com/sailorman-msx/games/blob/main/src/sample001.asm
次回はどちらかというとマシン語よりな説明になると思いますが、今回のコードを題材に何をしているかについて説明します。
では、また!
マシン語講座:マシン語はかく語りき(レジスタとアドレス)
マシン語では LD A, n がAレジスタへの値の転送であると冒頭で説明しました。レジスタはA、B、C、D、E、H、Lという8ビットの値を転送可能なレジスタとBC、DE、HL、IX、IY、SPという16ビットの値を転送可能なレジスタがあります。それ以外に転送は出来ないけれど「演算結果の状態」を持つFレジスタ(フラグレジスタ)というものもあります。その中でもAレジスタはアキュムレータと呼ばれるものでけっこうなんでもできるレジスタです。
HLレジスタもけっこうなんでも出来るのですがアキュムレータとの違いは私にはわかりません・・。これまた、さらに付け加えると私もまったく用途の判らないRレジスタ(リフレッシュレジスタ)というものもあります。
Rレジスタ以外はプログラミングにおいて頻繁に登場します。
FはRead Onlyなレジスタですが条件分岐とかで使います。このレジスタの役割はとても重要。使いかたは後の記事で説明します。
8ビットは00HからFFHまでの数字なのでそれを超える数値を8ビットレジスタに転送することは出来ません。
16ビットレジスタにはFFFFHまでの値であれば転送することが出来ます。
どんなときに8ビットと16ビットを使い分けるのか?という点ですが、アドレスを指定するときに16ビットレジスタを使います。16ビットレジスタは0000HからFFFFHまで使えます。RAMのデータはアドレス(番地)と呼ばれる場所に1バイトずつ入っています。RAMのアドレスは0000HからFFFFHまでです。箱がずらーっと並んでいてそれが0000HからFFFFHまでの65536個並んでる、とイメージしてください。そして、たとえばRAMの1800Hに値をセットする場合には次のように書きます。(1800H番地に値を転送するとも言います)
LD HL, 1800H ; 1800Hという16ビットの値をHLレジスタにセット
LD (HL), 10 ; HLレジスタが指し示すアドレスの中に10という数値をセット
ただし、次のようには書けません。
a. LD A, 1800H ; Aレジスタには8ビットの値しかセットできないからNG
b. LD (1800H), 10 ; アドレスの中に値を転送するときにはレジスタを経由しないといけないのでNG
c. LD 1800H, 10 ; アドレスを書き換えることはできないのでNG
(nn)は「nnが指し示すアドレスの中の値」です。
単なる nn はアドレスそのものになります。
上記cはアドレスを書き換えようとしているようなコードです。こういうことは出来ません。
レジスタの値は書き換えできる。
アドレスは書き換え出来ない。
アドレスの中の値(箱の中)は書き換え出来る。
ただし、アドレスの中の値はレジスタ経由でのみ書き換え出来る。
だいたい、そんなイメージを持ってもらえればOKです。
おおよそ暴走するのはアドレス操作まわりが原因となることがほとんどです。これ、わかってるつもりでも慣れるまでそこそこ時間がかかりますのでご注意ください。