現在ギコ猫と学ぶファミコンプログラミング 様のサイトを理解しようと修行中。
http://gikofami.fc2web.com/index.html
サンプルプログラムを配布されていたので、ダウンロードしてとりあえず最初のプログラムであるgiko005を理解しようとしてみた。
giko005のフォルダの中にグラフィックデータであるgiko.skrとgiko.bkgとgiko.palを突っ込んで、giko005をnesasm_64.exeにドラッグアンドドロップすると・・・
giko005.nesができた!!(なぜか感動)
これを高機能ファミコンエミュレータのfceuxで動かしてみるといろいろ見れました。パレットデータがどうなってるのかとか、やっぱり実際に見ると分かりやすいね
giko005.asmをメモ帳で開くとコメントだらけのプログラムが見れました。見る人のためにめっちゃ配慮されてる…(驚嘆)。きっとサイト主さんはイケメンに違いない。
さて、ギコ猫さんのところで非常に詳しく説明はされているのですが、素人がつまづいたところを忘れないように少しメモしておきたいと思う。
とりあえずgiko005asmのプログラムをなぞってみるぞ。
.inesprg 1;
.ineschr 1;
.inesmir 0;
.inesmap 0;
ここはどのプログラムにもある宣言。このカセットがどういう設定なのかを書いてるみたい。前回の記事でもちょろっと書いてるけど、ピリオドから始まる命令は疑似命令というものらしい。アセンブラに(`・ω・´)「このカセットはこういうものなんだよ」って教えているわけですな。
.inesprgと.ineschrはそれぞれ何個バンクを使うかを設定してるみたい。・・・バンクって何だ。銀行?
調べた感じのイメージは、「アドレスをまとめたグループ」と捉えるとわかりやすそう。ファミコンのメモリはアドレスが$0000~$FFFF。その中で$8000~$FFFFまでの32kBがプログラムの領域だった。これはhttp://1936noiv.blog.fc2.com/blog-entry-3.htmlの記事でも調べたね。プログラムの容量はマリオは32KBでいっきは16kBだった。(いっきの16kBは2倍に複製されることも忘れない。)
しかし、32kB以上の巨大プログラムを書きたいならどうすればいいのか?
それは、16kBのプログラムをいっぱい用意して、必要な時に切り替える
ってことをすればいいらしい。(32kBじゃないの?って思った人はhttps://img.atwikiimg.com/www34.atwiki.jp/cc65/pub/dsoft/mmap.html ←このサイトが詳しい。$8000~$BFFFまでの16kBを変更可能にして、$C000~$FFFFまでは固定。)
で、この16kBのプログラムをバンクっていうんだね。つまりバンクが8個あるプログラムは16(kB/bank)*8(bank)で合計128kBのプログラムってこと。
逆に256kBのプログラムには、256(kB)/16(kb/bank)で16bankあるってことだ。
いま言ってたのはプログラムについてだけど、グラフィックデータに関しても同じ。容量のめっちゃ大きいグラフィックデータを使いたいならバンクを切り替える必要があるってことみたい。(https://www.wizforest.com/OldGood/ntsc/famicom.html;p4 ←魔法使いの森 様のサイトが詳しい。ただの解説ってだけでなくて読み物として楽しめるように工夫されてますな。)
と、いうことで、ギコ猫様のgiko005.asmに書いてある.inesprg 1; と .ineschr 1; はそれぞれプログラムのバンクの個数とグラフィックデータのバンクの個数ってことだったわけだ。たった二行の命令だけど、こうやって調べてみると、足りない容量をいかに増やすかという昔の人の苦労のあとが垣間見えて面白いね。
次の命令であるinesmirっていうのは画面のミラーリングを垂直にするか水平にするかの設定。そもそも画面のミラーリングとは何だってことだけど、NES研究室様のサイトの図がわかりやすい http://hp.vector.co.jp/authors/VA042397/nes/ppu.html あとはNESDEVの図 https://wiki.nesdev.com/w/index.php/Mirroring 。
実はファミコンのバックグラウンド画面は四枚あって、この4枚は線対称になってる。横線に関して対象なら垂直ミラー、縦線に関して対象なら水平ミラー。
つまり、例えば垂直ミラーなら画面1と画面3は全く同じデータを持ってるってこと。全く同じデータを持つってことは、画面1のアドレスに入っているデータと、画面3に入っているデータは同じっていうこと。
ちなみに、PPUのアドレスだけど画面1は$2000~$23FF、画面2は$2400~$27FF、画面3は$2800~$2BFF、画面4は$2C00~$2FFF、ってことになってる。 .inesmir 0; で水平ミラーリングを設定したら、まずオリジナルのデータが画面1の$2000~$23FFと画面3の$2800~$2BFFに入れられる。そして、画面1の内容を画面2の$2400~$27FFに、画面3の内容を画面4の$2C00~$2FFFにコピーするっていうことだね。これは先程リンクを貼ったNES研究室様のサイトや、
↓のサイトがわかりやすい。(※nesdevは画面0~3で書いてることに注意。)
http://nesdev.com/NES_J.TXT
.inesmapはマッパー選択。マッパーはカセットの種類と思えばいい。とりあえずマッパー0でやっていく。マッパー0はNROMというのは以前の記事で書いたね。マリオもNROMだった。 http://1936noiv.blog.fc2.com/blog-entry-3.html
さて、giko005のプログラムを進めよう。
.bank 1;
.org $FFFA;
.dw 0;
.dw start;
.dw 0;
「.bank 1」っていうのは、bank1つまり割り込み処理に関することを書きますよってアセンブラに伝えているわけ。bank0やbank2はそれぞれ「プログラムについて書きますよ」、「グラフィックデータについて書きますよ」ってアセンブラに伝えてる。これら3つはすべてgiko005のプログラム中にあるね。
「.org $FFFA」っていうのを理解するためには、まず①.orgが何か ②$FFFAとは何のアドレスか
ということを知っていないといけない。まず、①の.orgが何かってことなんだけど、これは http://www.elc.ees.saitama-u.ac.jp/ITO/Ex3/asm.html のページがわかりやすい。アセンブリ言語では定番の疑似命令で、要するに「これから書くプログラムは(指定したアドレス)から入れ始めてくださいね」っていうことをアセンブラに命令しているわけ。 じゃあ、そうやって指定している②の$FFFAとは何のアドレスか。端的に言うと、割り込み処理として実行したいプログラムのアドレス(ふつうはラベル)を入れるアドレス。
https://symfoware.blog.fc2.com/blog-entry-1203.html このサイトが分かりやすかった。忘れないでほしいのは、cpuアドレス空間の$8000~$FFFFまではプログラムを入れるアドレスだったということ。つまり、$FFFA~$FFFFの6Byteもあくまでもプログラムの一部だってことだ。さっきバンク切り替えについて書いたけど、バンクがいっぱいあるときは可変バンクと固定バンクがあったね。この割り込み処理は固定バンクに入れられているのが一般的らしいよ( http://taotao54321.hatenablog.com/entry/2017/04/09/031113 )。
さてじゃあ「.dw 0; .dw Start; .dw 0」 ってなんだろう。まず、.dwについてはさっきの http://www.elc.ees.saitama-u.ac.jp/ITO/Ex3/asm.html にも書かれているけど、2Byteの数値を入れる疑似命令なんですな。つまり、.dwが3つあるから、合計6Byteの数字を入れているわけだ。どこに?・・・それはもちろんさっき.orgで指定した$FFFA~$FFFFの6Byteの空間に、ですな。
つまり「.dw 0; .dw Start; .dw 0」のプログラムを分かりやすく書くと、
$FFFAに00を代入
$FFFBに00を代入
$FFFCに(ラベルStart)のアドレスの下位1Byteを代入
$FFFDに(ラベルStart)のアドレスの上位1Byteを代入
$FFFEに00を代入
$FFFFに00を代入
という処理をしているってわけ。
なんで?と思ったら、先ほどの https://symfoware.blog.fc2.com/blog-entry-1203.html のサイトが超わかりやすい。
ファミコンの割り込みの種類には3種類しかなくて、①Vblank割り込み(NMI割り込みつまりモニターに画面を描写するときの隙をついて割り込む方法。) ②リセット割り込み(電源ONやリセットボタンの時の割り込み) ③ハードウェア割り込みとソフトウェア割り込み(IRQ、BRK割り込み) しかないみたい。 http://nesasm.web.fc2.com/familybasic/ このサイトが分かりやすい。
で、それぞれの3つの割り込みが起こった時に実行するプログラムのアドレスを上の$FFFA~$FFFFに入れているみたい。
$FFFAと$FFFBはVBlank割り込みのときに実行するプログラムのアドレス。$FFFCと$FFFDはリセット割り込みの時に実行するプログラムのアドレス。$FFFEと$FFFFはハードウェア割り込みもしくはソフトウェア割り込みの時に実行するプログラムのアドレス。
giko005では、リセット割り込みが起きたときはStartラベル以降のプログラムを実行したいから、$FFFCと$FFFDにStartラベルのアドレスを入れていたわけですな。さらに、VBlank割り込みとソフトウェア、ハードウェア割り込みは使いたくなかったから$0000を入れていたわけですな。
ちなみに、ギコ猫さんのところはVBlank割り込みを使わないで$2002を使っているみたいだけど、ふつうはVBlank割り込みを使うらしい。具体的には、PPUが描写中はPPUに送りたいデータをバッファーか何かに保存しておいて、VBlankになったらVBlank割り込みをつかってPPUにバッファしておいたデータを送るって感じ。
さて次。
.bank 0;
.org $8000;
これはもう復習だね。.bank0 っていうのはbank0 つまりプログラムを書きますよってアセンブラに疑似命令を出している。で、そのプログラムは$8000のアドレスから始めます。アセンブラさんは私が書いたプログラムを$8000から順番に入れていってくださいねと.orgという疑似命令を出しているだけ。
次。
Start:
lda $2002 ;
bpl Start ;
Startというラベルを貼ってプログラムのこの場所に名前を付けた。そして、lda $2002; という命令でアドレス$2002に入っている1Byteの内容をレジスタAにコピーしたんだね。$2002の内容は https://wiki.nesdev.com/w/index.php/PPU_registers が分かりやすいかな。ギコ猫さんのところにも書いてるけど、大切なのは7bit目のVBlankが発生しているかどうかのフラグ。つまり、モニターにVRAMの内容を描写中なら0、描写中じゃないなら1というわけ。
で、そのフラグが0つまり描写中ならVRAMの内容を書き換えるわけにはいかないから、ひたすらループしてフラグが1になることを待っているわけだ。そのフラグの判定に使っている命令がbpl。これは、Aレジスタの7bit目が0なら指定したアドレスのプログラムにジャンプして、1なら次の命令に移るというもの。これは次のサイトが分かりやすい http://oyasen20.tripod.com/analysis2.html
注意点は、bpl命令は引数とか取らなくてもAレジスタの7bit目を判定してくれているってことかな。
次。
lda #%00001000 ;
sta $2000;
lda #%00000110;
sta $2001;
まず前提知識として必ず知っておかなければならないのは、このアセンブリ言語では”$”は16進数を表して、”%”は2進数を表して、”#”は数値を表すってこと。#(シャープ)が分かりづらいかもしれないけど、ldaでこの#がなかったらどうなるかを想像してみたらいい。もしlda %00001000; だったら、2進数を16進数に変換して、 lda $0008; と同じ意味になる。つまり、$0008のアドレスの中身をAレジスタにコピーしてしまうってこと。それは違う。いまAレジスタに入れたいのは2進数の数値である%00001000だからね。
このことは、NES研究室様のサイトが詳しいね http://hp.vector.co.jp/authors/VA042397/nes/6502.html
で、$2000と$2001というPPUコントロールレジスタに設定を入れているわけだ。 https://wiki.nesdev.com/w/index.php/PPU_registers でもギコ猫さんのところでも、これらのそれぞれの値が何を示しているかは説明されているから割愛。
次。
ldx #$00
これはさっき書いたことだけど、$00という1Byteの16進数の「数値」をXレジスタに代入してる。二進数であらわすと%00000000という「数値」を代入しているということ。
次。
lda #$3F
sta $2006
lda #$00
sta $2006
これも同じこと。Aレジスタに$3Fという「数値」を代入した後、その内容を$2006というアドレスにコピーしてる。そのあと、同じようにして$2006に$00という「数値」を入れている。
・・・?なんで$2006に二回書き込んでるんだ?8bitCPUなのに、これ#$3Fが#$00で上書きされていないか?
色々調べたけど、$2006には二回書き込むことでアドレスを入れられるとしか書いてない。うーん
これは$2005も同じみたい。 https://img.atwikiimg.com/www34.atwiki.jp/cc65/pub/suzume/ns06.html
つまり、まず$2006にアドレスの上位ビットを書き込んだらPPUが「上位ビットは○○なんだ」とどこかに値を保存して、もう一回$2006にアドレスの下位ビットを書き込んだらPPUが「下位ビットは○○なんだ」と、認識するって感じ??CPUの側にアドレスを保存するんじゃなくてPPUの側にアドレスを保存するという感じ?うーん
と、悩んでましたがたぶん解決しました
https://wiki.nesdev.com/w/index.php/PPU_scrolling
↑このページにいろいろ書いてありました。なるほど、やっぱりPPU内部のレジスタに保存していたのか。
このアドレスが保存されているPPU内部のレジスタのことをvレジスタっていうみたいだね。
あぁ^~これはすごいアハ体験
ギコ猫さんのところでスクロールをやるときにもう一度熟読しよう。
次。
loadPal:
lda tilepal, x
sta $2007
inx
cpx #32
bne loadPal
loadPal:はラベル。今更か
lda tilepal,x はtilepalの命令を実行して、そのうえで(その命令のアドレス)+(xレジスタの値)のアドレスに入っている中身をAレジスタにコピーするということ。tilepalの内容は以下。
tilepal: .incbin “giko.pal”
つまり、この命令ではgiko.palを展開しているわけだから、giko.palの中身をAレジスタにコピーしているってわけだ。
・・・ちゃんとgiko005の例で説明しようか。giko.palの中身をstirlingのようなバイナリエディタで開いてみよう。そしたら、次のようなデータが出てくる。左から順に$00~$1Fね。
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 0D 15 30 24 0D 24 19 18 19 1A 1B 1C 1D 1E 1F
はい。これがgiko.palの中身だったわけだ。つまりパレットファイルね。このパレットファイルは$00~$0Fが背景用のパレットで、$10~$1Fがスプライトのパレット。
で、この32個のデータをひとつずつAレジスタを経由して$2007に送っている。
(ちなみに、giko.palのカラーコードは https://www.wizforest.com/OldGood/ntsc/famicom.html これ。あと、pal.exeは今は手に入らなさそうなので、yy-chrで開く方法はここに書いてあった https://www15.atwiki.jp/nano_001/pages/33.html 。もし暇なら、スプライトパレットの0番つまり↑で言う$10~$13の、 10 0D 15 30 という値をstirlingのようなバイナリエディタで書き換えてからアセンブルしてみてほしい。自分が書き換えたとおりに色が変わる。これ、まるでチートコードの作り方みたいだな・・・)
$2007はPPU内部のvレジスタに書かれたアドレスにデータを渡す。vレジスタっていうのは、先ほどCPUの$2006に渡して指定したアドレス。具体的には、さっきは$2006ではPPUの$3F00を指定したね。$3F00からの32Byteはパレット情報を保存するアドレスだったからだ。(NES研究室→ http://hp.vector.co.jp/authors/VA042397/nes/adrmap.html )もっとちゃんと言うと、PPUの$3F00~$3F0Fが背景用のパレット。PPUの$3F10~$3F1Fがスプライト用のパレット。と、いうことで、この32Byteの空間にgiko.palの中身をそっくりそのまま移してしているわけなんですな。
…あ、$2007に値を渡したときにvレジスタはインクリメントされることは注意ね。これはギコ猫さんのところにもちょろっとかいてある。vレジスタがインクリメントされるからこそ、32個のアドレスに順番に値を代入できるってこと。
ってことは、giko.palを読み込まずにそのまま書き込んでもいいってことだよね。そのまま書くなら、次のサイトのプログラムが超分かりやすい。giko.palの内容をラベルを使って表現しているみたい。 https://github.com/thata/nes_examples/blob/master/hello/hello.asm
さて、32回代入といったけど、どうやっているかも書いておこうか。giko005のプログラムに戻るけど、inxはNES研究室様 http://hp.vector.co.jp/authors/VA042397/nes/6502.html に書いてあるようにXレジスタをインクリメントしてる。
cpx #32とかbne loadPALはちょっと複雑かも。次のサイトが分かりやすかった http://taotao54321.hatenablog.com/entry/2017/04/09/151355 。簡単に要約するとcpx #32 っていうのは、Xレジスタの値と32という数値(シャープ#がついてるから数値、これは上で書いた)を比較して、もし一致していたらステータスレジスタのbit1(ゼロフラグ(Zフラグ))が1(他言語で言う真)になる。一致していなかったら0(偽)になる。 で、bneっていうのは、このゼロフラグ(Zフラグ)が真つまり1ならプログラムを進めて、偽つまり0ならば指定したラベルの位置にジャンプするという命令。まあ、32回ループしないと次に進めないよってこと。ギコ猫さんのところの6章にもちょろっとかいてありますな。ちなみにbneと全く逆なのがbeqという命令。
こうやって、パレットの情報をPPU$3F00~$3F1Fに代入したわけなんですな。
さて次。これがこのプログラムの山場だと思う。
lda #$00
sta $2003
lda #50
sta $2004
lda #00
sta $2004
sta $2004
lda #20
sta $2004
すでに何回も何回も言ってきたけど、#(シャープ)は値を示す。#$は16進数の値で、#%は2進数の値。
じゃあ、# 単体だったら?(例えば#50) これはじつは 10進数の値を示す。これは抑えておこう。
で、まずlda #$00 でAレジスタに16進数で書くと00、二進数で書くと00000000という値を代入した。そして、sta $2003でこのAレジスタの値を$2003にコピーしたわけだ。$2003っていうのはさっきの$2006と似たようなもの。PPU内部のスプライトレジスタ(nesDevなら「OAMレジスタ」と書いてある)は$00~$FFの256個のアドレスを持つんだけど、そのうちのどのアドレスを読み書きしたいですか?という指定をするのがCPUの$2003。んで、この$2003で例えば#$00 と指定してやると、CPUの$2004で、このOAMレジスタの指定したアドレスとデータをやり取りできるってわけ。
https://wiki.nesdev.com/w/index.php/PPU_OAMとかhttp://taotao54321.hatenablog.com/entry/2017/04/11/115205 をみれば深く理解できそう。
つまり、ファミコンは256個のスプライトを作れるんだけど、それぞれのスプライトのデータがこのOAMに保存されるというわけだ。
混乱するかもしれないのは、CPUのアドレスは一つのアドレスに入ってるデータが1Byteだったけど、OAMは何と4Byteだってこと。↑のサイトを見るかギコ猫さんのところを見てみればいい。①1Byte目がスプライトのY座標、②2Byte目がスプライトが自分の創ったスプライトテーブル(sprファイル)の何番のスプライトを使うのかというインデックス番号、③3Byte目がスプライトの具体的な設定、④4Byte目がスプライトのX座標。この4種類の情報をちゃーんと書かないといけないみたい。
さて、今、giko005のプログラムでは①は#50、②は$00(2進数で描くと%00000000)、③も$00(2進数で描くと%00000000)、④は#20
を代入したい。そこで、$2004に順番にこの値を渡していくわけだ。この時、Aレジスタを経由しているわけなんだね。
さっきと同じで、$2004に値を代入していくとさっきの$2007のvレジスタの例と同じように自動的にインクリメントしてくれるみたいだから、ただ順番に$2004に値を渡していくだけでいいみたいですな。PPU有能なり。
次。
lda #%00011110
sta $2001
これはさっきの復習みたいなもの。さっきは$2001に#%00000110を渡した。つまり、スプライトとBGを表示しなかった。
でも、もうスプライトとかの設定は終わったから、表示するために3bit目と4bit目を1にして$2001に渡しなおしたってわけだ。
次。
infinityLoop:
jmp infinityLoop
何回も自分の命令にジャンプすることで無限ループしているわけですな。ナニモイウコトハナイ
次。
.bank 2
.org $0000
.incbin “giko.bkg”
.incbin “giko.spr”
bank2っていうのはスプライトと背景のデータを入れるバンクだったね。つまり.bank2以降に書かれていることはVRAMのメモリ空間に書かれると。
んで、.org $0000っていう疑似命令でVRAMのメモリ空間の$0000っていうアドレスから始めてくれとアセンブラに伝えているわけだ。
そして、背景データ→スプライトデータの順番にそのVRAMに入れているというプログラム。ちょっと疑問になったのは、これプログラムの一番下に書いてもいいのかということ。この上にbank1の無限ループがあるからどうなんだろうって思った。
うーん、アセンブリ言語で書いていたプログラムっていうのはnesasmではそのまま上から実行されるんじゃなくて、CPUのメモリ空間の$8000以降にbank1とbank0の内容がとりあえずコピーされて、次にPPUのメモリ空間のVRAM領域にbank2が埋め込まれ、そうしてやっとプログラムが実行されるってイメージなのかな。
アセンブルっていうのは.nesファイルを作る作業であって、ゲームを実行することではない。無限ループとか関係ない。”無限ループ”というオペコードを.nesファイルに埋め込む、そういう作業だからね。実際に実行するのはエミュレータ。
さて、次・・・はない。やった、これでgiko005はとりあえずざっと理解できた気がする!!
(*´Д`)疲れた。めっちゃ色々調べたけどやっぱまだまだ入口なんだよなぁ・・・今更だけどド素人が手を出していいものじゃない気がしてきたぞ。
先行きが暗くなってきたところで今回はここまで。次回は.nesファイルをstirling(バイナリエディタ)で見て、中身がどうなっているかを確かめてみよう。
というかこの記事長すぎないか。カウントしてみたら原稿用紙25枚分ぐらいある(笑)
コメント