野うさぎ亭

NESプログラミング-テクニック

NESのブートルーチン

NESのパッド入力

パッド1(パッド3含む)とパッド2(パッド4含む)のボタンの押下状態を読み込む最適化されたルーチンを以下に示す。 <JOYPADにパッド1の押下状態、<JOYPAD+1にパッド2の押下状態が格納される。

格納される押下状態は次の通り。

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
A B SELECT START UP DOWN LEFT RIGHT

2017/10/16修正
門真なむ‏(@num_kadoma)さんから、ビットの並びを逆順にした方が合理的との知見を得たので修正。
修正した版では、修正前に対して下記の利点がある

  • 方向キーの状態が下位4bitに格納されるのでジャンプテーブルで分岐させるコストが小さくなる
  • bit <JOYPAD で、AとBボタンの判定を bpl/bmi bvc/bvs で分岐できる
  • ジョイパッド状態更新のルーチンで1byte/2clk短くできる
	; ジョイパッド状態更新
	lda	#$01
	sta	<JOYPAD+1
	sta	$4016
	lsr	a		; same as lda $00
	sta	$4016
.1:
	lda	$4016
	and	#$03
	cmp	#1
	rol	<JOYPAD
	lda	$4017
	and	#$03
	cmp	#1
	rol	<JOYPAD+1
	bcc	.1

ポイントとなるのは2点。

  • ループ前に<JOYPAD+1に$01を格納することで、<JOYPAD+1にパッド2の押下状態をビットシフトで格納するのと同時にループ終了判定も同時に行っている。
  • and #$03 cmp #1 とすることで、パッド1とパッド3(パッド2とパッド4)のボタンの押下状態を同時に判定してCフラグに反映している。

ジャンプテーブル

定番なのはrtsを使用する

	lda	.jmp_h,x	;  3byte  4clk
	pha			;  1byte  3clk
	lda	.jmp_l,x	;  3byte  4clk
	pha			;  1byte  3clk
	rts			;  1byte  6clk
				;  9byte 20clk total

ですが、ゼロページのメモリを使用する

	lda	.jmp_l,x	;  3byte  4clk
	sta	<$00		;  2byte  3clk
	lda	.jmp_h,x	;  3byte  4clk
	sta	<$01		;  2byte  3clk
	jmp	[$0000]		;  3byte  5clk
				; 13byte 19clk total

では、コード量が増えるもののクロック数は1clk早くなります。

ジャンプテーブルを256byte境界に.dwで配置し、先に <$b8 に $6c、<$ba にジャンプテーブルの上位アドレスを格納したうえで

	txa			;  1byte  2clk
	asl	a		;  1byte  2clk
	sta	<$b9		;  2byte  3clk
	jmp	$00b8		;  3byte  3clk
				;         5clk jmp [$nnnn]
				;  7byte 15clk total

のコードを実行するとさらに少ないクロックで実行することができます。

ジャンプテーブルを256byte境界に配置する制約が厳しい場合、oraの追加で多少緩和されます。 例えばジャンプテーブルの要素が8個以下で、ジャンプテーブルを16byte境界に配置したとすると

	txa			;  1byte  2clk
	asl	a		;  1byte  2clk
	ora	#LOW(JmpTable)	;  2byte  2clk
	sta	<$b9		;  2byte  3clk
	jmp	$00b8		;  3byte  3clk
				;         5clk jmp [$nnnn]
				;  9byte 17clk total

のコードで実行可能となります。

サブルーチンテーブル

ジャンプテーブルのサブルーチン版ですが、シンプルなのは、先にゼロページ <$b8 に $4c を格納して起き、

	lda	.fn_l,x		;  3byte  4clk
	sta	<$b9		;  2byte  3clk
	lda	.fn_h,x		;  3byte  4clk
	sta	<$ba		;  2byte  3clk
	jsr	$00b8		;  3byte  6clk
				;         3clk jmp $nnnn
				; 13byte 23clk total

のコードを実行する方法です。$4c は jmp $nnnn の命令ですので $00b8 に一度飛んだ後、書き換えたアドレスに飛びます。 $00b8に飛ぶ時に復帰アドレスをスタックに積んでいるので、書き換えたアドレスの先で rts を実行すれば jsr $00b8 の次の命令に戻ってきます。

