トップ 差分 一覧 Farm ソース 検索 ヘルプ RSS ログイン

MZ-700でForthを作る

昔、X1turbo3というパソコンを持っていた

プログラミングゲームをやっていた。プログラミングもしたのだが、長続きせず。それだけが心残り。

最近は、エミュレータという便利なものがある

MZ-700 Emulator MZ700WIN For Windows

これを使えば、今までの心残りも解消されるかも。あれ、X1turbo3は?あれは色んな機能があるので使いきれない。という訳で、MZ-700でForthを作る。MZ-700のマニュアルには、回路図とモニタプログラムのソースコードがある。これさえあれば、プログラムを作れる。

確かマニュアルがあったはずだ、あった

MZ-2000のがorz。ヤフオクで落札したのだ。どうする?MZ-2000でForthを作るに変更するか?と思ったら、互換モニタROMに解説テキストがあった。これがあれば何とかなりそうだ。

文字を表示させよう

互換モニタのサブルーチン一覧を見ると、1文字表示は0012H番地。表示させる文字をどうやって指定するかは、書いてない。たぶん、Aレジスタかスタックだと思うが。仕方ない、逆アセンブルしよう。目玉逆アセンブル。

0012 C3 JP $0935
0013 35
0014 09
  :
0935 FE CP $0D
0936 0D
0937 CA JP Z,$090E
0938 0E
0939 09
093A C5 PUSH BC
093B 4F LD C,A
093C 47 LD B,A
093D CD CALL $0196
093E 96
093F 01
0940 CD CALL $0946
0941 46
0942 09
0943 79 LD A,C
0944 C1 POP BC
0945 C9 RET

目玉逆アセンブルの結果、どうやらAレジスタに表示するコードを入れて呼び出すっぽい。というわけで、テストコードを書いてみた。もちろん目玉アセンブラを用いた。

3E LD A,$41
41
CD CALL $0012
12
00
C9 RET

このコードを何番地に置くか?MZ-700のメモリマップは、、たぶん、Wikipediaに書いてある、1000H番地で問題ないはず。1000H番地から置いてみる。モニタ画面でプログラムを入力し、GOTO$1000、Aが表示された。成功。文字列表示ルーチンも確認してみよう。

0015 C3 JP $0981
0016 81
0017 09
   :
0981 F5 PUSH AF
0982 C5 PUSH BC
0983 D5 PUSH DE
0984 06 LD B,$05
0985 05 
0986 CD CALL $0196
0987 96 
0988 01 
0989 1A LD A,(DE)
098A FE CP $0D
098B 0D 
098C CA JP Z,$0FDF
098D DF 
098E 0F 
098F 4F LD C,A
0990 CD CALL $0946
0991 46
0992 09
0993 13 INC DE
0994 10 DJNZ $F3
0995 F3
0996 C3 JP $0984
0997 84
0998 09
0999 F5 PUSH AF
099A C5 PUSH BC
099B D5 PUSH DE
099C 06 LD B,$05
099D 05
099E CD CALL $0196
099F 96
09A0 01
09A1 1A LD A,(DE)
09A2 FE CP $0D
09A3 0D
09A4 CA JP Z,$0FDF
09A5 DF
09A6 0F
09A7 CD CALL $0BB9
09A8 B9
09A9 0B
09AA CD CALL $0970
09AB 70
09AC 09
09AD 13 INC DE
09AE 10 DJNZ $F1
09AF F1
09B0 C3 JP $099C
09B1 9C
09B2 09
09B3 C5 PUSH BC
09B4 D5 PUSH DE
09B5 E5 PUSH HL
09B6 CD CALL $0FB1
09B7 B1
09B8 0F
09B9 CD CAll $0DA6
09BA A6
09BB 0D
09BC 7E LD A,(HL)
09BD 32 LD ($118E),A
09BE 8E
09BF 11
09C0 22 LD ($118F),HL 
09C1 8F
09C2 11
09C3 21 LD HL,$1192
09C4 92
09C5 11
09C6 CD CALL $01B8
09C7 B8
09C8 01
09C9 32 LD ($E000),A
09CA 00
09CB E0
09CC 32 LD ($1191),A
09CD 91
09CE 11
09CF 2F CPL
09D0 32 LD ($E000),A
09D1 00
09D2 E0
09D3 16 LD D,$14
09D4 14
09D5 CD CALL $09FF
09D6 FF
09D7 09
09D8 CD CALL $0A50
09D9 50
09DA 0A
09DB 78 LD A,B
09DC 07 RLC A
09DD DA JP C,$0BE6
09DE E6
09DF 0B
09E0 15 DEC D
09E1 C2 JP NZ,$09D5
09E2 D5
09E3 09
09E4 CD CALL $09FF
09E5 FF
09E6 09
09E7 CD CALL $08CA
09E8 CA
09E9 08
09EA FE CP $F0
09EB F0
09EC CA JP Z,$077A
09ED 7A
09EE 07
09EF F5 PUSH AF
09F0 CD CALL $0DA6
09F1 A6
09F2 0D
09F3 3A LD A,($118E)
09F4 8E
09F5 11
09F6 2A LD HL,($118F)
09F7 8F
09F8 11
09F9 77 LD (HL),A 
09FA F1 POP AF
09FB E1 POP HL
09FC D1 POP DE
09FD C1 POP BC
09FE C9 RET

疲れた。ここまでやって、0996H番地でループしていることに気がついた。AF、BC、DEレジスタをおそらく作業用レジスタとして使うためにPUSHしているから、HLレジスタが怪しい。それから、1000H番地台は、作業エリアとして使っているっぽい。2000Hからプログラムを配置する。とりあえず、0を終端と仮定してプログラムを作った。

2000 21 LD HL,$2007
2001 07
2002 20
2003 CD CALL $0015
2004 15
2005 00
2006 C9 RET
2007 54
2008 45
2009 53
200A 54
200B 00

うまくいけば、"TEST"と表示するはず。GOTO$2000としたら、表示されたのは、2000。失敗だ。もしかしてBCレジスタに入れるのかも、と思い、

2000 01 LD BC,$2007
2001 07
2002 20

とやってみた。これもだめ。じゃあDEレジスタかも、と思い、

2000 11 LD DE,$2007
2001 07
2002 20

とやってみた。"TEST"と表示された。が、どんどんスクロールしていく。どうやら終端は0ではないらしい。試しに$0D(CR)を入れてみたら、TESTと表示し、モニタに戻った。成功。

