320 lines
8.4 KiB
JavaScript
320 lines
8.4 KiB
JavaScript
const { num2hex } = require('./conversions.js');
|
|
|
|
module.exports = class CPU {
|
|
|
|
/**
|
|
* @arg {number} initialIP
|
|
**/
|
|
constructor(initialIP, cycleLimit) {
|
|
this.running = false;
|
|
this.IP = initialIP;
|
|
this.acc = 0;
|
|
this.flags = {'C': false, 'Z': false, 'N': false, 'O': false};
|
|
this.flagNums = {0: 'C', 1: 'Z', 2: 'N', 3: 'O'};
|
|
this.instruction = { opcode: null, operand: null };
|
|
this.memory = null;
|
|
|
|
this._cycleLimit = cycleLimit;
|
|
|
|
this.dbg = {
|
|
sourceInfo: null,
|
|
currentMnemonic: null,
|
|
previousIP: initialIP,
|
|
cycleCounter: 0,
|
|
}
|
|
}
|
|
|
|
|
|
/** Public interface **/
|
|
|
|
/**
|
|
* @param {Uint8Array} machineCode
|
|
**/
|
|
loadMemory(machineCode) {
|
|
this.memory = new Uint8Array(256);
|
|
this.memory.set(machineCode, 0);
|
|
}
|
|
|
|
peek() { return; } // TODO - implement Peek
|
|
|
|
poke() { return; } // TODO - implement Poke
|
|
|
|
/** @param {Array} info **/ // TODO - document type for 'sourceInfo'
|
|
loadSourceAnnotations(info) {
|
|
this.dbg.sourceInfo = info;
|
|
}
|
|
|
|
/** Set CPU state to "state.running" **/
|
|
start() {
|
|
this.running = true;
|
|
}
|
|
|
|
/** Execute the next instruction in memory **/
|
|
step() {
|
|
this._cycleStartCallbacks.forEach((fn) => fn());
|
|
|
|
if (this.IP >= this.memory.length) {
|
|
this.running = false;
|
|
throw new Error('HALTING - IP greater than memory size');
|
|
} else {
|
|
this.instruction.opcode = this.memory[this.IP];
|
|
this.instruction.operand = this.memory[this.IP+1];
|
|
let mnem = this._nums2mnems[this.instruction.opcode];
|
|
let op = this._ops[mnem];
|
|
if (typeof op === 'undefined') { this._failInvalidOpcode(); }
|
|
op(this.instruction.operand);
|
|
this.dbg.cycleCounter += 1;
|
|
}
|
|
|
|
// Temporary limit as a lazy way to halt infinite loops
|
|
if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) {
|
|
this.running = false;
|
|
throw new Error(' HALTING - reached cycle limit');
|
|
}
|
|
|
|
this._cycleEndCallbacks.forEach((fn) => fn());
|
|
if (!this.running) process.exit();
|
|
}
|
|
|
|
|
|
/** Private methods **/
|
|
|
|
_incrementIP(offset) {
|
|
this.dbg.previousIP = this.IP;
|
|
this.IP = this.IP + offset;
|
|
}
|
|
|
|
_setIP(address) {
|
|
this.dbg.previousIP = this.IP;
|
|
this.IP = address;
|
|
}
|
|
|
|
_updateFlagZero() {
|
|
this.flags.Z = this.acc === 0;
|
|
}
|
|
|
|
_updateFlagNegative() {
|
|
if (this.acc & 128)
|
|
{ this.flags.N = true; }
|
|
else
|
|
{ this.flags.N = false; }
|
|
}
|
|
|
|
|
|
/** Hooks **/
|
|
|
|
/** @type Array<Function> **/
|
|
_cycleStartCallbacks = [];
|
|
|
|
/** @type Array<Function> **/
|
|
_cycleEndCallbacks = [];
|
|
|
|
/** @param {function} fn **/
|
|
onCycleStart(fn) { this._cycleStartCallbacks.push(fn) };
|
|
|
|
/** @param {function} fn **/
|
|
onCycleEnd(fn) { this._cycleEndCallbacks.push(fn) };
|
|
|
|
_ops = {
|
|
end: () => {
|
|
this.dbg.currentMnemonic = 'END';
|
|
this.running = false;
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
store_lit: (lit) => {
|
|
this.dbg.currentMnemonic = 'STO lit';
|
|
this.memory[lit] = this.acc;
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
store_addr: (addr) => {
|
|
this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`;
|
|
this.memory[this.memory[addr]] = this.acc;
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
load_lit: (lit) => {
|
|
this.dbg.currentMnemonic = 'LDA lit';
|
|
this.acc = lit;
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
load_addr: (addr) => {
|
|
this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`;
|
|
this.acc = this.memory[addr];
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
add_lit: (lit) => {
|
|
this.dbg.currentMnemonic = 'ADD lit';
|
|
const [sum, carry, overflow] = sumCarryOverflow(this.acc, lit);
|
|
this.acc = sum;
|
|
this.flags.C = carry;
|
|
this.flags.O = overflow;
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
add_addr: (addr) => {
|
|
this.dbg.currentMnemonic = `ADD addr; @ addr: $${num2hex(this.memory[addr])}`;
|
|
const [sum, carry, overflow] = sumCarryOverflow(this.acc, this.memory[addr]);
|
|
this.acc = sum;
|
|
this.flags.C = carry;
|
|
this.flags.O = overflow;
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
sub_lit: (lit) => {
|
|
this.dbg.currentMnemonic = 'SUB lit';
|
|
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, lit);
|
|
this.acc = difference;
|
|
this.flags.C = carry;
|
|
this.flags.O = overflow;
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
sub_addr: (addr) => {
|
|
this.dbg.currentMnemonic = `SUB addr; @ addr: $${num2hex(this.memory[addr])}`;
|
|
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, this.memory[addr]);
|
|
this.acc = difference;
|
|
this.flags.C = carry;
|
|
this.flags.O = overflow;
|
|
this._updateFlagNegative();
|
|
this._updateFlagZero();
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
hop_lit: (lit) => {
|
|
this.dbg.currentMnemonic = `HOP lit; IP+2: $${this.memory[this.IP+2]}, IP+3: $${this.memory[this.IP+3]}`;
|
|
if (this.acc === lit) {
|
|
this._incrementIP(4);
|
|
} else {
|
|
this._incrementIP(2);
|
|
}
|
|
},
|
|
|
|
hop_addr: (addr) => {
|
|
this.dbg.currentMnemonic = 'HOP addr';
|
|
if (this.acc === this.memory[addr]) {
|
|
this._incrementIP(4);
|
|
} else {
|
|
this._incrementIP(2);
|
|
}
|
|
},
|
|
|
|
jump_lit: (lit) => {
|
|
this.dbg.currentMnemonic = 'JMP lit';
|
|
this._setIP(lit);
|
|
},
|
|
|
|
jump_addr: (addr) => {
|
|
this.dbg.currentMnemonic = 'JMP addr';
|
|
this._setIP(this.memory[addr]);
|
|
},
|
|
|
|
flag_toggle: (flagNum) => {
|
|
if (flagNum === null) {
|
|
let info = this.dbg.sourceInfo[this.IP];
|
|
throw new Error(`Invalid flag number: '${flagNum}' on line ${info.lineNumber}: ${info.source}`);
|
|
}
|
|
const flagName = this.flagNums[flagNum];
|
|
this.dbg.currentMnemonic = `FTG ${flagName}`;
|
|
this.flags[flagName] = !this.flags[flagName];
|
|
this._incrementIP(2);
|
|
},
|
|
|
|
flag_hop: (flagNum) => {
|
|
if (flagNum === null) {
|
|
console.error('Invalid flag number');
|
|
process.exit();
|
|
}
|
|
const flagName = this.flagNums[flagNum];
|
|
this.dbg.currentMnemonic =
|
|
`FHP ${flagName}; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`;
|
|
if (this.flags[this.flagNums[flagNum]]) {
|
|
this._incrementIP(4);
|
|
} else {
|
|
this._incrementIP(2);
|
|
}
|
|
},
|
|
|
|
no_op: () => {
|
|
this.dbg.currentMnemonic = `NOP`;
|
|
this._incrementIP(2);
|
|
},
|
|
}
|
|
|
|
_nums2mnems = {
|
|
0: "end",
|
|
1: "store_lit",
|
|
2: "store_addr",
|
|
3: "load_lit",
|
|
4: "load_addr",
|
|
5: "add_lit",
|
|
6: "add_addr",
|
|
7: "sub_lit",
|
|
8: "sub_addr",
|
|
9: "hop_lit",
|
|
10: "hop_addr",
|
|
11: "jump_lit",
|
|
12: "jump_addr",
|
|
13: "flag_toggle",
|
|
14: "flag_hop",
|
|
15: "no_op",
|
|
}
|
|
|
|
_failInvalidOpcode() {
|
|
let info = this.dbg.sourceInfo[this.dbg.previousIP];
|
|
console.error();
|
|
console.error(`Error: Invalid opcode`);
|
|
console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`);
|
|
console.error(` from line ${info.lineNumber}: ${info.source}`);
|
|
process.exit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @arg {number} n1
|
|
* @arg {number} n2
|
|
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
|
|
**/
|
|
function sumCarryOverflow(n1, n2) {
|
|
let sum = n1 + n2;
|
|
let carry = false;
|
|
if (sum > 255) {
|
|
carry = true;
|
|
sum = (sum % 255) - 1;
|
|
}
|
|
|
|
let n1_bit6 = (n1 & 64) === 64; // Bit 6 is the 64s place
|
|
let n2_bit6 = (n2 & 64) === 64; // 64 & n == 64 where n >= 64
|
|
let carryIntoLastBit = n1_bit6 && n2_bit6;
|
|
console.log('c_in', carryIntoLastBit, 'c_out', carry);
|
|
let overflow = carryIntoLastBit != carry;
|
|
|
|
return [sum, carry, overflow];
|
|
}
|
|
|
|
/**
|
|
* @arg {number} n1
|
|
* @arg {number} n2
|
|
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
|
|
**/
|
|
function differenceCarryOverflow(n1, n2) {
|
|
// https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
|
|
// > SBC simply takes the ones complement of the second value and then performs an ADC.
|
|
//
|
|
// https://stackoverflow.com/a/8966863
|
|
// > The signed overflow flag value, however, must be the same for both A-B and A+(-B) because it depends on whether or not the result has the correct sign bit and in both cases the sign bit will be the same.
|
|
return sumCarryOverflow(n1, -n2);
|
|
} |