ジャンプテーブルと同じく、サブルーチンテーブルを256byte境界に配置し、先に <$b8 に $6c、<$ba にサブルーチンテーブルの上位アドレスを格納したうえで

	txa			;  1byte  2clk
	asl	a		;  1byte  2clk
	sta	<$b9		;  2byte  3clk
	jsr	$00b8		;  3byte  6clk
				;         5clk jmp [$nnnn]
				;  7byte 18clk total

のコードを実行すると少ないクロックで実行することができます。

cフラグ

cフラグは、nフラグやzフラグと比べて状態が変化する命令が比較的少なく、cmp命令やビットシフト命令でレジスタの値を元に状態を更新することができるフラグであるため、うまく使えば、処理の簡素化に役に立ちます。

	cmp	#$nn

と実行した場合、aレジスタの値が$nn以上の場合にcフラグが1になります。

これを踏まえて

	cmp	#$80

と実行した場合、aレジスタの値が$80以上の場合にcフラグが1になります。

さらに見方を変えると、

  • aレジスタの最上位ビット(7bit目)が1の場合にcフラグが1になる。
  • aレジスタの最上位ビットの状態がcフラグに反映される。
  • aレジスタの値が負数($80~$ff)の場合にcフラグが1になる。
  • nフラグの代わりにcフラグに負数の状態を記録する。

ということになります。

応用として

	cmp	#$80
	ror	a

と実行すると、aレジスタの最上位ビットが維持されたまま右シフトすることになり、6502の命令セットに無い「算術右シフト」の動作となります。

	cmp	#$c0
	ror	a

と実行すると、$c0以上の場合だけcフラグが立って右シフトすることになり、これは8bitの値を-64~191の値とみなした上で算術右シフトする動作となります。

次に

	cmp	#$01

と実行した場合、aレジスタの値が$01以上の場合にcフラグが1になります。

見方を変えると、

  • aレジスタの値が$00以外の場合にcフラグが1になる。
  • zフラグの代わりにcフラグにゼロ/非ゼロの状態を記録する。

ということになります。

後で判定するためにnフラグやzフラグの状態をしばらく保持する使い方が可能です。

vフラグ

vフラグは、adc命令、sbc命令、bit命令、clv命令、plp命令でのみ状態が変化します。

役に立たなさそうですが、ほとんどの命令で状態が変化しないため、長期保持可能なフラグとして使用できます。

しかし、vフラグを0にするclv命令はあるもののvフラグを1にする命令がありません。bit命令は指定したアドレスの値の6bit目の状態をvフラグに反映します。この命令を使ってvフラグに1を設定します。

どのアドレスでもよいので、6bit目が1の値を設定しておいてbitで値参照するだけなのですが、お勧めは、ゼロページ <$b8 に $4c か $6c を設定する方法です。

$b8 は、clvの命令です。

Set1:
	.db	$24	; same as bit <$b8
Set0:
	clv

というコードで、ラベルSet0に飛んできた場合、clv命令でvフラグを0に設定した後、後続のコードを実行します。 一方ラベルSet1に飛んできた場合、$24 と clv($b8) が1つの命令として解釈され、bit <$b8 として実行されます。ゼロページ <$b8 に6bit目が1の値を設定しておけば、vフラグを1に設定した後、後続のコードを実行することになります。 vフラグの明示的な設定が最小限のコード量で可能となります。

次に、<$b8 に $4c か $6c を設定する理由ですが、$4c は jmp $nnnn 命令、$6c は jmp [$nnnn] 命令です。続く <$b9 <$ba に飛び先のアドレスを格納して、$00b8に飛ぶことにより、ジャンプテーブル、またはサブルーチンテーブルとして機能します。他の機能が使用するメモリ領域との兼用となりメモリ領域の節約となります。

	beq	.1
	.db	$24	; same as bit <$b8
.1:
	clv

というコードにすると、zフラグの状態をvフラグに設定する動作となります。変化しやすいnフラグやzフラグの状態をvフラグに保存しておくことが可能となります。