2000 11 LD DE,$2007
2001 07
2002 20
2003 CD CALL $0015
2004 15
2005 00
2006 C9 RET
2007 54 (T)
2008 45 (E)
2009 53 (S)
200A 54 (T)
200B 0D (CR)

という訳で、文字列表示ルーチン0015Hは

  • DEレジスタに文字列の先頭アドレスを入れる
  • 文字列の終端は0D(キャリッジリターン)である

ことが分かった。互換モニタには、0018Hにもうひとつ文字列表示のルーチンがあるようなのだが、違いが分からず。とりあえず、1文字表示・文字列表示が出来たので良しとする。

キー入力を得よう

0003Hがキーボードからの1行入力らしい。これには、DEレジスタに入力バッファの番地を指定すると書いてある。親切だ。

早速プログラムを書いてみよう。

2010 11 20 20 LD DE,$2020
2013 CD 03 00 CALL $0003
2016 C9 RET

GOTO$2010で実行し、"ABCDE"を入力したら、モニタのプロンプト"*"が出て終了。2020Hを見てみると、"ABCDE"のキャラクタコードが表示されている。成功。それと、以降41文字目までは0DHで埋められていた。

内部インタプリタを作ろう

Forthの内部インタプリタというのは、C言語風に書くと、

goto word_pointer_entry[program[program_cnt]];

な感じなわけで。word_pointer_entry[]は、各ワードの処理番地が格納された配列、program[]は、Forthで書かれたプログラムの配列、program_cntが、現在処理している位置。びっくりするくらい簡単。

ただし、C言語だとこういうgotoは使えない。のだが、gccは実はこういうことができる機能拡張を行っている。今回はアセンブリ言語なので、そういうことは気にしないでよい。

さて、Z80のマシン語で上記を実現する。gotoは、JPでそのまま置き換えられる。以下のどれが良いだろう?

命令 動作 バイト数 ステート数
JP (HL): HLの示している番地にジャンプ 1 4
JP (IX): IXの示している番地にジャンプ 2 8
JP (IY): IYの示している番地にジャンプ 2 8
JP nn: 直接ジャンプ命令(ただしnnの部分を、実行時に直接書き換える) 3 10

どう考えても、JP (HL)が良いと思う。バイト数も少ないし、ステート数も少ない。

次に、HLに与える番地の計算である。program_cnt、program、word_entry_pointerはある番地を表すとする。最初に、program[program_cnt]を計算する。まず、program_cntをレジスタにロードする。いい忘れたが、プログラムカウンタは2バイトである。

LD HL,PROG_CNT

そして、programは、program[]の先頭番地なので、2つを足し合わせ、その番地の内容を得れば目的を達成できる。

LD  DE,PROG
ADD HL,DE
LD  D, (HL)

こうしてDレジスタに入っている値が、program[program_cnt]である。この値をインデックスとして、各ワードへのポインタ配列にアクセスする。D = program[program_cnt]なので、word_pointer_entry[D]である。

LD  HL,WORD_ENTRY
ADD HL,D

あれ、8ビットと16ビットの足し算て出来ない、、Dの代わりにEレジスタを使って、Dには0を入れてみる。

;program_cntを得る
LD HL,PROG_CNT

;program[HL]を得る
LD  DE,PROG
ADD HL,DE
LD  E, (HL)

;word_pointer_entry[E]を得る
LD  HL,WORD_ENTRY
LD  D,0
ADD HL,DE

;ワード処理番地にジャンプする
JP (HL)

これでいいだろうか?まだダメだった。ワード処理番地の配列は2バイト配列で、Eの値が1バイトである。Eの値は2の倍数でないと正しくアクセスできない。ところで、Z80のニーモニックには、"ADD DE,DE"なんて命令はない。どうするか。

  1. Aレジスタで8ビットの足し算(ADD)を行う
  2. HLレジスタにいれ、ADD HL,HLを実行する

後者を使うことにする。

このようにしてみた。

;word_pointer_entry[E]を得る
LD  H,0
LD  L,E
ADD HL,HL
LD  DE,WORD_ENTRY
ADD HL,DE

それと、ワード処理番地へのジャンプだが、

JP  (HL)

では、計算した配列の番地にジャンプしてしまっている。本当は、処理番地の配列に保存されている内容がジャンプ先である。そのため、次のように変更する。

;word_pointer_entry[E]の内容を得る
LD  D,(HL)
INC HL
LD  E,(HL)
LD  H,D
LD  L,E

;ワード処理番地にジャンプする
JP  (HL)

ところで、LD DE,(HL)や、LD HL,DEという命令はない。昔、Z80の命令は直交性がない、と言われていたが、こうやってプログラミングしていると、なるほどと思う。まともにZ80のアセンブリ言語でプログラミングするのは、これがはじめて。

まとめてみた。

;program_cntを得る
LD  HL,PROG_CNT

;program[HL]を得る
LD  DE,PROG
ADD HL,DE
LD  E, (HL)

;word_pointer_entry[E]を指している番地を得る
LD  H,0
LD  L,E
ADD HL,HL
LD  DE,WORD_ENTRY
ADD HL,DE

;word_pointer_entry[E]の内容(ジャンプ先)を得る
LD  D,(HL)
INC HL
LD  E,(HL)
LD  H,D
LD  L,E

;ワード処理番地にジャンプする
JP  (HL)

さすがにアセンブラを使いたい

上記コードをハンドアセンブルするのは、勘弁願いたい。一瞬、アセンブラも作っちゃおうか、と考えるが、やめておく。ということで、フリーソフトを漁ってみる。するとこんなのがあった。

Z80アセンブラZASM

早速アセンブルしてみる。Can't openと言っている。どうやら、長いファイル名がだめなようだ。ファイル名moiforth_mz700.aszをmfmz700.aszに変更した。

プログラムはこうだ。

;
;  moiforth mz700 version 
;

	ORG 2000h

    ;program_cntを得る
    LD  HL,PROG_CNT

    ;program[HL]を得る
    LD  DE,PROG
    ADD HL,DE
    LD  E, (HL)

    ;word_pointer_entry[E]を指している番地を得る
    LD  H,0
    LD  L,E
    ADD HL,HL
    LD  DE,WORD_ENTRY
    ADD HL,DE

    ;word_pointer_entry[E]の内容(ジャンプ先)を得る
    LD  D,(HL)
    INC HL
    LD  E,(HL)
    LD  H,D
    LD  L,E

    ;ワード処理番地にジャンプする
    JP  (HL)
        

