464 lines
12 KiB
TypeScript
464 lines
12 KiB
TypeScript
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.incrementPC();
|
|
}else if(nib4 == 0x2) {
|
|
// 8XY2
|
|
// Vx &= Vy
|
|
this.regs[nib2] &= this.regs[nib3];
|
|
this.incrementPC();
|
|
}else if(nib4 == 0x3) {
|
|
// 8XY3
|
|
// Vx ^= Vy
|
|
this.regs[nib2] ^= this.regs[nib3];
|
|
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 = 0;
|
|
if(this.regs[nib2] >= this.regs[nib3]) {
|
|
subs = this.regs[nib2] - this.regs[nib3];
|
|
}else{
|
|
subs = this.regs[nib2] - this.regs[nib3];
|
|
//subs = 0xFF - subs;
|
|
}
|
|
this.regs[0xF] = this.regs[nib2] >= this.regs[nib3] ? 1 : 0;
|
|
this.regs[nib2] = subs & 0xFF;
|
|
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.
|
|
this.regs[0xF] = this.regs[nib2] & 0x1; // Get the least significat bit
|
|
this.regs[nib2] = (this.regs[nib2] >> 1) & 0xFF;
|
|
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
|
|
this.regs[0xF] = (this.regs[nib2]) >> 7; // Get msb
|
|
this.regs[nib2] = (this.regs[nib2] << 1) & 0xFF;
|
|
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 plus 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)
|
|
this.regs[0xF] = 0; // Reset register VF
|
|
|
|
let regX = this.regs[nib2];
|
|
let regY = this.regs[nib3];
|
|
|
|
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 mem = x + regX + ((y + regY) * SCREEN_WIDTH);
|
|
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.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 + i] = 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 + i];
|
|
}
|
|
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; |