codeslinger.co.uk

Gameboy - Example Opcodes.

ExecuteNextOpcode:

So far this tutorial has focussed on how the internals of the gameboy fit together and how to emulate each. I often mentioned how knowing how long each instruction takes to execute on the original hardware we can synchronize the graphics and timers. This information was acquired in the main update loop from the function called ExecuteNextOpcode, however I never gave an implementation to this function, mainly because it was to do with emulating the Z80 like processor, and I was leaving this to last. As I have discussed all other aspects of the gameboy I can now give the definition of this function.

int Emulator::ExecuteNextOpcode( )
{
   int res = 0
   BYTE opcode = ReadMemory(m_ProgramCounter) ;
   m_ProgramCounter++ ;
   res = ExecuteOpcode(opcode) ;
   return res ;
}

Well thats short and sweet, however now we have to implement the ExecuteOpcode function which is a big switch statement. There are approximately 300 opcodes for the gameboy cpu so to show the entire switch statement on this page would be ridiculous, so instead of showing all of them I shall just show the ones that I will give examples to on this page.

int Emulator::ExecuteOpcode(BYTE opcode)
{
   switch(opcode)
   {
   case 0x06:
   CPU_8BIT_LOAD(m_RegisterBC.hi) ;
   return 8;

   case 0x80:
   CPU_8BIT_ADD(m_RegisterAF.hi, m_RegisterBC.hi,false,false) ;
   return 4;

   case 0x90:
   CPU_8BIT_SUB(m_RegisterAF.hi, m_RegisterBC.hi,false,false) ;
   return 4 ;

   case 0xAF:
   CPU_8BIT_XOR(m_RegisterAF.hi, m_RegisterAF.hi, false) ;
   return 4;

   case 0x20 :
   CPU_JUMP_IMMEDIATE( true, FLAG_Z, false ) ;
   return 8;

   case 0xCC :
   CPU_CALL( true, FLAG_Z, true) ;
   case 0xD0:
   CPU_RETURN( true, FLAG_C, false ) ;
   return 8;

   case 0xCB:
   return ExecuteExtendedOpcode( ) ;

   default:
   assert(false); return 0 ;// unhandled opcode }

The opcode 0xCB is the extended opcode meaning that whenever we in encounter the 0xCB opcode we have to decode the next immediate byte in memory and treat that as the opcode. The opcodes 0xAB is different to 0xCB 0xAB. This means we can have more opcodes then the standard 0x0-0xFF. ExecuteExtendedOpcode function is identical layout to ExecuteOpcode except it deals with the extended opcodes like so:

int Emulator::ExecuteExtendedOpcode( )
{
   BYTE opcode = ReadMemory(m_ProgramCounter) ;
   m_ProgramCounter++;

   switch(opcode)
   {
   case 0xB :
   CPU_RRC(m_RegisterDE.lo) ;
   return 8;

   case 0x73 :
   CPU_TEST_BIT( m_RegisterDE.lo, 6 ) ;
   return 8;

   default:
   assert(false); return 0; // unhandled extended opcode
   } Thats all there is to emulating the Z80 except of course actually implementing all the functions that are called from the switch statement :-). Remember that all the CPU instructions you have to implement can be found in the Gameboy PDF im hosting.

8-Bit Loads:

The 8-Bit Loads put one byte of immediate data from memory into one of the 8 registers. So if the function CPU_8IT_LOAD takes a reference of one of the registers as an argument like opcode 0x06 then it can be used for all 8 registers. Opcode 0x06 loads one byte of immediate data into register B. Implementation:

void Emulator::CPU_8BIT_LOAD( BYTE& reg )
{
   BYTE n = ReadMemory(m_ProgramCounter) ;
   m_ProgramCounter++ ;
   reg = n ;
}

This function can be used for all 8 registers, not just B.

8-Bit ADD:

This function adds one byte to one of the registes and sets flags. Like CPU_8BIT_LOAD if we pass a reference as a parameter then it can be used for all 8 registes not just register A as in the example (opcode 0x80). This function takes a total of 4 arguments, these are:

Argument 1: The register reference where the ADD result will be placed
Argument 2: The byte to add to the register if it is not immediate data
Argument 3: Wheter what we want to add to the register is immediate data
Argument 4: Wheter we also want to add the carry flag

There are two type of z80 8-bit add instructions. The first adds a byte to the register and the second one adds a byte plus the carry flag to the register (hence the fourth argument). This instruction is called ADC.

This is the conditions the flags get set under:

FLAG_Z: If after the addition the result is 0
FLAG_N: Set to 0
FLAG_C: If the result will be greater than max byte (255)
FLAG_H: If there will be a carry from the lower nibble to the upper nibble

FLAG_C needs to check the result before the addition, otherwise how will you know if the result was greater than 255 if when it does go greater than 255 it wraps round back to 0?

This is the implementation of 8-Bit ADD

void Emulator::CPU_8BIT_ADD(BYTE& reg, BYTE toAdd, int cycles, bool useImmediate, bool addCarry)
{
   BYTE before = reg ;
   BYTE adding = 0 ;

   // are we adding immediate data or the second param?
   if (useImmediate)
   {
     BYTE n = ReadMemory(m_ProgramCounter) ;
     m_ProgramCounter++ ;
     adding = n ;
   }
   else
   {
     adding = toAdd ;
   }

   // are we also adding the carry flag?
   if (addCarry)
   {
     if (TestBit(m_RegisterAF.lo, FLAG_C))
       adding++ ;
   }

   reg+=adding ;

   // set the flags
   m_RegisterAF.lo = 0 ;

   if (reg == 0)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_Z) ;

   WORD htest = (before & 0xF) ;
   htest += (adding & 0xF) ;

   if (htest > 0xF)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_H) ;

   if ((before + adding) > 0xFF)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_C) ;
}