PROG_CNT:   DS 2
PROG:       DS 1000
WORD_ENTRY: DS 512

END

正常にアセンブルできたのだが、出力がHEX形式だ。

:10200000211620111820195E26006B2911002419B1
:0620100056235E626BE93D
:00000001FF

困ったと思ったらCOM形式を作る-cオプションがあった。内部は機械語のバイナリデータそのものなので使いやすい。さらにこれをテープフォーマットに変換しなければならない。

MZ700WIN FAQに、テープのフォーマット形式が書かれていた。

それによると、ヘッダ部が128バイト、残りは実データ。

ヘッダの形式を上記サイトから引用すると、

オフセット 内容
00h アトリビュート
01h-11h ファイル名。20hでパディングします。
12h-13h ファイルサイズ
14h-15h 格納アドレス
16h-17h 実行アドレス

アトリビュートがなんだか分からないが、0にでもしておこう。

内部インタプリタを動作させてみる

まだ、この内部インタプリタが動作するかわからないので、テストプログラムを作る。

;
;  moiforth mz700 version 
;

PRNT    EQU 0012H   ;1文字表示
MSG     EQU 0015H   ;文字列表示

    ORG 2000h

    ;プログラムカウンタをクリアする
    LD A,0
    LD (PROG_CNT),A

NEXT:
    ;program_cntを得る
    LD  HL,PROG_CNT

    ;program[HL]を得る
    LD  DE,PROG
    ADD HL,DE
    LD  E, (HL)

    ;word_pointer_entry[E]を指している番地を得る
    LD  H,0
    LD  L,E
    ADD HL,HL
    LD  DE,WORD_ENTRY
    ADD HL,DE

    ;word_pointer_entry[E]の内容(ジャンプ先)を得る
    LD  D,(HL)
    INC HL
    LD  E,(HL)
    LD  H,D
    LD  L,E

    ;ワード処理番地にジャンプする
    JP  (HL)
        

PROG_CNT:   DS 2
PROG:       DB 1, 2, 3
            ;DS 1000
WORD_ENTRY: 
            DW TEST1, TEST2, TEST3
            ;DS 512

EMBED_WORD:

TEST1:
    ;'A'を表示する
    LD A,'A'
    CALL PRNT

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

TEST2:
    ;"BCDE"を表示する
    LD HL,STR
    CALL MSG
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

TEST3:
    ;終了
    RET

STR:    DB 'B', 'C', 'D', 'E'


END

これをアセンブルして、さらに、 テープのヘッダをつける。ヘッダは、バイナリエディタStirlingで作成し、ヘッダファイルとアセンブルしたバイナリファイルを

copy /b head+mfmz700.com mfmz700.mzt

で連結した。さて、実行してみた。大量にわけの分からない文字がでてリセットがかかった。失敗だ。さて、プリントデバッグをやってみよう。数値を表示するモニタルーチンを探さないと。

しかし、それがどれだかわからない。しかたない、地道に探す。逆アセンブラを探したら、hojaというフリーソフトがあった。

http://www.vector.co.jp/soft/win95/prog/se054738.html

これを使って、互換モニタを逆アセンブルする。

hoja.exe -s0 -o0 -u newmon7.rom > newmon7.asm

これでアセンブリ言語のソースになった。MZ-700のエミュレータのドキュメントmz700win_jp.txtには、「単体動作時にエミュレーションしている1Z-009Aのエントリ一覧」があり、その中に"HEX"というルーチンがある。早速HEXルーチンの番地、041F番地を見てみる。

Z0083:	PUSH	BC		;41F
	LD	A,(DE)		;420
	INC	DE		;421
	JP	06F1H		;422
Z0182:	JR	C,0434H		;425
	RLCA			;427
	RLCA			;428
	RLCA			;429
	RLCA			;42A
	LD	C,A		;42B
	LD	A,(DE)		;42C
	INC	DE		;42D
	CALL	03F9H		;42E
	JR	C,0434H		;431
	OR	C		;433
Z0113:	POP	BC		;434
	RET			;435

Z0112:	CP	02FH		;6F1
	JR	Z,06FBH		;6F3
	CALL	03F9H		;6F5
	JP	0425H		;6F8

Z0181:	LD	A,(DE)		;6FB
	INC	DE		;6FC
	JP	0434H		;6FD

Z0114:	PUSH	BC		;3F9
	PUSH	HL		;3FA
	LD	BC,01000H	;3FB
	LD	HL,03E9H	;3FE
Z0110:	CP	(HL)		;401
	JR	NZ,0407H	;402
	LD	A,C		;404
	JR	040DH		;405
Z0108:	INC	HL		;407
	INC	C		;408
	DEC	B		;409
	JR	NZ,0401H	;40A
	SCF			;40C
Z0109:	POP	HL		;40D
	POP	BC		;40E
	RET			;40F

ここまでだろうか。、、、追ってみたが、皆目見当がつかない。もし、自分がHEX変換ルーチンをつくるなら、

  1. Aレジスタに値を入れて呼び出す
  2. 下位4ビットの計算のために、スタックにAFをPUSHする
  3. 4ビット右シフトし、0FHでANDして、上位4ビットのみ残す
  4. 用意しておいた16進テーブル'0','1',,'F'の先頭番地に先ほどの値を足す
  5. そのアドレスの内容がHEX変換文字
  6. 下位アドレスも同様に変換

そういう風に考えているから、どうにも頭に入ってこない。だれかモニタルーチンの使い方教えて、、と言っても仕方ないので、自分で数値→文字列変換を作ることに方針変換した。

結局、自分で16進変換ルーチンを作ることになった

こう作った。

HEX:
    ;Aレジスタの内容を16進表記に変換し、
    ;DEレジスタで示された番地に格納する
    PUSH HL
    PUSH DE
    PUSH AF
    ;上位4ビット
    AND 0F0H
    RRCA
    RRCA
    RRCA
    RRCA
    LD HL,HEXTBL
    ADD A,L
    LD A, (HL)
    LD (DE),A
    ;下位4ビット
    POP AF
    AND 0FH
    LD HL,HEXTBL
    ADD A,L
    LD A, (HL)
    INC DE
    LD (DE),A
    ;終端をつける
    INC DE
    LD A,0DH
    LD (DE),A
    POP DE
    POP HL
    RET
 
HEXTBL: DB '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'

これが間違っていたら悲しい。早速これを使って、デバッグしてみる。

