前回MSXのROMカートリッジのリーダーを作成しました。メガロムではないROMカートリッジはそのままアドレスを指定するだけでデータが読み出せますが、メガロムのゲームではメガロムコントローラを制御してページ切り替えを行ってROMにアクセスする必要があります。
今回はメガロムコントローラを制御してみます。
前回の記事はこちら
メガロムコントローラとは?
Z80はアドレスバスが16bitのため、アドレス空間が64KB(512Kbit)までしかありません。MSXでは64KBを4つのページに分割して、スロットごとに切り替えられる仕組みがあります(詳細は前回の記事を参照ください)
4つ全てをカートリッジのROMにすることもできますが、RAMなどのワークエリアが必要なため、基本的には0x4000〜0xBFFFの32KB分をROMに割り当てて使用するように設計されているようです。
ゲームデータの大容量化に伴って、32KBでは足りなくなり、これを拡張するために作られたのが通称メガロムコントローラです。コントローラにより0x4000〜0xBFFFの部分をバンク切り替えして32KB以上に拡張します。
メガロムコントローラはMSXの共通仕様ではないため、メーカが独自に様々なコントローラを搭載していたようです。ただ、当時のアスキー社がメガロムコントローラを作成してメーカに供給していた(?)ため、ゲームに用いられていたメガロムコントローラは大きく以下の4つの種類に分類できるようです。
- ASCII 8Kバンク
- ASCII 16Kバンク
- KONAMI SCCなし
- KONAMI SCCあり
メガロムコントローラの一部は解析済みで資料が公開されています。最も古く有名であろうものは似非職人工房の辻川さんによるもので、今でもサイバラさんの似非職人工房・非公認出張所でダウンロードすることができます。辻川さんはメガロムコントローラを使ってMSXの世界を革命的に大きく変えた方で、1ChipMSXの開発にも携わっていました。個人的にはMSX界隈では西さんの次くらいに有名な方なのではないかと思っています。
また、世界的にMSXの情報が集まっている MSX Resource Center にもメガロムの種類についての情報があります。
この中でも最も多いであろう、ASCII 8K/16Kバンクについて説明します。
ASCII 8Kバンク, 16Kバンクは、その名の通り、0x4000〜0xBFFFの32KBを8K/16Kのバンクで分割するもので、各バンクそれぞれ自由にROMのセグメントを割り当てることができます。
注意: バンク、セグメントなどの名称はサイトによってブレがあります。適宜読み替えてください。
セグメントは0〜255の最大256個で、16Kバンクの場合は 16KB*256=4MB、8Kバンクの場合は 8KB*256=2MBまでのメモリ空間を扱うことができるようになります。8bitマシンのMSXにとって見れば広大なメモリ空間ですね。
このセグメントは、必ずしもROMである必要はなく、バッテリバックアップのメモリを搭載しているゲームでは、SRAMなども割り当てられていますし、SCC音源搭載のコナミ製ゲームではSCC音源の制御用のレジスタも割り当てられています。
信長の野望 全国版で試してみる
メガロムのゲームである信長の野望・全国版で確認してみました。
pythonでROMカートリッジリーダのライブラリを作っていまして、対話モードでROMの読み出しをしてみます。こういう使い方ができるのはpython便利ですね。
まずは 0x4000 から 32 Byte を読み出してみました。先頭にROMヘッダの 'AB'
が読めました。
>>> cart.memShow(0x4000,0x20)
4000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
4010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
リセット直後は seg#0 がマップされているようになっているので、ゲームが起動できるわけですね。
続いて 0x6000 に 0x01 を書き込んで 0x4000 を読み出してみます。
>>> cart.memWrite(0x6000,0x01)
>>> cart.memShow(0x4000,0x20)
4000: 3e 01 c3 06 60 af 6f 7d c9 32 5a e4 3a d2 dc cd |>...`.o}.2Z.:...|
4010: ad 62 f5 3a 5a e4 cd ad 62 e1 bc c2 21 60 3e 01 |.b.:Z...b...!`>.|
読み出したデータが変わりました。0x6000に書き込むことで0x4000〜の領域のセグメントが切り替わることがわかりました。
続いて 8Kバンクか、16Kバンクか調べてみましょう。0x6000と0x8000, 0xa000 を読み出してみます。
>>> cart.memShow(0x6000,0x20)
6000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
6010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
>>> cart.memShow(0x8000,0x20)
8000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
8010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
>>> cart.memShow(0xA000,0x20)
a000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
a010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
0x6000 と 0xa000 の先頭が 'AB'
でしたので8Kバンクであることがわかります。16Kバンクであれば、0x6000は0x4000とは異なるはずです。
また、リセット後の初期セグメントはどのバンクも0であることがわかります。
ROMサイズを調べる
メガロムのサイズを調べてみます。セグメントを1,2,4,8.. のように 2^n に変更して先頭の 32 byte を読み出してみます。
★seg#0
>>> cart.memWrite(0x6000,0x00)
>>> cart.memShow(0x4000,0x20)
4000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
4010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
★seg#1
>>> cart.memWrite(0x6000,0x01)
>>> cart.memShow(0x4000,0x20)
4000: 3e 01 c3 06 60 af 6f 7d c9 32 5a e4 3a d2 dc cd |>...`.o}.2Z.:...|
4010: ad 62 f5 3a 5a e4 cd ad 62 e1 bc c2 21 60 3e 01 |.b.:Z...b...!`>.|
★seg#2
>>> cart.memWrite(0x6000,0x02)
>>> cart.memShow(0x4000,0x20)
4000: c3 3b 80 f5 cd db 42 f1 fe 01 c2 24 80 21 00 93 |.;....B....$.!..|
4010: e5 2e 15 e5 2e 14 e5 0e 1c 1e 00 3e 02 cd 76 58 |...........>..vX|
★seg#4
>>> cart.memWrite(0x6000,0x04)
>>> cart.memShow(0x4000,0x20)
4000: c3 5b 81 60 88 cb 88 57 8b c1 83 bb 84 c1 87 21 |.[.`...W.......!|
4010: 64 00 cd b4 64 11 03 00 cd bf 7d d2 21 80 3e 01 |d...d.....}.!.>.|
★seg#8
>>> cart.memWrite(0x6000,0x08)
>>> cart.memShow(0x4000,0x20)
4000: 21 11 a0 e5 6f 26 00 29 11 12 a0 19 5e 23 56 d5 |!...o&.)....^#V.|
4010: c9 c9 49 a0 00 a2 9d a3 b6 a4 06 a6 24 a7 ba a8 |..I.........$...|
★seg#0x10
>>> cart.memWrite(0x6000,0x10)
>>> cart.memShow(0x4000,0x20)
4000: 1c 00 a0 00 00 3d c0 f3 ff 19 7f bf df df df e7 |.....=..........|
4010: f8 ff 02 cc 8c cc cc cf cc 86 ff 0e f8 70 10 00 |.............p..|
★seg#0x20
>>> cart.memWrite(0x6000,0x20)
>>> cart.memShow(0x4000,0x20)
4000: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
4010: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
★seg#0x40
>>> cart.memWrite(0x6000,0x40)
>>> cart.memShow(0x4000,0x20)
4000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
4010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
★seg#0x80
>>> cart.memWrite(0x6000,0x80)
>>> cart.memShow(0x4000,0x20)
4000: 41 42 10 40 00 00 00 00 00 00 00 00 00 00 00 00 |AB.@............|
4010: f3 31 80 f3 cd 35 40 cd 55 40 cd 77 40 cd 81 40 |.1...5@.U@.w@..@|
セグメント0x20までは意味のありそうなデータが読み出せて、0x40 は All 0xFF、0x80 はセグメント0と同じものが読み出せていそうです。セグメント0x3F あたりまでが有意なデータなようです。
次にSRAM搭載かどうかを調べます。SRAMは書き込んでみて値が変更できるかで調べます。
SRAMに割り当てられてそうなのはデータが入っていた最後のセグメントである0x20です。SRAMが搭載されていなければ、ROMはセグメント0x3Fまでと考えられます。SRAM搭載の場合、セグメント0x20 はSRAMであり、ROMはセグメント0x1Fまでと考えられます。
- セグメント0x20がSRAM → ROMはセグメント0x1Fまで
- セグメント0x20がSRAMではない → ROMはセグメント0x3Fまで
SRAMに書き込むには0x8000~である必要があるため bank2 にマップして書き込みしてみます。
★bank2にSeg#0x20をマップ
>>> cart.memWrite(0x7000,0x20)
>>> cart.memShow(0x8000,0x10)
8000: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
★0x8000に0xffを書き込み
>>> cart.memWrite(0x8000,0xff)
>>> cart.memShow(0x8000,0x10)
8000: ff aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
↑ 0xFFが書き込めた!
★0x8000に0x00を書き込み
>>> cart.memWrite(0x8000,0x00)
>>> cart.memShow(0x8000,0x10)
8000: 00 aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
↑ 0x00が書き込めた!
bank2にセグメント0x20を割り当てて書き込んでみたところ、値が変化しました。このセグメントはSRAMであることがわかりました。
※セーブデータが書かれている場合は元の値に戻さないとデータが壊れるので注意が必要です
これにより、ROMのサイズは 8kバンク×0x20 = 256KB (2Mbit) であることがわかりました。MSXのゲームでも大容量の部類です。(たしか最大4Mbitだった気がします)
SRAMのサイズを調べる
ではSRAMのサイズはどのくらいでしょうか。先程先頭に 0x00 を書いたのでそれを印に調べてみます。
SRAM が 8KB以下の可能性もあるので、セグメント0x20の中で 0x00 が現れるか調べます。
>>> cart.memShow(0x8000,0x10)
8000: 00 aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
↑先頭が0x00
>>> cart.memShow(0x8010,0x10)
8010: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8020,0x10)
8020: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8040,0x10)
8040: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8080,0x10)
8080: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8100,0x10)
8100: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8200,0x10)
8200: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8400,0x10)
8400: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x8800,0x10)
8800: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
>>> cart.memShow(0x9000,0x10)
9000: aa aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
0x8000 以外に 0x00 が出てきませんでした。SRAMのサイズは 8KB 以上のようです。
また、ほとんどのデータが 0xaa, 0x55 であることからバッテリバックアップの電池が切れていて、データが飛んでしまっていることもわかります。
では次のセグメント0x21はどうでしょうか
>>> cart.memWrite(0x7000,0x21)
>>> cart.memShow(0x8000,0x10)
8000: 00 aa aa aa 55 55 55 55 aa aa aa aa 55 55 55 55 |....UUUU....UUUU|
↑0x00が現れた!
先頭に 0x00 が出てきました。セグメント0x21 は 0x20 のミラーと考えられます。
以上からSRAMのサイズは8KBであることがわかりました。
セグメントの構成をまとめると以下のようになっていました。
ここまで分かればROMを吸い出すことができそうです。
吸い出して openMSX で起動させてみた結果がこちら。
ちょっとしかプレイしていませんが、問題なく動作しました。正常に読み出せたようです。
まとめ
メガロムコントローラをROMリーダで実際に制御してみました。
メガロムゲームでも、セグメントを切り替えることでROMデータの読み出しができました。今回は試していないですが、SRAMのセーブデータのRead/Writeも行うことができそうです。
ROMのサイズや、SRAMのサイズはある程度自動認識できそうな感じがしますね。いくつかのメガロムのゲームで検証しながらROMリーダのプログラムを作っていこうと思います。