Re: 從零開始的紅白機模擬 - [33] MMC5 低語
任地獄MMC5
本文github備份地址
本文編寫以及具體實現中, wiki的MMC5頁面居然進行了修改(主要是針對BANK切換的說明進行了修正).
MMC5的Mapper編號就是005, 還是有不少的遊戲(NesCartDb記錄24款, 有美版日版之分). 不過用到MMC5擴展音源的只有:
- 光榮聖戰(Just Breed)
- 金屬之光(Metal Slader Glory)
- 新四人麻將: 役滿天下(Shin 4 Nin Uchi Mahjong: Yakuman Tengoku)
- 宇宙警備隊 SDF(Uchuu Keibitai SDF)
其中值得一提的是, 金屬之光被稱為畫面最好的FC遊戲, 畢竟是可視小說(VN)類型. 其ROM大小是1MB, 在那時肯定是巨無霸水平的.
不過雖然只有少數用了擴展音源, 不過不像VRC7, MMC5沒用上的遊戲卡帶還是擁有相應的硬體(至少FC版).
PRG-RAM
MMC5支持切換RAM-BANK, 這對於目前架構來說又是致命的, 只能再想想. 但是這不是最致命的, 最致命的是信息缺失.
例如《大航海時代》, 資料庫中表明其擁有一個電池供電支持的16kb WRAM(SRAM). 一般地, 原始iNES文件頭有:
- 8: Size of PRG RAM in 8 KB units (Value 0 infers 8 KB for compatibility; see PRG RAM circuit)
亦或者NES 2.0的文件頭:
- Byte 10 (RAM size)
但是自己手上的大航海時代ROM這兩個位元組均是0. 也就是說ROM信息不夠完整, wiki給出的解決方案是: 假定為64kb大小.
強大的MMC5
MMC5性能非常地強大:
- 4種RPG-ROM切換模式
- 4種CHR-ROM切換模式
- 最高支持到128kb的WRAM, 除了WRAM的$6000-$7FFF, 甚至還能映射到$8000-$DFFF
- 還有一個8bit-8bit到16bit的乘法器
- 基於掃描線的IRQ計數器
- 能夠讓8x16精靈和背景使用不同的CHR-BANK(黑科技啊)
- "填充模式"的名稱表, 用於場景轉換很有用
- 1024位元組的on-chip內存, 用途還不少
- MMC5擴展音源(自然)
BANK 切換
PRG mode 0:
- CPU $6000-$7FFF: 8 KB switchable PRG RAM bank
- CPU $8000-$FFFF: 32 KB switchable PRG ROM bank
PRG mode 1:
- CPU $6000-$7FFF: 8 KB switchable PRG RAM bank
- CPU $8000-$BFFF: 16 KB switchable PRG ROM/RAM bank
- CPU $C000-$FFFF: 16 KB switchable PRG ROM bank
PRG mode 2:
- CPU $6000-$7FFF: 8 KB switchable PRG RAM bank
- CPU $8000-$BFFF: 16 KB switchable PRG ROM/RAM bank
- CPU $C000-$DFFF: 8 KB switchable PRG ROM/RAM bank
- CPU $E000-$FFFF: 8 KB switchable PRG ROM bank
PRG mode 3:
- CPU $6000-$7FFF: 8 KB switchable PRG RAM bank
- CPU $8000-$9FFF: 8 KB switchable PRG ROM/RAM bank
- CPU $A000-$BFFF: 8 KB switchable PRG ROM/RAM bank
- CPU $C000-$DFFF: 8 KB switchable PRG ROM/RAM bank
- CPU $E000-$FFFF: 8 KB switchable PRG ROM bank
PRG-BANK 最後一個8kb BANK一定是ROM.
CHR mode 0:
- PPU $0000-$1FFF: 8 KB switchable CHR bank
CHR mode 1:
- PPU $0000-$0FFF: 4 KB switchable CHR bank
- PPU $1000-$1FFF: 4 KB switchable CHR bank
CHR mode 2:
- PPU $0000-$07FF: 2 KB switchable CHR bank
- PPU $0800-$0FFF: 2 KB switchable CHR bank
- PPU $1000-$17FF: 2 KB switchable CHR bank
- PPU $1800-$1FFF: 2 KB switchable CHR bank
CHR mode 3:
- PPU $0000-$03FF: 1 KB switchable CHR bank
- PPU $0400-$07FF: 1 KB switchable CHR bank
- PPU $0800-$0BFF: 1 KB switchable CHR bank
- PPU $0C00-$0FFF: 1 KB switchable CHR bank
- PPU $1000-$13FF: 1 KB switchable CHR bank
- PPU $1400-$17FF: 1 KB switchable CHR bank
- PPU $1800-$1BFF: 1 KB switchable CHR bank
- PPU $1C00-$1FFF: 1 KB switchable CHR bank
CHR-BANK這裡倒是沒有什麼.
PRG mode ($5100)
7 bit 0
---- ----
xxxx xxPP
||
++- Select PRG banking mode
大部分遊戲使用的是模式3(除了惡魔城3-美版, 用了模式2). 暗榮的遊戲從來不寫入該寄存器, 可知默認是模式3.
CHR mode ($5101)
7 bit 0
---- ----
xxxx xxCC
||
++- Select CHR banking mode
金屬之光使用的是模式1, 其他的使用的是模式3
PRG RAM Protect 1 ($5102)
7 bit 0
---- ----
xxxx xxWW
||
++- RAM protect 1
D1D0位必須是10(2)才能正常寫入. 需要結合$5103.
PRG RAM Protect 2 ($5103)
7 bit 0
---- ----
xxxx xxWW
||
++- RAM protect 2
D1D0位必須是01(1)才能正常寫入. 同樣需要結合$5102.
Extended RAM mode ($5104)
7 bit 0
---- ----
xxxx xxXX
||
++- Specify extended RAM usage
- 0 -Split模式, 作為額外的名稱表使用
- 1 -Split+ExGrafix模式, 作為擴展用屬性表(自然也能用於名稱表)
- 2 -ExRAM模式, 作為一般的RAM
- 3 -ExRAM模式, 作為一般的RAM, 寫入保護
ExRAM@$000-$3BF:
- ExGrafix模式: 用於強化背景顯示
- Split模式: 待補充
- ExRAM模式: 擴展用
ExRAM@$3C0-$3FF:
- ExGrafix模式: 未使用
- Split模式: 待補充
- ExRAM模式: 擴展用
Nametable mapping ($5105)
7 bit 0
---- ----
DDCC BBAA
|||| ||||
|||| ||++- Select nametable at PPU $2000-$23FF
|||| ++--- Select nametable at PPU $2400-$27FF
||++------ Select nametable at PPU $2800-$2BFF
++-------- Select nametable at PPU $2C00-$2FFF
- 0 -自帶的VRAM-前1kb
- 1 -自帶的VRAM-後1kb
- 2 -內部擴展RAM, 不過$5104必須是模式00或者01, 否則全部讀取到0
- 3 -填充模式數據
MMC5內部實現應該是, 例如模式3, 就根據地址返回填充數據就行. 作為模擬器的話, 可以實現為:
for (int i = 0; i != 4; ++i) {
uint8_t* ptr = NULL;
switch (value & 3)
{
case 0:
ptr = famicom->video_memory + 1024 * 0;
break;
case 1:
ptr = famicom->video_memory + 1024 * 1;
break;
case 2:
//ptr = (mapper->exram_mode & 2) ? sfc_mmc5_zero_nt(famicom) : sfc_mmc5_exram(famicom);
ptr = sfc_mmc5_exram(famicom);
break;
case 3:
ptr = sfc_mmc5_fill_nt(famicom);
break;
}
value >>= 2;
base[i] = ptr;
}
Fill-mode tile ($5106)
8位均用於填充模式的圖塊編號
Fill-mode color ($5107)
低2位用於填充模式的屬性位, 實際填充的的是:
color = value & 3;
color = color | (color << 2) | (color << 4) | (color << 6);
可以具體平臺使用位運算或者查表.
PRG Bank 0, 1, 2 ($5110-5112)
不在PRG空間內, 無效.
PRG Bank 3, RAM Only ($5113)
7 bit 0
---- ----
xxxx BBBB
||||
++++- PRG RAM bank number at $6000-$7FFF
+--- Select PRG RAM chip
這個就比較複雜了, 這就是前面提到的信息不完整. 就目前而言有以下幾種情況:
- 0KB: No chips
- 8KB: 1x 8KB chip
- 16KB: 2x 8KB chip
- 32KB: 1x 32KB chip
D2位表示哪個chip. wiki建議始終假設為64kb, 然後根據這4bit(3bit)載入偏移數據, 因為2x8kb模式是寫入100而不是001.
- 實際上有能力最多支持到128kb, 但是實際上最多搭載了32kb額外RAM.
- 自己打算使用32kb的實現, 但是將D2與D1做異或運算, 然後使用D1D0進行判斷.
- 這要求遊戲使用非常標準思路的進行切換, 可能會有BUG(而且不支持64~128kb的額外RAM).
- 這樣內部本來WRAM有8kb就富餘出來了, 用於MMC5內部其他的RAM.
- 對, C/C++程序猿就是摳門.
case 0x5104:
// Extended RAM mode ($5104)
#ifndef NDEBUG
printf("[%5d]MMC5: Extended RAM mode ($5104) = %02x
", famicom->frame_counter, value & 3);
#endif
mapper->exram_mode = value & 3;
mapper->exram_write_mask_mmc5 = 0x00;
famicom->ppu.data.ppu_mode = SFC_EZPPU_Normal;
if (mapper->exram_mode == 1) {
mapper->exram_write_mask_mmc5 = 0xff;
famicom->ppu.data.ppu_mode = SFC_EXPPU_ExGrafix;
}
break;
PRG Bank 4, ROM/RAM ($5114)
7 bit 0
---- ----
RBBB BBBB
|||| ||||
|+++-++++- PRG ROM bank number
| ++++- PRG RAM bank number
| +--- Select PRG RAM chip
+--------- RAM/ROM toggle (0: RAM; 1: ROM)
- 模式 0 - 忽略
- 模式 1 - 忽略
- 模式 2 - 忽略
- 模式 3 - 選擇 8KB PRG bank @ $8000-$9FFF
PRG Bank 5, ROM/RAM ($5115)
7 bit 0
---- ----
RBBB BBBB
|||| ||||
|+++-++++- PRG ROM bank number
| ++++- PRG RAM bank number
| +--- Select PRG RAM chip
+--------- RAM/ROM toggle (0: RAM; 1: ROM)
- 模式 0 - 忽略
- 模式 1 - 選擇 16KB PRG bank @ $8000-$BFFF (忽略最低位)
- 模式 2 - 選擇 16KB PRG bank @ $8000-$BFFF (忽略最低位)
- 模式 3 - 選擇 8KB PRG bank @ $A000-$BFFF
PRG Bank 6, ROM/RAM ($5116)
7 bit 0
---- ----
RBBB BBBB
|||| ||||
|+++-++++- PRG ROM bank number
| ++++- PRG RAM bank number
| +--- Select PRG RAM chip
+--------- RAM/ROM toggle (0: RAM; 1: ROM)
- Mode 0 - 忽略
- Mode 1 - 忽略
- Mode 2 - 選擇 8KB PRG bank @ $C000-$DFFF
- Mode 3 - 選擇 8KB PRG bank @ $C000-$DFFF
PRG Bank 7, ROM Only ($5117)
7 bit 0
---- ----
xBBB BBBB
||| ||||
+++-++++- PRG ROM bank number
- 模式 0 - 選擇32KB PRG-ROM bank @ $8000-$FFFF (忽略低2位)
- 模式 1 - 選擇 16KB PRG-ROM bank @ $C000-$FFFF (忽略最低1位)
- 模式 2 - 選擇 8KB PRG-ROM bank @ $E000-$FFFF
- 模式 3 - 選擇 8KB PRG-ROM bank @ $E000-$FFFF
似乎啟動時是往$5117寫入$FF, 也就是最後8kb RPG-BANK載入最後一個BANK.
CHR Bankswitching ($5120-$5130)
前面提到了MMC5的黑科技——8x16精靈使用的圖樣表允許和背景使用的不同. 8x8模式只會使用$5120-$5127, 而8x16模式下$5120-$5127是針對精靈, $5128-$512B是針對背景.
並且, 最後一次寫入的部分(前8, 後4), 會用於 PPUDATA ($2007)
wiki提到到目前未知還不清楚MMC5是怎麼檢測PPU處於哪種模式的, 真的黑科技.
寫入地址 | 1 KiB | 2 KiB | 4 KiB | 8 KiB
--------|--------|-------|-------|-------
$5120 | BANK0 | - | - | -
$5121 | BANK1 |BANK0-1| - | -
$5122 | BANK2 | - | - | -
$5123 | BANK3 |BANK2-3|BANK0-3| -
$5124 | BANK4 | - | - | -
$5125 | BANK5 |BANK4-5| - | -
$5126 | BANK6 | - | - | -
$5127 | BANK7 |BANK5-7|BANK4-7|BANK0-7
$5128 |BANK0, 4| - | - | -
$5129 |BANK1, 5| 01,45 | - | -
$512A |BANK2, 6| - | - | -
$512B |BANK3, 7| 23,67 |0-4,5-7| 0-7
根據$5130的說法, 比如8kb模式就是選擇的是8kb為窗口的BANK編號.
Upper CHR Bank bits ($5130)
7 bit 0
---- ----
xxxx xxBB
||
++- Upper bits for subsequent CHR bank writes
當使用1kb模式時, 最多隻能訪問256kb的CHR-ROM, 要訪問整個1024kb就需要這兩位了. 不過唯一一個超過256kb CHR-ROM的金屬之光卻使用的是4kb模式. 換句話說就是沒有一個遊戲使用了這個機能(甚至連初始化都沒有).
Expansion RAM ($5C00-$5FFF, read/write)
- 模式 0/1 - 不可讀, 僅可在PPU渲染時可寫(否則寫入0)
- 模式 2 - 可讀可寫
- 模式 3 - 只讀
模式1下( ExGrafix 模式), 就是MMC5實現的一個難點了: 擴展RAM區每個位元組可以用來強化背景顯示.
7 bit 0
---- ----
AACC CCCC
|||| ||||
||++-++++- Select 4 KB CHR bank to use with specified tile
++-------- Select palette to use with specified tile
4*64=256
, 為了使用整個1024kb空間, 需要配合$5130的兩位進行使用.
這種模式下基本可以確定使用的是單屏模式下. 舉個栗子, 第一個圖塊$2000. 原本的模式下, 首先確定背景是用的哪個圖樣表, 然後利用[$2000]的數據, 獲取圖樣數據.
現在ExGrafix模式下, 會在ExRAM:$000獲取相應信息, 而本來的圖樣表幾乎完全是為精靈服務的.
高精度的模擬器應該是模擬讀取過程, 不過作為中精度的模擬器可以直接拿渲染開刀.
Split模式相關寄存器
待補充
目前只有宇宙警備隊 SDF使用了該模式, 這個模式目前不想實現(懶), 等待以後實現吧.
IRQ Counter ($5203)
用於指定掃描線id來觸發IRQ, 內部比如寫入$04會在第5條可見掃描線開始時觸發. 寫入0應該是觸發不了IRQ的. 由於是基於掃描線的, 所以應該只會在可見掃描線觸發相關同步操作.
目前的Ez模式下, 本身自己是在每條掃描線最後觸發水平同步的, 所以就是應該寫入多少就在第幾條觸發.
IRQ Status ($5204, write)
7 bit 0
---- ----
Exxx xxxx
|
+--------- IRQ Enable flag (1=IRQs enabled)
寫入僅僅用來啟用/關閉IRQ功能, 即時關閉也能在本來可以觸發IRQ情況將Pending置為1(當然不會觸發IRQ).
IRQ Status ($5204, read)
7 bit 0
---- ----
SVxx xxxx MMC5A default power-on value = $00
||
|+-------- "In Frame" signal
+--------- IRQ Pending flag
In Frame是當MMC5不再檢測到掃描線信號時, 比如最後一根掃描線掃過, 或者說PPU沒有渲染背景/精靈($2001相關位). 也就是說實際上如果中途關閉渲染, 會提前清除In Frame標誌(懶得實現).
Pending標誌會在MMC5的相關IRQ掛起時觸發, 讀取後清除(確認IRQ), 或者在In Frame0->1時也會清除.
Unsigned 8x8 to 16 Multiplier ($5205, $5206 read/write)
這就是那個16位乘法器了, 寫入會進行乘法運算. 讀取時, 低地址讀取地址, 高地址讀取高地址.
其他寄存器
其他還有一些就不介紹了
switch-case
由於地址部分連續, 部分離散, 所以只好直接用case了, 讓編譯器自己優化.
switch (address)
{
case 0x5100:
// ...
case 0x5101:
// ...
case 0x5102:
// ...
// ...
};
新介面
read_low
, 讀取[$4020, $6000), 這部分區域會調用該介面. 在正常情況下, 與之前的PRG段快速訪問優化不衝突.
區別ROM RAM
目前是使用的32bit整型保存偏移量, 但是沒有辦法區別BANK是來自ROM還是RAM. 所以現在統一用最高位區別RAM與ROM.
/// <summary>
/// StepFC: 利用指針創建偏移量
/// </summary>
/// <param name="famicom">The famicom.</param>
/// <param name="ptr">The PTR.</param>
/// <returns></returns>
static inline uint32_t sfc_make_offset(sfc_famicom_t* famicom, const uint8_t* ptr) {
const uint8_t* const fc0 = famicom->video_memory;
const uint8_t* const fc1 = (const uint8_t*)(famicom + 1);
// RAM
if (ptr >= fc0 && ptr < fc1) {
const uintptr_t rv = ptr - fc0;
// 256 MiB夠大了
assert(rv < 0x10000000);
return (uint32_t)rv;
}
// ROM
else {
const uintptr_t rv = ptr - famicom->rom_info.data_prgrom;
// 256 MiB夠大了
assert(rv < 0x10000000);
return (uint32_t)rv | (uint32_t)0x80000000;
}
}
/// <summary>
/// StepFC: 利用偏移量創建指針
/// </summary>
/// <param name="famicom">The famicom.</param>
/// <param name="offset">The offset.</param>
/// <returns></returns>
static inline uint8_t* sfc_make_pointer(sfc_famicom_t* famicom, uint32_t offset) {
// ROM
if (offset & 0x80000000) return famicom->rom_info.data_prgrom + (offset & 0x7fffffff);
// RAM
else return famicom->video_memory + offset;
}
之前的文件頭"-StepFC-SRAMWRAM"完全沒有必要, 但是看著文件管理器顯示9kb有點煩, 乾脆去掉了. 完全根據大小信息判斷:
- 8kb: SRAM
- 32kb: MMC5 PRG-RAM