↑ページトップへ
表示調整
閉じる
挿絵表示切替ボタン
▼配色
▼行間
▼文字サイズ
▼メニューバー
×閉じる

ブックマークに追加しました

設定
0/400
設定を保存しました
エラーが発生しました
※文字以内
ブックマークを解除しました。

エラーが発生しました。

エラーの原因がわからない場合はヘルプセンターをご確認ください。

ブックマーク機能を使うにはログインしてください。
PR
39/50

エピ28 MZ80のディスプレイでプログラミング


 ***


 エピ28 MZ80のディスプレイでプログラミング


 ***


 MZ80の画面ディスプレイに書き込んだ文字は、機械語のプログラムとして実行できるのです。エピ27で幾つか見ましたね。


 SP5030などの言語をカセットテープからロードしてなくても、ちょっとしたプログラムなら作って遊べそうです。実際、ググっていたら機械語入力ツールを作成された方を見つけましたし(参考文献 [W02])。

 画面でプログラミングするのは割とアリなのかも?


 それでね。そんな環境でどんなプログラミングができるかなと考えて見たくなりました。


 今回はそんな話です。


 気分は FX502P で作るゲーム感覚でしょうか。あるいは LKit-16 のキーボードでニモニック入力する感じ? まあ、興味本位で進むので迷走しますよ。


 ***


 MZ80の電源オン後に動作開始するSP1002(モニタROM)を前提とします。この環境で可能なプログラミングを考えます。


 SP1002のメインループは GETL で1行分の文字入力を行って、LOAD や GOTO などのコマンドだったらそれを実行します。


 文字の入力中はカーソル・キーで画面上を移動して、好きな場所に好きな文字ディスプレイコードを入力できます。カーソル制御の文字も INS キーと組合せて入力できます。入力できない文字もありますがね。エピ22で調べました。


 カーソル制御 [C] で画面をクリアして、画面1行目の先頭から文字(=プログラム)を入力した後、[CR] を押下して GOTO$D000 すると先頭の文字(D000h番地)から実行できます。「ネ木1ネキ4」はこのパターンです。

 2行目の先頭から書いた場合は、D000h+40 = D028h です。好きな場所にプログラムを書いて、そこに GOTO すればよいのです。

 プログラムの最後は JP 0082h にすればSP1002のメインループに戻ります。


 *


 ただし。GETL では入力できない、あるいは入力できても画面表示できないディスプレイコードがあって、それがためにZ80の全ての命令を使えるわけではないです。つまり「使える命令」と「使えない命令」があるのです。


 次の表で緑色が入力できる文字(使える命令)で、朱色が入力できない文字(使えない命令)です。

 挿絵(By みてみん)


 表では使える命令の方が多いように見えますけど、...。EDh を入力できないので LDIR 命令(EDh,B0h)が使えないとかダメージが大です。これは良く見て考えないと行けませんね。


 *


 使える命令と使えない命令について詳しく確認します。以下、○印は使える、〼印は使えないです。


 *


 コール命令についてです。


  ○C4:CALL NZ,nm ○D4:CALL NC,nm  ○DC:CALL C, nm

  ○D0:RET NC   ○D8:RET C


  〼CD:CALL nm   〼CC:CALL Z, nm 

  〼E4:CALL PO,nm 〼EC:CALL PE,nm 

  〼F4:CALL P, nm 〼FC:CALL M, nm

  〼C9:RET     〼C0:RET NZ   〼C8:RET Z    

  〼E0:RET PO   〼E8:RET PE   〼F0:RET P 〼F8:RET M


 「ネ木1ネキ4」のときから分っていたけど、CALL nm 命令も RET 命令も使えないのね。だからコールの前に XOR A などをして条件をセットして CALL NC,nm や RET NC するのね。


 なお、Cfキャリ・フラグは SCF 命令でセットできて、AND,OR,XOR 命令でクリアできる。


 XOR A「ネ」だと A の値が 00h になるけど、AND A「ャ」と OR A「ョ」なら A の値を保持できる。SCF と CALL C,nm でもレジスタを壊さない。...けど「ネ木」で良いかな?


 *


 PUSH/POP は次の命令は使えないです。BC と DE は PUSH/POP できるです。


  〼E5:PUSH HL  〼E1:POP HL  

  〼F5:PUSH AF  〼F1:POP AF     


 HL を PUSH/POP したい場合は、...EX (SP),HL も EX DE,HL も使えないから、


  PUSH HL ⇒ PUSH DE; LD D,H; LD E,L; PUSH DE

  POP HL ⇒ POP DE; LD H,D; LD L,E; POP DE


 などと苦しいことになりそう。メモリを2バイト使うと LD (nm),HL と LD HL,(nm) で退避できるけど6バイトだよ。HL の立場が揺らぐね。

 AF は EX AF,AF' で回避できるかも。

 EXX は使えるけどZ80に相当慣れていないと使い熟せないかな。


 *


 分岐命令です。


  ○18:JR +n   ○10:DJNZ +n

  ○20:JR NZ,+n ○28:JR Z, +n ○30:JR NC,+n ○38:JR C, +n

  ○C3:JP nm   ○C2:JP NZ,nm ○D2:JP NC,nm ○DA:JP C, nm


  〼CA:JP Z, nm 〼E9:JP (HL)  〼DDE9:JP (IX) 〼FDE9:JP (IY)

  〼E2:JP PO,nm 〼EA:JP PE,nm 〼F2:JP P, nm 〼FA:JP M, nm


 むむ。JP Z,nm 命令が使えないのは注意ね。JP NZ,nm は使えるから IF-ELSE を逆にすれば良いか。JP P,nm と JP M,nm が使えないのは不安。


 JP (HL) が使えないのは、PUSH HL; RET でジャンプでき...、あ、PUSH HL も RET も直には使えない。LD BC,HL; PUSH BC; SCF; RET C か。

 Cf が分っていて番地が BC に入っていると PUSH BC; RET C/NC で2バイト。


 JR は全部使えるし、DJNZ も使える命令だけど、マイナス方向の番地 E0h〜FFh を入力できない。条件付き分岐でループを作るときに困りそう。ループの範囲が 32 バイト以上なら良いけど。

 n バイト前に戻るには、...。


  D9  ; EXX

  C1  ; POP BC

  0A  ; LD A,(BC)

  6F  ; LD L,A

  79  ; LD A,C

  95  ; SUB L

  4F  ; LD C,A

  D201 ; JR NC,+01

  05  ; DEC B

  C5  ; PUSH BC

  D9  ; EXX

  AF  ; XOR A

  D0  ; RET NC


 というサブルーチンを用意して、


  C4mmnn07 ; CALL Z, nnmmh, 07h

  D4mmnn07 ; CALL NC, nnmmh, 07h

  DCmmnn07 ; CALL C, nnmmh, 07h


 とすればマイナス方向に条件分岐できるけど、使えるかな? ルーチンが 14 バイトでジャンプ毎に4バイト。...、使えない?


 DJNZ は JP 命令と組合せると、...


  _LOOP:

    :

    :

    DJNZ _SKIP

    JP _EXIT

  _SKIP:

    JP _LOOP

  _EXIT:


 これなら普通に DEC B; JP NZ,_LOOP の方がよさそう。DJNZ は実質使えない感じ? 


 *


 即値イミディエイトを使う命令は、こんな感じです


  ○C6:ADD A,n ○D6:SUB n  〼E6:AND n 〼F6:OR n

  〼CE:ADC A,n ○DE:SBC A,n 〼EE:XOR n 〼FE:CP n


 ADD A,n と SUB n, SBC A,n は使えるけど、論理演算は全滅。

 LD A,n などの8ビットの LD 命令は全てOKなので、AND n したいときは LD B,n; AND B などと逃げるかな。CP n は SUB n で代用して、必要なら後で ADD A,n して戻す方が良いか?


 *


 SP関係の命令は、...


  ○31:LD SP,nm   ○39:ADD HL,SP  ○DD39:ADD IX,SP

  ○33:INC SP    ○3B:DEC SP


  〼F9:LD SP,HL   〼DDF9:LD SP,IX   〼FDF9:LD SP,IY

  〼ED7A:ADC HL,SP  〼ED72:SBC HL,SP   〼FD39:ADD IY,SP

  〼E3:EX (SP),HL  〼DDE3:EX (SP),IX  〼FDE3:EX (SP),IY

  〼ED7B:LD SP,(nm) 〼ED73:LD (nm),SP


 ほぼ使えません。特に EX (SP),HL が使えないのは残念です。


 *


 その他、使えない命令


  〼40:LD B,B 〼80:ADD A,B 〼EB:EX DE,HL

  〼CB:{BIT}  〼ED:{EXT}  〼FD:{IY}  (○DD:{IX}は使える)


 ADD A,B が使えないことは覚えておかないと。EX DE,HL はどうしよう? A 経由で D,E,H,L を個別に交換する? EDh が使えなくて悲しい。BIT 操作や IY 関係は不安。


  〼FB:EI     〼F3:DI 

  〼C7:RST 00h  〼CF:RST 08h (○D7:RST 10h) 〼DF:RST 18h

  〼E7:RST 20h  〼EF:RST 28h  〼F7:RST 30h  〼FF:RST 38h


 ...。使えないけど、使わないかな。ブドウはいつも酸っぱいさ。


 *


 使えない命令でもプログラムで書き換えれば使えるのだけど。これは現実的だろうか?

 E0h〜FFh を画面に書き込みたいなら、90h〜AFh をキー入力しておいて、


   LD HL,D000h

   LD B,20h

  _LOOP:

   LD A,(HL)

   ADD A,50h

   LD (HL),A

   INC HL

   DEC B

   JP NZ,_LOOP


 だけど、プログラムの個々の命令を書き換えるのは面倒だよね。1つか2つなら良いけど、コードサイズ等でメリットがあるか不明確。小さなプログラムなら命令の組合せで逃げるのが良さそう。


 *

 **

 ***


 プログラミング環境?を確認したので、何かゲームを作ってみましょう。


 ライフゲームです。...、えーと、人生ゲームではなくてコンウェイのライフゲームです(参考文献 [N06])。


 *


 コンウェイのライフゲームとは。


 画面上の点を次のルールで、点けたり(誕生)消したり(死亡)します。ここで周囲と言うのは上下左右とナナメ4方向も含めた8点です。


  ・周囲に点が2つあれば存続する(=現状維持)

  ・周囲に点が3つあれば誕生あるいは存続する

  ・それ以外は死亡する


 見てるだけのゲームだけどさ。実行すると点々が動き回って楽しいのです。グライダと呼ばれるパターンが生まれると行方を見守ってしまいます。単純なルールなのに生まれるパターンは複雑で、チューニング完全な計算能力があるのが魅力的なのでしょうか?


 *


 スクリプトで書いて見ました。


  import numpy as np

  import matplotlib.pyplot as plt

  sx = 150; sy = 150

  m0 = np.random.randint(0,2,(sx,sy)); m1 = m0.copy()

  vv = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]

  fg = plt.figure(figsize=(8,8))

  while 1:

    for x in range(sx):

     for y in range(sy):

      a = sum([m0[(x+vx)%sx,(y+vy)%sy] for vx,vy in vv])

      m1[x,y] = m0[x,y] if a==2 else 1 if a==3 else 0

    m0 = m1.copy()

    plt.clf(); plt.xticks([]); plt.yticks([]); plt.imshow(m0)

    plt.show(block=False); plt.pause(0.00001)


 ライブラリの numpy と matplotlib の使用を宣言します。

 変数 m0 と m1 が画面となる配列で、配列のサイズを変数 sx と sy で指定します。m0,m1 の初期値は乱数です。

 変数 vv は周囲を参照するための値です。x,y の周囲の点の個数を数えて変数 a にします。画面の上側は下側と、右側は左側と連続します。つまりトーラスです。楕円曲線の授業で学びましたね。

 While ループの中ではライフゲームのルールにより m0[x,y] と変数 a を参照して m1[x,y] に設定します。そして m1 を m0 にコピーして、m0 を imshow() して表示します。


 見飽きたらブレークして止めてね(笑)


 *


 グライダのパターンは、


     ■    ■ ■     ■    ■

      ■ ⇒  ■■ ⇒ ■ ■ ⇒   ■■ ⇒(戻る)

    ■■■    ■     ■■    ■■


 です。


 150x150 の広さの画面で乱数生成すると、グライダが何個か(何度か)発生するはずです。大抵は進行方向にある別の点と衝突して壊れてしまうのですけど。グライダを1個だけ配置するなら延々と進んで行きます。


 何も無いトーラスな空間を進んでいくグライダって、単体テストのお供にどうでしょうか。心を虚無にして淡々と、ですね。


 ***

 **

 *


 では。Z80の機械語にしてみます。ドキドキ


 *


 周囲の点の個数をカウントするルーチン _ev を作りましょう。このルーチンは2行目に置きます。ORG D000h+40 = D028h という感じです。

 _ev の入力は B,C(=x,y 座標)と Dxxxh(=D000h+B+C*40の周囲)です。結果は周囲の点をカウントした値で D に入れます。A と HL は保持しません。


 D028: AF57__________ |_ev: xor A; ld D,A

 D02A: 04D450D0______ |   inc B; call NC,_sb

 D02E: 0CD450D0______ |   inc C; call NC,_sb

 D032: 05D450D0______ |   dec B; call NC,_sb

 D036: 05D450D0______ |   dec B; call NC,_sb

 D03A: 0DD450D0______ |   dec C; call NC,_sb

 D03E: 0DD450D0______ |   dec C; call NC,_sb

 D042: 04D450D0______ |   inc B; call NC,_sb

 D046: 04D450D0______ |   inc B; call NC,_sb

 D04A: 0C05__________ |   inc C; dec B

 D04C: D0____________ |   ret NC

 D04D:


 まず xor A で Cf をクリアします。ld と inc, dec では Cf は変化せず、サブルーチン _sb は Cf をクリアしてリターンします。よって Cf はクリアのままです。

 B,C を inc/dec して周囲の座標にして、_sb で点のオンオフを調べます。B,C は最後に元の位置に戻します。


 とりあえず思い付いたロジックですけど。inc/dec をテーブル参照にして、CALL のコードを1つにできると良いね。


 *


 行数の圧縮のためにマルチステートメントにしています。普通はこの様な書き方はしないと思うけど。エディタで書いてハンドアセンブルしているから、好きにできます(...、好きにしてます?)。


 *


 次は _sb です。このルーチンは3行目に置きます。

 入力は B,C で、出力は D です。B,C は保持します。A,H,L を破壊します。Dxxxh も参照します。


 D050: 6069__________ |_sb: ld H,B; ld L,C

 D052: 3E00BC20022627 |   ld A, 0; cp H; jr NZ,_s1; ld H,39

 D059: 3E28BC20022601 |_s1: ld A,40; cp H; jr NZ,_s2; ld H, 1

 D060: 3E04BD20022E18 |_s2: ld A, 4; cp L; jr NZ,_s3: ld L,24

 D067: 3E19BD20022E05 |_s3: ld A,25; cp L; jr NZ,_s4; ld L, 5

 D06E: AFD4B40F______ |_s4: xor A; call NC,_0FB4h

 D072: 7E____________ |   ld A,(HL)

 D073: B7280114______ |   or A; jr Z,_s5; inc D

 D077: D0____________ |_s5: ret NC

 D078:


 _sb は LD HL,BC(相当)をして、HL が画面の端を超えていたら補正して、ROMルーチン 0FB4h をコールして座標 (x,y) を番地 Dxxxh に変換します。0FB4h のルーチンは A,BC,DE,HL を保持します。

 [HL] が 00h でなければ inc D して、リターンです。

 D052h 番地の ld A,0 は xor A; dec A に直すと良いです。D072h 番地の ld A,(HL) は or A,(HL) の方が良いかな。


 画面をトーラスにしなければ _sb は 11 バイトで書けそうですけど、トーラスにした方が楽しいのです。


 *


 次は周囲の点の個数により点のオンオフを決めるルーチン _nx です。4行目に置きます。

 入力は D と B,C, Dxxxh です。出力は Cxxxh です。A,DE,HL は保持しません。BC は保持です。


 D078: 6069__________ |_nx: ld H,B; ld L,C

 D07A: AFD4B40F______ |   xor A; call NC,_0FB4h # HL:D0xx

 D07E: 5E____________ |   ld E,(HL) # keep now

 D07F: 7CD61067______ |   ld A,H; sub 10h; ld H,A # HL:C0xx

 D083: 15152003______ |   dec D; dec D; jr NZ,_n2

 D087: 731809________ |   ld (HL),E; jr _n4 # case 2, copy

 D08A: 152004________ |_n2: dec D; jr NZ,_n3

 D08D: 36551802______ |   ld (HL),55h; jr _n4 # case 3, set 1

 D091: 3600__________ |_n3: ld (HL),00h # else, set 0

 D093: AFD0__________ |_n4: xor A; ret NC

 D095:


 スクリプトの変数との対応は D: 変数 a, B,C: 変数 x,y, Dxxxh: 変数 m0, Cxxxh: 変数 m1 です。

 D07Ah 番地の XOR A は不要かも。case 2 のコピーは不要にできますね。


 *


 C0C8h〜C3E7h を D0C8h〜D3E7h にコピーするルーチン _cy です。5行目に置きます。

 入力は Cxxxh で、出力は Dxxxh です。A,BC,DE,HL を破壊します。


 D0A0: 012003________ |_cy: ld BC,0320h # = 40*20

 D0A3: 11C6D01C1C____ |   ld DE,D0C6h; inc E; inc E

 D0A8: 7AD610676B____ |   ld A,D; sub A,10h; ld H,A; ld L,E

 D0AD: 7E12__________ |_c1: ld A,[HL]; ld [DE],A

 D0AF: 23130B________ |   inc HL; inc DE; dec BC

 D0B2: 78B1C2ADD0____ |   ld A,B; or C; jp NZ,_c1

 D0B7: D0____________ |   ret NC

 D0B8:


 D0C8h とは D000h+40*5 のことで、D3E7h は D000h+40*25-1 です。


 LDIR も DJNZ も使えないので、ゴテっとしたコードになります。なんとかならないかな。


 ***


 バグがありました。


 下記の C0h と C8h はキー入力できない文字です。


   誤)D0A3: 11C8D0 | ld DE,D0C8h

   誤)D0A6: 21C8C0 | ld HL,C0C8h


 とりあえず修正(2026.5.11)。


 Cxxxh でなくて Bxxxh を使えばよいのだけど。全体的に見直したい感じです。無駄なコードが散在している。個々の処理をサブルーチンにするのを止めると CALL と RET を省けるけど。4バイト節約できるけど。どうしよう。


 ***


 最後はメインループです。1行目に置きます。


 D000: 0E18__________ |_ml: ld C,18h

 D002: 0627__________ |_yl: ld B,27h

 D004: AFD428D0D478D0 |_xl: xor A; call NC,_ev; call NC,_nx

 D00B: 05AFB8C204D0__ |   dec B; xor A; cp B; JP NZ,_xl

 D011: 0D3E04B9C202D0 |   dec C; ld A,04h; cp C; JP NZ,_yl

 D018: AFD4A0D0______ |   xor A; call NC,_cy

 D01C: D41E00C200D0__ |   call NC,_001Eh; jp NZ,_ml

 D022: C38200________ |   jp _0082h

 D025:


 001Eh のルーチンは SHIFT+Break の検出です。押下されていたら Zf をセットします。


 _nx はリターン前に XOR A しているから D00Ch 番地の XOR A と CP B は不要ね。画面の左端が欠けているから、ここは DEC A するとよい。Cf に注意。


 ***

 **

 *


 バージョンアップしました(2026.5.13)。コードは次の通りです。無駄を省いて整理したら4行に収まりました!


 0E18 0627 AFD4 25D0 D470 D005 3DB8 C204 D00D 3E03

 B9C2 02D0 AFD4 8CD0 D41E 00C2 00D0 C382 0057 04D4

 49D0 0CD4 49D0 05D4 49D0 05D4 49D0 0DD4 49D0 0DD4

 49D0 04D4 49D0 04D4 49D0 0C05 D060 69AF 3DBC 2002

 2627 3E28 BC20 0226 003E 03BD 2002 2E18 3E19 BD20

 022E 04AF D4B4 0FB6 2801 14D0 6069 D4B4 0F5E 7CD6

 2067 1515 2003 7318 0915 2004 3655 1802 3600 AFD0

 0148 0311 A0D0 21A0 B07E 1223 130B 78B1 C295 D0D0


 *


 バージョンアップしました(2026.5.17)。コードに対応する文字は次の通りです。3行半より短くできました。...、この辺が限界かも。

 挿絵(By みてみん)

 クリックすると綺麗な図が見れるよ。


 *


 バージョンアップしました(2026.5.19)。3と1/3行になりました。3行に納めることができたら良いのだけど。...、これ以上は無理よ。

 挿絵(By みてみん)


 *


 バージョンアップしました(2026.5.29)。3行と5バイトになりました。...、これ以上は、もう鼻血も出ないよ。

 挿絵(By みてみん)


 *


 バージョンアップしました(2026.5.31)。3行に収まりました。やったね!

 挿絵(By みてみん)


 このバージョンからプログラムを書く場所を変更しました。


     1〜21:ゲーム画面

    22〜24:プログラム領域

       25:空白


 前バージョンまでは画面の1行目からプログラムを書きましたが、このバージョンは下の方(22〜24行目)にプログラムを書きます。

 ゲームの画面は1〜21行目になります。


 プログラムを開始するには GOTO$D348 とします。D348h = D000h + 21*40 です。


 25行目を空白にしたのは、...リターン・キー等を押下したりして、間違って画面がスクロールアップするのを避けるためです。


 プログラムを別の場所(23〜25行目など)に移動したい場合は、JP NZ,nm 命令などで(絶対)番地を参照している部分を書き換えます。4箇所あります。

 普通は、ORG 指定によるプログラムを配置する番地を修正してアセンブルし直すのですけど。ハンドアセンブルですから機械語を直接修正してね。


 *

 **

 ***


 さほどの無理をすることなく、使えない命令を回避してプログラミングでき、...た、かな。うーん、どうかな? ソレらしく動くプログラムが出来たと思うけど。


 プログラムを見直してみると無駄なコードが目立つ。でもこれは使えない命令があるためではなくて、主に技量不足のため。...、orz。


 機械語はお気楽スクリプトとは違うのです。Z80の機械語のことを一通りは分かったつもりでしたが、書いてみたら違いを実感しました。


 画面プログラミングの制約としては、マイナス方向の番地を記述できないからループの記述で JR や DJNZ が使えない点、HL を使用するROMルーチンの前後で PUSH HL / POP HL が使えない点、LDIR が使えない点かな。LDIR をどうしても使いたいときは自己書換することになるよ。

 あと、CALL NC,nm と RET NC のための XOR A は、Cf の状態を分析したら不要な場合が多かったです。意外と平気。

 ビット操作命令 BIT, SET, RES が全て使えないのは(ライフゲームでは未使用でしたが)辛いときがあるかも。


 ***


 間違いの指摘とか疑問とか、ご意見・ご感想とかありましたら、どうぞ感想欄に!


 ***

2026.5.11 デバックと推敲

2026.5.12 推敲と加筆

2026.5.13 バージョンアップ!

2026.5.17 推敲とバージョンアップ!

2026.5.19 微推敲とバージョンアップ

2026.5.23 微推敲

2026.5.29 バージョンアップ

2026.5.31 推敲とバージョンアップ!

2026.7.16 微推敲



評価をするにはログインしてください。
ブックマークに追加
ブックマーク機能を使うにはログインしてください。
― 新着の感想 ―
このエピソードに感想はまだ書かれていません。
感想一覧
+注意+

特に記載なき場合、掲載されている作品はすべてフィクションであり実在の人物・団体等とは一切関係ありません。
特に記載なき場合、掲載されている作品の著作権は作者にあります(一部作品除く)。
作者以外の方による作品の引用を超える無断転載は禁止しており、行った場合、著作権法の違反となります。