The memory itself is really easy to implement. It's just an array of values of the needed size. We could create the memory at compile time (i.e type mem[SIZE];) but just to make sure the compiler lets use the amount of memory needed we will do it dynamically. We should know that the compiler limits us in the static memory allocation size but not in the dynamic one, though the last one may fail due to lack of free memory in the system, but that shouldn't happen anyway.
So I implement it like this:
1 #define MEMORY_SIZE 0x10000
2 UINT8 mem*;
3 mem = new UINT8[MEMORY_SIZE];
4 if(mem == NULL)
5 exit(-1); //Oops we ran out of memory!
There we have a fully functional memory for the GB! But well, it is not as simple.
Our memory is separated in different sections which are meant to do different things. For example it has the ROM, RAM, Tile Data, Tile Map, Sprites, Internal Registers, VRAM, I/O space, etc. and because of that we can't treat all memory the same.
Luckyly we can get around this problem and act as needed given the proper memory address. We have two actions that can be done on the memory: Read and Write. And each of those could act different depending the address they are intending to interact with. So we'll need a memory map that takes care of this.
First we define the basic structures to do this task. This are the Memory Handlers:
1 struct readMem
2 {
3 UINT32 startAddress;
4 UINT32 endAddress;
5 UINT8 (*memoryHandler) (UINT32);
6 void *pUserData;
7 };
8
9 struct writeMem
10 {
11 UINT32 startAddress;
12 UINT32 endAddress;
13 void (*memoryHandler) (UINT32, UINT8);
14 void *pUserData;
15 };
Reading and writing structures are very similar but they differ in the function pointer because the read returns a value whereas the write receives a value, and of course both receives the address they want to affect. The start and end Addresses mean the range in which a given Handler will be called. So you will have a specific handler for each different behaviour when reading and/or writing.
So.. what kind of behaviour do I refer to? For example in the GB when writing to an address which is in the ROM range won't write, but rather try to change the Switchable ROM bank.
The function that gets called would be something like this:
1 void writeROMBankSelect(UINT32 address, UINT8 data)
2 {
3 data = data & 0x1F; //Just uses 5 bits for this (max 32 banks).
4 if(data <= gb->rom.rom_b.n) //If there are enough banks in the rom.
5 memcpy(gb->mem.mainMemory + gb->rom.rom_b.size,gb->rom.rom_b.banks[data],gb->rom.rom_b.size); //Copy the ROM bank to the MainMemory.
6 }
There is some additional handling(i.e. banks 1,21,41 and 61 can't be used) but that's the main idea.
The address for this Handling function is 0x2000 to 0x3FFF. The rest of the addresses have their own handling functions yielding to a memory handler like:
1 writeMem implWriteMem[] =
2 {
3 { 0x0000, 0x1FFF, writeRAMBankEnable, NULL},
4 { 0x2000, 0x3FFF, writeROMBankSelect, NULL}, //This one!
5 { 0x4000, 0x5FFF, writeRAMBankSelect, NULL},
6 { 0x6000, 0x7FFF, writeRAMROMModeSelect, NULL},
7 { 0x8000, 0x9FFF, writeVRAM, NULL},
8 { 0xA000, 0xBFFF, writeSRAM, NULL},
9 { 0xC000, 0xDFFF, writeRAM, NULL},
10 { 0xE000, 0xFDFF, writeERAM, NULL},
11 { 0xFE00, 0xFE9F, writeSprite, NULL},
12 { 0xFF00, 0xFF7F, writeIOM, NULL},
13 { 0xFF80, 0xFFFF, writeHRAM, NULL},
14 { (UINT32) -1, (UINT32) -1, NULL, NULL }
15 };
This just creates an static array of the structure defined before(struct writeMem).
Ok.. now we have the handlers, but how do we use them? There are functions for this, which are:
1 UINT8 data readByte(UINT32 address);
2 void writeByte(UINT32 address, UINT8 data);
3
4 UINT16 data readWord(UINT32 address); //Read 2 bytes
5 void writeWord(UINT32 address, UINT16 data); //Write 2 bytes
There are operations in the gameboy which reads or writes in pair, for the AF, BC, DE and HL mixed registers and as well for the SP and PC registers so the 2 bytes read and write come in handy.
So then when we want to read something is as simple as
data = readByte(address);
But let's get in the implementation of these functions:
The different functions work in similar way, so I will show how the writeByte(..) works:
1 void writeByte(UINT32 address, UINT8 data)
2 {
3 writeMem *last; //Last handler being checked for valid address.
4 UINT32 i = 0;
5 last = &(mem_h->WMem[i++]); //Update pointer.
6 //While there are handlers left.
7 while ((last->startAddress != (UINT32)-1) && (last->endAddress != (UINT32)-1)){
8 //If address is in range.
9 if((address >= last->startAddress) && (address <= last->endAddress)){
10 //If there is a handler
11 if(last->memoryHandler != NULL){
12 //Call the function
13 (*(last->memoryHandler))(address, data);
14 return;
15 }
16 else{ //If not use the user data.
17 ((UINT8 *) last->pUserData)[address] = data;
18 return;
19 }
20 }
21 else //If not, update pointer.
22 last = &(mem_h->WMem[i++]);
23 }
24 mem->mainMemory[address] = data; //If there is no specific handler
25 //Access the memory directly.
26 }
So the idea is simple. Search for the entry in the array of Handlers which is supposed to handle the given address. If there is one, and you have a function pointer, then call it. If there's no such function pointer then use the user data pointer(pointer to some memory type). Now.. if there is no handler for the given address, then access the memory directly.
This code is effective and flexible, but I think it has a couple of issues in performance. I will use it for now, until I get to a good point with the emulator and I run a profiler to check what to optimize. I'm sure this will appear in the optimization list.
In the CPU post I talked about initializing some values besides the CPU registers. Well, we can do that now. That's not exactly from this section but I wouldn't create a section for it. So an example(there are a lot of values) would be:
1 writeByte(0xFF05, 0x00);
2 writeByte(0xFF06, 0x00);
3 writeByte(0xFF07, 0x00);
4 writeByte(0xFF10, 0x80);
5 writeByte(0xFF11, 0xBF);
6 ...
7 writeByte(0xFFFF, 0x00);
I think that summarizes the Memory Mapping and Handling.