From b7d13087e67a220014a770510a7fd04f7d49010c Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 28 Aug 2023 16:55:24 -0400 Subject: [PATCH] cpu - WIP - Continue removing material that has to move elsewhere so that this can be treated as a library --- src/cpu.js | 201 +++++++++++++++-------------------------------------- 1 file changed, 57 insertions(+), 144 deletions(-) diff --git a/src/cpu.js b/src/cpu.js index d6861f2..5ccbc22 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -3,7 +3,7 @@ const readlineSync = require('readline-sync'); const { INITIAL_IP_ADDRESS, - DEFAULT_CYCLE_LIMIT, + DEFAULT_CYCLE_LIMIT: CYCLE_LIMIT, KEYPAD_ADDR, KEY_MAP, } = require('./machine.config'); @@ -29,6 +29,7 @@ const CPU = { /** Debug info **/ debug: { + sourceInfo: null, previousIP: 0, currentInstruction: { opcode: null, @@ -40,12 +41,17 @@ const CPU = { /** Functions that update state **/ - /** @param {Uint8Array} data */ + /** @param {Uint8Array} data **/ loadMemory(data) { this.state.memory = new Uint8Array(256); this.state.memory.set(data, 0); }, + /** @param {Array} info **/ // TODO type info + loadSourceInfo(info) { + this.debug.sourceInfo = info; + }, + incrementIP(offset) { this.state.previousIP = this.state.IP; this.state.IP = this.state.IP + offset; @@ -59,13 +65,52 @@ const CPU = { updateFlagZero() { this.state.flags.Z = this.state.acc === 0; }, 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 **/ - onTickHooks: [], + cycleStartCallbacks: [], + cycleEndCallbacks: [], /** @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(), }; -/** - * 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 - // (you can single-step, but keys aren't passed - // through to the Cardiograph) - // - // -> The fix is maybe to remove readlineSync, - // and instead stash the keypress into a buffer variable.* - // Then have the stepping function check that buffer, - // 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(); -}; \ No newline at end of file +function failInvalidOpcode() { + let info = CPU.debug.sourceInfo[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(); +} \ No newline at end of file