aquova.net Github's logo Mastodon's logo Blue Sky's logo Backloggd's logo Pico-8's logo The RSS logo


Chapter XXXIV. MBC2

Return to Index

Previous Chapter

We'll continue our expansion of possible game our emulator can play by implementing the MBC2 chip next. It functions much in the same way the MBC1 does, although it does have a few unique implementation details. Unlike the MBC1, the MBC2 was a relatively uncommon chip, only being used in about dozen commercial games (with Golf and Kirby's Pinball Land being perhaps the best known). Nevertheless, it's relatively easy to implement its functionality.

Reading from RAM

As we stated in the MBC1 implementation, all the MBCs read from the ROM in the same fashion, so the first unique behavior to implement is reading from external RAM. This is a bit of a misnomer though, as the MBC2 doesn't support external RAM. Instead, the MBC2 chip itself contains 512 bytes of RAM1. The reading of this memory is exactly the same as our implementation for the MBC1 and no MBC cases, so we can simply expand the match statement to include the MBC2 as well.


// In cart/mod.rs
// Unchanged code omitted

impl Cart {
    pub fn read_ram(&self, addr: u16) -> u8 {
        match self.mbc {
            MBC::NONE | MBC::MBC1 | MBC::MBC2 => {
                let rel_addr = (addr - EXT_RAM_START) as usize;
                let bank_addr = (self.ram_bank as usize) * RAM_BANK_SIZE + rel_addr;
                self.ram[bank_addr]
            },
            _ => unimplemented!()
        }
    }
}
            

The MBC2 is unique though, in that there is another area we need to modify. Since the MBC2 always has 512 bytes of memory, the header doesn't bother to signify this when listing the external RAM size. Thus, we need to add a special case when initializing our ram vector.


// In cart/mod.rs
// Unchanged code omitted

impl Cart {
    fn init_ext_ram(&mut self) {
        let mut ram_size_idx = self.rom[RAM_SIZE_ADDR] as usize;

        // Some headers don't report their external RAM capacity correctly
        if self.has_external_ram() && ram_size_idx == 0 {
            ram_size_idx = 1;
        }

        if self.mbc == MBC::MBC2 {
            // MBC2 always has 512 bytes of RAM directly on chip
            self.ram = vec![0; 512];
        } else {
            let ram_size = RAM_SIZES[ram_size_idx] * 1024;
            self.ram = vec![0; ram_size];
        }
    }
}
            

1 The MBC2 built-in RAM actually only supports 512 half bytes. Only the lower four bits are valid, with the upper four bits are always undefined. This fact would be known to the game developer, so we'll not have any special handling for this.

Writing to ROM

Writing to the ROM address space works in a number of different ways than the MBC1. Firstly, while that chip had four separate regions to provide control, the MBC2 only has two control mechanisms — for toggling RAM access and setting the ROM bank. Since the MBC2 has no external RAM support, it also has no need to setting the ROM/RAM mode or performing RAM bank switching.

The MBC2 also doesn't use the same model of breaking the address space into different pieces. Instead, the specific address and value written provides additional context. In this case, it depends on whether or not the 8th bit of the address is 0 or 1. If it's 0, then the written value controls whether RAM access is enabled or not — again, depending on whether 0x0A was written, as was the case with the MBC1. If the 8th bit is a 1, then the value written is the new ROM bank. It's a notably different system than what the MBC1 uses, but is no less valid.


// In cart/mod.rs
// Unchanged code omitted

impl Cart {
    pub fn write_cart(&mut self, addr: u16, val: u8) {
        match self.mbc {
            MBC::NONE => {},
            MBC::MBC1 => { self.mbc1_write_rom(addr, val); },
            MBC::MBC2 => { self.mbc2_write_rom(addr, val); },
            _ => unimplemented!()
        }
    }

    fn mbc2_write_rom(&mut self, addr: u16, val: u8) {
        let bank_swap = addr.get_bit(MBC2_ROM_CONTROL_BIT);
        if bank_swap {
            self.rom_bank = (val & 0x0F) as u16;
        } else {
            self.ram_enabled = val == 0x0A;
        }
    }
}
            

One other thing to note is that the MBC2 only supports up to 16 ROM banks, so we only need to keep the last four bits when changing the ROM bank.

Writing to RAM

The differences between the MBC1 and MBC2 end here, as writing to the external RAM with the MBC2 is exactly the same as the MBC1. The only change we need to make here is to give mbc1_write_ram a better name to reflect more than one MBC is using it.


// In cart/mod.rs
// Unchanged code omitted

impl Cart {
    pub fn write_ram(&mut self, addr: u16, val: u8) {
        match self.mbc {
            MBC::NONE => {
                let rel_addr = addr - EXT_RAM_START;
                self.ram[rel_addr as usize] = val;
            },
            MBC::MBC1 | MBC::MBC2 => self.mbc12_write_ram(addr, val),
            _ => unimplemented!()
        }
    }

    fn mbc12_write_ram(&mut self, addr: u16, val: u8) {
        if self.ram_enabled {
            let rel_addr = (addr - EXT_RAM_START) as usize;
            let ram_addr = (self.ram_bank as usize) * RAM_BANK_SIZE + rel_addr;
            self.ram[ram_addr] = val;
        }
    }
}
            
Next Chapter