テープヘッダ作成が面倒くさい

デバッグするといっても、アセンブルしただけではMZ-700エミュレータにロードできない。テープ形式に変換しなければいけない。ヘッダをつけるだけなのだが、バイナリエディタでちまちまやっていたら、効率が上がらない。そこで、ヘッダ追加プログラムを作る。だんだんForthから脱線し始めている、、、

プログラムはC言語で作る。一番慣れているので。やっつけで作った。これでテープ形式作成が楽になった。

cnvm7tape.exe MFMZ700.COM -s 2000 -n MFM7.MZT

で作ってくれる。テープ形式変換プログラムを作っている途中で、今までヘッダのファイル名の長さを間違えていることに気がついた。11Hは11ではなく、17だった。

チェックプログラムは、

   ORG 2000h

   LD HL,0AB12H
   LD A,H
   LD DE,TEMP
   CALL HEX
   CALL MSG 
   LD A,L
   LD DE,TEMP
   CALL HEX
   CALL MSG 
   RET

これで画面にAB12と表示されるはず→表示は0000だった。自作の16進変換ルーチンがおかしいようだ。

とりあえず、デバッグする。紙の上に書き出してみる。わかった。ADD A,Lしたあと、LD L,Aしていないため、テーブルの番地を計算している部分が常に0になっている。修正して、AB12が表示された。めでたい。ようやく16進変換ルーチンが完成した。

HEX:
    ;Aレジスタの内容を16進表記に変換し、
    ;DEレジスタで示された番地に格納する
    PUSH HL
    PUSH DE
    PUSH AF
    ;上位4ビット
    AND 0F0H
    RRCA
    RRCA
    RRCA
    RRCA
    LD HL,HEXTBL
    ADD A,L
    LD L,A    ;追加した
    LD A, (HL)
    LD (DE),A
    ;下位4ビット
    POP AF
    AND 0FH
    LD HL,HEXTBL
    ADD A,L
    LD L,A    ;追加した
    LD A, (HL)
    INC DE
    LD (DE),A
    ;終端をつける
    INC DE
    LD A,0DH
    LD (DE),A
    POP DE
    POP HL
    RET

HEXTBL: DB '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'

ようやく本題に戻った。デバッグだ

そもそも16進変換ルーチンを作ったのは、内部インタプリタがわけの分からない文字を大量に出してリセットしたので、メモリやレジスタの値を確認したかったからだ。さっそく表示ルーチンを埋め込んで、確認する。その結果、最後のジャンプ先が正しくないことを確認した。

なぜ、ジャンプ先の計算がおかしいか?

   ;word_pointer_entry[E]の内容(ジャンプ先)を得る
   LD  D,(HL)    ;下位8ビットを上位8ビットのDに入れている
   INC HL
   LD  E,(HL)    ;上位8ビットを下位8ビットのEに入れている
   LD  H,D
   LD  L,E

Z80では、2バイト値をメモリに保存する際、リトルエンディアンで配置されている。そのため、今まで上位と下位を取り違えていた。さっそく修正。うまくいけば、ABCDと表示されるはず。実行した。画面が全部Aで埋まった。

Aの表示とBCDの表示は別のコードが担当しているから、Aの処理だけぐるぐる回っていることになる。今度は何がおかしいのだろう。

結局、色んなところがおかしかった。

  • PROG_CNTの初期化が下位バイトのみだった
  • ワードコードがずれていた
  • BCDEの後に文字終端がなかった
  • TEST2のカウンタのインクリメントの際、カウンタをロードしていなかった

最終的にこうなった。ようやく正常に'ABCDE'を表示した。

;
;  moiforth mz700 version 
;

PRNT    EQU 0012H   ;1文字表示
MSG     EQU 0015H   ;文字列表示
LINP    EQU 0003H   ;1行入力

	ORG 2000h

    LD HL,0
    LD (PROG_CNT),HL

NEXT:
    ;program_cntを得る
    LD  HL,(PROG_CNT)

    ;program[HL]を得る
    LD  DE,PROG
    ADD HL,DE
    LD  E, (HL)

    ;word_pointer_entry[E]を指している番地を得る
    LD  H,0
    LD  L,E
    ADD HL,HL
    LD  DE,WORD_ENTRY
    ADD HL,DE

    ;word_pointer_entry[E]の内容(ジャンプ先)を得る
    LD  E,(HL)
    INC HL
    LD  D,(HL)
    LD  H,D
    LD  L,E

    ;ワード処理番地にジャンプする
    JP  (HL)
        

PROG_CNT:   DS 2
PROG:       DB 0, 1, 2
            ;DS 1000
WORD_ENTRY: 
            DW TEST1, TEST2, TEST3
            ;DS 512

EMBED_WORD:

TEST1:
    ;'A'を表示する
    LD A,'A'
    CALL PRNT

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

TEST2:
    ;"BCDE"を表示する
    LD DE,STR
    CALL MSG

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

TEST3:
    ;終了
    RET

STR:    DB 'B', 'C', 'D', 'E', 0DH

HEX:
    ;Aレジスタの内容を16進表記に変換し、
    ;DEレジスタで示された番地に格納する
    PUSH HL
    PUSH DE
    PUSH AF
    ;上位4ビット
    AND 0F0H
    RRCA
    RRCA
    RRCA
    RRCA
    LD HL,HEXTBL
    ADD A,L
    LD L,A
    LD A, (HL)
    LD (DE),A
    ;下位4ビット
    POP AF
    AND 0FH
    LD HL,HEXTBL
    ADD A,L
    LD L,A
    LD A, (HL)
    INC DE
    LD (DE),A
    ;終端をつける
    INC DE
    LD A,0DH
    LD (DE),A
    POP DE
    POP HL
    RET

HEXTBL: DB '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'

TEMP:   DS 3

DISPHL:
    ;HLの値を16進で画面に表示する
    LD A,H
    LD DE,TEMP
    CALL HEX
    CALL MSG 
    LD A,L
    LD DE,TEMP
    CALL HEX
    CALL MSG 
    RET

KEYBUF: DS 41

END

ワードを作ろう

ところで、前節のデバッグ中、エミュレータを暴走させてから、最初の起動画面が'MZ-700'から'MZ 700'となり、カーソルが消えてしまった。どうにもならず、再インストールした。エミュレータが壊れるのは初めての経験だ。

話をもどさないといけない。内部インタプリタは作ったが、ワードを作っていない。なにから作ろうか?と思っていたが、、、そうだ、機械語命令をスタック用に作っていこう。

