MMC3(マッパー#4)を改造マリオで使う際のポイント

 はじめに

MMC3は機能性と使いやすさのバランスがよく、人気(?)の拡張チップです。
しかし、実態は結構、というかかなり複雑(とくにIRQ周りが)で、たとえば「実機で動く」ことを
一つの基準としても、ちゃんとしたものを作るのは意外と大変なんですよね。

私自身、作ろうとししては、思った通りの動作をせず、うんうん悩んではいろんな資料を当たり、
試行錯誤を重ねてきました。その都度、わかったことをメモにまとめていっていたのですが、
結構な分量になってきたので綺麗にまとめて公開することにしました。

「改造マリオで使う際の」と銘打ってはいますが、MMC3一般に言えることが大部分だと思うので、
マリオクラスタ以外の方々にも役に立つんじゃないかなと思います。
「ポイント」としてあるのは、MMC3入門講座ではなく、「ある程度どんなマッパーか知っていて、
使おうとしたことはあるけどなんかうまくいかなかった」的な人への助言的意味合いを込めています。

例によって、誤りを含む可能性が大いにあるので、気づいた方はメールでもSNSのメッセージとかでも
いいので教えていただければとおもいます。

 目次


 ◆バンク切り替えについて・その1 -起動時の動作-
ご存知のように、MMC3では$8000-$9FFF, $A000-$BFFF, $C000-$DFFFの3つのバンクが
切り替えの対象になり、最終バンク$E000-$FFFFが最終ページで固定されています。
それでは、ゲームを起動した直後のバンクの配置はどうなっているのでしょうか?

実は、起動した直後のバンク配置は完全にランダムで、
最後の固定バンク以外どのページがロードされるかは全く保証されません

このことを踏まえて、起動時に呼び出されるRESET割込を$E000-$FFFFのどこかに置き、
そのルーチン内でバンク指定をすべきです。

SMBは、RESET割込が固定されていない$8000からスタートしているので、
この点を考慮に入れて内部の構造を結構大幅に書き換える必要があります。


参考:自作のMMC3バンク切り替えテストプログラムの様子

GNES, VirtuaNES, FCEU, Nester系, Nestopiaで起動した場合。 最初の2ページと最後の2ページがロードされています。大部分のエミュレータが この流儀をとっているようです。 Nintendulator, famtasiaで起動した場合。 最後の4ページがロードされています。少数派ですがこのような流儀を取っているエミュレータも ありました。 ファミコン実機で動かした場合です。左記2種のようなジェントルなふるまいでは決してないということが わかります。起動後のバンク指定の重要さがわかる結果でした。



 ◆バンク切り替えについて・その2 -スワップ-

 ◇CPUメモリのスワッピング
MMC3では、CPUメモリは$8000-$9FFF(バンク#1), $A000-$BFFF(バンク#2),
$C000-$DFFF(バンク#3)の3つのバンクを切り替えることができますが、
3つを完全に自由に指定できるわけではありません。

基本的に、3つのうち2つは自由に指定でき、残りの1つはPRG-ROMの最後から2ページ目が入ることになります。
最後から2ページ目が入るバンクをどれにとるかを選択するのが、レジスタ$8000の6bit目(PROMスワップフラグ)です。

すなわち、
  ・PROMスワップフラグが0の時
   …バンク#1、バンク#2は自由に指定でき、バンク#3に最後から2ページ目が入ることになります。

  ・PROMスワップフラグが1の時
   …バンク#2、バンク#3は自由に指定でき、バンク#1に最後から2ページ目が入ることになります。

 上記のMMC3バンク切り替えテストプログラムを動かした結果の写真をご覧になってください。
エミュで動かした左2枚はスワップが起こっておらず、実機で動かした右の1枚はスワップが起こっているということです。

まあ、ぶっちゃけPメモリをスワップさせるメリットが私自身思いつきませんし、ややこしくするだけなので
基本的にPメモリのバンク切り替え対象は$8000-$9FFF, $A000-$BFFFの2バンクのみであると考えておけばいいと思います。


 ◇PPUメモリのスワッピング
CHRの方は、PRGよりも簡単です。CROMスワップフラグはレジスタ$8000の最上位bitで、
これが立っているときは、レジスタ$8000で指定する切り替え対象の位置が下図のように反転します。


左半分がPPUメモリの$0000-$0FFF, 右半分が$1000-$1FFFです。
重ねて書いてある数字はレジスタ$8000に入れる値(バンク切り替えコマンド)で、
赤いほうはスワップなし、黄色のほうはスワップありです。
例えば、$8000に$03を入れれば、?ブロックや土管のグラがあるところが、
$83を入れればマリオやクリボー、キノコなどがある領域が切り替え対象になります。



注意すべきことが1つあります。
例えば、初めに、CHRをスワップしてPPUメモリの$1000-$17FFにページ00-01を、
PPUメモリの$0000-$03FFにページ04を入れたとします。そのあと、CPUメモリのバンク切り替えを行おうと、
レジスタ$8000に値06を入れたとします。PPUメモリの内容は初めのままでしょうか?

答えはNOです。
この場合、$8000に値$06を入れたことによってCROMスワップフラグがクリアされました。
したがって、この瞬間スワッピングが解除され、PPUメモリの$0000-$03FFにページ00が、$0400-$07FFにページ01が、
$1000-$17FFにページ04-05が入ることになります。

このように、スワップを行うかどうかは、バンクを指定するそのときだけではなく、
常にレジスタ$8000のスワップフラグと対応しているということです。

CPUメモリと違って、PPUメモリのスワッピングはマリオでMMC3を使う上でも
実際的に重要になってくることが多いので、しっかり理解しましょう。



 ◆IRQ割込み・その1 -IRQを起こすために必要な3つのこと-
SMBをMMC3にするとなれば、やはりゼロ爆弾で行っているラスタースクロールをIRQでやりたいと思うものです。
しかし、MMC3のIRQ発生メカニズムはかなり複雑で、ちゃんとコードを書いたつもりなのにIRQが起こらない、
エミュによって挙動が違うということが、往々にしてあります。実は、MMC3のIRQはとてもややこしいのです。

まず、一般に割込みが生成されるプロセスについて説明します。
これを理解していないとMMC3が割込みを発生させる仕組みも理解できないからです。

まず、割込みの生成元デバイスにおいて、<割込み発生条件>が満たされたとき
生成元はCPUに割込み発生の通知シグナルを送ります。

  今回の例ではもちろん、MMC3が生成元デバイスで、「スキャンラインを所定の本数だけ描画すること」が発生条件ですね。


通知シグナルを送るだけであることに注意してください。
発生条件が満たされたら直ちに割込みが呼び出されるのではありません。
このとき、この割込みは保留状態になります。
割込みが保留されずに実際に発生するためには、CPUは、割込みを認めるデバイスに対して、
アクノリッジ・シグナルを送っていなければなりません。
なぜこのような機構になっているかというと、一般に割込みを生成するデバイスは1つだけではないからです。

  割込みの発生はよく電話がかかってくることにたとえられますが、割込みのアクノリッジは、
  電話機を持っている多数の人の中に、自分に電話をしてきてもいい相手に対して
  自分の番号を教えておくようなものです。



以上をまとめると、MMC3のIRQを使用する手続きは、大体以下のようなものになります:

  1.MMC3以外のIRQ発生を禁止

  2.内部のスキャンラインカウンタに本数の設定

  3.MMC3のIRQをアクノリッジ

1は、プログラムの開始時に1回だけやればいいです。2と3が、毎フレーム行う処理です。

まず、1.について。
6502は、MMC3以外にもIRQを発生しる機構がいくつか存在します。サウンドに関するもので、それぞれ、
APUフレームカウンタ、DPCMがらみの割り込みなのですが、まずそれらを禁止しなければなりません。
これらは、対応するサウンドレジスタへの書き込みによって実現します:
	lda	#$FF
	sta	$4017		; フレームIRQのディセーブル
	lda	#$00		; DMC IRQのディゼーブル
	sta	$4010
  オリジナルのSMBでも、$4017への書き込みはサウンドルーチンのさいしょのほうで何気にやっています。
  DPCMをオフにしているからか、$4010への書き込みはやっていないようです。
  このふたつはRESETで1回やればいいと思っているのですが、SMBだと前者を毎フレーム行っていますね。
  RESET割込みで$4010へのストアとやってもやらなくても、IRQは正しく生成されていました。
  ところが、実験的にNMIで毎フレーム行うようにすると、フレームIRQが発生し続けてプログラムが動作しませんでした。
  サウンドに詳しくないので、このあたりはどうするのが正解なのかぶっちゃけよくわからないです。



2.について。
MMC3は、内部にスキャンラインカウンタを持っており、このカウンタはスキャンラインを1本描画するごとに
デクリメントされていき、0に到達したとき(IRQがイネーブルであれば)IRQを発生させます。

ここで注意すべきことですが、プログラマは内部のスキャンラインカウンタへダイレクトにアクセスする術を持ちません。
プログラマは、カウンタに入れたい値を$C000レジスタへストアし、スキャンラインカウンタのリロードフラグを立てることで
間接的にアクセスする
ことしかできません。リロードフラグが立ったとき、(より正確には、その次のスキャンラインの立ち上がりで)
内部スキャンラインカウンタに$C000の値がリロードされます。そして、リロードフラグが立つのは、

 ・$C001へ任意の値がストアされたとき
 ・内部スキャンラインカウンタがデクリメントされて0になったとき

の2つの場合です。今は、毎フレームのはじめに、そのフレーム内で起こしたいIRQのタイミングを設定しているわけですから、
$C001へのアクセスによって意図的にリロードフラグを立ててやることになります。

3.について。
MMC3内部のカウンタに描くべきスキャンラインの本数を設定したら、あとはMMC3のIRQをアクノリッジしてやります。
単純に、レジスタ$E000へ任意の値をストア(IRQディゼーブル)したのちに、$E001へ任意の値をストア(IRQイネーブル)
すればOKです。

以上をまとめると、VBlankが起きたときに(NMIルーチンのさいしょのほうで)次のような処理をすればよいことになります:
	lda	#$1F			; SMBの場合は、大体上から2ブロック分くらいのところでラスターを起こす 
	sta	$E000		; いったんIRQをディゼーブル 
	sta	$C000		; 内部のカウンタにリロードする値を設定 
	sta	$C001		; リロードフラグを立てる 
	sta	$E001		; MMC3によるIRQ生成をアクナリッジ 
	

最後に、当然ですがプロセッサステータスレジスタのbit-2(Iフラグ)がONになっていると
IRQは発生しません。Iフラグは、意図的にsei/cliした時以外にも、NMI、IRQそれぞれの発生時に
CPUが自動でこれをセット
しますので注意してください。これ、意外と盲点です。

ともかく、とりあえずこれでIRQを任意のタイミングで起こすことができます。



 ◆IRQ割込み・その2 -IRQルーチン内ですべきこと-
今度は、IRQルーチン内ですべきことを考えましょう。これを適切に行わないと大変なことになります。
SMBの場合、スコアボードとメイン画面に2分割するためにラスタースクロールしているだけなので、1フレームに起こすべき
IRQの回数は1回だけです。したがって、IRQルーチン内で、以降はIRQを起こさないような処理をしてやらなければなりません。
要は、$E000にアクセスして、IRQをディゼーブルするだけです。

  *人によっては、IRQルーチン内でも$C000と$C001にアクセスしていることもありますが、これは必要ではありません。
   必要ではないですが、もしするのであれば、$C000に入れる値は$00以外にしておくほうが無難です。
   その理由は、次の「さらに注意すべき点」のセクションでわかります。


あとは、もちろんIRQルーチンの初めに各種レジスタをスタックに退避し、最後にはスタックからプルして終わるようにしましょう。

以上をまとめると、IRQルーチンの書き方は大体次のような感じになります:
IRQ
	php			;| 各種レジスタの値をスタックエリアにプッシュ
	pha			;|
	txa			;|
	pha			;|
	tya			;|
	pha			;|
	
	lda	#$01		; 任意の値($C000,$C001にも入れるのであれば、00以外)
	sta	$E000	; IRQディゼーブル
	
	ここにユーザの行いたい処理を書く
	SMBであれば、$2005へスクロール値のストア
	
	pla		;| 各種レジスタの値をスタックエリアからプル
	tay		;|
	pla		;|
	tax		;|
	pla		;|
	plp		;|
	
	rti		; 割込みルーチンおわり.
	



 ◆IRQ割込み・その3 -さらに注意すべき点-

 ◇BGをleft-patternに、スプライトをright-patternにすべきこと
実は、PPUアドレスバスA12の立ち上がりが、スキャンラインカウントの合図です。
スキャンラインを1本描画するのにかかるクロック総数は341で、PPUは320-340、0-255の間にBGデータを、
クロック256-319の間にスプライトデータをパターンテーブルからフェッチしてきます。
スプライトを見るフェーズから、BGを見るフェーズに移行するタイミングで、PPU A12の遷移が起こり、
カウンタデクリメントのタイミングをとっているわけです。

そこで、BGをPPUメモリの$0000-$0FFF(right-pattern)に、スプライトを$1000-$1FFF(left-pattern)に
格納するようにすれば、ちょうどHBlankに入るあたりのタイミングでPPU A12の立ち上がりが起きますね。

IRQを正確なタイミングで起こすためには、スキャンラインを1本描画するごとにたった1度だけPPU A12の立ち上がりが起きることが重要です。
スプライトをleft-patternに、BGをright-patternにすると、スキャンラインの描画終了とデクリメントのタイミングがずれるだけでなく、
1本の描画中に更にA12の立ち上がりが発生したりと、いろいろとよくないことが起こります。

ハードウェアに直結した話で難しいですが
MMC3でIRQを使うときは、BGをleft-pattern, スプライトをright-patternにしておけばとりあえず問題はない
ということを覚えておくとよいということです。
オリジナルのSMBは逆の配置になっているので、修正が必要になります。

 ◇リビジョンごとの違いについて
MMC3にはMMC3A. MMC3B, MMC3Cという3つのリビジョンが存在します。
基本的な動作は3つとも同じなのですが、IRQ発生に関して微妙に違うんですね。どう違うかというと、

  MMC3A ・・・ 内部のスキャンラインカウンタが0になった時にIRQを発生させる。このため
           ・$C000が$00の時、IRQはたった一度発生する。
           ・$C000が$00の状態で$C001に書き込みを行うと、再度IRQが一度だけ発生する。

  MMC3A ・・・ 内部のスキャンラインカウンタが0に等しいときにIRQを発生させる。このため
           ・$C000が$00の時、IRQは発生し続ける。

界隈では、MMC3A準拠の動作をオールドモード、MMC3C準拠の動作をノーマルモードと呼んでいるようです。
MMC3Bはというと、製造会社によって異なり、シャープ社製のものはノーマルモード、それ以外のものはオールドモード動作をします。

これで、前のセクションで$C000に$00を入れるのは避けるべきだと書いた理由がわかったでしょう。

 ◇IRQを使用してラスタースクロールを実現するための注意点
…私自身、ファミコンのスクローリングについて予想以上に理解していなかったので、もっと勉強と実験をしてから書きます



 ◆IRQ割込み・その4 -SMBに特有の問題-
 ここまでで説明してきたとおりにマリオをMMC3化してIRQによるラスタースクロールを実装したとしても、
パッと見はうまく動いているように見えるんですが、実はまだ直すべきことがあるんですね。
何が起こるかというと、マリオを右に歩行させて画面をスクロールした時に、微妙に水平方向にガタガタするんですよ。
エミュレータだと微妙ながたつきなんですが、実機で動かしたり、VirtuaNESで「TVサイズに補正」を有効にしてみると
かなり目立ちます。

なぜでしょうか?なめらかなスクロールを見せるためには、当然
  ・画面スクロール値$073Fの更新
  ・IRQ($2005へのストア)
という2つの処理は、はっきり決まった順序を持っている必要があります。

では、スクロール関係の処理がなされるタイミングと、スコアボードの下あたりを描画するタイミングの
(時間軸上の)位置関係はどうなっているかというと…微妙なんですよね~これが。

  

上の図で、緑の横線が描画中のスキャンライン、横線と縦線の交点が現在描画中のポイントです。
そして、大方の予想通り、この図で示されている交点がちょうど$073Fの更新をやっているところです。
明らかにかなり微妙で、左はたまたまラスター起こす位置より若干上ですが、処理量によっては、
これがラスター位置より下にくることもあります。たぶん右はアウトですね。

つまりこのままだとまずいわけです。
スコアボードの下あたりでラスターを起こすなら、$C000には0x1F位を書き込んでおけばいいですが、
実際はそれではダメで、もっと早くIRQを起こして、IRQハンドラ内でウェイトをかけて待つ必要があります

せっかくMMC3に拡張してIRQを使っても、結局は、サイクル数をかなり無駄にせざるをえないわけですね…(泣)

戻る