8-Bit Sub:

This is pretty much identical to 8-Bit Add except its for subtractions. It still can use immediate data or the second argument and it can also minus the carry flag aswell. This is how the flags are affected:

FLAG_Z: If after the subtraction the result is 0
FLAG_N: Set to 1
FLAG_C: If the result will be less than 0
FLAG_H: If there will be a carry from the lower upper nibble to the lower nibble

It is implemented like so:

void Emulator::CPU_8BIT_SUB(BYTE& reg, BYTE subtracting, int cycles, bool useImmediate, bool subCarry)
{
   BYTE before = reg ;
   BYTE toSubtract = 0 ;

   if (useImmediate)
   {
     BYTE n = ReadMemory(m_ProgramCounter) ;
     m_ProgramCounter++ ;
     toSubtract = n ;
   }
   else
   {
     toSubtract = subtracting ;
   }

   if (subCarry)
   {
     if (TestBit(m_RegisterAF.lo, FLAG_C))
       toSubtract++ ;
   }

   reg -= toSubtract ;

   m_RegisterAF.lo = 0 ;

   if (reg == 0)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_Z) ;

   m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_N) ;

   // set if no borrow
   if (before < toSubtract)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_C) ;

   SIGNED_WORD htest = (before & 0xF) ;
   htest -= (toSubtract & 0xF) ;

   if (htest < 0)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_H) ;
}

8-Bit XOR:

All the logical operators AND, OR and XOR work the same as ADD and SUB, except it doesnt care about the carry flag (so there is no fourth argument). The flags are set like so for XOR:

FLAG_Z: If the result is 0
FLAG_N: Set to 0
FLAG_C: Set to 0
FLAG_H: Set to 0

Nice and easy. This is the implementation:

void Emulator::CPU_8BIT_XOR(BYTE& reg, BYTE toXOr, int cycles, bool useImmediate)
{
   BYTE myxor = 0 ;

   if (useImmediate)
   {
     BYTE n = ReadMemory(m_ProgramCounter) ;
     m_ProgramCounter++ ;
     myxor = n ;
   }
   else
   {
     myxor = toXOr ;
   }

   reg ^= myxor ;

   m_RegisterAF.lo = 0 ;

   if (reg == 0)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_Z) ;
}

Jumps:

There are a fair few jump instructions but they all do the same thing. Jumps to a specific address, however most of the jump instruction only jumps if a certain condition is true. Some jump instructions jumps to the address pointed to by the next two bytes of the immediate data. Where the other jump instructions jumps to the current program counter + the next immediate data (called Jump Immediate). One jump instructions jumps to the address pointed to by register HL. This is the implementation of the JUMP_IMMEDIATE instruction:

