ファミコン戦士に向けて(2日目)
2日目といっても、一週間ぶりだけど。
とりあえず実行環境ができたっぽいので、サンプルを読みながら、その中身を理解していくことに。で、ここまで見た結果、cc65とNESASMでは、NES専用のNESASMのほうが、疑似命令がたくさんあって簡単にかけそうであることがわかってきた。でも、NES研究室 - サンプルのサンプルは、cc65で書かれているので、たぶん、疑似命令に頼らない方がきちんと中身を理解できるだろう、と自分に言い聞かせてこちらを使っていくことにした。
サンプルを頭から読んでいく。
.setcpu "6502" .autoimport on
この「.setcpu」や「.autoimport」は「Control commands」と呼ばれているらしくてここに説明が書かれている。「.setcpu」は見たとおりCPUの種類をセットする命令で6502が選ばれている。「.autoimport」は、ONにすると、未定義シンボルが自動的にインポートされるそうな。でも、ドキュメントによれば「+」と「-」で指定するらしいんだけど、onって書いてある。いいのだろうか。他のところで見たサンプルもみんなonになっている。よくわからないが、とりあえず先に進もう。
続いて、ヘッダ。NESのファイルは先頭に「NES」という文字が埋め込まれている。PNGとかと似た感じだ。続いて、プログラムにいくつのバンクを使うかとか、そういう設定を含めて、16バイトのヘッダが必要になる。
; 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 "HEADER"」は、その後に続くデータをどこのセグメントに置くかを指定するもので、「sample1.cfg」の中で指定されている。
そして「.byte」って書くと1バイトずつ直接値を記述できるらしい。プログラムのバンク数は2になっているけど、1でもいいんだろうな。プログラム量少ないし。
続いて実際のプログラム領域。セグメントは「STARTUP」で、これは$8000番地からに設定されている。NESのプログラムでは、ROMデータは$8000番地からのアドレスになるらしい。
.segment "STARTUP" ; リセット割り込み .proc Reset sei ldx #$ff txs
「.proc」は「.endproc」までを同じスコープのひとかたまりのコードとするための、プロシージャを定義する書き方で、ここでは「Reset」という名前になっているけど、「.endproc」はコードの一番最後にあるので、プログラム全体が一つのプロシージャになっている。「Reset」というのは、リセット割り込みがかかったときに、ここから再実行されるよ、という意味らしい。
SEI | Set Interrupt disable(IRQ割り込みを禁止) |
LDX #$FF | レジスタXに"$FF"をセット(#は即値を表す) |
TXS | Xレジスタの値をスタックポインタにコピー |
なんか割り込み関係の何かと思われるが、よくわからなかったのでとりあえず次へ。
; スクリーンオフ lda #$00 sta $2000 sta $2001
命令を分解すると以下のようになる。
LDA #$00 | レジスタAに"$00"をセット |
STA $2000 | レジスタAの内容を$2000番地にセット |
STA $2001 | レジスタAの内容を$2001番地にセット |
$2000番地と$2001番地はNES研究室によればI/Oポートで、画面表示の各種フラグをセットするための場所。これを全部0でクリアしている。どうも、このあたりをクリアすると、画面表示がとまるっぽい。表示中に画面が描画されると乱れるから、それを止めているのだと思う。
; パレットテーブルへ転送(BG用のみ転送) lda #$3f sta $2006 lda #$00 sta $2006
これは、レジスタAを介して、$2006番地に"$3F"、"$00"という値を2回連続で書き込んでいる。なんじゃこりゃ、と思ったら、$2006番地はI/Oポートで、個々に2回連続で書き込むと、合わせて16ビットでVRAMの出力先アドレスが設定できるらしい。すごい仕様。で、設定されたアドレスは"$3F00"になるんだけど、これはパレットテーブルの場所である、と。ちなみにパレットテーブルの場所は「$3F00-$3F1F」である。
ldx #$00 ldy #$10 copypal: lda palettes, x sta $2007 inx dey bne copypal
続いて、レジスタXに「$00」を、レジスタYに「$10」を書き込んで、ループを回している。回す回数はレジスタYなので16回。で、レジスタXをインクリメントしながら、レジスタAに「palettes+X」という番地からデータを読み込んで、その値を$2007番地に書き込んでいる。palettesの値は以下の通りで、プログラムの後ろのほうに記述されている。ちょうど16バイト。
; パレットテーブル palettes: .byte $0f, $00, $10, $20 .byte $0f, $06, $16, $26 .byte $0f, $08, $18, $28 .byte $0f, $0a, $1a, $2a
$2007番地はVRAMへの書き込みを行うI/Oポートらしい。LDAでパレットテーブルからレジスタAに1バイト読み込んで、STAで$2007番地に書き込むと、先ほど設定したVRAMの$3F00に書き込みが行われる。$2007に書き込むと、自動的に書き込み先が1バイトずれるので、次は$3F01に書き込まれるらしい。で、INXでレジスタXをインクリメント、DEYでレジスタYをデクリメントして、ゼロフラグ(直前の演算、つまりレジスタYのデクリメントで結果が0になったかを示すフラグ)が経っていなかったら、copypalに戻るので、結果16回ループが回って、データがコピーされるわけだ。
パレットテーブルの値は、1バイトが1色で、4色で一つのセットを表している。で、先頭のパレットセット4つがバックグラウンド(BG)用、後ろの4つがスプライト用らしい。このサンプルではBG用の4つのパレットセット(計16バイト)だけがセットされている。色のサンプルはここにあった。
続いて、いよいよ文字の表示部分らしきところへ。
; ネームテーブルへ転送(画面の中央付近) lda #$21 sta $2006 lda #$c9 sta $2006 ldx #$00 ldy #$0d ; 13文字表示 copymap: lda string, x sta $2007 inx dey bne copymap
再び$2006番地を使ってVRAMのセット。今度は「$21c9」番地。ここはネームテーブルといって、背景のデータをセットしておくと画面に表示される部分。画面は2画面分あって$2000〜$23BFと、$2400〜$27BFになってる。画面は32x30キャラクタ分あって、キャラクタコードを指定して表示する。
今度はstring:というラベルで示される場所から13回ループ(またループ用にレジスタYを利用)して、$2007に出力している。文字は以下のように設定されている。
; 表示文字列 string: .byte "HELLO, WORLD!"
そしてそのパターンは、以下のように格納されている。ASCIIの文字コード配列に沿って英数字が配置されているので、上記のような文字をそのまま表示できるようになっている。
; パターンテーブル .segment "CHARS" .incbin "character.chr"
そして、表示が終ったら、スクロール位置の設定。
; スクロール設定 lda #$00 sta $2005 sta $2005
$2005番地はスクロール位置を設定するI/Oポートで、1回目の書き込みが水平スクロール位置、2回目が垂直スクロール位置になる。なんか、こういうタイミングによって意味が変わるのが多いんだな。詳しくはNES研究室で。どちらも0なので、画面1だけが表示されている状態に。
; スクリーンオン lda #$08 sta $2000 lda #$1e sta $2001
ここで、再びI/0ポートを操作して、いくつかフラグをセットしている。たとえば$2001番地の第3ビットはBGの表示を行うフラグで、これを立てないと表示がされないらしい。第1ビットが「画面左端8ピクセルのBGを表示」とか、何のためにあるのかまだよくわからないぞ。
最後にJMPで無限ループを作っている。
; 無限ループ mainloop: jmp mainloop .endproc
これでプログラムが全部読み終わった。と、さらに下を読むと、割り込みの設定があった。
.segment "VECINFO" .word $0000 .word Reset .word $0000
「VECINFO」は$FFFA番地にセットされていて、ここには3つのアドレスが入る。それぞれ「VBlank割り込み」「リセット割り込み」「ハードウェア割り込み/ソフトウェア割り込み」を表すらしく、ここでは「リセット割り込み」をResetラベルにセットしている。この割り込みは、起動時、リセットボタン押下時に発生するらしいので、そのどちらかが発生したらResetからプログラムが実行されるというわけだ。
なるほど、やっぱりアセンブラはシンプルなことをやるのも手数がかかる、がそれが楽しい。Z80を紙に書いてハンドアセンブル(PC-6001で)してたのが思い出される。懐かしいなあ。