cpu - WIP - Continue removing material that has to move elsewhere so that this can be treated as a library
This commit is contained in:
parent
b173d46cb6
commit
b7d13087e6
201
src/cpu.js
201
src/cpu.js
|
|
@ -3,7 +3,7 @@ const readlineSync = require('readline-sync');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
INITIAL_IP_ADDRESS,
|
INITIAL_IP_ADDRESS,
|
||||||
DEFAULT_CYCLE_LIMIT,
|
DEFAULT_CYCLE_LIMIT: CYCLE_LIMIT,
|
||||||
KEYPAD_ADDR,
|
KEYPAD_ADDR,
|
||||||
KEY_MAP,
|
KEY_MAP,
|
||||||
} = require('./machine.config');
|
} = require('./machine.config');
|
||||||
|
|
@ -29,6 +29,7 @@ const CPU = {
|
||||||
|
|
||||||
/** Debug info **/
|
/** Debug info **/
|
||||||
debug: {
|
debug: {
|
||||||
|
sourceInfo: null,
|
||||||
previousIP: 0,
|
previousIP: 0,
|
||||||
currentInstruction: {
|
currentInstruction: {
|
||||||
opcode: null,
|
opcode: null,
|
||||||
|
|
@ -40,12 +41,17 @@ const CPU = {
|
||||||
|
|
||||||
/** Functions that update state **/
|
/** Functions that update state **/
|
||||||
|
|
||||||
/** @param {Uint8Array} data */
|
/** @param {Uint8Array} data **/
|
||||||
loadMemory(data) {
|
loadMemory(data) {
|
||||||
this.state.memory = new Uint8Array(256);
|
this.state.memory = new Uint8Array(256);
|
||||||
this.state.memory.set(data, 0);
|
this.state.memory.set(data, 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** @param {Array} info **/ // TODO type info
|
||||||
|
loadSourceInfo(info) {
|
||||||
|
this.debug.sourceInfo = info;
|
||||||
|
},
|
||||||
|
|
||||||
incrementIP(offset) {
|
incrementIP(offset) {
|
||||||
this.state.previousIP = this.state.IP;
|
this.state.previousIP = this.state.IP;
|
||||||
this.state.IP = this.state.IP + offset;
|
this.state.IP = this.state.IP + offset;
|
||||||
|
|
@ -59,13 +65,52 @@ const CPU = {
|
||||||
updateFlagZero() { this.state.flags.Z = this.state.acc === 0; },
|
updateFlagZero() { this.state.flags.Z = this.state.acc === 0; },
|
||||||
updateFlagNegative() { this.state.acc & 128 ? this.state.flags.N = true : this.state.flags.N = false },
|
updateFlagNegative() { this.state.acc & 128 ? this.state.flags.N = true : this.state.flags.N = false },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load code into memory and set CPU state to "state.running"
|
||||||
|
* @param {Uint8Array} code - Machine code to load
|
||||||
|
**/
|
||||||
|
startCPU(code) {
|
||||||
|
CPU.loadMemory(code);
|
||||||
|
CPU.state.running = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Execute the next instruction in memory **/
|
||||||
|
stepCPU() {
|
||||||
|
CPU.cycleStartCallbacks.forEach((fn) => fn());
|
||||||
|
|
||||||
|
if (CPU.state.IP >= CPU.state.memory.length) {
|
||||||
|
console.error('HALTING - IP greater than memory size'); // TODO -- should this throw an error instead?
|
||||||
|
CPU.state.running = false;
|
||||||
|
} else {
|
||||||
|
CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP];
|
||||||
|
CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1];
|
||||||
|
let executeInstruction = opcodes2mnemonics[CPU.debug.currentInstruction.opcode];
|
||||||
|
|
||||||
|
if (typeof executeInstruction === 'undefined') { failInvalidOpcode(); }
|
||||||
|
|
||||||
|
executeInstruction(CPU.debug.currentInstruction.operand);
|
||||||
|
CPU.debug.cycleCounter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary limit as a lazy way to halt infinite loops
|
||||||
|
if ((CYCLE_LIMIT > 0) && CPU.debug.cycleCounter >= CYCLE_LIMIT) {
|
||||||
|
console.warn(' HALTING - reached cycle limit'); // TODO -- throw error?
|
||||||
|
CPU.state.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CPU.state.running) process.exit();
|
||||||
|
CPU.cycleEndCallbacks.forEach((fn) => fn());
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/** Hooks **/
|
/** Hooks **/
|
||||||
|
|
||||||
onTickHooks: [],
|
cycleStartCallbacks: [],
|
||||||
|
cycleEndCallbacks: [],
|
||||||
|
|
||||||
/** @param {function} fn **/
|
/** @param {function} fn **/
|
||||||
onTick(fn) { this.onTickHooks.push(fn) },
|
onCycleStart(fn) { this.cycleStartCallbacks.push(fn) },
|
||||||
|
onCycleEnd(fn) { this.cycleEndCallbacks.push(fn) },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -296,144 +341,12 @@ const opcodes2mnemonics = {
|
||||||
15: (operand) => Instructions.no_op(),
|
15: (operand) => Instructions.no_op(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Load code into memory and set CPU state to "state.running"
|
|
||||||
* @param {Uint8Array} code - Machine code to load
|
|
||||||
**/
|
|
||||||
function startCPU(code) {
|
|
||||||
CPU.loadMemory(code);
|
|
||||||
CPU.debug.cycleCounter = 0;
|
|
||||||
CPU.state.running = true;
|
|
||||||
|
|
||||||
// FIXME: This conflicts with single-stepping
|
function failInvalidOpcode() {
|
||||||
// (you can single-step, but keys aren't passed
|
let info = CPU.debug.sourceInfo[CPU.previousIP];
|
||||||
// through to the Cardiograph)
|
console.error();
|
||||||
//
|
console.error(`Error: Invalid opcode`);
|
||||||
// -> The fix is maybe to remove readlineSync,
|
console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`);
|
||||||
// and instead stash the keypress into a buffer variable.*
|
console.error(` from line ${info.lineNumber}: ${info.source}`);
|
||||||
// Then have the stepping function check that buffer,
|
process.exit();
|
||||||
// and then clear the buffer, each time it runs.
|
}
|
||||||
//
|
|
||||||
// * If it's in the set of keys that are relevant
|
|
||||||
// to single-stepping.
|
|
||||||
|
|
||||||
// Start listening for keypresses...
|
|
||||||
readline.emitKeypressEvents(process.stdin);
|
|
||||||
if (process.stdin.setRawMode != null) {
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
}
|
|
||||||
process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again?
|
|
||||||
if (key.sequence === '\x03') process.exit();
|
|
||||||
let name = key.name.toUpperCase();
|
|
||||||
if (name in KEY_MAP) {
|
|
||||||
CPU.state.memory[KEYPAD_ADDR] = KEY_MAP[name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute just the next instruction in memory
|
|
||||||
* @param {Object} debugInfo
|
|
||||||
* @param {Boolean} [debug] - Print machine status and the line of code being executed
|
|
||||||
**/
|
|
||||||
async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) {
|
|
||||||
if (CPU.state.IP >= CPU.state.memory.length) {
|
|
||||||
console.error('HALTING - IP greater than memory size');
|
|
||||||
CPU.state.running = false;
|
|
||||||
process.exit();
|
|
||||||
} else {
|
|
||||||
CPU.debug.currentInstruction.opcode = CPU.state.memory[CPU.state.IP];
|
|
||||||
CPU.debug.currentInstruction.operand = CPU.state.memory[CPU.state.IP+1];
|
|
||||||
let executeInstruction = opcodes2mnemonics[CPU.debug.currentInstruction.opcode];
|
|
||||||
if (typeof executeInstruction === 'undefined') {
|
|
||||||
let info = debugInfo[CPU.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();
|
|
||||||
}
|
|
||||||
executeInstruction(CPU.debug.currentInstruction.operand);
|
|
||||||
CPU.debug.cycleCounter += 1;
|
|
||||||
}
|
|
||||||
logCPUState(debugInfo, debug, prettyPrintDisplay);
|
|
||||||
if (DEFAULT_CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops
|
|
||||||
if (CPU.debug.cycleCounter >= DEFAULT_CYCLE_LIMIT) {
|
|
||||||
console.warn(' HALTING - reached cycle limit');
|
|
||||||
CPU.state.running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!CPU.state.running) process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} code - Machine code to run
|
|
||||||
* @param {Object} debugInfo TODO type
|
|
||||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
|
||||||
* @param {Boolean} [singleStep]
|
|
||||||
* @param {Boolean} [prettyPrint] - Print display with black and white emoji, instead of in hex
|
|
||||||
* @param {Number} [clockSpeed] - CPU clock speed in milliseconds
|
|
||||||
**/
|
|
||||||
exports.runProgram =
|
|
||||||
(code, debugInfo, debug=false, singleStep=false, prettyPrint=false, clockSpeed=100) => {
|
|
||||||
if (singleStep) {
|
|
||||||
this.singleStepProgram(code, debugInfo, debug, prettyPrint);
|
|
||||||
} else {
|
|
||||||
startCPU(code);
|
|
||||||
// Animate the output by pausing between steps
|
|
||||||
const loop = setInterval(async () => {
|
|
||||||
stepCPU(debugInfo, debug, prettyPrint);
|
|
||||||
if (!CPU.state.running) {
|
|
||||||
logCPUState(debugInfo, debug, prettyPrint);
|
|
||||||
console.log('Halted');
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
}, clockSpeed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} code - Machine code to run
|
|
||||||
* @param {any} debugInfo - TODO
|
|
||||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
|
||||||
* @param {Boolean} [prettyPrintDisplay] - Print display using black and white emoji
|
|
||||||
**/
|
|
||||||
exports.singleStepProgram = (code, debugInfo, debug = false, prettyPrintDisplay = false) => {
|
|
||||||
startCPU(code);
|
|
||||||
while (CPU.state.running) {
|
|
||||||
stepCPU(debugInfo, debug, prettyPrintDisplay);
|
|
||||||
// FIXME: this prevents exiting with Ctrl+C:
|
|
||||||
let key = readlineSync.keyIn('S to step, Q to quit > ', {
|
|
||||||
limit: ['s', 'S', 'q', 'Q'],
|
|
||||||
});
|
|
||||||
if (key.toLowerCase() === 'q') { process.exit(); }
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
|
||||||
**/
|
|
||||||
function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) {
|
|
||||||
debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false;
|
|
||||||
console.group(`Step ${CPU.debug.cycleCounter}`);
|
|
||||||
console.log();
|
|
||||||
if (!debug) console.clear();
|
|
||||||
display.show(CPU.state.memory, prettyPrintDisplay);
|
|
||||||
console.log();
|
|
||||||
if (debugInfo) {
|
|
||||||
console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`);
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
console.log('Mnemonic:', CPU.debug.currentInstruction.mnemonic);
|
|
||||||
console.log(`Machine: $${num2hex(CPU.debug.currentInstruction.opcode)} $${num2hex(CPU.debug.currentInstruction.operand)}`);
|
|
||||||
console.log();
|
|
||||||
console.log(`IP: $${num2hex(CPU.state.IP)} Acc: $${num2hex(CPU.state.acc)} ONZC ${bool2bit(CPU.state.flags.O)}${bool2bit(CPU.state.flags.N)}${bool2bit(CPU.state.flags.Z)}${bool2bit(CPU.state.flags.C)}`);
|
|
||||||
console.log(`KEY: $${num2hex(CPU.state.memory[KEYPAD_ADDR])} ${CPU.state.running ? "state.running" : "halted" }`);
|
|
||||||
console.log();
|
|
||||||
console.log();
|
|
||||||
console.groupEnd();
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue