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 ;
}
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 ;
memset(&m_RAMBanks,0,sizeof(m_RAMBanks) ;
m_CurrentRAMBank=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] ;
}
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) ;
}
}
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++ ;
}
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 ;
}