Z80の機械語命令は、命令の種類だけで158、レジスタの組み合わせを入れるとすごいことになるが、スタックマシン(≒Forth)なら、演算対象がスタック上のデータなので命令数は激減する。

  8ビット足し算命令

足し算を行うには、

  • スタックからデータを取り出し、Aレジスタに入れる
  • スタックからデータを取り出し、Bレジスタに入れる
  • ADD A,Bする
  • 結果をスタックに入れる

作ってみる。そういえば、まだスタックも作ってなかった。さっそく作らないと。

STACK_PTR:  DS 2
STACK:      DS 1000
STACK_END:

完成。では足し算ワードを作る。

ADDB:
    LD HL,STACK_PTR
    ;スタックからデータを取り出し、Aレジスタに入れる
    LD A,(HL)   ;pop
    INC HL
    ;スタックからデータを取り出し、Bレジスタに入れる
    LD B,(HL)   ;pop
    INC HL
    ;足し算し、スタックに載せる
    ADD A,B
    DEC HL
    LD (HL),A   ;push
    LD (STACK_PTR),HL

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

こんな感じになった。

ところで、スタックへの保存方法は次の2通りあることに気がついた。

  1. ポインタが示しているところにはデータは入っておらず、そこにデータをPUSHした後、ポインタをずらす
  2. ポインタが示しているところにはデータが入っていて、ポインタをずらした後、PUSHする

どちらがいいか考えてみると、スタックに保存データは何バイトか分からないから、自分で必要なサイズを確保できる2が良いように思い、そのように作った。

  8ビット即値ロード命令

次に、データをPUSHするワードを作る。即値というのは、数値そのまま、という意味である。英語ではimmediate。

PUSHB:
    ;program_cnt + 1を得る
    LD  HL,(PROG_CNT)
    INC HL

    ;program[HL]を得る
    LD  DE,PROG
    ADD HL,DE
    LD  E, (HL)

    ;スタックに載せる
    LD HL,STACK_PTR
    DEC HL
    LD (HL),E   ;push
    LD (STACK_PTR),HL

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

とりあえず、この2つがあれば計算が出来るので、数値を表示するワードを作る。

10進数の表示はどうする?

足し算の結果は、MZ-700内では2進数で表現されている。これを10進文字列に変換して表示させたい。文字表示ルーチンはあるので、2進→10進変換が必要である。

以前2進→16進変換をやったが、これは簡単だった。これは、どちらも上位桁の数が下位桁の数を含まないため、その桁のみ考えればいいためである。

10進数はどうか。2進数の下位4ビットのみに注目すればいいかというと、違う。たとえば、2^8は256。200と50と6が入っている。2^16は65536。こんな具合なので、ビットシフトさせて終わりとはいかない。そこで、方法を考えてみる。

8ビットの数は、10進数で0から255の数を表現できる。ということは、100の桁がいくらか、10の桁がいくらか、1の桁がいくらかが分かればよい。100は2進数で1100100、10は1010、1は1である。これらの数で割っていき、それぞれの桁がいくらかを求める。

2進数の割り算は、10進数より大幅に簡単になる。桁がとる値は0か1しかないから。掛け算表は0*0=0、0*1=0、1*0=0、1*1=1の4つのみ。割り算は、除数より被除数が大きければ割れる。

ためしに、99(1100011)を10(1010)で割ってみる。

          1001
     ---------
1010 ) 1100011 1010 < 1100011 だから割れる。商に1
       1010
      --------
         100   1010 > 100 だから割れない。商に0
           0
      --------
         1001  1010 > 1001 だから割れない。商に0
            0
      --------
         10011 1010 < 10011 だから割れる。商に1。商は1001 = 9
          1010
      --------
          1001 余り9

これで、10の桁は9、1の桁も9と分かる。ここまでくれば、それぞれの桁の値にASCIIコードの0x30('0')を足せば表示用の文字データ"99"を得られる。

8ビットの2進値から10進文字列への変換ルーチンは、以下のようになる。

  1. 100で割り、100の桁の値と余りを得る
  2. 余りを10で割り、10の桁の値と余り(1の桁の値)を得る
  3. 3つの値それぞれに0x30を足し、指定番地に連続して保管する

作ってみよう。

まず割り算ルーチン。先ほどは大きければ割れると書いたが、実際には桁を合わせないといけない。人間は無意識にどこの桁から割れるかわかるが、CPUは動作をすべて指定しなければならない。そこでこうした。

  1. レジスタに被除数、除数を入れ、計算用レジスタ、商レジスタをクリアする
  2. 被除数を1ビット左シフトし、最上位ビットをキャリーフラグに入れる
  3. キャリーフラグが立っていたら、計算用レジスタの最下位ビットに1をセットする
  4. 計算用レジスタの値が除数より大きければ、除数で減算し、商レジスタの最下位ビットに1をセットする
  5. 商レジスタを1ビット左シフトする
  6. 計8回繰り返すまで、2.に戻る

ここまでで、除数で割った答えが商レジスタに、余りが計算用レジスタに得られる。余りは、下位の桁の値を求めるために引き続き利用できる。

では作ってみよう。まず、8ビットの割り算ルーチン。

DIV8:
    ;8ビット値の割り算を行う
    ;B: 被除数(計算後は余りが入る)、C: 除数
    ;D: 商、A: 計算用、E: カウンタ
    PUSH AF
    PUSH DE
    LD A,0
    LD D,0
    LD E,0
DIV8_1:
    ;被除数をシフトする
    SLA B
    ;キャリーフラグが立っていたら、計算用レジスタの最下位ビットを立てる
    JP NC,DIV8_2
    SLA A
    OR 01H
DIV8_2:
    ;計算用レジスタ >= 除数 なら、除数で減算し、商の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    JP NZ,DIV8_3
    SUB C
    OR 01H
DIV8_3:
    ;商を左シフトする
    SLA C
    ;8回繰り返したら、終了する
    INC E:
    PUSH AF
    LD A,E
    CP 8
    POP AF
    JP NZ,DIV8_1
    POP DE
    POP AF
    RET

DIV8を用いて、10進文字列変換関数を作る。

CONVDECI:
    ;数値を10進文字列に変換する
    ;A: 表示する値
    ;HL: 文字列格納アドレス

    ;100の桁を得る
    LD C,100
    CALL DIV8
    LD A,C
    OR 30H
    LD (HL),A
    INC HL

    ;10の桁を得る
    LD C,10
    CALL DIV8
    LD A,C
    OR 30H
    LD (HL),A
    INC HL

    ;1の桁を得る
    LD A,B
    OR 30H
    LD (HL),A
    INC HL

    ;終端を付ける
    LD (HL),0DH

    RET

