ファミコン戦士への道(まだ3日目)
ずいぶん間が空いたけど、ファミコン戦士への道をまた歩き出した。とりあえずこの前、NES研究室のサンプルを読んだので、今回もこれを改造していくことにした。とりあえずの目標としては、コントローラを操作すると、メッセージが切り替わるようにする、という感じ。
コントローラの操作を受け付ける
コントローラの入力は、1P側なら$4016というI/Oポートを読み込むことで判断できるらしい。2P側なら$4017。これを読み込むんだけど、ギコ猫でもわかるファミコンプログラミングによれば、ポイントは以下の2点。
- 繰り返して読むと、Aボタン、Bボタン、SELECT、START...と順番に押下情報がわかる。押下情報は第0ビット。
- 一度読み込みを行うとそのままの状態になるので、読み込む前にリセットする。リセットは同じポートに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
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"
次回はスクロールに挑戦してみる予定。