見出し画像

Z80:分岐をまとめるテクニック

師走ですね。忙しいです。
今回はIF分岐をテーブルにまとめてしまうロジックについて紹介します。
まあどちらかというとMSXに限らずのZ80プログラミングテクニックですね。あわせてCフラグを使った加算ADC命令と、JP (HL)についても説明します。

CPを羅列するとどーなるか

仮にジョイスティックの状態を判定するロジックをCPとJPを使って判定すると次のような感じになります。

CALL GTSTCK
OR A ; 押されてない?
JP Z,PRCEND
CP 1 ; 上が押された?
JP Z,UP
CP 2 ; 右上が押された?
JP Z,UPRIGHT
CP 3 ; 右が押された?
JP Z,RIGHT
CP 4 ; 右下が押された?
JP Z,DOWNRIGHT
CP 5 ; 下が押された?
JP Z,DOWN
CP 6 ; 左下が押された?
JP Z,DOWNLEFT
CP 7 ; 左が押された?
JP Z,LEFT
CP 8 ; 左上が押された?
JP Z,UPLEFT

これはパッとみわかりやすいコードですが、左上が押されたかどうかの判定まで
CP n
JP Z, nn
を何回も繰り返すことになっていまいます。これはステート数の無駄遣いです。ジョイスティックの数程度であれば特に問題ないですが、これが0から99までの数値が変数に設定されていてそれぞれの数値ごとに判定するとなると100回の無駄判定がステート数として発生する原因になります。
100回もCPのコードを羅列するなんて愚かな行為です。。。
ステート数のみならず容量の無駄遣いでもあります。
今回はその無駄を取り除くロジックの説明です。

かなり前の記事で同じことを説明していましたが、今回はその部分にしぼってより詳細に解説します。

ジョイスティックの値で各処理にジャンプするコード

処理(ステート数)を無駄遣いしないためのソリューションとしては以下のようなコードになります。このコードであれば条件分岐の対象がどんな値でも同じステート数で動作するようになります。

; ジョイスティックが押されたときの
; ジャンプ先アドレス
DIRPRC:
DEFW PRCEND,UP,UPRIGHT,RIGHT,DOWNRIGHT,DOWN,DOWNLEFT,LEFT,UPLEFT

; 分岐処理(ジョイスティックの値によって分岐する)
XOR A         
CALL GTSTCK   
LD  BC,DIRPRC 
ADD  A,A        
ADD  A,C        
LD   C,A        
ADC  A,B        
SUB  C
LD   B,A
LD   A,(BC)
LD   L,A
INC  BC
LD   A,(BC)
LD   H,A   
JP  (HL)        

; ジョイスティックの方向なし
PRCEND:

; 上方向が押された
UP:
; 処理
JP PRCEND

; 右上方向が押された
UPRIGHT:
; 処理
JP PRCEND

; 右方向が押された
RIGHT:
; 処理
JP PRCEND

; 右下方向が押された
DOWNRIGHT:
; 処理
JP PRCEND

; 下方向が押された
DOWN:
; 処理
JP PRCEND

; 左下方向が押された
DOWNLEFT:
; 処理
JP PRCEND

; 左方向が押された
LEFT:
; 処理
JP PRCEND

; 左上方向が押された
UPLEFT:
; 処理
JP PRCEND

処理解説

XOR AからJP (HL)までが主処理です。DIRPRCに書かれてあるラベルが各方向に対応した処理になるって感じですね。
ではどんな処理をしているのか各行ごとに説明します。
はい、こんな感じ。

XOR A
→Aレジスタをゼロクリアする

CALL GTSTCK
→BIOSのGTSTCKを呼び出す(押された方向の値がAレジスタに格納される)

LD  BC,DIRPRC
→DIRPRCのアドレスをBCレジスタにセット

ADD  A,A
→Aの値を2倍する

ADD  A,C
→Aレジスタの値をCレジスタにセット
 繰り上がりが発生したらCフラグに1がセットされる

LD   C,A
→Aレジスタの値をCレジスタにセットする

ADC  A,B
→AレジスタにBレジスタの値とCフラグの値とを加算する