これを用いて数値を表示するワードを作る。

PRINT:
    LD HL,STACK_PTR
    ;スタックからデータを取り出し、Aレジスタに入れる
    LD A,(HL)   ;pop
    INC HL
    LD (STACK_PTR),HL
 
    ;表示する
    LD HL,PRINTBUF
    CALL CONVDECI
    CALL MSG

    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    INC HL
    LD (PROG_CNT),HL
    JP NEXT

これで、データのPUSH、足し算、表示の各ワードができたので、ワードでプログラムを作ってみる。行う処理は、"2 3 + ."である。ただし、まだ構文解析できないので、ソースコードに直接内部コードを記述する。

PROG:       DB 1, 2, 1, 3, 0, 2, 3

WORD_ENTRY: 
            DW ADDB, PUSHB, PRINT, WORDRET, TEST1, TEST2, TEST3

1がPUSHB、続く2はPUSHする値、

 、、、さて、

1年以上放置していた。久しぶりに現状を確認しようと、アセンブルし、エミュレータにかけると、暴走した。そうか、デバッグ中だったか。

ぼちぼち再開します。

ラベルPROGに"2 3 + ."を表す中間コードを書いてある。それの意味するところは、ラベルWORD_ENTRYのインデックスである。

1 .. PUSHB。続く数値(1バイト)をスタックに積む
2 .. 数値
1 .. PUSHB。続く数値をスタックに積む
3 .. 数値
0 .. ADDB。スタック上の2つの1バイト数値を加算し、スタックに積む
2 .. PRINT。スタック上の数値を取り出し、表示する
3 .. WORDRET。戻る

これのどこかで暴走している。どうやってデバッグしよう?HALT(CPU停止)命令で止めてみよう。

PUSHBルーチンがNEXTルーチンにジャンプする前にHALTを入れて、実行してみた。止まった。PUSHBが暴走している訳ではないらしい。

ADDBにHALTを入れてみた。止まった。

PRINTにHALTを入れてみた。いくらかゴミを表示して止まった。表示にバグが有るようだ。でも止まった。

WORDRETにHALTを入れてみた。£記号を画面上に表示し続けている。これが原因だ。しかし、WORDRETの実体はRET命令1つなので、実際はNEXTルーチンがおかしいのだ。

まず、PRINTのバグを取り除こう。

いま気がついたが、STACK_PTRの内容をSTACKのアドレスで初期化していない。バグとはこんなモノだ。早速初期化する。

    ;スタックポインタを初期化する
    LD HL,STACK_END
    LD (STACK_PTR), HL

表示が000になった。本当は005になるはずなのだが。また、モニタに制御が戻らず暴走している。

まず、モニタに制御がもどるようにしよう。ワードWORDRETだけをプログラムに記述し、アセンブル、実行してみる。

PROG:       DB 3 ;DB 1, 2, 1, 3, 0, 2, 3

すると、モニタにちゃんと戻ってきた。

 MZ-700
*G2000
*

ということは、この暴走はワードPRINTで引き起こされていることになる。そこで、どこにRET命令を入れたらモニタに制御がもどるか、試してみる。

PRINT:
    LD HL,STACK_PTR
    ;スタックからデータを取り出し、Aレジスタに入れる
    LD A,(HL)   ;pop
    INC HL
    LD (STACK_PTR),HL
                         <-- 3
    ;表示する
    LD HL,PRINTBUF
    CALL CONVDECI
    LD DE, PRINTBUF
    CALL MSG
                         <-- 2
    ;ジャンプする
    LD HL,(PROG_CNT)
    INC HL
    INC HL
    LD (PROG_CNT),HL
    JP NEXT
                         <-- 1

1は手前にJP命令があるので、戻らなかった。2にRET命令を入れてみると、モニタに戻った。ということは、NEXTルーチンに問題があるらしい。

と思っていたが、実際には"JP NEXT"の手前の"INC HL"が2つあるためだった。PUSHBは続くデータの分を含めてPROG_CNTを+2する必要があるが、PRINTは+1だけでよい。どうやらPUSHBからコピペしたためらしい。

暴走は止まった。現在の問題は、"2 3 +"の結果である"005"を表示しないことだ。

 MZ-700
*G2000
000
*

この通り、"000"である。ワードPRINTは、表示をするのに

  • CONVDECIでAレジスタの値を3桁文字列に変換し、バッファに保存する
  • MSGでバッファを表示する

の処理を行なっている。MSGはバッファをそのまま表示しているだけなので、問題ない。問題はCONVDECIだろう。今までちゃんと動いているか確認されたことがないのだ。

その前に大きな勘違いをしていた。スタックだ。

スタックに積まれたデータにアクセスするには、

  • STACK_PTRに保存された、スタックの現在位置をレジスタにロードする
  • 現在位置に保存されたデータをロードする

としなければならないのに、最初のロードが

  • STACK_PTRをロードする

になっている。

    LD HL,STACK_PTR
    ↓
    LD HL,(STACK_PTR)

に変更し、スタックポインタの動きを確認した。確認方法は、スタックポインタの値を変更した直後にHLレジスタの表示ルーチン"CALL DISPHL"を挿入する、デバッグプリント。

 MZ-700
*G2000
24182417241724162416241724172418000

2418が初期位置で、PUSHBで-1、PUSHBで-1、ADDBで+1、PRINTで+1、初期位置に戻っている。OK。最後の000は、ワードPRINTの出力。

そして、ワードPRINTがスタックからAレジスタにロードしたデータを"CALL DISPA"でデバッグプリントした。

 MZ-700
*G2000
05000

Aレジスタの値は5なので、"2 3 +"を正しく計算したことがわかる。しかし出力は"000"だ。そうすると、文字列変換を行うCONVDECIにバグがあるのかもしれない。

こういう時は、ステップ実行をしてみるとよい。しかし、デバッガがないので、紙の上で。

紙の上でデバッグしてみる。命令を一行ずつ見て、レジスタ値の変化を記録する。

その途中で、商をDレジスタで返す、としているのに、最後にDEレジスタをPOPしている。これではDレジスタの値が消えてしまう。それ以前に、Dレジスタに商を保存していない。DレジスタはPOPするので、Cレジスタに商を保存するように変更する。

 MZ-700
