cardiograph-computer/cpu.js

423 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const readline = require('readline');
const readlineSync = require('readline-sync');
const {
INITIAL_IP_ADDRESS,
CYCLE_LIMIT,
KEYPAD_ADDR,
KEY_MAP,
} = require('./machine.config');
const {
num2hex,
bool2bit,
} = require('./logging.js');
const display = require('./display.js');
// STATE
const CPU = {
// Core state
running: false,
IP: INITIAL_IP_ADDRESS,
FLAGS: {'C': false, 'Z': false, 'N': false, 'O': false},
FLAGNUMS2NAMES: {0: 'C', 1: 'Z', 2: 'N', 3: 'O'},
Acc: 0,
memory: null,
// Functions that update core state
/** @param {Uint8Array} data */
loadMemory: (data) => {
CPU.memory = new Uint8Array(256);
CPU.memory.set(data, 0);
},
incrementIP: (offset) => {
CPU.previousIP = CPU.IP;
CPU.IP = CPU.IP + offset;
},
setIP: (address) => {
CPU.previousIP = CPU.IP;
CPU.IP = address;
},
updateFlagZero: () => { CPU.FLAGS.Z = CPU.Acc === 0; },
updateFlagNegative: () => { CPU.Acc & 128 ? CPU.FLAGS.N = true : CPU.FLAGS.N = false },
// Debug info
previousIP: 0,
currentInstruction: {
opcode: null,
operand: null,
mnemonic: null,
},
cycleCounter: 0,
}
// FUNCTIONS THAT MODIFY STATE
const Instructions = {
end: () => {
CPU.currentInstruction.mnemonic = 'END';
CPU.running = false;
CPU.incrementIP(2);
},
store_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'STO lit';
CPU.memory[lit] = CPU.Acc;
CPU.incrementIP(2);
},
store_addr: (addr) => {
CPU.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.memory[addr])}`;
CPU.memory[CPU.memory[addr]] = CPU.Acc;
CPU.incrementIP(2);
},
load_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'LDA lit';
CPU.Acc = lit;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
load_addr: (addr) => {
CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`;
CPU.Acc = CPU.memory[addr];
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
add_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'ADD lit';
// Calculate sum
let sum = CPU.Acc + lit;
if (sum > 255) {
CPU.FLAGS.C = true;
sum = (sum % 255) - 1;
} else {
CPU.FLAGS.C = false;
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
// FIXME FIXME FIXME
// I'm on a plane and can't remember how this works
let overflow = 0;
if (overflow) {
CPU.FLAGS.O = true;
} else {
CPU.FLAGS.O = false;
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
add_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'ADD addr';
// Calculate sum
let sum = CPU.Acc + CPU.memory[addr];
if (sum > 255) {
CPU.FLAGS.C = true;
sum = (sum % 255) - 1;
} else {
CPU.FLAGS.C = false;
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
// FIXME FIXME FIXME
// I'm on a plane and can't remember how this works
let overflow = 0;
if (overflow) {
CPU.FLAGS.O = true;
} else {
CPU.FLAGS.O = false;
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
sub_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'SUB lit';
// Calculate sum
let sum = CPU.Acc - lit;
if (sum < 0) {
CPU.FLAGS.C = true;
sum = sum + 256;
} else {
CPU.FLAGS.C = false;
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
// FIXME FIXME FIXME
// I'm on a plane and can't remember how this works
let overflow = 0;
if (overflow) {
CPU.FLAGS.O = true;
} else {
CPU.FLAGS.O = false;
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
sub_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'SUB addr';
// Calculate sum
let sum = CPU.Acc - CPU.memory[addr];
if (sum < 0) {
CPU.FLAGS.C = true;
sum = sum + 256;
} else {
CPU.FLAGS.C = false;
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
// FIXME FIXME FIXME
// I'm on a plane and can't remember how this works
let overflow = 0;
if (overflow) {
CPU.FLAGS.O = true;
} else {
CPU.FLAGS.O = false;
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.incrementIP(2);
},
hop_lit: (lit) => {
CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
if (CPU.Acc === lit) {
CPU.incrementIP(4);
} else {
CPU.incrementIP(2);
}
},
hop_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'HOP addr';
if (CPU.Acc === CPU.memory[addr]) {
CPU.incrementIP(4);
} else {
CPU.incrementIP(2);
}
},
jump_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'JMP lit';
CPU.setIP(lit);
},
jump_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'JMP addr';
CPU.setIP(CPU.memory[addr]);
},
flag_toggle: (flagNum) => {
if (flagNum === null) {
console.error('Invalid flag number');
process.exit();
}
const flagName = CPU.FLAGNUMS2NAMES[flagNum];
CPU.currentInstruction.mnemonic = `FTG ${flagName}`;
CPU.FLAGS[flagName] = !CPU.FLAGS[flagName];
CPU.incrementIP(2);
},
flag_hop: (flagNum) => {
if (flagNum === null) {
console.error('Invalid flag number');
process.exit();
}
const flagName = CPU.FLAGNUMS2NAMES[flagNum];
CPU.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
if (CPU.FLAGS[CPU.FLAGNUMS2NAMES[flagNum]]) {
CPU.incrementIP(4);
} else {
CPU.incrementIP(2);
}
},
no_op: () => {
CPU.currentInstruction.mnemonic = `NOP`;
CPU.incrementIP(2);
},
}
const opcodes2mnemonics = {
0: (operand) => Instructions.end(),
1: (operand) => Instructions.store_lit(operand),
2: (operand) => Instructions.store_addr(operand),
3: (operand) => Instructions.load_lit(operand),
4: (operand) => Instructions.load_addr(operand),
5: (operand) => Instructions.add_lit(operand),
6: (operand) => Instructions.add_addr(operand),
7: (operand) => Instructions.sub_lit(operand),
8: (operand) => Instructions.sub_addr(operand),
9: (operand) => Instructions.hop_lit(operand),
10: (operand) => Instructions.hop_addr(operand),
11: (operand) => Instructions.jump_lit(operand),
12: (operand) => Instructions.jump_addr(operand),
13: (operand) => Instructions.flag_toggle(operand),
14: (operand) => Instructions.flag_hop(operand),
15: (operand) => Instructions.no_op(),
};
/**
* Load code into memory and set CPU state to "running"
* @param {Uint8Array} code - Machine code to load
**/
function startCPU(code) {
CPU.loadMemory(code);
CPU.cycleCounter = 0;
CPU.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();
str = str.toUpperCase();
if (str in KEY_MAP) {
CPU.memory[KEYPAD_ADDR] = KEY_MAP[str];
}
});
}
/**
* 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 (CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops
if (CPU.cycleCounter > CYCLE_LIMIT) {
console.warn('HALTING - reached cycle limit');
CPU.running = false;
}
}
if (CPU.IP >= CPU.memory.length) {
console.error('HALTING - IP greater than memory size');
CPU.running = false;
process.exit();
} else {
CPU.currentInstruction.opcode = CPU.memory[CPU.IP];
CPU.currentInstruction.operand = CPU.memory[CPU.IP+1];
let executeInstruction = opcodes2mnemonics[CPU.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.currentInstruction.operand);
CPU.cycleCounter += 1;
}
logCPUState(debugInfo, debug, prettyPrintDisplay);
if (!CPU.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.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.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.cycleCounter}`);
console.log();
if (!debug) console.clear();
display.show(CPU.memory, prettyPrintDisplay);
console.log();
if (debugInfo) {
console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`);
console.log();
}
console.log('Mnemonic:', CPU.currentInstruction.mnemonic);
console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`);
console.log();
console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} ONZC ${bool2bit(CPU.FLAGS.O)}${bool2bit(CPU.FLAGS.N)}${bool2bit(CPU.FLAGS.Z)}${bool2bit(CPU.FLAGS.C)}`);
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])}  ${CPU.running ? "running" : "halted" }`);
console.log();
console.log();
console.groupEnd();
};