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 ;
}
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
}
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 ;
}
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
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
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
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
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++ ;
}
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 ;
}
}
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
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) ;
}