*G2000
300

数値が出た。しかし、5ではない (2011.01.18)。

 、、、いつの間にか3年半経過した。

が、再開する。どうやら、紙の上でデバッグしていたらしい。とりあえず、Z80のデバッガを探そう (2014.07.03)。

プリントデバッグでいいや、と思い直す。今実行しているプログラムは、こうだ。

PROG:       DB 1, 2, 1, 3, 0, 2, 3

ワードのジャンプテーブルがこうである。

WORD_ENTRY: 
            DW ADDB, PUSHB, PRINT, WORDRET, TEST1, TEST2, TEST3

なので、2をプッシュし、3をプッシュし、足して、画面に表示する、を行う。予想値は5だ。そして、今は、表示がうまくいかない。

そして今気づいたが、これ、表示したあと、止まらないのでは?(2014年7月5日)

と思ったが、最後の3はWORDRET、中身は「RET」命令なので止まるはず (2014年7月6日)。

表示したあと、表示が止まらないのではなく、3年半前にデバッグ出力のコードが残っていただけだった、、

現在、"005"を表示するはずが、"300"を表示している。少し原因がわかった。サブルーチン DIV8 は、8ビットの割り算のために8回ループしているはずが、256回ループしている。判定がおかしい。

ここのコードがおかしいようだ。

    PUSH AF
    LD A,E
    CP 8
    POP AF
    JP NZ,DIV8_1

ループカウンタ E を A にセットし、8と比較し、ゼロフラグが立っていなかったらループ先頭に戻る、が正しい動作。CP と JP NZ の間に POP が挟まっているのが原因?しかし、POPではフラグは変化しないはず (2014年7月7日)。

念のため、途中のPOPを除いて書いてみる。

    PUSH AF
    LD A,E
    CP 8
    JP Z,DIV8_5

DIV8_4:
    POP AF
    JP DIV8_1

DIV8_5:
    POP AF

    ;商を入れる
    LD C, A
    POP DE
    POP AF
    RET

すると、、ループが8回になった。

01020304050607080102030405060708300

DIV8 は2回呼び出されているので、"01...08" が2回、最後の "300" が実際の出力。結果は "300" のまま。

もう一つ気になったのが、この部分。JP NC と JP NZ を続けて実行している。

DIV8_2:
    ;計算用レジスタ >= 除数 なら、除数で減算し、商の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    JP NZ,DIV8_3
    SUB C
    OR 01H

JP命令ではフラグは変化しないはずなので、これでいいはずだが、途中に CP を入れてみる。、、変化せず (2014年7月8日)。

CONVDECI は次のようなコードである。Bレジスタに被除数をセットし、DIV8 が呼び出されるとBレジスタには余りだけが残る。それをさらに DIV8 で処理し、最後の1の桁はそのまま余りが入る。そこで、途中のBレジスタの値をデバッグ出力してみた。

CONVDECI:
    ;数値を10進文字列に変換する
    ;A: 表示する値
    ;HL: 文字列格納アドレス

    ;被除数をセットする
    LD B, A

    call dispb  ;deb

    ;100の桁を得る
    LD C,100
    CALL DIV8
    LD A, C
    OR 30H
    LD (HL),A
    INC HL

    call dispb  ;deb

    ;10の桁を得る
    LD C,10
    CALL DIV8
    LD A, C
    OR 30H
    LD (HL),A
    INC HL

    call dispb  ;deb

    ;1の桁を得る
    LD A,B
    OR 30H
    LD (HL),A
    INC HL

    ;終端を付ける
    LD (HL),0DH

    RET

結果は、、最初の DIV8 を呼び出す前が 5。当然である。処理したあとは、0、0。本当は最後まで5が残らなければならない。

となると怪しいのは DIV8。コードを良く見てみる。

DIV8:
    ;8ビット値の割り算を行う
    ;B: 被除数(計算後は余りが入る)、C: 除数(計算後は商が入る)
    ;A: 計算用、E: カウンタ
    PUSH AF
    PUSH DE
    LD A,0
    LD D,0
    LD E,0

DIV8_1:
    ;被除数をシフトする
    SLA B
    ;キャリーフラグが立っていたら、計算用レジスタの最下位ビットを立てる
    JP NC,DIV8_2
    SLA A
    OR 01H

DIV8_2:
    ;計算用レジスタ >= 除数 なら、除数で減算し、商の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    CP C
    JP NZ,DIV8_3
    SUB C
    OR 01H

DIV8_3:
    ;商を左シフトする
    SLA C
    ;8回繰り返したら、終了する
    INC E:

    PUSH AF
    LD A,E
    CP 8
    JP Z,DIV8_5

DIV8_4:
    POP AF
    JP DIV8_1

DIV8_5:
    POP AF

    ;商を入れる
    LD C, A
    POP DE
    POP AF
    RET

(2014年7月9日)

気がついた。8回ループしているので、Bレジスタを8回左シフトすることになる。つまり、Bレジスタは0になってしまう。本当は次の、10による割り算のために余りが入っていないといけない。まず1つ。

それと、CONVDECIの結果は"300"だった。これは、100の桁の数を調べるために5を100で割った結果が"3"ということである。これもおかしい。

DIV8_2がおかしい。商の最下位ビットを立てる、と書いてあるが、ビットを立てているのは計算用レジスタ = 最終的な余りである。

なんか、色々おかしい (2014年7月10日)。

まず、余りを本来のBレジスタに入れるように修正する。余りは計算用のAレジスタにあるので、最後に LD B, A すれば良い。

そして、商だが、今まで商を保存しておく場所がなかったようだ。除数であるCレジスタを、商と混同している。そこで、Dレジスタを商を保存するレジスタとする。

さらに気づいた。被除数をシフトしてビットを取り出す際、ビットがあるときしか計算用レジスタをシフトしていなかった。商レジスタも同じ間違いをしていた。

修正後、こうなった。

DIV8:
    ;8ビット値の割り算を行う
    ;B: 被除数(計算後は余りが入る)、C: 除数(計算後は商が入る)
    ;A: 計算用、E: カウンタ、D: 商保存用
    PUSH AF
    PUSH DE
    LD A,0
    LD D,0
    LD E,0

DIV8_1:
    ;計算用レジスタ (A) をシフトしておく
    SLA A
    ;被除数をシフトする
    SLA B
    ;キャリーフラグが立っていたら、計算用レジスタの最下位ビットを立てる
    JP NC,DIV8_2
    OR 01H

