■マークIII・マスターシステム ソフトウェア開発資料 2014/2/18 Author Mt. Chocolate Twitter @MtChocolate 一般的なゲームソフトの開発を目的として説明しています。 ハードウェアやユーティリティーソフト等の開発で主に係わる部分については触れていません。 著者が動作確認してない部分については参考資料の翻訳のみか、説明されていません。 対象機種の明記が無い場合は、マークIII・マスターシステムについて説明しています。 VDPに関する記述ではTMS9918A互換モードについて説明していません。 サンプルコードはSDCC向けに書かれていますが検証されていません。 ■参考資料 Enri's Home PAGE by Enri http://www43.tok2.com/home/cmpslv/ Sega Master System VDP documentation by Charles MacDonald http://cgfm2.emuviews.com/ SMS/GG hardware notes by Charles MacDonald http://cgfm2.emuviews.com/ ■Z80のメモリーマップ 16kB 単位で区切られた以下の4つの基本領域に別れます。 0x0000 - 0xBFFF: ROMバンク0 (16kB) 0x4000 - 0xBFFF: ROMバンク1 (16kB) 0x8000 - 0xBFFF: ROMバンク2 (16kB) 0xC000 - 0xBFFF: ワークRAM (16kB) → 本体内蔵ワークRAM有効時: 0xC000 - 0xDFFF: ワークRAM 0xE000 - 0xFFFF: (0xC000 - 0xDFFF のミラー及び拡張機能) → 本体内蔵ワークRAM無効時: 0xC000 - 0xDFFF: 外部RAM/ROM 0xE000 - 0xFFFF: 外部RAM/ROM ■ワークRAM ワークRAMは 16kB の領域がありますが、実装されているワークRAMの容量は機種毎に異なります。 容量 機種 1kB SG-1000 2kB SC-3000 2kB オセロマルチビジョン (FG-1000) 8kB マークIII 8kB マスターシステム 未実装の領域はミラーになりますが、一部のカートリッジや周辺機器では拡張機能に 割り当てられています。 ■3Dグラス 3Dグラス アダプターを接続しているか内蔵している機種ではアドレス 0xFFF8 - 0xFFFB への 書き込みが 3DグラスのLCDの制御ポートになります。 いずれの制御ポートも機能は同じで bit 0 への書き込みでLCDを制御します。 ■バンク切り替え バンク切り替えのあるカートリッジではアドレス 0xFFFC - 0xFFFF への 書き込みがメモリーコントローラーの制御ポートになります。 0xFFFC: メモリーコントローラーの設定 bit 7 1 = ROMバンクへの書き込み有効, 0 = 無効 bit 6 未使用 bit 5 未使用 bit 4 1 = アドレス 0xC000 - 0xFFFF にカートリッジRAMを割り当てる, 0 = 割り当てない bit 3 1 = アドレス 0x8000 - 0xBFFF にカートリッジRAMを割り当てる, 0 = 割り当てない bit 2 カートリッジRAMバンク選択 (16kB 単位) bit 1 - 0 バンクシフト (使用しているカートリッジ無し) ROMバンクへの書き込み、カートリッジRAM (バックアップRAM)、バンクシフトを使わない 大容量カートリッジはアドレス 0xFFFC = 0x00 とします。 0xFFFD: アドレス 0x0000 - 0x3FFF (バンク0) に割り当てるROM領域 (16kB 単位) 0xFFFE: アドレス 0x4000 - 0x7FFF (バンク1) に割り当てるROM領域 (16kB 単位) 0xFFFF: アドレス 0x8000 - 0xBFFF (バンク2) に割り当てるROM領域 (16kB 単位) アドレス 0x0000 - 0x3FFF (バンク0) の先頭 1kB は割り当てたROM領域とは無関係に、 常にROM領域の先頭 1kB に固定されます。 多くのソフトはアドレス 0x0000 - 0x7FFF (バンク0と1) を常にROMの先頭 32kB に割り当て、 アドレス 0x8000 - 0xBFFF (バンク2) に動的なバンク割り当てを行っています。 以下はその実装例です。 /* メモリーコントローラーとバンクを初期化する */ void bank_init(){ /* ROMバンクへの書き込み、カートリッジRAM (バックアップRAM)、バンクシフトを使わない */ *(uint8_t *)0xFFFC = 0x00; /* バンク0 はROMの 0x0000 - 0x3FFF 固定 */ *(uint8_t *)0xFFFD = 0x00; /* バンク1 はROMの 0x4000 - 0x7FFF 固定 */ *(uint8_t *)0xFFFE = 0x01; } /* バンク2に割り当てるROM領域を変更する */ void bank_select(uint8_t bank){ *(uint8_t *)0xFFFF = bank; } ■Z80のI/Oポートマップ 以下の8つのI/Oポートがあります。 それぞれのI/Oの詳細は別項で説明しています。 0x3E: メモリー コントロール 0x3F: I/Oポート コントロール 0x7E: Vカウンター (Read時) / SN76489データ (Write時) 0x7F: Hカウンター (Read時) / SN76489データ (Write時, 0x7Eのミラー) 0xBE: VDPデータポート (Read/Write) 0xBF: VDPコントロールポート (Read/Write) 0xDC: I/Oポート入力 A & B 0xDD: I/Oポート入力 B & 他 /* SDCCでのI/O定義例 */ sfr at 0x3E MEMORY_CTRL; sfr at 0x3F IO_CTRL; sfr at 0x7E VDP_V; sfr at 0x7F VDP_H; sfr at 0xBE VDP_DATA; sfr at 0xBF VDP_CTRL; sfr at 0xDC PORT_A; sfr at 0xDD PORT_B; ■VDP - Video Display Processor 映像処理を行うデバイスです。 MSX等に搭載されている TMS9918A を拡張する形で実装しています。 オリジナルの TMS9918A には無い新しい画面モードを追加しています。 基本的にマークIII・マスターシステムでは TMS9918A のオリジナルの画面モードを使わずに、 常に追加された新しい画面モード (Mode 4) を使います。 メガドライブのVDPは TMS9918A のオリジナルの画面モードを実装していません。 それらの画面モードを使っているマークIII・マスターシステム用ソフトをメガドライブで動作させた場合、 正常に動作しません。 新規マークIII・マスターシステム用ソフトの作成においては Mode 4 以外を使う事は推奨されません。 ■VDPポート VDPにはZ80のI/O命令でアクセスします。 SN76489もVDPの一部として組み込まれています。 0x7E: Vカウンター (Read時) / SN76489データ (Write時) 0x7F: Hカウンター (Read時) / SN76489データ (Write時, 0x7Eのミラー) 0xBE: VDPデータポート (Read/Write) 0xBF: VDPコントロールポート (Read/Write) 0x7E と 0x7F は読み込みがH/Vカウンター、書き込みが SN76489 に割り振られています。 読み込むとラスター位置のカウント値が読み出され、 書き込むとPSGポートにデータを書き込みます。 /* SDCCでのI/O定義例 */ sfr at 0x7E VDP_V; sfr at 0x7F VDP_H; sfr at 0xBE VDP_DATA; sfr at 0xBF VDP_CTRL; ■VRAM - Video RAM VRAMには以下の配列を配置します。 ・背景のパターン - Background Pattern ・スプライトのパターン - Sprite Pattern ・背景のネームテーブル - Background Name Table ・スプライト アトリビュート - Sprite Attribute ■VRAMへの書き込み VDPコントロールポート (0xBF) に2回値を書き込みます。 MSB LSB A07 A06 A05 A04 A03 A02 A01 A00 1回目 0 1 A13 A12 A11 A10 A09 A08 2回目 A13 〜 A00: VRAMアドレス 次にVDPデータポート (0xBE) 書き込むと、その値がVRAMに書き込まれます。 VRAMに書き込まれる度にVRAMアドレスが 1 byte インクリメントされます。 /* VRAMのアドレス addr 以降に length バイトの data を書き込む */ void vram_write(uint16_t addr, uint8_t *data, uint16_t length){ uint16_t c; VDP_CTRL = addr & 0x00FF; VDP_CTRL = (addr >> 8) + (1 << 6); for(c = length; c != 0; c--){ VDP_DATA = *data++; } } ■VDPステータスフラグ VDPコントロールポート (0xBF) を読み込むとVDPステータスフラグが得られます。 0xBF = VDPコントロールポート (Read/Write) MSB LSB INT OVR COL --- --- --- --- --- INT: 1 = VBLANK突入 OVR: 1 = スキャンライン中のスプライト数がオーバーした COL: 1 = スプライト同士が重なった いずれのフラグもコントロールポートを読み込んだ時点でクリアされます。 /* VBLANK同期を取る例 */ void vsync_wait(){ int8u d; d = VDP_CTRL; /* clear flags */ while(373){ if(VDP_CTRL & 0x80) break; } } ■CRAM - Color RAM CRAMは 16色の背景と 16色のスプライトの色を保持します。 1 byteで1色を表します。 MSB LSB --- --- B01 B00 G01 G00 R01 R00 R00 〜 R01: 赤 G00 〜 G01: 緑 B00 〜 B01: 青 全部で32色になるので、CRAMの容量は 32 bytes あります。 ■CRAMへの書き込み VDPコントロールポート (0xBF) に2回値を書き込みます。 MSB LSB 0 0 0 A04 A03 A02 A01 A00 1回目 1 1 0 0 0 0 0 0 2回目 A04 〜 A00: CRAMアドレス 次にVDPデータポート (0xBE) 書き込むと、その値がCRAMに書き込まれます。 CRAMに書き込まれる度にCRAMアドレスが 1 byte インクリメントされます。 /* VRAMのアドレス addr 以降に length バイトの data を書き込む */ void cram_write(uint8_t addr, uint8_t *data, uint8_t length){ uint8_t c; VDP_CTRL = addr & 0x00FF; VDP_CTRL = 0 + (3 << 6); for(c = length; c != 0; c--){ VDP_DATA = *data++; } } ■VDPレジスター VDPレジスターはVDPの動作を保持しています。 VDPレジスターに値を書きこむ事によって、画面モードや描画方法を設定します。 ■VDPレジスターへの書き込み VDPコントロールポート (0xBF) に2回値を書き込みます。 MSB LSB D07 D06 D05 D04 D03 D02 D01 D00 1回目 1 0 - - R03 R02 R01 R00 2回目 D07 〜 D00: レジスターへ書き込む値 R03 〜 R00: レジスター番号 /* VDPレジスター 0x03 に reg_data を書き込む */ void vdp_reg_write(uint8_t reg, uint8_t data){ VDP_CTRL = data; VDP_CTRL = reg + (2 << 6); } ■VDPレジスターの詳細 1. レジスター 0x00 - モード コントロール 1 bit 機能 7 垂直スクロール ロック: 1 = 24 〜 31 番の列を垂直スクロールしない 6 水平スクロール ロック: 1 = 0 〜 1 番の行を水平スクロールしない 5 左列のマスク: 1 = 0番目の列に背景とスプライトを表示しない 4 HBLANK割り込み: 1 = 有効 3 スプライトのシフト: 1 = 左に 8 pixel シフトする。 2 画面モード(M4) 1 画面モード(M2) 0 ビデオ出力: 1 = モノクローム, 0 = 通常 2. レジスター 0x01 - モード コントロール 2 bit 機能 7 未使用 6 表示: 1 = 有効 5 VBLANK割り込み: 1 = 有効 4 画面モード(M1) 3 画面モード(M3) 2 未使用 1 スプライトサイズ: 1 = 8 * 16 pixel, 0 = 8 * 8 pixel 0 拡大スプライト: 1 = 有効 3. レジスター 0x02 - パターンネームテーブル ベースアドレス MSB LSB - - - - A13 A12 A11 - パターン ネーム テーブルのベースアドレスを設定します。 アドレスは上位 3 bit のみ指定するので 2 kB 単位になります。 4. レジスター 0x03 - 未使用 5. レジスター 0x04 - 未使用 6. レジスター 0x05 - スプライト アトリビュート テーブル ベースアドレス MSB LSB - A13 A12 A11 A10 A09 A08 - スプライト アトリビュート テーブルのベースアドレスを設定します。 アドレスは上位 6 bit のみ指定するので 128 Bytes 単位になります。 7. レジスター 0x06 - スプライト パターン ジェネレーター ベースアドレス MSB LSB - - - - - A13 - - スプライト パターン ジェネレーターのベースアドレスを設定します。 アドレスは上位 1 bit のみ指定するので 8kB 単位になります。 8. レジスター 0x07 - 背景色 MSB LSB - - - - C3 C2 C1 C0 背景色をスプライト用パレットの16色中から指定します。 9. レジスター 0x08 - 背景のXスクロール位置 MSB LSB X7 X6 X5 X4 X3 X2 X1 X0 背景の水平スクロール位置を指定します。 10. レジスター 0x09 - 背景のYスクロール位置 MSB LSB Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0 背景の垂直スクロール位置を指定します。 11. レジスター 0x0A - ラインカウンター MSB LSB L7 L6 L5 L4 L3 L2 L1 L0 割り込みで使うラインカウンターを設定します。 ■パターン パターンは背景とスプライトを表す画素の配列です。 パターン1枚は 8 * 8 pixel の画素数から成り、全ての画素は 4 bit で表される16色です。 この事からパターン1枚は 32 bytes の容量があり、 16 kBytes のVRAMを全てパターンで埋めた場合、512枚のパターンをVRAMに配置する事が出来ます。 /* パターンと容量の関係 */ 1 pixel = 4 bit = 0.5 byte 2 pixels = 8 bit = 1 byte 8 * 8 pixels = 32 bytes 128 * 256 pixels = 16 kBytes (16,384 Bytes) 実際にはVRAMにパターン以外のデータも配置するので、それらのデータの大きさにより、 パターンを何枚VRAMに配置出来るかが変わります。 但しスプライト アトリビュートやパターン ネーム テーブルでパターン番号を指定する場合は、 常に512枚のパターンがVRAMに配置されていると仮定してパターン番号を指定します。 パターン1枚中の1ライン (8 * 1 pixel) 当たりのフォーマットは以下の通りです。 MSB LSB C0 C0 C0 C0 C0 C0 C0 C0 1ラインの画素の 1 bit目 C1 C1 C1 C1 C1 C1 C1 C1 1ラインの画素の 2 bit目 C2 C2 C2 C2 C2 C2 C2 C2 1ラインの画素の 3 bit目 C3 C3 C3 C3 C3 C3 C3 C3 1ラインの画素の 4 bit目 これが8ライン分連続する事で、パターン1枚になります。 ■パターンネームテーブル 背景はパターンを並べた二次元配列です。 この配列はパターンネームテーブルと呼ばれ、配列の各要素は以下のフォーマットになります。 MSB LSB n7 n6 n5 n4 n3 n2 n1 n0 -- -- -- P C V H n8 P: 1 = 優先, 0 = 通常 C: 1 = 2番目のパレット, 0 = 1番目のパレット V: 1 = 垂直反転, 0 = 垂直反転しない H: 1 = 水平反転, 0 = 水平反転しない n8 〜 n0: 0 〜 511 = パターン番号 Pを優先にすると、スプライトよりも手前に表示されます。 パターンネームテーブルの大きさは画面サイズにより変わります。 画面サイズ パターンネームテーブル (pixel) (pattern) 256 * 192 32 * 28 256 * 224 32 * 32 256 * 240 32 * 32 ■スプライト 概要的には TMS9918A やファミコンのスプライトと同じです。 TMS9918A が2色、ファミコンが4色だったのに対し、マークIII・マスターシステムでは4色に増えています。 全枚数 32枚 1ライン 8枚 サイズ 8 * 8pixel 8 * 16pixel 色 64色中16色 パレット 1枚 パターン 256枚 1ライン当たりのスプライト数がオーバーしたかどうかはフラグで表されます。 サイズの選択はスプライト毎では無く全スプライトに対して共通です。 パレットは背景は2枚の中から選択出来ますが、スプライトは2枚目固定です。 ■スプライト アトリビュート テーブル スプライト アトリビュート テーブルは連続した3つの一次元配列で出来ています。 Offset 0x00: yyyyyyyyyyyyyyyy (1) Y座標の配列, 64 Bytes 0x10: yyyyyyyyyyyyyyyy 0x20: yyyyyyyyyyyyyyyy 0x30: yyyyyyyyyyyyyyyy 0x40: ???????????????? (2) 未使用の配列, 64 Bytes 0x50: ???????????????? 0x60: ???????????????? 0x70: ???????????????? 0x80: xnxnxnxnxnxnxnxn (3) X座標とパターン番号の配列, 128 Bytes 0x90: xnxnxnxnxnxnxnxn 0xA0: xnxnxnxnxnxnxnxn 0xB0: xnxnxnxnxnxnxnxn 0xC0: xnxnxnxnxnxnxnxn 0xD0: xnxnxnxnxnxnxnxn 0xE0: xnxnxnxnxnxnxnxn 0xF0: xnxnxnxnxnxnxnxn y: 0 〜 255 = Y座標 x: 0 〜 255 = X座標 n: 0 〜 255 = パターン番号 ?: 未使用 /* スプライトを1個セットする */ void sprite_set(uint8_t x, uint8_t y, uint8_t n){ uint8_t *buf; buf = &sprite_buffer[0]; buf += sprite_used; *buf = y; buf += 0x80; buf += sprite_used++; *buf++ = x; *buf = n; } ■スプライトの描画 スプライトはライン毎にスプライト アトリビュート テーブルの1番目の要素から順にスキャンされ、 スプライトが対象のライン内なら描画されます。 そして以下のいずれかの条件に当てはまった時点でスプライトのスキャンが終了します。 ・64枚全てスキャンし終わる。 ・あるスプライトのY座標が208ライン目に達している。 ・8枚のスプライトを描き終わる。 /* VBLANK中にスプライトアトリビュートのバッファーをVRAMに書き込み、バッファーをクリアする */ wait_vsync(); vram_write(VRAM_SPRITE_ATTRIBUTE, sprite_buffer, 0x100); sprite_clear(); ■割り込み 通常Z80の割り込みモード1を使用します。 他のモードではハードウェアの違いにより動作が異なります。 以下の割り込みがあります。 0x66: PAUSEボタン割り込み (NMI) 0x38: VBLANK割り込み 0x38: HBLANK割り込み ■SN76489 SG-1000ではSN76489が使われていましたが、マークIII以降ではセガの互換品が使われています。 扱い方はSN76489と変わりません。 Z80のI/Oポートは 0x7E と 0x7F が割り振られていますが、どちらも同じです。 ■メモリーコントロール Z80のI/Oポート 0x3E でメモリーコントロールを行います。 D7 : 1 = 拡張スロット無効, 0 = 有効 D6 : 1 = カートリッジ スロット無効, 0 = 有効 D5 : 1 = カード スロット無効, 0 = 有効 D4 : 1 = ワークRAM無効, 0 = 有効 D3 : 1 = BIOS ROM無効, 0 = 有効 D2 : 1 = ジョイパッド ポート無効, 0 = 有効 D1 : 未使用 D0 : 未使用 ■ジョイパッド ポート ジョイパッド ポートAとBの2つがあります。 それぞれ TH ピンと TR ピンのみ入出力方向を選択する事が出来ます。 他のピンは入力方向に固定されています。 Z80のI/Oポート 0x3F で TH ピンと TR ピンの状態と入出力方向を設定します。 D7 : ポート B の TH ピンの出力 (1 = HIGH, 0 = LOW) D6 : ポート B の TR ピンの出力 (1 = HIGH, 0 = LOW) D5 : ポート A の TH ピンの出力 (1 = HIGH, 0 = LOW) D4 : ポート A の TR ピンの出力 (1 = HIGH, 0 = LOW) D3 : ポート B の TH ピンの方向 (1 = 入力, 0 = 出力) D2 : ポート B の TR ピンの方向 (1 = 入力, 0 = 出力) D1 : ポート A の TH ピンの方向 (1 = 入力, 0 = 出力) D0 : ポート A の TR ピンの方向 (1 = 入力, 0 = 出力) Z80のI/Oポート 0xDC と 0xDD で各ピンの状態を読み取ります。 Port 0xDC D7 : ポート B ↓ ピン入力 D6 : ポート B ↑ ピン入力 D5 : ポート A TR ピン入力 D4 : ポート A TL ピン入力 D3 : ポート A → ピン入力 D2 : ポート A ← ピン入力 D1 : ポート A ↓ ピン入力 D0 : ポート A ↑ ピン入力 Port 0xDD D7 : ポート B TH ピン入力 D6 : ポート A TH ピン入力 D5 : 未使用 D4 : リセットボタン入力 D3 : ポート B TR ピン入力 D2 : ポート B TL ピン入力 D1 : ポート B → ピン入力 D0 : ポート B ← ピン入力 /* SDCCでのI/Oポートとビットの定義例 */ sfr at 0x3F PORT_CTRL; #define PORTB_TH_OUT 7 #define PORTB_TR_OUT 6 #define PORTA_TH_OUT 5 #define PORTA_TR_OUT 4 #define PORTB_TH_DIR 3 #define PORTB_TR_DIR 2 #define PORTA_TH_DIR 1 #define PORTA_TR_DIR 0 sfr at 0xDC PORT_A; #define PORTB_DOWN 7 #define PORTB_UP 6 #define PORTA_TR 5 #define PORTA_TL 4 #define PORTA_RIGHT 3 #define PORTA_LEFT 2 #define PORTA_DOWN 1 #define PORTA_UP 0 sfr at 0xDD PORT_B; #define PORTB_TH 7 #define PORTA_TH 6 #define PORT_RESET 4 #define PORTB_TR 3 #define PORTB_TL 2 #define PORTB_RIGHT 1 #define PORTB_LEFT 0 /* I/Oポートをジョイパッドとして初期化する */ void port_init(){ PORT_CTRL = 0xFF; } /* ジョイパッドAのボタンの状態を読み込む */ uint8_t pad_read(){ return ~PORT_A; } /* ジョイパッド1Pの A か B ボタンが押されるまで永久ループする */ port_init(); while(1){ wait_vsync(); if(pad_read() & ((1 << PORTA_TR) | (1 << PORTA_TL))){ break; } }