codeslinger.co.uk

Gameboy - Rom and Ram Banking.

Detecting Rom Bank Mode:

There are two types of ROM banking that I have emulated, MBC1 and MBC2. The majority of the games (80%) are MBC1 so to have a decent emulator this is a must to emulate. Some games like Tetris and Bubble Ghost dont use ROM banking at all. They just load the entire game into memory region 0x0-0x8000 and never need to swap memory in and out. To detect what ROM mode the game is you have to read memory 0x147 after the game has been loaded into memory. If 0x147 is 0 then the game has no memory banking (like tetris), however if it is 1,2 or 3 then it is MBC1 and if it is 5 or 6 then it is MBC2. This gives the following code:

m_MBC1 = false ;
m_MBC2 = false ;

switch (m_CartridgeMemory[0x147])
{
   case 1 : m_MBC1 = true ; break
   case 2 : m_MBC1 = true ; break
   case 3 : m_MBC1 = true ; break
   case 5 : m_MBC2 = true ; break
   case 6 : m_MBC2 = true ; break
   default : break ;
}

We also need a variable declaration to specify which ROM bank is currently loaded into internal memory address 0x4000-0x7FFF. As ROM Bank 0 is fixed into memory address 0x0-0x3FFF this variable should never be 0, it should be at least 1. We need to initialize this variable on emulator load to 1.

m_CurrentROMBank = 1 ; // this is type BYTE

Detecting RAM Banking:

Cartridge memory address 0x148 tells how much RAM banks the game has. The maximum is 4. The size of 1 RAM bank is 0x2000 bytes so if we have an array of size 0x8000 this is enough space for all the RAM banks. Like ROM banking we also need a variable to point at which RAM bank the game is using between values of 0-3. This gives us the following declarations.

BYTE m_RAMBanks[0x8000] ;
BYTE m_CurrentRAMBank ;

We then need to initialize these like so:

