S59/01/22
阿閉雅宏の Z-80まにあくす Stage.01
解説:杉之原名人
 うぁけましておめでとうございます。
 皆さんのマイコンの調子はどうですか?それから浅田さん、結婚のご予定は?(注:'05現在結婚12年目・・・いや、どうでもいいんですけどね・・・)
 ところで、スタックポインタを使用したプログラムを突然にも紹介します。これは SHARP BASIC SP-5030で使われていたものをさらに改良したものです。HLレジスタの内容をテキスト画面上のアドレスDEから、左詰め十進数で表示するものです。
HL = 表示する数値(バイナリ)
DE = 表示するテキストV-RAMアドレス
START:
F5 PUSH AF 全レジスタ退避
C5 PUSH BC
D5 PUSH DE
E5 PUSH HL
5. 06 00 LD B , 0 左詰め表示用フラグクリア
D5 PUSH DE DEをスタックに置く・・・・ここに注目
11 10 27 LD DE , 10000 万の桁表示
CD xx xx CALL SUB
11 8E 03 LD DE , 1000 千の桁表示
CD xx xx CALL SUB
11 64 00 LD DE , 100 百の桁表示
CD xx xx CALL SUB
11 0A 00 LD DE , 10 十の桁表示
CD xx xx CALL SUB
D1 POP DE 画面上のアドレスを取り出す
7D LD A , L 一の桁表示
17. C6 20 ADD A , '0'
12 LD (DE) , A
E1 POP HL 全レジスタ復帰
D1 POP DE
C1 POP BC
F1 POP AF
C9 RET リターン
24.
SUB: 3E FF LD A , $FF Aレジスタには十進数一桁
LOOP: 3C INC A その桁の十進値を求める[※1]
27. A7 AND A
ED 52 SBC HL , DE
29. 30 FA JR NC , LOOP
19 ADD HL , DE HLを正の値に戻す
31. B7 OR A その桁が0でなければSUB1へ
20 03 JR NZ , SUB1
B0 OR B それまでに0でない数字があればリターン
C8 RET Z
35. AF XOR A Aを0に設定する
SUB1:
37. C6 20 ADD A , '0' 数値をASCIIコードに変換
04 INC B フラグを立てる [※2]
D1 POP DE 戻り先アドレス保護のためDEへ退避
40. E3 EX (SP) , HL ここに注目のPUSH DEはここで使用
77 LD (HL) , A 一文字表示
23 INC HL
43. E3 EX (SP) , HL スタックを元に戻す
D5 PUSH DE 戻り先アドレスを元に戻す
C9 RET リターン
 以上がそのプログラムです。少し長いものですが、スタックポインタ(SP)を利用するといろいろと面白いプログラムができるものです。
解説 (杉之原名人)
※1: Z-80には16bitの減算命令がSBCしかない、つまりキャリーを含んだ減算命令しかないため、使用前にAND Aとして、キャリーフラグを0にしている。
※2: Bレジスタはゼロブランキング用のフラグとして使用されている。

27行目付近のループで各桁の十進値を計算している。もし除算命令があれば一発で求められるのだが、残念ながら乗除算命令の無いZ-80では減算を繰り返して求めている。たった6バイトのループだが、最悪10回も繰り返すとなるとバカにならない。Z-80のALU(演算回路)は4ビット長しかないので、このループ内の16ビット減算にはなんと19ステートもかかる。しかし、Z-80用のアルゴリズムとしてはよくできていると思われる。
 このループのこれ以上の最適化は不可能か?と問われると実はそうではない。24行目以降を次のように変更する。

SUB: XOR A ; A = 0
LOOP: ADD A , C ; A = A + 1  Cレジスタにはあらかじめ"1"をロードしておく
SBC HL , DE ; HL = HL - DE - CY (CYは常に0)
JR NC , LOOP ; if(HL < 0) then LOOP
ADD HL , DE ; HL = HL + DE
CP C ; if(A <> 1) then SUB1
JR NZ , SUB1 ;
OR B ; if(B = 0) then return
RET Z
LD A , C ; A = 1
SUB1:
ADD A , '0' - 1 ; キャラクタコードから1を引いた値を加算する
以下、同じ

 このように変更することでループ内の"AND A"が不要となり、1ループあたり4ステート分高速化する。また、25行目の直値ロード命令も3ステート高速化し、サイズも1バイト小さくなる。変更された26・31・35行目もステート数・サイズとも変わらないので、全体として確実に小さく、速くなっているというわけである。
 念のため解説しておくと、キャリーフラグの変化しないINC命令に変えてADD命令を使うことにより、Aレジスタのインクリメントと同時にキャリーフラグを降ろしているというわけ。演算の結果Aレジスタの値が9を超えることがないので、常にキャリーフラグは0となる。これらはCレジスタが空いていることを利用している。

40~43行目で、スタックに積んであった表示アドレスをHLレジスタに入れ、そこへASCIIコード化した数値を書き込み、表示位置を更新してからスタックに戻している。ここが本プログラムのアピールポイントである。組み込みを前提としたROM化を睨んだ手法であるといえる。16ビットCPUでは当たり前になっている「スタックフレーム」をZ-80でやってみました・・・というところか。ちなみにZ-80には16ビットレジスタ間の転送命令がないので、IXやIYを作業用記憶域として利用するメリットがない。

Cレジスタが使用されていないので、5行目を LD BC , $0030 として、Cレジスタに$30(='0')を予め設定しておけば、17行目と37行目を ADD A , C とすることによってプログラムサイズも小さくなり、実行速度も上がる(先の最適化を行わない場合)。さらに言うなら、十の桁を求めるときはサブルーチン"SUB"を使うのではなく、専用のコードを使えばグンと速くなる。十の桁は8ビットレジスタでも計算できるので19ステートかかっていた減算が4ステートで終わってしまう。若干プログラムサイズは増すだろうが、利は大きい。