游戏ROM的扩容

  • 对已有的Gameboy游戏文件进行修改时,最经常碰到的问题是空间受限,没有多余的空间了。通过扩容和【存储块】切换技术,游戏修改者可以获得更多自由空间。
    在明晰原理后,整个实施过程非常简单。以MBC5型卡带为例。假设我们手里的需要扩容的ROM为1MiB(ROM00~ROM3F)。首先利用软件对原ROM追加1MiB,得到2MiB的新ROM,获得了空白的ROM40~ROM7F。使用新空间时,向0x2000~0x2FFF 的任一地址写入【存储块】编号,即可切换【存储块】。
    如,
    LD A, $60
    LD ($2000), A
    就将ROM60调入到了0x4000 ~ 0x7FFF 寻址空间。


    以下为详细内容,文末附有扩容小软件FGBDemo。
    -----
    1.Gameboy的程序ROM寻址空间设计
    Gameboy 拥有8位中央处理器和16位地址线。因此Gameboy的最大寻址空间是2的16次方。通常我们以16进制表示寻址空间,其为0x0000 ~ 0xFFFF。若以字节表示,则Gameboy的寻址能力有64KiB。
    1KiB =1024 Bytes
    4Mib = 512 KiB
    这64KiB空间要分配给游戏程序,显存,硬件寄存器等。游戏程序耗费空间最多,分配到了前32KiB地址(0x0000 ~ 0x7FFF)。尽管在Gameboy全盛时期,存储工业的发展技术不足以支持现在动辄数十GiB的游戏,但是32KiB的空间对于当时的游戏来说,也是不足够的。(如口袋妖怪的游戏ROM就有1MiB或2MiB)。为了让中央处理器可以访问到游戏ROM的每一处数据,Gameboy采用了【存储块切换技术】。
    所谓【存储块切换技术】,简单来说,就是将游戏ROM按照16KiB大小,依次划分为若干【存储块】,并顺次编号(ROM0,ROM,ROM2,… )。在游戏运行过程中,Gameboy最多同时访问两个【存储块】(每个16KiB),也就是32KiB的游戏程序寻址空间。特别地,0x0000-0x3FFFF永远对应ROM0,而0x4000-0x7FFF则可在游戏运行中被自由切换。比如,某一时刻,游戏中载入的是0号和1号【存储块】,需要显示新的地图时,该地图数据存储在5号【存储块】。为了显示新地图,Gameboy决定放弃对1号【存储块】的访问,转而去访问5号。Gameboy通过【存储块切换技术】将1号替换为5号,这个时候,Gameboy的32KiB游戏程序寻址空间就成为了ROM0和ROM5。通过这样的技术,5号【存储块】的内容就可以被Gameboy自由的使用了。通常,【存储块】的编号一般用十六进制表示。

    【存储块】编号 ROM的真实地址
    ROM0 0x0000-0x3FFF
    ROM1 0X4000-0X7FFF
    ROM2 0x8000-0xBFFF
    ROM3 0xC000-0xFFFF
    ROM4 0x10000-0x13FFF
    ROM5 0x14000-0x17FFF
    ROM6 0x18000-0x1BFFF
    ROM7 0x1C000-0x1FFF
    ... ...


    …按16KiB(0x4000)大小递增,依次类推 …



    既然一个ROM【存储块】有16KiB大小,那么我们可以知道,
    若游戏ROM有128KiB大小,则有8个【存储块】:ROM0~ROM8.
    若游戏ROM有512KiB大小,则有32个【存储块】:ROM0~ROM1F。注意32的用十六进制表示就是0x1F。
    若游戏ROM有1MiB(1024KiB)大小,则有64个【存储块】:ROM0~ROM3F。注意,64用16进制表示就是0x3F。
    依次类推。。。


    2.扩容
    尽管可以通过切换【存储块】获得更多使用空间,可如果所有内存块都不够游戏修改者使用,那就只好扩容了。扩容,就是扩大ROM的容量。一般在原有ROM文件后面追加若干0xFF或者0x00字节,从而扩大ROM的尺寸,创造新的可自由使用的【存储块】。
    一个最简单的例子:
    如果游戏ROM只有2个字节大小如
    0x3C 0x2F
    那么扩容就是在原ROM后追加若干无意义数据(0xFF或0x00)供修改者修改使用,变成
    0x3C 0x2F 0xFF 0xFF 0xFF
    如上面,扩容后的ROM变成了5个字节。
    同样,1MiB大小的ROM也可以通过追加字节的方法,变成2MiB,3MiB甚至更多。由于追加的都是空白内容,游戏修改者可以在新追加的空间里自由发挥,编写自己的代码。
    扩容本是无拘束的,但Gameboy的ROM尺寸规格是必须符合规定,即ROM的大小必须是下列尺寸的一种(单位为KiB)
    32, 64, 128, 256, 512, 1024,2048,4096,8192
    也就是说,如果手里的游戏ROM为512KiB,那么扩容后的ROM必须为1024,2048,等等更大的尺寸。
    游戏修改者可自行编写程序,给原ROM追加内容。本文亦附上一个正在开发中的工具FGBDemo。需要用命令行工具打开,操作命令为
    fgbdemo –p 填充内容 目标尺寸 游戏文件名(不需要后缀.gb/.gbc)
    各参数之间用空格隔开。如要将1MiB大小的ROM文件 hello.gb 扩容为2MiB(2048KiB),操作命令为
    fgbdemo –p 0xFF 2048 hello


    3.存储块切换
    扩容之后,有了新的地方可以写自己的代码,但是如何让Gameboy读取这些代码呢?这个时候需要用到【存储块切换技术】。被选中的【存储块】将会被载入原ROM1的范围(0x4000-0x7FFF)。每一种卡带都有不同的【存储块】切换技术,以下详说:


    MBC1
    MBC1有两种模式,最大ROM可以为512KiB或2MiB
    当最大支持ROM为512KiB时,
    向地址范围02000-0x3FFF任一地址写入0x01-0x1F即可选中对应内存块



    Code
    1. LD A, $0C
    2. LD ($2000), A


    当上面的代码被运行后,当Gameboy访问0x4000-0x7FFF时,访问的将是ROM0C的内容
    当2MiB大小的ROM,即128个内存块(ROM0-ROM7F)
    向地址范围02000-0x3FFF任一地址写入0x01-0x1F,即可选中对应【存储块】(0x01-0x1F),若同时再向0x4000-0x5FFF任一地址写入ROM编号的最高两位,则可选中【存储块】(0x20-0x7F)

    Code
    1. LD A, $00
    2. LD ($2000), A
    3. LD A, $01
    4. LD ($4000), A

    当上面的代码被运行后,当Gameboy访问0x4000-0x7FFF时,访问的将是ROM20的内容



    MBC2
    最多支持256KiB大小的ROM,即16个内存块
    向地址范围02100-0x21FF任一地址写入0x01-0x0F即可选中对应【存储块】

    Code
    1. LD A, $0C
    2. LD ($2100), A

    当上面的代码被运行后,当Gameboy访问0x4000-0x7FFF时,访问的将是ROM0C的内容



    MBC3
    最多支持2MiB大小的ROM,即128个【存储块】(ROM0-ROM7F)
    向地址范围02000-0x3FFF任一地址写入0x01-0x7F即可选中对应【存储块】


    Code
    1. LD A, $3D
    2. LD ($2100), A

    当上面的代码被运行后,当Gameboy访问0x4000-0x7FFF时,访问的将是ROM3D的内容



    MBC5
    最多支持8MiB大小的ROM,即512个内存块(ROM0-ROM1FF)
    (1)向地址范围02000-0x2FFF任一地址写入0x01-0xFF即可选中对应【存储块】;
    (2)向地址范围02000-0x2FFF任一地址写入编号的低8位,并向地址范围03000-0x3FFF任一地址写入编号的最高1位,则可选中ROM100-ROM1FF

    Code
    1. LD A, $FF
    2. LD ($2000), A
    3. LD A, $01
    4. LD ($3000), A


    当上面的代码被运行后,当Gameboy访问0x4000-0x7FFF时,访问的将是ROM1FF的内容.



    FGBDS Demo 扩容软件

    解压缩
    将fgbdemo和欲扩容的rom放在同一目录内
    打开命令行工具(开始菜单->运行->CMD)
    进入rom所在目录。假设ROM文件名为gbre.gb, 欲扩容到2MiB(2048KiB)则输入以下命令
    fgbdemo -p 0xFF 2048 gbre

  • 注:为了避免和中文语境下【内存】的混淆,【存储块】作为ROM BANK的术语。



    GAMEBOY硬件没有内建的机制记录当下选中的存储块(ROM BANK)编号。





    如果需要修改/扩写的数据位于ROM0 (0x0000-0x3FFF),而ROM0的空间又不足够。这时候可以考虑跳转到ROMXX,这样就有了新的空间继续写代码。为了不影响游戏进行,【存储块】必须在新代码运行完毕后切换回来,那么之前的【存储块】编号是多少呢?如何正确的跳转回来呢?根据不同的游戏,有的可能很难,有的可能很简单,需要按照不同的情况考虑。可以考虑以下几个方法:
    (1)检查原游戏ROM是否有ROM BANK记录机制。如在0xC000-0xCFFF区域的某一字节(一般在最开始的几个字节),是否记录了当前选中的ROM BANK。这个可以通过【存储快】切换时的上下文代码判断。
    (2)如果RAM有空白位置,在受影响的【存储块】跳转时,将该【存储块】的编号写入内存。但这样,就可能需要改写/插入大量的代码。
    (3)如果RAM中有空白位置,可以考虑将ROM0中频繁被其他【存储块】调用的程序写入到内存(RAM)中。这样,就不会有【存储块】来回跳转的困扰。



    总之,游戏修改受制于原游戏的设计。如果需要修改ROM0之外的代码,往往扩容后跳转【存储块】即可,完成后再跳转回来。如果需要修改ROM0处的代码,就需要按照原游戏的编程风格,谨慎分析。

Join us

Have your own thoughts on this discussion? Wanna help others, avoid the mistakes you met before? Wanna contribute more to this or other topic? Just join us! Registration is free. Join us!