DIV8_2:
    ;商 (D) を左シフトしておく
    SLA D

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    CP C
    JP NZ,DIV8_3
    SUB C

    ;商 (D) の最下位ビットを立てる
    PUSH AF
    LD A, D
    OR 01H
    LD D, A
    POP AF

DIV8_3:
    ;8回繰り返したら、終了する
    INC E:

    PUSH AF
    LD A,E
    CP 8
    JP Z,DIV8_5

DIV8_4:
    POP AF
    JP DIV8_1

DIV8_5:
    POP AF

    ;余りを入れる
    LD B, A

    ;商を入れる
    LD C, D

    POP DE
    POP AF
    RET

実行結果は、、

 MZ-700
*G2000
005
*

成功した。4年半かかった (2014年7月11日)。

十の桁と百の桁も確認してみる

PROG:       DB 1, 7, 1, 8, 0, 2, 3

7と8を足すので、15が表示されるはず。

 MZ-700
*G2000
00?
*

、、、00? が表示された。

ADDB ルーチンの計算結果は合っていたので、やはり DIV8 ルーチンのバグだと思う。DIV8 ルーチンが呼び出されたあと、商と余りを確認する。

    ;100の桁を得る
    LD C,100
    CALL DIV8
    LD A, C
    OR 30H
    LD (HL),A
    INC HL
    
    call dispc  ;deb
    call dispb  ;deb

    ;10の桁を得る
    LD C,10
    CALL DIV8
    LD A, C
    OR 30H
    LD (HL),A
    INC HL

    call dispc  ;deb
    call dispb  ;deb

その結果、

  1. 100で割った結果: 商: 0, 余り: 15
  2. 10で割った結果: 商: 0, 余り: 15

10で割った結果がおかしい。ということは、DIV8_2 が間違っている。

DIV8_2:
    ;商 (D) を左シフトしておく
    SLA D

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    CP C
    JP NZ,DIV8_3
    SUB C

    ;商 (D) の最下位ビットを立てる
    PUSH AF
    LD A, D
    OR 01H
    LD D, A
    POP AF

ここでは被除数 (== A) が除数以上であれば減算し、商のビットを立てる処理を行っている。そこで、比較を行う前にAレジスタの値を確認してみる。

DIV8_2:
    ;商 (D) を左シフトしておく
    SLA D

    call dispa  ;deb

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる

結果:

 MZ-700
*G2000
001A1A1A1B1B1B1B
001A1A1A1B1B1B1B
00?
*

Aレジスタには元の被除数 15 から、左シフトして取り出した値が入っていくため、01, 03, 07, 0F,, のように増えるはず。一体、どこで値が混入しているのか、あちこちに表示を挟んでみた。

その結果、表示をさせるルーチン HEX が、Aレジスタを破壊することが分かった、、、 (2014年7月13日)

気を取り直して、HEXルーチンを修正し、再度実行した。

 MZ-700
*G2000
000000000103070F
000000000103070F
00?
*

ここは想定したとおり。次はCレジスタにある除数を表示させる。

 MZ-700
*G2000
6464646464646464
0A0A0A0A0A0A0A0A
00?
*

64Hは100、0AHは10であるため問題ない。問題は、2段目の最後の 0FH は 0AH より大きいので、減算されるはずなのにされないことである。

減算部の処理をよく見てみる。

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる
    CP C
    JP NC,DIV8_3
    CP C
    JP NZ,DIV8_3
    SUB C
  1. キャリーフラグが立っていなかったら (A > C)、ジャンプする → 減算しない
  2. ゼロフラグが立っていなかったら (A != C)、ジャンプする → 減算しない

、、、どうやら論理が逆である。本当は次のようでなくてはいけない。

  1. キャリーフラグが立っていたら (A > C)、ジャンプする → 減算しない

そこで、コードを修正する

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる
    CP C
    JP C,DIV8_3
    SUB C

実行してみると、結果は 00? ではなく、015 になった。これは、一回も減算していないことになる。そこで、確認のため、上記コードにデバッグプリントを挟んでみた。

    ;計算用レジスタ (A) >= 除数 (C) なら、除数で減算し、商 (D) の最下位ビットを立てる
    CP C
    JP C,DIV8_3

    call dispa  ;deb
    call dispc  ;deb

    SUB C
    call dispa  ;deb

正常なら、1回だけここを通るはず。結果は、、通っている。

 MZ-700
*G2000
0F0A05
015
*

(2014年7月14日)

正しい余り 05H になっている。商が保存されているDレジスタも 01H なので、あっている。

、、、あれ?結果が 015 なら、合っている。100で割った結果が 0 で、10で割った結果が 1 で、余りが 5 だ。何か勘違いしていたみたいだ。結果が 015 になった時点で、バグは直ったんだ。

では次は、123を表示してみる。

PROG:       DB 1, 100, 1, 23, 0, 2, 3

結果は、、

 MZ-700
*G2000
123
*

8ビットで表現できる最大値、255を表示してみる。

PROG:       DB 1, 199, 1, 56, 0, 2, 3
 MZ-700
*G2000
255
*

OK、これでようやく10進数での表示が終わった。次は何やるんだっけ?4年半かかったので忘れている (2014年7月15日)。

マシン語命令の置き換え

4年半前に何をやっていたか?思い出した。マシン語の命令をワードにしようとしていた。

Z80のマシン語命令をForthのワードにするには、、

 ロード・ストア命令

Z80は、レジスタへのロード、メモリへのストアの両方とも、LDで表現する。Forthの場合、レジスタが存在せずスタックのみなので、レジスタへのロードがなくなる。ただし、指定番地からスタックへのPUSH、スタックから指定番地へのPOPがそれぞれ、ロード・ストアに当たる。

  PUSH・POP命令

(2014年7月16日)

Forthの場合、元からPUSH・POPしかないため、必須。

  データ交換命令

レジスタ間、レジスタ-メモリ間のデータを交換する。スタック上のデータを入れ替えるワード SWAP が該当するか。

  8ビット演算命令

すべてスタック上のデータ間の演算となる。

  • 加算 (ADD)
  • キャリー付き加算 (ADC)
  • 減算 (SUB)
  • キャリー付き減算 (SUB)
  • 論理積 (AND)
  • 論理和 (OR)
  • 排他的論理和 (XOR)
  • 比較 (CP)

、、、さて、これから数ヶ月更新を止める (2014年7月17日 つづく)

cnvm7tape.zip