野うさぎ亭
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の値