cardiograph-computer/cpu.js

415 lines
11 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,
num2bin_4bit,
} = require('./logging.js');
const display = require('./display.js');
// STATE
const CPU = {
// Core state
running: false,
IP: INITIAL_IP_ADDRESS,
FLAGS: 0, // A bit field! 0000 = ONZC
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;
},
setFlagOverflow: () => { CPU.FLAGS |= 4 },
setFlagNegative: () => { CPU.FLAGS |= 4 },
setFlagZero: () => { CPU.FLAGS |= 2 },
setFlagCarry: () => { CPU.FLAGS |= 1 },
unsetFlagOverflow: () => { CPU.FLAGS &= ~8 },
unsetFlagNegative: () => { CPU.FLAGS &= ~4 },
unsetFlagZero: () => { CPU.FLAGS &= ~2 },
unsetFlagCarry: () => { CPU.FLAGS &= ~1 },
updateFlagZero: () => {
if (CPU.Acc === 0) {
CPU.setFlagZero();
} else {
CPU.unsetFlagZero();
}
},
updateFlagNegative: () => {
CPU.Acc & 128 ? CPU.setFlagNegative : CPU.unsetFlagNegative
},
// 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';
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.setFlagCarry();
sum = (sum % 255) - 1;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
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.setFlagCarry();
sum = (sum % 255) - 1;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
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.setFlagCarry();
sum = sum + 256;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
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.setFlagCarry();
sum = sum + 256;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
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) => {
CPU.currentInstruction.mnemonic = 'FTG';
let mask = null;
if (flagNum === 0) { mask = 1; }
if (flagNum === 1) { mask = 2; }
if (flagNum === 2) { mask = 4; }
if (flagNum === 3) { mask = 8; }
if (mask === null) { throw new Error('Invalid flag number'); }
CPU.FLAGS = CPU.FLAGS ^= mask;
CPU.incrementIP(2);
},
flag_hop: (flagNum) => {
CPU.currentInstruction.mnemonic = `FHP; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
let mask = null;
if (flagNum === 0) { mask = 1; }
if (flagNum === 1) { mask = 2; }
if (flagNum === 2) { mask = 4; }
if (flagNum === 3) { mask = 8; }
if (mask === null) { throw new Error('Invalid flag number'); }
if (CPU.FLAGS & mask) {
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
**/
async function stepCPU(debugInfo, debug = 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.running) {
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);
}
/**
* @param {Uint8Array} code - Machine code to run
* @param {any} debugInfo TODO type
* @param {Boolean} [debug] - Enable/disable debugging printouts
* @param {Number} [clockSpeed] - CPU clock speed in milliseconds
**/
exports.runProgram = (code, debugInfo, debug = false, clockSpeed = 100) => {
startCPU(code);
// Animate the output by pausing between steps
const loop = setInterval(async () => {
stepCPU(debugInfo, debug);
if (!CPU.running) clearInterval(loop);
}, clockSpeed);
}
/**
* @param {Uint8Array} code - Machine code to run
* @param {any} debugInfo - TODO
* @param {Boolean} [debug] - Enable/disable debugging printouts
**/
exports.singleStepProgram = (code, debugInfo, debug = false) => {
startCPU(code);
while (CPU.running) {
stepCPU(debugInfo, debug);
// 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) {
console.group(`Step ${CPU.cycleCounter}`);
console.log();
if (!debug) console.clear();
if (debug) {
display.printDisplay(CPU.memory);
} else {
display.prettyPrintDisplay(CPU.memory);
}
console.log();
console.log(`Line ${debugInfo[CPU.previousIP].lineNumber}: ${debugInfo[CPU.previousIP].source}`);
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: ${num2bin_4bit(CPU.FLAGS)}`);
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])}  ${CPU.running ? "running" : "halted" }`);
console.log();
console.log();
console.groupEnd();
};