Skip to content →

more todo’s – mmu, mbc and controls

Time to get more details added to our project, and get some games really working.

mmu

The Memory Management Unit (MMU) is the interface between our CPU / PPU / SPU and the memory itself. With this interface, we can prevent writes, trigger functions on accessing certain memory addresses, and even switch the data that resides at a memory range.

So the MMU ultimately is the only unit, directly accessing the memory array, all other calls to memory are supposed to run through the MMU functions.

// initial implementation
void writeToMem(uint16_t adr, unsigned char val) {
    memory[adr] = val;
}
unsigned char& readFromMem(uint16_t adr) {
    return memory[adr];
}

If you remember, Dr. Mario was behaving oddly, not playing the demo correctly. This was because of illegal memory writes. This can be prevented through our new MMU functions.

//	MBC0
if (romtype == 0x00) {
	//	make ROM readonly
	if (adr >= 0x8000)
		memory[adr] = val;
}
MBC0 shows it is a 32 kb cartridge, which doesn't use memory banking

With this simple fix, I was able to finally run the Dr. Mario demo successfully.

Dr. Mario – demo fixed by restricting memory write access

The MMU makes use of multiple special cases, depending on which address is read from / written to.

//	[0xff26 - enable sound, all channels]
if (adr == 0xff26) {
	if (val)
		memory[0xff26] = 0xff;
	else
		memory[0xff26] = 0x00;
	return;
}

//	[0xff00] - joypad input
if (adr == 0xff00) {
	memory[adr] = readInput(val);
	return;
}

//	[0xff50] - lock bootrom
else if (adr == 0xff50 && val == 1) {
	lockBootROM();
	memory[adr] = val;
}

//	[0xff46] - oam dma transfer
else if (adr == 0xff46) {
	dmaOAMtransfer();
	memory[adr] = val;
}

//	[0xff11] - SC1 trigger
else if (adr == 0xff14 && (val >> 7)) {
	memory[adr] = val;
	resetSC1length(readFromMem(0xff11) & 0x3f);
	return;
}
//	[0xff21] - SC2 trigger
else if (adr == 0xff19 && (val >> 7)) {
	memory[adr] = val;
	resetSC2length(readFromMem(0xff16) & 0x3f);
	return;
}
//	[0xff31] - SC3 trigger
else if (adr == 0xff1e && (val >> 7)) {
	memory[adr] = val;
	resetSC3length(readFromMem(0xff1b));
	return;
}
//	[0xff41] - SC4 trigger
else if (adr == 0xff23 && (val >> 7)) {
	memory[adr] = val;
	resetSC4length(readFromMem(0xff20) & 0x3f);
	return;
}
This is from the final version of the MMU, already including sound, DMA and controller related code.

mbc (memory banking)

The probably most important part inside the MMU is the ability of Memory Banking. Each cartridge can have some form of MBC (Memory Banking Controller), each dictating a certain number of ROM and RAM banks, that can be swapped on runtime.

This means, certain writes will cause the MBC to map another bank of RAM or ROM to the available memory addresses. Therefore, it is able to provide more memory, than usually would be accessible with the limit address length.

Most common MBCs

MBC0  (no memory banking)
MBC1  (max 2 MByte ROM and/or 32 KByte RAM)
MBC2  (max 256 KByte ROM and 512x4 bits RAM) 
MBC3  (max 2 MByte ROM and/or 64 KByte RAM and Timer) 
MBC5  (max 8 MByte ROM and/or 128 KByte RAM) 

To be able to load games like Super Mario Land etc., that make use of MBC’s we will have to implement each of the MBC’s specifications.

//    Example for MBC1, MBC2 and no MBC
//    readFromMem()