SUB  C
→Aレジスタの値からCレジスタの値を減算する

LD   B,A
→BレジスタにAレジスタの値をセットする

LD   A,(BC)
→AレジスタにBCレジスタが指すアドレスの値をセットする

LD   L,A
→LレジスタにAレジスタが指すアドレスの値をセットする

INC BC
→BCレジスタの値をインクリメントする

LD   A,(BC)
→AレジスタにBCレジスタが指すアドレスの値をセットする

LD   H,A   
→LレジスタにAレジスタの値をセットする

JP  (HL)
→HLレジスタのアドレスにジャンプする

「うーん・・?」という声が聞こえてきました。
今回、初めて登場した命令はADCJP (HL)でしょうか。
*JP (HL)は前にも説明した気がする・・。まあ、いいや。

ADCはAレジスタに他のレジスタを加算する、なおかつCフラグの値も加算する。という命令になっていて、加算したあとの繰り上がり計算で使う命令です。
JP (HL)はHLレジスタに格納されているアドレスにジャンプする命令です。
(HL)なのでHLレジスタに格納されているアドレスに格納されているアドレスと思いがちですが、そうではありません。HLレジスタに格納されているアドレスにジャンプします。Z80のオペランド(命令)の謎仕様です。
コードを見ただけではわかりづらいので、値の例を使ってさらに説明します。はい、こんな感じ。

DIRPRCがD0FFH、PRCENDが8500H、UPが8502Hだとして
ジョイスティックの上方向(1)が押されたと仮定すると以下のようになる
(CALL GTSTCKまでの説明は省略)

※アドレスの内容(リトルエンディアンです)
D0FF : 00 85 ; PRCEND (DIRPRCと同じアドレス)
D101 : 02 85 ; UP

これをもとに処理説明をさらに解説

LD BC, DIRPRC
→ BCレジスタにはD0FFHがセットされる

ADD A,A
→ A=A+1で、02Hがセットされる

ADD A,C
→ A=2+FFHで、01Hがセットされる。
  ただし繰り上がってるのでCフラグには1がセットされる

LD  C,A
→ Cレジスタに01Hがセットされる

ADC  A,B
→ A=D1H+Cフラグ(1)で、D2Hがセットされる

SUB  C
→ A=A-01Hで、AレジスタにはD1Hがセットされる

LD   B,A
→BレジスタにAレジスタの値(D1H)をセットする

この時点でBCレジスタはD101Hがセットされることになる

LD   A,(BC)
→AレジスタにD101Hのアドレスに格納されている値がセットされる

LD   L,A
→LレジスタにAレジスタの値02Hが格納される

INC BC
→BCレジスタの値をインクリメントする

LD   A,(BC)
→AレジスタにD102Hのアドレスに格納されている値がセットされる

LD   H,A   
→HレジスタにAレジスタの値85Hがセットされる

この時点でHLアドレスには8502Hがセットされることになる

JP (HL)
→ 8502Hにジャンプする

DIRPRCがD000Hだとどうなるでしょうか?
はい、こんな感じになります。

DIRPRCがD000H、PRCENDが8500H、UPが8502Hだとして
ジョイスティックの上方向(1)が押されたと仮定すると以下のようになる
(CALL GTSTCKまでの説明は省略)

※アドレスの内容(リトルエンディアンです)
D000 : 00 85 ; PRCEND (DIRPRCと同じアドレス)
D002 : 02 85 ; UP

これをもとに処理説明をさらに解説

LD BC, DIRPRC
→ BCレジスタにはD000Hがセットされる

ADD A,A
→ A=A+1で、02Hがセットされる

ADD A,C
→ A=2+00Hで、02Hがセットされる。
  ただし繰り上がってないのでCフラグには0がセットされる

LD  C,A
→ Cレジスタに02Hがセットされる

ADC  A,B
→ A=A(02H)+B(D0H)+Cフラグ(0)で、D2Hがセットされる

SUB  C
→ A=A(D2H)-C(02H)で、AレジスタにはD0Hがセットされる

LD   B,A
→BレジスタにAレジスタの値(D0H)をセットする

この時点でBCレジスタはD000Hがセットされることになる

LD   A,(BC)
→AレジスタにD000Hのアドレスに格納されている値(02H)がセットされる

LD   L,A
→LレジスタにAレジスタの値02Hが格納される

INC BC
→BCレジスタの値をインクリメントする

LD   A,(BC)
→AレジスタにD001Hのアドレスに格納されている値(85H)がセットされる

LD   H,A   
→HレジスタにAレジスタの値(85H)がセットされる

この時点でHLアドレスには8502Hがセットされることになる

JP (HL)
→ 8502Hにジャンプする

と、ここまででADCの計算結果がどうなるかはだいたい理解できたかと思います。こんな感じでADCを使うと2バイトに限らず複数バイト(65535を超える数)の計算もできるようになるっていう寸法です。

さて、話をもどしましょう。今回の手法を応用すると各フラグの値によって分岐させることも可能になります。

変数の値をつかった分岐

Aレジスタにセットされている0から127までの値でそれぞれに分岐させるようにするには前述したコードを応用すれば良いです。はい、こんな感じです。VALPRCに書かれてあるラベルが各数値に対応した処理になるって感じですね。

; フラグによって処理を行う
; ジャンプ先アドレス
; VARVAL=1ならFLGAPRC
; VARVAL=2ならFLGBPRC
; VARVAL=3ならFLGCPRC
; VARVALの値0-127まで有効

VALPRC:
DEFW PRCEND,VARAPRC,VARBPRC,VARCPRC

; フラグ別の処理を行う
XOR A
LD A,(VARVAL)
LD  BC,DIRPRC
ADD  A,A ; Aレジスタの値を2倍するのでAレジスタが128以上だと正常に動作しないよ!
ADD  A,C
LD   C,A
ADC  A,B
SUB  C
LD   B,A
LD A,(BC)
LD L,A
INC BC
LD A,(BC)
LD H,A
JP  (HL)

; 処理なし
PRCEND:

; FLGVAL=1の処理
FLGAPRC:
; 処理
JP PRCEND

; FLGVAL=2の処理
FLGBPRC:
; 処理
JP PRCEND

; FLGVAL=3の処理
FLGCPRC:
; 処理
JP PRCEND

まとめ

とまあ、こんな感じで条件分岐をおまとめする方法なのですが、ADC使わなければGTSTCK程度の分岐であれば以下のコードでも十分だったりします(もともこもない)
128以上の値に対応するためにはBCも2バイト数値に対応させる必要があります。要は使い分けです。
というか128個を超える分岐ってかなりスパゲティです。
設計に問題があると思います。

; わかりやすいコード
XOR A
CALL GTSTCK
LD  HL,DIRPRC
ADD A,A   ; Aレジスタの値が128だと0になるよ!
LD  B,0
LD  C,A
ADD HL,BC
LD A,(HL)
LD C,A
INC HL
LD A,(HL)
LD B,A
LD H,B
LD L,C 
JP  (HL)

今回の記事で、みなさんのコードにたくさんある
CP n
JP Z, nn

が削減されますように!
では、また!ノシ

追記:BCレジスタを使わない方法

@TKZ80さんからのアドバイスでBCレジスタを使わない方法を教えてもらったので載せておきます。
BCレジスタはいろいろと便利なので使わなくて済むなら使わないほうが良いとのことです。
BレジスタはDJNZとかで使ったりするし、CレジスタはIN/OUT命令で使ったりする。
パズルですね!!

XOR A         
CALL GTSTCK   
LD  HL,DIRPRC 
ADD  A,A        
ADD  A,L        
LD   L,A        
ADC  A,H       
SUB  L
LD   H,A

; ここからポイント
LD  A,(HL) ; HLのアドレスの値(下位バイト)をAレジスタに退避
INC HL     ; HLのアドレスをインクリメント
LD  H,(HL) ; HLのアドレスの値(上位バイト)をHレジスタにセット
LD  L,A    ; 退避していたAレジスタの値をLレジスタにセット

JP  (HL)  


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