memset(&m_RAMBanks,0,sizeof(m_RAMBanks) ;
m_CurrentRAMBank=0;

RAM Banking is not used in MBC2! Therefore m_CurrentRAMBank will always be 0!

Controlling reading from ROM and RAM:

As stated in the section called Memory Control and Map we need to control Reading and Writing to the internal memory. The main reason to control the reading is to make sure everything reads from the correct ROM and RAM banks. This will give us the following function:

// read memory should never modify member variables hence const
BYTE Emulator::ReadMemory(WORD address) const
{
   // are we reading from the rom memory bank?
   if ((address>=0x4000) && (address <= 0x7FFF))
   {
     WORD newAddress = address - 0x4000 ;
     return m_CartridgeMemory[newAddress + (m_CurrentROMBank*0x4000)] ;
   }

   // are we reading from ram memory bank?
   else if ((address >= 0xA000) && (address <= 0xBFFF))
   {
     WORD newAddress = address - 0xA000 ;
     return m_RAMBanks[newAddress + (m_CurrentRAMBank*0x2000)] ;
   }

   // else return memory
   return m_Rom[address] ;
}

That was actually pretty easy wasn't it? Hopefully now you can see why you must ALWAYS you ReadMemory and WriteMemory when accessing internal gameboy memory

Changing the current ROM and RAM Banks:

Now we know how to read from the correct memory banks but how does the game request the banks to be changed? In my opinion this is one of the most difficult parts of the gameboy emulation. It isnt difficult to code but it is difficult to make sense of what you have to do. The way it works is the gameboy attempts to write directy to ROM but our WriteMemory function will trap it and decypher why it is trying to write to ROM. Depending on the memory address of where it is trying to write to ROM we need to take different action. If the address is between 0x2000-0x4000 then it is a ROM bank change. If the address is 0x4000-0x6000 then it is a RAM bank change or a ROM bank change depending on what current ROM/RAM mode is selected (explained in a minute). If the value is between 0x0-0x2000 then it enables RAM bank writing (also explained in a minute). We can now change the ROM part of our WriteMemory function to this:

void Emulator::WriteMemory(WORD address, BYTE data)
{
   if (address < 0x8000)
   {
     HandleBanking(address,data) ;
   }

   else if ((address >= 0xA000) && (address < 0xC000))
   {
     if (m_EnableRAM)
     {
       WORD newAddress = address - 0xA000 ;
       m_RAMBanks[newAddress + (m_CurrentRAMBank*0x2000)] = data ;
     }
   }

   // the rest of this function carries on as before
}

/////////////////////////////////////////////////////////////////

void Emulator::HandleBanking(WORD address, BYTE data)
{
   // do RAM enabling
   if (address < 0x2000)
   {
     if (m_MBC1 || m_MBC2)
     {
       DoRamBankEnable(address,data) ;
     }
   }

   // do ROM bank change
   else if ((address >= 0x200) && (address < 0x4000))
   {
     if (m_MBC1 || m_MBC2)
     {
       DoChangeLoROMBank(data) ;
     }
   }

   // do ROM or RAM bank change
   else if ((address >= 0x4000) && (address < 0x6000))
   {
     // there is no rambank in mbc2 so always use rambank 0
     if (m_MBC1)
     {
       if(m_ROMBanking)
       {
         DoChangeHiRomBank(data) ;
       }
       else
       {
         DoRAMBankChange(data) ;
       }
     }
   }

   // this will change whether we are doing ROM banking
   // or RAM banking with the above if statement
   else if ((address >= 0x6000) && (address < 0x8000))
   {
     if (m_MBC1)
       DoChangeROMRAMMode(data) ;
   }

}

Read on for a full explanation of what the above code means

Enabling RAM:

In order to write to RAM banks the game must specifically request that ram bank writing is enabled. It does this by attempting to write to internal ROM address between 0 and 0x2000. For MBC1 if the lower nibble of the data the game is writing to memory is 0xA then ram bank writing is enabled else if the lower nibble is 0 then ram bank writing is disabled. MBC2 is exactly the same except there is an additional clause that bit 4 of the address byte must be 0. This gives the following function:

void Emulator::DoRAMBankEnable(WORD address, BYTE data)
{
   if (m_MBC2)
   {
     if (TestBit(address,4) == 1) return ;
   }

   BYTE testData = data & 0xF ;
   if (testData == 0xA)
     m_EnableRAM = true ;
   else if (testData == 0x0)
     m_EnableRAM = false ;
}

Changing ROM Banks Part 1:

If the memory bank is MBC1 then there is two parts to changing the current rom bank. The first way is if the game writes to memory address 0x2000-0x3FFF then it changes the lower 5 bits of the current rom bank but not bits 5 and 6. The second way is writing to memory address 0x4000-0x5FFF during rombanking mode (explained later) which only changes bits 5 and 6 not bits 0-4. So combining these two methods you can change bits 0-6 of which rom bank is currently in use. However if the game is using MBC2 then this is much easier. If the game writes to address 0x2000-0x3FFF then the current ram bank changes bits 0-3 and bits 5-6 are never set. This means writing to address 0x4000-0x5FFF in MBC2 mode does nothing. This section explains what happens when the game writes to memory address 0x2000-0x3FFF.

void Emulator::DoChangeLoROMBank(BYTE data)
{
   if (m_MBC2)
   {
     m_CurrentROMBank = data & 0xF ;
     if (m_CurrentROMBank == 0) m_CurrentROMBank++ ;
     return ;
   }

   BYTE lower5 = data & 31 ;
   m_CurrentROMBank &= 224; // turn off the lower 5
   m_CurrentROMBank |= lower5 ;
   if (m_CurrentROMBank == 0) m_CurrentROMBank++ ;
}

So if we're using MBC2 then the current rom bank becomes the lower nibble of data. However if we are using MBC1 then we must set the lower 5 bits of current rom bank to the lower 5 bits of data whilst keeping the upper 3 bits the same. You may be wondering why I increment m_CurrentROMBank if it is set to 0. The reason is that rom bank 0 is static and can always be found in memory address 0x000-0x4000 so rom bank 0 should never be loaded into memory 0x4000-0x8000. If m_CurrentROMBank is ever set to 0 then it is treated as rombank 1 so rombank 1 or greater will reside in the bank region 0x4000-0x8000.

Changing ROM Banks Part 2:

As just stated there are two ways to change the current rom bank in MBC1 mode. This shows how to change the bits 5 and 6 when writing to memory address 0x4000-0x6000 and m_RomBanking is true (explained later)

void Emulator::DoChangeHiRomBank(BYTE data)
{
   // turn off the upper 3 bits of the current rom
   m_CurrentROMBank &= 31 ;

   // turn off the lower 5 bits of the data
   data &= 224 ;
   m_CurrentROMBank |= data ;
   if (m_CurrentROMBank == 0) m_CurrentROMBank++ ;
}

Changing RAM Banks:

You cannot change RAM Banks in MBC2 as that has external ram on the cartridge. To change RAM Banks in MBC1 the game must again write to memory address 0x4000-0x6000 but this time m_RomBanking must be false (explained later). The current ram bank gets set to the lower 2 bits of the data like so:

void Emulator::DoRAMBankChange(BYTE data)
{
   m_CurrentRAMBank = data & 0x3 ;
}

Selecting either ROM or RAM banking mode:

Finally the last part of this banking marathon is this m_RomBanking I keep going on about. This variable is responsible for how to act when the game writes to memory address 0x4000-0x6000. This variable defaults to true but is changes during MBC1 mode when the game writes to memory address 0x6000-0x8000. If the least significant bit of the data being written to this address range is 0 then m_RomBanking is set to true, otherwise it is set to false meaning there is about to be a ram bank change. It is important to set m_CurrentRAMBank to 0 whenever you set m_RomBanking to true because the gameboy can only use rambank 0 in this mode.

void Emulator::DoChangeROMRAMMode(BYTE data)
{
   BYTE newData = data & 0x1 ;
   m_RomBanking = (newData == 0)?true:false ;
   if (m_RomBanking)
     m_CurrentRAMBank = 0 ;
}

That is the end of the banking section. Next is The Timers section which is easier than this!