浅谈 Gameboy 的 MBC1 卡带的模拟实现
背景
MBC(Multiple Bank Controller), 是 gamboy 游戏卡带上的一个控制芯片,旨在于实现在运行过程中对同一片内存地址的不同空间映射。
从 pandoc 的内存分布我们可以得出, $0000-$7FFF
和 $A000-$BFFF
为卡带的内存地址范围。这些地址范围总共只有 32+8=40k 的空间,而 $0000-$7FFF
通常为 rom 的地址范围,也就是游戏本体的大小,而$A000-$BFFF
为游戏数据的存储范围(通常用于游戏进度的保存)。
对于简单的游戏,32k 的大小已经足够了,对于更大玩法更丰富的游戏来说,32k是远远不够的,为了解决这个问题,有几种办法:
- 扩大地址空间,Gameboy 只有 16 位地址空间。但是这样会增加硬件成本。
- 将更多的地址空间交由游戏卡带使用。
- 使用“虚拟内存”
所以,上古先辈选择了第三种方案,把不同的空间通过一些奇怪的方式映射到相应的地址上,达到 1+1>2 的效果,不得不感慨那个年代的设计和制作是如此的优秀,都是一个bit一个bit的抠,不能有丝毫的浪费。
MBC1 的寄存器
MBC1 一共有 4 个寄存器,分别为:
地址空间 | 名称 | 备注 |
---|---|---|
$0000-$1FFF | RAM Enable | 1位,控制是否启用 ram |
$2000-$3FFF | ROM Bank | 5位,控制使用第几个 rom bank |
$4000-$5FFF | RAM Bank | 2位,控制使用第几个 ram bank |
$6000-$7FFF | Bank Mode | 1位,bank 模式,分为 simple(默认)和 advance 两种,有些文章也称为 0/1 模式 |
写入上述地址范围内任意一个地址将会写入指定的寄存器。
$0000-$1FFF RAM Enable
对于写入该范围的值,低 4 位为 0xA 都会启动 RAM.
如果 RAM 关闭,任何对 RAM 的读取将会返回 0xFF
$2000-$3FFF ROM Bank
对于该范围写入的值,取低 5 位,且根据 header 中 ROM 的大小,还需要忽略一定的高位,规则如下
header[0x0148] | ROM size | ROM bank 个数 | 掩码 |
---|---|---|---|
$00 | 32 KiB | 2 (no banking) | 0x0 |
$01 | 64 KiB | 4 | 0x3 |
$02 | 128 KiB | 8 | 0x7 |
$03 | 256 KiB | 16 | 0xF |
$04 | 512 KiB | 32 | 0x1F |
$05 | 1 MiB | 64 | 0x1F |
$06 | 2 MiB | 128 | 0x1F |
$07 | 4 MiB | 256 | 0x1F |
$08 | 8 MiB | 512 | 0x1F |
$52 | 1.1 MiB | 72 | 0x1F |
$53 | 1.2 MiB | 80 | 0x1F |
$54 | 1.5 MiB | 96 | 0x1F |
由于该寄存器只有 5 位,所以最多只能 5 位为 1(0x1F)。而高于 5 位的,将会根据 Bank Mode 的模式,将 RAM bank 的两位纳入计算。
而在实际的模拟实现中,由于我们需要取有效地址,所以 5 位的掩码是不够的,我们可以通过仔细观察上面的表格,得出这个公式计算出有效位的掩码
(1 << (header[0x0148] + 1)) - 1
,或者通过 打表/if else/swich 的方式得出。
$4000-$5FFF RAM Bank
对于该范围写入的值,取低 2 位
同样的,和 ROM Bank 一样,也需要根据 header 中的大小忽略高位,规则如下
header[0x0149] | SRAM size | Comment | 掩码 |
---|---|---|---|
$00 | 0 | No RAM | 0 |
$01 | 2 KiB | 0 | |
$02 | 8 KiB | 1 bank | 0 |
$03 | 32 KiB | 4 banks of 8 KiB each | 0x3 |
$04 | 128 KiB | 16 banks of 8 KiB each | 0xF |
$05 | 64 KiB | 8 banks of 8 KiB each | 0x7 |
$6000-$7FFF Bank Mode
对于该范围写入的值,将控制卡带的工作模式 0 为 simple, 1 为 advance
RAM 的写入
$A000-$BFFF External RAM
对于该地址范围的写入将会写到相应的 RAM bank 中,如果 RAM enable 未启用,写入将被忽略。
否则:
-
Simple Mode:
则无需计算地址,直接写入即可
-
Advance Mode:
需要用以下公式计算地址
(RAM_BANK & RAM_BANK_MASK) * 0x2000 + (ADDRESS - 0xA000)
其中,
RAM_BANK
为$4000-$5FFF
寄存器的值,RAM_BANK_MASK
为上述表格中提到的掩码。
MBC1 中的内存
对于 ROM 的数据,为只读。
$0000-$3FFF ROM Bank 0
对于该区间内存的读取,受到 Bank Mode 的控制。
Simple Mode
直接返回读取地址的内容
Advance Mode
使用 RAM Bank 作为 ROM bank 根据公式计算地址:
((RAM_BANK << 5) & ROM_BANK_MASK) * 0x4000 + ADDRESS)
$4000-$7FFF
这个地址范围不受 Bank Mode 控制
使用 RAM Bank 和 ROM Bank 共同计算出地址
(((RAM_BANK << 5) | ROM_BANK) & ROM_BANK_MASK)) * 0x4000 + (ADDRESS - 0x4000)
由于
ROM_BANk
不能为 0,所以在该位置读取的地址永远不可能为 (0x20, 0x40, 0x60 和 0x00)。如果需要读取 0x20/0x40/0x60/0x00,可以设置 Advance 模式,从 $0000-$3FFF 处读取。
$A000-$BFFF
如果没有开启 RAM 读写,那么返回 0xFF。
否则将受到 Bank Mode 的控制
Simple
按地址直接返回即可
Advance
需要根据公式计算地址
(RAM_BANK & RAM_BANK_MASK) * 0x2000 + (ADDRESS - 0xA000)