391 lines
10 KiB
JavaScript
391 lines
10 KiB
JavaScript
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 = NZOC
|
||
Acc: 0,
|
||
memory: null,
|
||
|
||
// Functions that update core state
|
||
/** @param {Uint8Array} data */
|
||
loadMemory: (data) => {
|
||
CPU.memory = new Uint8Array(256);
|
||
CPU.memory.set(data, 0);
|
||
},
|
||
setFlagNegative: () => { CPU.FLAGS |= 8 },
|
||
setFlagZero: () => { CPU.FLAGS |= 4 },
|
||
setFlagOverflow: () => { CPU.FLAGS |= 2 },
|
||
setFlagCarry: () => { CPU.FLAGS |= 1 },
|
||
unsetFlagNegative: () => { CPU.FLAGS &= ~8 },
|
||
unsetFlagZero: () => { CPU.FLAGS &= ~4 },
|
||
unsetFlagOverflow: () => { 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
|
||
currentInstruction: {
|
||
opcode: null,
|
||
operand: null,
|
||
mnemonic: null,
|
||
},
|
||
cycleCounter: 0,
|
||
}
|
||
|
||
|
||
// FUNCTIONS THAT MODIFY STATE
|
||
|
||
const Instructions = {
|
||
end: () => {
|
||
CPU.currentInstruction.mnemonic = 'END';
|
||
CPU.running = false;
|
||
},
|
||
|
||
store_lit: (lit) => {
|
||
CPU.currentInstruction.mnemonic = 'STO lit';
|
||
CPU.memory[lit] = CPU.Acc;
|
||
CPU.IP = CPU.IP += 2;
|
||
},
|
||
|
||
store_addr: (addr) => {
|
||
CPU.currentInstruction.mnemonic = 'STO addr';
|
||
CPU.memory[CPU.memory[addr]] = CPU.Acc;
|
||
CPU.IP = CPU.IP += 2;
|
||
},
|
||
|
||
load_lit: (lit) => {
|
||
CPU.currentInstruction.mnemonic = 'LDA lit';
|
||
CPU.Acc = lit;
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 2;
|
||
},
|
||
|
||
load_addr: (addr) => {
|
||
CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`;
|
||
CPU.Acc = CPU.memory[addr];
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 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 & 1)
|
||
if (overflow) {
|
||
CPU.setFlagOverflow();
|
||
} else {
|
||
CPU.unsetFlagOverflow();
|
||
}
|
||
CPU.Acc = sum;
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 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 & 1)
|
||
if (overflow) {
|
||
CPU.setFlagOverflow();
|
||
} else {
|
||
CPU.unsetFlagOverflow();
|
||
}
|
||
CPU.Acc = sum;
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 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 & 1)
|
||
if (overflow) {
|
||
CPU.setFlagOverflow();
|
||
} else {
|
||
CPU.unsetFlagOverflow();
|
||
}
|
||
CPU.Acc = sum;
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 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 & 1)
|
||
if (overflow) {
|
||
CPU.setFlagOverflow();
|
||
} else {
|
||
CPU.unsetFlagOverflow();
|
||
}
|
||
CPU.Acc = sum;
|
||
CPU.updateFlagNegative();
|
||
CPU.updateFlagZero();
|
||
CPU.IP = CPU.IP += 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.IP += 4;
|
||
} else {
|
||
CPU.IP += 2;
|
||
}
|
||
},
|
||
|
||
hop_addr: (addr) => {
|
||
CPU.currentInstruction.mnemonic = 'HOP addr';
|
||
if (CPU.Acc === CPU.memory[addr]) {
|
||
CPU.IP += 4;
|
||
} else {
|
||
CPU.IP += 2;
|
||
}
|
||
},
|
||
|
||
jump_lit: (lit) => {
|
||
CPU.currentInstruction.mnemonic = 'JMP lit';
|
||
CPU.IP = lit;
|
||
},
|
||
|
||
jump_addr: (addr) => {
|
||
CPU.currentInstruction.mnemonic = 'JMP addr';
|
||
CPU.IP = 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.IP += 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.IP += 4;
|
||
} else {
|
||
CPU.IP += 2;
|
||
}
|
||
},
|
||
|
||
no_op: () => {
|
||
CPU.currentInstruction.mnemonic = `NOP`;
|
||
CPU.IP += 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(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;
|
||
} else {
|
||
CPU.currentInstruction.opcode = CPU.memory[CPU.IP];
|
||
CPU.currentInstruction.operand = CPU.memory[CPU.IP+1];
|
||
let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode];
|
||
executeInstruction(CPU.currentInstruction.operand);
|
||
CPU.cycleCounter += 1;
|
||
}
|
||
}
|
||
logCPUState(debug);
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array} code - Machine code to run
|
||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||
* @param {Number} [clockSpeed] - CPU clock speed in milliseconds
|
||
**/
|
||
exports.runProgram = (code, debug = false, clockSpeed = 100) => {
|
||
startCPU(code);
|
||
// Animate the output by pausing between steps
|
||
const loop = setInterval(async () => {
|
||
stepCPU(debug);
|
||
if (!CPU.running) clearInterval(loop);
|
||
}, clockSpeed);
|
||
}
|
||
|
||
/**
|
||
* @param {Uint8Array} code - Machine code to run
|
||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||
**/
|
||
exports.singleStepProgram = (code, debug = false) => {
|
||
startCPU(code);
|
||
while (CPU.running) {
|
||
stepCPU(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(debug = false) {
|
||
console.group(`Step`);
|
||
if (!debug) console.clear();
|
||
if (debug) {
|
||
display.printDisplay(CPU.memory);
|
||
} else {
|
||
display.prettyPrintDisplay(CPU.memory);
|
||
}
|
||
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)} NZOC: ${num2bin_4bit(CPU.FLAGS)}`);
|
||
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])} ${CPU.running ? "running" : "halted" }`);
|
||
console.log();
|
||
console.groupEnd();
|
||
}; |