ファミコン戦士への道(まだ3日目)

ずいぶん間が空いたけど、ファミコン戦士への道をまた歩き出した。とりあえずこの前NES研究室のサンプルを読んだので、今回もこれを改造していくことにした。とりあえずの目標としては、コントローラを操作すると、メッセージが切り替わるようにする、という感じ。

コントローラの操作を受け付ける

コントローラの入力は、1P側なら$4016というI/Oポートを読み込むことで判断できるらしい。2P側なら$4017。これを読み込むんだけど、ギコ猫でもわかるファミコンプログラミングによれば、ポイントは以下の2点。

  1. 繰り返して読むと、Aボタン、Bボタン、SELECT、START...と順番に押下情報がわかる。押下情報は第0ビット。
  2. 一度読み込みを行うとそのままの状態になるので、読み込む前にリセットする。リセットは同じポートに1,0を順番に書き込む。

で、今回はAボタンが押されるまでそのままの状態で待つ、という処理にするので、リセット→1回$4016ポートを読んで第0ビットを調べる。もし、ビットが立ってなければ、ループ。という処理にした。フラグのチェックはAND(論理積)で。

; Aキーが押されるのを待つ
keywait:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	bne keywait
keywait_a:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	beq keywait_a

あと、これは後でわかったことだが、単に押下だけをチェックすると、押しっぱなしにすると連続してページが繰られてしまうので、直前にページが離されていることもチェックした。bneはゼロフラグ(前の演算の結果がゼロの時にたつ)が立っていない時ジャンプ、beqはゼロフラグが立っているときにジャンプ。

サブルーチン呼び出し

続いて、サブルーチン呼び出しを調査。呼び出して、リターン命令で戻る、という処理はきっとあるはず(Z80からの連想)ということで調べた。やっぱりある。jsrで(無条件)ジャンプ、rtsで戻る。キー待ちをサブルーチン化した。
以下で呼び出し。

	jsr keywait

以下のように、最後にrtsで戻るだけ。

; Aキーが押されるのを待つ
keywait:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	bne keywait
keywait_a:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	beq keywait_a
	rts

ちなみに、Z80ではRTSに当たるRETは0xC9でおなじみだけど、6502の場合は0x60なのね。

RAMを使う

アセンブラレジスタが少ないから、データを記録するのにメモリを使うわけだけど、ファミコンの場合ってどこに記録すればいいのだろうか、ということで調べると、サンプルの「sample1.cfg」を見ると、メモリマップのRAMが$0400から始まっていたので、この辺に書くことにした。
書き込むのは、表示するページ数と、現在の表示位置。

	lda	#$05       ; ページ数
	sta	$0400
	lda	#$00       ; データ位置
	sta	$0401

なんか、ラベルを付けた方がわかりやすそうだったけど、とりあえずこれで。
メモリのインクリメント、デクリメントはinc、decでできる。

	inc $0401
	dec $0400

表示データ

表示するデータは、とりあえずアルファベットで。サンプルのデータにはひらがななども入っているのだけれど、とりあえずわかりやすいところで。

string:
	.byte	"           Hello!               "
	.byte	"        My name is ...          "
	.byte	"        Takaaki Mizuno          "
	.byte	"       I want to be a ...       "
	.byte	"       FamiCom Fighter !        "

データは、横32ブロックに合わせて32文字にした。これで、前の情報を完全に上書きできるので、クリアする必要はない気がする。文法あってるかな?
で、データ位置を、$0401に保存してあるデータ情報から、計算するロジックを書く。

; 文字データの位置を計算
	lda #$00
	ldy $0401
	beq setpage
strposcounter:
	adc #$20
	dey
	bne strposcounter
setpage:
	sta $0402

なんかもう少しきれいにかけそうな気がするが、とりあえず。計算結果は$0402にストア。というか、いろいろ試行錯誤しているうちにこうなったのだが、これならこの値そのものをメモリに保存しておけばいいな。でも、これだと最大8行までしか処理できないので、今後拡張するときに考えることにする。とりあえずこれで、stringラベルの位置からの相対位置を計算できるので、以下のように書き込めばいい。

; 書き込み位置計算
	lda #$21
	sta $2006
	lda #$c0
	sta $2006
	ldx $0402
	ldy #$20
copymap:
	lda string, x
	sta $2007
	inx
	dey
	bne	copymap

VBlankでVRAMを変更

で、動かしてみたのだけど、うまくいかない。表示されるデータがおかしくなる。今回はこの部分で一番苦労した。いろいろ試行錯誤をした後、わかったことは、ファミコンでは、1/60秒に1回、VBlankという状態になるらしく、どうもこのタイミングでないと、VRAMの書き込みが正しく行われないらしいということがわかる。

