Z80 でプログラムを組んでいると、Acc の内容によって、それぞれの処理にジャンプしたいことが割と多くあります。例えば、Acc が 0,1,2... という場合に、それぞれ対応したラベル L0, L1, L2... にジャンプしたい場合です。今回はその実装方法について解説していきたいと思います。

下記の説明はすべて Acc が 0-7 の場合を想定しています。


  • テーブル参照
Acc の値に応じてテーブルを用意する方法です。最もオーソドックスな手法だと思います。

add a, a ld e, a ld d, 0 ld hl, jumtbl add hl, de ld a, (hl) inc hl ld h, (hl) ld l, a jp (hl) L0: ret L1: ret L2: ret L3: ret L4: ret L5: ret L6: ret L7: ret jumtbl dw L0, L1, L2, L3, L4, L5, L6, L7
64clkと余り速くはないですが、テーブルを書き換えるだけで飛び先を簡単に指定できるメリットは大きいと思います。あと、実はこれ、256バイト境界内にテーブルがある前提とすると、実行速度を稼ぐことが出来ます。AILZ80ASM アセンブラには、その支援マクロがありますので使ってみます。

add a, a add a, jumtbl.@L ld l, a ld h, jumtbl.@H ld a, (hl) inc l ld h, (hl) ld l, a jp (hl) L0: ret L1: ret L2: ret L3: ret L4: ret L5: ret L6: ret L7: ret CHECK ALIGN 256 jumtbl dw L0, L1, L2, L3, L4, L5, L6, L7 ENDC
CHECK ALIGN 256が、ENDC の範囲まで上位バイトが変化しない256境界範囲内かどうかを保証するマクロです。もし、テーブル範囲が256境界を跨いでしまった場合は、エラー表示されてアセンブルが停止します。これで先程 64clkだった処理が 48clkと大幅に処理速度が上がっています。

なお、HL の値を保存しながらジャンプしたい場合はスタックを利用します。

ex de, hl add a, a add a, jumtbl.@L ld l, a ld h, jumtbl.@H ld a, (hl) inc l ld h, (hl) ld l, a push hl ex de, hl ret
※仕事とかで袖まくり…するもすぐにずり下がり、ええい鬱陶しいと思ったことはないですか。よく、事務の人が腕にしてるのをみたりするコレ、案外通常のPC作業でも便利だったりします。

  • ビット分解
下のビットから分解してジャンプしていく方法です。

rra jr c, b1 rra jr c, b10 rra jr c, L4 L0: ret ; Acc = 0 L4: ret ; Acc = 4 b10: rra jr c, L6 L2: ret ; Acc = 2 L6: ret ; Acc = 6 b1: rra jr c, b11 rra jr c, L5 L1: ret ; Acc = 1 L5: ret ; Acc = 5 b11: rra jr c, L7 L3: ret ; Acc = 3 L7: ret ; Acc = 7
メリットは凡そ 48clkと比較的分岐が速い事です。デメリットは拡張性が低い事です。8分岐でこの複雑さです。0-15 の場合は、このソースコードは2倍の長さになります。そのうち、相対ジャンプが届かなくなりますから、jr ではなく jp を使い始めてデータサイズまで大きくなっていきます。そのため、4分岐くらいであれば、この方法の採用を検討しても良いと思いますが、それ以上では別の実装方法を考えたほうが良いと思います。

※2024/02/24 20:45追記
ちなみに Acc の格納されている値が不連続な 1,9,11,13,15,19,23 のような数値の条件分岐は、以下のように実装すると良いです。この場合はビットで見ずに値で判別するようにします。

cp 13 jr z, L13 jr c, .low cp 19 jr z, L19 jr c, L15 L23 ret ; Acc = 23 L13: ret ; Acc = 13 L15: ret ; Acc = 15 L19: ret ; Acc = 19 .low cp 9 jr z, L9 jr c, L1 L11: ret ; Acc = 11 L1: ret ; Acc = 1 L9: ret ; Acc = 9

考えはクイックソートアルゴリズムと同じです。値の範囲の真ん中と比較して、同じか小さいか大きいかの3分岐とします。分岐後は凡そ候補数が半分になっているので、またその中心の値と比較して同様の手順で追いかけていきます。これぞ Z80 の switch case の処理ですねー


  • 相対ジャンプ
相対ジャンプの特性を利用します。この方法はプログラム書き換えを使いますので、ROM媒体では使用できません。その点はご注意ください。

add a, a ld (.jmp), a .jmp equ $ + 1 jr $ jr L0 jr L1 jr L2 jr L3 jr L4 jr L5 jr L6 jr L7 L0: ret L1: ret L2: ret L3: ret L4: ret L5: ret L6: ret L7: ret
相対ジャンプは、現在のプログラムカウンタ PC の値に、1バイトのオフセットを加算してジャンプする命令です。jr 命令の実行時、PC は次の命令のアドレスに移動していますから $1800(JR offset $00)は何も動作しません。Acc が 1 だった場合は、プログラム書き換えで $1802(JR offset $02)となりますので、jr L0 の命令の次の命令を実行することになります。

相対ジャンプ2連というと遅いイメージはありますが、この実装方法であれば 41clk で分岐が出来ます。なんと今までで一番速いんです。欠点は相対ジャンプが届かなくなる事があるのと、なんといってもプログラム書き換え前提ですので、使用できる環境に制限がある事でしょうか。add a, a の2倍の処理を3倍にして、jr の羅列を jp に変える事で、その欠点は補えますが、今度は一気に消費メモリが増えてしまいます…

なお、このサンプルですが、たった2バイトですがサイズを小さくすることが出来ます。

add a, a ld (.jmp), a .jmp equ $ + 1 jr $ jr L0 jr L1 jr L2 jr L3 jr L4 jr L5 jr L6 L7: ret L0: ret L1: ret L2: ret L3: ret L4: ret L5: ret L6: ret
L7 へのジャンプだけ、直接そこに飛ぶようにしただけです。Z80 のメモリ空間は 64KB しかないため、こういうたった2バイトですら、毎回ケチる事で総合的なメモリを減らすことが大切になるときもあります。

以上、何かの参考になれば幸いです。

※ 困った時にシュッと一吹きで、滑りが良くなるまるで魔法のスプレー。シリコンなので、素材的にデリケートな部分にも使用できると思います。取り置きに如何でしょうか?