//	MBC1 
if (romtype == 0x01 && mbc1romNumber && (adr >= 0x4000 && adr < 0x8000)) {
	uint32_t target = (mbc1romNumber * 0x4000) + (adr - 0x4000);
	return rom[target];
}
//	MBC2
else if (romtype == 0x02) {
	//	ROM banking
	if (mbc2romNumber && (adr >= 0x4000 && adr < 0x8000)) {
		uint32_t target = (mbc2romNumber * 0x4000) + (adr - 0x4000);
		return rom[target];
	}
	else
    	return memory[adr];
}
else
	return memory[adr];
//    Example for MBC1, MBC2 and no MBC
//    writeToMem()

//	MBC0
if (romtype == 0x00) {
		
	//	make ROM readonly
	if (adr >= 0x8000)
		memory[adr] = val;
}
//	MBC1
else if (romtype == 0x01) {

	//	external RAM enable / disable
	if (adr < 0x2000) {
		mbc1ramEnabled = val > 0;
	}
	//	choose ROM bank nr (lower 5 bits, 0-4)
	else if (adr < 0x4000) {
		mbc1romNumber = val & 0x1f;
		if (val == 0x00 || val == 0x20 || val == 0x40 || val == 0x60)
			mbc1romNumber = (val & 0x1f) + 1;
	}
	//	choose RAM bank nr OR ROM bank top 2 bits (5-6)
	else if (adr < 0x6000) {
		//	mode: ROM bank 2 bits
		if (mbc1romMode == 0)
			mbc1romNumber |= (val & 3) << 5;
		//	mode: RAM bank selection
		else
			mbc1ramNumber = val & 3;
	}
	else if (adr < 0x8000) {
		mbc1romMode = val > 0;
	}
	else {
		memory[adr] = val;
	}
}
//	MBC2
else if (romtype == 0x02) {
	//	external RAM enable / disable
	if (adr < 0x2000) {
		mbc2ramEnabled = val > 0;
	}
	//	choose ROM bank nr (lower 5 bits, 0-4)
	else if (adr < 0x4000) {
		mbc2romNumber = val & 0x1f;
		if (val == 0x00 || val == 0x20 || val == 0x40 || val == 0x60)
			mbc2romNumber = (val & 0x1f) + 1;
	}
	else {
		memory[adr] = val;
	}
}

