6800のCPX命令とエミュレーター(1)(ベーシックマスター開発 その26)

2024/09/01BASICMASTER, 昔のパソコン

開発にはエミュレーターを使っているのだけど、CPX命令の処理が正しくないエミュレーターがあるようだ。そもそも6800のCPX命令は変なのである。

私がBASICMASTERを使っていたころは、世の中のプログラムはCPX命令の後は必ずBEQ/BNEで分岐していた。

BEQは比較の結果が等しい(Z=1)なら分岐し、BNEは等しくなければ(Z=0)分岐する命令である。

BASICMASTERのモニタやBASIC ROMを見ても、CPXの後に来るのは100% BEQかBNE命令である。

私もCPXは(他のフラグも変化するけど)Zフラグだけが使えると思い込んでいた。

そもそもIXレジスタを使うのは、メモリを舐めるループ用のポインタか、ループカウンタとして使う場合であり、前者はCPX、後者はDEXを使ってZフラグと比較すれば用は足りる。

他のフラグの出番はなかった。

IXレジスタ便利

さて、GAMEコンパイラを作っていると単純な代入が多いことに気づく。これはCなどの他の高級言語でも同じ。

例えば、小さな数値(多いのは0や1)の代入、変数のコピー、変数と小さな数値(±1が多い)の加算である。

最近のCPUアーキテクチャーだと小さな数値の設定が優遇されてたり、ゼロレジスタが存在するのはこれが理由である。

例えば Var=1 という代入文を、真面目にAccABを使うようにコンパイルすると 14cycle/10bytes 必要になる。

CLRA/STAA Var/INC A/STAA Var+1 とするトリックはあるけど、それでも14cycle/8bytesである。


	LDAB	#1	; 2 2
	LDAA	#0	; 2 2
	STAB	Var+1	; 5 3
	STAA	Var	; 5 3

IXレジスタを使うと、9cycle/6bytesである。速くて小さい。素晴らしい。


	LDX	#1	; 3 3
	STX	Var	; 6 3

Var = Var+1 の場合、AccABを使うと、22cycle/14bytes。INCBではCarryが立たないのでADDBを使う必要がある。


	LDAB	Var+1	; 4 2
	LDAA	Var	; 4 2
	ADDB	#1	; 2 2
	ADCA	#0	; 2 2
	STAB	Var+1	; 5 3
	STAA	Var	; 5 3

IXを使うと 15cycle/7bytes。やっぱり小さくて速い。+2/-2まではINX/DEXが速いし小さい。


	LDX	Var	; 5 3
	INX		; 4 1
	STX	Var	; 6 3

IXで比較も実行する

どの言語でも、==0、!=0 の比較は多く使われる。CPXの出番だ。

MC6800では、AccABを使った比較は上位・下位バイトを別々に比べないといけないので非常に面倒くさい。

if(Var==0)… をコンパイルするとこうなる。これ、==0 だから4行で済んでいるけど、expr>expr だとSUBB/SUBAも入ってきて長大なプログラムが生成される。

(BNE/BRAで届く保証がない場合は更に奇怪なコードになるが、わかりやすくするためにBNE/BRAを使っている)


	LDAB	Var+1
	BNE	ELSE
	LDAA	Var
	BNE	ELSE
THEN:	...
	BRA	NEXT
ELSE:	...
NEXT:	...

IXだとこうなる。少し嬉しい。代入文で使った直後ならZフラグが残っているので、LDXも省略できる。


	LDX	Var
	BNE	ELSE
THEN:	...
	BRA	NEXT
ELSE:	...
NEXT:	...


Nフラグも使えるのでは?

2バイトの数値であっても、正(0以上)か負(0未満)であるかは、最上位ビットだけを見ればわかる。0かどうかは2バイト全部見ないといけないが、正負は1バイトで良い。

すると、Var>=0 はこう書ける。これは素晴らしい。A>B も A-B>0 と引き算にすれば、引いてみて最上位ビットを見るだけで良い

オーバーフローがあるので、Vも見ないといけません。BPL/BMIじゃなくてBGE/BLTが必要。TST命令の場合はBPLで良い。

	TST	Var
	BMI	ELSE
THEN:	...
	BRA	NEXT
ELSE:	...
NEXT:	...

A>=10 のような場合も、A>9と置き換えればBMIBGT一発で分岐できる(もちろん整数型に限るけど、GAMEは整数型しかないのでこれで良い)。

では、CPXでNフラグを見るのはどうだろう?

代入演算などでIXレジスタに変数値が残っていることがある。これを流用できると嬉しい。

そもそも、TST Var は1命令で正負を判断できて便利なんだけど、実行に6cycleも必要である。AccABを壊して良いなら、LDAB Varとした方が4cycleなので2cycle速い。

(TST命令ではダイレクトページアクセスが無いのも痛いけど、その話はまた別の機会に)

IXを使うということは、以下のように書けるかということである(課題その1)


	CPX	#0
	BMI	ELSE

定数や変数の比較はどうだろうか(課題その2)


	CPX	#const
	BMI	ELSE


CPX命令のフラグ変化はマニュアルにはどう書かれているか

ベーシックマスターL2II(MB-6881)のマニュアルにはこう書かれている。Jr.(MB-6885)のマニュアルも同じ。

上位・下位バイトを別々に引き算していることはわかる。ではNはどうなるのか?

ベーシックマスターレベル2II 取扱説明書 P.128

Nの説明はこうなってる。“上位8bitからの”、って何よ?

ベーシックマスターレベル2II 取扱説明書 P.123

これは、Motrola の M6800 Microprocessor Applications Manual だとこう書かれている。なるほどわからん。

(Bit N) Test: Sign bit of most significant (MS) byte of result = I?

M6800 Microprocessor Applications Manual 1-19 Figure 1-3-1

Motorola M6800 Programming Reference Manual がわかりやすい。結局、N・Vフラグは、上位バイト同士の演算結果だけを反映していて、下位バイトの演算は無視される。

たぶん、下位バイトからのキャリーを無視しているのだろう。これはMC6800の仕組み上仕方ないと思う(後述)。

Motorola M6800 Programming Reference Manual A-33

6800 Assembly Language Programming だと例が挙げられている。Zは上位下位両方見るけど、NとVは上位しか見ない。この本はN,Vの代わりにS,Oを使っているんだけど、なぜだろう?

6800 Assembly Language Programming 3-50


CPX後のN/Vフラグは使えるのか?

下位からのキャリーを見ていないので、下位が0以外の数値との比較は不正確になる。わかりやすい例。これ、CPX #0でもCPX #1でもCPX#255でもN,Vフラグの変化は同じ。


	LDX	#0
	CPX	#1

CPX #0, CPX #256, CPX #512… なら使える。一番使い勝手が良いのは CPX #0 かな。LDX/STXでも同様にフラグが立つので、正負の判断だけならIXレジスタも使えそうである。

だが、しかし…

次回に続く