内存记忆体
在本系列的前一部分中,计算机被介绍为一个处理单元,它从内存中获取指令。几乎在所有情况下,计算机的内存都不是简单的连续区域; GameBoy 在这方面也不例外。由于 GameBoy CPU 可以访问其地址总线上的 65,536 个单独位置,因此可以绘制 CPU 可以访问的所有区域的“内存映射”。
详细地查看内存区域
[0000-3FFF] 卡带程序
卡带程序的前 16,384 个字节在内存映射中始终可用。特殊情况适用:
[0000-00FF] BIOS
当 CPU 启动时,PC 从 0000h 开始,这是 256 字节 GameBoy BIOS 代码的开始。一旦 BIOS 运行,它就会从内存映射中删除,并且盒式 ROM 的这个区域变得可寻址。
[0100-014F] 卡带头部
这一部分包含有关其名称和制造商的数据,并且必须以特定格式写入。
[4000-7FFF] 卡带程序(其他)
任何后续的16k 的盒式程序都可以在这里 一一 提供给CPU;卡带上的芯片通常用于在段之间切换,并使特定区域可访问。最小的程序是32k,这意味着不需要选段芯片。
[8000-9FFF] 显存
图形子系统使用的背景和角色所需的数据保存在这里,并且可以通过卡带程序进行更改。
[A000-BFFF] 卡带(拓展)RAM
GameBoy 中有少量可写内存;如果制作的游戏需要的 RAM 多于硬件中可用的 RAM,则可以在此处设置额外的 8k 块 RAM 可寻址。
[C000-DFFF] 运行内存RAM
GameBoy 的内部 8k RAM,可由 CPU 读取或写入。
[E000-FDFF] 运行内存RAM(映射)
由于 GameBoy 硬件的接线,工作 RAM 的精确副本可在内存映射中高出 8k。此副本在映射的最后 512 字节之前可用,其他区域可在此处访问。
[FE00-FE9F] 图形:角色信息
图形芯片渲染的角色数据保存在这里,包括精灵的位置和属性。
[FF00-FF7F] 内存 I/O 映射
GameBoy 的每个子系统(图形、声音等)都有控制值,以允许程序创建效果和使用硬件。这些值可直接在该区域的地址总线上提供给 CPU。
[FF80-FFFF] 零页 RAM
内存顶部有一个 128 字节的高速 RAM 区域。虽然这是内存的“页面”255,但它被称为第 0 页,因为程序和 GameBoy 硬件之间的大部分交互都是通过使用这页内存发生的。
与 CPU 的接口
为了让仿真 CPU 分别访问这些区域,每个区域都必须在内存管理单元中作为特殊情况处理。这部分代码在前面的部分已经提到过,是为MMU对象描述的一个基本接口;接口的完善可以像 switch 语句一样简单。
文件:MMU.js
MMU = {
_bios: [
0x31, 0xFE, 0xFF, 0xAF, 0x21, 0xFF, 0x9F, 0x32, 0xCB, 0x7C, 0x20, 0xFB, 0x21, 0x26, 0xFF, 0x0E,
0x11, 0x3E, 0x80, 0x32, 0xE2, 0x0C, 0x3E, 0xF3, 0xE2, 0x32, 0x3E, 0x77, 0x77, 0x3E, 0xFC, 0xE0,
0x47, 0x11, 0x04, 0x01, 0x21, 0x10, 0x80, 0x1A, 0xCD, 0x95, 0x00, 0xCD, 0x96, 0x00, 0x13, 0x7B,
0xFE, 0x34, 0x20, 0xF3, 0x11, 0xD8, 0x00, 0x06, 0x08, 0x1A, 0x13, 0x22, 0x23, 0x05, 0x20, 0xF9,
0x3E, 0x19, 0xEA, 0x10, 0x99, 0x21, 0x2F, 0x99, 0x0E, 0x0C, 0x3D, 0x28, 0x08, 0x32, 0x0D, 0x20,
0xF9, 0x2E, 0x0F, 0x18, 0xF3, 0x67, 0x3E, 0x64, 0x57, 0xE0, 0x42, 0x3E, 0x91, 0xE0, 0x40, 0x04,
0x1E, 0x02, 0x0E, 0x0C, 0xF0, 0x44, 0xFE, 0x90, 0x20, 0xFA, 0x0D, 0x20, 0xF7, 0x1D, 0x20, 0xF2,
0x0E, 0x13, 0x24, 0x7C, 0x1E, 0x83, 0xFE, 0x62, 0x28, 0x06, 0x1E, 0xC1, 0xFE, 0x64, 0x20, 0x06,
0x7B, 0xE2, 0x0C, 0x3E, 0x87, 0xF2, 0xF0, 0x42, 0x90, 0xE0, 0x42, 0x15, 0x20, 0xD2, 0x05, 0x20,
0x4F, 0x16, 0x20, 0x18, 0xCB, 0x4F, 0x06, 0x04, 0xC5, 0xCB, 0x11, 0x17, 0xC1, 0xCB, 0x11, 0x17,
0x05, 0x20, 0xF5, 0x22, 0x23, 0x22, 0x23, 0xC9, 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, 0x3c, 0x42, 0xB9, 0xA5, 0xB9, 0xA5, 0x42, 0x4C,
0x21, 0x04, 0x01, 0x11, 0xA8, 0x00, 0x1A, 0x13, 0xBE, 0x20, 0xFE, 0x23, 0x7D, 0xFE, 0x34, 0x20,
0xF5, 0x06, 0x19, 0x78, 0x86, 0x23, 0x05, 0x20, 0xFB, 0x86, 0x20, 0xFE, 0x3E, 0x01, 0xE0, 0x50
],
_rom: '',
_carttype: 0,
_mbc: [
{},
{rombank:0, rambank:0, ramon:0, mode:0}
],
_romoffs: 0x4000,
_ramoffs: 0,
_eram: [],
_wram: [],
_zram: [],
_inbios: 1,
_ie: 0,
_if: 0,
reset: function() {
for(i=0; i<8192; i++) MMU._wram[i] = 0;
for(i=0; i<32768; i++) MMU._eram[i] = 0;
for(i=0; i<127; i++) MMU._zram[i] = 0;
MMU._inbios=1;
MMU._ie=0;
MMU._if=0;
MMU._carttype=0;
MMU._mbc[0] = {};
MMU._mbc[1] = {rombank:0, rambank:0, ramon:0, mode:0};
MMU._romoffs=0x4000;
MMU._ramoffs=0;
LOG.out('MMU', 'Reset.');
},
load: function(file) {
b=new BinFileReader(file);
MMU._rom=b.readString(b.getFileSize(), 0);
MMU._carttype = MMU._rom.charCodeAt(0x0147);
LOG.out('MMU', 'ROM loaded, '+MMU._rom.length+' bytes.');
},
rb: function(addr) {
switch(addr&0xF000)
{
// ROM bank 0
case 0x0000:
if(MMU._inbios)
{
if(addr<0x0100) return MMU._bios[addr];
else if(Z80._r.pc == 0x0100)
{
MMU._inbios = 0;
LOG.out('MMU', 'Leaving BIOS.');
}
}
else
{
return MMU._rom.charCodeAt(addr);
}
case 0x1000:
case 0x2000:
case 0x3000:
return MMU._rom.charCodeAt(addr);
// ROM bank 1
case 0x4000: case 0x5000: case 0x6000: case 0x7000:
return MMU._rom.charCodeAt(MMU._romoffs+(addr&0x3FFF));
// VRAM
case 0x8000: case 0x9000:
return GPU._vram[addr&0x1FFF];
// External RAM
case 0xA000: case 0xB000:
return MMU._eram[MMU._ramoffs+(addr&0x1FFF)];
// Work RAM and echo
case 0xC000: case 0xD000: case 0xE000:
return MMU._wram[addr&0x1FFF];
// Everything else
case 0xF000:
switch(addr&0x0F00)
{
// Echo RAM
case 0x000: case 0x100: case 0x200: case 0x300:
case 0x400: case 0x500: case 0x600: case 0x700:
case 0x800: case 0x900: case 0xA00: case 0xB00:
case 0xC00: case 0xD00:
return MMU._wram[addr&0x1FFF];
// OAM
case 0xE00:
return ((addr&0xFF)<0xA0) ? GPU._oam[addr&0xFF] : 0;
// Zeropage RAM, I/O, interrupts
case 0xF00:
if(addr == 0xFFFF) { return MMU._ie; }
else if(addr > 0xFF7F) { return MMU._zram[addr&0x7F]; }
else switch(addr&0xF0)
{
case 0x00:
switch(addr&0xF)
{
case 0: return KEY.rb(); // JOYP
case 4: case 5: case 6: case 7:
return TIMER.rb(addr);
case 15: return MMU._if; // Interrupt flags
default: return 0;
}
case 0x10: case 0x20: case 0x30:
return 0;
case 0x40: case 0x50: case 0x60: case 0x70:
return GPU.rb(addr);
}
}
}
},
rw: function(addr) { return MMU.rb(addr)+(MMU.rb(addr+1)<<8); },
wb: function(addr,val) {
switch(addr&0xF000)
{
// ROM bank 0
// MBC1: Turn external RAM on
case 0x0000: case 0x1000:
switch(MMU._carttype)
{
case 1:
MMU._mbc[1].ramon = ((val&0xF)==0xA)?1:0;
break;
}
break;
// MBC1: ROM bank switch
case 0x2000: case 0x3000:
switch(MMU._carttype)
{
case 1:
MMU._mbc[1].rombank &= 0x60;
val &= 0x1F;
if(!val) val=1;
MMU._mbc[1].rombank |= val;
MMU._romoffs = MMU._mbc[1].rombank * 0x4000;
break;
}
break;
// ROM bank 1
// MBC1: RAM bank switch
case 0x4000: case 0x5000:
switch(MMU._carttype)
{
case 1:
if(MMU._mbc[1].mode)
{
MMU._mbc[1].rambank = (val&3);
MMU._ramoffs = MMU._mbc[1].rambank * 0x2000;
}
else
{
MMU._mbc[1].rombank &= 0x1F;
MMU._mbc[1].rombank |= ((val&3)<<5);
MMU._romoffs = MMU._mbc[1].rombank * 0x4000;
}
}
break;
case 0x6000: case 0x7000:
switch(MMU._carttype)
{
case 1:
MMU._mbc[1].mode = val&1;
break;
}
break;
// VRAM
case 0x8000: case 0x9000:
GPU._vram[addr&0x1FFF] = val;
GPU.updatetile(addr&0x1FFF, val);
break;
// External RAM
case 0xA000: case 0xB000:
MMU._eram[MMU._ramoffs+(addr&0x1FFF)] = val;
break;
// Work RAM and echo
case 0xC000: case 0xD000: case 0xE000:
MMU._wram[addr&0x1FFF] = val;
break;
// Everything else
case 0xF000:
switch(addr&0x0F00)
{
// Echo RAM
case 0x000: case 0x100: case 0x200: case 0x300:
case 0x400: case 0x500: case 0x600: case 0x700:
case 0x800: case 0x900: case 0xA00: case 0xB00:
case 0xC00: case 0xD00:
MMU._wram[addr&0x1FFF] = val;
break;
// OAM
case 0xE00:
if((addr&0xFF)<0xA0) GPU._oam[addr&0xFF] = val;
GPU.updateoam(addr,val);
break;
// Zeropage RAM, I/O, interrupts
case 0xF00:
if(addr == 0xFFFF) { MMU._ie = val; }
else if(addr > 0xFF7F) { MMU._zram[addr&0x7F]=val; }
else switch(addr&0xF0)
{
case 0x00:
switch(addr&0xF)
{
case 0: KEY.wb(val); break;
case 4: case 5: case 6: case 7: TIMER.wb(addr, val); break;
case 15: MMU._if = val; break;
}
break;
case 0x10: case 0x20: case 0x30:
break;
case 0x40: case 0x50: case 0x60: case 0x70:
GPU.wb(addr,val);
break;
}
}
break;
}
},
ww: function(addr,val) { MMU.wb(addr,val&255); MMU.wb(addr+1,val>>8); }
};
在上面的代码段中,需要注意的是,0xFF00 和 0xFF7F 之间的内存区域是未处理的;这些位置用作提供 I/O 的各种芯片的内存映射 I/O,并将在后面的部分中介绍这些系统时对其进行定义。写入字节的处理方式非常相似;每个操作都是相反的,值被写入内存的各个区域,而不是从函数返回。
加载 ROM
正如 CPU 仿真没有其内存访问、图形等支持元素就毫无用处一样,如果没有加载程序,能够从内存中读取程序也毫无用处。将程序拉入模拟器有两种主要方法:将其硬编码到模拟器的源代码中,或允许从某个位置加载 ROM 文件。硬编码程序的明显缺点是它是固定的,不能轻易更改。
对于这个 JavaScript 模拟器,GameBoy BIOS 被硬编码到 MMU 中,因为它不容易改变;然而,在模拟器初始化之后,程序文件是从服务器异步加载的。这可以通过 XMLHTTP 完成,使用二进制文件阅读器,例如 Andy Na 的 BinFileReader;其结果是一个包含 ROM 文件的字符串。
内存加载ROM可以看上面的 load
方法,里面的 BinFileReader
来自于下面的 fileread.js 源码
文件:fileread.js
/**
* BinFileReader.js
* You can find more about this function at
* http://nagoon97.com/reading-binary-files-using-ajax/
*
* Copyright (c) 2008 Andy G.P. Na <nagoon97@naver.com>
* The source code is freely distributable under the terms of an MIT-style license.
*/
function BinFileReader(fileURL){
var _exception = {};
_exception.FileLoadFailed = 1;
_exception.EOFReached = 2;
var filePointer = 0;
var fileSize = -1;
var fileContents;
this.getFileSize = function(){
return fileSize;
}
this.getFilePointer = function(){
return filePointer;
}
this.movePointerTo = function(iTo){
if(iTo < 0) filePointer = 0;
else if(iTo > this.getFileSize()) throwException(_exception.EOFReached);
else filePointer = iTo;
return filePointer;
};
this.movePointer = function(iDirection){
this.movePointerTo(filePointer + iDirection);
return filePointer;
};
this.readNumber = function(iNumBytes, iFrom){
iNumBytes = iNumBytes || 1;
iFrom = iFrom || filePointer;
this.movePointerTo(iFrom + iNumBytes);
var result = 0;
for(var i=iFrom + iNumBytes; i>iFrom; i--){
result = result * 256 + this.readByteAt(i-1);
}
return result;
};
this.readString = function(iNumChars, iFrom){
iNumChars = iNumChars || 1;
iFrom = iFrom || filePointer;
this.movePointerTo(iFrom);
var result = "";
var tmpTo = iFrom + iNumChars;
for(var i=iFrom; i<tmpTo; i++){
result += String.fromCharCode(this.readNumber(1));
}
return result;
};
this.readUnicodeString = function(iNumChars, iFrom){
iNumChars = iNumChars || 1;
iFrom = iFrom || filePointer;
this.movePointerTo(iFrom);
var result = "";
var tmpTo = iFrom + iNumChars*2;
for(var i=iFrom; i<tmpTo; i+=2){
result += String.fromCharCode(this.readNumber(2));
}
return result;
};
function throwException(errorCode){
switch(errorCode){
case _exception.FileLoadFailed:
throw new Error('Error: Filed to load "'+fileURL+'"');
break;
case _exception.EOFReached:
throw new Error("Error: EOF reached");
break;
}
}
function BinFileReaderImpl_IE(fileURL){
var vbArr = BinFileReaderImpl_IE_VBAjaxLoader(fileURL);
fileContents = vbArr.toArray();
fileSize = fileContents.length-1;
if(fileSize < 0) throwException(_exception.FileLoadFailed);
this.readByteAt = function(i){
return fileContents[i];
}
}
function BinFileReaderImpl(fileURL){
var req = new XMLHttpRequest();
req.open('GET', fileURL, false);
//XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]
req.overrideMimeType('text/plain; charset=x-user-defined');
req.send(null);
if (req.status != 200) throwException(_exception.FileLoadFailed);
fileContents = req.responseText;
fileSize = fileContents.length;
this.readByteAt = function(i){
return fileContents.charCodeAt(i) & 0xff;
}
}
if(/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent))
BinFileReaderImpl_IE.apply(this, [fileURL]);
else
BinFileReaderImpl.apply(this, [fileURL]);
}
document.write('<script type="text/vbscript">\n\
Function BinFileReaderImpl_IE_VBAjaxLoader(fileName)\n\
Dim xhr\n\
Set xhr = CreateObject("Microsoft.XMLHTTP")\n\
\n\
xhr.Open "GET", fileName, False\n\
\n\
xhr.setRequestHeader "Accept-Charset", "x-user-defined"\n\
xhr.send\n\
\n\
Dim byteArray()\n\
\n\
if xhr.Status = 200 Then\n\
Dim byteString\n\
Dim i\n\
\n\
byteString=xhr.responseBody\n\
\n\
ReDim byteArray(LenB(byteString))\n\
\n\
For i = 1 To LenB(byteString)\n\
byteArray(i-1) = AscB(MidB(byteString, i, 1))\n\
Next\n\
End If\n\
\n\
BinFileReaderImpl_IE_VBAjaxLoader=byteArray\n\
End Function\n\
</script>');
由于 ROM 文件保存为字符串,而不是数字数组,因此必须更改 rb 和 wb 函数以索引字符串:
ROM 文件索引
case 0x1000:
case 0x2000:
case 0x3000:
return MMU._rom.charCodeAt(addr);
下一步
有了 CPU 和 MMU,就可以逐步观察正在执行的程序:可以实现仿真,并在正确的寄存器中生成预期值。缺少的是对图形输出意味着什么的感觉。在本系列的下一部分中,将研究图形问题,包括 GameBoy 如何构建其图形输出,以及如何将图形渲染到屏幕上。