//This needs to be called in a loop, it gets the next instruction from memory along with the associated data //and then figures out what todo with the data. public void Tick() { //Read two bytes from memory for the opCode and advance the program counter. var opCode = (ushort)(Memory[PC++] << 8 | Memory[PC++]); //process the opCode + data and split it up into the various variables that may be needed for the opcode. var op = new OpCodeData() { OpCode = opCode, Instruction = (ushort)((opCode & 0xF000) >> 12), NNN = (ushort)(opCode & 0x0FFF), NN = (byte)(opCode & 0x00FF), N = (byte)(opCode & 0x00F), X = (byte)((opCode & 0x0F00) >> 8), Y = (byte)((opCode & 0x00F0) >> 4) }; //Decode and execute the op codes switch (op.Instruction) { //Clear Screen / Return from subroutine case 0x0: { if (op.X == 0x0) { if (op.N == 0x0) { //Clear screen Graphics.ClearScreen(); } else if (op.N == 0xE) { //Return from sub PC = Stack.Pop(); } } break; } //0x1NNNN - Jumps to address NNN. case 0x1: { PC = (ushort)(op.NNN); break; } //0x2NNN - Calls subroutine at NNN. case 0x2: { Stack.Push((ushort)(PC)); //You need to subtract 2 from the PC as its incremented above to the next position already when its reached here. PC = (ushort)(op.NNN); break; } //0x3XNN - Skips the next instruction if VX equals NN. (Usually the next instruction is a jump to skip a code block) case 0x3: { if (V[op.X] == op.NN) { PC += 2; } break; } //0x4XNN - Skips the next instruction if VX doesn't equal NN. (Usually the next instruction is a jump to skip a code block) case 0x4: { if (V[op.X] != op.NN) { PC += 2; } break; } //0x5XY0 - Skips the next instruction if VX equals VY. (Usually the next instruction is a jump to skip a code block) case 0x5: { if (V[op.X] == V[op.Y]) { PC += 2; } break; } //0x6XNN - Sets VX to NN. case 0x6: { V[op.X] = op.NN; break; } //0x7XNN - Adds NN to VX. (Carry flag is not changed) case 0x7: { V[op.X] += op.NN; break; } //0x8XY? - BitWise / Math operations based on ? case 0x8: { //Switch based on the last nibble of the opcode switch (op.N) { case 0x0: // 8XY0 - Sets VX to the value of VY. { V[op.X] = V[op.Y]; break; } case 0x1: // 8XY1 - Sets VX to VX or VY. (Bitwise OR operation) { V[op.X] |= V[op.Y]; break; } case 0x2: // 8XY2 - Sets VX to VX and VY. (Bitwise AND operation) { V[op.X] &= V[op.Y]; break; } case 0x3: // 8XY3 - Sets VX to VX xor VY. { V[op.X] ^= V[op.Y]; break; } case 0x4: // 8XY4 - Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't. { if ((V[op.X] + V[op.Y]) > 255) { V[0xF] = 1; } else { V[0xF] = 0; } V[op.X] = (byte)(V[op.X] + V[op.Y]); break; } case 0x5: // 8XY5 - VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't. { if (V[op.X] > V[op.Y]) { V[0xF] = 1; } else { V[0xF] = 0; } V[op.X] = (byte)(V[op.X] - V[op.Y]); break; } case 0x6: // 8XY6 - Stores the least significant bit of VX in VF and then shifts VX to the right by 1. { V[0xF] = (byte)(V[op.X] & 0x1); V[op.X] >>= 1; break; } case 0x7: // 8XY7 - Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't. { if (V[op.Y] > V[op.X]) { V[0xF] = 1; } else { V[0xF] = 0; } V[op.X] = (byte)(V[op.Y] - V[op.X]); break; } case 0xE: // 8XYE - Stores the most significant bit of VX in VF and then shifts VX to the left by 1. { V[0xF] = (byte)(V[op.X] >> 7); V[op.X] <<= 1; break; } } break; } //0x9XY0 - Skips the next instruction if VX doesn't equal VY. (Usually the next instruction is a jump to skip a code block) case 0x9: { if (V[op.X] != V[op.Y]) { PC += 2; } break; } //0xANNN - Sets I to the address NNN. case 0xA: { I = op.NNN; break; } //0xBNNN - Jumps to the address NNN plus V0. case 0xB: { PC = (ushort)(op.NNN + V[0]); break; } //0xCXNN - Sets VX to the result of a bitwise and operation on a random number (Typically: 0 to 255) and NN. case 0xC: { V[op.X] = (byte)(new Random().Next(255) & op.NN); break; } //0xDXYN - Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N pixels. // Each row of 8 pixels is read as bit-coded starting from memory location I; // I value doesn’t change after the execution of this instruction. // As described above, VF is set to 1 if any screen pixels are flipped from set to unset when the sprite // is drawn, and to 0 if that doesn’t happen case 0xD: { byte[] sprite = new byte[op.N]; Array.Copy(Memory, I, sprite, 0, op.N); bool pixelChanged = Graphics.DrawSprite(V[op.X], V[op.Y], op.N, sprite); if (pixelChanged) { V[0xF] = 1; } else { V[0xF] = 0; } break; } //0xEX?? - Handles key presses case 0xE: { //EX9E - Skips the next instruction if the key stored in VX is pressed. (Usually the next instruction is a jump to skip a code block) if (op.N == 0xE) { //Check to see if the key stored in VX was pressed if (Input.KeyPressed(V[op.X])) { PC += 2; //Skip over the next instruction } } else { //EXA1 - Skips the next instruction if the key stored in VX isn't pressed. (Usually the next instruction is a jump to skip a code block) //Check to see if the key stored in VX wasn't pressed if (!Input.KeyPressed(V[op.X])) { PC += 2; //Skip over the next instruction } } break; } case 0xF: { switch (op.NN) { //0xFX07 - Sets VX to the value of the delay timer. case 0x07: { V[op.X] = DelayTimer; break; } //0xF0A - A key press is awaited, and then stored in VX. (Blocking Operation. All instruction halted until next key event) case 0x0A: { byte keyPressed = 0x00; //Check if a key has been pressed if (Input.WaitForKey(out keyPressed)) { //If a key has been pressed put it in VX V[op.X] = keyPressed; } else { PC -= 2; //If no key was pressed move the program counter one instruction back so it keep repeating this instruction. } break; } //0xFX15 - Sets the delay timer to VX. case 0x15: { DelayTimer = V[op.X]; break; } //0xFX18 - Sets the sound timer to VX. case 0x18: { SoundTimer = V[op.X]; break; } //0xFX1E - Adds VX to I case 0x1E: { I += V[op.X]; break; } //0xFX29 - Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font. case 0x29: { I = (ushort)(V[op.X] * 5); break; } //0xFX33 - Stores the binary-coded decimal representation of VX, with the most significant of three digits at the address in I, the middle digit at I plus 1, and the least significant digit at I plus 2. case 0x33: { decimal num = V[op.X]; Memory[I] = (byte)(num / 100); Memory[I + 1] = (byte)((num % 100) / 10); Memory[I + 2] = (byte)((num % 100) % 10); break; } //0xF55 - Stores V0 to VX (including VX) in memory starting at address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. case 0x55: { for (int i = 0; i <= op.X; i++) { Memory[I + i] = V[i]; } break; } //0xFX65 - Fills V0 to VX (including VX) with values from memory starting at address I. The offset from I is increased by 1 for each value written, but I itself is left unmodified. case 0x65: { for (int i = 0; i <= op.X; i++) { V[i] = Memory[I + i]; } break; } } break; } default: { throw new Exception("Unknown OpCode: " + opCode.ToString()); } } //Run the countdown timer code every 60hz or every 16ms +/- if (DateTime.Now.Subtract(LastTimerEvent).Milliseconds >= 16) { if (DelayTimer > 0) { DelayTimer--; } if (SoundTimer > 0) { SoundTimer--; if (SoundTimer == 0) { Audio.Beep(); } } LastTimerEvent = DateTime.Now; } }