codeslinger.co.uk

Gameboy - Interupts.

Interupts:

As the chip8 system didnt have any interupts I'll explain what they are and what their purpose is. An interupt is usually a hardware signal which tells the CPU that something special has happened. The CPU will check its settings to see if it can respond to that interupt. The way the CPU responds to the interupt is by saving its current program counter onto the stack and then jumping to the interupt service routine address for the appropriate interupt. It will then carry on servicing this interupts routine until it has finished where it will then reload the old program counter and carry on executing the code from where it was before it was interupted. Hence the name interupt.

There are two special registers to do with the state of interupt handling in the gameboy. The first is the Interupt Enabled register (aka IE) located at memory addres 0xFFFF. This is written to by the game to enable and disable specific interupts. For example some event may happen that would trigger an interupt like the timer overflows, but this interupt would only get serviced if its corresponding bit is enabled in the Interupt Enabled Register (aka IE). If it is enabled then the interupt would be serviced but if it is not enabled the interupt would sit pending until it becomes enabled or the game resets its request. The second interupt register is the Interupt Request Register (aka IF) located at memory address 0xFF0F. Using the timer interupt as an example again, whenever the timer overflows it requests its interupt by setting its corresponding bit in the Interupt Request Register where it will stay set until servicing of the interupt begins or the game resets it.

The final point you need to know about interupt handling is the master interupt enabled switch. This is not part of game memory and is just a bool that the game sets on and off. When this bool is set to false no interupts will get serviced. Below is the criteria needed to handle an interupt:

1. When an event occurs that needs to trigger its interupt it must make an interupt request by setting its corresponding bit in the Interupt Request Register (0xFF0F).

2. An interupt can only be serviced if the Interupt Master Enable switch is set to true

3. If the above two conditions are true and their is no other interupt with a higher priority awaiting to be serviced then it checks the Interupt Enabled Register(0xFFFF) to see if its corresponding interupt bit is set to 1 to allow servicing of this particular interupt.

Step 1 gets set to true and false by the EI and DI cpu instructions respectively. If either step 2 or 3 is false then the interupt continues to wait until both 2 and 3 are true and the game hasnt turned the interupt request off by resetting its corresponding interupt bit the Interupt Request Register(0xFF0F).

Gameboy Interupts:

I have mentioned a few times that an interupt has its corresponding bit in the Interupt Enabled Register and Interupt Request Register. I have also mentioned that there are 4 Gameboy interupts we will be emulating. Below is the list of interupts and their corresponding bit in the interupt request register and interupt enabled register.

Bit 0: V-Blank Interupt
Bit 1: LCD Interupt
Bit 2: Timer Interupt
Bit 4: Joypad Interupt

This is also the priority listing of the interupts. The lower the bit the higher priority that interupt has, so V-Blank has the highest priority meaning if this interupt and another interupt are both requested in the Interupt Request Register then V-Blank will be serviced first. Now for an explanation of the interupts:

V-BLANK: The gameboy draws the display a scanline at a time. There are 144 scanlines on the display and when it has drawn the last one it starts again from the beginning. The time it takes to stop drawing scanline 144 and starting again at scanline 0 is the Vertical Blank period and this is when it requests the v-blank interupt. This is the most important interupt to emulate correctly because during V-Blank the game can read from memory that was previously restricted, mainly video memory. As stated previously the gameboy has a vertical refresh rate of 60 frames per second meaning that if Step 2 and Step 3 of the above steps is always true then there should be 60 V-Blank interupts a second. You will want to monitor this to make sure you are accurately emulating V-Blank.

LCD: There are many reasons why the LCD would request an interupt and these will be looked at in more detail in the next chapter called LCD. The main info you need to know about the LCD interupt is it lets the game know what state the LCD is in because depending what state its in certain memory regions become restricted. The game can also set up a conincidence variable which means "let me know when you're active scanline is X". One of the things that stumped me with the LCD interupt is that it can request an LCD interupt whenever V-Blank happens. I didnt understand why there were two v-blank interupts, the main one which was discussed above and the one nested into the LCD interupt. What you need to remember is that during V-Blank if the game is allowing it there will be two V-Blank interupts requested. The first is the main V-Blank interupt and the second is the LCD interupt. However the first vblank interupt will have the higher priority.

TIMER: This interupt has been discussed previously in the timers section. Basically the gameboy timer counts up at a dynamic frequency and when it gets to value 255 and is about to overflow it lets the game know by requesting the timer interupt.

JOYPAD: The joypad will be discussed later on in the chapter Joypad. This interupt is requested whenever one of the buttons goes from the unpressed state to the pressed state.

Requesting an Interupt:

Throughout the gameboy part of this site you will see me use the function RequestInterupt. I call this whenever an event happens that needs to request an interupt and I pass it the interupt identifier bit (see table above) of the interupt I wish to request. This is how the implementation of this function looks:

void Emulator::RequestInterupt(int id)
{
   BYTE req = ReadMemory(0xFF0F) ;
   req = BitSet(req,id) ;
   WriteMemory(0xFF0F,id) ;
}

Checking the Interupts:

Now we know under what circumstances and interupt is requested we must also know how to handle it. If you look back to the Getting Started chapter you will see the main emulator update loop. Notice how after ever opcode there is a function called DoInterupts? This function works by firstly checking if the master interupt switch is set to true. If it is then it checks to see if there are any pending interupts in the Interupt Request Flag. If there is then it checks all the interupts in their priority order to see if it being requested. If it is being requested it checks to see if this particular interupt is enabled in the Interupt Enabled register and if so it services it, if not it carries on checking the lower priority interupts.

void Emulator::DoInterupts( )
{
   if (m_InteruptMaster == true)
   {
     BYTE req = ReadMemory(0xFF0F) ;
     BYTE enabled = ReadMemory(0xFFFF) ;
     if (req > 0)
     {
       for (int i = 0 ; i < 5; i++)
       {
         if (TestBit(req,i) == true)
         {
           if (TestBit(enabled,i))
             ServiceInterupt(i) ;
         }
       }
     }
   }
}

Servicing the Interupts:

Now we have detected that an interupt is to be serviced what happens then? Firstly the master interupt enabled switch gets set to false and its corresponding bit in the Interupt Request Register is now unset. Each interupt has a specific interupt service routine in game memory which the program counter gets set to and continues execution from there. When the interupt has finished servicing the program counter gets restored to where it currently was and game exection continues. The following is the location of the Interupt Service Routine for each of the 4 interupts.

V-Blank: 0x40
LCD: 0x48
TIMER: 0x50
JOYPAD: 0x60

With this new knowledge we can write the ServiceInterupt function like so:

void Emulator::ServiceInterupt(int interupt)
{
   m_InteruptMaster = false
   BYTE req = ReadMemory(0xFF0F) ;
   req = BitReset(req,interupt) ;
   WriteMemory(0xFF0F,req) ;

   /// we must save the current execution address by pushing it onto the stack
   PushWordOntoStack(m_ProgramCounter);

   switch (interupt)
   {
     case 0: m_ProgramCounter = 0x40 ; break ;
     case 1: m_ProgramCounter = 0x48 ; break ;
     case 2: m_ProgramCounter = 0x50 ; break ;
     case 4: m_ProgramCounter = 0x60 ; break ;
   }
}

Nested Interupts:

I put this section in here because it is important to know about but it is not something you need to emulate. It is possible that during the servicing of one of the interupts that the interupt re-enables the master interupt switch. By doing this the current interupt can get interupted itself by another interupt and will carry on finishing its own interupt when the new interupt finishes servicing. If you have emulated your interupts the same way I have then this functionality will automatically work but it is useful to know incase you look in your debug logs and see it is servicing one interupt before it finishes servicing another.