| JP (BC)を実現する |
PUSH BC
RET ; 11+10clk
JP (HL) の4clk や EX DE,HL | JP (HL) の8clkに比べるとがくっと遅くなるが、他のレジスタを破壊してはいけない場合にはこれしかない。HLレジスタを破壊してもよいなら、素直にBCレジスタをHLレジスタに転送してからJP (HL) すれば12clkで済む。
CALL _CLHL
[処理]
;
_CLHL: JP (HL) ; 17+4clk
普通のCALL命令に1クッション加わるだけ。上のJP (xx)と組み合わせて、CALL (xx)をも実現できることは言うまでもない。
| LD HL,PCを実現する |
ORG 0038H
POP HL
JP (HL) ; HL=next PC
;
ORG [任意]
[処理]
RST 38H ; 11+14clk
[処理] ; HL=PC
CALL命令でも同様の事はできるが、当然next PCを取得するモジュールは自身に組み込めない。組み込んでしまうと、CALL先アドレスが確定した時点でリロケータブルではなくなってしまうので、意味がない。
| 16ビット相対ジャンプを実現する |
ORG 0030H
POP HL
LD E,(HL)
INC HL
LD D,(HL)
INC HL
; PUSH HL
ADD HL,DE
JP (HL) ; HL=next PC
;
ORG [任意]
[処理]
RST 30H ; 11+51clk
DW [飛び先]-xxx
xxx:
いわゆる8086のnearジャンプと同じである。実現シリーズの中で一番遅い上に、一番使い道がないであろう事は想像に難くない。
| HL > DE ならcf=1 とする |
SCF
SBC HL,DE
CCF
ADD HL,xxが1バイトなのに、SBC HL,xxが(SUB HL,xxとして使う訳だが)実質3バイトになってしまうあたりがZ80追加命令の悲しさ。
LD DE,-n
ADD HL,DE
とすればSUB HL,nが実現する。つまりインデックスレジスタでも減算が可能になる。これらの場合、cfを始めとする全てのフラグが、減算結果としてはでたらめに変化するので注意。
| HL と DE を比較する |
PUSH HL
OR A
SBC HL,DE
POP HL ; 40clk, 5bytes
などとしなければならず、冗長な気がしなくもない。そこで、
LD A,L
SUB E
LD A,H
SBC A,D ; 16clk, 4bytes
としてみる。さすが8ビットプロセッサ、Aレジスタを破壊するが上の倍以上速くなる。おまけに被減算ペアレジスタを選ばない。但し、比較結果として正しく変更されるのはcfのみだ。そこで、
LD A,H
SUB D
JR NZ,_S1
LD A,L
SUB E
_S1: ; 20/23clk 6bytes
これで_S1:の時点でzf(ゼロフラグ)とcfが正しく変更される。上位8ビットと下位8ビットの評価順が逆になっている点に注意。
OR A
SBC HL,DE
ADD HL,DE ; 30clk 4bytes
cfのみADD HL,xx命令の影響を受けるが、必ず直前のSBC HL,xx命令と同じ変化をするのでめでたくsf(サインフラグ),zf,cfが変更される。最近の(16ビットALUを持つ)高速互換プロセッサの場合はこれがお勧め。
| ADD HL,Aを実現する |
ADD A,L
LD L,A
ADC A,H
SUB L
LD H,A ; 20clk
BC、DEレジスタでも同様。インデックスレジスタでは、普通に16ビットADD命令を使う方が速い。
ADD A,L
LD L,A
JR NC,_Z1
INC H ; 20/19clk
_Z1:
とすればバイト数は変わらないが、命令数が少ないので気持ち早くなる。分岐が嫌いな人は素直に上を使う事。
| EXTS Aを実現する |
LD L,A
RLCA
SBC A,A ; sign expand
LD H,A
要は拡張元ビットをcfに代入するだけ。
| パック値の2分割 |
LD L,A
AND 0F0H ; mask value#1
LD H,A ; upper 4bits
LD A,L
AND 0FH ; mask value#2(complement value#1)
LD L,A ; lower 4bits
などとするが、途中をちょっとアレして
LD L,A
AND 0F0H
LD H,A ; upper 4bits
XOR L
LD L,A ; lower 4bits
とすれば、2バイトの短縮になる。マスク値#2の代わりに摘出値#1のNOT値を使うというだけ。3分割以上やビットフィールドに隙間がある(2つのAND値の和がFFhでない)場合には使えない。
| NEG HLを実現する |
LD A,L
CPL
LD L,A
LD A,H
CPL
LD H,A
INC HL ; 30clk
最後のINC HLを取れば、CPL HL(NOT HL)になる。
1) 素直に減算する。
EX DE,HL
XOR A ; cf=0
LD H,A
LD L,A
SBC HL,DE ; 31clk
cf=0が確定しているなら、中3行はLD HL,0とした方が何かとよい。
2) 8ビット演算に分解する。
XOR A
SUB L
LD L,A
SBC A,H
SUB L
LD H,A ; 24clk
Aレジスタは破壊され見通しも悪くなるが、こちらはレジスタを選ばない。
| ペアレジスタのカウンタ値を8ビットレジスタに分割する |
DEC HL
INC H
INC L
Z80はペアレジスタINC/DECではフラグが一切変化しないので、カウンタとして使用する場合には、毎度
xxx:
[処理]
DEC HL
LD A,H ; HL=0?
OR L
JP NZ,xxx ; 24clk
てな処理を行わないといけないが、上の処理でこれを8ビットカウンタの二重ループに変形すると、ループ256回に付き(24-14)*256-16=2544clk速くなる。
xxx:
[処理]
DEC L
JP NZ,xxx ; 14clk
DEC H
JR NZ,xxx ; 30clk
この変形により、カウンタを任意の2つの8ビットレジスタに振り分ける事もできる。例えば下位のカウンタをBレジスタに代入してDJNZで回せば、ループ256回に付き更に261clk速くなる。ついでに上位カウンタにインデックスレジスタ(の8ビット分)を使っても、同4clkしか遅くならない。Aレジスタも(カウンタとして使用しない限り)破壊しないので、レジスタのやりくりが厳しい時にも便利。
| Aレジスタの中央付近ビットを手早くcfに押し出す |
LD B,8
xxx: IN A,(device)
RLCA
RLCA
RLCA
RLCA ; cf=device:bit4
RL C
DJNZ xxx
よほどの事がない限りこれで充分なのだが、最短命令とはいえ同じものが4つも並ぶのは我慢ならーんという場合は、ある程度条件が揃えばこのように書き換える事ができる。
LD B,8
xxx: IN A,(device)
OR 11101111b ; bit4 get
ADD A,B ; B=内容が1〜16であることが判っているレジスタ
RL C
DJNZ xxx
ソース2行、1バイト5clkの短縮だが、OR値を8ビットレジスタに代入しておけば、さらに1バイト3clk短縮できる。ビット4もしくはビット3限定の手法で、ホントに気持ちの問題。ビット3の抽出時は、加算レジスタの許容範囲が1〜8になる。
LD B,8
xxx: IN A,(device)
AND 00010000b ; bit4 get
CP B ; B=内容が1〜16であることが判っているレジスタ
RL C
DJNZ xxx
ORをANDに、ADDをSUB(CP)にと逆にしただけ。
| 1バイトを16進アスキーコード2文字に変換する |
LD A,(DE)
RRCA
RRCA
RRCA
RRCA
CALL _BTOH
LD A,(DE)
INC DE
_BTOH: OR 0F0H ; low nibble convert
DAA
CP 5FH
SBC A,1FH ;
LD (HL),A
INC HL
RET
このルーチンの目玉は _BTOH: からの 7バイト。Z80がパズルとはよく言ったものだが、これはZ80が分岐を苦手にしている為の副産物で、すき好んでこういうコードを書いている訳ではない(と思う)。
RLD
CALL _BTOH
RLD
INC HL
_BTOH: OR 0F0H ; low nibble convert
DAA
CP 5FH
SBC A,1FH ;
LD (DE),A
INC DE
RET
| シフトJISコードをJISコードに変換する |
; IN HL = shift JIS code
; OUT HL = JIS code
;
LD A,H
AND 0BFH
ADD A,8FH
ADC A,A
LD H,A
LD A,L
OR A
JP P,_USKP
CP 9FH
JR C,_USKP
SUB 5FH ; SUB 7Eh
INC H
_USKP: SBC A,1FH
LD L,A
これもある意味パズル然としていなくもないが、変換式を比較的素直にコード化してある。後半の条件分岐命令連続部分がちょっと美しくないが、これ以上変形させるとかえって遅くなってしまいそうだ。
| 8×8ビットの符号無し乗算 |
; HL=AxB
LD HL,0
LD C,L ; C=0
JR _LMS1
_LML1: ADD HL,BC
_LMS1: SRL B ; SRA B なら符号付き
RR C
ADD A,A
JR C,_LML1
JR NZ,_LMS1
ループチェックに乗数の一方のビットマップをそのまま使っている為、破壊レジスタが少ないのが特徴。所要クロックには60〜370clkと幅があり、Aレジスタ(被乗算値)のLSBから0が連続している程速くなる。
| 16×16ビット符号無し乗算(積32ビット)(Z80) |
; IN HL = mult value1
; DE = mult value2
; OUT DE:HL answer
;
LD A,H
LD C,L
LD HL,0
LD B,16
_LM32: RRA
RR C
JR NC,_LS32
ADD HL,DE
_LS32: SRL H
RR L
DJNZ _LM32
RRA
RR C
LD E,C
LD D,A
EX DE,HL ; 25bytes
2進数の筆算タイプという奴。
| 16×16ビット符号無し乗算(積32ビット)(Z180) |
; IN HL = mult value1
; DE = mult value2
; OUT DE:HL answer
;
XOR A
LD B,H
LD C,D
PUSH BC
LD D,B
LD H,C
LD B,E
LD C,L
MLT BC ; L x E
MLT HL ; L x D
MLT DE ; H x E
ADD HL,DE
LD E,C
LD C,B
LD B,A
ADC A,B ; get cf
ADD HL,BC
ADC A,B ; get cf
LD D,L
POP BC
MLT BC ; H x D
LD L,H
LD H,A
ADD HL,BC
EX DE,HL ; 29bytes
MLT命令は、入力値を全部破壊してしまうので、値のリロードにPUSH/POPを使っている。それでも170clk程度しか掛からず、同一クロック動作Z80比4〜5倍の速度だ。MLT命令様々である。
| 16×16ビット符号無し乗算(積32ビット)(i8080/i8085/Z80) |
; IN HL = mult value1
; DE = mult value2
; OUT DE:HL answer
;
DATA0 EQU 0
DATA1 EQU DATA0+1
LD A,E
OUT (DATA0),A
LD A,L
OUT (DATA1),A ; L x E
IN A,(DATA0)
LD C,A
IN A,(DATA1)
LD L,A
LD A,D
OUT (DATA0),A ; L x D
IN A,(DATA0)
ADD A,L
LD L,A
IN A,(DATA1)
ADC A,0
LD B,A
LD A,E
OUT (DATA0),A
LD A,H
OUT (DATA1),A ; H x E
LD E,C
IN A,(DATA0)
LD C,A
IN A,(DATA1)
LD H,A
ADD HL,BC
LD A,D
OUT (DATA0),A ; H x D
LD D,L
IN A,(DATA0)
LD C,A
IN A,(DATA1)
LD B,A
LD L,H
RLA ; get cf
AND 1
LD H,A
ADD HL,BC
EX DE,HL ; 55bytes
それぞれ8ビットの乗数と被乗数をOUTすると、16ビットの積をINできると想定した例。約270clkで積を獲得できる。しかし見通し悪いねこりゃ。
| 16÷16ビット符号無し除算(商16ビット) |
; IN HL = divide value
; BC = by value
; OUT DE = answer
; HL = remain
;
LD DE,0
LD A,B
OR C
JR Z,_DVS1 ; /0 skip
LD A,1
BIT 7,B
JR NZ,_DVA1
_DVL1: INC A
SLA C
RL B
JP P,_DVL1
_DVA1: SBC HL,BC
JR NC,_DVN1
ADD HL,BC
_DVN1: CCF
RL E
RL D
SRL B
RR C
DEC A
JR NZ,_DVA1
_DVS1:
DEレジスタに関する命令を削除すれば、商を求めずに余だけをHLレジスタに求める本来の動作になるが、応用はきかないと思う。
| プロセッサを判定する |
XOR A
DEC A
DAA
CP 65H
JP Z,P8080
SUB 99H
JR NZ,PZ180 ; if A=F9h then Z180/Z380
OR 40H
DB 0CBH,37H ; SLL A/TSET A
JP M,PZ080 ; if sf=1 then Z80
CP 0FFH
JR Z,PZ280
XOR A
LD C,A
INC A
LD H,A
LD L,A
DB 0EDH,0C9H ; MULUB A,C
DB 0,0
LD A,H
OR L
JR Z,PR800 ; if zf=1 then R800
PZ180:
XOR A
LD HL,PTR01+1
PTR01: DB 0EDH,27H ; ##TRAP/EX A,H/LD HL,(HL)
NOP
OR A
JR NZ,PZ380
ADD A,H
JR NZ,P64180
; end of check(eZ80)
PZ280:
; end of check(Z280)
PR800:
; end of check(R800)
P64180:
; end of check(Z180)
PZ380:
; end of check(Z380)
PZ080:
; end of check(Z80)
P8080:
; end of check(i8080/i8085)
美しくないなぁ。i8080とi8085を判別していないし。i8085にはほぼすべてのセカンドソースで動作する未定義命令が存在するらしいので、どうしても判定が必要ならそれを利用するのも手だろう。