import RNG from './RNG' import Chip8Audio from './audio'; import Keyboard from './keyboard'; const SCREEN_WIDTH = 64; const SCREEN_HEIGHT = 32; const MEMORY_SIZE = 4096; const STACK_SIZE = 16; const REGISTERS = 16; class CPU { pc: number; // 16b indexReg: number; // 16b regs: Uint8Array; delayTimer: number; // 8b soundTimer: number; // 8b stackPointer: number; // 16b stack: Uint16Array; // 16b memory: Uint8Array; displayMemory: Uint8Array; keyboard: Keyboard; audio: Chip8Audio; rng: RNG; constructor(keyboard: Keyboard, audio: Chip8Audio) { this.memory = new Uint8Array(MEMORY_SIZE); this.displayMemory = new Uint8Array(SCREEN_WIDTH*SCREEN_HEIGHT); this.pc = 0x200; this.indexReg = 0; this.stackPointer = 0; this.stack = new Uint16Array(STACK_SIZE); this.delayTimer = 0; this.soundTimer = 0; this.regs = new Uint8Array(REGISTERS); this.keyboard = keyboard; this.audio = audio; this.rng = new RNG(0); this.clearMemory(); this.clearDisplay(); this.clearRegisters(); // 0x000-0x1FF - Chip 8 interpreter // 0x050-0x0A0 - 4x5 pixel font set (0-F) // 0x200-0xFFF - Program ROM and work RAM this.setupFont(); } clearRegisters(): void { this.regs.fill(0); this.pc = 0x200; // Start of the program on normal programs this.indexReg = 0; this.stackPointer = 0; this.delayTimer = 0; this.soundTimer = 0; this.rng = new RNG(0); } clearMemory(): void { this.memory.fill(0); } clearDisplay(): void { this.displayMemory.fill(0); } private setupFont(): void { // Using address from 050 - 09F let addressFont = 0x050; // Every character has 5 Bytes const characters = [ 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 0x20, 0x60, 0x20, 0x20, 0x70, // 1 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 0xF0, 0x80, 0xF0, 0x80, 0x80 // F ]; this.memory.set(characters, addressFont); } public loadRom(rom: Uint8Array): void { // Loads a ROM into starting PC address for(let i = 0; i < rom.length; i++) { this.memory[this.pc+i] = rom[i]; } this.memory.set(rom, this.pc); } public getCPUStatus() : string{ let status = ""; status += "PC: " + this.pc + "\n"; status += "I: " + this.indexReg.toString(16) + "\n"; for(let i = 0; i < REGISTERS; i++) { status += "V" + i + ": " + this.regs[i].toString(16).toUpperCase().padStart(2, '0') + "\n"; } status += "SP: " + this.stackPointer.toString(16) + "\n"; for(let i = 0; i < this.stackPointer; i++) { status += "ST" + i + ": " + this.stack[i] + "\n"; } status += "\n"; let firstOpcode: number = this.memory[this.pc]; let secondOpcode: number = this.memory[this.pc + 1]; let opcode: number = firstOpcode << 8 | secondOpcode; status += "OPCODE FOR PC: " + opcode.toString(16).toUpperCase().padStart(4, '0') + "\n"; return status; } private incrementPC(): void { this.pc += 2; } public cycle(): void { // Fetch opcode let firstOpcode: number = this.memory[this.pc]; let secondOpcode: number = this.memory[this.pc + 1]; let opcode: number = firstOpcode << 8 | secondOpcode; // Decode opcode let nib1 = firstOpcode >> 4; // X000 let nib2 = firstOpcode & 0x0F; // 0X00 let nib3 = secondOpcode >> 4; // 00X0 let nib4 = secondOpcode & 0x0F; // 000X switch(nib1) { case 0x0: if(opcode == 0x00E0) { // 00E0 // Clear the screen this.clearDisplay(); this.incrementPC(); }else if(opcode == 0x00EE) { // 00EE // Returns from a subroutine this.stackPointer--; if(this.stackPointer < 0) { throw new Error("Stack pointer cant go to -1"); } this.pc = this.stack[this.stackPointer]; } break; case 0x1: { // 1NNN // goto NNN; let address = opcode & 0x0FFF; this.pc = address; } break; case 0x2: { // 2NNN // Calls subroutine at NNN this.stack[this.stackPointer] = this.pc + 2; // Store PC at stack this.stackPointer++; // Increment it let address = opcode & 0x0FFF; this.pc = address; // Go to that address to execute code } break; case 0x3: { // 3XNN // Skips the next instruction if VX equals NN if(this.regs[nib2] == secondOpcode){ this.incrementPC(); } this.incrementPC(); } break; case 0x4: { // 4XNN // Skips the next instruction if VX NOT equals NN if(this.regs[nib2] != secondOpcode){ this.incrementPC(); } this.incrementPC(); } break; case 0x5: { // 5XY0 // Skips the next instruction if VX == VY if(this.regs[nib2] == this.regs[nib3]) { this.incrementPC(); } this.incrementPC(); } break; case 0x6: { // 6XNN // Sets VX to NN this.regs[nib2] = secondOpcode; this.incrementPC(); } break; case 0x7: { // 7XNN // Adds NN to VX this.regs[nib2] += secondOpcode; this.incrementPC(); } break; case 0x8: { if(nib4 == 0x0) { // 8XY0 // Vx = Vy this.regs[nib2] = this.regs[nib3]; this.incrementPC(); }else if(nib4 == 0x1) { // 8XY1 // Vx |= Vy this.regs[nib2] |= this.regs[nib3]; this.regs[0xF] = 0; this.incrementPC(); }else if(nib4 == 0x2) { // 8XY2 // Vx &= Vy this.regs[nib2] &= this.regs[nib3]; this.regs[0xF] = 0; this.incrementPC(); }else if(nib4 == 0x3) { // 8XY3 // Vx ^= Vy this.regs[nib2] ^= this.regs[nib3]; this.regs[0xF] = 0; this.incrementPC(); }else if(nib4 == 0x4) { // 8XY4 // Vx += Vy, Set VF to carry let sum = this.regs[nib2] + this.regs[nib3]; this.regs[nib2] = sum & 0xFF; // Get only the first byte this.regs[0xF] = sum > 255 ? 1 : 0; // Set carry flag this.incrementPC(); }else if(nib4 == 0x5) { // 8XY5 // Vx -= Vy let subs = this.regs[nib2] - this.regs[nib3]; let carryBit = this.regs[nib2] >= this.regs[nib3] ? 1 : 0; this.regs[nib2] = subs & 0xFF; this.regs[0xF] = carryBit; this.incrementPC(); }else if(nib4 == 0x6) { // 8XY6 // Vx >>= 1 // Stores the least significant bit of VX prior to the shift in VF // Then. Shift Vx by 1 to the right. let lsb = this.regs[nib2] & 0x01; this.regs[nib2] = this.regs[nib3]; this.regs[nib2] = this.regs[nib2] >> 1; this.regs[0xF] = lsb; this.incrementPC(); }else if(nib4 == 0x7) { // 8XY7 // Vx = Vy - Vx let subs = this.regs[nib3] - this.regs[nib2]; this.regs[nib2] = subs; this.regs[0xF] = this.regs[nib3] > this.regs[nib2] ? 1 : 0; this.incrementPC(); }else if(nib4 == 0xE) { // 8XYE // Vx <<= 1 // Set VF to 1 if the most significant bit of VX. // Then shift to the left Vx let msb = this.regs[nib2] >> 7; this.regs[nib2] = this.regs[nib3]; this.regs[nib2] = this.regs[nib2] << 1; this.regs[0xF] = msb; this.incrementPC(); } } break; case 0x9: { // 9XY0 // Skips the next instruction if VX != VY if(this.regs[nib2] != this.regs[nib3]) { this.incrementPC(); } this.incrementPC(); } break; case 0xA: { // ANNN // Sets I to the address NNN let address = opcode & 0x0FFF; this.indexReg = address; this.incrementPC(); } break; case 0xB:{ // BNNN // Jumps to the address (NNN + V0) let address = opcode & 0x0FFF; this.pc = this.regs[0] + address; } break; case 0xC: { // CXNN // Vx = rand() & NN this.regs[nib2] = this.rng.next255() & secondOpcode; this.incrementPC(); } break; case 0xD: { // DXYN // draw(Vx, Vy, N) // Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N pixels this.regs[0xF] = 0; // Reset register VF let regX = this.regs[nib2]; let regY = this.regs[nib3]; regX = regX % 64; regY = regY % 32; for(let y = 0; y < nib4; y++) { // A sprite is always 8 bits horizontal let pixel = this.memory[this.indexReg + y]; // Fetch pixel from memory starting at I reg for(let x = 0; x < 8; x++) { const MSB = 0x80; // 0b10000000 if((pixel & (MSB >> x)) != 0) { let r = x + regX; let c = ((y + regY) * SCREEN_WIDTH); let mem = r + c; if(r+c > 64*32) { mem = mem % (64*32); } if(this.displayMemory[mem] == 1) { this.regs[0xF] = 1; } this.displayMemory[mem] ^= 1; } } } this.incrementPC(); } break; case 0xE: { if(secondOpcode == 0x9E) { // EX9E // if (key() == Vx) // Skips the next instruction if the key stored at VX is pressed if(this.keyboard.keys[this.regs[nib2]] == 1) { this.incrementPC(); } }else if(secondOpcode == 0xA1) { // EXA1 // if (key() != Vx) // Skips the next instruction if the key stored at VX is NOT pressed if(this.keyboard.keys[this.regs[nib2]] != 1) { this.incrementPC(); } } this.incrementPC(); } break; case 0xF: { if(secondOpcode == 0x07) { // FX07 // Vx = delayTimer this.regs[nib2] = this.delayTimer; this.incrementPC(); }else if(secondOpcode == 0x0A) { // FX0A // A key pressed is awaited and then stored at VX. // [BLOCKING OPERATION], all instructions are halted until next key event // Vx = get_key() let keyPressed = false; for(let i = 0; i < this.keyboard.keys.length; i++) { if(this.keyboard.keys[i] != 0) { keyPressed = true; this.regs[nib2] = i; break; } } if(!keyPressed) return; // NOT INCREMENT PC SO WE ARE STUCK IN THIS WAITING (also don't decrement timers???) this.incrementPC(); }else if(secondOpcode == 0x15) { // FX15 // delayTimer = Vx this.delayTimer = this.regs[nib2]; this.incrementPC(); }else if(secondOpcode == 0x18) { // FX18 // soundTimer = Vx this.soundTimer = this.regs[nib2]; this.incrementPC(); }else if(secondOpcode == 0x1E) { // FX1E // I += Vx // Adds VX to I. VF is not affected this.indexReg += this.regs[nib2]; this.indexReg = this.indexReg & 0xFFFF; this.incrementPC(); }else if(secondOpcode == 0x29) { // FX29 // I = sprite_addr[Vx] // Sets I to the location of the sprite for the character in VX this.indexReg = this.regs[nib2] * 0x5; this.incrementPC(); }else if(secondOpcode == 0x33) { // FX33 // set_BCD(Vx) // *(I+0) = BCD(3); // *(I+1) = BCD(2); // *(I+2) = BCD(1); // Stores the binary-coded decimal representation of Vx, with the hundreds digit memory at location in I, // this.memory[this.indexReg] = this.regs[nib2] / 100; this.memory[this.indexReg + 1] = (this.regs[nib2] / 10) % 10; this.memory[this.indexReg + 2] = this.regs[nib2] % 10; this.incrementPC(); }else if(secondOpcode == 0x55) { // FX55 // reg_dump(Vx, &I) // Stores from V0 to VX in memory starting at address I. for(let i = 0; i <= nib2; i++) { this.memory[this.indexReg++] = this.regs[i]; } this.incrementPC(); }else if(secondOpcode == 0x65) { // FX65 // reg_load(Vx, &I) // Fills fomr V0 to VX with values from memory staring at address I. for(let i = 0; i <= nib2; i++) { this.regs[i] = this.memory[this.indexReg++]; } this.incrementPC(); } } break; } // Timers if(this.delayTimer > 0) { this.delayTimer--; } if(this.soundTimer > 0) { this.audio.startBeep(); this.soundTimer--; }else{ this.audio.stopBeep(); } } } export default CPU;