Comments

  1. LilaQ LilaQ

    You should complete this section, dude!

  2. Faz Faz

    Hi there,

    I am reading this article and find it very interesting. However I am very confused how your initial implementation of the memory write function managed to pass the individual CPU test – In particularly test 07 – Jumps. I say this because your memory write function is a 2-byte write with just one single step (not two).

    void writeToMem(uint16_t adr, unsigned char val) {
    memory[adr] = val;
    }

    A two byte load instructions like: LD (nn), SP should load the two-byte SP into mem[nn] in two steps:
    writeToMem[adr, Low byte of val];
    writeToMem[adr + 1, High byte of val];

    But with your implementation the entire 2-bytes is loaded in one location, not two (as it should). This also means that your memory array can store 2 bytes as opposed to 1 bytes (which is another matter).

    So can you explain why you are writing 2-bytes into one mem location and not two? I doubt cpu test 07 will pass if you implement it the way shown above.

    • Faz Faz

      Ah ignore my comment above. I thought your code is in Java where char is 2-byte unsigned. Silly me lol.

    • LilaQ LilaQ

      Hi there!

      So, my memory write function does in fact only write a single byte to the desired memory location. unsigned char, as it is passed to the function, is the same as uint8_t, so just a single byte.

      This function only exists so we can intercept certain writes to addresses that are not allowed or not even writable. And yes, this does happen in games (you can see it fixed Dr. Mario for me, for example).

      And you are spot on, of course instructions that need to store 2 bytes, as “LD (nn), SP”, of course need to run two calls to this method. But this gets handled inside the instructions method itself.
      So, basically, whenever I encounter the opcode $08, it gets caught by my switch/case, and this is being executed:

      case 0x08:
      {
      writeToMem(((readFromMem(pc + 2) << 8) | readFromMem(pc + 1)), sp & 0xff); writeToMem(((readFromMem(pc + 2) << 8) | readFromMem(pc + 1)) + 1, sp >> 8);
      pc += 3;
      return 5;
      break;
      }

      (Sorry for the lack of formatting in comments, there is a linebreak between the 2 writes, that just doesn’t want to show)

      I hope this clears it up a bit.
      If you have any follow up questions, just let me know!

      Cheers

      • Faz Faz

        Yes – that makes perfect sense. Thanks for the quick reply! Your code looks very similar to Java syntax so I wrongly assumed the char was 2-byte. Btw it looks like you are the author of this article? “You should complete this section, dude!” – Is that your own comment?

        Also, is your GB emulator hosted on github? I wanted to peer through some of your implementation as a reference to my own at some point.

        Thanks again!
        Cheers.

        • LilaQ LilaQ

          Yes, exactly, I should remove that comment 😀 I put it there some time when I was still in progress of creating the page 🙂

          Yea I have it on github, it might still have some bugs though, that’s I haven’t prominently linked my Github here yet. But feel free to browse the code if you want!

          https://github.com/LilaQ/q00_gb

          Cheers!

          • Faz Faz

            Great, thanks!

          • Faz Faz

            So my test 09 (op r, r) is failing at RLCA, RLA, RRCA, RRA, and I compared my code to yours and noticed that for above op codes you are setting the Z flag to 0 (without checking if A/result == 0). This got me confused because according to the GB manual Z is set if the result (in accumulator A) is 0 (reset otherwise). Am I misinterpreting the description given in the manual? Thanks in advance.

            Lines 484 and 502:
            https://github.com/LilaQ/q00_gb/blob/master/cpu.cpp

          • LilaQ LilaQ

            Hey!

            I’m not sure which manual you are using, but there are definitely contradicting information out there. In case it is “GBCPUMan.pdf” – don’t use it. You can see in Pandocs that setting Z to 0 is the (probably) right way to handle these instructions: https://gbdev.io/pandocs/ (search for RRCA).

            rlca 07 4 000c rotate akku left
            rla 17 4 000c rotate akku left through carry
            rrca 0F 4 000c rotate akku right
            rra 1F 4 000c rotate akku right through carry

            I hope this helps! Just message me if you have any more questions.

            Cheers!

  3. Faz Faz

    Yes – GBManual.pdf is my reference up to now but I will not use it anymore and will switch to pandocs. Thanks for your help! Setting Z to 0 worked lol. Btw when you say ‘message me’ do you mean here or can I contact you at some email address?

    fizaan@gmail.com

    • LilaQ LilaQ

      I’m glad it did the trick!

      You can always ask here (sometimes people may have the same questions), or via mail to info@emudev.de or via Discord (LilaQ#4628)

      • Faz Faz

        Very odd but my opcodes 0x33 and 0x3B (INC SP/DEC SP) are failing. How can that be as these are such simple opcodes to implement lol.

        public int getSP() {
        return SP;
        }
        public void setSP(int sP) {
        SP = sP & 0xFFFF;
        }

        public void H33() {
        reg.setSP(reg.getSP()+1);
        }

        public void H3B() {
        reg.setSP(reg.getSP()-1);
        }

        • LilaQ LilaQ

          Hey, sorry for replying so late!
          Are you still failing on these opcodes?
          If something really simple fails in tests, chances are it’s not the opcodes themselves that are failing, but something around them. If you’re still having problems with it, just text me again and we can get to the bottom of that 😉

          Cheers

          • Faz Faz

            Yup, still failing. I got distracted on another project so didn’t work on this for a while since December last year lol. I’m so lazy. I’ll look you up on discord and maybe I can get help from you there.

            Here’s the output that’s failing for me:
            03-op sp,hl

            33 3B 39 F9 E8 E8 F8 F8
            Failed

Leave a Reply