Game boy模拟器:内存池
Posted 妇男主人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器:内存池相关的知识,希望对你有一定的参考价值。
到目前为止,在本系列中,我们一直在处理GameBoy的简单内存映射的加载和仿真,整个游戏 ROM 都适合内存的下半部分。完全适合内存的游戏并不多(俄罗斯方块是少数游戏之一);大多数游戏都比这大,并且必须采用独立机制将游戏 ROM 的“银行”交换到 GameBoy CPU 的视图中。
GameBoy 库中的一些第一批游戏是在卡带内使用内存库控制器构建的,它完成了将 ROM 库交换到视图中的工作;一直以来,各种版本的卡带 MBC 都是为越来越大的游戏而设计的。在与此部分相关的演示的特定示例中,MBC 的第一个版本用于处理 64kB ROM 的加载。
内存池与延展
多年来,许多计算机系统不得不处理程序过多而无法装入内存的问题。传统上,有两种方法可以解决这个问题。
- 增加地址空间:构建具有更多地址线的新 CPU,使其能够查看和理解更大的内存量。这是首选的解决方案,但需要大量时间来重新开发有问题的计算机系统,并且可能需要对 CPU 的支持芯片组进行更多更改。
- 虚拟内存:这可以指在磁盘上保存大块 RAM,并在需要时进行交换;或者在需要时交换预先写入的 ROM 块。在这两种情况下,系统硬件几乎不需要扩展,但系统的任何软件都必须知道寻呼/银行系统,以便使用它。
由于GameBoy是一个固定的硬件平台,分布广泛,在制作较大的游戏时没有办法增加地址空间;取而代之的是,内置在卡带中的内存库控制器提供了一种将 16kB ROM 库切换到视图中的方法。除此之外,MBC1 支持高达 32kB 的“外部 RAM”,这是盒式存储器中的可写存储器;[A000-BFFF]如果可用,这可以存入内存映射中的空间。
为了方便软件使用MBC1,ROM的第一个16kB bank(bank 0)被固定在地址处0000;ROM 空间的后半部分可以在 1 到 127 之间的任何 ROM 组上制作一个窗口,最大 ROM 大小为 2048kB。MBC1 的一个奇怪之处在于它在 32 位内部进行交易:银行 #32、#64 和 #96 无法访问,因为它们在银行系统内被视为银行 #0。这意味着除了固定 bank #0 之外还有 125 个 bank 可用。
MBC1 芯片中有四个寄存器,可以切换 ROM 和 RAM 的存储体;这些可以通过写入特定范围内的(通常是只读的)ROM 空间来更改。详细信息如下表所示。
MBC1 寄存器组
Locations | Register | Details |
---|---|---|
0000-1FFF | Enable external RAM | 4 bits wide; value of 0x0A enables RAM, any other value disables |
2000-3FFF | ROM bank (low 5 bits) | Switch between banks 1-31 (value 0 is seen as 1) |
4000-5FFF | ROM bank (high 2 bits) RAM bank | ROM mode: switch ROM bank "set" {1-31}-{97-127} RAM mode: switch RAM bank 0-3 |
6000-7FFF | Mode | 0: ROM mode (no RAM banks, up to 2MB ROM) 1: RAM mode (4 RAM banks, up to 512kB ROM) |
MBCs and the cartridge header
由于有多种用于内存池业务的控制器,任何给定的游戏都必须在卡带头数据中说明使用的是哪个 MBC。这是盒式 ROM 中的第一个数据块,并遵循特定格式。
Cartridge header format
Location(s) | Value | Size (bytes) | Details |
---|---|---|---|
0100-0103h | Entry point | 4 | Where the game starts Usually "NOP; JP 0150h" |
0104-0133h | Nintendo logo | 48 | Used by the Bios to verify checksum |
0134-0143h | Title | 16 | Uppercase, padded with 0 |
0144-0145h | Publisher | 2 | Used by newer GameBoy games |
0146h | Super GameBoy flag | 1 | Value of 3 indicates SGB support |
0147h | Cartridge type | 1 | MBC type/extras |
0148h | ROM size | 1 | Usually between 0 and 7 Size = 32kB << [0148h] |
0149h | RAM size | 1 | Size of external RAM |
014Ah | Destination | 1 | 0 for Japan market, 1 otherwise |
014Bh | Publisher | 1 | Used by older GameBoy games |
014Ch | ROM version | 1 | Version of the game, usually 0 |
014Dh | Header checksum | 1 | Checked by BIOS before loading |
014E-014Fh | Global checksum | 2 | Simple summation, not checked |
0150h | Start of game |
在这种特殊情况下,我们0147h对cartridge 类型的值感兴趣。如果cartridge 安装了 MBC1,cartridge 类型可以是以下值之一:
与 MBC1 相关的 Cartridge 类型值
Value | Definition |
---|---|
00h | No MBC |
01h | MBC1 |
02h | MBC1 with external RAM |
03h | MBC1 with battery-backed external RAM |
就本文而言,不会为外部 RAM 实施电池后备系统;这个特性经常被游戏用来保存它们的状态以备后用,我们将在后面的部分更详细地介绍。
MBC1的实现
存储库控制器显然是对内存的一种操作,因此非常适合 MMU。由于第一个 ROM 组(组 #0)是固定的,因此只需要为 MBC 维护一个偏移量,以指示第二个组的读取位置。为了允许稍后添加更多 MBC 处理,可以使用一组数据来保存给定控制器的状态:
MMU.js:MBC 状态和重置
MMU = {
// MBC states
_mbc: [],
// Offset for second ROM bank
_romoffs: 0x4000,
// Offset for RAM bank
_ramoffs: 0x0000,
// Copy of the ROM's cartridge-type value
_carttype: 0,
reset: function()
{
...
// In addition to previous reset code,
// initialise MBC internal data
MMU._mbc[0] = {};
MMU._mbc[1] = {
rombank: 0, // Selected ROM bank
rambank: 0, // Selected RAM bank
ramon: 0, // RAM enable switch
mode: 0 // ROM/RAM expansion mode
};
MMU._romoffs = 0x4000;
MMU._ramoffs = 0x0000;
},
load: function(file)
{
...
MMU._carttype = MMU._rom.charCodeAt(0x0147);
}
}
从上面的代码中可以看出,MBC1 的四个寄存器的内部状态由 MMU 内的一个对象表示,与 MBC 类型 1 相关联。当这些改变时,可以修改 ROM 和 RAM 偏移量以指向适当的记忆库;一旦设置了指针,对内存的访问几乎可以照常进行。
MMU.js:基于 MBC1 的访问
MMU = {
rb: function(addr)
{
switch(addr & 0xF000)
{
...
// ROM (switched bank)
case 0x4000:
case 0x5000:
case 0x6000:
case 0x7000:
return MMU._rom.charCodeAt(MMU._romoffs +
(addr & 0x3FFF));
// External RAM
case 0xA000:
case 0xB000:
return MMU._eram[MMU._ramoffs +
(addr & 0x1FFF)];
}
}
};
这些指针偏移量的计算是在写入 MBC 寄存器时进行的,如下所示。
MMU.js:MBC1控制器
wb: function(addr, val)
{
switch(addr & 0xF000)
{
// MBC1: External RAM switch
case 0x0000:
case 0x1000:
switch(MMU._carttype)
{
case 2:
case 3:
MMU._mbc[1].ramon =
((val & 0x0F) == 0x0A) ? 1 : 0;
break;
}
break;
// MBC1: ROM bank
case 0x2000:
case 0x3000:
switch(MMU._carttype)
{
case 1:
case 2:
case 3:
// Set lower 5 bits of ROM bank (skipping #0)
val &= 0x1F;
if(!val) val = 1;
MMU._mbc[1].rombank =
(MMU._mbc[1].rombank & 0x60) + val;
// Calculate ROM offset from bank
MMU._romoffs = MMU._mbc[1].rombank * 0x4000;
break;
}
break;
// MBC1: RAM bank
case 0x4000:
case 0x5000:
switch(MMU._carttype)
{
case 1:
case 2:
case 3:
if(MMU._mbc[1].mode)
{
// RAM mode: Set bank
MMU._mbc[1].rambank = val & 3;
MMU._ramoffs = MMU._mbc[1].rambank * 0x2000;
}
else
{
// ROM mode: Set high bits of bank
MMU._mbc[1].rombank =
(MMU._mbc[1].rombank & 0x1F) +
((val & 3) << 5);
MMU._romoffs = MMU._mbc[1].rombank * 0x4000;
}
break;
}
break;
// MBC1: Mode switch
case 0x6000:
case 0x7000:
switch(MMU._carttype)
{
case 2:
case 3:
MMU._mbc[1].mode = val & 1;
break;
}
break;
...
// External RAM
case 0xA000:
case 0xB000:
MMU._eram[MMU._ramoffs + (addr & 0x1FFF)] = val;
break;
}
}
在上面的控制代码中,声明为附加了外部 RAM 的 MBC1 实例是具有 RAM 存储区的实例。有了这段代码,图 1 所示的演示就可以正常加载并运行了;如果没有 MBC1 处理程序,代码将在尝试访问显示的精灵和背景数据时崩溃。
接下来
除了能够将更大的游戏放入内存之外,游戏更重要的方面之一是保持时间的能力:例如,基于时钟的游戏如果没有某种计时机制作为其时钟的基础就毫无用处. 如前所述,很多游戏都使用垂直消隐中断来做这个计时,但有些游戏需要更细粒度的时间结构;这是由 GameBoy 中的硬件计时器提供的,与 CPU 时钟相关联。
计时器还提供了一种检查 CPU 时钟的方法,这使它可用作随机数生成器的种子;例如,俄罗斯方块使用硬件定时器的这个功能来挑选它的块。在下一部分中,我将详细了解计时器的工作原理以及它的实现方式。
以上是关于Game boy模拟器:内存池的主要内容,如果未能解决你的问题,请参考以下文章