16ビット演算

16bitと16bitの加算は次の通り

	lda	<$00	;  2byte  3clk
	clc		;  1byte  2clk
	adc	<$02	;  2byte  3clk
	sta	<$00	;  2byte  3clk
	lda	<$01	;  2byte  3clk
	adc	<$03	;  2byte  3clk
	sta	<$01	;  2byte  3clk
			; 13byte 20clk total

16bitと8bitの加算は、キャリーの加算をbccとincで行う方がコード量、クロック数とも少なく済みます。

	lda	<$00	;  2byte  3clk
	clc		;  1byte  2clk
	adc	<$02	;  2byte  3clk
	sta	<$00	;  2byte  3clk
	bcc	.1	;  2byte  2/3clk
	inc	<$01	;  2byte  5clk
.1:
			; 11byte 18/14clk total

 

	lda	<$00	;  2byte  3clk
	clc		;  1byte  2clk
	adc	<$02	;  2byte  3clk
	sta	<$00	;  2byte  3clk
	lda	<$01	;  2byte  3clk
	adc	#0	;  2byte  2clk
	sta	<$01	;  2byte  3clk
			; 13byte 19clk total

16bitと16bitの減算、16bitと8bitの減算も同様

	lda	<$00	;  2byte  3clk
	sec		:  1byte  2clk
	sbc	<$02	;  2byte  3clk
	sta	<$00	;  2byte  3clk
	lda	<$01	;  2byte  3clk
	sbc	<$03	;  2byte  3clk
	sta	<$01	;  2byte  3clk
			; 13byte 20clk total

 

	lda	<$00	;  2byte  3clk
	sec		;  1byte  2clk
	sbc	<$02	;  2byte  3clk
	sta	<$00	;  2byte  3clk
	bcs	.1	;  2byte  2/3clk
	dec	<$01	;  2byte  5clk
.1:
			; 11byte  18/14clk total

16bitと16bitの比較では上位バイトから比較を行う。
下位バイトの比較では、ボローが発生していたらnフラグに1を設定する処理が必要である。
例えば、$0000 と $00c0 を比較した場合、$0000-$00c0=$ff40で、nフラグに1が設定されるべきであるが、 下位バイトの$00と$c0を比較した直後は$00-$c0=$40でnフラグが0に設定されている。
下記のコードでは、lda #$ff を実行して、nフラグ=1 zフラグ=0 cフラグ=0(cmp <$00の結果を保持)としている。

	lda	<$01	;  2byte 3clk
	cmp	<$03	;  2byte 3clk
	bne	.1	;  2byte 2/3clk
	lda	<$00	;  2byte 3clk
	cmp	<$02	;  2byte 3clk
	bcs	.1	;  2byte 2/3clk
	lda	#$ff	;  2byte 2clk
.1:
			; 14byte 18/17/9clk total

16bitのインクリメントは次の通り。
incではcフラグは変化しないので、インクリメント後が$00(インクリメント前は$ff)であるかで繰り上がりを判定する。

	inc	<$00	;  2byte  5clk
	bne	.1	;  2byte  2/3clk
	inc	<$01	;  2byte  5clk
.1:
			;  6byte 12/8clk total

16bitのデクリメントは次の通り。
decではcフラグは変化しないので、下位バイトをデクリメントする前に$00であるかを判定して繰り下がりを判定する。

	lda	$<00	;  2byte  3clk
	beq	.1	;  2byte  2/3clk
	dec	$<01	;  2byte  5clk
.1:
	dec	$<00	;  2byte  5clk
			;  8byte 15/11clk total

定数テーブル

Int定数テーブルは、$00~$ffの数値が順に並んでいるテーブルです。 さらにテーブルの前に$f0~$ff、後に$00~$0fの数値を並べています。

	.org	$fdf0

	.db	$f0,$f1,$f2,$f3,$f4,$f5,$f6,$f7
	.db	$f8,$f9,$fa,$fb,$fc,$fd,$fe,$ff

	.org	$fe00
