Remove files that aren't notes
This commit is contained in:
parent
3cc41b8b5a
commit
3982ce3254
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "src/argparser"]
|
||||
path = src/argparser
|
||||
url = https://git.nloewen.com/n/argv-parser.git
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Cardiograph issues
|
||||
|
||||
## Open
|
||||
|
||||
### #1 - Improve CLI interface
|
||||
|
||||
I'm thinking of an interface like this...
|
||||
|
||||
$ ./cpu.js -mc code.bin
|
||||
$ ./cpu.js code.asm
|
||||
$ ./cpu.js --debug code.asm
|
||||
|
||||
Full list of flags I want:
|
||||
|
||||
-d --debug
|
||||
-s --singlestep
|
||||
-p --prettydisplay
|
||||
-mc --machinecode
|
||||
|
||||
### #2 - Startup: Execute `JMP $FF`
|
||||
|
||||
See [2023-08-24](../notes/2023-08-24--dev-notes.md#cpu-start-up)
|
||||
|
||||
... say that there's a 1-byte ROM at $FF.
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FE` - free
|
||||
- `FF ` - ROM (unwriteable) - pointer to initial IP
|
||||
|
||||
- store `$1D` at `$FF`
|
||||
- make CPU execute `JMP $FF` on startup
|
||||
- make ROM unwriteable
|
||||
|
||||
More step-by-step:
|
||||
|
||||
- Change memory from a Uint8Array to a regular array,
|
||||
and make every entry { number | { type: 'ROM', value: number }}
|
||||
- Store ROM as an object in machine.config.js
|
||||
- Load ROM data into memory at CPU startup (`startCPU(RAM, ROM)`)
|
||||
|
||||
|
||||
## Closed
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# To do — Summary
|
||||
|
||||
This is a quick todo list.
|
||||
|
||||
For extended commentary, see [issues](issues.md).
|
||||
|
||||
## Open
|
||||
|
||||
### Todo
|
||||
|
||||
- Finish WIP on run-cli arg parsing
|
||||
- Pass CYCLE_COUNT as a cli arg
|
||||
|
||||
- (cpu) !! Fix overflow flag
|
||||
- Add a flag for bank-switching to the ~zero-page
|
||||
- Remove run-scripts and add the ability to run `./cpu.js` and `./assembler.js` directly -- cf. [#1](issues.md#1---improve-cli-interface)
|
||||
- [fix] (cpu) Make single-stepping work with simulated keypad
|
||||
|
||||
### Features
|
||||
|
||||
- (cpu) allow arrow keys, too
|
||||
- [fix] (cpu) Execute `JMP $FF` on startup / Implement ROM — see [#2](issues.md#2---startup-execute-jmp-ff)
|
||||
- (assembler) Validate labels
|
||||
- (assembler) Extract debugging to its own module
|
||||
- (cpu) Consider adding a VIP-style keypad-based machine code monitor
|
||||
- (cpu) Add a mode that prints the display as text, but still animates
|
||||
- (cpu) Allow running pre-compiled machine code
|
||||
- (cpu) DRY out addition and subtraction
|
||||
- [Extended system (secret bonus operations)](../notes/2023-08-07--dev-notes.md)
|
||||
- (research) Review CHIP-8
|
||||
- read about the spec / ISA
|
||||
- read these, and add them to the bibliography:
|
||||
- Steve Losh: https://stevelosh.com/blog/2016/12/chip8-input/
|
||||
- https://tonisagrista.com/blog/2021/chip8-spec/
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve docs for flags register
|
||||
|
||||
### Testing
|
||||
|
||||
- Display (hex) numbers
|
||||
- Greater than
|
||||
- Minimal LOGO-ish interpreter for turtle graphics
|
||||
|
||||
|
||||
## Closed
|
||||
|
||||
- 2023-08-26 - [fix] (logging) - 'undefined operand' situation is caused by assembling to an initial IP of $1C, which is an odd number
|
||||
- (assembler) Pass asm line thru to cpu to print when debugging
|
||||
|
||||
|
||||
## Abandoned
|
||||
|
||||
- (assembler) Return pure machine code when printing to stdout (and not in debug mode)
|
||||
175
readme.md
175
readme.md
|
|
@ -1,175 +0,0 @@
|
|||
# Cardiograph Mark I — simulator for an imaginary computer
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Node.js
|
||||
- readline-sync
|
||||
|
||||
## Run
|
||||
|
||||
### Assemble
|
||||
|
||||
Hex output:
|
||||
```./run-assembler run source_code.asm```
|
||||
|
||||
Binary output:
|
||||
```./run-assembler runbin source_code.asm```
|
||||
|
||||
Verbose debugging output (hex):
|
||||
```./run-assembler debug source_code.asm```
|
||||
|
||||
### Assemble and run
|
||||
|
||||
With animated display of screen memory:
|
||||
```./run-cpu run source_code.asm```
|
||||
|
||||
With verbose debugging output:
|
||||
```./run-cpu debug source_code.asm```
|
||||
|
||||
With single stepping + pretty-printed display:
|
||||
```./run-cpu step source_code.asm```
|
||||
|
||||
With single stepping + verbose debugging output:
|
||||
```./run-cpu stepdebug source_code.asm```
|
||||
|
||||
|
||||
## Registers and Flags
|
||||
|
||||
- `A` - accumulator
|
||||
- `IP` - instruction pointer (aka program counter)
|
||||
- `FLAGS` - flags: **O**verflow, **N**egative, **Z**ero, **C**arry
|
||||
- in machine language, each flag is given a number:
|
||||
- O = 3
|
||||
N = 2
|
||||
Z = 1
|
||||
C = 0
|
||||
- (bitwise, `0000 = ONZC`)
|
||||
|
||||
|
||||
## Instruction set
|
||||
|
||||
### Operations
|
||||
|
||||
```
|
||||
Hex Mnem. Operand Effect
|
||||
|
||||
00 END (ignored) Halt CPU
|
||||
01 STO literal # mem[lit#] = A
|
||||
02 STO address mem[mem[addr]] = A
|
||||
03 LDA literal # A = lit#
|
||||
04 LDA address A = addr
|
||||
05 ADD literal # A = A + lit#
|
||||
06 ADD address A = A + mem[addr]
|
||||
07 SUB literal # A = A - lit#
|
||||
08 SUB address A = A - mem[addr]
|
||||
09 HOP literal # If A == lit#, skip next op (IP += 4)
|
||||
0A HOP address If A == mem[addr], skip next instruction (IP += 4)
|
||||
0B JMP literal # IP = lit#
|
||||
0C JMP address IP = mem[addr]
|
||||
0D FTG literal # Toggle flag, where flag number == lit#
|
||||
0E FHP literal # Skip next op if flag is set, where flag number == lit#
|
||||
0F NOP (ignored) None
|
||||
```
|
||||
|
||||
- Instructions are two bytes long:
|
||||
one byte for the opcode, one for the operand
|
||||
|
||||
|
||||
### Effects on memory, flags, registers
|
||||
|
||||
```
|
||||
op mem flags IP
|
||||
|
||||
END +2
|
||||
NOP +2
|
||||
|
||||
STO w +2
|
||||
LDA r NZ +2
|
||||
ADD ONZC +2
|
||||
SUB ONZC +2
|
||||
HOP +2/+4
|
||||
JMP arg
|
||||
FTG ONZC +2
|
||||
FHP ONZC +2/+4
|
||||
|
||||
STO r,w +2
|
||||
LDA r,r NZ +2
|
||||
ADD r ONZC +2
|
||||
SUB r ONZC +2
|
||||
HOP r +2/+4
|
||||
JMP r arg
|
||||
FTG r ONZC +2
|
||||
FHP r ONZC +2/+4
|
||||
```
|
||||
|
||||
|
||||
## CPU start-up
|
||||
|
||||
When starting up, the CPU executes a `JMP $FF`.
|
||||
|
||||
Put differently: it starts executing instructions at the address contained in `$FF`.
|
||||
|
||||
## Cardiograph memory map
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FE` - free
|
||||
- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented)
|
||||
|
||||
## Peripherals
|
||||
|
||||
### Keypad
|
||||
|
||||
The value of the latest keypress on a hex keypad is stored at `$1B`.
|
||||
|
||||
The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set:
|
||||
|
||||
```
|
||||
1 2 3 C 1 2 3 4
|
||||
4 5 6 D Q W E R
|
||||
7 8 9 E A S D F
|
||||
A 0 B F Z X C V
|
||||
|
||||
hex pad simulator
|
||||
```
|
||||
|
||||
The arrow keys are also mapped onto the hex keypad:
|
||||
|
||||
```
|
||||
5 ↑
|
||||
7 8 9 ← ↓ →
|
||||
|
||||
hex pad simulator
|
||||
```
|
||||
|
||||
## Assembly language
|
||||
|
||||
ADD $01 ; comments follow a `;`
|
||||
|
||||
ADD $FF ; this is direct addressing
|
||||
ADD ($CC) ; this is indirect addressing
|
||||
|
||||
END ; END and NOP don't require operands
|
||||
; (the assembler will fill in a default value of 0)
|
||||
|
||||
@subroutine ; create a label
|
||||
ADD $01 ; (it must be on the line before the code it names)
|
||||
ADD $02
|
||||
|
||||
JMP @subroutine ; use a label as operand
|
||||
; the label will be replaced with
|
||||
; the address of the label
|
||||
|
||||
#foo $FF ; define a constant
|
||||
; (must be defined before it is referenced)
|
||||
|
||||
ADD #foo ; use a constant as an operand
|
||||
|
||||
LDA * ; `*` is a special label referencing the memory address
|
||||
; where the current line will be stored after assembly
|
||||
|
||||
- Hexadecimal numbers are preceded by a `$`
|
||||
- Whitespace is ignored
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 584d9dd95f4b1b3c69065826cf96b3cda0cf9e16
|
||||
422
src/assembler.js
422
src/assembler.js
|
|
@ -1,422 +0,0 @@
|
|||
const { logMemory, num2hex } = require('./logging.js');
|
||||
const {
|
||||
INITIAL_IP_ADDRESS,
|
||||
DISPLAY_ADDR,
|
||||
POINTER_TO_DISPLAY,
|
||||
} = require('./machine.config.js');
|
||||
|
||||
// 1 = verbose
|
||||
// 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()
|
||||
|
||||
/**
|
||||
* @param {string} assemblyCode
|
||||
* @param {Boolean} [debug = false]
|
||||
**/
|
||||
exports.assemble = (assemblyCode, debug = false) => {
|
||||
DEBUG = debug;
|
||||
return decodeInstructions(assemblyCode);
|
||||
}
|
||||
|
||||
// Configure pseudo-ops:
|
||||
const ASM_IP_LABEL = '*';
|
||||
const ASM_CONSTANT_PREFIX = '#';
|
||||
const ASM_LABEL_PREFIX = '@';
|
||||
|
||||
const mnemonicsWithOptionalArgs = ['end', 'nop'];
|
||||
const mnemonics2opcodes = {
|
||||
end: { direct: 0, indirect: 0 },
|
||||
sto: { direct: 1, indirect: 2 },
|
||||
lda: { direct: 3, indirect: 4 },
|
||||
add: { direct: 5, indirect: 6 },
|
||||
sub: { direct: 7, indirect: 8 },
|
||||
hop: { direct: 9, indirect: 10 },
|
||||
jmp: { direct: 11, indirect: 12 },
|
||||
ftg: { direct: 13, indirect: 13 },
|
||||
fhp: { direct: 14, indirect: 14 },
|
||||
nop: { direct: 15, indirect: 15 },
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {('code'|'comment'|'blank')} SourceLineType
|
||||
**/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SourceLineInfo
|
||||
* @property {number} number - line number
|
||||
* @property {string} source - source text
|
||||
* @property {string} sanitized - source text, with comments and whitespace removed
|
||||
* @property {SourceLineType} type - line type
|
||||
* @property {string} [operation] - For code: the first non-whitespace chunk
|
||||
* @property {string} [argument] - For code: the second non-whitespace chunk, if there is one
|
||||
* @property {string} [extraArgument] - For code: the third non-whitespace chunk, if there is one
|
||||
**/
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @returns {Array<SourceLineInfo>}
|
||||
**/
|
||||
function preparseSourceCode(source) {
|
||||
let lines = source.split(/\n/); // returns an array of lines
|
||||
|
||||
const isLineBlank = (l) => { return stripWhitespaceFromEnds(l).length === 0 ? true : false };
|
||||
const isLineComment = (l) => { return stripWhitespaceFromEnds(l).startsWith(';') };
|
||||
|
||||
/**
|
||||
* @param {string} l
|
||||
* @returns {SourceLineType}
|
||||
**/
|
||||
const getLineType = (l) => {
|
||||
if (isLineBlank(l)) return 'blank';
|
||||
if (isLineComment(l)) return 'comment';
|
||||
return 'code';
|
||||
}
|
||||
|
||||
return lines.map((line, index) => {
|
||||
dbg(1, ` in: ${line}`);
|
||||
let info = {
|
||||
number: index + 1,
|
||||
source: line,
|
||||
sanitized: stripWhitespaceFromEnds(stripComments(line)),
|
||||
type: getLineType(line),
|
||||
};
|
||||
dbg(1, ` → ${info.number} - ${info.type}: ${info.sanitized}`);
|
||||
dbg(1, ``);
|
||||
|
||||
if (info.type === 'code') {
|
||||
const op_arg_array = info.sanitized.split(/\s+/); // split line into an array of [op, arg, extra_arg]
|
||||
if (op_arg_array[0] !== 'undefined') {
|
||||
info.operation = op_arg_array[0];
|
||||
}
|
||||
if (op_arg_array.length === 2) {
|
||||
info.argument = op_arg_array[1];
|
||||
}
|
||||
if (op_arg_array.length === 3) {
|
||||
info.argument = op_arg_array[1];
|
||||
info.extraArgument = op_arg_array[2];
|
||||
}
|
||||
// If there's too many arguments, throw an error
|
||||
// NB. there's a special case:
|
||||
// lines with the ASM_IP_LABEL can take an extra argument
|
||||
let maxArgs = 2;
|
||||
if (op_arg_array.length > 2 && op_arg_array[1].startsWith(ASM_IP_LABEL)) {
|
||||
maxArgs = 3;
|
||||
}
|
||||
if (op_arg_array.length > maxArgs) {
|
||||
console.error();
|
||||
console.error(`Error: Too many arguments`);
|
||||
console.error(` at line ${info.number}`);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} arg
|
||||
* @returns {number}
|
||||
**/
|
||||
function decodeNumericOp(arg) {
|
||||
if (arg.startsWith("$")) return hex2num(arg.replace("$", ""));
|
||||
return parseInt(arg);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} op
|
||||
* @param {object} labels // TODO document better
|
||||
* @param {number} IP
|
||||
* @returns {Array<string>} - array of labels
|
||||
**/
|
||||
function handleLabelDefinition(op, IP, labels) {
|
||||
let label = op.substring(1); // strip label prefix
|
||||
|
||||
if (label in labels) {
|
||||
labels[label].pointsToByte = IP;
|
||||
} else {
|
||||
labels[label] = {
|
||||
pointsToByte: IP,
|
||||
bytesToReplace: [],
|
||||
};
|
||||
}
|
||||
dbg(1, ` Label definition:`);
|
||||
dbg(1, ` Points to byte: ${labels[label].pointsToByte}`);
|
||||
dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`);
|
||||
dbg(1, ` IP: $${num2hex(IP)}, new code: none`);
|
||||
dbgGroupEnd(1, 'Input line');
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} op
|
||||
* @param {string} arg
|
||||
* @param {number} IP
|
||||
* @returns {Array<string>} - array of constants
|
||||
**/
|
||||
function handleConstantDefinitions(op, arg, IP, constants) {
|
||||
let constantName = op.substring(1); // strip '>'
|
||||
let constantValue = arg;
|
||||
if (constantValue === ASM_IP_LABEL) {
|
||||
constantValue = IP.toString();
|
||||
}
|
||||
constants[constantName] = constantValue;
|
||||
dbg(1, '');
|
||||
dbg(1, `Constants:`);
|
||||
dbg(1, constants);
|
||||
dbg(1, '');
|
||||
return constants;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assemble source code.
|
||||
*
|
||||
* If the source doesn't explicitly set an address to assemble to,
|
||||
* it will be assembled to the default intial value of the IP,
|
||||
* as specified in `machine.config.js`.
|
||||
* @param {string} source - Assembly source to decode
|
||||
* @return {{ debugInfo: Object, machineCode: Uint8Array }};
|
||||
**/
|
||||
function decodeInstructions(source) {
|
||||
dbg(1, 'Pre-parsing...');
|
||||
let lines = preparseSourceCode(source);
|
||||
dbg(1, '');
|
||||
dbg(1, 'Done pre-parsing.');
|
||||
dbg(1, '');
|
||||
dbg(1, 'Assembling...');
|
||||
|
||||
// Figure out where to start assembly...
|
||||
|
||||
/** @type {number} IP - Destination addr for the next line **/
|
||||
let IP;
|
||||
|
||||
// Check if the source code explicitly sets an address to assemble at
|
||||
// by including a `* [addr]` as the first (non-blank, non-comment) line
|
||||
let idOfFirstLineWithCode = lines.findIndex((el) => el.type === 'code');
|
||||
if (lines[idOfFirstLineWithCode].operation.startsWith(ASM_IP_LABEL)) {
|
||||
IP = parseInt(lines[idOfFirstLineWithCode].argument);
|
||||
} else {
|
||||
IP = INITIAL_IP_ADDRESS;
|
||||
}
|
||||
|
||||
// Initialize arrays to collect assembled code
|
||||
|
||||
/** @type {Array<number>} - Assembled source code, as an array of bytes **/
|
||||
let machineCode = new Array(IP).fill(0);
|
||||
|
||||
let debugInfo = {};
|
||||
|
||||
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
|
||||
machineCode[POINTER_TO_DISPLAY] = DISPLAY_ADDR;
|
||||
|
||||
// Initialize arrays that collect code references that
|
||||
// have to be revisited after our first pass through the source
|
||||
let labels = {};
|
||||
let constants = {};
|
||||
|
||||
|
||||
// Decode line by line...
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
// dbg(2, `line info:`);
|
||||
// dbg(2, line);
|
||||
if (line.type === 'code') {
|
||||
const op = line.operation;
|
||||
|
||||
if (typeof line.argument === 'undefined') {
|
||||
// If this isn't a label definition,
|
||||
// or one of the ops with optional arguments,
|
||||
// then it's an error
|
||||
if (!line.operation.startsWith('@')) {
|
||||
if (mnemonicsWithOptionalArgs.indexOf(line.operation.toLowerCase()) < 0) {
|
||||
console.error('');
|
||||
console.error(`Error: Missing operand ${line.source}`);
|
||||
console.error(` at line ${line.number}`);
|
||||
process.exit();
|
||||
} else {
|
||||
// It *is* one of the special optional-arg ops
|
||||
// So let's fill in the implicit operand with $00
|
||||
line.argument = '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// *** Decode special operations ***
|
||||
|
||||
// Opcodes - Handle label definitions
|
||||
if (op.startsWith(ASM_LABEL_PREFIX)) {
|
||||
labels = handleLabelDefinition(op, IP, labels);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Opcodes - Handle constant definitions
|
||||
if (op.startsWith(ASM_CONSTANT_PREFIX)) {
|
||||
constants = handleConstantDefinitions(op, line.argument, IP, constants);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Opcodes - Handle setting value of IP
|
||||
if (op.startsWith(ASM_IP_LABEL)) {
|
||||
IP = parseInt(line.argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
// *** Decode regular operations ***
|
||||
|
||||
/** @type {number|null} decodedOp **/
|
||||
let decodedOp = null;
|
||||
|
||||
/** @type {number|null} decodedArg **/
|
||||
let decodedArg = null;
|
||||
|
||||
/** @typedef {'direct'|'indirect'} AddressingMode **/
|
||||
let addressingMode = 'direct';
|
||||
|
||||
// Now that it can't be a label or a constant, normalize the opcode
|
||||
line.operation = line.operation.toLowerCase();
|
||||
|
||||
// Operands - Handle references to labels
|
||||
if (line.argument.startsWith(ASM_LABEL_PREFIX)) {
|
||||
let label = line.argument.substring(1); // strip label prefix
|
||||
if (label in labels) {
|
||||
dbg(1, `'${label}' already in labels object`);
|
||||
labels[label].bytesToReplace.push(IP + 1);
|
||||
} else {
|
||||
dbg(1, `'${label}' NOT in labels object`);
|
||||
labels[label] = {
|
||||
bytesToReplace: [IP + 1],
|
||||
};
|
||||
}
|
||||
dbg(1, `Label reference:`);
|
||||
dbg(1, ` Points to byte: ${labels[label].pointsToByte}`);
|
||||
dbg(1, ` Bytes to replace: ${labels[label].bytesToReplace}`);
|
||||
decodedArg = 0; // Return 0 for operand for now -- we'll replace it later
|
||||
}
|
||||
|
||||
// Operands - Handle references to the Instruction Pointer
|
||||
if (line.argument === ASM_IP_LABEL) {
|
||||
dbg(1, ` References current IP - ${IP}`);
|
||||
if (typeof line.extraArgument === 'undefined') {
|
||||
decodedArg = IP;
|
||||
} else {
|
||||
decodedArg = IP + decodeNumericOp(line.extraArgument);
|
||||
}
|
||||
}
|
||||
|
||||
// Operands - Handle references to constants
|
||||
if (line.argument.startsWith(ASM_CONSTANT_PREFIX)) {
|
||||
dbg(1, `References '${line.argument}'`);
|
||||
if (typeof constants[line.argument.substring(1)] === 'undefined') {
|
||||
console.error();
|
||||
console.error(`Error: Undefined constant '${line.argument}'`);
|
||||
console.error(` at line ${line.number}`);
|
||||
process.exit();
|
||||
}
|
||||
decodedArg = decodeNumericOp(constants[line.argument.substring(1)]); // substring(1) strips '>'
|
||||
}
|
||||
|
||||
// Operands - Handle references to constants in indirect mode
|
||||
if (line.argument.startsWith(`(${ASM_CONSTANT_PREFIX}`)) {
|
||||
addressingMode = "indirect";
|
||||
dbg(1, `(Indirectly) References '${line.argument}'`);
|
||||
let constName = line.argument.replace(`(${ASM_CONSTANT_PREFIX}`, "");
|
||||
constName = constName.replace(")", "");
|
||||
decodedArg = decodeNumericOp(constants[constName]);
|
||||
}
|
||||
|
||||
// Operands - Handle indirect expressions
|
||||
if (decodedArg === null && line.argument.startsWith("(")) {
|
||||
addressingMode = "indirect";
|
||||
let indyTemp = line.argument.replace("(", "").replace(")", "");
|
||||
decodedArg = decodeNumericOp(indyTemp);
|
||||
}
|
||||
|
||||
// Decode regular opcodes
|
||||
if (decodedOp === null) {
|
||||
decodedOp = mnemonics2opcodes[line.operation][addressingMode];
|
||||
}
|
||||
|
||||
// Decode regular operands
|
||||
if (decodedArg === null) {
|
||||
decodedArg = decodeNumericOp(line.argument);
|
||||
}
|
||||
|
||||
machineCode[IP] = decodedOp;
|
||||
machineCode[IP + 1] = decodedArg;
|
||||
|
||||
debugInfo[IP] = {
|
||||
lineNumber: line.number,
|
||||
source: line.source,
|
||||
address: IP,
|
||||
machine: [decodedOp, decodedArg]
|
||||
};
|
||||
|
||||
|
||||
dbg(3, ``);
|
||||
dbg(3, `Line ${line.number}: ${line.source}`);
|
||||
if (line.argument) {
|
||||
dbg(3, ` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`);
|
||||
} else if (line.operation) {
|
||||
dbg(3, ` Asm operation: ${line.operation.toUpperCase()}`);
|
||||
}
|
||||
|
||||
dbg(3, ` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`);
|
||||
dbg(3, ` IP: $${num2hex(IP)}`);
|
||||
IP += 2;
|
||||
};
|
||||
}
|
||||
|
||||
dbg(1, '');
|
||||
dbgGroup(1, 'Memory before filling in label constants');
|
||||
dbgExec(1, () => logMemory(new Uint8Array(machineCode)));
|
||||
dbgGroupEnd(1);
|
||||
|
||||
// Backfill label references
|
||||
for (let k of Object.keys(labels)) {
|
||||
dbgGroup(1, `${ASM_LABEL_PREFIX}${k}`);
|
||||
let label = labels[k];
|
||||
dbg(1, `Points to byte: ${label.pointsToByte}`);
|
||||
dbg(1, `Bytes to replace: ${label.bytesToReplace}`);
|
||||
dbgGroupEnd(1);
|
||||
for (let j = 0; j < label.bytesToReplace.length; j++) {
|
||||
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
|
||||
}
|
||||
}
|
||||
|
||||
return { 'debugInfo': debugInfo, 'machineCode': new Uint8Array(machineCode) };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {string}
|
||||
**/
|
||||
function stripComments(line) {
|
||||
return line.replace(/;.+/,"");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @returns {string}
|
||||
**/
|
||||
function stripWhitespaceFromEnds(line) {
|
||||
line = line.replace(/^\s+/,"");
|
||||
line = line.replace(/\s+$/,"");
|
||||
return line;
|
||||
}
|
||||
|
||||
function hex2num(hex) { return parseInt(hex, 16) };
|
||||
|
||||
// Debug helpers
|
||||
const dbg = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.log(s) };
|
||||
const dbgGroup = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.group(s) };
|
||||
const dbgGroupEnd = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.groupEnd() };
|
||||
const dbgExec = (lvl, func) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) func(); }
|
||||
422
src/cpu.js
422
src/cpu.js
|
|
@ -1,422 +0,0 @@
|
|||
const readline = require('readline');
|
||||
const readlineSync = require('readline-sync');
|
||||
|
||||
const {
|
||||
INITIAL_IP_ADDRESS,
|
||||
DEFAULT_CYCLE_LIMIT,
|
||||
KEYPAD_ADDR,
|
||||
KEY_MAP,
|
||||
} = require('./machine.config');
|
||||
|
||||
const {
|
||||
num2hex,
|
||||
bool2bit,
|
||||
} = require('./logging.js');
|
||||
const display = require('./display.js');
|
||||
|
||||
// STATE
|
||||
const CPU = {
|
||||
// Core state
|
||||
running: false,
|
||||
IP: INITIAL_IP_ADDRESS,
|
||||
FLAGS: {'C': false, 'Z': false, 'N': false, 'O': false},
|
||||
FLAGNUMS2NAMES: {0: 'C', 1: 'Z', 2: 'N', 3: 'O'},
|
||||
Acc: 0,
|
||||
memory: null,
|
||||
|
||||
// Functions that update core state
|
||||
/** @param {Uint8Array} data */
|
||||
loadMemory: (data) => {
|
||||
CPU.memory = new Uint8Array(256);
|
||||
CPU.memory.set(data, 0);
|
||||
},
|
||||
incrementIP: (offset) => {
|
||||
CPU.previousIP = CPU.IP;
|
||||
CPU.IP = CPU.IP + offset;
|
||||
},
|
||||
setIP: (address) => {
|
||||
CPU.previousIP = CPU.IP;
|
||||
CPU.IP = address;
|
||||
},
|
||||
updateFlagZero: () => { CPU.FLAGS.Z = CPU.Acc === 0; },
|
||||
updateFlagNegative: () => { CPU.Acc & 128 ? CPU.FLAGS.N = true : CPU.FLAGS.N = false },
|
||||
|
||||
// Debug info
|
||||
previousIP: 0,
|
||||
currentInstruction: {
|
||||
opcode: null,
|
||||
operand: null,
|
||||
mnemonic: null,
|
||||
},
|
||||
cycleCounter: 0,
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT MODIFY STATE
|
||||
|
||||
const Instructions = {
|
||||
end: () => {
|
||||
CPU.currentInstruction.mnemonic = 'END';
|
||||
CPU.running = false;
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
store_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'STO lit';
|
||||
CPU.memory[lit] = CPU.Acc;
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
store_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = `STO addr; @addr: ${num2hex(CPU.memory[addr])}`;
|
||||
CPU.memory[CPU.memory[addr]] = CPU.Acc;
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
load_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'LDA lit';
|
||||
CPU.Acc = lit;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
load_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`;
|
||||
CPU.Acc = CPU.memory[addr];
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
add_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'ADD lit';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc + lit;
|
||||
if (sum > 255) {
|
||||
CPU.FLAGS.C = true;
|
||||
sum = (sum % 255) - 1;
|
||||
} else {
|
||||
CPU.FLAGS.C = false;
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
|
||||
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
|
||||
// FIXME FIXME FIXME
|
||||
// I'm on a plane and can't remember how this works
|
||||
let overflow = 0;
|
||||
if (overflow) {
|
||||
CPU.FLAGS.O = true;
|
||||
} else {
|
||||
CPU.FLAGS.O = false;
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
add_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'ADD addr';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc + CPU.memory[addr];
|
||||
if (sum > 255) {
|
||||
CPU.FLAGS.C = true;
|
||||
sum = (sum % 255) - 1;
|
||||
} else {
|
||||
CPU.FLAGS.C = false;
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
|
||||
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
|
||||
// FIXME FIXME FIXME
|
||||
// I'm on a plane and can't remember how this works
|
||||
let overflow = 0;
|
||||
if (overflow) {
|
||||
CPU.FLAGS.O = true;
|
||||
} else {
|
||||
CPU.FLAGS.O = false;
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
sub_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'SUB lit';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc - lit;
|
||||
if (sum < 0) {
|
||||
CPU.FLAGS.C = true;
|
||||
sum = sum + 256;
|
||||
} else {
|
||||
CPU.FLAGS.C = false;
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
|
||||
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
|
||||
// FIXME FIXME FIXME
|
||||
// I'm on a plane and can't remember how this works
|
||||
let overflow = 0;
|
||||
if (overflow) {
|
||||
CPU.FLAGS.O = true;
|
||||
} else {
|
||||
CPU.FLAGS.O = false;
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
sub_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'SUB addr';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc - CPU.memory[addr];
|
||||
if (sum < 0) {
|
||||
CPU.FLAGS.C = true;
|
||||
sum = sum + 256;
|
||||
} else {
|
||||
CPU.FLAGS.C = false;
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
|
||||
// let overflow = bitSixCarry ^ (CPU.FLAGS & 8);
|
||||
// FIXME FIXME FIXME
|
||||
// I'm on a plane and can't remember how this works
|
||||
let overflow = 0;
|
||||
if (overflow) {
|
||||
CPU.FLAGS.O = true;
|
||||
} else {
|
||||
CPU.FLAGS.O = false;
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
hop_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
|
||||
if (CPU.Acc === lit) {
|
||||
CPU.incrementIP(4);
|
||||
} else {
|
||||
CPU.incrementIP(2);
|
||||
}
|
||||
},
|
||||
|
||||
hop_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'HOP addr';
|
||||
if (CPU.Acc === CPU.memory[addr]) {
|
||||
CPU.incrementIP(4);
|
||||
} else {
|
||||
CPU.incrementIP(2);
|
||||
}
|
||||
},
|
||||
|
||||
jump_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'JMP lit';
|
||||
CPU.setIP(lit);
|
||||
},
|
||||
|
||||
jump_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'JMP addr';
|
||||
CPU.setIP(CPU.memory[addr]);
|
||||
},
|
||||
|
||||
flag_toggle: (flagNum) => {
|
||||
if (flagNum === null) {
|
||||
console.error('Invalid flag number');
|
||||
process.exit();
|
||||
}
|
||||
const flagName = CPU.FLAGNUMS2NAMES[flagNum];
|
||||
CPU.currentInstruction.mnemonic = `FTG ${flagName}`;
|
||||
CPU.FLAGS[flagName] = !CPU.FLAGS[flagName];
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
|
||||
flag_hop: (flagNum) => {
|
||||
if (flagNum === null) {
|
||||
console.error('Invalid flag number');
|
||||
process.exit();
|
||||
}
|
||||
const flagName = CPU.FLAGNUMS2NAMES[flagNum];
|
||||
CPU.currentInstruction.mnemonic = `FHP ${flagName}; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
|
||||
if (CPU.FLAGS[CPU.FLAGNUMS2NAMES[flagNum]]) {
|
||||
CPU.incrementIP(4);
|
||||
} else {
|
||||
CPU.incrementIP(2);
|
||||
}
|
||||
},
|
||||
|
||||
no_op: () => {
|
||||
CPU.currentInstruction.mnemonic = `NOP`;
|
||||
CPU.incrementIP(2);
|
||||
},
|
||||
}
|
||||
|
||||
const opcodes2mnemonics = {
|
||||
0: (operand) => Instructions.end(),
|
||||
1: (operand) => Instructions.store_lit(operand),
|
||||
2: (operand) => Instructions.store_addr(operand),
|
||||
3: (operand) => Instructions.load_lit(operand),
|
||||
4: (operand) => Instructions.load_addr(operand),
|
||||
5: (operand) => Instructions.add_lit(operand),
|
||||
6: (operand) => Instructions.add_addr(operand),
|
||||
7: (operand) => Instructions.sub_lit(operand),
|
||||
8: (operand) => Instructions.sub_addr(operand),
|
||||
9: (operand) => Instructions.hop_lit(operand),
|
||||
10: (operand) => Instructions.hop_addr(operand),
|
||||
11: (operand) => Instructions.jump_lit(operand),
|
||||
12: (operand) => Instructions.jump_addr(operand),
|
||||
13: (operand) => Instructions.flag_toggle(operand),
|
||||
14: (operand) => Instructions.flag_hop(operand),
|
||||
15: (operand) => Instructions.no_op(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Load code into memory and set CPU state to "running"
|
||||
* @param {Uint8Array} code - Machine code to load
|
||||
**/
|
||||
function startCPU(code) {
|
||||
CPU.loadMemory(code);
|
||||
CPU.cycleCounter = 0;
|
||||
CPU.running = true;
|
||||
|
||||
// FIXME: This conflicts with single-stepping
|
||||
// (you can single-step, but keys aren't passed
|
||||
// through to the Cardiograph)
|
||||
//
|
||||
// -> The fix is maybe to remove readlineSync,
|
||||
// and instead stash the keypress into a buffer variable.*
|
||||
// Then have the stepping function check that buffer,
|
||||
// and then clear the buffer, each time it runs.
|
||||
//
|
||||
// * If it's in the set of keys that are relevant
|
||||
// to single-stepping.
|
||||
|
||||
// Start listening for keypresses...
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
if (process.stdin.setRawMode != null) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again?
|
||||
if (key.sequence === '\x03') process.exit();
|
||||
let name = key.name.toUpperCase();
|
||||
if (name in KEY_MAP) {
|
||||
CPU.memory[KEYPAD_ADDR] = KEY_MAP[name];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute just the next instruction in memory
|
||||
* @param {Object} debugInfo
|
||||
* @param {Boolean} [debug] - Print machine status and the line of code being executed
|
||||
**/
|
||||
async function stepCPU(debugInfo, debug = false, prettyPrintDisplay = false) {
|
||||
if (CPU.IP >= CPU.memory.length) {
|
||||
console.error('HALTING - IP greater than memory size');
|
||||
CPU.running = false;
|
||||
process.exit();
|
||||
} else {
|
||||
CPU.currentInstruction.opcode = CPU.memory[CPU.IP];
|
||||
CPU.currentInstruction.operand = CPU.memory[CPU.IP+1];
|
||||
let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode];
|
||||
if (typeof executeInstruction === 'undefined') {
|
||||
let info = debugInfo[CPU.previousIP];
|
||||
console.error();
|
||||
console.error(`Error: Invalid opcode`);
|
||||
console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`);
|
||||
console.error(` from line ${info.lineNumber}: ${info.source}`);
|
||||
process.exit();
|
||||
}
|
||||
executeInstruction(CPU.currentInstruction.operand);
|
||||
CPU.cycleCounter += 1;
|
||||
}
|
||||
logCPUState(debugInfo, debug, prettyPrintDisplay);
|
||||
if (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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
|
||||
|
||||
/**
|
||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||||
**/
|
||||
function logCPUState(debugInfo, debug = false, prettyPrintDisplay = false) {
|
||||
debugInfo = debugInfo[CPU.previousIP] !== 'undefined' ? debugInfo[CPU.previousIP] : false;
|
||||
console.group(`Step ${CPU.cycleCounter}`);
|
||||
console.log();
|
||||
if (!debug) console.clear();
|
||||
display.show(CPU.memory, prettyPrintDisplay);
|
||||
console.log();
|
||||
if (debugInfo) {
|
||||
console.log(`Line ${debugInfo.lineNumber}: ${debugInfo.source}`);
|
||||
console.log();
|
||||
}
|
||||
console.log('Mnemonic:', CPU.currentInstruction.mnemonic);
|
||||
console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`);
|
||||
console.log();
|
||||
console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} ONZC ${bool2bit(CPU.FLAGS.O)}${bool2bit(CPU.FLAGS.N)}${bool2bit(CPU.FLAGS.Z)}${bool2bit(CPU.FLAGS.C)}`);
|
||||
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])} ${CPU.running ? "running" : "halted" }`);
|
||||
console.log();
|
||||
console.log();
|
||||
console.groupEnd();
|
||||
};
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* Display a table of memory locations.
|
||||
* Call with [start] and [end] indices to display a range.
|
||||
* @param {Uint8Array} mem - Memory to display
|
||||
* @param {number} [start] - A start-index, in decimal
|
||||
* @param {number} [end] - An end-index, in decimal
|
||||
**/
|
||||
const logMemory = (mem, start=0, end=mem.length) => {
|
||||
let top1 = `┌─────────┬────────┬─────────┐`;
|
||||
let top2 = `│ addrs │ opcode │ operand │`;
|
||||
let top3 = `├─────────┼────────┼─────────┤`;
|
||||
let blnk = `│ │ │ │`;
|
||||
let bot1 = `└─────────┴────────┴─────────┘`;
|
||||
console.log(`${top1}\n${top2}\n${top3}`);
|
||||
for (let i = start; i < mem.length; i +=2) {
|
||||
let operand = mem[i+1];
|
||||
if (typeof operand === 'undefined') {
|
||||
console.log(` ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ │`);
|
||||
} else {
|
||||
console.log(`│ ${num2hex(i)} ${num2hex(i+1)} │ ${num2hex(mem[i])} │ ${num2hex(operand)} │`);
|
||||
}
|
||||
|
||||
// Add a blank row every 4 lines:
|
||||
let rowNum = i - start + 2; // Not actually the row number...
|
||||
if ((rowNum % 8 === 0)
|
||||
&& (i < (mem.length - 2))) {
|
||||
console.log(blnk);
|
||||
}
|
||||
}
|
||||
console.log(bot1);
|
||||
}
|
||||
|
||||
const logRunningHeader = () => {
|
||||
console.log();
|
||||
let time = new Date();
|
||||
console.log( `┌─────────────────────┐`);
|
||||
console.log( `│ Running at ${time.toLocaleTimeString('en-GB')} │` );
|
||||
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 = {
|
||||
"logMemory": logMemory,
|
||||
"logRunningHeader": logRunningHeader,
|
||||
"num2hex": num2hex,
|
||||
"hex2num": hex2num,
|
||||
"num2bin": num2bin,
|
||||
"num2bin_4bit": num2bin_4bit,
|
||||
"bin2num": bin2num,
|
||||
"bool2bit": bool2bit,
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
module.exports = {
|
||||
"INITIAL_IP_ADDRESS": 29,
|
||||
|
||||
// Use these in CPU:
|
||||
"DISPLAY_ADDR": 0,
|
||||
"KEYPAD_ADDR": 27,
|
||||
// Store the `DISPLAY_ADDR` at this address when assembling:
|
||||
"POINTER_TO_DISPLAY": 26,
|
||||
|
||||
"KEY_MAP": {
|
||||
// Same layout as COSMAC VIP / CHIP-8
|
||||
// (This object maps qwerty keys to hex keys
|
||||
// so that they are arranged in the same layout
|
||||
// as the real keypad)
|
||||
'1':'1', '2':'2', '3':'3', '4':'C',
|
||||
'Q':'4', 'W':'5', 'E':'6', 'R':'D',
|
||||
'A':'7', 'S':'8', 'D':'9', 'F':'E',
|
||||
'Z':'A', 'X':'0', 'C':'B', 'V':'F',
|
||||
|
||||
// Include conventional arrow keys
|
||||
'UP': '5',
|
||||
'LEFT': '7',
|
||||
'DOWN': '8',
|
||||
'RIGHT': '9',
|
||||
},
|
||||
|
||||
// max number of times to step the CPU,
|
||||
// to stop endless loops
|
||||
// 0 = infinite
|
||||
"DEFAULT_CYCLE_LIMIT": 2048,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "paper-computer",
|
||||
"scripts": {
|
||||
"jsdoc": "./node_modules/.bin/jsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readline-sync": "^1.4.10"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue