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レジスタのアドレスにジャンプする
「うーん・・?」という声が聞こえてきました。
今回、初めて登場した命令はADCとJP (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)
セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。