Z80の割り込み許可状態取得バグとその対策

Z80では、LD A,IまたはLD A,Rを実行するとP/Vフラグに割り込み許可状態が入るのですが、ここで問題があります。

LD A,I(もしくはLD A,R)実行中に割り込みがかかると割り込み禁止になり、その状態がPフラグに取り込まれる、というバグがあります。

こちらはNMOS品で発生し、CMOS品やR800では発生しないとのことですが、MSXだと普通にNMOS品を積んでいる物も結構ありますので、対策は知っておいた方が良さそうです。

特に最近はMSX1用の個人開発ソフトも熱いですし。

 

この問題について、Zilog公式の対応方法があるとの事なので、調べてみました。

 

きっかけはこちらのツイート。

 

ツイートの記事を見ていくとコメント欄に対策方法の紹介がありました。

igarage.cocolog-nifty.com

 

Zilog『Z80 FAMILY DATA BOOK ,JANUARY 1989』

という本に、公式の対策プログラムが紹介されているとの事で、ご紹介したいと思います。

実は以前この話を聞いた事がありまして、その時に考えた対策とほぼ同じでした。
ただ、こちらはもっと合理的なコードで、ルーチンの実行アドレスが00XXh以外であれば短縮できるのは勉強になりました。マシン語は面白いですね。

 

このルーチンの仕様として、

  • P/VフラグではなくCyフラグで取得
  • Iレジスタの値そのものは保証せず、割り込み状態だけ取得

というのは一緒でした。(P/Vフラグだけ変更するのはややこしいので)
割り込み許可フラグを知りたい時以外は、ややこしい事は必要ないですしね。

 

https://github.com/uniskie/MSX_GAME_TOOL/blob/main/GETIFF_ENG.asm

"Zilog Z80 FAMILY DATA BOOK JANUARY 1989" 413ページより

Caution, these routines presume that the service routine for any acceptable interrupt will re-enable interrupts before it terminates. This is almost always the case. They may not return the correct result if an interrupt service routine, which does not re-enable interrupts, is entered after the execution of LD A,I (or LD A,R).
;================================================================
;Listing 1: This rouline may not be loaded in page zero
;  				(not 0000h to 00FFh).
;================================================================
GETIFF:
	XOR A		;C flag, acc. := 0
	PUSH AF		;stack bottom := 00xxh
	POP AF		;Restore SP
	LD A,I		;P flag := IFF2
	RET PE		;Exit if enabled
	DEC SP		;May be disabled.
	DEC SP		;Has stack bottom been
	POP AF		;overwritten ?
	AND A		;lt not 00xxh, INTs were
	RET NZ		;actually enabled.
	SCF		;Otherwise, they really are
	RET		;disabled
	END
;
;================================================================
;Listing 2: This routine may be loaded anywhere in memory.
;================================================================
GETIFF2:
	PUSH HL		;Save HL contents
	XOR A		;C flag, acc. :=0
	LD H,A		;HL:=0000h
	LD L,A
	PUSH HL		;Stack bottom := 0000h
	POP HL		;Restore SP
	LD A,I		;P flag := IFF2
	JP PE,_POPHL	;Exit if isn't enabled
	DEC SP		;May be disabled.
	DEC SP		;Let's see if slack bottom
	POP HL		;is still 0000h.
	LD A,H		;Are any bits set in H
	OR L		;or in L ?
	POP HL		;Restore old contents.
	RET NZ		;HL <> 0: isn't enabled.
	SCF		;Otherwise, they really are
	RET		;disabled.
_POPHL:
	POP HL 		;Exit when P flag is
	RET		;set by LD A,I
	END

参考までに日本語訳を乗せるとこんな感じです。

https://github.com/uniskie/MSX_GAME_TOOL/blob/main/GETIFF.asm

;================================================================
; "Zilog Z80 FAMILY DATA BOOK JANUARY 1989" page 413 より、
;
; LD A,I (or LD A,R) IFF2 flag のバグ対策
;
;注意1:	これらのルーチンは、全ての割り込みのサービスルーチンが、
;	終了する前に割り込みを有効にすることを前提としています。
;	ほとんどの場合、これが当てはまります。
;	LD A,I(またはLD A,R)の実行後に、割り込みを有効にしない
;	割り込みサービスルーチンがあると、正しい結果が返されない
;	場合があります。
;
;注意2:	返り値は割り込み状態の取得のみが保証されます。
;	(Iレジスタの値は保証されません)
;
;注意3:	割り込み状態の判定はP/Vフラグではなく、
;	Cyフラグで返されます。
;================================================================
;
;================================================================
;リスト1:実行アドレスが00xx以外専用
;	(このルーチンは0000h~00FFh以外に置く事)
;
;GETIFF:	割り込み状態をCyフラグで返す
;	
;入力:	なし
;返り値:Cy==0 : EI(割り込み許可)である
;	Cy==1 : DI(割り込み禁止)である
;使用:	AF
;================================================================
GETIFF:
	XOR A		;CyフラグとAレジスタを0にする
	PUSH AF		;スタックに00xxhを書き込む
	POP AF		;SPを戻す->(SP-2)が00xxh
	LD A,I		;P flag = IFF2
	RET PE		;EIなら戻る(Cyは0)
	DEC SP		;DIなら
	DEC SP		;LD A,(SP-2)
	POP AF		;が
	AND A		;0でなければ
	RET NZ		;EIである(Cyは0)
	SCF		;そうでなければDI(Cy=1)
	RET		;返す
	END
;
;================================================================
;リスト2:どこに配置してもOK
;	(ただし、リロケータブルには出来ない)
;
;GETIFF2:	割り込み状態をCyフラグで返す
;
;入力:	なし
;返り値:Cy==0 : EI(割り込み許可)である
;	Cy==1 : DI(割り込み禁止)である
;使用:	AF
;================================================================
GETIFF2:
	PUSH HL		;HLの値をスタックに退避
	XOR A		;CyフラグとAレジスタを0にする
	LD H,A		;HL=0000h
	LD L,A
	PUSH HL		;スタックに0000hを積む
	POP HL		;SPを戻す->(SP-2)が0000h
	LD A,I		;P flag = IFF2
	JP PE,_POPHL	;EIならこのまま終了処理へ飛ぶ
	DEC SP		;DIの場合
	DEC SP		;LD HL,(SP-2)
	POP HL		;が0000hか
	LD A,H		;調べる
	OR L		;
	POP HL		;スタックに退避したHLの値を復帰
	RET NZ		;LD HL,(SP-2)が0000hでなければEI(Cyは0)
	SCF		;そうでないならDI(Cy=1)
	RET		;返す
_POPHL:
	POP HL 		;退避したHLの値を復帰
	RET		;LD A,IでCyは変化しないのでCyは0
	END    

基本的には短い方のGETIFFで事足りそうですね。

STACKに門番を置いて、書き換えられていたら割り込みがあった=割り込み許可状態*1だと判断する方式が使えたのは不幸中の幸いでしょうかね。
これが逆に「間違ってEIが入る」現象だったらどういうコードが必要になったのやら。

 

今回の件で、もう少しちゃんとマシン語を勉強しようと思いました。

*1:割り込み許可=Enable Interrupt=EI / 割り込み禁止=Disable Interrupt=DI