Writing a Gameboy Emulator, Part 1
- From the sandbox
- Tutorial
Hello!
Not so long ago, an article appeared on Habré about the creation of the chip-8 emulator, thanks to which it was possible to understand at least superficially how emulators are written. After implementing my emulator, a desire appeared to go further. The choice fell on the original Gameboy. As it turned out, the choice was ideal for a situation where you want to implement something more serious, and there is practically no experience in developing emulators.
In terms of emulation, Gameboy is relatively simple, but even it requires the study of a fairly large amount of information. For this reason, several articles will be devoted to the development of the Gameboy emulator. The end result will be an emulator with good compatibility, support for almost all the functions of the original, including sound, which is often absent in other emulators. As a bonus, our emulator will pass almost all test ROMs, but more on that later.
These articles will not contain an exhaustive description of the emulator implementation. This is too voluminous, and all the interest from the implementation disappears. It will reach a specific code only in rare cases. I set myself the task of giving a more theoretical description with small hints of implementation, which, ideally, should allow you to write your emulator without too much difficulty and at the same time feel that you wrote it yourself. Where necessary, I will refer to my own implementation - if necessary, you can find the right code without tearing through tons of lines of code.
In this article, we will get to know Gameboy and start by emulating its processor and memory.
We write the Gameboy emulator, part 1
We write the Gameboy emulator, part 2
We write the Gameboy emulator, part 3
Architecture Interrupt
Processor Memory Conclusion
Gameboy is a Nintendo handheld console that was launched in 1989. It's about the original black and white Gameboy. It is worth noting that in the various documents that we will be guided by, the code name Gameboy is used - DMG (Dot Matrix Game). Further I will use it.
Before you begin, you must familiarize yourself with the technical characteristics of DMG:
After reviewing the subject, the next step is documentation. The volumes of necessary information do not allow placing absolutely everything in the article, therefore it is necessary to arm yourself with documentation in advance.
There is a great document for DMG called Gameboy CPU Manual . It includes several well-known documents from eminent developers and contains almost all the information we need. Naturally, this is not all, but at this stage this is more than enough.
I immediately warn that there will be errors in the documents, even in official ones. During this series of articles I will try to mention all the shortcomings of various documents that I could find (remember). I will also try to fill in many of the gaps. The bottom line is that there is no comprehensive description for DMG. Available materials give only a superficial picture of the work of many console nodes. If the programmer is not aware of such "pitfalls", then developing an emulator will become much more complicated than it could be. DMG is simple enough when you have reliable and detailed information on hand. And the problem is that many important details can be learned only from the source code of other emulators, which, however, does not make our task easier. The code of well-known emulators is either unnecessarily complicated (Gambatte), or it is a terrible heap, ahem,
Since the articles are written with an eye on my emulator, it's just a link to the source code and binary CookieBoy.
I say this for a reason - it was by piling all the components into one superclass that I started developing the emulator. It soon became apparent that things would go a lot easier if everyone would do what they should. Although it is worth recognizing that this approach has obvious complexity. You need to have a pretty good understanding of the internal structure of the DMG in order to correctly distinguish between class responsibilities.
So let's get started.
The processor contains eight 8-bit registers A, B, C, D, E, F, H, L and two 16-bit special purpose registers - PC and SP. Some instructions allow you to combine 8-bit registers and use them as 16-bit registers, namely AF, BC, DE, HL. For example, the BC register is the “glued” registers B and C, where the C register acts as the low byte and B the high.
Registers A, B, C, D, E, H, L are general purpose registers. Register A is also a battery. Register F contains processor flags and is not directly accessible. The following is a register outline. Bits 0 through 3 are not used.
The purpose of the flags:
Register PC (program counter), as you might guess, is an instruction counter and contains the address of the next instruction.
The SP register (stack pointer), respectively, is a pointer to the top of the stack. For those who are not in the know, the stack is a memory area into which the values of variables, return addresses, etc. are written. SP contains the address of the top of the stack - the stack grows down, from high to low. For him, there are always at least two operations. PUSH allows you to insert a certain value - first, the SP register is reduced, and then a new value is inserted. POP allows you to retrieve a value - first, at the SP address, the value is retrieved from memory, and then SP is incremented.
The processor also contains the so-called IME (interrupt master enable) - a flag that allows interrupt processing. It takes, respectively, two values - disable (0) and enable (1).
With theory, everything can begin to be implemented. Since we will have to work with both 8-bit registers and their 16-bit pairs, it is advisable to implement a mechanism that allows simultaneous access to those and those without the need to use bit operations. To do this, declare the following type:
The processor registers will be stored as pairs, and we will have access to individual parts thanks to the WordRegister association. The word field will give access to the entire 16-bit register. The “bytes” field gives access to individual registers in a pair. The only thing is that registers A and F should be stored separately. Register A is a battery, which means it is used very often. A similar situation with register F - processor flags have to be set quite often.
Now let's start implementing the processor itself - the Cookieboy :: CPU class will be responsible for this. Reading and executing instructions will be implemented in the usual way - reading an opcode from memory, and then decoding and executing using the switch construct:
All opcodes have a length of 1 byte, but some instructions use the so-called prefix - the first byte is the prefix of the instruction set (for us, the only prefix is 0xCB), the second byte is the opcode from this set. The implementation is elementary - as soon as we stumbled upon 0xCB, we read another byte and decode it with a nested switch.
This code is placed in the void Step () function, which executes one processor instruction in one call and performs other necessary operations.
Naturally, for reading and writing to memory, we need another class - Cookieboy :: Memory, whose object can be seen above under the name "MMC". At this stage, stubs with basic methods are sufficient:
The DMG processor has a fairly large number of instructions, a list of which can be found in the Gameboy CPU Manual. It also indicates which processor flags should be set and how many clock cycles each instruction takes. VERY carefully read the description of the flags - incorrectly implemented setting of the flags often leads to inoperative games, and debugging turns into torture. But I hasten to reassure a little - there are test ROMs for processor flags, but we are still far from executing ROMs.
Speaking of measures. If chip-8 was simple enough, and its emulation did not require taking into account the duration of instruction execution, then with DMG, the situation is different. The console components do not work anyhow, but are synchronized using a clock generator. For us, this means that we need to synchronize the work of all components of our emulator with the processor.
To solve this problem is quite simple. The processor is the central link in our emulator. Following the instructions, we transfer to other components the time spent by the processor in cycles to synchronize all components with each other. To do this, I use the SYNC_WITH_CPU macro (clockDelta), which transfers the time spent by the processor on the execution of the instruction. It already calls the synchronization functions of the remaining components of the emulator. The solution to the synchronization problem could be easily taken outside the limits of the processor class, if not one but.
The components of the console work simultaneously, no one waits until the processor finishes executing the instructions, as we do. Some instructions require a long time to execute, and in the process of reading and writing data to memory. The processor, as you might guess, spends a certain time reading / writing to memory (4 clock cycles). This leads to the fact that during the execution the contents of the memory can change, which, naturally, it would be nice to emulate too.
In this case, it is required to use the synchronization macro several times during execution, so that the correct data were in the memory at the time of reading or writing. Most instructions do not require such precise synchronization, and allow it to be executed after execution. Others require an exact sequence of synchronization functions and memory read / write operations.
It is nevertheless more correct and more beautiful to do it differently. We know for sure that each write or read operation from the memory of one byte takes 4 clock cycles. It is enough to add auxiliary read and write functions, which themselves call the synchronization functions. As soon as this is done, most instructions will immediately acquire the correct duration, because in reality their execution time is composed of read and write operations. Getting the opcode of a command also applies here. This is exactly what I did in my emulator, which almost completely freed me from manual synchronization and timing. Only a few instructions required my intervention.
Now let's digress a bit to clarify the situation with the bars. There is confusion in various documentation. Some documents write numbers such that, for example, NOP has a duration of 4 measures, others - 1 measure (for example, it is written in the official Nintendo documentation). To understand the reason it is worth a little distraction on the theory.
Any processor instruction has a specific duration, which we call a machine cycle. In one machine cycle, the processor can perform one action from and to, such as reading an opcode, decoding it, and executing a command; reading or writing values in memory. In turn, a machine cycle consists of machine cycles, since a processor can perform several operations in one machine cycle. And so we come to our processor. If we say that NOP lasts 4 cycles, then we are talking about machine cycles. If we are talking about 1 clock cycle for NOP, then we are talking about machine cycles. This is exactly how the DMG processor works - its machine cycle lasts 4 machine cycles and many instructions have exactly 4 cycles or 1 machine cycle - the DMG processor is able to read the opcode from memory, decode it and execute the instruction in just 4 machine cycles.
Hereinafter I will use more familiar machine clocks. They correspond to one period of the clock generator, which means that they are the minimum and indivisible unit of time for our emulator. Thus, the NOP operation will last 4 measures.
At this stage, it is already possible to fully emulate all processor instructions. Separately, it is worth mentioning some of them:
In addition to these shortcomings, there are others. The CPU Manual contains an incomplete description of the duration of the instructions. As you might guess, conditional branch instructions should have different durations depending on whether the branch has occurred or not. It would be possible to use test ROMs, but they do not work correctly on their own because of these instructions, so they display an unknown error without even starting the test. Here is a table of these instructions indicating their duration:
Also for instructions RST n (opcodes 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7, 0xFF) the wrong duration is indicated. The correct value is 16 measures.
And so, at the moment, our “processor” is able to read instructions from memory, execute them and synchronize other components with itself (as it synchronizes, while all these are dummy functions). After that, we need to check if there was an interruption after all the work done.
During synchronization, we call the synchronization methods of other components of the emulator, which may request an interrupt. In DMG, this is done as follows. There are two registers (where they are located will be discussed later) - IF (interrupt flags) and IE (interrupt enable). Their bits have a specific purpose, which is identical in both registers:
IF register bits indicate which interrupts were requested. If the bit is set, then an interrupt is requested.
IE register bits enable interrupt handling. If the bit is set to one and the corresponding interrupt has been requested, then it will be processed. If not, the interrupt will not be processed.
As you can see, the identical assignment of bits is very useful and allows you to use the logical operation AND to find out which interrupts should be processed.
One important detail is that the interrupt takes the processor out of the shutdown state that occurred as a result of executing a HALT or STOP. And here the algorithm by which the interrupt registers are checked is very important. The algorithm is as follows:
Interrupts are processed one at a time and in a strictly defined order. All information about the priorities and addresses of the handlers is indicated in the CPU Manual.
An important detail. Again, someone might have thought: interrupt handling is very similar to a procedure call, and therefore should take some time. This is true and it takes 20 measures. For some reason, this point is omitted in the documents describing the DMG.
Now we start implementation. The cookieboy :: Interrupts class will deal with us. We put IE and IF registers in it and declare functions to access these registers (we will need them later), as well as a function that allows you to request a specific interrupt (we don’t want to manipulate bits every time to request some kind of interrupt) . We also need a function that will check which interrupts are worth processing. We place a call to this function at the end of the Step function of the processor and additionally synchronize the components.
A little about interrupt request. It is done by setting the corresponding bits in the IF register. Prior to installation, IE case verification is not required. Even if the bits in it prohibit a specific interrupt, we still set the bits in the IF register for this interrupt.
If you looked at the source code of my implementation of Cookieboy :: Interrupts, you might have noticed that I am returning the value of the IE and IF registers after setting to one all the bits that are not used in them (OR operation with the value 0xE0). I do it for a reason. Many registers in I / O ports (more on that below) do not use all bits, others restrict read access to some bits or to the entire register at once. This also needs to be taken into account - for this, unused and prohibited for reading bits should be set to 1 before returning.
To summarize. Our emulator is able to execute processor instructions, synchronizes all components of the emulator with each other, processes interrupts. True, so far it's all just in words. To get a really working emulator, we need to emulate DMG memory.
Turning to the addresses 0x4000-0x7FFF, without memory banks, we would get to this address in the image of the game. Using memory banks, we can set the image to be divided into banks, and at the address 0x4000-0x7FFF the selected bank will be displayed. Thus, at one moment in this area there is a second bank, at another - a tenth. As we want, in general. Thus, we arrive at virtual and physical addresses. 0x4000-0x7FFF are virtual addresses that do not have to match physical addresses. A physical address is the real address at which memory cells are accessed.
All this is necessary so that our DMG can work with game images that far exceed not only 0x8000 bytes, but also the entire address space. In words, all this may seem too complicated, but during the implementation it will be clear that these are extremely elementary things that are easier and faster to implement than to explain.
All the same applies to RAM. Banks allow you to expand its volume by placing microcircuits in the cartridge. In addition, this way you can implement a full-fledged storage system using the battery built into the cartridge to power the RAM.
The task of translating a virtual address into a physical one lies with the MBC controller, which is located inside the cartridge. All read and write operations in the ROM area go through it. Operations associated with external RAM are also redirected here.
Naturally, we cannot change the contents of ROM. Write operations are used as control commands for the MBC. In the CPU Manual, you can read which addresses are responsible for which functions. Thus, having written the number 9 at a specific address, we say that we want to select bank 9. After that, we can read its contents by contacting addresses 0x4000-0x7FFF.
The figure below shows the simplest MBC operation scheme. Here, the region 0x0000-0x3FFF is always redirected to bank 0, as in some real controllers, but the region 0x4000-0x7FFF is redirected to the current bank.
Consider the DMG address space scheme:
More about each section:
Since the architecture of the emulator assumes that each DMG component will have its own class, the Cookieboy :: Memory class that emulates the memory will contain only the following memory areas - ROM banks, internal RAM 1, Echo of internal RAM 1, Switchable RAM bank, internal RAM 2. When accessing all other areas, access methods of the corresponding classes will be called.
Let's start with the read and write operations in memory. Everything is extremely simple - we look at the address and redirect operations to the corresponding memory areas. I did as follows. As you can see, many memory areas are well aligned, which allows you to implement everything using switch and logical operations. Here's what it looks like:
And no bulky conditional structures. For now, you can leave only a blank, since some areas of memory will be in other classes (for example, video memory) that we have not yet implemented. You can only implement what is really in Cookieboy :: Memory. Here it is worth paying attention to the ROM banks and Switchable RAM bank.
If the cartridge from which the ROM was removed contained an MBC controller, then in these memory areas we need to implement the logic of these controllers. You can do this very simply - access to these areas is redirected to classes that are implemented by the corresponding MBC controllers, and they themselves let them decide where, how and what. Let's look at two examples - MBC 2 and MMM01. The first is an example that will allow you to implement the rest. MMM01 is a rather strange MBC. There is practically no documentation on it, and its implementation is quite different from other MBCs. It won't hurt to fill this gap in DMG emulation.
To get started, let's get the base class MBC. It will look like this:
As you can see, the write and read functions come first - they will be called from our Cookieboy :: Memory. Next come the functions of saving and loading RAM. Here we immediately prepare the way for future emulation of memory in a cartridge, which is powered by a battery to save its contents after turning off the console. I will omit their implementation - this is just saving and reading the RAMBanks array from the file, no more. Then the extremely obvious constructor and several fields:
With the base class over, now let's start implementing the class that emulates MBC2. Let's look at the code right away, and then we'll figure out how this controller works:
Reading is easy. ROMOffset is used as an offset to access the current ROM bank. There is one detail with RAM. MBC2 has 512 4-bit RAM blocks. Of course, we allocate all 512 bytes, just write and read operations truncate the values to the 4 least significant bits.
Now the recording function. This is where the MBC logic is emulated. MBC2 only supports changing ROM banks. They change by writing a bank number with a length of 4 bits in the address area 0x2000-0x3FFF. You cannot select a zero bank, because it is already at 0x0000-0x3FFF. It is also worth checking out the ROM limits. Some games, for an unspecified reason, try to choose a bank that does not exist. This naturally leads to an error. The game works with verification, as if nothing had happened. One such game is WordZap. Maybe these are the consequences of inaccurate emulation (I naturally do not pretend to perfect DMG emulation), but in any case, the check will not hurt.
Yes, 0xFF is not returned accidentally - on the DMG, this value is returned when the content is not defined.
Finally, consider MMM01. I am not sure of the correctness of my code, since the description of this controller was found on the forum, and it was written by an unknown person. The code:
As you can see, there is a lot of code. I will not explain each line - after the previous example, I hope that it will not be difficult for you to understand what is being done and why. I can only say that MMM01 seems to be used in only 2 games, so it is no coincidence that it is not in all emulators.
Returning to memory emulation, it’s worth a little clarification of the memory area called I / O ports. Because DMG consists of various components, it would be nice to be able to somehow influence their work and even control. To do this, in the I / O ports memory area, we have access to the registers of all other DMG components: screen controller, sound, timers, control, etc. Naturally, all these registers in our emulator will be in the corresponding classes, which means Cookieboy :: Memory will only redirect all operations in them. A list and purpose of all the registers can be found in the CPU Manual. I will also consider them if necessary. By the way, we have already considered one of them - IF. This register is available in this memory area, so you need to redirect read and write operations to the Cookieboy :: Interrupts class. We can already do this, t.
The time has come for another important feature - loading ROM from a file. Before implementing the loading of the image into memory, it's time to mention what happens when you turn on the DMG.
First comes the execution of Bootstrap ROM, which is stored inside the DMG. Its contents can be found in the source code of the Cookieboy :: Memory class. It does nothing special except checking the contents of the cartridge and displaying the Nintendo logo. It has a length of 256 bytes, execution starts at 0 - i.e. after turning on, the PC processor register is zero. Its execution ends with a command that writes to the address 0xFF50. This address contains a hidden register that indicates where the processor commands are currently coming from - from Bootstrap ROM or a cartridge. Oddly enough, there is practically no description of this register anywhere. Moreover, there is not even a mention of him.
Interesting fact. Bootstrap ROM was obtained not so long ago, and they extracted it through a photograph of the processor chip. The author photographed the part of the processor in which this ROM was located, and on the eye one bit counted all the contents.
I note that when you turn on the RAM memory of the DMG and the cartridge contains random numbers. This is a minor detail, so emulators usually fill these areas with zeros. How to act is up to you. Most likely, they won’t work better or worse from this game. Please note that this is only about RAM. Filling with random values of other areas will lead to incorrect operation of the emulator.
Of course, I would not want to launch this image every time. To do this, you can do the following. PC register should be 0x100 - this is the address where the first team in the images of the games is located. Further, all processor registers and the I / O ports memory area must be initialized with the values that Bootstrap ROM leaves behind - these values can be found in the CPU Manual. Not all games are well written to set all the necessary values on their own, some may rely on the values that are set after running Bootstrap ROM. To do this, all components contain the EmulateBIOS function, through which all the necessary values are set.
And so, let's start downloading the image. The entire image file is read into an array, and image metadata is read from the header of the image. The most important thing is to find out the type of cartridge (type of MBC controller) and the size of the external RAM inside the cartridge. Addresses are indicated in the CPU Manual. It is also worth implementing the checks that Bootstrap ROM does. Using them, you can easily find out if a file is really an image for DMG. The first check is the Nintendo logo. Each ROM contains the Nintendo logo, which appears when you run Bootstrap ROM. It must have a strictly defined meaning. Which is indicated in the CPU Manual. You can also check the checksum of the image header. To do this, use the following code:
If the checks have passed, then we allocate space for the RAM of the cartridge and create an object of the corresponding MBC chip.
Regarding the RAM of the cartridge, it’s good to always have at least one memory bank on hand, even if the image “says” that it is not used. Some games pretend to be cartridges without MBC, but, nevertheless, they can have a simple chip only for RAM.
Everything is over with memory.
Not so long ago, an article appeared on Habré about the creation of the chip-8 emulator, thanks to which it was possible to understand at least superficially how emulators are written. After implementing my emulator, a desire appeared to go further. The choice fell on the original Gameboy. As it turned out, the choice was ideal for a situation where you want to implement something more serious, and there is practically no experience in developing emulators.
In terms of emulation, Gameboy is relatively simple, but even it requires the study of a fairly large amount of information. For this reason, several articles will be devoted to the development of the Gameboy emulator. The end result will be an emulator with good compatibility, support for almost all the functions of the original, including sound, which is often absent in other emulators. As a bonus, our emulator will pass almost all test ROMs, but more on that later.
These articles will not contain an exhaustive description of the emulator implementation. This is too voluminous, and all the interest from the implementation disappears. It will reach a specific code only in rare cases. I set myself the task of giving a more theoretical description with small hints of implementation, which, ideally, should allow you to write your emulator without too much difficulty and at the same time feel that you wrote it yourself. Where necessary, I will refer to my own implementation - if necessary, you can find the right code without tearing through tons of lines of code.
In this article, we will get to know Gameboy and start by emulating its processor and memory.
We write the Gameboy emulator, part 1
We write the Gameboy emulator, part 2
We write the Gameboy emulator, part 3
Table of contents
IntroductionArchitecture Interrupt
Processor Memory Conclusion
Introduction
Gameboy is a Nintendo handheld console that was launched in 1989. It's about the original black and white Gameboy. It is worth noting that in the various documents that we will be guided by, the code name Gameboy is used - DMG (Dot Matrix Game). Further I will use it.
Before you begin, you must familiarize yourself with the technical characteristics of DMG:
CPU | 8-bit Sharp LR35902 operating at a frequency of 4.19 MHz |
RAM | 8 kB |
Video memory | 8 kB |
Screen resolution | 160x144 |
Vertical frequency | 59.73 Hz |
Sound | 4 channels, stereo sound |
After reviewing the subject, the next step is documentation. The volumes of necessary information do not allow placing absolutely everything in the article, therefore it is necessary to arm yourself with documentation in advance.
There is a great document for DMG called Gameboy CPU Manual . It includes several well-known documents from eminent developers and contains almost all the information we need. Naturally, this is not all, but at this stage this is more than enough.
I immediately warn that there will be errors in the documents, even in official ones. During this series of articles I will try to mention all the shortcomings of various documents that I could find (remember). I will also try to fill in many of the gaps. The bottom line is that there is no comprehensive description for DMG. Available materials give only a superficial picture of the work of many console nodes. If the programmer is not aware of such "pitfalls", then developing an emulator will become much more complicated than it could be. DMG is simple enough when you have reliable and detailed information on hand. And the problem is that many important details can be learned only from the source code of other emulators, which, however, does not make our task easier. The code of well-known emulators is either unnecessarily complicated (Gambatte), or it is a terrible heap, ahem,
Since the articles are written with an eye on my emulator, it's just a link to the source code and binary CookieBoy.
Architecture
Let's start with the architecture of the future emulator. To emulate DMG, we will have to implement many modules that are almost independent of each other. In such conditions, it would be foolish to go ahead, putting everything in one heap (which is often observed in other emulators. Hi VBA). A more elegant solution is to implement individual parts of DMG as separate classes that emulate their parts of iron.I say this for a reason - it was by piling all the components into one superclass that I started developing the emulator. It soon became apparent that things would go a lot easier if everyone would do what they should. Although it is worth recognizing that this approach has obvious complexity. You need to have a pretty good understanding of the internal structure of the DMG in order to correctly distinguish between class responsibilities.
So let's get started.
CPU
DMG contains an 8-bit Sharp LR35902 processor, operating at a frequency of 4194304 Hz (do not be surprised at such accuracy - we will need this number in the future). You can consider it a simplified version of the Zilog Z80 processor, which, in turn, is based on the Intel 8080. Compared to the Z80, some registers and instruction sets are missing.The processor contains eight 8-bit registers A, B, C, D, E, F, H, L and two 16-bit special purpose registers - PC and SP. Some instructions allow you to combine 8-bit registers and use them as 16-bit registers, namely AF, BC, DE, HL. For example, the BC register is the “glued” registers B and C, where the C register acts as the low byte and B the high.
Registers A, B, C, D, E, H, L are general purpose registers. Register A is also a battery. Register F contains processor flags and is not directly accessible. The following is a register outline. Bits 0 through 3 are not used.
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Flag | Z | N | H | C | 0 | 0 | 0 | 0 |
The purpose of the flags:
- Zero Flag (Z) - the flag is set (bit is 1) if the result of the last mathematical operation is zero or two operands turned out to be equal when comparing.
- Substract Flag (N) - the flag is set if the last operation was a subtraction.
- Half Carry Flag (H) - the flag is set if, as a result of the last mathematical operation, a transfer from the lower half-byte has occurred.
- Carry Flag (C) - the flag is set if a transfer occurred as a result of the last mathematical operation.
Register PC (program counter), as you might guess, is an instruction counter and contains the address of the next instruction.
The SP register (stack pointer), respectively, is a pointer to the top of the stack. For those who are not in the know, the stack is a memory area into which the values of variables, return addresses, etc. are written. SP contains the address of the top of the stack - the stack grows down, from high to low. For him, there are always at least two operations. PUSH allows you to insert a certain value - first, the SP register is reduced, and then a new value is inserted. POP allows you to retrieve a value - first, at the SP address, the value is retrieved from memory, and then SP is incremented.
The processor also contains the so-called IME (interrupt master enable) - a flag that allows interrupt processing. It takes, respectively, two values - disable (0) and enable (1).
With theory, everything can begin to be implemented. Since we will have to work with both 8-bit registers and their 16-bit pairs, it is advisable to implement a mechanism that allows simultaneous access to those and those without the need to use bit operations. To do this, declare the following type:
union WordRegister
{
struct
{
BYTE L;
BYTE H;
} bytes;
WORD word;
};
The processor registers will be stored as pairs, and we will have access to individual parts thanks to the WordRegister association. The word field will give access to the entire 16-bit register. The “bytes” field gives access to individual registers in a pair. The only thing is that registers A and F should be stored separately. Register A is a battery, which means it is used very often. A similar situation with register F - processor flags have to be set quite often.
Now let's start implementing the processor itself - the Cookieboy :: CPU class will be responsible for this. Reading and executing instructions will be implemented in the usual way - reading an opcode from memory, and then decoding and executing using the switch construct:
BYTE opcode = MMC.Read(PC);
PC++;
switch (opcode)
{
case 0x00:
break;
}
All opcodes have a length of 1 byte, but some instructions use the so-called prefix - the first byte is the prefix of the instruction set (for us, the only prefix is 0xCB), the second byte is the opcode from this set. The implementation is elementary - as soon as we stumbled upon 0xCB, we read another byte and decode it with a nested switch.
This code is placed in the void Step () function, which executes one processor instruction in one call and performs other necessary operations.
Naturally, for reading and writing to memory, we need another class - Cookieboy :: Memory, whose object can be seen above under the name "MMC". At this stage, stubs with basic methods are sufficient:
class Memory
{
public:
void Write(WORD addr, BYTE value);
BYTE Read(WORD addr);
};
The DMG processor has a fairly large number of instructions, a list of which can be found in the Gameboy CPU Manual. It also indicates which processor flags should be set and how many clock cycles each instruction takes. VERY carefully read the description of the flags - incorrectly implemented setting of the flags often leads to inoperative games, and debugging turns into torture. But I hasten to reassure a little - there are test ROMs for processor flags, but we are still far from executing ROMs.
Speaking of measures. If chip-8 was simple enough, and its emulation did not require taking into account the duration of instruction execution, then with DMG, the situation is different. The console components do not work anyhow, but are synchronized using a clock generator. For us, this means that we need to synchronize the work of all components of our emulator with the processor.
To solve this problem is quite simple. The processor is the central link in our emulator. Following the instructions, we transfer to other components the time spent by the processor in cycles to synchronize all components with each other. To do this, I use the SYNC_WITH_CPU macro (clockDelta), which transfers the time spent by the processor on the execution of the instruction. It already calls the synchronization functions of the remaining components of the emulator. The solution to the synchronization problem could be easily taken outside the limits of the processor class, if not one but.
The components of the console work simultaneously, no one waits until the processor finishes executing the instructions, as we do. Some instructions require a long time to execute, and in the process of reading and writing data to memory. The processor, as you might guess, spends a certain time reading / writing to memory (4 clock cycles). This leads to the fact that during the execution the contents of the memory can change, which, naturally, it would be nice to emulate too.
In this case, it is required to use the synchronization macro several times during execution, so that the correct data were in the memory at the time of reading or writing. Most instructions do not require such precise synchronization, and allow it to be executed after execution. Others require an exact sequence of synchronization functions and memory read / write operations.
It is nevertheless more correct and more beautiful to do it differently. We know for sure that each write or read operation from the memory of one byte takes 4 clock cycles. It is enough to add auxiliary read and write functions, which themselves call the synchronization functions. As soon as this is done, most instructions will immediately acquire the correct duration, because in reality their execution time is composed of read and write operations. Getting the opcode of a command also applies here. This is exactly what I did in my emulator, which almost completely freed me from manual synchronization and timing. Only a few instructions required my intervention.
Now let's digress a bit to clarify the situation with the bars. There is confusion in various documentation. Some documents write numbers such that, for example, NOP has a duration of 4 measures, others - 1 measure (for example, it is written in the official Nintendo documentation). To understand the reason it is worth a little distraction on the theory.
Any processor instruction has a specific duration, which we call a machine cycle. In one machine cycle, the processor can perform one action from and to, such as reading an opcode, decoding it, and executing a command; reading or writing values in memory. In turn, a machine cycle consists of machine cycles, since a processor can perform several operations in one machine cycle. And so we come to our processor. If we say that NOP lasts 4 cycles, then we are talking about machine cycles. If we are talking about 1 clock cycle for NOP, then we are talking about machine cycles. This is exactly how the DMG processor works - its machine cycle lasts 4 machine cycles and many instructions have exactly 4 cycles or 1 machine cycle - the DMG processor is able to read the opcode from memory, decode it and execute the instruction in just 4 machine cycles.
Hereinafter I will use more familiar machine clocks. They correspond to one period of the clock generator, which means that they are the minimum and indivisible unit of time for our emulator. Thus, the NOP operation will last 4 measures.
At this stage, it is already possible to fully emulate all processor instructions. Separately, it is worth mentioning some of them:
- HALT has a rather interesting behavior, which is described in the CPU Manual (2.7.3. Low-Power Mode). A decision in the forehead will cause the HALT instruction test to fail. Here you need to be careful both in the implementation of the instruction itself and in the implementation of the interrupt handler (more on this later). The implementation of the instruction is such that it does not suspend execution and leads to the mentioned bug only if the IME is zero and at the moment there are no interrupts that need to be processed (more about this later) - this is the last point omitted in most documents. Otherwise, there is no bug, and execution is suspended. Naturally, the clock and all other components continue to work, which means that we must continue to call the synchronization functions, giving 4 measures as an argument (it makes no sense to count one measure in this mode). It is as if the processor is executing NOP.
- In POP AF, it is worth considering the fact that there are unused bits in the F register. To do this, it is necessary to reset the lower 4 bits of the register F after its contents are removed from the stack.
- Instructions RLCA, RLA, RRCA, RRA always reset the flag Z in register F.
In addition to these shortcomings, there are others. The CPU Manual contains an incomplete description of the duration of the instructions. As you might guess, conditional branch instructions should have different durations depending on whether the branch has occurred or not. It would be possible to use test ROMs, but they do not work correctly on their own because of these instructions, so they display an unknown error without even starting the test. Here is a table of these instructions indicating their duration:
Opcodes | The transition did not occur | Transition has occurred |
0xC2, 0xCA, 0xD2, 0xDA | 12 | 16 |
0x20,0x28,0x30,0x38 | 8 | 12 |
0xC4, 0xCC, 0xD4, 0xDC | 12 | 24 |
0xC0,0xC8,0xD0,0xD8 | 8 | 20 |
Also for instructions RST n (opcodes 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7, 0xFF) the wrong duration is indicated. The correct value is 16 measures.
And so, at the moment, our “processor” is able to read instructions from memory, execute them and synchronize other components with itself (as it synchronizes, while all these are dummy functions). After that, we need to check if there was an interruption after all the work done.
Interruptions
An interrupt is an event that pauses the execution of current processor instructions and transfers control to the interrupt handler. DMG works on this principle.During synchronization, we call the synchronization methods of other components of the emulator, which may request an interrupt. In DMG, this is done as follows. There are two registers (where they are located will be discussed later) - IF (interrupt flags) and IE (interrupt enable). Their bits have a specific purpose, which is identical in both registers:
Bit | Interrupt |
4 | Joypad |
3 | Serial I / O transfer complete |
2 | Timer overflow |
1 | LCDC |
0 | V-blank |
IF register bits indicate which interrupts were requested. If the bit is set, then an interrupt is requested.
IE register bits enable interrupt handling. If the bit is set to one and the corresponding interrupt has been requested, then it will be processed. If not, the interrupt will not be processed.
As you can see, the identical assignment of bits is very useful and allows you to use the logical operation AND to find out which interrupts should be processed.
One important detail is that the interrupt takes the processor out of the shutdown state that occurred as a result of executing a HALT or STOP. And here the algorithm by which the interrupt registers are checked is very important. The algorithm is as follows:
- Check if there are any interrupts that are worth processing. This is done using a logical AND operation between the IE and IF registers. Additionally, it is worth performing a logical AND operation with the result and the number 0x1F to remove possible garbage, since the most significant three bits are not used in both registers.
- If there are no such interruptions, then we exit the function. If they are, then right now we must bring the processor out of the shutdown state.
- Now we are starting to process interrupts. To do this, we check if the IME flag prohibits their processing. If not, then:
- zeroing IME;
- load the PC register onto the stack;
- we call the interrupt handler by setting the PC register to the address of the handler in memory;
- set the IF register bit to the processed interrupt.
Interrupts are processed one at a time and in a strictly defined order. All information about the priorities and addresses of the handlers is indicated in the CPU Manual.
An important detail. Again, someone might have thought: interrupt handling is very similar to a procedure call, and therefore should take some time. This is true and it takes 20 measures. For some reason, this point is omitted in the documents describing the DMG.
Now we start implementation. The cookieboy :: Interrupts class will deal with us. We put IE and IF registers in it and declare functions to access these registers (we will need them later), as well as a function that allows you to request a specific interrupt (we don’t want to manipulate bits every time to request some kind of interrupt) . We also need a function that will check which interrupts are worth processing. We place a call to this function at the end of the Step function of the processor and additionally synchronize the components.
A little about interrupt request. It is done by setting the corresponding bits in the IF register. Prior to installation, IE case verification is not required. Even if the bits in it prohibit a specific interrupt, we still set the bits in the IF register for this interrupt.
If you looked at the source code of my implementation of Cookieboy :: Interrupts, you might have noticed that I am returning the value of the IE and IF registers after setting to one all the bits that are not used in them (OR operation with the value 0xE0). I do it for a reason. Many registers in I / O ports (more on that below) do not use all bits, others restrict read access to some bits or to the entire register at once. This also needs to be taken into account - for this, unused and prohibited for reading bits should be set to 1 before returning.
To summarize. Our emulator is able to execute processor instructions, synchronizes all components of the emulator with each other, processes interrupts. True, so far it's all just in words. To get a really working emulator, we need to emulate DMG memory.
Memory
We define one term in advance - a memory bank. By this is meant a memory area of a strictly defined size. There are two types of banks - ROM banks with a length of 0x4000 bytes, and RAM banks with a length of 0x2000 (once you get used to the hexadecimal number system, it will be easier for me and you). Why is this needed? The DMG processor is capable of working with 16-bit addresses, which means that the address space is limited to 0x10000 bytes. Of these, only 0x8000 bytes are reserved for the image of the game. In most cases, this is not enough and memory banks come into play.Turning to the addresses 0x4000-0x7FFF, without memory banks, we would get to this address in the image of the game. Using memory banks, we can set the image to be divided into banks, and at the address 0x4000-0x7FFF the selected bank will be displayed. Thus, at one moment in this area there is a second bank, at another - a tenth. As we want, in general. Thus, we arrive at virtual and physical addresses. 0x4000-0x7FFF are virtual addresses that do not have to match physical addresses. A physical address is the real address at which memory cells are accessed.
All this is necessary so that our DMG can work with game images that far exceed not only 0x8000 bytes, but also the entire address space. In words, all this may seem too complicated, but during the implementation it will be clear that these are extremely elementary things that are easier and faster to implement than to explain.
All the same applies to RAM. Banks allow you to expand its volume by placing microcircuits in the cartridge. In addition, this way you can implement a full-fledged storage system using the battery built into the cartridge to power the RAM.
The task of translating a virtual address into a physical one lies with the MBC controller, which is located inside the cartridge. All read and write operations in the ROM area go through it. Operations associated with external RAM are also redirected here.
Naturally, we cannot change the contents of ROM. Write operations are used as control commands for the MBC. In the CPU Manual, you can read which addresses are responsible for which functions. Thus, having written the number 9 at a specific address, we say that we want to select bank 9. After that, we can read its contents by contacting addresses 0x4000-0x7FFF.
The figure below shows the simplest MBC operation scheme. Here, the region 0x0000-0x3FFF is always redirected to bank 0, as in some real controllers, but the region 0x4000-0x7FFF is redirected to the current bank.
Consider the DMG address space scheme:
Memory section | Start address | End address |
ROM bank 0 | 0x0000 | 0x3FFF |
Switchable ROM bank | 0x4000 | 0x7FFF |
Video ram | 0x8000 | 0x9FFF |
Switchable ram bank | 0xA000 | 0xBFFF |
Internal RAM 1 | 0xC000 | 0xDFFF |
Echo of Internal RAM 1 | 0xE000 | 0xFDFF |
Oam | 0xFE00 | 0xFE9F |
Not used | 0xFEA0 | 0xFEFF |
I / O ports | 0xFF00 | 0xFF4B |
Not used | 0xFF4C | 0xFF7F |
Internal RAM 2 | 0xFF80 | 0xFFFE |
Interrupt enable register | 0xFFFF | 0xFFFF |
More about each section:
- ROM bank 0. Switchable ROM bank. These areas we have already considered.
- Video RAM More details will be considered when implementing graphics.
- Internal RAM 1. RAM inside the DMG.
- Echo of Internal RAM 1. The contents of Internal RAM 1 are duplicated here.
- OAM This is where the description of the sprites is stored.
- I / O ports. Here we get access to the registers of other DMG components.
- Internal RAM 2. RAM inside the DMG.
- Interrupt enable register. This register stores flags that allow the processing of certain interrupts. This is the same IE register that we already talked about.
Since the architecture of the emulator assumes that each DMG component will have its own class, the Cookieboy :: Memory class that emulates the memory will contain only the following memory areas - ROM banks, internal RAM 1, Echo of internal RAM 1, Switchable RAM bank, internal RAM 2. When accessing all other areas, access methods of the corresponding classes will be called.
Let's start with the read and write operations in memory. Everything is extremely simple - we look at the address and redirect operations to the corresponding memory areas. I did as follows. As you can see, many memory areas are well aligned, which allows you to implement everything using switch and logical operations. Here's what it looks like:
switch (addr & 0xF000)
{
case 0x8000:
case 0x9000:
//осуществляем операции с видеопамятью
break;
}
And no bulky conditional structures. For now, you can leave only a blank, since some areas of memory will be in other classes (for example, video memory) that we have not yet implemented. You can only implement what is really in Cookieboy :: Memory. Here it is worth paying attention to the ROM banks and Switchable RAM bank.
If the cartridge from which the ROM was removed contained an MBC controller, then in these memory areas we need to implement the logic of these controllers. You can do this very simply - access to these areas is redirected to classes that are implemented by the corresponding MBC controllers, and they themselves let them decide where, how and what. Let's look at two examples - MBC 2 and MMM01. The first is an example that will allow you to implement the rest. MMM01 is a rather strange MBC. There is practically no documentation on it, and its implementation is quite different from other MBCs. It won't hurt to fill this gap in DMG emulation.
To get started, let's get the base class MBC. It will look like this:
const int ROMBankSize = 0x4000;
const int RAMBankSize = 0x2000;
class MBC
{
public:
virtual void Write(WORD addr, BYTE value) = 0;
virtual BYTE Read(WORD addr) = 0;
virtual bool SaveRAM(const char *path, DWORD RAMSize);
virtual bool LoadRAM(const char *path, DWORD RAMSize);
protected:
MBC(BYTE *ROM, DWORD ROMSize, BYTE *RAMBanks, DWORD RAMSize) : ROM(ROM), ROMSize(ROMSize), RAMBanks(RAMBanks), RAMSize(RAMSize) {}
BYTE *ROM;
BYTE *RAMBanks;
DWORD ROMOffset;
DWORD RAMOffset;
DWORD ROMSize;
DWORD RAMSize;
};
As you can see, the write and read functions come first - they will be called from our Cookieboy :: Memory. Next come the functions of saving and loading RAM. Here we immediately prepare the way for future emulation of memory in a cartridge, which is powered by a battery to save its contents after turning off the console. I will omit their implementation - this is just saving and reading the RAMBanks array from the file, no more. Then the extremely obvious constructor and several fields:
- ROM Here we have the whole image of the game.
- RAMbanks. Here is the RAM of the cartridge.
- RAMOffset and ROMOffset. These are offsets that indicate the current memory bank.
- ROMSize and RAMSize, I think, require no explanation. Values are stored in memory banks, not in bytes.
With the base class over, now let's start implementing the class that emulates MBC2. Let's look at the code right away, and then we'll figure out how this controller works:
class MBC2 : public MBC
{
public:
MBC2(BYTE *ROM, DWORD ROMSize, BYTE *RAMBanks, DWORD RAMSize) : MBC(ROM, ROMSize, RAMBanks, RAMSize)
{
ROMOffset = ROMBankSize;
RAMOffset = 0;
}
virtual void Write(WORD addr, BYTE value)
{
switch (addr & 0xF000)
{
//ROM bank switching
case 0x2000:
case 0x3000:
ROMOffset = value & 0xF;
ROMOffset %= ROMSize;
if (ROMOffset == 0)
{
ROMOffset = 1;
}
ROMOffset *= ROMBankSize;
break;
//RAM bank 0
case 0xA000:
case 0xB000:
RAMBanks[addr - 0xA000] = value & 0xF;
break;
}
}
virtual BYTE Read(WORD addr)
{
switch (addr & 0xF000)
{
//ROM bank 0
case 0x0000:
case 0x1000:
case 0x2000:
case 0x3000:
return ROM[addr];
//ROM bank 1
case 0x4000:
case 0x5000:
case 0x6000:
case 0x7000:
return ROM[ROMOffset + (addr - 0x4000)];
//RAM bank 0
case 0xA000:
case 0xB000:
return RAMBanks[addr - 0xA000] & 0xF;
}
return 0xFF;
}
};
Reading is easy. ROMOffset is used as an offset to access the current ROM bank. There is one detail with RAM. MBC2 has 512 4-bit RAM blocks. Of course, we allocate all 512 bytes, just write and read operations truncate the values to the 4 least significant bits.
Now the recording function. This is where the MBC logic is emulated. MBC2 only supports changing ROM banks. They change by writing a bank number with a length of 4 bits in the address area 0x2000-0x3FFF. You cannot select a zero bank, because it is already at 0x0000-0x3FFF. It is also worth checking out the ROM limits. Some games, for an unspecified reason, try to choose a bank that does not exist. This naturally leads to an error. The game works with verification, as if nothing had happened. One such game is WordZap. Maybe these are the consequences of inaccurate emulation (I naturally do not pretend to perfect DMG emulation), but in any case, the check will not hurt.
Yes, 0xFF is not returned accidentally - on the DMG, this value is returned when the content is not defined.
Finally, consider MMM01. I am not sure of the correctness of my code, since the description of this controller was found on the forum, and it was written by an unknown person. The code:
class MBC_MMM01 : public MBC
{
public:
enum MMM01ModesEnum
{
MMM01MODE_ROMONLY = 0,
MMM01MODE_BANKING = 1
};
MBC_MMM01(BYTE *ROM, DWORD ROMSize, BYTE *RAMBanks, DWORD RAMSize) : MBC(ROM, ROMSize, RAMBanks, RAMSize)
{
ROMOffset = ROMBankSize;
RAMOffset = 0;
RAMEnabled = false;
Mode = MMM01MODE_ROMONLY;
ROMBase = 0x0;
}
virtual void Write(WORD addr, BYTE value)
{
switch (addr & 0xF000)
{
//Modes switching
case 0x0000:
case 0x1000:
if (Mode == MMM01MODE_ROMONLY)
{
Mode = MMM01MODE_BANKING;
}
else
{
RAMEnabled = (value & 0x0F) == 0x0A;
}
break;
//ROM bank switching
case 0x2000:
case 0x3000:
if (Mode == MMM01MODE_ROMONLY)
{
ROMBase = value & 0x3F;
ROMBase %= ROMSize - 2;
ROMBase *= ROMBankSize;
}
else
{
if (value + ROMBase / ROMBankSize > ROMSize - 3)
{
value = (ROMSize - 3 - ROMBase / ROMBankSize) & 0xFF;
}
ROMOffset = value * ROMBankSize;
}
break;
//RAM bank switching in banking mode
case 0x4000:
case 0x5000:
if (Mode == MMM01MODE_BANKING)
{
value %= RAMSize;
RAMOffset = value * RAMBankSize;
}
break;
//Switchable RAM bank
case 0xA000:
case 0xB000:
if (RAMEnabled)
{
RAMBanks[RAMOffset + (addr - 0xA000)] = value;
}
break;
}
}
virtual BYTE Read(WORD addr)
{
if (Mode == MMM01MODE_ROMONLY)
{
switch (addr & 0xF000)
{
//ROM bank 0
case 0x0000:
case 0x1000:
case 0x2000:
case 0x3000:
//ROM bank 1
case 0x4000:
case 0x5000:
case 0x6000:
case 0x7000:
return ROM[addr];
//Switchable RAM bank
case 0xA000:
case 0xB000:
if (RAMEnabled)
{
return RAMBanks[RAMOffset + (addr - 0xA000)];
}
}
}
else
{
switch (addr & 0xF000)
{
//ROM bank 0
case 0x0000:
case 0x1000:
case 0x2000:
case 0x3000:
return ROM[ROMBankSize * 2 + ROMBase + addr];
//ROM bank 1
case 0x4000:
case 0x5000:
case 0x6000:
case 0x7000:
return ROM[ROMBankSize * 2 + ROMBase + ROMOffset + (addr - 0x4000)];
//Switchable RAM bank
case 0xA000:
case 0xB000:
if (RAMEnabled)
{
return RAMBanks[RAMOffset + (addr - 0xA000)];
}
}
}
return 0xFF;
}
private:
bool RAMEnabled;
MMM01ModesEnum Mode;
DWORD ROMBase;
};
As you can see, there is a lot of code. I will not explain each line - after the previous example, I hope that it will not be difficult for you to understand what is being done and why. I can only say that MMM01 seems to be used in only 2 games, so it is no coincidence that it is not in all emulators.
Returning to memory emulation, it’s worth a little clarification of the memory area called I / O ports. Because DMG consists of various components, it would be nice to be able to somehow influence their work and even control. To do this, in the I / O ports memory area, we have access to the registers of all other DMG components: screen controller, sound, timers, control, etc. Naturally, all these registers in our emulator will be in the corresponding classes, which means Cookieboy :: Memory will only redirect all operations in them. A list and purpose of all the registers can be found in the CPU Manual. I will also consider them if necessary. By the way, we have already considered one of them - IF. This register is available in this memory area, so you need to redirect read and write operations to the Cookieboy :: Interrupts class. We can already do this, t.
The time has come for another important feature - loading ROM from a file. Before implementing the loading of the image into memory, it's time to mention what happens when you turn on the DMG.
First comes the execution of Bootstrap ROM, which is stored inside the DMG. Its contents can be found in the source code of the Cookieboy :: Memory class. It does nothing special except checking the contents of the cartridge and displaying the Nintendo logo. It has a length of 256 bytes, execution starts at 0 - i.e. after turning on, the PC processor register is zero. Its execution ends with a command that writes to the address 0xFF50. This address contains a hidden register that indicates where the processor commands are currently coming from - from Bootstrap ROM or a cartridge. Oddly enough, there is practically no description of this register anywhere. Moreover, there is not even a mention of him.
Interesting fact. Bootstrap ROM was obtained not so long ago, and they extracted it through a photograph of the processor chip. The author photographed the part of the processor in which this ROM was located, and on the eye one bit counted all the contents.
I note that when you turn on the RAM memory of the DMG and the cartridge contains random numbers. This is a minor detail, so emulators usually fill these areas with zeros. How to act is up to you. Most likely, they won’t work better or worse from this game. Please note that this is only about RAM. Filling with random values of other areas will lead to incorrect operation of the emulator.
Of course, I would not want to launch this image every time. To do this, you can do the following. PC register should be 0x100 - this is the address where the first team in the images of the games is located. Further, all processor registers and the I / O ports memory area must be initialized with the values that Bootstrap ROM leaves behind - these values can be found in the CPU Manual. Not all games are well written to set all the necessary values on their own, some may rely on the values that are set after running Bootstrap ROM. To do this, all components contain the EmulateBIOS function, through which all the necessary values are set.
And so, let's start downloading the image. The entire image file is read into an array, and image metadata is read from the header of the image. The most important thing is to find out the type of cartridge (type of MBC controller) and the size of the external RAM inside the cartridge. Addresses are indicated in the CPU Manual. It is also worth implementing the checks that Bootstrap ROM does. Using them, you can easily find out if a file is really an image for DMG. The first check is the Nintendo logo. Each ROM contains the Nintendo logo, which appears when you run Bootstrap ROM. It must have a strictly defined meaning. Which is indicated in the CPU Manual. You can also check the checksum of the image header. To do this, use the following code:
BYTE Complement = 0;
for (int i = 0x134; i <= 0x14C; i++)
{
Complement = Complement - ROM[i] - 1;
}
if (Complement != ROM[0x14D])
{
//проверка не пройдена
}
If the checks have passed, then we allocate space for the RAM of the cartridge and create an object of the corresponding MBC chip.
Regarding the RAM of the cartridge, it’s good to always have at least one memory bank on hand, even if the image “says” that it is not used. Some games pretend to be cartridges without MBC, but, nevertheless, they can have a simple chip only for RAM.
Everything is over with memory.