ファミコンの画面サイズは 256 x 240 ピクセルで1秒間に60回画面が更新されますが、実際には240ラインを描画した後に20ライン程分の時間何も描画しない特殊な時間があります。その時間を「VBlank」と言って、通常VRAMにアクセスするのはこの期間内にするべきとされています。

それを知るには、$2002というI/Oポートを読んで、その第7ビットを調べればいいらしい。そこで、VBlankを待つサブルーチンを作る。

; VBlankまち
vblankwait:
	lda $2002
	bpl vblankwait
	rts

これを呼び出してタイミングを合わせたけど、それでも正しく表示されなかったけど、表示の際にスクロール位置を毎回設定するようにしたらうまくいった。まだ、なぜだかよくわからない。

; スクロール設定
	lda #$00
	sta $2005
	sta $2005

ちなみに、VBlankは、VBlank割り込みというのを利用できて、VBlankが発生したときに、指定したアドレスの呼び出しを行うようにすることも可能らしいのだが、今回は利用しなかった。以下のVECINFOで、一つ目のアドレスの設定がVBlank割り込みの呼び出しアドレスらしい。

.segment "VECINFO"
	.word	$0000
	.word	Reset
	.word	$0000

ファミコン開発におけるデバッグ

VMエミュレータ)としてはVirtualNESを利用しているのだけれど、これはメモリマップを別ウインドウで表示できる機能がついていた。なので、必要な情報を適宜適当なメモリに書き出すようにしておくと、デバッグができる。たとえば、VRAMへの書き出しも、同時にメモリにも同じものを書き出しておくと、想定したものが出力されているかを調べられた。

	lda string, x
	sta $2007
	sta $0440, x

けど、もっといい方法があるはず。調査中。

今回のまとめ

ということで、Aボタンを押すと、文字がどんどん変化していくものが完成した。$0400にはページ数が保存されているが、それをデクリメントしていって、0になったら、それ以上ループせず、無限ループにはいるようにした。ソースはこんな感じに。

.setcpu		"6502"
.autoimport	on

; iNESヘッダ
.segment "HEADER"
	.byte	$4E, $45, $53, $1A	; "NES" Header
	.byte	$02			; PRG-BANKS
	.byte	$01			; CHR-BANKS
	.byte	$01			; Vetrical Mirror
	.byte	$00			; 
	.byte	$00, $00, $00, $00	; 
	.byte	$00, $00, $00, $00	; 

.segment "STARTUP"
; リセット割り込み
.proc	Reset
	sei
	ldx	#$ff
	txs

; スクリーンオフ
	lda	#$00
	sta	$2000
	sta	$2001

; 定数
	lda	#$5       ; ページ数
	sta	$0400
	lda	#$0       ; データ位置
	sta	$0401

; パレットテーブルへ転送(BG用のみ転送)
	lda	#$3f
	sta	$2006
	lda	#$00
	sta	$2006
	ldx	#$00
	ldy	#$10
copypal:
	lda	palettes, x
	sta	$2007
	inx
	dey
	bne	copypal

; スクロール設定
	lda	#$00
	sta	$2005
	sta	$2005

; スクリーンオン
	lda #$08
	sta $2000
	lda #$1e
	sta $2001

mainloop:
;	jmp	mainloop

; 文字データの位置を計算
	lda #$00
	ldy $0401
	beq setpage
strposcounter:
	adc #$20
	dey
	bne strposcounter
setpage:
	sta $0402

; VRAM WAIT
	jsr vblankwait
	
; 書き込み位置計算
	lda #$21
	sta $2006
	lda #$c0
	sta $2006

	ldx $0402
	ldy #$20
copymap:
	lda string, x
	sta $2007
	inx
	dey
	bne	copymap

; スクロール設定
	lda #$00
	sta $2005
	sta $2005

; 定数の変更
	inc $0401
	dec $0400
	beq finalloop

; Aボタンのチェック
	jsr keywait
	jmp mainloop

; 無限ループ
finalloop:
	jmp	finalloop

; Aキーが押されるのを待つ
keywait:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	bne keywait
keywait_a:
	lda #$01
	sta $4016
	lda #$00
	sta $4016
	lda $4016
	and #$01
	beq keywait_a
	rts

; VBlankまち
vblankwait:
	lda $2002
	bpl vblankwait
	rts

.endproc

; パレットテーブル
palettes:
	.byte	$0f, $00, $10, $20
	.byte	$0f, $06, $16, $26
	.byte	$0f, $08, $18, $28
	.byte	$0f, $0a, $1a, $2a

; 表示文字列
string:
	.byte	"           Hello!               "
	.byte	"        My name is ...          "
	.byte	"        Takaaki Mizuno          "
	.byte	"       I want to be a ...       "
	.byte	"       FamiCom Fighter !        "

.segment "VECINFO"
	.word	$0000
	.word	Reset
	.word	$0000

; パターンテーブル
.segment "CHARS"
	.incbin	"character.chr"

次回はスクロールに挑戦してみる予定。