Merge branch 'rearchitect'
This commit is contained in:
commit
f3f6a58a65
|
|
@ -1,4 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
*.tmp.*
|
||||||
node_modules
|
node_modules
|
||||||
cardiograph.code-workspace
|
cardiograph.code-workspace
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "src/argparser"]
|
[submodule "src/argparser"]
|
||||||
path = src/argparser
|
path = src/opter
|
||||||
url = https://git.nloewen.com/n/argv-parser.git
|
url = https://git.nloewen.com/n/argv-parser.git
|
||||||
|
|
|
||||||
106
readme.md
106
readme.md
|
|
@ -8,52 +8,77 @@ Cardiograph is an imaginary computer. It has three main components:
|
||||||
|
|
||||||
## Simulator
|
## Simulator
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
Cardiograph is an imaginary computer. It has three main components:
|
||||||
|
|
||||||
|
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
|
||||||
|
2. an input-output processor, *IO*
|
||||||
|
3. a display, *Graph*
|
||||||
|
|
||||||
|
## Simulator
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Node.js
|
- Node.js
|
||||||
- readline-sync
|
|
||||||
|
|
||||||
### Use
|
|
||||||
|
|
||||||
#### Assemble
|
### Quick examples
|
||||||
|
|
||||||
Hex output:
|
Assemble and run:
|
||||||
```./run-assembler run source_code.asm```
|
```./assembler.js -i <source.asm> | ./cardiograph.js```
|
||||||
|
|
||||||
Binary output:
|
Assemble to a file:
|
||||||
```./run-assembler runbin source_code.asm```
|
```./assembler.js -i <source.asm> -o <machinecode.out>```
|
||||||
|
|
||||||
Verbose debugging output (hex):
|
Run from a file:
|
||||||
```./run-assembler debug source_code.asm```
|
```./cardiograph.js -i <machinecode.out>```
|
||||||
|
|
||||||
#### Assemble and run
|
|
||||||
|
|
||||||
With animated display of screen memory:
|
### Assembler: assembler.js
|
||||||
```./run-cpu run source_code.asm```
|
|
||||||
|
|
||||||
With verbose debugging output:
|
```
|
||||||
```./run-cpu debug source_code.asm```
|
Usage: ./assembler.js [-a] -i <input-file> [-o <output-file>]
|
||||||
|
|
||||||
With single stepping + pretty-printed display:
|
-a, --annotate Output code with debugging annotations
|
||||||
```./run-cpu step source_code.asm```
|
-i, --in <file> Assembly-language input
|
||||||
|
-o, --out <file> Machine-code output
|
||||||
|
```
|
||||||
|
|
||||||
With single stepping + verbose debugging output:
|
- If an output file is not provided, the output is printed to stdout
|
||||||
```./run-cpu stepdebug source_code.asm```
|
|
||||||
|
- If the `annotate` flag is not set, the machine code is returned as a string of space-separated decimal numbers
|
||||||
|
|
||||||
|
|
||||||
|
### Simulator: cardiograph.js
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: ./cardiograph.js [-i <file>]
|
||||||
|
|
||||||
|
-i, --in <file> Machine-code input
|
||||||
|
```
|
||||||
|
|
||||||
|
- If an input file is not provided, the input is read from stdin
|
||||||
|
|
||||||
|
|
||||||
## CPU
|
## CPU
|
||||||
|
|
||||||
### Registers and Flags
|
### Registers and Flags
|
||||||
|
|
||||||
- `A` - accumulator
|
There are three registers:
|
||||||
- `IP` - instruction pointer (aka program counter)
|
|
||||||
- `FLAGS` - flags: **O**verflow, **N**egative, **Z**ero, **C**arry
|
1. **A**, an 8-bit accumulator
|
||||||
- in machine language, each flag is given a number:
|
2. **IP**, an 8-bit instruction pointer (aka program counter)
|
||||||
- O = 3
|
3. **flags**, a 4-bit flag register
|
||||||
N = 2
|
|
||||||
Z = 1
|
The four flags are **O**verflow, **N**egative, **Z**ero, and **C**arry.
|
||||||
C = 0
|
|
||||||
- (bitwise, `0000 = ONZC`)
|
(Overflow is the high bit and carry is the low bit.)
|
||||||
|
|
||||||
|
In decimal:
|
||||||
|
|
||||||
|
| O | N | Z | C |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3 | 2 | 1 | 0 |
|
||||||
|
|
||||||
|
|
||||||
### Instruction set
|
### Instruction set
|
||||||
|
|
@ -82,7 +107,7 @@ Hex Mnem. Operand Effect
|
||||||
```
|
```
|
||||||
|
|
||||||
- Instructions are two bytes long:
|
- Instructions are two bytes long:
|
||||||
one byte for the opcode, one for the operand
|
one byte for the opcode, one for the operand
|
||||||
|
|
||||||
|
|
||||||
#### Effects on memory, flags, registers
|
#### Effects on memory, flags, registers
|
||||||
|
|
@ -118,6 +143,9 @@ When starting up, the CPU executes a `JMP $FF`.
|
||||||
|
|
||||||
Put differently: it starts executing instructions at the address contained in `$FF`.
|
Put differently: it starts executing instructions at the address contained in `$FF`.
|
||||||
|
|
||||||
|
<mark>TODO: currently the simulator doesn't actually do this</mark>
|
||||||
|
|
||||||
|
|
||||||
### Assembly language
|
### Assembly language
|
||||||
|
|
||||||
ADD $01 ; comments follow a `;`
|
ADD $01 ; comments follow a `;`
|
||||||
|
|
@ -144,18 +172,24 @@ Put differently: it starts executing instructions at the address contained in `$
|
||||||
LDA * ; `*` is a special label referencing the memory address
|
LDA * ; `*` is a special label referencing the memory address
|
||||||
; where the current line will be stored after assembly
|
; where the current line will be stored after assembly
|
||||||
|
|
||||||
- Hexadecimal numbers are preceded by a `$`
|
- Prefix hexadecimal numbers with `$` (or `0x`)
|
||||||
|
- Prefix binary numbers with `0b`
|
||||||
- Whitespace is ignored
|
- Whitespace is ignored
|
||||||
|
|
||||||
## Cardiograph memory map
|
## Cardiograph memory map
|
||||||
|
|
||||||
- `00-19` - display (5x5)
|
| Address | Used for... |
|
||||||
- `1A ` - pointer to display memory
|
|----------|-----------------------------------------------|
|
||||||
- `1B ` - keypad: value of latest key pressed
|
| 00 to 19 | display (5x5) |
|
||||||
- `1C ` - reserved for future use (bank switching flag)
|
| 1A | pointer to display memory |
|
||||||
- `1D ` - initial IP
|
| 1B | keypad: value of latest key pressed |
|
||||||
- `1D-FE` - free
|
| 1C | reserved for future use (bank switching flag) |
|
||||||
- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented)
|
| 1D | initial IP |
|
||||||
|
| 1D to FE | free |
|
||||||
|
| FF | * ROM (unwriteable) pointer to initial IP |
|
||||||
|
|
||||||
|
\* Not implemented yet
|
||||||
|
|
||||||
|
|
||||||
## Cardiograph peripherals
|
## Cardiograph peripherals
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 584d9dd95f4b1b3c69065826cf96b3cda0cf9e16
|
|
||||||
|
|
@ -1,31 +1,21 @@
|
||||||
const { logMemory, num2hex } = require('./logging.js');
|
#!/usr/bin/env node
|
||||||
const {
|
|
||||||
INITIAL_IP_ADDRESS,
|
|
||||||
DISPLAY_ADDR,
|
|
||||||
POINTER_TO_DISPLAY,
|
|
||||||
} = require('./machine.config.js');
|
|
||||||
|
|
||||||
// 1 = verbose
|
const fs = require('fs');
|
||||||
// 2 = what i'm currently focusing on
|
|
||||||
// 3 = always print
|
|
||||||
// 4 = silent
|
|
||||||
const DEBUG_LEVEL = 2;
|
|
||||||
let DEBUG; // Turn debugging on/off -- set by assemble()
|
|
||||||
|
|
||||||
/**
|
const Opter = require('./opter/opter.js');
|
||||||
* @param {string} assemblyCode
|
const { logMemory } = require('./logging.js');
|
||||||
* @param {Boolean} [debug = false]
|
const { num2hex, hex2num, bin2num } = require('./conversions.js');
|
||||||
**/
|
const DBG = require('./dbg.js');
|
||||||
exports.assemble = (assemblyCode, debug = false) => {
|
|
||||||
DEBUG = debug;
|
|
||||||
return decodeInstructions(assemblyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure pseudo-ops:
|
const CFG = require('./machine.config.js');
|
||||||
|
|
||||||
|
|
||||||
|
/** Configure pseudo-ops **/
|
||||||
const ASM_IP_LABEL = '*';
|
const ASM_IP_LABEL = '*';
|
||||||
const ASM_CONSTANT_PREFIX = '#';
|
const ASM_CONSTANT_PREFIX = '#';
|
||||||
const ASM_LABEL_PREFIX = '@';
|
const ASM_LABEL_PREFIX = '@';
|
||||||
|
|
||||||
|
/** Configure mnemonics **/
|
||||||
const mnemonicsWithOptionalArgs = ['end', 'nop'];
|
const mnemonicsWithOptionalArgs = ['end', 'nop'];
|
||||||
const mnemonics2opcodes = {
|
const mnemonics2opcodes = {
|
||||||
end: { direct: 0, indirect: 0 },
|
end: { direct: 0, indirect: 0 },
|
||||||
|
|
@ -40,7 +30,6 @@ const mnemonics2opcodes = {
|
||||||
nop: { direct: 15, indirect: 15 },
|
nop: { direct: 15, indirect: 15 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {('code'|'comment'|'blank')} SourceLineType
|
* @typedef {('code'|'comment'|'blank')} SourceLineType
|
||||||
**/
|
**/
|
||||||
|
|
@ -77,15 +66,15 @@ function preparseSourceCode(source) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.map((line, index) => {
|
return lines.map((line, index) => {
|
||||||
dbg(1, ` in: ${line}`);
|
dbg.nit(` in: ${line}`);
|
||||||
let info = {
|
let info = {
|
||||||
number: index + 1,
|
number: index + 1,
|
||||||
source: line,
|
source: line,
|
||||||
sanitized: stripWhitespaceFromEnds(stripComments(line)),
|
sanitized: stripWhitespaceFromEnds(stripComments(line)),
|
||||||
type: getLineType(line),
|
type: getLineType(line),
|
||||||
};
|
};
|
||||||
dbg(1, ` → ${info.number} - ${info.type}: ${info.sanitized}`);
|
dbg.nit(` → ${info.number} - ${info.type}: ${info.sanitized}`);
|
||||||
dbg(1, ``);
|
dbg.nit(``);
|
||||||
|
|
||||||
if (info.type === 'code') {
|
if (info.type === 'code') {
|
||||||
const op_arg_array = info.sanitized.split(/\s+/); // split line into an array of [op, arg, extra_arg]
|
const op_arg_array = info.sanitized.split(/\s+/); // split line into an array of [op, arg, extra_arg]
|
||||||
|
|
@ -125,13 +114,15 @@ function preparseSourceCode(source) {
|
||||||
**/
|
**/
|
||||||
function decodeNumericOp(arg) {
|
function decodeNumericOp(arg) {
|
||||||
if (arg.startsWith("$")) return hex2num(arg.replace("$", ""));
|
if (arg.startsWith("$")) return hex2num(arg.replace("$", ""));
|
||||||
|
if (arg.startsWith("0x")) return hex2num(arg.replace("0x", ""));
|
||||||
|
if (arg.startsWith("0b")) return bin2num(arg.replace("0b", ""));
|
||||||
return parseInt(arg);
|
return parseInt(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} op
|
* @param {string} op
|
||||||
* @param {object} labels // TODO document better
|
* @param {object} labels // TODO - document labels object
|
||||||
* @param {number} IP
|
* @param {number} IP
|
||||||
* @returns {Array<string>} - array of labels
|
* @returns {Array<string>} - array of labels
|
||||||
**/
|
**/
|
||||||
|
|
@ -146,11 +137,11 @@ function handleLabelDefinition(op, IP, labels) {
|
||||||
bytesToReplace: [],
|
bytesToReplace: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
dbg(1, ` Label definition:`);
|
dbg.nit(` Label definition:`);
|
||||||
dbg(1, ` Points to byte: ${labels[label].pointsToByte}`);
|
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
|
||||||
dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`);
|
dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`);
|
||||||
dbg(1, ` IP: $${num2hex(IP)}, new code: none`);
|
dbg.nit(` IP: $${num2hex(IP)}, new code: none`);
|
||||||
dbgGroupEnd(1, 'Input line');
|
dbg.nitGroupEnd('Input line');
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,10 +158,10 @@ function handleConstantDefinitions(op, arg, IP, constants) {
|
||||||
constantValue = IP.toString();
|
constantValue = IP.toString();
|
||||||
}
|
}
|
||||||
constants[constantName] = constantValue;
|
constants[constantName] = constantValue;
|
||||||
dbg(1, '');
|
dbg.nit('');
|
||||||
dbg(1, `Constants:`);
|
dbg.nit(`Constants:`);
|
||||||
dbg(1, constants);
|
dbg.nit(constants);
|
||||||
dbg(1, '');
|
dbg.nit('');
|
||||||
return constants;
|
return constants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,15 +173,16 @@ function handleConstantDefinitions(op, arg, IP, constants) {
|
||||||
* it will be assembled to the default intial value of the IP,
|
* it will be assembled to the default intial value of the IP,
|
||||||
* as specified in `machine.config.js`.
|
* as specified in `machine.config.js`.
|
||||||
* @param {string} source - Assembly source to decode
|
* @param {string} source - Assembly source to decode
|
||||||
* @return {{ debugInfo: Object, machineCode: Uint8Array }};
|
* @return {{ sourceAnnotations: Object, machineCode: Array }};
|
||||||
**/
|
**/
|
||||||
|
// TODO rename?
|
||||||
function decodeInstructions(source) {
|
function decodeInstructions(source) {
|
||||||
dbg(1, 'Pre-parsing...');
|
dbg.nit('Pre-parsing...');
|
||||||
let lines = preparseSourceCode(source);
|
let lines = preparseSourceCode(source);
|
||||||
dbg(1, '');
|
dbg.nit('');
|
||||||
dbg(1, 'Done pre-parsing.');
|
dbg.nit('Done pre-parsing.');
|
||||||
dbg(1, '');
|
dbg.nit('');
|
||||||
dbg(1, 'Assembling...');
|
dbg.nit('Assembling...');
|
||||||
|
|
||||||
// Figure out where to start assembly...
|
// Figure out where to start assembly...
|
||||||
|
|
||||||
|
|
@ -203,7 +195,7 @@ function decodeInstructions(source) {
|
||||||
if (lines[idOfFirstLineWithCode].operation.startsWith(ASM_IP_LABEL)) {
|
if (lines[idOfFirstLineWithCode].operation.startsWith(ASM_IP_LABEL)) {
|
||||||
IP = parseInt(lines[idOfFirstLineWithCode].argument);
|
IP = parseInt(lines[idOfFirstLineWithCode].argument);
|
||||||
} else {
|
} else {
|
||||||
IP = INITIAL_IP_ADDRESS;
|
IP = CFG.initialIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize arrays to collect assembled code
|
// Initialize arrays to collect assembled code
|
||||||
|
|
@ -211,18 +203,18 @@ function decodeInstructions(source) {
|
||||||
/** @type {Array<number>} - Assembled source code, as an array of bytes **/
|
/** @type {Array<number>} - Assembled source code, as an array of bytes **/
|
||||||
let machineCode = new Array(IP).fill(0);
|
let machineCode = new Array(IP).fill(0);
|
||||||
|
|
||||||
let debugInfo = {};
|
let sourceAnnotations = {};
|
||||||
|
|
||||||
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
|
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
|
||||||
machineCode[POINTER_TO_DISPLAY] = DISPLAY_ADDR;
|
machineCode[CFG.pointerToDisplay] = CFG.displayAddr;
|
||||||
|
|
||||||
// Initialize arrays that collect code references that
|
// Initialize arrays that collect code references that
|
||||||
// have to be revisited after our first pass through the source
|
// have to be revisited after our first pass through the source
|
||||||
let labels = {};
|
let labels = {};
|
||||||
let constants = {};
|
let constants = {};
|
||||||
|
|
||||||
|
|
||||||
// Decode line by line...
|
// Decode line by line...
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
let line = lines[i];
|
let line = lines[i];
|
||||||
// dbg(2, `line info:`);
|
// dbg(2, `line info:`);
|
||||||
|
|
@ -248,7 +240,6 @@ function decodeInstructions(source) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// *** Decode special operations ***
|
// *** Decode special operations ***
|
||||||
|
|
||||||
// Opcodes - Handle label definitions
|
// Opcodes - Handle label definitions
|
||||||
|
|
@ -287,23 +278,23 @@ function decodeInstructions(source) {
|
||||||
if (line.argument.startsWith(ASM_LABEL_PREFIX)) {
|
if (line.argument.startsWith(ASM_LABEL_PREFIX)) {
|
||||||
let label = line.argument.substring(1); // strip label prefix
|
let label = line.argument.substring(1); // strip label prefix
|
||||||
if (label in labels) {
|
if (label in labels) {
|
||||||
dbg(1, `'${label}' already in labels object`);
|
dbg.nit(`'${label}' already in labels object`);
|
||||||
labels[label].bytesToReplace.push(IP + 1);
|
labels[label].bytesToReplace.push(IP + 1);
|
||||||
} else {
|
} else {
|
||||||
dbg(1, `'${label}' NOT in labels object`);
|
dbg.nit(`'${label}' NOT in labels object`);
|
||||||
labels[label] = {
|
labels[label] = {
|
||||||
bytesToReplace: [IP + 1],
|
bytesToReplace: [IP + 1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
dbg(1, `Label reference:`);
|
dbg.nit(`Label reference:`);
|
||||||
dbg(1, ` Points to byte: ${labels[label].pointsToByte}`);
|
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
|
||||||
dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`);
|
dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`);
|
||||||
decodedArg = 0; // Return 0 for operand for now -- we'll replace it later
|
decodedArg = 0; // Return 0 for operand for now -- we'll replace it later
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operands - Handle references to the Instruction Pointer
|
// Operands - Handle references to the Instruction Pointer
|
||||||
if (line.argument === ASM_IP_LABEL) {
|
if (line.argument === ASM_IP_LABEL) {
|
||||||
dbg(1, ` References current IP - ${IP}`);
|
dbg.nit(` References current IP - ${IP}`);
|
||||||
if (typeof line.extraArgument === 'undefined') {
|
if (typeof line.extraArgument === 'undefined') {
|
||||||
decodedArg = IP;
|
decodedArg = IP;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -313,7 +304,7 @@ function decodeInstructions(source) {
|
||||||
|
|
||||||
// Operands - Handle references to constants
|
// Operands - Handle references to constants
|
||||||
if (line.argument.startsWith(ASM_CONSTANT_PREFIX)) {
|
if (line.argument.startsWith(ASM_CONSTANT_PREFIX)) {
|
||||||
dbg(1, `References '${line.argument}'`);
|
dbg.nit(`References '${line.argument}'`);
|
||||||
if (typeof constants[line.argument.substring(1)] === 'undefined') {
|
if (typeof constants[line.argument.substring(1)] === 'undefined') {
|
||||||
console.error();
|
console.error();
|
||||||
console.error(`Error: Undefined constant '${line.argument}'`);
|
console.error(`Error: Undefined constant '${line.argument}'`);
|
||||||
|
|
@ -326,7 +317,7 @@ function decodeInstructions(source) {
|
||||||
// Operands - Handle references to constants in indirect mode
|
// Operands - Handle references to constants in indirect mode
|
||||||
if (line.argument.startsWith(`(${ASM_CONSTANT_PREFIX}`)) {
|
if (line.argument.startsWith(`(${ASM_CONSTANT_PREFIX}`)) {
|
||||||
addressingMode = "indirect";
|
addressingMode = "indirect";
|
||||||
dbg(1, `(Indirectly) References '${line.argument}'`);
|
dbg.nit(`(Indirectly) References '${line.argument}'`);
|
||||||
let constName = line.argument.replace(`(${ASM_CONSTANT_PREFIX}`, "");
|
let constName = line.argument.replace(`(${ASM_CONSTANT_PREFIX}`, "");
|
||||||
constName = constName.replace(")", "");
|
constName = constName.replace(")", "");
|
||||||
decodedArg = decodeNumericOp(constants[constName]);
|
decodedArg = decodeNumericOp(constants[constName]);
|
||||||
|
|
@ -352,46 +343,45 @@ function decodeInstructions(source) {
|
||||||
machineCode[IP] = decodedOp;
|
machineCode[IP] = decodedOp;
|
||||||
machineCode[IP + 1] = decodedArg;
|
machineCode[IP + 1] = decodedArg;
|
||||||
|
|
||||||
debugInfo[IP] = {
|
sourceAnnotations[IP] = {
|
||||||
lineNumber: line.number,
|
lineNumber: line.number,
|
||||||
source: line.source,
|
source: line.source,
|
||||||
address: IP,
|
address: IP,
|
||||||
machine: [decodedOp, decodedArg]
|
machine: [decodedOp, decodedArg]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dbg.i();
|
||||||
dbg(3, ``);
|
dbg.i(`Line ${line.number}: ${line.source}`);
|
||||||
dbg(3, `Line ${line.number}: ${line.source}`);
|
|
||||||
if (line.argument) {
|
if (line.argument) {
|
||||||
dbg(3, ` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`);
|
dbg.i(` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`);
|
||||||
} else if (line.operation) {
|
} else if (line.operation) {
|
||||||
dbg(3, ` Asm operation: ${line.operation.toUpperCase()}`);
|
dbg.i(` Asm operation: ${line.operation.toUpperCase()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbg(3, ` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`);
|
dbg.i(` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`);
|
||||||
dbg(3, ` IP: $${num2hex(IP)}`);
|
dbg.i(` IP: $${num2hex(IP)}`);
|
||||||
IP += 2;
|
IP += 2;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
dbg(1, '');
|
dbg.nit('');
|
||||||
dbgGroup(1, 'Memory before filling in label constants');
|
dbg.nitGroup('Memory before filling in label constants');
|
||||||
dbgExec(1, () => logMemory(new Uint8Array(machineCode)));
|
dbg.nitExec(() => logMemory(new Uint8Array(machineCode)));
|
||||||
dbgGroupEnd(1);
|
dbg.nitGroupEnd();
|
||||||
|
|
||||||
// Backfill label references
|
// Backfill label references
|
||||||
for (let k of Object.keys(labels)) {
|
for (let k of Object.keys(labels)) {
|
||||||
dbgGroup(1, `${ASM_LABEL_PREFIX}${k}`);
|
dbg.nitGroup(`${ASM_LABEL_PREFIX}${k}`);
|
||||||
let label = labels[k];
|
let label = labels[k];
|
||||||
dbg(1, `Points to byte: ${label.pointsToByte}`);
|
dbg.nit(`Points to byte: ${label.pointsToByte}`);
|
||||||
dbg(1, `Bytes to replace: ${label.bytesToReplace}`);
|
dbg.nit(`Bytes to replace: ${label.bytesToReplace}`);
|
||||||
dbgGroupEnd(1);
|
dbg.nitGroupEnd();
|
||||||
for (let j = 0; j < label.bytesToReplace.length; j++) {
|
for (let j = 0; j < label.bytesToReplace.length; j++) {
|
||||||
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
|
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { 'debugInfo': debugInfo, 'machineCode': new Uint8Array(machineCode) };
|
return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -413,10 +403,57 @@ function stripWhitespaceFromEnds(line) {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hex2num(hex) { return parseInt(hex, 16) };
|
/**
|
||||||
|
* Assemble source code into machine code.
|
||||||
|
* If 'includeMetadata' is true, a JSON object containing
|
||||||
|
* both machine code and metadata is written to the output file.
|
||||||
|
* Otherwise, a string of decimal numbers is written.
|
||||||
|
* @arg {string} inputFilename File containing code to assemble
|
||||||
|
* @arg {boolean} outputToFile If false, output is on stdout
|
||||||
|
* @arg {boolean} includeMetadata Include metadata when writing output to a file? (for use when debugging using the simulator)
|
||||||
|
* @arg {string} [outputFilename] Output file for machine code (and optional metadata)
|
||||||
|
**/
|
||||||
|
function assemble(inputFilename, outputToFile, includeMetadata, outputFilename=null) {
|
||||||
|
const sourceCode = fs.readFileSync(inputFilename, 'utf8');
|
||||||
|
const out = decodeInstructions(sourceCode);
|
||||||
|
|
||||||
// Debug helpers
|
if (includeMetadata) {
|
||||||
const dbg = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.log(s) };
|
const debugJSON = JSON.stringify(out);
|
||||||
const dbgGroup = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.group(s) };
|
if (outputToFile) {
|
||||||
const dbgGroupEnd = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.groupEnd() };
|
fs.writeFileSync(outputFilename, debugJSON);
|
||||||
const dbgExec = (lvl, func) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) func(); }
|
} else {
|
||||||
|
console.log(debugJSON);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' ');
|
||||||
|
if (outputToFile) {
|
||||||
|
fs.writeFileSync(outputFilename, asciiMachineCode);
|
||||||
|
} else {
|
||||||
|
console.log(asciiMachineCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MAIN **/
|
||||||
|
|
||||||
|
// Initialize debugger...
|
||||||
|
const dbg = new DBG('nitpick');
|
||||||
|
|
||||||
|
// Handle command-line options...
|
||||||
|
const opter = new Opter();
|
||||||
|
opter.addOption('-a', '--annotate');
|
||||||
|
opter.addOption('-i', '--in', true, true, 1);
|
||||||
|
opter.addOption('-o', '--out', false, true, 1);
|
||||||
|
let opts = opter.parse(process.argv);
|
||||||
|
|
||||||
|
const inputFilename = opts.in[0];
|
||||||
|
let outputWithAnnotations = 'annotate' in opts;
|
||||||
|
|
||||||
|
// Assemble...!
|
||||||
|
if ('out' in opts) {
|
||||||
|
const outputFilename = opts.out[0];
|
||||||
|
assemble(inputFilename, true, outputWithAnnotations, outputFilename);
|
||||||
|
} else {
|
||||||
|
dbg.setLevel('none');
|
||||||
|
assemble(inputFilename, false, outputWithAnnotations);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const DBG = require('./dbg.js');
|
||||||
|
const Opter = require('./opter/opter.js');
|
||||||
|
const { num2hex, bool2bit } = require('./conversions.js');
|
||||||
|
|
||||||
|
const CFG = require('./machine.config.js');
|
||||||
|
const CPU = require('./cpu.js');
|
||||||
|
const io = require('./io.js');
|
||||||
|
|
||||||
|
|
||||||
|
/** SETUP **/
|
||||||
|
const dbg = new DBG('nitpick');
|
||||||
|
let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit);
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const opter = new Opter();
|
||||||
|
opter.addOption('-i', '--in', false, true, 1);
|
||||||
|
const opts = opter.parse(process.argv);
|
||||||
|
|
||||||
|
let input = null;
|
||||||
|
if ('in' in opts) { // Read from file
|
||||||
|
input = fs.readFileSync(opts.in[0], 'utf8');
|
||||||
|
} else { // Read from stdin
|
||||||
|
input = await readPipedStdin();
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = null;
|
||||||
|
let sourceAnnotations = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedInput = JSON.parse(input);
|
||||||
|
sourceAnnotations = parsedInput.sourceAnnotations;
|
||||||
|
code = new Uint8Array(parsedInput.machineCode);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SyntaxError') {
|
||||||
|
code = new Uint8Array(input.split(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.loadMemory(code);
|
||||||
|
if (sourceAnnotations !== null) { cpu.loadSourceAnnotations(sourceAnnotations); }
|
||||||
|
|
||||||
|
cpu.onCycleEnd(tick);
|
||||||
|
cpu.onCycleEnd(logCPUState);
|
||||||
|
|
||||||
|
cpu.start();
|
||||||
|
io.getKeypadInput(cpu);
|
||||||
|
cpu.step();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function tick() {
|
||||||
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
await sleep(100);
|
||||||
|
cpu.step();
|
||||||
|
if (!cpu.running) {
|
||||||
|
console.log('Halted');
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logCPUState() {
|
||||||
|
let lineInfo = null;
|
||||||
|
if (cpu.dbg.sourceAnnotations) {
|
||||||
|
lineInfo = cpu.dbg.sourceAnnotations[cpu.dbg.previousIP];
|
||||||
|
}
|
||||||
|
console.group(`Step ${cpu.dbg.cycleCounter}`);
|
||||||
|
console.log();
|
||||||
|
io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing
|
||||||
|
console.log();
|
||||||
|
if (lineInfo) {
|
||||||
|
console.log(`Line ${lineInfo.lineNumber}: ${lineInfo.source}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
console.log('Mnemonic:', cpu.dbg.currentMnemonic);
|
||||||
|
console.log(`Machine: $${num2hex(cpu.instruction.opcode)} $${num2hex(cpu.instruction.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: ${io.readKeyMem(cpu.memory)} ${cpu.running ? "running" : "halted" }`);
|
||||||
|
console.log();
|
||||||
|
console.groupEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function readPipedStdin() {
|
||||||
|
// https://wellingguzman.com/notes/node-pipe-input
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const stdin = process.stdin;
|
||||||
|
stdin.setEncoding('utf8');
|
||||||
|
let data = '';
|
||||||
|
stdin.on('data', function (chunk) { data += chunk; });
|
||||||
|
stdin.on('end', function () { resolve(data); });
|
||||||
|
stdin.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* @param {number} num
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const hex2num = (hex) => parseInt(hex, 16);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a number to binary, padded to 8 bits
|
||||||
|
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
|
||||||
|
* @param {number} num
|
||||||
|
* @returns {string} binary representation of the input
|
||||||
|
**/
|
||||||
|
const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a number to binary, padded to 4 bits
|
||||||
|
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
|
||||||
|
* @param {number} num
|
||||||
|
* @returns {string} binary representation of the input
|
||||||
|
**/
|
||||||
|
const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} bin
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const bin2num = (bin) => parseInt(bin, 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Boolean} bool
|
||||||
|
* @returns {0|1}
|
||||||
|
**/
|
||||||
|
const bool2bit = (bool) => bool ? 1 : 0;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"num2hex": num2hex,
|
||||||
|
"hex2num": hex2num,
|
||||||
|
"num2bin": num2bin,
|
||||||
|
"num2bin_4bit": num2bin_4bit,
|
||||||
|
"bin2num": bin2num,
|
||||||
|
"bool2bit": bool2bit,
|
||||||
|
}
|
||||||
712
src/cpu.js
712
src/cpu.js
|
|
@ -1,422 +1,320 @@
|
||||||
const readline = require('readline');
|
const { num2hex } = require('./conversions.js');
|
||||||
const readlineSync = require('readline-sync');
|
|
||||||
|
|
||||||
const {
|
module.exports = class CPU {
|
||||||
INITIAL_IP_ADDRESS,
|
|
||||||
DEFAULT_CYCLE_LIMIT,
|
|
||||||
KEYPAD_ADDR,
|
|
||||||
KEY_MAP,
|
|
||||||
} = require('./machine.config');
|
|
||||||
|
|
||||||
const {
|
/**
|
||||||
num2hex,
|
* @arg {number} initialIP
|
||||||
bool2bit,
|
**/
|
||||||
} = require('./logging.js');
|
constructor(initialIP, cycleLimit) {
|
||||||
const display = require('./display.js');
|
this.running = false;
|
||||||
|
this.IP = initialIP;
|
||||||
|
this.acc = 0;
|
||||||
|
this.flags = {'C': false, 'Z': false, 'N': false, 'O': false};
|
||||||
|
this.flagNums = {0: 'C', 1: 'Z', 2: 'N', 3: 'O'};
|
||||||
|
this.instruction = { opcode: null, operand: null };
|
||||||
|
this.memory = null;
|
||||||
|
|
||||||
// STATE
|
this._cycleLimit = cycleLimit;
|
||||||
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
|
this.dbg = {
|
||||||
/** @param {Uint8Array} data */
|
sourceInfo: null,
|
||||||
loadMemory: (data) => {
|
currentMnemonic: null,
|
||||||
CPU.memory = new Uint8Array(256);
|
previousIP: initialIP,
|
||||||
CPU.memory.set(data, 0);
|
cycleCounter: 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();
|
|
||||||
let name = key.name.toUpperCase();
|
|
||||||
if (name in KEY_MAP) {
|
|
||||||
CPU.memory[KEYPAD_ADDR] = KEY_MAP[name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute just the next instruction in memory
|
/** Public interface **/
|
||||||
* @param {Object} debugInfo
|
|
||||||
* @param {Boolean} [debug] - Print machine status and the line of code being executed
|
/**
|
||||||
**/
|
* @param {Uint8Array} machineCode
|
||||||
async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) {
|
**/
|
||||||
if (CPU.IP >= CPU.memory.length) {
|
loadMemory(machineCode) {
|
||||||
console.error('HALTING - IP greater than memory size');
|
this.memory = new Uint8Array(256);
|
||||||
CPU.running = false;
|
this.memory.set(machineCode, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
peek() { return; } // TODO - implement Peek
|
||||||
|
|
||||||
|
poke() { return; } // TODO - implement Poke
|
||||||
|
|
||||||
|
/** @param {Array} info **/ // TODO - document type for 'sourceInfo'
|
||||||
|
loadSourceAnnotations(info) {
|
||||||
|
this.dbg.sourceInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set CPU state to "state.running" **/
|
||||||
|
start() {
|
||||||
|
this.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute the next instruction in memory **/
|
||||||
|
step() {
|
||||||
|
this._cycleStartCallbacks.forEach((fn) => fn());
|
||||||
|
|
||||||
|
if (this.IP >= this.memory.length) {
|
||||||
|
this.running = false;
|
||||||
|
throw new Error('HALTING - IP greater than memory size');
|
||||||
|
} else {
|
||||||
|
this.instruction.opcode = this.memory[this.IP];
|
||||||
|
this.instruction.operand = this.memory[this.IP+1];
|
||||||
|
let mnem = this._nums2mnems[this.instruction.opcode];
|
||||||
|
let op = this._ops[mnem];
|
||||||
|
if (typeof op === 'undefined') { this._failInvalidOpcode(); }
|
||||||
|
op(this.instruction.operand);
|
||||||
|
this.dbg.cycleCounter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary limit as a lazy way to halt infinite loops
|
||||||
|
if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) {
|
||||||
|
this.running = false;
|
||||||
|
throw new Error(' HALTING - reached cycle limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cycleEndCallbacks.forEach((fn) => fn());
|
||||||
|
if (!this.running) process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Private methods **/
|
||||||
|
|
||||||
|
_incrementIP(offset) {
|
||||||
|
this.dbg.previousIP = this.IP;
|
||||||
|
this.IP = this.IP + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setIP(address) {
|
||||||
|
this.dbg.previousIP = this.IP;
|
||||||
|
this.IP = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFlagZero() {
|
||||||
|
this.flags.Z = this.acc === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateFlagNegative() {
|
||||||
|
if (this.acc & 128)
|
||||||
|
{ this.flags.N = true; }
|
||||||
|
else
|
||||||
|
{ this.flags.N = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Hooks **/
|
||||||
|
|
||||||
|
/** @type Array<Function> **/
|
||||||
|
_cycleStartCallbacks = [];
|
||||||
|
|
||||||
|
/** @type Array<Function> **/
|
||||||
|
_cycleEndCallbacks = [];
|
||||||
|
|
||||||
|
/** @param {function} fn **/
|
||||||
|
onCycleStart(fn) { this._cycleStartCallbacks.push(fn) };
|
||||||
|
|
||||||
|
/** @param {function} fn **/
|
||||||
|
onCycleEnd(fn) { this._cycleEndCallbacks.push(fn) };
|
||||||
|
|
||||||
|
_ops = {
|
||||||
|
end: () => {
|
||||||
|
this.dbg.currentMnemonic = 'END';
|
||||||
|
this.running = false;
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
store_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = 'STO lit';
|
||||||
|
this.memory[lit] = this.acc;
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
store_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`;
|
||||||
|
this.memory[this.memory[addr]] = this.acc;
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
load_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = 'LDA lit';
|
||||||
|
this.acc = lit;
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
load_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`;
|
||||||
|
this.acc = this.memory[addr];
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
add_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = 'ADD lit';
|
||||||
|
const [sum, carry, overflow] = sumCarryOverflow(this.acc, lit);
|
||||||
|
this.acc = sum;
|
||||||
|
this.flags.C = carry;
|
||||||
|
this.flags.O = overflow;
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
add_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = `ADD addr; @ addr: $${num2hex(this.memory[addr])}`;
|
||||||
|
const [sum, carry, overflow] = sumCarryOverflow(this.acc, this.memory[addr]);
|
||||||
|
this.acc = sum;
|
||||||
|
this.flags.C = carry;
|
||||||
|
this.flags.O = overflow;
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
sub_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = 'SUB lit';
|
||||||
|
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, lit);
|
||||||
|
this.acc = difference;
|
||||||
|
this.flags.C = carry;
|
||||||
|
this.flags.O = overflow;
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
sub_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = `SUB addr; @ addr: $${num2hex(this.memory[addr])}`;
|
||||||
|
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, this.memory[addr]);
|
||||||
|
this.acc = difference;
|
||||||
|
this.flags.C = carry;
|
||||||
|
this.flags.O = overflow;
|
||||||
|
this._updateFlagNegative();
|
||||||
|
this._updateFlagZero();
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
hop_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = `HOP lit; IP+2: $${this.memory[this.IP+2]}, IP+3: $${this.memory[this.IP+3]}`;
|
||||||
|
if (this.acc === lit) {
|
||||||
|
this._incrementIP(4);
|
||||||
|
} else {
|
||||||
|
this._incrementIP(2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hop_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = 'HOP addr';
|
||||||
|
if (this.acc === this.memory[addr]) {
|
||||||
|
this._incrementIP(4);
|
||||||
|
} else {
|
||||||
|
this._incrementIP(2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
jump_lit: (lit) => {
|
||||||
|
this.dbg.currentMnemonic = 'JMP lit';
|
||||||
|
this._setIP(lit);
|
||||||
|
},
|
||||||
|
|
||||||
|
jump_addr: (addr) => {
|
||||||
|
this.dbg.currentMnemonic = 'JMP addr';
|
||||||
|
this._setIP(this.memory[addr]);
|
||||||
|
},
|
||||||
|
|
||||||
|
flag_toggle: (flagNum) => {
|
||||||
|
if (flagNum === null) {
|
||||||
|
let info = this.dbg.sourceInfo[this.IP];
|
||||||
|
throw new Error(`Invalid flag number: '${flagNum}' on line ${info.lineNumber}: ${info.source}`);
|
||||||
|
}
|
||||||
|
const flagName = this.flagNums[flagNum];
|
||||||
|
this.dbg.currentMnemonic = `FTG ${flagName}`;
|
||||||
|
this.flags[flagName] = !this.flags[flagName];
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
flag_hop: (flagNum) => {
|
||||||
|
if (flagNum === null) {
|
||||||
|
console.error('Invalid flag number');
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
const flagName = this.flagNums[flagNum];
|
||||||
|
this.dbg.currentMnemonic =
|
||||||
|
`FHP ${flagName}; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`;
|
||||||
|
if (this.flags[this.flagNums[flagNum]]) {
|
||||||
|
this._incrementIP(4);
|
||||||
|
} else {
|
||||||
|
this._incrementIP(2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
no_op: () => {
|
||||||
|
this.dbg.currentMnemonic = `NOP`;
|
||||||
|
this._incrementIP(2);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_nums2mnems = {
|
||||||
|
0: "end",
|
||||||
|
1: "store_lit",
|
||||||
|
2: "store_addr",
|
||||||
|
3: "load_lit",
|
||||||
|
4: "load_addr",
|
||||||
|
5: "add_lit",
|
||||||
|
6: "add_addr",
|
||||||
|
7: "sub_lit",
|
||||||
|
8: "sub_addr",
|
||||||
|
9: "hop_lit",
|
||||||
|
10: "hop_addr",
|
||||||
|
11: "jump_lit",
|
||||||
|
12: "jump_addr",
|
||||||
|
13: "flag_toggle",
|
||||||
|
14: "flag_hop",
|
||||||
|
15: "no_op",
|
||||||
|
}
|
||||||
|
|
||||||
|
_failInvalidOpcode() {
|
||||||
|
let info = this.dbg.sourceInfo[this.dbg.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();
|
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 (DEFAULT_CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops
|
|
||||||
if (CPU.cycleCounter >= DEFAULT_CYCLE_LIMIT) {
|
|
||||||
console.warn(' HALTING - reached cycle limit');
|
|
||||||
CPU.running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @arg {number} n1
|
||||||
|
* @arg {number} n2
|
||||||
|
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
|
||||||
|
**/
|
||||||
|
function sumCarryOverflow(n1, n2) {
|
||||||
|
let sum = n1 + n2;
|
||||||
|
let carry = false;
|
||||||
|
if (sum > 255) {
|
||||||
|
carry = true;
|
||||||
|
sum = (sum % 255) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
|
let n1_bit6 = (n1 & 64) === 64; // Bit 6 is the 64s place
|
||||||
|
let n2_bit6 = (n2 & 64) === 64; // 64 & n == 64 where n >= 64
|
||||||
|
let carryIntoLastBit = n1_bit6 && n2_bit6;
|
||||||
|
console.log('c_in', carryIntoLastBit, 'c_out', carry);
|
||||||
|
let overflow = carryIntoLastBit != carry;
|
||||||
|
|
||||||
|
return [sum, carry, overflow];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
* @arg {number} n1
|
||||||
|
* @arg {number} n2
|
||||||
|
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
|
||||||
**/
|
**/
|
||||||
function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) {
|
function differenceCarryOverflow(n1, n2) {
|
||||||
debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false;
|
// https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
|
||||||
console.group(`Step ${CPU.cycleCounter}`);
|
// > SBC simply takes the ones complement of the second value and then performs an ADC.
|
||||||
console.log();
|
//
|
||||||
if (!debug) console.clear();
|
// https://stackoverflow.com/a/8966863
|
||||||
display.show(CPU.memory, prettyPrintDisplay);
|
// > The signed overflow flag value, however, must be the same for both A-B and A+(-B) because it depends on whether or not the result has the correct sign bit and in both cases the sign bit will be the same.
|
||||||
console.log();
|
return sumCarryOverflow(n1, -n2);
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
module.exports = class DBG {
|
||||||
|
/**
|
||||||
|
* @param ${'none'|'warn'|'info'|'debug'|'nitpick'} [level='info']
|
||||||
|
**/
|
||||||
|
constructor(level = 'info') {
|
||||||
|
this.setLevel(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
_levels = ['nitpick', 'debug', 'info', 'warn', 'none'];
|
||||||
|
|
||||||
|
setLevel(level) {
|
||||||
|
if (this._levels.includes(level)) {
|
||||||
|
this._level = level;
|
||||||
|
} else {
|
||||||
|
throw new Error(`'${level}' is not a valid debug level`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {any} s **/
|
||||||
|
warn = (s='', ...z) => {
|
||||||
|
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
|
||||||
|
console.log(s, ...z);
|
||||||
|
}
|
||||||
|
/** @param {any} s **/
|
||||||
|
i = (s='', ...z) => {
|
||||||
|
if (this._lvl2num('info') < this._lvl2num(this._level)) return
|
||||||
|
console.log(s, ...z);
|
||||||
|
}
|
||||||
|
/** @param {any} s **/
|
||||||
|
d = (s='', ...z) => {
|
||||||
|
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
|
||||||
|
console.log(s, ...z);
|
||||||
|
}
|
||||||
|
/** @param {any} s **/
|
||||||
|
nit = (s='', ...z) => {
|
||||||
|
if (this._lvl2num('nitpick') < this._lvl2num(this._level)) return
|
||||||
|
console.log(s, ...z);
|
||||||
|
}
|
||||||
|
|
||||||
|
warnGroup = (s) => {
|
||||||
|
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
|
||||||
|
console.group(s);
|
||||||
|
}
|
||||||
|
infoGroup = (s) => {
|
||||||
|
if (this._lvl2num('info') < this._lvl2num(this._level)) return
|
||||||
|
console.group(s);
|
||||||
|
}
|
||||||
|
debugGroup = (s) => {
|
||||||
|
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
|
||||||
|
console.group(s);
|
||||||
|
}
|
||||||
|
nitGroup = (s) => {
|
||||||
|
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
|
||||||
|
console.group(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
warnGroupEnd = (s) => {
|
||||||
|
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
infoGroupEnd = (s) => {
|
||||||
|
if (this._lvl2num('info') < this._lvl2num(this._level)) return
|
||||||
|
console.group();
|
||||||
|
}
|
||||||
|
debugGroupEnd = (s) => {
|
||||||
|
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
|
||||||
|
console.group();
|
||||||
|
}
|
||||||
|
nitGroupEnd = (s) => {
|
||||||
|
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
|
||||||
|
console.group();
|
||||||
|
}
|
||||||
|
|
||||||
|
warnExec = (fn) => {
|
||||||
|
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
infoExec = (fn) => {
|
||||||
|
if (this._lvl2num('info') < this._lvl2num(this._level)) return
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
debugExec = (fn) => {
|
||||||
|
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
nitExec = (fn) => {
|
||||||
|
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
|
||||||
|
_lvl2num(lvl) {
|
||||||
|
return 1 + this._levels.findIndex(l => l === lvl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TEST
|
||||||
|
const dbg = new DBG('nitpick');
|
||||||
|
dbg.warnGroup('w');
|
||||||
|
dbg.warn('warn');
|
||||||
|
dbg.warnGroupEnd();
|
||||||
|
|
||||||
|
dbg.iGroup('i');
|
||||||
|
dbg.i('info');
|
||||||
|
dbg.iGroupEnd();
|
||||||
|
|
||||||
|
dbg.dGroup('d');
|
||||||
|
dbg.d('debug');
|
||||||
|
dbg.dGroupEnd();
|
||||||
|
|
||||||
|
dbg.nitGroup('n');
|
||||||
|
dbg.nit('nitpick');
|
||||||
|
dbg.nitGroupEnd();
|
||||||
|
*/
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
const { POINTER_TO_DISPLAY } = require('./machine.config');
|
|
||||||
const { num2hex } = require('./logging.js');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print the contents of display memory
|
|
||||||
* by default, each pixel is shown as a hex number
|
|
||||||
* @param {Uint8Array} mem - CPU memory
|
|
||||||
* @param {Boolean} pretty - Display pixels using black and white emoji circles
|
|
||||||
**/
|
|
||||||
const printDisplay = (mem, pretty=false) => {
|
|
||||||
const disp = mem[POINTER_TO_DISPLAY];
|
|
||||||
const num2pic = (n) => n > 0 ? '⚫' : '⚪';
|
|
||||||
let fmt = (n) => num2hex(n);
|
|
||||||
if (pretty) fmt = (n) => num2pic(n);
|
|
||||||
for (let i = disp; i < disp + 25; i += 5) {
|
|
||||||
console.log(`${fmt(mem[i])} ${fmt(mem[i+1])} ${fmt(mem[i+2])} ${fmt(mem[i+3])} ${fmt(mem[i+4])}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
"show": printDisplay,
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
const CFG = require('./machine.config.js');
|
||||||
|
const { num2hex } = require('./logging.js');
|
||||||
|
|
||||||
|
function readKeyMem(mem) {
|
||||||
|
return mem[CFG.keypadAddr];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeypadInput(cpu) {
|
||||||
|
readline.emitKeypressEvents(process.stdin);
|
||||||
|
if (process.stdin.setRawMode != null) {
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
}
|
||||||
|
process.stdin.on('keypress', (str, key) => {
|
||||||
|
if (key.sequence === '\x03') process.exit();
|
||||||
|
let name = key.name.toUpperCase();
|
||||||
|
if (name in CFG.keyMap) {
|
||||||
|
cpu.memory[CFG.keypadAddr] = CFG.keyMap[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the contents of display memory
|
||||||
|
* by default, each pixel is shown as a hex number
|
||||||
|
* @param {Uint8Array} mem - CPU memory
|
||||||
|
* @param {Boolean} pretty - Display pixels using black and white emoji circles
|
||||||
|
**/
|
||||||
|
function showDisplay(mem, pretty=false) {
|
||||||
|
const disp = mem[CFG.pointerToDisplay];
|
||||||
|
const num2pic = (n) => n > 0 ? '⚫' : '⚪';
|
||||||
|
let fmt = (n) => num2hex(n);
|
||||||
|
if (pretty) fmt = (n) => num2pic(n);
|
||||||
|
for (let i = disp; i < disp + 25; i += 5) {
|
||||||
|
console.log(`${fmt(mem[i])} ${fmt(mem[i+1])} ${fmt(mem[i+2])} ${fmt(mem[i+3])} ${fmt(mem[i+4])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"showDisplay": showDisplay,
|
||||||
|
"getKeypadInput": getKeypadInput,
|
||||||
|
"readKeyMem": readKeyMem,
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
let { num2hex } = require('./conversions.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a table of memory locations.
|
* Display a table of memory locations.
|
||||||
* Call with [start] and [end] indices to display a range.
|
* Call with [start] and [end] indices to display a range.
|
||||||
|
|
@ -15,7 +17,7 @@ const logMemory = (mem, start=0, end=mem.length) => {
|
||||||
for (let i = start; i < mem.length; i +=2) {
|
for (let i = start; i < mem.length; i +=2) {
|
||||||
let operand = mem[i+1];
|
let operand = mem[i+1];
|
||||||
if (typeof operand === 'undefined') {
|
if (typeof operand === 'undefined') {
|
||||||
console.log(` ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ │`);
|
console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ │`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ ${num2hex(operand)} │`);
|
console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ ${num2hex(operand)} │`);
|
||||||
}
|
}
|
||||||
|
|
@ -38,54 +40,7 @@ const logRunningHeader = () => {
|
||||||
console.log( `└─────────────────────┘`);
|
console.log( `└─────────────────────┘`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} num
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} hex
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
const hex2num = (hex) => parseInt(hex, 16);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a number to binary, padded to 8 bits
|
|
||||||
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
|
|
||||||
* @param {number} num
|
|
||||||
* @returns {string} binary representation of the input
|
|
||||||
**/
|
|
||||||
const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a number to binary, padded to 4 bits
|
|
||||||
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
|
|
||||||
* @param {number} num
|
|
||||||
* @returns {string} binary representation of the input
|
|
||||||
**/
|
|
||||||
const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} bin
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
const bin2num = (bin) => parseInt(bin, 2)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Boolean} bool
|
|
||||||
* @returns {0|1}
|
|
||||||
**/
|
|
||||||
const bool2bit = (bool) => bool ? 1 : 0;
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"logMemory": logMemory,
|
"logMemory": logMemory,
|
||||||
"logRunningHeader": logRunningHeader,
|
"logRunningHeader": logRunningHeader,
|
||||||
"num2hex": num2hex,
|
|
||||||
"hex2num": hex2num,
|
|
||||||
"num2bin": num2bin,
|
|
||||||
"num2bin_4bit": num2bin_4bit,
|
|
||||||
"bin2num": bin2num,
|
|
||||||
"bool2bit": bool2bit,
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"INITIAL_IP_ADDRESS": 29,
|
"initialIP": 29,
|
||||||
|
|
||||||
// Use these in CPU:
|
// Use these in CPU:
|
||||||
"DISPLAY_ADDR": 0,
|
"displayAddr": 0,
|
||||||
"KEYPAD_ADDR": 27,
|
"keypadAddr": 27,
|
||||||
// Store the `DISPLAY_ADDR` at this address when assembling:
|
// Store the `DISPLAY_ADDR` at this address when assembling:
|
||||||
"POINTER_TO_DISPLAY": 26,
|
"pointerToDisplay": 26,
|
||||||
|
|
||||||
"KEY_MAP": {
|
"keyMap": {
|
||||||
// Same layout as COSMAC VIP / CHIP-8
|
// Same layout as COSMAC VIP / CHIP-8
|
||||||
// (This object maps qwerty keys to hex keys
|
// (This object maps qwerty keys to hex keys
|
||||||
// so that they are arranged in the same layout
|
// so that they are arranged in the same layout
|
||||||
|
|
@ -27,5 +27,5 @@ module.exports = {
|
||||||
// max number of times to step the CPU,
|
// max number of times to step the CPU,
|
||||||
// to stop endless loops
|
// to stop endless loops
|
||||||
// 0 = infinite
|
// 0 = infinite
|
||||||
"DEFAULT_CYCLE_LIMIT": 2048,
|
"defaultCycleLimit": 2048,
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1d98a0707c3e61e362d2d3d5413b475437b5de0e
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "paper-computer",
|
"name": "cardiograph",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"jsdoc": "./node_modules/.bin/jsdoc"
|
"jsdoc": "./node_modules/.bin/jsdoc"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Run with hex output: `./run-assembler.js run assembly.asm`
|
|
||||||
// Run with binary output: `./run-assembler.js runbin assembly.asm`
|
|
||||||
// Debug: `./run-assembler.js debug assembly.asm`
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const assembler = require('./assembler.js');
|
|
||||||
const { logMemory, num2hex, num2bin } = require('./logging.js');
|
|
||||||
const machineConfig = require('./machine.config.js');
|
|
||||||
|
|
||||||
const mode = process.argv[2];
|
|
||||||
const filename = process.argv[3];
|
|
||||||
const inputFile_str = fs.readFileSync(filename, 'utf8');
|
|
||||||
|
|
||||||
let assembler_output;
|
|
||||||
|
|
||||||
if (mode === "debug") {
|
|
||||||
assembler_output = assembler.assemble(inputFile_str, true);
|
|
||||||
console.log('');
|
|
||||||
console.group("Machine code output");
|
|
||||||
logMemory(assembler_output.machineCode, machineConfig.INITIAL_IP_ADDRESS);
|
|
||||||
console.groupEnd();
|
|
||||||
} else {
|
|
||||||
assembler_output = assembler.assemble(inputFile_str);
|
|
||||||
let output = '';
|
|
||||||
if (mode === 'runbin') { // print binary output
|
|
||||||
assembler_output.machineCode.forEach((n) => output = `${output} ${num2bin(n)}`);
|
|
||||||
} else { // print hex output
|
|
||||||
assembler_output.machineCode.forEach((n) => output = `${output} ${num2hex(n)}`);
|
|
||||||
}
|
|
||||||
console.log(output);
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Usage: ./run-cpu.js -f code.asm [--debug] [--step] [--pretty]
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const computer = require('./cpu.js');
|
|
||||||
const assembler = require('./assembler.js');
|
|
||||||
const { logRunningHeader } = require('./logging.js');
|
|
||||||
|
|
||||||
// Load file...
|
|
||||||
|
|
||||||
let filename;
|
|
||||||
try {
|
|
||||||
filename = getArgumentValue('-f', `Missing filename`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputFile_str = fs.readFileSync(filename, 'utf8');
|
|
||||||
|
|
||||||
|
|
||||||
// Check optional arguments...
|
|
||||||
|
|
||||||
let debug = false;
|
|
||||||
let singleStep = false;
|
|
||||||
let prettyPrint = false;
|
|
||||||
process.argv.forEach((arg) => { if (arg === '--debug') { debug = true } });
|
|
||||||
process.argv.forEach((arg) => { if (arg === '--step') { singleStep = true } });
|
|
||||||
process.argv.forEach((arg) => { if (arg === '--pretty') { prettyPrint = true } });
|
|
||||||
|
|
||||||
let speed = null;
|
|
||||||
process.argv.forEach((arg, index) => {
|
|
||||||
if (arg === '--speed' && process.argv.length > (index -1)) {
|
|
||||||
speed = parseInt(process.argv[index + 1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let assemblerOutput = assembler.assemble(inputFile_str);
|
|
||||||
logRunningHeader();
|
|
||||||
computer.runProgram(
|
|
||||||
assemblerOutput.machineCode,
|
|
||||||
assemblerOutput.debugInfo,
|
|
||||||
debug,
|
|
||||||
singleStep,
|
|
||||||
prettyPrint,
|
|
||||||
speed
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// CLI args TODO
|
|
||||||
// - check if value is the name of another arg
|
|
||||||
// - usage info
|
|
||||||
// - catch nonexistant flags
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} flag - The command line flag, eg. '-f'
|
|
||||||
* @param {string} errorMessage - The error to throw if a value isn't found
|
|
||||||
* @returns {string}
|
|
||||||
**/
|
|
||||||
function getArgumentValue(flag, errorMessage) {
|
|
||||||
let value = null;
|
|
||||||
process.argv.forEach((arg, index) => {
|
|
||||||
if (arg === flag && process.argv.length > (index -1)) {
|
|
||||||
value = process.argv[index + 1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!value) throw new Error(errorMessage);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
;; Test behaviour of flags during addition and subtraction
|
||||||
|
;; with a focus on the Overflow flag
|
||||||
|
;; 2023-08-29
|
||||||
|
|
||||||
|
; http://teaching.idallen.com/dat2343/11w/notes/040_overflow.txt:
|
||||||
|
;
|
||||||
|
; > 1. If the sum of two numbers with the sign bits off yields a result number
|
||||||
|
; with the sign bit on, the "overflow" flag is turned on.
|
||||||
|
; >
|
||||||
|
; > 0100 + 0100 = 1000 (overflow flag is turned on)
|
||||||
|
; >
|
||||||
|
; > 2. If the sum of two numbers with the sign bits on yields a result number
|
||||||
|
; > with the sign bit off, the "overflow" flag is turned on.
|
||||||
|
; >
|
||||||
|
; > 1000 + 1000 = 0000 (overflow flag is turned on)
|
||||||
|
; >
|
||||||
|
; > Otherwise, the overflow flag is turned off.
|
||||||
|
; > * 0100 + 0001 = 0101 (overflow flag is turned off)
|
||||||
|
; > * 0110 + 1001 = 1111 (overflow flag is turned off)
|
||||||
|
; > * 1000 + 0001 = 1001 (overflow flag is turned off)
|
||||||
|
; > * 1100 + 1100 = 1000 (overflow flag is turned off)
|
||||||
|
|
||||||
|
;; Check simple addition and subtraction
|
||||||
|
; LDA 1
|
||||||
|
; STO 0
|
||||||
|
; LDA 1
|
||||||
|
; ADD 1
|
||||||
|
; LDA 1
|
||||||
|
; ADD (0)
|
||||||
|
; LDA 3
|
||||||
|
; SUB 1
|
||||||
|
; LDA 3
|
||||||
|
; SUB (0)
|
||||||
|
|
||||||
|
;; Check zero flag, negative flag
|
||||||
|
; LDA 0
|
||||||
|
; LDA 255
|
||||||
|
|
||||||
|
;; Check overflow flag
|
||||||
|
|
||||||
|
LDA 0b01000000
|
||||||
|
ADD 0b01000000 ; 10000000 ; Overflow flag is on
|
||||||
|
|
||||||
|
LDA 0b10000000
|
||||||
|
ADD 0b10000000 ; 00000000 ; Overflow flag is on
|
||||||
|
|
||||||
|
|
||||||
|
; > * 0100 + 0001 = 0101 (overflow flag is turned off)
|
||||||
|
; > * 0110 + 1001 = 1111 (overflow flag is turned off)
|
||||||
|
; > * 1000 + 0001 = 1001 (overflow flag is turned off)
|
||||||
|
; > * 1100 + 1100 = 1000 (overflow flag is turned off)
|
||||||
|
|
||||||
|
LDA 0b01000000
|
||||||
|
ADD 0b00010000 ; 01010000 ; overflow off
|
||||||
|
|
||||||
|
LDA 0b01100000
|
||||||
|
ADD 0b10010000 ; 11110000 ; overflow off
|
||||||
|
|
||||||
|
LDA 0b10000000
|
||||||
|
ADD 0b00010000 ; 10010000 ; overflow off
|
||||||
|
|
||||||
|
LDA 0b11000000
|
||||||
|
ADD 0b11000000 ; 10000000 ; overflow off
|
||||||
Loading…
Reference in New Issue