Int:
	.db	$00,$01,$02,$03,$04,$05,$06,$07
	.db	$08,$09,$0a,$0b,$0c,$0d,$0e,$0f
	.db	$10,$11,$12,$13,$14,$15,$16,$17
	.db	$18,$19,$1a,$1b,$1c,$1d,$1e,$1f
	.db	$20,$21,$22,$23,$24,$25,$26,$27
	.db	$28,$29,$2a,$2b,$2c,$2d,$2e,$2f
	.db	$30,$31,$32,$33,$34,$35,$36,$37
	.db	$38,$39,$3a,$3b,$3c,$3d,$3e,$3f
	.db	$40,$41,$42,$43,$44,$45,$46,$47
	.db	$48,$49,$4a,$4b,$4c,$4d,$4e,$4f
	.db	$50,$51,$52,$53,$54,$55,$56,$57
	.db	$58,$59,$5a,$5b,$5c,$5d,$5e,$5f
	.db	$60,$61,$62,$63,$64,$65,$66,$67
	.db	$68,$69,$6a,$6b,$6c,$6d,$6e,$6f
	.db	$70,$71,$72,$73,$74,$75,$76,$77
	.db	$78,$79,$7a,$7b,$7c,$7d,$7e,$7f
	.db	$80,$81,$82,$83,$84,$85,$86,$87
	.db	$88,$89,$8a,$8b,$8c,$8d,$8e,$8f
	.db	$90,$91,$92,$93,$94,$95,$96,$97
	.db	$98,$99,$9a,$9b,$9c,$9d,$9e,$9f
	.db	$a0,$a1,$a2,$a3,$a4,$a5,$a6,$a7
	.db	$a8,$a9,$aa,$ab,$ac,$ad,$ae,$af
	.db	$b0,$b1,$b2,$b3,$b4,$b5,$b6,$b7
	.db	$b8,$b9,$ba,$bb,$bc,$bd,$be,$bf
	.db	$c0,$c1,$c2,$c3,$c4,$c5,$c6,$c7
	.db	$c8,$c9,$ca,$cb,$cc,$cd,$ce,$cf
	.db	$d0,$d1,$d2,$d3,$d4,$d5,$d6,$d7
	.db	$d8,$d9,$da,$db,$dc,$dd,$de,$df
	.db	$e0,$e1,$e2,$e3,$e4,$e5,$e6,$e7
	.db	$e8,$e9,$ea,$eb,$ec,$ed,$ee,$ef
	.db	$f0,$f1,$f2,$f3,$f4,$f5,$f6,$f7
	.db	$f8,$f9,$fa,$fb,$fc,$fd,$fe,$ff

	.db	$00,$01,$02,$03,$04,$05,$06,$07
	.db	$08,$09,$0a,$0b,$0c,$0d,$0e,$0f

一見、何の役に立つのかわからない定数テーブルですが、

	ldx	Int,y

と記述することで、yレジスタの値を直接xレジスタに渡すことができます。 さらに

	lda	Int+4,x
	lda	Int-2,y

といった記述をすることで、xレジスタに4を加算した値をaレジスタに代入したり、 yレジスタから2を引いた値をaレジスタに代入することが可能となります。 また、Cフラグを変化させずに加減算した結果を得ることができます。

あと、使用頻度は低いのですが

	bit	Int+$10

と記述することで、aレジスタを破壊せずにaレジスタの値と即値(上記例では$10)とのandの結果をzフラグに反映できます。

Xcn定数テーブルは、上位4bitと下位4bitの値が入れ替わる値のテーブルです。(eXChange Nibble)

	.org	$fc00