void Emulator::CPU_JUMP_IMMEDIATE(bool useCondition, int flag, bool condition)
{
   SIGNED_BYTE n = (SIGNED_BYTE)ReadMemory(m_ProgramCounter) ;

   if (!useCondition)
   {
     m_ProgramCounter += n;
   }
   else if (TestBit(m_RegisterAF.lo, flag) == condition)
   {
     m_ProgramCounter += n ;
   }

   m_ProgramCounter++ ;
}

As you can see if we are using a jump condition (i.e. jump if the Z flag is set) then we jump to address of m_ProgramCounter+ReadMemory(m_ProgramCounter).

Calls:

The call instruction is the same as the Jump instruction which jumps to the address of the next two immediate data. However the difference is that a call is a subroutine and will eventually return to its current address, where as a jump doesn't. So in order to know where to return to we have to push the current program counter on to the stack.

void Emulator::CPU_CALL(bool useCondition, int flag, bool condition)
{
   WORD nn = ReadWord( ) ;
   m_ProgramCounter += 2;

   if (!useCondition)
   {
     PushWordOntoStack(m_ProgramCounter) ;
     m_ProgramCounter = nn ;
     return ;
   }

   if (TestBit(m_RegisterAF.lo, flag)==condition)
   {
     PushWordOntoStack(m_ProgramCounter) ;
     m_ProgramCounter = nn ;
   }
}

Remember this is the same as the other Jump instruction (not jump immediate) except that the jump instruction doesnt push anything onto the stack

Returns:

Now we've handled calls to subroutines we neeed to return from them. The way you return is you pop a word from the stack and assign it to the program counter (remember you pushed the program counter onto the stack with the call function). Once again we can put a condition on the return aswell.

void Emulator::CPU_RETURN(bool useCondition, int flag, bool condition)
{
   if (!useCondition)
   {
     m_ProgramCounter = PopWordOffStack( ) ;
     return ;
   }

   if (TestBit(m_RegisterAF.lo, flag) == condition)
   {
     m_ProgramCounter = PopWordOffStack( ) ;
   }
}

Extended: Rotate Right Through Carry:

This instruction rotates a regiser A right and sets the carry flag with the least significant bit before the rotation. A right rotation means everything gets shifted right one and the most significant bit gets set to what the least significant bit was before the rotation. All the flags get set to 0 except the carry flag which as previously mentioned gets set to the least significant bit of reg a before the rotation

void Emulator::CPU_RRC(BYTE& reg)
{
   bool isLSBSet = TestBit(reg, 0) ;

   m_RegisterAF.lo = 0 ;

   reg >>= 1;

   if (isLSBSet)
   {
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_C) ;
     reg = BitSet(reg,7) ;
   }
   if (reg == 0)
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_Z) ;
}

Extended: TestBit:

This tests the bit of a byte and sets the approprite flags. The flags are set like so:

FLAG Z: Set if the bit is 0
FLAG_N: Set to 0
FLAG_C: Unchanged
FLAG_H: Set to 1

This is the implemenation:

void Emulator::CPU_TEST_BIT(BYTE reg, int bit, int cycles)
{
   if (TestBit(reg, bit))
     m_RegisterAF.lo = BitReset(m_RegisterAF.lo, FLAG_Z) ;
   else
     m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_Z) ;

   m_RegisterAF.lo = BitReset(m_RegisterAF.lo, FLAG_N) ;
   m_RegisterAF.lo = BitSet(m_RegisterAF.lo, FLAG_H) ;
}

The important thing to know with the TEST_BIT instruction (and with the SET_BIT and RESET_BIT instructions). Is that the documentation states that extended opcode 0x40 tests bit 0 of register B. But doesnt give the opcodes for when bit 1 is needed to be tested or bit 2. Luckily all you have to do is add 0x8 to get hex value of the other opcodes. So to test bit 1 of register B it is 0x48. To test bit 2 of register B it is 0x50. This also works for the other registers, for example the documentation says that opcode 0x41 tests bit 0 of register C. From this we can also deduce that 0x49 tests bit 1 of register C etc. You need to emulate all these instruction to test all 7 bits of all 8 registers. The same goes for emulating the SET_BIT and RESET_BIT functions.