Xcn:
	.db	$00,$10,$20,$30,$40,$50,$60,$70
	.db	$80,$90,$a0,$b0,$c0,$d0,$e0,$f0
	.db	$01,$11,$21,$31,$41,$51,$61,$71
	.db	$81,$91,$a1,$b1,$c1,$d1,$e1,$f1
	.db	$02,$12,$22,$32,$42,$52,$62,$72
	.db	$82,$92,$a2,$b2,$c2,$d2,$e2,$f2
	.db	$03,$13,$23,$33,$43,$53,$63,$73
	.db	$83,$93,$a3,$b3,$c3,$d3,$e3,$f3
	.db	$04,$14,$24,$34,$44,$54,$64,$74
	.db	$84,$94,$a4,$b4,$c4,$d4,$e4,$f4
	.db	$05,$15,$25,$35,$45,$55,$65,$75
	.db	$85,$95,$a5,$b5,$c5,$d5,$e5,$f5
	.db	$06,$16,$26,$36,$46,$56,$66,$76
	.db	$86,$96,$a6,$b6,$c6,$d6,$e6,$f6
	.db	$07,$17,$27,$37,$47,$57,$67,$77
	.db	$87,$97,$a7,$b7,$c7,$d7,$e7,$f7
	.db	$08,$18,$28,$38,$48,$58,$68,$78
	.db	$88,$98,$a8,$b8,$c8,$d8,$e8,$f8
	.db	$09,$19,$29,$39,$49,$59,$69,$79
	.db	$89,$99,$a9,$b9,$c9,$d9,$e9,$f9
	.db	$0a,$1a,$2a,$3a,$4a,$5a,$6a,$7a
	.db	$8a,$9a,$aa,$ba,$ca,$da,$ea,$fa
	.db	$0b,$1b,$2b,$3b,$4b,$5b,$6b,$7b
	.db	$8b,$9b,$ab,$bb,$cb,$db,$eb,$fb
	.db	$0c,$1c,$2c,$3c,$4c,$5c,$6c,$7c
	.db	$8c,$9c,$ac,$bc,$cc,$dc,$ec,$fc
	.db	$0d,$1d,$2d,$3d,$4d,$5d,$6d,$7d
	.db	$8d,$9d,$ad,$bd,$cd,$dd,$ed,$fd
	.db	$0e,$1e,$2e,$3e,$4e,$5e,$6e,$7e
	.db	$8e,$9e,$ae,$be,$ce,$de,$ee,$fe
	.db	$0f,$1f,$2f,$3f,$4f,$5f,$6f,$7f
	.db	$8f,$9f,$af,$bf,$cf,$df,$ef,$ff

次の記述により、xレジスタの値の上下4bitを入れ替えた値をaレジスタに代入できます。

	lda	Xcn,x

ただ、単純にaレジスタの上位4bitを取得するだけであれば、

	tax		; 1byte 2clk
	lda	Xcn,x	; 3byte 4clk
	and	#$0f	; 2byte 2clk total:6byte 8clk

	lsr	a	; 1byte 2clk
	lsr	a	; 1byte 2clk
	lsr	a	; 1byte 2clk
	lsr	a	; 1byte 2clk total:4byte 8clk

普通にlsr aを使用した方がコードサイズで有利だったりするので 利用する状況をよく考えなければなりません。

次のような処理では役に立つでしょう。

	tax
	and	#$0f
	sta	<$00
	lda	Xcn,x
	and	#$0f
	sta	<$01


Bit定数テーブルは、ビット位置(0~7)をそのビットが立った値に変換するテーブルです。

	.org	$fbf8
Bit:
	.db	$01,$02,$04,$08,$10,$20,$40,$80

例えば、下記のような記述でxレジスタで指す位置のビットを立てることができます。

	ora	Bit,x

8bit値の合成と分離

$00に格納されている値の下位4bitと$01に格納されている値の上位4bitを合成するには 次のように記述します。

			; 上位4bit         下位4bit
	lda	<$00	; $00の値          $00の値
	eor	<$01	; $00の値^$01の値  $00の値^$01の値
	and	#$0f	; 0                $00の値^$01の値
	eor	<$01	; $01の値          $00の値($00の値^$01の値^$00の値)
	sta	<$02	; $01の値          $00の値

$00に格納されている値の上位4bitと下位4bitを分離して、それぞれ$01、$02に格納するには 次のように記述します。

			; 上位4bit            下位4bit
	lda	<$00	; $00の値             $00の値
	and	#$f0	; $00の値             0
	sta	<$01	; $00の値             0
	eor	<$00	; 0($00の値^$00の値)  $00の値(0^$00の値)
	sta	<$02	; 0                   $00の値