Compare commits
175 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
ceb6019a1c | |
|
|
353b7d7fdc | |
|
|
82c0283b25 | |
|
|
3261b6c97a | |
|
|
98b19ce936 | |
|
|
4c605e92c5 | |
|
|
6bea93308c | |
|
|
cf68271440 | |
|
|
e82a429d5e | |
|
|
6f164294e5 | |
|
|
3fd770ab53 | |
|
|
f21e57cafe | |
|
|
d5ded67b79 | |
|
|
fd4ca3e8c8 | |
|
|
fa504685d2 | |
|
|
6e58241288 | |
|
|
61361c5f3f | |
|
|
7e842bd7a6 | |
|
|
34e2c24b88 | |
|
|
f3f6a58a65 | |
|
|
35d164b0a7 | |
|
|
62b7396ab6 | |
|
|
9bd88aa8bc | |
|
|
b2933a50a0 | |
|
|
83e980b685 | |
|
|
4854ce34fa | |
|
|
92a619fded | |
|
|
ae587a0712 | |
|
|
f802420799 | |
|
|
3e32cb97e1 | |
|
|
91cba57aa1 | |
|
|
98bfa50269 | |
|
|
9c82265a88 | |
|
|
63eb4a9500 | |
|
|
51c64cc615 | |
|
|
98fa9a4ab7 | |
|
|
93f88560a2 | |
|
|
b0996d30c3 | |
|
|
77a41d47c3 | |
|
|
101421c4c3 | |
|
|
d26bf39f05 | |
|
|
c8c188e55a | |
|
|
e25723fcc9 | |
|
|
9c1cf4abba | |
|
|
2848588fc2 | |
|
|
b3d10a4197 | |
|
|
16f85d3b9f | |
|
|
0b91a71575 | |
|
|
f9901d304b | |
|
|
c84c86c160 | |
|
|
7b143ab000 | |
|
|
866f553346 | |
|
|
eff9043665 | |
|
|
1fe582663b | |
|
|
8bb3b9b43f | |
|
|
e9d721042b | |
|
|
19784b56b6 | |
|
|
12d592c262 | |
|
|
5671314b10 | |
|
|
a6e9966797 | |
|
|
8a38d6f831 | |
|
|
be13802e1e | |
|
|
45c8fe3bd2 | |
|
|
049d9367ac | |
|
|
7faf190fe2 | |
|
|
41d7b7ba54 | |
|
|
14a32b9c27 | |
|
|
8a19238612 | |
|
|
28b92b4980 | |
|
|
a2a79cea46 | |
|
|
b776951e81 | |
|
|
d9212ab620 | |
|
|
37bb92f296 | |
|
|
b08d9854c4 | |
|
|
74c6f83fcc | |
|
|
ccc032b379 | |
|
|
efe20eabdf | |
|
|
144ae8de6c | |
|
|
dee5b4afd4 | |
|
|
e6883fbc65 | |
|
|
072d2ccdb5 | |
|
|
9f4a67770f | |
|
|
5b52143ad0 | |
|
|
fbda5ce927 | |
|
|
18d77086df | |
|
|
81617dfe42 | |
|
|
49cb3171da | |
|
|
03715a6a8e | |
|
|
b7d13087e6 | |
|
|
b173d46cb6 | |
|
|
ccebc6ec3d | |
|
|
2d210303e6 | |
|
|
bf589411e7 | |
|
|
e09bd9a66f | |
|
|
1c8722c65e | |
|
|
6545df9cfd | |
|
|
f335aba94d | |
|
|
97e54d01cb | |
|
|
29f25bda7a | |
|
|
872489c18d | |
|
|
ac0fccf2e9 | |
|
|
86d0b57c7a | |
|
|
9f68bd3027 | |
|
|
b290eb1568 | |
|
|
ddab8f42c4 | |
|
|
edad9ecbb8 | |
|
|
41632b0a0f | |
|
|
d0b620f1ad | |
|
|
7cfad439f8 | |
|
|
4bb394f039 | |
|
|
1ce6757838 | |
|
|
dea1a445f6 | |
|
|
902b218547 | |
|
|
0a3d474c43 | |
|
|
98c17cf925 | |
|
|
f612e7582a | |
|
|
897749b108 | |
|
|
2802623c4c | |
|
|
1bf2144a67 | |
|
|
d30050a292 | |
|
|
dbe630eb5e | |
|
|
d56eb34a44 | |
|
|
d6b55db381 | |
|
|
dc78518b73 | |
|
|
51bc13fe5b | |
|
|
0fca21dfab | |
|
|
9268b2c59a | |
|
|
621afa0553 | |
|
|
527d70fcdf | |
|
|
1dc535a71e | |
|
|
f0632b0969 | |
|
|
68ae53a245 | |
|
|
aafeeb572b | |
|
|
7074f0c5f5 | |
|
|
be8b1ef272 | |
|
|
6e1a3faff2 | |
|
|
2ad6d46b5d | |
|
|
6fc6e727d0 | |
|
|
f2e43888b5 | |
|
|
78ac43bead | |
|
|
ee1e899108 | |
|
|
14770db506 | |
|
|
91853cd7e3 | |
|
|
5f29c7c3eb | |
|
|
e1cc491e28 | |
|
|
ae3743926c | |
|
|
944a0932f6 | |
|
|
af52d4f373 | |
|
|
1d35f659ee | |
|
|
c8d30e524a | |
|
|
c0f11f2b03 | |
|
|
a766fd867c | |
|
|
12273a6389 | |
|
|
f0e8664ab8 | |
|
|
4481fc10d4 | |
|
|
a52336db07 | |
|
|
00ddf2c8fe | |
|
|
d48ead819f | |
|
|
97f1d02912 | |
|
|
eaa0597552 | |
|
|
efab770460 | |
|
|
209a03f281 | |
|
|
286875135f | |
|
|
5e1cf9e413 | |
|
|
9fa1bf5392 | |
|
|
c5c64e5cd5 | |
|
|
f421abaaf7 | |
|
|
08fe395a5f | |
|
|
c53bc14c36 | |
|
|
37578f038d | |
|
|
190592813d | |
|
|
2cceaa71f2 | |
|
|
934f4c0f3f | |
|
|
1baa0ded32 | |
|
|
c415056194 |
|
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
*.tmp.*
|
||||
node_modules
|
||||
cardiograph.code-workspace
|
||||
*venv*
|
||||
*__pycache__
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[submodule "src/argparser"]
|
||||
path = src/opter
|
||||
url = https://git.nloewen.com/n/argv-parser.git
|
||||
[submodule "src/python/opter-py"]
|
||||
path = src/python/opter-py
|
||||
url = https://git.nloewen.com/n/opter-py.git
|
||||
228
assembler.js
228
assembler.js
|
|
@ -1,228 +0,0 @@
|
|||
const { logMemory, num2hex } = require('./logging.js');
|
||||
const {
|
||||
INITIAL_IP_ADDRESS,
|
||||
DISPLAY_ADDR,
|
||||
KEYPAD_ADDR,
|
||||
POINTER_TO_DISPLAY,
|
||||
POINTER_TO_KEYPAD
|
||||
} = require('./machine.config.js');
|
||||
|
||||
// 1 = verbose
|
||||
// 2 = what i'm currently focusing on
|
||||
// 3 = always print
|
||||
// 4 = silent
|
||||
const DEBUG_LEVEL = 2;
|
||||
let DEBUG = false; // Turn debugging on/off -- set by assemble()
|
||||
|
||||
exports.assemble = (str, debug = false) => {
|
||||
DEBUG = debug;
|
||||
return decodeInstructions(str);
|
||||
}
|
||||
|
||||
// Configure pseudo-ops:
|
||||
const POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND = '*addr';
|
||||
const CONSTANT_PREFIX = '#';
|
||||
const 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 },
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} line - One line of assembly to decode
|
||||
**/
|
||||
function decodeInstructions(line) {
|
||||
let lines = line.split(/\n/); // returns an array of lines
|
||||
|
||||
let machineCode = new Array(INITIAL_IP_ADDRESS).fill(0);
|
||||
machineCode[POINTER_TO_DISPLAY] = DISPLAY_ADDR;
|
||||
machineCode[POINTER_TO_KEYPAD] = KEYPAD_ADDR;
|
||||
|
||||
let labels = {};
|
||||
let constants = {};
|
||||
let IP = INITIAL_IP_ADDRESS;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
dbg(2, '');
|
||||
dbgGroup(1, `Input line ${i}, IP ${num2hex(IP)}`);
|
||||
dbg(3, `> ${lines[i]}`);
|
||||
let line = stripWhitespaceFromEnds(stripComments(lines[i]));
|
||||
|
||||
// Handle blank lines
|
||||
if (line.length === 0) {
|
||||
dbg(3, `IP: $${num2hex(IP)}, new code: none`);
|
||||
dbg(1, 'blank');
|
||||
dbgGroupEnd(1, 'Input line');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// HANDLE OPS
|
||||
|
||||
// Handle label definitions
|
||||
if (line.startsWith(LABEL_PREFIX)) {
|
||||
let label = line.substring(1); // strip label prefix
|
||||
|
||||
if (label in labels) {
|
||||
labels[label].pointsToByte = IP;
|
||||
} else {
|
||||
labels[label] = {
|
||||
pointsToByte: IP,
|
||||
bytesToReplace: [],
|
||||
};
|
||||
}
|
||||
dbg(2, `pointsToByte: ${labels[label].pointsToByte}`);
|
||||
dbg(2, `bytesToReplace: ${labels[label].bytesToReplace}`);
|
||||
dbg(3, `IP: $${num2hex(IP)}, new code: none`);
|
||||
dbgGroupEnd(1, 'Input line');
|
||||
continue;
|
||||
}
|
||||
|
||||
let op_arg_array = line.split(/\s+/); // split line into an array of [op, arg]
|
||||
let opName = op_arg_array[0].toLowerCase();
|
||||
let arg_str = op_arg_array[1];
|
||||
let arg_num = null;
|
||||
let addressingMode = 'direct'; // Must be "direct" or "indirect"
|
||||
|
||||
// Handle constant definitions
|
||||
if (opName.startsWith(CONSTANT_PREFIX)) {
|
||||
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
|
||||
let constantName = opName.substring(1).toLowerCase(); // strip '>'
|
||||
let constantValue = arg_str;
|
||||
if (constantValue.toLowerCase() === POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND) {
|
||||
constantValue = IP.toString();
|
||||
}
|
||||
constants[constantName] = constantValue;
|
||||
dbg(2, `constants:`);
|
||||
dbg(2, constants);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle mnemonics without operands (eg END) ...
|
||||
if (typeof arg_str === 'undefined') {
|
||||
if (mnemonicsWithOptionalArgs.indexOf(opName) < 0) {
|
||||
console.error(`Missing opcode: ${line}`);
|
||||
throw new Error("Missing opcode");
|
||||
}
|
||||
arg_num = 0;
|
||||
|
||||
|
||||
// HANDLE OPERANDS
|
||||
|
||||
// Handle references to labels
|
||||
} else if (arg_str.startsWith(LABEL_PREFIX)) {
|
||||
let label = arg_str.substring(1); // strip label prefix
|
||||
arg_num = 0;
|
||||
|
||||
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(2, `pointsToByte: ${labels[label].pointsToByte}`);
|
||||
dbg(2, `bytesToReplace: ${labels[label].bytesToReplace}`);
|
||||
|
||||
// Handle references to the Instruction Pointer
|
||||
} else if (arg_str.toLowerCase() === POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND) {
|
||||
dbg(2, `operand references current address`);
|
||||
arg_num = IP;
|
||||
dbg(2, `arg_num: ${num2hex(arg_num)}`);
|
||||
|
||||
// Handle references to constants
|
||||
} else if (arg_str.startsWith(CONSTANT_PREFIX)) {
|
||||
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
|
||||
arg_str = arg_str.substring(1).toLowerCase(); // strip '>'
|
||||
dbg(2, `operand references '${arg_str}'`);
|
||||
arg_str = constants[arg_str];
|
||||
dbg(2, `arg_str from '${arg_str}'`);
|
||||
|
||||
// Handle references to constants in indirect mode
|
||||
} else if (arg_str.startsWith(`(${CONSTANT_PREFIX}`)) {
|
||||
addressingMode = "indirect";
|
||||
arg_str = arg_str.replace(`(${CONSTANT_PREFIX}`, "");
|
||||
arg_str = arg_str.replace(")", "");
|
||||
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
|
||||
arg_str = arg_str.toLowerCase();
|
||||
dbg(2, `INDY - operand references '${arg_str}'`);
|
||||
arg_str = constants[arg_str];
|
||||
|
||||
// Handle indirect expressions
|
||||
} else if (arg_str.startsWith("(")) {
|
||||
addressingMode = "indirect";
|
||||
arg_str = arg_str.replace("(", "");
|
||||
arg_str = arg_str.replace(")", "");
|
||||
}
|
||||
|
||||
// Handle numeric operands
|
||||
if (arg_num === null) {
|
||||
if (arg_str.startsWith("$")) {
|
||||
// Handle hex
|
||||
arg_str = arg_str.replace("$", "");
|
||||
arg_num = hex2num(arg_str);
|
||||
} else {
|
||||
// Accept decimal i guess
|
||||
arg_num = parseInt(arg_str);
|
||||
}
|
||||
}
|
||||
|
||||
// DECODE!
|
||||
const op = mnemonics2opcodes[opName][addressingMode];
|
||||
|
||||
machineCode.push(op);
|
||||
machineCode.push(arg_num);
|
||||
dbg(3, `IP: $${num2hex(IP)}, new code: $${num2hex(op)} $${num2hex(arg_num)}`);
|
||||
IP += 2;
|
||||
dbgGroupEnd(1, 'Input line');
|
||||
};
|
||||
|
||||
dbg(1, '');
|
||||
dbgGroup(1, 'Memory before filling in label constants');
|
||||
dbgExec(1, () => logMemory(new Uint8Array(machineCode)));
|
||||
dbgGroupEnd(1, 'Memory before filling in label constants');
|
||||
|
||||
// Backfill label references
|
||||
for (let k of Object.keys(labels)) {
|
||||
dbgGroup(2, `${LABEL_PREFIX}${k}`);
|
||||
let label = labels[k];
|
||||
dbg(2, `pointsToByte: ${label.pointsToByte}`);
|
||||
dbg(2, `bytesToReplace: ${label.bytesToReplace}`);
|
||||
dbgGroupEnd(2, `label`);
|
||||
for (let j = 0; j < label.bytesToReplace.length; j++) {
|
||||
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(machineCode);
|
||||
}
|
||||
|
||||
|
||||
function stripComments(line) {
|
||||
return line.replace(/;.+/,"");
|
||||
}
|
||||
|
||||
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(); }
|
||||
391
cpu.js
391
cpu.js
|
|
@ -1,391 +0,0 @@
|
|||
const readline = require('readline');
|
||||
const readlineSync = require('readline-sync');
|
||||
|
||||
const {
|
||||
INITIAL_IP_ADDRESS,
|
||||
CYCLE_LIMIT,
|
||||
KEYPAD_ADDR,
|
||||
KEY_MAP,
|
||||
} = require('./machine.config');
|
||||
|
||||
const {
|
||||
num2hex,
|
||||
num2bin_4bit,
|
||||
} = require('./logging.js');
|
||||
const display = require('./display.js');
|
||||
|
||||
// STATE
|
||||
const CPU = {
|
||||
// Core state
|
||||
running: false,
|
||||
IP: INITIAL_IP_ADDRESS,
|
||||
FLAGS: 0, // A bit field! 0000 = NZOC
|
||||
Acc: 0,
|
||||
memory: null,
|
||||
|
||||
// Functions that update core state
|
||||
/** @param {Uint8Array} data */
|
||||
loadMemory: (data) => {
|
||||
CPU.memory = new Uint8Array(256);
|
||||
CPU.memory.set(data, 0);
|
||||
},
|
||||
setFlagNegative: () => { CPU.FLAGS |= 8 },
|
||||
setFlagZero: () => { CPU.FLAGS |= 4 },
|
||||
setFlagOverflow: () => { CPU.FLAGS |= 2 },
|
||||
setFlagCarry: () => { CPU.FLAGS |= 1 },
|
||||
unsetFlagNegative: () => { CPU.FLAGS &= ~8 },
|
||||
unsetFlagZero: () => { CPU.FLAGS &= ~4 },
|
||||
unsetFlagOverflow: () => { CPU.FLAGS &= ~2 },
|
||||
unsetFlagCarry: () => { CPU.FLAGS &= ~1 },
|
||||
updateFlagZero: () => {
|
||||
if (CPU.Acc === 0) {
|
||||
CPU.setFlagZero();
|
||||
} else {
|
||||
CPU.unsetFlagZero();
|
||||
}
|
||||
},
|
||||
updateFlagNegative: () => {
|
||||
CPU.Acc & 128 ? CPU.setFlagNegative : CPU.unsetFlagNegative
|
||||
},
|
||||
|
||||
// Debug info
|
||||
currentInstruction: {
|
||||
opcode: null,
|
||||
operand: null,
|
||||
mnemonic: null,
|
||||
},
|
||||
cycleCounter: 0,
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT MODIFY STATE
|
||||
|
||||
const Instructions = {
|
||||
end: () => {
|
||||
CPU.currentInstruction.mnemonic = 'END';
|
||||
CPU.running = false;
|
||||
},
|
||||
|
||||
store_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'STO lit';
|
||||
CPU.memory[lit] = CPU.Acc;
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
store_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'STO addr';
|
||||
CPU.memory[CPU.memory[addr]] = CPU.Acc;
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
load_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'LDA lit';
|
||||
CPU.Acc = lit;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
load_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`;
|
||||
CPU.Acc = CPU.memory[addr];
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
add_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'ADD lit';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc + lit;
|
||||
if (sum > 255) {
|
||||
CPU.setFlagCarry();
|
||||
sum = (sum % 255) - 1;
|
||||
} else {
|
||||
CPU.unsetFlagCarry();
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
|
||||
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
|
||||
if (overflow) {
|
||||
CPU.setFlagOverflow();
|
||||
} else {
|
||||
CPU.unsetFlagOverflow();
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
add_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'ADD addr';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc + CPU.memory[addr];
|
||||
if (sum > 255) {
|
||||
CPU.setFlagCarry();
|
||||
sum = (sum % 255) - 1;
|
||||
} else {
|
||||
CPU.unsetFlagCarry();
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
|
||||
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
|
||||
if (overflow) {
|
||||
CPU.setFlagOverflow();
|
||||
} else {
|
||||
CPU.unsetFlagOverflow();
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
sub_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'SUB lit';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc - lit;
|
||||
if (sum < 0) {
|
||||
CPU.setFlagCarry();
|
||||
sum = sum + 256;
|
||||
} else {
|
||||
CPU.unsetFlagCarry();
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
|
||||
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
|
||||
if (overflow) {
|
||||
CPU.setFlagOverflow();
|
||||
} else {
|
||||
CPU.unsetFlagOverflow();
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
sub_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'SUB addr';
|
||||
// Calculate sum
|
||||
let sum = CPU.Acc - CPU.memory[addr];
|
||||
if (sum < 0) {
|
||||
CPU.setFlagCarry();
|
||||
sum = sum + 256;
|
||||
} else {
|
||||
CPU.unsetFlagCarry();
|
||||
}
|
||||
// Calculate overflow flag status
|
||||
let bitSixCarry = 0;
|
||||
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
|
||||
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
|
||||
if (overflow) {
|
||||
CPU.setFlagOverflow();
|
||||
} else {
|
||||
CPU.unsetFlagOverflow();
|
||||
}
|
||||
CPU.Acc = sum;
|
||||
CPU.updateFlagNegative();
|
||||
CPU.updateFlagZero();
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
hop_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
|
||||
if (CPU.Acc === lit) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
|
||||
hop_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'HOP addr';
|
||||
if (CPU.Acc === CPU.memory[addr]) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
|
||||
jump_lit: (lit) => {
|
||||
CPU.currentInstruction.mnemonic = 'JMP lit';
|
||||
CPU.IP = lit;
|
||||
},
|
||||
|
||||
jump_addr: (addr) => {
|
||||
CPU.currentInstruction.mnemonic = 'JMP addr';
|
||||
CPU.IP = CPU.memory[addr];
|
||||
},
|
||||
|
||||
flag_toggle: (flagNum) => {
|
||||
CPU.currentInstruction.mnemonic = 'FTG';
|
||||
let mask = null;
|
||||
if (flagNum === 0) { mask = 1; }
|
||||
if (flagNum === 1) { mask = 2; }
|
||||
if (flagNum === 2) { mask = 4; }
|
||||
if (flagNum === 3) { mask = 8; }
|
||||
if (mask === null) { throw new Error('Invalid flag number'); }
|
||||
CPU.FLAGS = CPU.FLAGS ^= mask;
|
||||
CPU.IP += 2;
|
||||
},
|
||||
|
||||
flag_hop: (flagNum) => {
|
||||
CPU.currentInstruction.mnemonic = `FHP; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
|
||||
let mask = null;
|
||||
if (flagNum === 0) { mask = 1; }
|
||||
if (flagNum === 1) { mask = 2; }
|
||||
if (flagNum === 2) { mask = 4; }
|
||||
if (flagNum === 3) { mask = 8; }
|
||||
if (mask === null) { throw new Error('Invalid flag number'); }
|
||||
if (CPU.FLAGS & mask) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
|
||||
no_op: () => {
|
||||
CPU.currentInstruction.mnemonic = `NOP`;
|
||||
CPU.IP += 2;
|
||||
},
|
||||
}
|
||||
|
||||
const opcodes2mnemonics = {
|
||||
0: (operand) => Instructions.end(),
|
||||
1: (operand) => Instructions.store_lit(operand),
|
||||
2: (operand) => Instructions.store_addr(operand),
|
||||
3: (operand) => Instructions.load_lit(operand),
|
||||
4: (operand) => Instructions.load_addr(operand),
|
||||
5: (operand) => Instructions.add_lit(operand),
|
||||
6: (operand) => Instructions.add_addr(operand),
|
||||
7: (operand) => Instructions.sub_lit(operand),
|
||||
8: (operand) => Instructions.sub_addr(operand),
|
||||
9: (operand) => Instructions.hop_lit(operand),
|
||||
10: (operand) => Instructions.hop_addr(operand),
|
||||
11: (operand) => Instructions.jump_lit(operand),
|
||||
12: (operand) => Instructions.jump_addr(operand),
|
||||
13: (operand) => Instructions.flag_toggle(operand),
|
||||
14: (operand) => Instructions.flag_hop(operand),
|
||||
15: (operand) => Instructions.no_op(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Load code into memory and set CPU state to "running"
|
||||
* @param {Uint8Array} code - Machine code to load
|
||||
**/
|
||||
function startCPU(code) {
|
||||
CPU.loadMemory(code);
|
||||
CPU.cycleCounter = 0;
|
||||
CPU.running = true;
|
||||
|
||||
// FIXME: This conflicts with single-stepping
|
||||
// (you can single-step, but keys aren't passed
|
||||
// through to the Cardiograph)
|
||||
//
|
||||
// -> The fix is maybe to remove readlineSync,
|
||||
// and instead stash the keypress into a buffer variable.*
|
||||
// Then have the stepping function check that buffer,
|
||||
// and then clear the buffer, each time it runs.
|
||||
//
|
||||
// * If it's in the set of keys that are relevant
|
||||
// to single-stepping.
|
||||
|
||||
// Start listening for keypresses...
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
if (process.stdin.setRawMode != null) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again?
|
||||
if (key.sequence === '\x03') process.exit();
|
||||
|
||||
str = str.toUpperCase();
|
||||
if (str in KEY_MAP) {
|
||||
CPU.memory[KEYPAD_ADDR] = KEY_MAP[str];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute just the next instruction in memory
|
||||
**/
|
||||
async function stepCPU(debug = false) {
|
||||
if (CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops
|
||||
if (CPU.cycleCounter > CYCLE_LIMIT) {
|
||||
console.warn('HALTING - reached cycle limit');
|
||||
CPU.running = false;
|
||||
}
|
||||
}
|
||||
if (CPU.running) {
|
||||
if (CPU.IP >= CPU.memory.length) {
|
||||
console.error('HALTING - IP greater than memory size');
|
||||
CPU.running = false;
|
||||
} else {
|
||||
CPU.currentInstruction.opcode = CPU.memory[CPU.IP];
|
||||
CPU.currentInstruction.operand = CPU.memory[CPU.IP+1];
|
||||
let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode];
|
||||
executeInstruction(CPU.currentInstruction.operand);
|
||||
CPU.cycleCounter += 1;
|
||||
}
|
||||
}
|
||||
logCPUState(debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} code - Machine code to run
|
||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||||
* @param {Number} [clockSpeed] - CPU clock speed in milliseconds
|
||||
**/
|
||||
exports.runProgram = (code, debug = false, clockSpeed = 100) => {
|
||||
startCPU(code);
|
||||
// Animate the output by pausing between steps
|
||||
const loop = setInterval(async () => {
|
||||
stepCPU(debug);
|
||||
if (!CPU.running) clearInterval(loop);
|
||||
}, clockSpeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} code - Machine code to run
|
||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||||
**/
|
||||
exports.singleStepProgram = (code, debug = false) => {
|
||||
startCPU(code);
|
||||
while (CPU.running) {
|
||||
stepCPU(debug);
|
||||
// FIXME: this prevents exiting with Ctrl+C:
|
||||
let key = readlineSync.keyIn('S to step, Q to quit > ', {
|
||||
limit: ['s', 'S', 'q', 'Q'],
|
||||
});
|
||||
if (key.toLowerCase() === 'q') { process.exit(); }
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
|
||||
|
||||
/**
|
||||
* @param {Boolean} [debug] - Enable/disable debugging printouts
|
||||
**/
|
||||
function logCPUState(debug = false) {
|
||||
console.group(`Step`);
|
||||
if (!debug) console.clear();
|
||||
if (debug) {
|
||||
display.printDisplay(CPU.memory);
|
||||
} else {
|
||||
display.prettyPrintDisplay(CPU.memory);
|
||||
}
|
||||
console.log();
|
||||
console.log('Mnemonic:', CPU.currentInstruction.mnemonic);
|
||||
console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`);
|
||||
console.log();
|
||||
console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} NZOC: ${num2bin_4bit(CPU.FLAGS)}`);
|
||||
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])} ${CPU.running ? "running" : "halted" }`);
|
||||
console.log();
|
||||
console.groupEnd();
|
||||
};
|
||||
30
display.js
30
display.js
|
|
@ -1,30 +0,0 @@
|
|||
const { POINTER_TO_DISPLAY } = require('./machine.config');
|
||||
const { num2hex } = require('./logging.js');
|
||||
|
||||
/**
|
||||
* Print the contents of display memory as hex number
|
||||
* @param {Uint8Array} mem - CPU memory
|
||||
**/
|
||||
const printDisplay = (mem) => {
|
||||
const disp = mem[POINTER_TO_DISPLAY];
|
||||
for (let i = disp; i < disp + 16; i += 4) {
|
||||
console.log(`${num2hex(mem[i])} ${num2hex(mem[i+1])} ${num2hex(mem[i+2])} ${num2hex(mem[i+3])}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the contents of display memory using black and white emoji circles
|
||||
* @param {Uint8Array} mem - CPU memory
|
||||
**/
|
||||
const prettyPrintDisplay = (mem) => {
|
||||
const disp = mem[POINTER_TO_DISPLAY];
|
||||
const num2pic = (n) => n > 0 ? '⚫' : '⚪';
|
||||
for (let i = disp; i < disp + 16; i += 4) {
|
||||
console.log(`${num2pic(mem[i])}${num2pic(mem[i+1])}${num2pic(mem[i+2])}${num2pic(mem[i+3])}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
"printDisplay": printDisplay,
|
||||
"prettyPrintDisplay": prettyPrintDisplay,
|
||||
}
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const Opter = require('./opter/opter.js');
|
||||
const { logMemory } = require('./logging.js');
|
||||
const { num2hex, hex2num, bin2num } = require('./conversions.js');
|
||||
const DBG = require('./dbg.js');
|
||||
|
||||
const CFG = require('./machine.config.js');
|
||||
|
||||
|
||||
/** Configure pseudo-ops **/
|
||||
const ASM_IP_LABEL = '*';
|
||||
const ASM_CONSTANT_PREFIX = '#';
|
||||
const ASM_LABEL_PREFIX = '@';
|
||||
|
||||
/** Configure mnemonics **/
|
||||
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.nit(` in: ${line}`);
|
||||
let info = {
|
||||
number: index + 1,
|
||||
source: line,
|
||||
sanitized: stripWhitespaceFromEnds(stripComments(line)),
|
||||
type: getLineType(line),
|
||||
};
|
||||
dbg.nit(` → ${info.number} - ${info.type}: ${info.sanitized}`);
|
||||
dbg.nit(``);
|
||||
|
||||
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("$", ""));
|
||||
if (arg.startsWith("0x")) return hex2num(arg.replace("0x", ""));
|
||||
if (arg.startsWith("0b")) return bin2num(arg.replace("0b", ""));
|
||||
return parseInt(arg);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} op
|
||||
* @param {object} labels // TODO - document labels object
|
||||
* @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.nit(` Label definition:`);
|
||||
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
|
||||
dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`);
|
||||
dbg.nit(` IP: $${num2hex(IP)}, new code: none`);
|
||||
dbg.nitGroupEnd('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.nit('');
|
||||
dbg.nit(`Constants:`);
|
||||
dbg.nit(constants);
|
||||
dbg.nit('');
|
||||
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 {{ sourceAnnotations: Object, machineCode: Array }};
|
||||
**/
|
||||
// TODO rename?
|
||||
function decodeInstructions(source) {
|
||||
dbg.nit('Pre-parsing...');
|
||||
let lines = preparseSourceCode(source);
|
||||
dbg.nit('');
|
||||
dbg.nit('Done pre-parsing.');
|
||||
dbg.nit('');
|
||||
dbg.nit('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 = CFG.initialIP;
|
||||
}
|
||||
|
||||
// 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 sourceAnnotations = {};
|
||||
|
||||
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
|
||||
machineCode[CFG.pointerToDisplay] = CFG.displayAddr;
|
||||
|
||||
// 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.nit(`'${label}' already in labels object`);
|
||||
labels[label].bytesToReplace.push(IP + 1);
|
||||
} else {
|
||||
dbg.nit(`'${label}' NOT in labels object`);
|
||||
labels[label] = {
|
||||
bytesToReplace: [IP + 1],
|
||||
};
|
||||
}
|
||||
dbg.nit(`Label reference:`);
|
||||
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
|
||||
dbg.nit(` 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.nit(` 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.nit(`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.nit(`(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;
|
||||
|
||||
sourceAnnotations[IP] = {
|
||||
lineNumber: line.number,
|
||||
source: line.source,
|
||||
address: IP,
|
||||
machine: [decodedOp, decodedArg]
|
||||
};
|
||||
|
||||
dbg.i();
|
||||
dbg.i(`Line ${line.number}: ${line.source}`);
|
||||
if (line.argument) {
|
||||
dbg.i(` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`);
|
||||
} else if (line.operation) {
|
||||
dbg.i(` Asm operation: ${line.operation.toUpperCase()}`);
|
||||
}
|
||||
|
||||
dbg.i(` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`);
|
||||
dbg.i(` IP: $${num2hex(IP)}`);
|
||||
IP += 2;
|
||||
};
|
||||
}
|
||||
|
||||
dbg.nit('');
|
||||
dbg.nitGroup('Memory before filling in label constants');
|
||||
dbg.nitExec(() => logMemory(new Uint8Array(machineCode)));
|
||||
dbg.nitGroupEnd();
|
||||
|
||||
// Backfill label references
|
||||
for (let k of Object.keys(labels)) {
|
||||
dbg.nitGroup(`${ASM_LABEL_PREFIX}${k}`);
|
||||
let label = labels[k];
|
||||
dbg.nit(`Points to byte: ${label.pointsToByte}`);
|
||||
dbg.nit(`Bytes to replace: ${label.bytesToReplace}`);
|
||||
dbg.nitGroupEnd();
|
||||
for (let j = 0; j < label.bytesToReplace.length; j++) {
|
||||
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
|
||||
}
|
||||
}
|
||||
|
||||
return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (includeMetadata) {
|
||||
const debugJSON = JSON.stringify(out);
|
||||
if (outputToFile) {
|
||||
fs.writeFileSync(outputFilename, debugJSON);
|
||||
} 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,
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
const { num2hex } = require('./conversions.js');
|
||||
|
||||
module.exports = class CPU {
|
||||
|
||||
/**
|
||||
* @arg {number} initialIP
|
||||
**/
|
||||
constructor(initialIP, cycleLimit) {
|
||||
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;
|
||||
|
||||
this._cycleLimit = cycleLimit;
|
||||
|
||||
this.dbg = {
|
||||
sourceInfo: null,
|
||||
currentMnemonic: null,
|
||||
previousIP: initialIP,
|
||||
cycleCounter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Public interface **/
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} machineCode
|
||||
**/
|
||||
loadMemory(machineCode) {
|
||||
this.memory = new Uint8Array(256);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @arg {number} n1
|
||||
* @arg {number} n2
|
||||
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
|
||||
**/
|
||||
function differenceCarryOverflow(n1, n2) {
|
||||
// https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
|
||||
// > SBC simply takes the ones complement of the second value and then performs an ADC.
|
||||
//
|
||||
// https://stackoverflow.com/a/8966863
|
||||
// > 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.
|
||||
return sumCarryOverflow(n1, -n2);
|
||||
}
|
||||
|
|
@ -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();
|
||||
*/
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
let { num2hex } = require('./conversions.js');
|
||||
|
||||
/**
|
||||
* 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( `└─────────────────────┘`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
"logMemory": logMemory,
|
||||
"logRunningHeader": logRunningHeader,
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
module.exports = {
|
||||
"INITIAL_IP_ADDRESS": 48,
|
||||
"initialIP": 29,
|
||||
|
||||
// Use these in CPU:
|
||||
"DISPLAY_ADDR": 0,
|
||||
"KEYPAD_ADDR": 32,
|
||||
// Store the `X_ADDR`s at these addresses when assembling:
|
||||
"POINTER_TO_DISPLAY": 33,
|
||||
"POINTER_TO_KEYPAD": 34,
|
||||
"displayAddr": 0,
|
||||
"keypadAddr": 27,
|
||||
// Store the `DISPLAY_ADDR` at this address when assembling:
|
||||
"pointerToDisplay": 26,
|
||||
|
||||
"KEY_MAP": {
|
||||
"keyMap": {
|
||||
// Same layout as COSMAC VIP / CHIP-8
|
||||
// (This object maps qwerty keys to hex keys
|
||||
// so that they are arranged in the same layout
|
||||
|
|
@ -17,10 +16,16 @@ module.exports = {
|
|||
'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
|
||||
"CYCLE_LIMIT": 256,
|
||||
"defaultCycleLimit": 2048,
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "paper-computer",
|
||||
"name": "cardiograph",
|
||||
"scripts": {
|
||||
"jsdoc": "./node_modules/.bin/jsdoc"
|
||||
},
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# Cardiograph Mark I — simulator for an imaginary computer
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
- Node.js
|
||||
|
||||
|
||||
### Quick examples
|
||||
|
||||
Assemble and run:
|
||||
```./assembler.js -i <source.asm> | ./cardiograph.js```
|
||||
|
||||
Assemble to a file:
|
||||
```./assembler.js -i <source.asm> -o <machinecode.out>```
|
||||
|
||||
Run from a file:
|
||||
```./cardiograph.js -i <machinecode.out>```
|
||||
|
||||
|
||||
### Assembler: assembler.js
|
||||
|
||||
```
|
||||
Usage: ./assembler.js [-a] -i <input-file> [-o <output-file>]
|
||||
|
||||
-a, --annotate Output code with debugging annotations
|
||||
-i, --in <file> Assembly-language input
|
||||
-o, --out <file> Machine-code output
|
||||
```
|
||||
|
||||
- If an output file is not provided, the output is printed to stdout
|
||||
|
||||
- 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
|
||||
|
||||
### Registers and Flags
|
||||
|
||||
There are three registers:
|
||||
|
||||
1. **A**, an 8-bit accumulator
|
||||
2. **IP**, an 8-bit instruction pointer (aka program counter)
|
||||
3. **flags**, a 4-bit flag register
|
||||
|
||||
The four flags are **O**verflow, **N**egative, **Z**ero, and **C**arry.
|
||||
|
||||
(Overflow is the high bit and carry is the low bit.)
|
||||
|
||||
In decimal:
|
||||
|
||||
| O | N | Z | C |
|
||||
|---|---|---|---|
|
||||
| 3 | 2 | 1 | 0 |
|
||||
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Start-up
|
||||
|
||||
When starting up, the CPU executes a `JMP $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
|
||||
|
||||
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
|
||||
|
||||
- Prefix hexadecimal numbers with `$` (or `0x`)
|
||||
- Prefix binary numbers with `0b`
|
||||
- Whitespace is ignored
|
||||
|
||||
## Cardiograph memory map
|
||||
|
||||
| Address | Used for... |
|
||||
|----------|-----------------------------------------------|
|
||||
| 00 to 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 to FE | free |
|
||||
| FF | * ROM (unwriteable) pointer to initial IP |
|
||||
|
||||
\* Not implemented yet
|
||||
|
||||
|
||||
## Cardiograph 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`
|
||||
|
||||
The arrow keys are also mapped onto the hex keypad:
|
||||
|
||||
` ` `5` ` ` = ` ` `↑` ` `
|
||||
`7` `8` `9` = `←` `↓` `→`
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
; Routine for drawing at (x, y) coordinates
|
||||
|
||||
#zeroflag 1
|
||||
|
||||
; *** Set your desired (x, y) here: ***
|
||||
#input_x 2
|
||||
#input_y 2
|
||||
|
||||
; Set up some handy shortcuts
|
||||
#x $FA
|
||||
#y $FB
|
||||
#return_addr_ptr $FE
|
||||
|
||||
|
||||
; Main:
|
||||
|
||||
LDA #input_x
|
||||
STO #x
|
||||
LDA #input_y
|
||||
STO #y
|
||||
LDA * 6 ; acc = current address + 6 (LDA, STO, JMP = 6)
|
||||
STO #return_addr_ptr
|
||||
JMP @getxy
|
||||
|
||||
LDA $FF
|
||||
STO ($FD)
|
||||
END
|
||||
|
||||
;; Convert a pair of (x, y) coords
|
||||
;; to the address of a pixel on the display
|
||||
;;
|
||||
;; Call with:
|
||||
;; - x in #x
|
||||
;; - y in #y
|
||||
;; - return address in #return_addr_ptr
|
||||
;;
|
||||
;; Returns: pixel address in $FD
|
||||
@getxy
|
||||
#gxy_px $FD
|
||||
|
||||
; stash x...
|
||||
LDA (#x)
|
||||
STO #gxy_px
|
||||
|
||||
; check if this is row 0...
|
||||
LDA (#y)
|
||||
FHP #zeroflag
|
||||
JMP @getxy_loop
|
||||
JMP (#return_addr_ptr) ; if row 0, we're done
|
||||
|
||||
@getxy_loop
|
||||
LDA (#gxy_px)
|
||||
ADD 5 ; add 5 to get to the next row
|
||||
STO #gxy_px
|
||||
LDA (#y) ; decrement y (it's acting as a loop counter)...
|
||||
SUB 1
|
||||
STO #y
|
||||
FHP #zeroflag
|
||||
JMP @getxy_loop
|
||||
JMP (#return_addr_ptr)
|
||||
|
||||
;; Main variables:
|
||||
;; F8
|
||||
;; F9
|
||||
;; FA - x coord
|
||||
;; FB - y coord
|
||||
;; FC - gxy temp
|
||||
;; FD - gxy temp
|
||||
;; FE - Return address for subroutine
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
;; Fill display with $FF
|
||||
; updated for 5x5 display
|
||||
|
||||
#Zero 1
|
||||
#px_ptr $F0
|
||||
#fill $F1
|
||||
|
||||
; Initialize variables...
|
||||
LDA $00 ; (Address for the first px on the display)
|
||||
STO #px_ptr ; Pointer to current px
|
||||
LDA $FF ; ($FF is 'on', $00 is 'off')
|
||||
STO #fill ; Stash value to fill with
|
||||
|
||||
@paint
|
||||
LDA (#fill) ; (A = mem[fill] = $FF)
|
||||
STO (#px_ptr); Paint pixel (mem[mem[*px]] = A = $FF)
|
||||
|
||||
LDA (#px_ptr) ; Increment pixel pointer...
|
||||
ADD $01
|
||||
STO #px_ptr
|
||||
|
||||
LDA (#px_ptr) ; Test whether to loop or not...
|
||||
SUB $19 ; if *px - $19 == 0, we've reached the end
|
||||
FHP #Zero
|
||||
JMP @paint
|
||||
END
|
||||
|
|
@ -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
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
#LOOPCOUNT $80
|
||||
|
||||
#Z 2 ; the zero flag is #2
|
||||
#Z 1 ; the zero flag is #1
|
||||
#keypad $20 ; magic memory location containing latest key pressed
|
||||
#loopIter $FF ; address of loop iterator
|
||||
#iterPx $03 ; where to display iterator
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
;; Conway's Game of Life
|
||||
; n loewen & Elizabeth Pankratz
|
||||
; 2023-08-23 -
|
||||
|
||||
; Flag numbers for easier reference
|
||||
#Carry 0
|
||||
#Zero 1
|
||||
|
||||
#live_colour $FF
|
||||
#dead_colour $00
|
||||
|
||||
#top_left $00
|
||||
#top_right $04
|
||||
#bot_left $14
|
||||
#bot_right $18
|
||||
|
||||
#px_ptr $00
|
||||
#live_neighbours_ptr $FF
|
||||
|
||||
; start of code
|
||||
|
||||
* $1D
|
||||
|
||||
@loop
|
||||
LDA (#px_ptr)
|
||||
|
||||
; do something...
|
||||
|
||||
; increment pixel pointer
|
||||
LDA (#px_ptr)
|
||||
STA #px_ptr
|
||||
JMP @loop
|
||||
|
||||
@check_for_tl_corner
|
||||
LDA (#px_ptr)
|
||||
; choose a memory location to stash result. 0=false, 1=true
|
||||
HOP
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
; Draw a pixel, and move it when a key is pressed
|
||||
; 2023-08-26
|
||||
|
||||
#flagZ 1
|
||||
#flagN 2
|
||||
#keypad $1B ; contains latest key pressed
|
||||
|
||||
; Starting (x, y) coordinates
|
||||
#input_x 0
|
||||
#input_y 0
|
||||
|
||||
; Some handy shortcuts
|
||||
#x $FA
|
||||
#y $FB
|
||||
#px_addr $FD ; holds return value from @xy2id
|
||||
#return_addr_ptr $FE
|
||||
|
||||
; Main variables:
|
||||
; F8
|
||||
; F9 - xy2id temp
|
||||
; FA - x coord
|
||||
; FB - y coord
|
||||
; FC - xy2id temp
|
||||
; FD - xy2id return value / xy2id temp
|
||||
; FE - Return address for subroutine
|
||||
|
||||
|
||||
@setup
|
||||
LDA #input_x
|
||||
STO #x
|
||||
LDA #input_y
|
||||
STO #y
|
||||
LDA @update
|
||||
STO #return_addr_ptr
|
||||
JMP @xy2id
|
||||
|
||||
|
||||
@update
|
||||
; draw pixel
|
||||
LDA $FF
|
||||
STO (#px_addr)
|
||||
|
||||
; determine direction
|
||||
#up 5
|
||||
#left 7
|
||||
#down 8
|
||||
#right 9
|
||||
|
||||
; test up
|
||||
lda (#keypad)
|
||||
sub #up
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @up
|
||||
|
||||
; test left
|
||||
lda (#keypad)
|
||||
sub #left
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @left
|
||||
|
||||
; test right
|
||||
lda (#keypad)
|
||||
sub #right
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @right
|
||||
|
||||
; test down
|
||||
lda (#keypad)
|
||||
sub #down
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @down
|
||||
|
||||
;; no key pressed...
|
||||
jmp @stay_put
|
||||
|
||||
|
||||
@up
|
||||
lda (#y)
|
||||
sub 1
|
||||
ftg #flagN
|
||||
fhp #flagN
|
||||
jmp @stay_put
|
||||
sto #y
|
||||
jmp @xy2id
|
||||
|
||||
@left
|
||||
lda (#x)
|
||||
sub 1
|
||||
ftg #flagN
|
||||
fhp #flagN
|
||||
jmp @stay_put
|
||||
sto #x
|
||||
jmp @xy2id
|
||||
|
||||
@right
|
||||
lda (#x)
|
||||
sub 4
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @stay_put
|
||||
lda (#x)
|
||||
add 1
|
||||
sto #x
|
||||
jmp @xy2id
|
||||
|
||||
@down
|
||||
lda (#y)
|
||||
sub 4
|
||||
ftg #flagZ
|
||||
fhp #flagZ
|
||||
jmp @stay_put
|
||||
lda (#y)
|
||||
add 1
|
||||
sto #y
|
||||
jmp @xy2id
|
||||
|
||||
@stay_put
|
||||
; draw pixel
|
||||
LDA $FF
|
||||
STO (#px_addr)
|
||||
; TODO
|
||||
; END
|
||||
|
||||
|
||||
;; Convert a pair of (x, y) coords
|
||||
;; to the address of a pixel on the display
|
||||
;;
|
||||
;; Call with:
|
||||
;; - x in #x
|
||||
;; - y in #y
|
||||
;; - return address in #return_addr_ptr
|
||||
;;
|
||||
;; Returns:
|
||||
;; - pixel address in #px_addr
|
||||
@xy2id
|
||||
; stash x, y...
|
||||
#xy2id_y $FC
|
||||
#xy2id_x $F9
|
||||
LDA (#y)
|
||||
STO #xy2id_y
|
||||
LDA (#x)
|
||||
STO #xy2id_x
|
||||
STO #px_addr
|
||||
|
||||
; check if this is row 0...
|
||||
LDA (#xy2id_y)
|
||||
FHP #flagZ
|
||||
JMP @xy2id_loop
|
||||
JMP (#return_addr_ptr) ; if row 0, we're done
|
||||
|
||||
@xy2id_loop
|
||||
LDA (#px_addr)
|
||||
ADD 5 ; add 5 to get to the next row
|
||||
STO #px_addr
|
||||
LDA (#xy2id_y) ; decrement y (it's acting as a loop counter) ...
|
||||
SUB 1
|
||||
STO #xy2id_y
|
||||
FHP #flagZ
|
||||
JMP @xy2id_loop
|
||||
JMP (#return_addr_ptr)
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
;; Test referencing address of line being assembled
|
||||
|
||||
NOP ; Push the const below to a later address
|
||||
#initAddr *ADDR
|
||||
* 30
|
||||
|
||||
LDA *ADDR
|
||||
NOP ; Push the const below to a later address
|
||||
#initAddr *
|
||||
|
||||
LDA *
|
||||
STO $25
|
||||
FHP 0 ; hop if carry set
|
||||
JMP @setCarry
|
||||
83
logging.js
83
logging.js
|
|
@ -1,83 +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) => {
|
||||
// This function can only handle
|
||||
// an even number of array entries
|
||||
if ((start % 2) === 1) { start -= 1; }
|
||||
if ((end % 2) === 1) { end += 1; }
|
||||
|
||||
mem = mem.slice(start, end);
|
||||
console.log(`┌────────┬────────┬─────────┐`);
|
||||
console.log(`│ addr │ opcode │ operand │`);
|
||||
console.log(`├────────┼────────┼─────────┤`);
|
||||
//for (let i = 0; i < mem.length; i += 2) {
|
||||
for (let i = start; i < mem.length; i +=2) {
|
||||
console.log(`│ ${num2hex(i)} │ ${num2hex(mem[i])} │ ${num2hex(mem[i+1])} │`);
|
||||
|
||||
// Add a blank row every 4 lines:
|
||||
if (((i + 2) % 8) === 0) {
|
||||
if ((i < (mem.length - 2))) {
|
||||
console.log(`│ │ │ │`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`└────────┴────────┴─────────┘`);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
module.exports = {
|
||||
"logMemory": logMemory,
|
||||
"logRunningHeader": logRunningHeader,
|
||||
"num2hex": num2hex,
|
||||
"hex2num": hex2num,
|
||||
"num2bin": num2bin,
|
||||
"num2bin_4bit": num2bin_4bit,
|
||||
"bin2num": bin2num,
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Dev notes — 2023-08-05
|
||||
|
||||
- [ ] consider refactoring assembler to do something more like 'tokenize, then call helper functons'
|
||||
|
||||
- [ ] consider an extended system:
|
||||
- add a byte in the ~zero-page to act as a flag for display mode
|
||||
- 0 = 1 byte per pixel (default)
|
||||
- 1 = 1 bit per pixel
|
||||
- add another 16 ops (or fewer)
|
||||
- rotate left / right (or shift?)
|
||||
- AND
|
||||
- OR
|
||||
- more flags?
|
||||
- another register?
|
||||
- would require several new ops
|
||||
- add binary input/output to assembler
|
||||
|
||||
- consider gamepad vs. hex keypad
|
||||
- stick with hex
|
||||
- but permit gamepad since that's a subset (NES layout = 8 bits/bytes, depending on mode)
|
||||
- look at how uxn does it?
|
||||
|
||||
- [ ] rewrite to call things 'opcodes' and 'operands'
|
||||
|
||||
- add bank switching for higher 128 bytes
|
||||
- add a flag in the ~zero-page
|
||||
|
||||
- try writing:
|
||||
- a 'greater than' routine
|
||||
|
||||
- [x] make coding forms!
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# Dev notes — 2023-08-07
|
||||
|
||||
## Carry vs overflow
|
||||
|
||||
[Understanding the difference between overflow and carry flags](https://stackoverflow.com/questions/69124873/understanding-the-difference-between-overflow-and-carry-flags)
|
||||
|
||||
> Carry indicates the result isn't mathematically correct when interpreted as unsigned, overflow indicates the result isn't mathematically correct when interpreted as signed.
|
||||
> - 1111 + 0001 = 0000 should set carry (15 + 1 = 0 is false) and clear overflow (-1 + 1 = 0 is true).
|
||||
> - 0111 + 0010 = 1001 should clear carry (7 + 2 = 9 is true) and set overflow (7 + 2 = -7 is false).
|
||||
> - 1001 + 1001 = 0010 should set both (9 + 9 = 2 and -7 + -7 = 2 are both false).
|
||||
|
||||
so <mark>carry is unsigned</mark>
|
||||
and <mark>overflow is signed</mark>
|
||||
|
||||
(which is what i've got, good)
|
||||
|
||||
## add more flags + change flag ops
|
||||
|
||||
### flags register
|
||||
|
||||
- [ ] Replace the current 'Carry Flag' with a Flags Register.
|
||||
|
||||
Here's a sketch for the bit pattern:
|
||||
|
||||
```
|
||||
hi bit
|
||||
|
||||
0
|
||||
0
|
||||
0
|
||||
0
|
||||
|
||||
0 ? negative
|
||||
0 ? zero
|
||||
0 overflow
|
||||
0 carry
|
||||
|
||||
lo bit
|
||||
```
|
||||
|
||||
cf. 6502:
|
||||
|
||||
- NV-BDIZC
|
||||
- 7: Negative, 6: Overflow, 5: none, 4: Break, 3: Decimal, 2: Interrupt disable, 1: Zero, 0: Carry
|
||||
|
||||
### flag opcodes
|
||||
|
||||
- [ ] replace `CHP` and `CFC` with `FHP` and `FTG`
|
||||
|
||||
- `FHP n`: hop if flag _n_ is set
|
||||
- eg: `FHP 0` = hop if carry flag set
|
||||
- eg: `FHP 1` = hop if overflow flag set
|
||||
- to keep it simple, we're just giving each flag a number, not fussing with bitmasking or anything
|
||||
- `FTG n`: toggle flag _n_ on/off
|
||||
- eg: if Carry is on, `FTG 0` turns it off
|
||||
- eg: if Overflow is off, `FTG 1` turns it on
|
||||
|
||||
`FHP` and `FTG` can be combined to create `set flag` and `unset flag` routines:
|
||||
|
||||
```
|
||||
@set_carry:
|
||||
FHP 0
|
||||
FTG 0
|
||||
```
|
||||
If Carry is on when this is called, then `FTG` is skipped and Carry remains set. Otherwise, `FTG` sets carry.
|
||||
|
||||
```
|
||||
; call with a return address stored at $01
|
||||
|
||||
@unset_carry:
|
||||
FHP 0 ; 1
|
||||
FHP 0 ; 2
|
||||
FTG 0 ; 3
|
||||
JMP ($01) ; jump back to caller
|
||||
```
|
||||
If Carry is on when this is called, then the execution is: 1, 3, and Carry is turned off.
|
||||
|
||||
If Carry is off, then the execution is: 1, 2, (hop over 3; Carry is still off), jump back to caller.
|
||||
|
||||
## Think about a subroutine stack?
|
||||
|
||||
Maybe?
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
# Dev notes — 2023-08-07
|
||||
|
||||
## Keypad
|
||||
|
||||
An actual hexadecimal layout:
|
||||
|
||||
```
|
||||
0 1 2 3
|
||||
4 5 6 7
|
||||
8 9 A B
|
||||
C D E F
|
||||
```
|
||||
|
||||
Another:
|
||||
|
||||
```
|
||||
1 2 3 A
|
||||
4 5 6 B
|
||||
7 8 9 C
|
||||
0 F E D
|
||||
```
|
||||
|
||||
The conventional layout for cheap hardware:
|
||||
|
||||
```
|
||||
1 2 3 A
|
||||
4 5 6 B
|
||||
7 8 9 C
|
||||
* 0 # D
|
||||
```
|
||||
|
||||
Kim-1:
|
||||
|
||||
```
|
||||
GO ST RS
|
||||
AD DA PC +
|
||||
C D E F
|
||||
8 9 A B
|
||||
4 5 6 7
|
||||
0 1 2 3
|
||||
```
|
||||
|
||||
COSMAC VIP/CHIP-8:
|
||||
|
||||
```
|
||||
1 2 3 C
|
||||
4 5 6 D
|
||||
7 8 9 E
|
||||
A 0 B F
|
||||
```
|
||||
|
||||
CHIP-8 to QWERTY mapping:
|
||||
|
||||
```
|
||||
1 2 3 4
|
||||
Q W E R
|
||||
A S D F
|
||||
Z X C V
|
||||
```
|
||||
|
||||
## Turtle graphics
|
||||
|
||||
- yesterday Elizabeth had the great idea to create a turtle robot to go with the paper computer
|
||||
- [ ] a minimal LOGO would be a fantastic (if challenging) program to write for the computer
|
||||
- it could use the hex keypad:
|
||||
- using CHIP-8 layout...
|
||||
- A key: mode toggle
|
||||
- mode 1: numeric
|
||||
- mode 2: commands (tokens?)... something like this:
|
||||
- `F` - mode toggle
|
||||
- `2` - forward
|
||||
- `4` - left
|
||||
- `6` - right
|
||||
- `0` - reverse
|
||||
- `5` - turn
|
||||
- `1` - pen up
|
||||
- `C` - pen down
|
||||
|
||||
## Dot-matrix display
|
||||
|
||||
- [ ] Maybe try 5x5, like the micro:bit?
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Dev notes — 2023-08-10
|
||||
|
||||
- [x] Name it "Cardiograph"
|
||||
- homage to CARDIAC
|
||||
- (and cardboard)
|
||||
- and the special feature is that unlike other paper computers, this one has a graphical display
|
||||
|
||||
- art note: "performing computation"
|
||||
|
||||
## Keypad (and memory map)
|
||||
|
||||
- [ ] Copy the CHIP-8 approach: one memory location that stores the current key pressed
|
||||
- (Only one key can be pressed at a time)
|
||||
- And then do a bitmask-y thing for the secret advanced mode, to add the ability to detect multiple simultaneous keypresses
|
||||
|
||||
## Display (and memory map)
|
||||
|
||||
- Move display to $10?
|
||||
- Then $00 could contain a jump to the start of code, and we wouldn't have this strange "IP doesn't start at 0" situation
|
||||
- But, this might feel more complicated, and it would make working with the display a little less elegant...
|
||||
- C64 has random stuff at $0000
|
||||
- How does the C64's PC get initialized ??
|
||||
|
||||
## (Moved from readme:) Nice features that didn't fit
|
||||
|
||||
- Hop `IF<` and hop `IF>`
|
||||
- `MUL` and `DIV`
|
||||
- Rotates and shifts
|
||||
|
||||
## (Moved from readme:) Possible features, maybe someday
|
||||
|
||||
- Timer (for a version in software/electronic-hardware)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# Dev notes — 2023-08-12
|
||||
|
||||
Brainstorming/sketching around subroutines with a return stack...
|
||||
|
||||
; need an instruction for IP → A i guess?
|
||||
; ideally…
|
||||
; but a jump table would work
|
||||
; put that at beginning of the code
|
||||
; then store numbers for subroutine labels in a designated memory slot
|
||||
|
||||
lda $1
|
||||
sto $19 ; contains ID # for the next fn to jump to
|
||||
|
||||
@jump_table
|
||||
hop $1
|
||||
jmp @jt2
|
||||
jmp @example_computation
|
||||
@jt2
|
||||
hop $2
|
||||
; jmp @jt3
|
||||
nop
|
||||
; etc …
|
||||
jmp @end
|
||||
|
||||
@example_computation
|
||||
lda 5
|
||||
sto $20
|
||||
lda 3
|
||||
sto $21
|
||||
; $19 still has the # for this routine
|
||||
; but let’s pretend it doesn’t and demonstrate updating it
|
||||
lda $1
|
||||
sto $19
|
||||
jmp @greater?
|
||||
|
||||
; call with numbers to test in $20 and $21
|
||||
; result is stored in acc
|
||||
@greater?
|
||||
; lda ($20)
|
||||
; sub ($21)
|
||||
; todo…
|
||||
; wouldn’t it be great to have a “hop if neg” op…
|
||||
; do we have to just subtract numbers until we get 0?
|
||||
|
||||
; no!
|
||||
; here’s an approach that’s at least better than that
|
||||
lda ($21)
|
||||
sto $22 ; stash
|
||||
@loop
|
||||
lda ($21)
|
||||
sub $1
|
||||
sto $22 ; stash
|
||||
sub ($20)
|
||||
hop $0
|
||||
jmp @loop
|
||||
sto $1
|
||||
jmp $jmp_table
|
||||
; ok this isn’t quite it… we also need to chexk if we hit 0 by just deceementinf and if so retuen 0
|
||||
|
||||
jmp @jump_table
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# Dev notes — 2023-08-15
|
||||
|
||||
## Goals for today
|
||||
|
||||
- [x] Review planned changes to simulator
|
||||
- [x] 'opcodes' and 'operands'
|
||||
- [x] fix $00 contains $20 bug
|
||||
|
||||
- [x] Review planned changes to the system
|
||||
- [x] CHP, CFC -> FHP, FTG -- dev note 2023-08-07
|
||||
- [/] bank-switching flag in 0 page
|
||||
- added notes below, but decided to leave implementation for another day
|
||||
- [x] ? 5x5 display
|
||||
|
||||
- [/] Implement any changes necessary for writing a program?
|
||||
- [-] Write a program
|
||||
- [-] LOGO turtle on 5x5?
|
||||
|
||||
## Overflow flag
|
||||
|
||||
Ken Shirriff, [The 6502 overflow flag explained mathematically](https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html):
|
||||
|
||||
> A common definition of overflow is `V = C6 xor C7`. That is, overflow happens if the carry into bit 7 is different from the carry out.
|
||||
|
||||
## Bank switching
|
||||
|
||||
### Planned memory map
|
||||
|
||||
- `00-0F` - display (4x4)
|
||||
- `10-1F` - keypad? (details TBD)
|
||||
- `20 ` - pointer to display memory
|
||||
- `21 ` - pointer to keypad memory
|
||||
- `22 ` - pointer to memory bank
|
||||
- `23-2F` - reserved for future use / variable storage
|
||||
- `30 ` - initial value for IP
|
||||
- `30-80` - free
|
||||
- `80-FF` - free, can be bank-switched
|
||||
|
||||
## Looping using an interval timer
|
||||
|
||||
const loop = setInterval(async () => {
|
||||
step = step + 1;
|
||||
// Temporary limit as a lazy way to halt infinite loops:
|
||||
if (CYCLE_LIMIT && (step > CYCLE_LIMIT)) {
|
||||
console.log('SIMULATION HALTING - reached cycle limit');
|
||||
clearInterval(loop);
|
||||
}
|
||||
if (!CPU.running) clearInterval(loop);
|
||||
if (CPU.IP >= CPU.memory.length) clearInterval(loop);
|
||||
stepCPU();
|
||||
await logCPUState(debug);
|
||||
}, frameRate);
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Dev notes — 2023-08-16
|
||||
|
||||
## Goals for today
|
||||
|
||||
- [x] Finish implementing *ADDR
|
||||
- [x] Rename =constants to #constants
|
||||
- [ ] ? Bank switching
|
||||
- [ ] Notes re: ROM and/or tape loader
|
||||
- [x] CPU updates
|
||||
- [x] Rename to CPU
|
||||
- [x] Implement single-stepping
|
||||
- [x] Implement keypad input
|
||||
- [-] Look at KIM-1 and VIP buttons for memory editing
|
||||
- [x] Rename to 'Cardiograph Mark I' (a 'Harvard Mark I' reference, plus a dumb drawing joke)
|
||||
|
||||
- Programming ideas:
|
||||
- Draw dot at (x, y)
|
||||
- Move dot around display using keypad
|
||||
- simple LOGO
|
||||
|
||||
## Misc. earlier notes
|
||||
|
||||
- make sth that can run on phone!
|
||||
- ? rename repository
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# Bibliography
|
||||
|
||||
Entries in bold are especially influential to my work on this project so far.
|
||||
|
||||
## To-read
|
||||
|
||||
- https://www.drdobbs.com/embedded-systems/paper-to-fpga/240155922
|
||||
- LMC:
|
||||
- http://elearning.algonquincollege.com/coursemat/dat2343/lectures.f03/12-LMC.htm
|
||||
- http://www.povinelli.org/teaching/eece2710/lmc.html
|
||||
- https://web.archive.org/web/20220628132003/https://thgie.ch/notes/Paper%20Computer.html
|
||||
- https://wiki.osdev.org/Expanded_Main_Page
|
||||
- (I haven't looked at this in this context at all yet)
|
||||
- https://www.computerenhance.com/p/table-of-contents
|
||||
- https://turingcomplete.game
|
||||
|
||||
### Games
|
||||
|
||||
- https://xscx.itch.io/110
|
||||
- **https://jimhall.itch.io/toy-cpu**
|
||||
- https://annwan.itch.io/a-cpu
|
||||
- **https://nandgame.com**
|
||||
- https://tobiasvl.itch.io/flip-8
|
||||
- https://internet-janitor.itch.io/octo
|
||||
|
||||
### Machines
|
||||
|
||||
- https://en.wikipedia.org/wiki/CHIP-8
|
||||
- https://en.wikipedia.org/wiki/KIM-1
|
||||
- BBC Micro:bit
|
||||
|
||||
### 4-bits
|
||||
|
||||
- https://github.com/Subsystems-us/4-bit-Microprocessor-Trainer/blob/main/SubsySTEM2_Manual_rev01.pdf
|
||||
- https://blog.lapinozz.com/learning/2016/11/19/calculator-with-caordboard-and-marbles.html
|
||||
- https://jacobsweeten.github.io/4-Bit-Computer/
|
||||
- **CHUMP**
|
||||
- http://darcy.rsgc.on.ca/ACES/TEI4M/4BitComputer/index.html
|
||||
- https://www.youtube.com/watch?app=desktop&v=b5qDwCN9Q2c
|
||||
|
||||
|
||||
### Misc./To-sort
|
||||
|
||||
- https://retrocomputingforum.com/t/some-mechanical-and-optical-curiosities/1598/5
|
||||
- "Coloring computers"
|
||||
- **"NAND to Tetris" / Elements of Computing Systems**
|
||||
- **Charles Petzold, _Code_.**
|
||||
- **Mark Jones Lorenzo, _The Paper Computer Unfolded: A Twenty-First Century Guide to the Bell Labs CARDIAC (CARDboard Illustrative Aid to Computation), the LMC (Little Man Computer), and the IPC (Instructo Paper Computer)_ (self published, 2017).**
|
||||
|
||||
## Implementation reference
|
||||
|
||||
### Assembler design
|
||||
|
||||
- https://stackoverflow.com/questions/10244422/how-is-a-2-pass-assembler-different-from-a-one-pass-assembler-in-resolving-the-f
|
||||
- https://gear.kku.ac.th/~watis/courses/188231/sp2-4.pdf
|
||||
|
||||
### Javascript
|
||||
|
||||
- https://devhints.io/jsdoc
|
||||
- https://stackoverflow.com/questions/25354313/saving-a-uint8array-to-a-binary-file
|
||||
|
||||
|
||||
## Temporary(?) references
|
||||
|
||||
- [Putting the “You” in CPU](https://cpu.land)
|
||||
- on HN: https://news.ycombinator.com/item?id=36823605
|
||||
- "These projects are really fun. On the other hand, you might want to learn in a way that lets you build hardware (esp for FPGA's). For that, I suggest a few types of books with examples:
|
||||
..." https://news.ycombinator.com/item?id=36825693
|
||||
|
||||
- https://stackoverflow.com/questions/29193303/6502-emulation-proper-way-to-implement-adc-and-sbc
|
||||
- http://6502.org/tutorials/6502opcodes.html
|
||||
|
||||
|
||||
## Learning about CPUs
|
||||
|
||||
- http://skilldrick.github.io/easy6502/
|
||||
- [Beagle Bros "6502 instruction reference"](https://raw.githubusercontent.com/camsaul/nesasm/master/beagle_bros_6502_reference.png)
|
||||
|
||||
## Instructional/toy computers
|
||||
|
||||
### Paper based
|
||||
|
||||
- **https://en.wikipedia.org/wiki/CARDboard_Illustrative_Aid_to_Computation**
|
||||
- <3
|
||||
- **https://en.wikipedia.org/wiki/Little_man_computer**
|
||||
- **https://en.wikipedia.org/wiki/WDR_paper_computer**
|
||||
|
||||
### Mechanical
|
||||
|
||||
- Paperclip Computer
|
||||
- https://hackaday.com/2020/02/08/a-modern-take-on-the-paperclip-computer/
|
||||
- https://en.wikipedia.org/wiki/Digi-Comp_I
|
||||
- https://en.wikipedia.org/wiki/Digi-Comp_II
|
||||
- https://en.wikipedia.org/wiki/Turing_Tumble
|
||||
- https://en.wikipedia.org/wiki/Dr._Nim
|
||||
- https://en.wikipedia.org/wiki/Geniac
|
||||
|
||||
### Virtual machine
|
||||
|
||||
- https://wiki.xxiivv.com/site/uxn.html
|
||||
- <3
|
||||
|
||||
### Computer games
|
||||
|
||||
- https://en.wikipedia.org/wiki/TIS-100
|
||||
- https://en.wikipedia.org/wiki/Human_Resource_Machine
|
||||
- I haven't played this one
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# To do
|
||||
|
||||
## Research
|
||||
|
||||
- [ ] Learn how the C64's PC gets initialized (what is the initial value? how is it set?)
|
||||
- [ ] 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
|
||||
- [ ] Play with JSDoc
|
||||
|
||||
## Design
|
||||
|
||||
- [ ] Add a flag for bank-switching to the ~zero-page
|
||||
- [ ] Move the initial IP value, to eliminate the gap between it and the keypad pointer
|
||||
- [ ] Consider adding a VIP-style keypad-based machine code monitor
|
||||
|
||||
### For consideration
|
||||
|
||||
- [ ] Subroutine stack
|
||||
- [ ] [Extended system (secret bonus operations)](2023-08-07--dev-notes.md)
|
||||
|
||||
## Programming
|
||||
|
||||
- [ ] Write some more complex test programs
|
||||
- [ ] Display (hex) numbers
|
||||
- [ ] Greater than
|
||||
- [ ] Minimal LOGO-ish interpreter for turtle graphics
|
||||
|
||||
## Run-scripts
|
||||
|
||||
- [ ] Replace with running `./cpu.js` and `./assembler.js` diretly
|
||||
|
||||
## Simulator
|
||||
|
||||
### Assembler
|
||||
|
||||
- [ ] Validate labels
|
||||
- [ ] Return pure machine code when printing to stdout (and not in debug mode)
|
||||
|
||||
### CPU
|
||||
|
||||
- [ ] Add a mode that prints the display as text, but still animates
|
||||
|
||||
- [ ] Make single-stepping work with simulated keypad
|
||||
|
||||
- [ ] Allow running pre-compiled machine code
|
||||
|
||||
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
|
||||
|
||||
|
||||
### Possible under-the-hood improvements
|
||||
|
||||
- [ ] Do a proper binary version... (or lose the typed arrays?)
|
||||
- [ ] Extract debugging to its own module
|
||||
- [ ] DRY out addition and subtraction
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
# SPDX-FileCopyrightText: 2020 Tim C, 2021 Jeff Epler for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text`
|
||||
=======================
|
||||
"""
|
||||
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
from displayio import Group, Palette
|
||||
|
||||
try:
|
||||
from typing import Optional, List, Tuple
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def wrap_text_to_pixels(
|
||||
string: str,
|
||||
max_width: int,
|
||||
font: Optional[FontProtocol] = None,
|
||||
indent0: str = "",
|
||||
indent1: str = "",
|
||||
) -> List[str]:
|
||||
# pylint: disable=too-many-branches, too-many-locals, too-many-nested-blocks, too-many-statements
|
||||
|
||||
"""wrap_text_to_pixels function
|
||||
A helper that will return a list of lines with word-break wrapping.
|
||||
Leading and trailing whitespace in your string will be removed. If
|
||||
you wish to use leading whitespace see ``indent0`` and ``indent1``
|
||||
parameters.
|
||||
|
||||
:param str string: The text to be wrapped.
|
||||
:param int max_width: The maximum number of pixels on a line before wrapping.
|
||||
:param font: The font to use for measuring the text.
|
||||
:type font: ~fontio.FontProtocol
|
||||
:param str indent0: Additional character(s) to add to the first line.
|
||||
:param str indent1: Additional character(s) to add to all other lines.
|
||||
|
||||
:return: A list of the lines resulting from wrapping the
|
||||
input text at ``max_width`` pixels size
|
||||
:rtype: List[str]
|
||||
|
||||
"""
|
||||
if font is None:
|
||||
|
||||
def measure(text):
|
||||
return len(text)
|
||||
|
||||
else:
|
||||
if hasattr(font, "load_glyphs"):
|
||||
font.load_glyphs(string)
|
||||
|
||||
def measure(text):
|
||||
total_len = 0
|
||||
for char in text:
|
||||
this_glyph = font.get_glyph(ord(char))
|
||||
if this_glyph:
|
||||
total_len += this_glyph.shift_x
|
||||
return total_len
|
||||
|
||||
lines = []
|
||||
partial = [indent0]
|
||||
width = measure(indent0)
|
||||
swidth = measure(" ")
|
||||
firstword = True
|
||||
for line_in_input in string.split("\n"):
|
||||
newline = True
|
||||
for index, word in enumerate(line_in_input.split(" ")):
|
||||
wwidth = measure(word)
|
||||
word_parts = []
|
||||
cur_part = ""
|
||||
|
||||
if wwidth > max_width:
|
||||
for char in word:
|
||||
if newline:
|
||||
extraspace = 0
|
||||
leadchar = ""
|
||||
else:
|
||||
extraspace = swidth
|
||||
leadchar = " "
|
||||
if (
|
||||
measure("".join(partial))
|
||||
+ measure(cur_part)
|
||||
+ measure(char)
|
||||
+ measure("-")
|
||||
+ extraspace
|
||||
> max_width
|
||||
):
|
||||
if cur_part:
|
||||
word_parts.append(
|
||||
"".join(partial) + leadchar + cur_part + "-"
|
||||
)
|
||||
|
||||
else:
|
||||
word_parts.append("".join(partial))
|
||||
cur_part = char
|
||||
partial = [indent1]
|
||||
newline = True
|
||||
else:
|
||||
cur_part += char
|
||||
if cur_part:
|
||||
word_parts.append(cur_part)
|
||||
for line in word_parts[:-1]:
|
||||
lines.append(line)
|
||||
partial.append(word_parts[-1])
|
||||
width = measure(word_parts[-1])
|
||||
if firstword:
|
||||
firstword = False
|
||||
else:
|
||||
if firstword:
|
||||
partial.append(word)
|
||||
firstword = False
|
||||
width += wwidth
|
||||
elif width + swidth + wwidth < max_width:
|
||||
if index > 0:
|
||||
partial.append(" ")
|
||||
partial.append(word)
|
||||
width += wwidth + swidth
|
||||
else:
|
||||
lines.append("".join(partial))
|
||||
partial = [indent1, word]
|
||||
width = measure(indent1) + wwidth
|
||||
if newline:
|
||||
newline = False
|
||||
|
||||
lines.append("".join(partial))
|
||||
partial = [indent1]
|
||||
width = measure(indent1)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def wrap_text_to_lines(string: str, max_chars: int) -> List[str]:
|
||||
"""wrap_text_to_lines function
|
||||
A helper that will return a list of lines with word-break wrapping
|
||||
|
||||
:param str string: The text to be wrapped
|
||||
:param int max_chars: The maximum number of characters on a line before wrapping
|
||||
|
||||
:return: A list of lines where each line is separated based on the amount
|
||||
of ``max_chars`` provided
|
||||
:rtype: List[str]
|
||||
"""
|
||||
|
||||
def chunks(lst, n):
|
||||
"""Yield successive n-sized chunks from lst."""
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i : i + n]
|
||||
|
||||
string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines
|
||||
words = string.split(" ")
|
||||
the_lines = []
|
||||
the_line = ""
|
||||
for w in words:
|
||||
if len(w) > max_chars:
|
||||
if the_line: # add what we had stored
|
||||
the_lines.append(the_line)
|
||||
parts = []
|
||||
for part in chunks(w, max_chars - 1):
|
||||
parts.append("{}-".format(part))
|
||||
the_lines.extend(parts[:-1])
|
||||
the_line = parts[-1][:-1]
|
||||
continue
|
||||
|
||||
if len(the_line + " " + w) <= max_chars:
|
||||
the_line += " " + w
|
||||
elif not the_line and len(w) == max_chars:
|
||||
the_lines.append(w)
|
||||
else:
|
||||
the_lines.append(the_line)
|
||||
the_line = "" + w
|
||||
if the_line: # Last line remaining
|
||||
the_lines.append(the_line)
|
||||
# Remove any blank lines
|
||||
while not the_lines[0]:
|
||||
del the_lines[0]
|
||||
# Remove first space from first line:
|
||||
if the_lines[0][0] == " ":
|
||||
the_lines[0] = the_lines[0][1:]
|
||||
return the_lines
|
||||
|
||||
|
||||
class LabelBase(Group):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""Superclass that all other types of labels will extend. This contains
|
||||
all of the properties and functions that work the same way in all labels.
|
||||
|
||||
**Note:** This should be treated as an abstract base class.
|
||||
|
||||
Subclasses should implement ``_set_text``, ``_set_font``, and ``_set_line_spacing`` to
|
||||
have the correct behavior for that type of label.
|
||||
|
||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||
Must include a capital M for measuring character size.
|
||||
:type font: ~fontio.FontProtocol
|
||||
:param str text: Text to display
|
||||
:param int color: Color of all text in RGB hex
|
||||
:param int background_color: Color of the background, use `None` for transparent
|
||||
:param float line_spacing: Line spacing of text to display
|
||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||
surround text. When set to 'True' Padding parameters will be ignored.
|
||||
:param int padding_top: Additional pixels added to background bounding box at top
|
||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||
:param int padding_left: Additional pixels added to background bounding box at left
|
||||
:param int padding_right: Additional pixels added to background bounding box at right
|
||||
:param (float,float) anchor_point: Point that anchored_position moves relative to.
|
||||
Tuple with decimal percentage of width and height.
|
||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple
|
||||
containing x,y pixel coordinates.
|
||||
:param int scale: Integer value of the pixel scaling
|
||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||
This is helpful when two or more labels need to be aligned to the same baseline
|
||||
:param (int,str) tab_replacement: tuple with tab character replace information. When
|
||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||
tab character
|
||||
:param str label_direction: string defining the label text orientation. See the
|
||||
subclass documentation for the possible values.
|
||||
:param bool verbose: print debugging information in some internal functions. Default to False
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
font: FontProtocol,
|
||||
x: int = 0,
|
||||
y: int = 0,
|
||||
text: str = "",
|
||||
color: int = 0xFFFFFF,
|
||||
background_color: int = None,
|
||||
line_spacing: float = 1.25,
|
||||
background_tight: bool = False,
|
||||
padding_top: int = 0,
|
||||
padding_bottom: int = 0,
|
||||
padding_left: int = 0,
|
||||
padding_right: int = 0,
|
||||
anchor_point: Tuple[float, float] = None,
|
||||
anchored_position: Tuple[int, int] = None,
|
||||
scale: int = 1,
|
||||
base_alignment: bool = False,
|
||||
tab_replacement: Tuple[int, str] = (4, " "),
|
||||
label_direction: str = "LTR",
|
||||
verbose: bool = False,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments, too-many-locals
|
||||
|
||||
super().__init__(x=x, y=y, scale=1)
|
||||
|
||||
self._font = font
|
||||
self._text = text
|
||||
self._palette = Palette(2)
|
||||
self._color = 0xFFFFFF
|
||||
self._background_color = None
|
||||
self._line_spacing = line_spacing
|
||||
self._background_tight = background_tight
|
||||
self._padding_top = padding_top
|
||||
self._padding_bottom = padding_bottom
|
||||
self._padding_left = padding_left
|
||||
self._padding_right = padding_right
|
||||
self._anchor_point = anchor_point
|
||||
self._anchored_position = anchored_position
|
||||
self._base_alignment = base_alignment
|
||||
self._label_direction = label_direction
|
||||
self._tab_replacement = tab_replacement
|
||||
self._tab_text = self._tab_replacement[1] * self._tab_replacement[0]
|
||||
self._verbose = verbose
|
||||
|
||||
self._ascent, self._descent = self._get_ascent_descent()
|
||||
self._bounding_box = None
|
||||
|
||||
self.color = color
|
||||
self.background_color = background_color
|
||||
|
||||
# local group will hold background and text
|
||||
# the self group scale should always remain at 1, the self._local_group will
|
||||
# be used to set the scale of the label
|
||||
self._local_group = Group(scale=scale)
|
||||
self.append(self._local_group)
|
||||
|
||||
self._baseline = -1.0
|
||||
|
||||
if self._base_alignment:
|
||||
self._y_offset = 0
|
||||
else:
|
||||
self._y_offset = self._ascent // 2
|
||||
|
||||
def _get_ascent_descent(self) -> Tuple[int, int]:
|
||||
"""Private function to calculate ascent and descent font values"""
|
||||
if hasattr(self.font, "ascent") and hasattr(self.font, "descent"):
|
||||
return self.font.ascent, self.font.descent
|
||||
|
||||
# check a few glyphs for maximum ascender and descender height
|
||||
glyphs = "M j'" # choose glyphs with highest ascender and lowest
|
||||
try:
|
||||
self._font.load_glyphs(glyphs)
|
||||
except AttributeError:
|
||||
# Builtin font doesn't have or need load_glyphs
|
||||
pass
|
||||
# descender, will depend upon font used
|
||||
ascender_max = descender_max = 0
|
||||
for char in glyphs:
|
||||
this_glyph = self._font.get_glyph(ord(char))
|
||||
if this_glyph:
|
||||
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
|
||||
descender_max = max(descender_max, -this_glyph.dy)
|
||||
return ascender_max, descender_max
|
||||
|
||||
@property
|
||||
def font(self) -> FontProtocol:
|
||||
"""Font to use for text display."""
|
||||
return self._font
|
||||
|
||||
def _set_font(self, new_font: FontProtocol) -> None:
|
||||
raise NotImplementedError("{} MUST override '_set_font'".format(type(self)))
|
||||
|
||||
@font.setter
|
||||
def font(self, new_font: FontProtocol) -> None:
|
||||
self._set_font(new_font)
|
||||
|
||||
@property
|
||||
def color(self) -> int:
|
||||
"""Color of the text as an RGB hex number."""
|
||||
return self._color
|
||||
|
||||
@color.setter
|
||||
def color(self, new_color: int):
|
||||
self._color = new_color
|
||||
if new_color is not None:
|
||||
self._palette[1] = new_color
|
||||
self._palette.make_opaque(1)
|
||||
else:
|
||||
self._palette[1] = 0
|
||||
self._palette.make_transparent(1)
|
||||
|
||||
@property
|
||||
def background_color(self) -> int:
|
||||
"""Color of the background as an RGB hex number."""
|
||||
return self._background_color
|
||||
|
||||
def _set_background_color(self, new_color):
|
||||
raise NotImplementedError(
|
||||
"{} MUST override '_set_background_color'".format(type(self))
|
||||
)
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, new_color: int) -> None:
|
||||
self._set_background_color(new_color)
|
||||
|
||||
@property
|
||||
def anchor_point(self) -> Tuple[float, float]:
|
||||
"""Point that anchored_position moves relative to.
|
||||
Tuple with decimal percentage of width and height.
|
||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)"""
|
||||
return self._anchor_point
|
||||
|
||||
@anchor_point.setter
|
||||
def anchor_point(self, new_anchor_point: Tuple[float, float]) -> None:
|
||||
if new_anchor_point[1] == self._baseline:
|
||||
self._anchor_point = (new_anchor_point[0], -1.0)
|
||||
else:
|
||||
self._anchor_point = new_anchor_point
|
||||
|
||||
# update the anchored_position using setter
|
||||
self.anchored_position = self._anchored_position
|
||||
|
||||
@property
|
||||
def anchored_position(self) -> Tuple[int, int]:
|
||||
"""Position relative to the anchor_point. Tuple containing x,y
|
||||
pixel coordinates."""
|
||||
return self._anchored_position
|
||||
|
||||
@anchored_position.setter
|
||||
def anchored_position(self, new_position: Tuple[int, int]) -> None:
|
||||
self._anchored_position = new_position
|
||||
# Calculate (x,y) position
|
||||
if (self._anchor_point is not None) and (self._anchored_position is not None):
|
||||
self.x = int(
|
||||
new_position[0]
|
||||
- (self._bounding_box[0] * self.scale)
|
||||
- round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
|
||||
)
|
||||
if self._anchor_point[1] == self._baseline:
|
||||
self.y = int(new_position[1] - (self._y_offset * self.scale))
|
||||
else:
|
||||
self.y = int(
|
||||
new_position[1]
|
||||
- (self._bounding_box[1] * self.scale)
|
||||
- round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
|
||||
)
|
||||
|
||||
@property
|
||||
def scale(self) -> int:
|
||||
"""Set the scaling of the label, in integer values"""
|
||||
return self._local_group.scale
|
||||
|
||||
@scale.setter
|
||||
def scale(self, new_scale: int) -> None:
|
||||
self._local_group.scale = new_scale
|
||||
self.anchored_position = self._anchored_position # update the anchored_position
|
||||
|
||||
def _set_text(self, new_text: str, scale: int) -> None:
|
||||
raise NotImplementedError("{} MUST override '_set_text'".format(type(self)))
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""Text to be displayed."""
|
||||
return self._text
|
||||
|
||||
@text.setter # Cannot set color or background color with text setter, use separate setter
|
||||
def text(self, new_text: str) -> None:
|
||||
if new_text == self._text:
|
||||
return
|
||||
self._set_text(new_text, self.scale)
|
||||
|
||||
@property
|
||||
def bounding_box(self) -> Tuple[int, int]:
|
||||
"""An (x, y, w, h) tuple that completely covers all glyphs. The
|
||||
first two numbers are offset from the x, y origin of this group"""
|
||||
return tuple(self._bounding_box)
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
"""The height of the label determined from the bounding box."""
|
||||
return self._bounding_box[3]
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""The width of the label determined from the bounding box."""
|
||||
return self._bounding_box[2]
|
||||
|
||||
@property
|
||||
def line_spacing(self) -> float:
|
||||
"""The amount of space between lines of text, in multiples of the font's
|
||||
bounding-box height. (E.g. 1.0 is the bounding-box height)"""
|
||||
return self._line_spacing
|
||||
|
||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||
raise NotImplementedError(
|
||||
"{} MUST override '_set_line_spacing'".format(type(self))
|
||||
)
|
||||
|
||||
@line_spacing.setter
|
||||
def line_spacing(self, new_line_spacing: float) -> None:
|
||||
self._set_line_spacing(new_line_spacing)
|
||||
|
||||
@property
|
||||
def label_direction(self) -> str:
|
||||
"""Set the text direction of the label"""
|
||||
return self._label_direction
|
||||
|
||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||
raise NotImplementedError(
|
||||
"{} MUST override '_set_label_direction'".format(type(self))
|
||||
)
|
||||
|
||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||
raise NotImplementedError(
|
||||
"{} MUST override '_get_valid_label_direction'".format(type(self))
|
||||
)
|
||||
|
||||
@label_direction.setter
|
||||
def label_direction(self, new_label_direction: str) -> None:
|
||||
"""Set the text direction of the label"""
|
||||
if new_label_direction not in self._get_valid_label_directions():
|
||||
raise RuntimeError("Please provide a valid text direction")
|
||||
self._set_label_direction(new_label_direction)
|
||||
|
||||
def _replace_tabs(self, text: str) -> str:
|
||||
return text if text.find("\t") < 0 else self._tab_text.join(text.split("\t"))
|
||||
|
|
@ -0,0 +1,597 @@
|
|||
# SPDX-FileCopyrightText: 2020 Kevin Matocha
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text.bitmap_label`
|
||||
================================================================================
|
||||
|
||||
Text graphics handling for CircuitPython, including text boxes
|
||||
|
||||
|
||||
* Author(s): Kevin Matocha
|
||||
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
**Hardware:**
|
||||
|
||||
**Software and Dependencies:**
|
||||
|
||||
* Adafruit CircuitPython firmware for the supported boards:
|
||||
https://circuitpython.org/downloads
|
||||
|
||||
"""
|
||||
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
import displayio
|
||||
from adafruit_display_text import LabelBase
|
||||
|
||||
try:
|
||||
import bitmaptools
|
||||
except ImportError:
|
||||
# We have a slower fallback for bitmaptools
|
||||
pass
|
||||
|
||||
try:
|
||||
from typing import Optional, Tuple
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class Label(LabelBase):
|
||||
"""A label displaying a string of text that is stored in a bitmap.
|
||||
Note: This ``bitmap_label.py`` library utilizes a :py:class:`~displayio.Bitmap`
|
||||
to display the text. This method is memory-conserving relative to ``label.py``.
|
||||
|
||||
For further reduction in memory usage, set ``save_text=False`` (text string will not
|
||||
be stored and ``line_spacing`` and ``font`` are immutable with ``save_text``
|
||||
set to ``False``).
|
||||
|
||||
The origin point set by ``x`` and ``y``
|
||||
properties will be the left edge of the bounding box, and in the center of a M
|
||||
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||
it will try to have it be center-left as close as possible.
|
||||
|
||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||
Must include a capital M for measuring character size.
|
||||
:type font: ~fontio.FontProtocol
|
||||
:param str text: Text to display
|
||||
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
||||
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
||||
for transparent
|
||||
:param float line_spacing: Line spacing of text to display
|
||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||
surround text. When set to 'True' Padding parameters will be ignored.
|
||||
:param int padding_top: Additional pixels added to background bounding box at top
|
||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom
|
||||
:param int padding_left: Additional pixels added to background bounding box at left
|
||||
:param int padding_right: Additional pixels added to background bounding box at right
|
||||
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
||||
Tuple with decimal percentage of width and height.
|
||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
||||
containing x,y pixel coordinates.
|
||||
:param int scale: Integer value of the pixel scaling
|
||||
:param bool save_text: Set True to save the text string as a constant in the
|
||||
label structure. Set False to reduce memory use.
|
||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||
This is helpful when two or more labels need to be aligned to the same baseline
|
||||
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||
tab character
|
||||
:param str label_direction: string defining the label text orientation. There are 5
|
||||
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||
``UPD``-Upside Down ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``
|
||||
:param bool verbose: print debugging information in some internal functions. Default to False
|
||||
|
||||
"""
|
||||
|
||||
# This maps label_direction to TileGrid's transpose_xy, flip_x, flip_y
|
||||
_DIR_MAP = {
|
||||
"UPR": (True, True, False),
|
||||
"DWR": (True, False, True),
|
||||
"UPD": (False, True, True),
|
||||
"LTR": (False, False, False),
|
||||
"RTL": (False, False, False),
|
||||
}
|
||||
|
||||
def __init__(self, font: FontProtocol, save_text: bool = True, **kwargs) -> None:
|
||||
self._bitmap = None
|
||||
self._tilegrid = None
|
||||
self._prev_label_direction = None
|
||||
|
||||
super().__init__(font, **kwargs)
|
||||
|
||||
self._save_text = save_text
|
||||
self._text = self._replace_tabs(self._text)
|
||||
|
||||
# call the text updater with all the arguments.
|
||||
self._reset_text(
|
||||
font=font,
|
||||
text=self._text,
|
||||
line_spacing=self._line_spacing,
|
||||
scale=self.scale,
|
||||
)
|
||||
|
||||
def _reset_text(
|
||||
self,
|
||||
font: Optional[FontProtocol] = None,
|
||||
text: Optional[str] = None,
|
||||
line_spacing: Optional[float] = None,
|
||||
scale: Optional[int] = None,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
||||
|
||||
# Store all the instance variables
|
||||
if font is not None:
|
||||
self._font = font
|
||||
if line_spacing is not None:
|
||||
self._line_spacing = line_spacing
|
||||
|
||||
# if text is not provided as a parameter (text is None), use the previous value.
|
||||
if (text is None) and self._save_text:
|
||||
text = self._text
|
||||
|
||||
if self._save_text: # text string will be saved
|
||||
self._text = self._replace_tabs(text)
|
||||
else:
|
||||
self._text = None # save a None value since text string is not saved
|
||||
|
||||
# Check for empty string
|
||||
if (text == "") or (
|
||||
text is None
|
||||
): # If empty string, just create a zero-sized bounding box and that's it.
|
||||
self._bounding_box = (
|
||||
0,
|
||||
0,
|
||||
0, # zero width with text == ""
|
||||
0, # zero height with text == ""
|
||||
)
|
||||
# Clear out any items in the self._local_group Group, in case this is an
|
||||
# update to the bitmap_label
|
||||
for _ in self._local_group:
|
||||
self._local_group.pop(0)
|
||||
|
||||
# Free the bitmap and tilegrid since they are removed
|
||||
self._bitmap = None
|
||||
self._tilegrid = None
|
||||
|
||||
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
||||
# append to the self Group
|
||||
|
||||
# Calculate the text bounding box
|
||||
|
||||
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
||||
# anchor_position calculations
|
||||
(
|
||||
box_x,
|
||||
tight_box_y,
|
||||
x_offset,
|
||||
tight_y_offset,
|
||||
loose_box_y,
|
||||
loose_y_offset,
|
||||
) = self._text_bounding_box(
|
||||
text,
|
||||
self._font,
|
||||
) # calculate the box size for a tight and loose backgrounds
|
||||
|
||||
if self._background_tight:
|
||||
box_y = tight_box_y
|
||||
y_offset = tight_y_offset
|
||||
self._padding_left = 0
|
||||
self._padding_right = 0
|
||||
self._padding_top = 0
|
||||
self._padding_bottom = 0
|
||||
|
||||
else: # calculate the box size for a loose background
|
||||
box_y = loose_box_y
|
||||
y_offset = loose_y_offset
|
||||
|
||||
# Calculate the background size including padding
|
||||
tight_box_x = box_x
|
||||
box_x = box_x + self._padding_left + self._padding_right
|
||||
box_y = box_y + self._padding_top + self._padding_bottom
|
||||
|
||||
# Create the Bitmap unless it can be reused
|
||||
new_bitmap = None
|
||||
if (
|
||||
self._bitmap is None
|
||||
or self._bitmap.width != box_x
|
||||
or self._bitmap.height != box_y
|
||||
):
|
||||
new_bitmap = displayio.Bitmap(box_x, box_y, len(self._palette))
|
||||
self._bitmap = new_bitmap
|
||||
else:
|
||||
self._bitmap.fill(0)
|
||||
|
||||
# Place the text into the Bitmap
|
||||
self._place_text(
|
||||
self._bitmap,
|
||||
text if self._label_direction != "RTL" else "".join(reversed(text)),
|
||||
self._font,
|
||||
self._padding_left - x_offset,
|
||||
self._padding_top + y_offset,
|
||||
)
|
||||
|
||||
if self._base_alignment:
|
||||
label_position_yoffset = 0
|
||||
else:
|
||||
label_position_yoffset = self._ascent // 2
|
||||
|
||||
# Create the TileGrid if not created bitmap unchanged
|
||||
if self._tilegrid is None or new_bitmap:
|
||||
self._tilegrid = displayio.TileGrid(
|
||||
self._bitmap,
|
||||
pixel_shader=self._palette,
|
||||
width=1,
|
||||
height=1,
|
||||
tile_width=box_x,
|
||||
tile_height=box_y,
|
||||
default_tile=0,
|
||||
x=-self._padding_left + x_offset,
|
||||
y=label_position_yoffset - y_offset - self._padding_top,
|
||||
)
|
||||
# Clear out any items in the local_group Group, in case this is an update to
|
||||
# the bitmap_label
|
||||
for _ in self._local_group:
|
||||
self._local_group.pop(0)
|
||||
self._local_group.append(
|
||||
self._tilegrid
|
||||
) # add the bitmap's tilegrid to the group
|
||||
|
||||
# Set TileGrid properties based on label_direction
|
||||
if self._label_direction != self._prev_label_direction:
|
||||
tg1 = self._tilegrid
|
||||
tg1.transpose_xy, tg1.flip_x, tg1.flip_y = self._DIR_MAP[
|
||||
self._label_direction
|
||||
]
|
||||
|
||||
# Update bounding_box values. Note: To be consistent with label.py,
|
||||
# this is the bounding box for the text only, not including the background.
|
||||
if self._label_direction in ("UPR", "DWR"):
|
||||
if self._label_direction == "UPR":
|
||||
top = self._padding_right
|
||||
left = self._padding_top
|
||||
if self._label_direction == "DWR":
|
||||
top = self._padding_left
|
||||
left = self._padding_bottom
|
||||
self._bounding_box = (
|
||||
self._tilegrid.x + left,
|
||||
self._tilegrid.y + top,
|
||||
tight_box_y,
|
||||
tight_box_x,
|
||||
)
|
||||
else:
|
||||
self._bounding_box = (
|
||||
self._tilegrid.x + self._padding_left,
|
||||
self._tilegrid.y + self._padding_top,
|
||||
tight_box_x,
|
||||
tight_box_y,
|
||||
)
|
||||
|
||||
if (
|
||||
scale is not None
|
||||
): # Scale will be defined in local_group (Note: self should have scale=1)
|
||||
self.scale = scale # call the setter
|
||||
|
||||
# set the anchored_position with setter after bitmap is created, sets the
|
||||
# x,y positions of the label
|
||||
self.anchored_position = self._anchored_position
|
||||
|
||||
@staticmethod
|
||||
def _line_spacing_ypixels(font: FontProtocol, line_spacing: float) -> int:
|
||||
# Note: Scaling is provided at the Group level
|
||||
return_value = int(line_spacing * font.get_bounding_box()[1])
|
||||
return return_value
|
||||
|
||||
def _text_bounding_box(
|
||||
self, text: str, font: FontProtocol
|
||||
) -> Tuple[int, int, int, int, int, int]:
|
||||
# pylint: disable=too-many-locals,too-many-branches
|
||||
|
||||
bbox = font.get_bounding_box()
|
||||
if len(bbox) == 4:
|
||||
ascender_max, descender_max = bbox[1], -bbox[3]
|
||||
else:
|
||||
ascender_max, descender_max = self._ascent, self._descent
|
||||
|
||||
lines = 1
|
||||
|
||||
# starting x and y position (left margin)
|
||||
xposition = x_start = yposition = y_start = 0
|
||||
|
||||
left = None
|
||||
right = x_start
|
||||
top = bottom = y_start
|
||||
|
||||
y_offset_tight = self._ascent // 2
|
||||
|
||||
newlines = 0
|
||||
line_spacing = self._line_spacing
|
||||
|
||||
for char in text:
|
||||
if char == "\n": # newline
|
||||
newlines += 1
|
||||
|
||||
else:
|
||||
my_glyph = font.get_glyph(ord(char))
|
||||
|
||||
if my_glyph is None: # Error checking: no glyph found
|
||||
print("Glyph not found: {}".format(repr(char)))
|
||||
else:
|
||||
if newlines:
|
||||
xposition = x_start # reset to left column
|
||||
yposition += (
|
||||
self._line_spacing_ypixels(font, line_spacing) * newlines
|
||||
) # Add the newline(s)
|
||||
lines += newlines
|
||||
newlines = 0
|
||||
if xposition == x_start:
|
||||
if left is None:
|
||||
left = 0
|
||||
else:
|
||||
left = min(left, my_glyph.dx)
|
||||
xright = xposition + my_glyph.width + my_glyph.dx
|
||||
xposition += my_glyph.shift_x
|
||||
|
||||
right = max(right, xposition, xright)
|
||||
|
||||
if yposition == y_start: # first line, find the Ascender height
|
||||
top = min(top, -my_glyph.height - my_glyph.dy + y_offset_tight)
|
||||
bottom = max(bottom, yposition - my_glyph.dy + y_offset_tight)
|
||||
|
||||
if left is None:
|
||||
left = 0
|
||||
|
||||
final_box_width = right - left
|
||||
|
||||
final_box_height_tight = bottom - top
|
||||
final_y_offset_tight = -top + y_offset_tight
|
||||
|
||||
final_box_height_loose = (lines - 1) * self._line_spacing_ypixels(
|
||||
font, line_spacing
|
||||
) + (ascender_max + descender_max)
|
||||
final_y_offset_loose = ascender_max
|
||||
|
||||
# return (final_box_width, final_box_height, left, final_y_offset)
|
||||
|
||||
return (
|
||||
final_box_width,
|
||||
final_box_height_tight,
|
||||
left,
|
||||
final_y_offset_tight,
|
||||
final_box_height_loose,
|
||||
final_y_offset_loose,
|
||||
)
|
||||
|
||||
# pylint: disable = too-many-branches
|
||||
def _place_text(
|
||||
self,
|
||||
bitmap: displayio.Bitmap,
|
||||
text: str,
|
||||
font: FontProtocol,
|
||||
xposition: int,
|
||||
yposition: int,
|
||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||
# when copying glyph bitmaps (this is important for slanted text
|
||||
# where rectangular glyph boxes overlap)
|
||||
) -> Tuple[int, int, int, int]:
|
||||
# pylint: disable=too-many-arguments, too-many-locals
|
||||
|
||||
# placeText - Writes text into a bitmap at the specified location.
|
||||
#
|
||||
# Note: scale is pushed up to Group level
|
||||
|
||||
x_start = xposition # starting x position (left margin)
|
||||
y_start = yposition
|
||||
|
||||
left = None
|
||||
right = x_start
|
||||
top = bottom = y_start
|
||||
line_spacing = self._line_spacing
|
||||
|
||||
for char in text:
|
||||
if char == "\n": # newline
|
||||
xposition = x_start # reset to left column
|
||||
yposition = yposition + self._line_spacing_ypixels(
|
||||
font, line_spacing
|
||||
) # Add a newline
|
||||
|
||||
else:
|
||||
my_glyph = font.get_glyph(ord(char))
|
||||
|
||||
if my_glyph is None: # Error checking: no glyph found
|
||||
print("Glyph not found: {}".format(repr(char)))
|
||||
else:
|
||||
if xposition == x_start:
|
||||
if left is None:
|
||||
left = 0
|
||||
else:
|
||||
left = min(left, my_glyph.dx)
|
||||
|
||||
right = max(
|
||||
right,
|
||||
xposition + my_glyph.shift_x,
|
||||
xposition + my_glyph.width + my_glyph.dx,
|
||||
)
|
||||
if yposition == y_start: # first line, find the Ascender height
|
||||
top = min(top, -my_glyph.height - my_glyph.dy)
|
||||
bottom = max(bottom, yposition - my_glyph.dy)
|
||||
|
||||
glyph_offset_x = (
|
||||
my_glyph.tile_index * my_glyph.width
|
||||
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
||||
# for BDF loaded fonts, this should equal 0
|
||||
|
||||
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
||||
|
||||
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
||||
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
||||
y_clip = 0
|
||||
if y_blit_target < 0:
|
||||
y_clip = -y_blit_target # clip this amount from top of bitmap
|
||||
y_blit_target = 0 # draw the clipped bitmap at y=0
|
||||
if self._verbose:
|
||||
print(
|
||||
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
||||
char
|
||||
)
|
||||
)
|
||||
|
||||
if (y_blit_target + my_glyph.height) > bitmap.height:
|
||||
if self._verbose:
|
||||
print(
|
||||
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
||||
char
|
||||
)
|
||||
)
|
||||
|
||||
self._blit(
|
||||
bitmap,
|
||||
max(xposition + my_glyph.dx, 0),
|
||||
y_blit_target,
|
||||
my_glyph.bitmap,
|
||||
x_1=glyph_offset_x,
|
||||
y_1=y_clip,
|
||||
x_2=glyph_offset_x + my_glyph.width,
|
||||
y_2=my_glyph.height,
|
||||
skip_index=skip_index, # do not copy over any 0 background pixels
|
||||
)
|
||||
|
||||
xposition = xposition + my_glyph.shift_x
|
||||
|
||||
# bounding_box
|
||||
return left, top, right - left, bottom - top
|
||||
|
||||
def _blit(
|
||||
self,
|
||||
bitmap: displayio.Bitmap, # target bitmap
|
||||
x: int, # target x upper left corner
|
||||
y: int, # target y upper left corner
|
||||
source_bitmap: displayio.Bitmap, # source bitmap
|
||||
x_1: int = 0, # source x start
|
||||
y_1: int = 0, # source y start
|
||||
x_2: int = None, # source x end
|
||||
y_2: int = None, # source y end
|
||||
skip_index: int = None, # palette index that will not be copied
|
||||
# (for example: the background color of a glyph)
|
||||
) -> None:
|
||||
# pylint: disable=no-self-use, too-many-arguments
|
||||
|
||||
if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it
|
||||
# this function should perform its own input checks
|
||||
bitmap.blit(
|
||||
x,
|
||||
y,
|
||||
source_bitmap,
|
||||
x1=x_1,
|
||||
y1=y_1,
|
||||
x2=x_2,
|
||||
y2=y_2,
|
||||
skip_index=skip_index,
|
||||
)
|
||||
elif hasattr(bitmaptools, "blit"):
|
||||
bitmaptools.blit(
|
||||
bitmap,
|
||||
source_bitmap,
|
||||
x,
|
||||
y,
|
||||
x1=x_1,
|
||||
y1=y_1,
|
||||
x2=x_2,
|
||||
y2=y_2,
|
||||
skip_source_index=skip_index,
|
||||
)
|
||||
|
||||
else: # perform pixel by pixel copy of the bitmap
|
||||
# Perform input checks
|
||||
|
||||
if x_2 is None:
|
||||
x_2 = source_bitmap.width
|
||||
if y_2 is None:
|
||||
y_2 = source_bitmap.height
|
||||
|
||||
# Rearrange so that x_1 < x_2 and y1 < y2
|
||||
if x_1 > x_2:
|
||||
x_1, x_2 = x_2, x_1
|
||||
if y_1 > y_2:
|
||||
y_1, y_2 = y_2, y_1
|
||||
|
||||
# Ensure that x2 and y2 are within source bitmap size
|
||||
x_2 = min(x_2, source_bitmap.width)
|
||||
y_2 = min(y_2, source_bitmap.height)
|
||||
|
||||
for y_count in range(y_2 - y_1):
|
||||
for x_count in range(x_2 - x_1):
|
||||
x_placement = x + x_count
|
||||
y_placement = y + y_count
|
||||
|
||||
if (bitmap.width > x_placement >= 0) and (
|
||||
bitmap.height > y_placement >= 0
|
||||
): # ensure placement is within target bitmap
|
||||
# get the palette index from the source bitmap
|
||||
this_pixel_color = source_bitmap[
|
||||
y_1
|
||||
+ (
|
||||
y_count * source_bitmap.width
|
||||
) # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||
+ x_1
|
||||
+ x_count
|
||||
]
|
||||
|
||||
if (skip_index is None) or (this_pixel_color != skip_index):
|
||||
bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
|
||||
y_placement * bitmap.width + x_placement
|
||||
] = this_pixel_color
|
||||
elif y_placement > bitmap.height:
|
||||
break
|
||||
|
||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||
if self._save_text:
|
||||
self._reset_text(line_spacing=new_line_spacing, scale=self.scale)
|
||||
else:
|
||||
raise RuntimeError("line_spacing is immutable when save_text is False")
|
||||
|
||||
def _set_font(self, new_font: FontProtocol) -> None:
|
||||
self._font = new_font
|
||||
if self._save_text:
|
||||
self._reset_text(font=new_font, scale=self.scale)
|
||||
else:
|
||||
raise RuntimeError("font is immutable when save_text is False")
|
||||
|
||||
def _set_text(self, new_text: str, scale: int) -> None:
|
||||
self._reset_text(text=self._replace_tabs(new_text), scale=self.scale)
|
||||
|
||||
def _set_background_color(self, new_color: Optional[int]):
|
||||
self._background_color = new_color
|
||||
if new_color is not None:
|
||||
self._palette[0] = new_color
|
||||
self._palette.make_opaque(0)
|
||||
else:
|
||||
self._palette[0] = 0
|
||||
self._palette.make_transparent(0)
|
||||
|
||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||
# Only make changes if new direction is different
|
||||
# to prevent errors in the _reset_text() direction checks
|
||||
if self._label_direction != new_label_direction:
|
||||
self._prev_label_direction = self._label_direction
|
||||
self._label_direction = new_label_direction
|
||||
self._reset_text(text=str(self._text)) # Force a recalculation
|
||||
|
||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||
return "LTR", "RTL", "UPD", "UPR", "DWR"
|
||||
|
||||
@property
|
||||
def bitmap(self) -> displayio.Bitmap:
|
||||
"""
|
||||
The Bitmap object that the text and background are drawn into.
|
||||
|
||||
:rtype: displayio.Bitmap
|
||||
"""
|
||||
return self._bitmap
|
||||
|
|
@ -0,0 +1,447 @@
|
|||
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text.label`
|
||||
====================================================
|
||||
|
||||
Displays text labels using CircuitPython's displayio.
|
||||
|
||||
* Author(s): Scott Shawcroft
|
||||
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
**Hardware:**
|
||||
|
||||
**Software and Dependencies:**
|
||||
|
||||
* Adafruit CircuitPython firmware for the supported boards:
|
||||
https://circuitpython.org/downloads
|
||||
|
||||
"""
|
||||
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
|
||||
from displayio import Bitmap, Palette, TileGrid
|
||||
from adafruit_display_text import LabelBase
|
||||
|
||||
try:
|
||||
from typing import Optional, Tuple
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class Label(LabelBase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""A label displaying a string of text. The origin point set by ``x`` and ``y``
|
||||
properties will be the left edge of the bounding box, and in the center of a M
|
||||
glyph (if its one line), or the (number of lines * linespacing + M)/2. That is,
|
||||
it will try to have it be center-left as close as possible.
|
||||
|
||||
:param font: A font class that has ``get_bounding_box`` and ``get_glyph``.
|
||||
Must include a capital M for measuring character size.
|
||||
:type font: ~fontio.FontProtocol
|
||||
:param str text: Text to display
|
||||
:param int|Tuple(int, int, int) color: Color of all text in HEX or RGB
|
||||
:param int|Tuple(int, int, int)|None background_color: Color of the background, use `None`
|
||||
for transparent
|
||||
:param float line_spacing: Line spacing of text to display
|
||||
:param bool background_tight: Set `True` only if you want background box to tightly
|
||||
surround text. When set to 'True' Padding parameters will be ignored.
|
||||
:param int padding_top: Additional pixels added to background bounding box at top.
|
||||
This parameter could be negative indicating additional pixels subtracted from the
|
||||
background bounding box.
|
||||
:param int padding_bottom: Additional pixels added to background bounding box at bottom.
|
||||
This parameter could be negative indicating additional pixels subtracted from the
|
||||
background bounding box.
|
||||
:param int padding_left: Additional pixels added to background bounding box at left.
|
||||
This parameter could be negative indicating additional pixels subtracted from the
|
||||
background bounding box.
|
||||
:param int padding_right: Additional pixels added to background bounding box at right.
|
||||
This parameter could be negative indicating additional pixels subtracted from the
|
||||
background bounding box.
|
||||
:param Tuple(float, float) anchor_point: Point that anchored_position moves relative to.
|
||||
Tuple with decimal percentage of width and height.
|
||||
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
|
||||
:param Tuple(int, int) anchored_position: Position relative to the anchor_point. Tuple
|
||||
containing x,y pixel coordinates.
|
||||
:param int scale: Integer value of the pixel scaling
|
||||
:param bool base_alignment: when True allows to align text label to the baseline.
|
||||
This is helpful when two or more labels need to be aligned to the same baseline
|
||||
:param Tuple(int, str) tab_replacement: tuple with tab character replace information. When
|
||||
(4, " ") will indicate a tab replacement of 4 spaces, defaults to 4 spaces by
|
||||
tab character
|
||||
:param str label_direction: string defining the label text orientation. There are 5
|
||||
configurations possibles ``LTR``-Left-To-Right ``RTL``-Right-To-Left
|
||||
``TTB``-Top-To-Bottom ``UPR``-Upwards ``DWR``-Downwards. It defaults to ``LTR``"""
|
||||
|
||||
def __init__(self, font: FontProtocol, **kwargs) -> None:
|
||||
self._background_palette = Palette(1)
|
||||
self._added_background_tilegrid = False
|
||||
|
||||
super().__init__(font, **kwargs)
|
||||
|
||||
text = self._replace_tabs(self._text)
|
||||
|
||||
self._width = len(text)
|
||||
self._height = self._font.get_bounding_box()[1]
|
||||
|
||||
# Create the two-color text palette
|
||||
self._palette[0] = 0
|
||||
self._palette.make_transparent(0)
|
||||
|
||||
if text is not None:
|
||||
self._reset_text(str(text))
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def _create_background_box(self, lines: int, y_offset: int) -> TileGrid:
|
||||
"""Private Class function to create a background_box
|
||||
:param lines: int number of lines
|
||||
:param y_offset: int y pixel bottom coordinate for the background_box"""
|
||||
|
||||
left = self._bounding_box[0]
|
||||
if self._background_tight: # draw a tight bounding box
|
||||
box_width = self._bounding_box[2]
|
||||
box_height = self._bounding_box[3]
|
||||
x_box_offset = 0
|
||||
y_box_offset = self._bounding_box[1]
|
||||
|
||||
else: # draw a "loose" bounding box to include any ascenders/descenders.
|
||||
ascent, descent = self._ascent, self._descent
|
||||
|
||||
if self._label_direction in ("DWR", "UPR"):
|
||||
box_height = (
|
||||
self._bounding_box[3] + self._padding_right + self._padding_left
|
||||
)
|
||||
x_box_offset = -self._padding_left
|
||||
box_width = (
|
||||
(ascent + descent)
|
||||
+ int((lines - 1) * self._width * self._line_spacing)
|
||||
+ self._padding_top
|
||||
+ self._padding_bottom
|
||||
)
|
||||
elif self._label_direction == "TTB":
|
||||
box_height = (
|
||||
self._bounding_box[3] + self._padding_top + self._padding_bottom
|
||||
)
|
||||
x_box_offset = -self._padding_left
|
||||
box_width = (
|
||||
(ascent + descent)
|
||||
+ int((lines - 1) * self._height * self._line_spacing)
|
||||
+ self._padding_right
|
||||
+ self._padding_left
|
||||
)
|
||||
else:
|
||||
box_width = (
|
||||
self._bounding_box[2] + self._padding_left + self._padding_right
|
||||
)
|
||||
x_box_offset = -self._padding_left
|
||||
box_height = (
|
||||
(ascent + descent)
|
||||
+ int((lines - 1) * self._height * self._line_spacing)
|
||||
+ self._padding_top
|
||||
+ self._padding_bottom
|
||||
)
|
||||
|
||||
if self._label_direction == "DWR":
|
||||
padding_to_use = self._padding_bottom
|
||||
elif self._label_direction == "TTB":
|
||||
padding_to_use = self._padding_top
|
||||
y_offset = 0
|
||||
ascent = 0
|
||||
else:
|
||||
padding_to_use = self._padding_top
|
||||
|
||||
if self._base_alignment:
|
||||
y_box_offset = -ascent - padding_to_use
|
||||
else:
|
||||
y_box_offset = -ascent + y_offset - padding_to_use
|
||||
|
||||
box_width = max(0, box_width) # remove any negative values
|
||||
box_height = max(0, box_height) # remove any negative values
|
||||
|
||||
if self._label_direction == "UPR":
|
||||
movx = y_box_offset
|
||||
movy = -box_height - x_box_offset
|
||||
elif self._label_direction == "DWR":
|
||||
movx = y_box_offset
|
||||
movy = x_box_offset
|
||||
elif self._label_direction == "TTB":
|
||||
movx = x_box_offset
|
||||
movy = y_box_offset
|
||||
else:
|
||||
movx = left + x_box_offset
|
||||
movy = y_box_offset
|
||||
|
||||
background_bitmap = Bitmap(box_width, box_height, 1)
|
||||
tile_grid = TileGrid(
|
||||
background_bitmap,
|
||||
pixel_shader=self._background_palette,
|
||||
x=movx,
|
||||
y=movy,
|
||||
)
|
||||
|
||||
return tile_grid
|
||||
|
||||
# pylint: enable=too-many-branches
|
||||
def _set_background_color(self, new_color: Optional[int]) -> None:
|
||||
"""Private class function that allows updating the font box background color
|
||||
|
||||
:param int new_color: Color as an RGB hex number, setting to None makes it transparent
|
||||
"""
|
||||
|
||||
if new_color is None:
|
||||
self._background_palette.make_transparent(0)
|
||||
if self._added_background_tilegrid:
|
||||
self._local_group.pop(0)
|
||||
self._added_background_tilegrid = False
|
||||
else:
|
||||
self._background_palette.make_opaque(0)
|
||||
self._background_palette[0] = new_color
|
||||
self._background_color = new_color
|
||||
|
||||
lines = self._text.rstrip("\n").count("\n") + 1
|
||||
y_offset = self._ascent // 2
|
||||
|
||||
if self._bounding_box is None:
|
||||
# Still in initialization
|
||||
return
|
||||
|
||||
if not self._added_background_tilegrid: # no bitmap is in the self Group
|
||||
# add bitmap if text is present and bitmap sizes > 0 pixels
|
||||
if (
|
||||
(len(self._text) > 0)
|
||||
and (
|
||||
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||
)
|
||||
and (
|
||||
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||
)
|
||||
):
|
||||
self._local_group.insert(
|
||||
0, self._create_background_box(lines, y_offset)
|
||||
)
|
||||
self._added_background_tilegrid = True
|
||||
|
||||
else: # a bitmap is present in the self Group
|
||||
# update bitmap if text is present and bitmap sizes > 0 pixels
|
||||
if (
|
||||
(len(self._text) > 0)
|
||||
and (
|
||||
self._bounding_box[2] + self._padding_left + self._padding_right > 0
|
||||
)
|
||||
and (
|
||||
self._bounding_box[3] + self._padding_top + self._padding_bottom > 0
|
||||
)
|
||||
):
|
||||
self._local_group[0] = self._create_background_box(
|
||||
lines, self._y_offset
|
||||
)
|
||||
else: # delete the existing bitmap
|
||||
self._local_group.pop(0)
|
||||
self._added_background_tilegrid = False
|
||||
|
||||
def _update_text(self, new_text: str) -> None:
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
|
||||
x = 0
|
||||
y = 0
|
||||
if self._added_background_tilegrid:
|
||||
i = 1
|
||||
else:
|
||||
i = 0
|
||||
tilegrid_count = i
|
||||
if self._base_alignment:
|
||||
self._y_offset = 0
|
||||
else:
|
||||
self._y_offset = self._ascent // 2
|
||||
|
||||
if self._label_direction == "RTL":
|
||||
left = top = bottom = 0
|
||||
right = None
|
||||
elif self._label_direction == "LTR":
|
||||
right = top = bottom = 0
|
||||
left = None
|
||||
else:
|
||||
top = right = left = 0
|
||||
bottom = 0
|
||||
|
||||
for character in new_text:
|
||||
if character == "\n":
|
||||
y += int(self._height * self._line_spacing)
|
||||
x = 0
|
||||
continue
|
||||
glyph = self._font.get_glyph(ord(character))
|
||||
if not glyph:
|
||||
continue
|
||||
|
||||
position_x, position_y = 0, 0
|
||||
|
||||
if self._label_direction in ("LTR", "RTL"):
|
||||
bottom = max(bottom, y - glyph.dy + self._y_offset)
|
||||
if y == 0: # first line, find the Ascender height
|
||||
top = min(top, -glyph.height - glyph.dy + self._y_offset)
|
||||
position_y = y - glyph.height - glyph.dy + self._y_offset
|
||||
|
||||
if self._label_direction == "LTR":
|
||||
right = max(right, x + glyph.shift_x, x + glyph.width + glyph.dx)
|
||||
if x == 0:
|
||||
if left is None:
|
||||
left = 0
|
||||
else:
|
||||
left = min(left, glyph.dx)
|
||||
position_x = x + glyph.dx
|
||||
else:
|
||||
left = max(
|
||||
left, abs(x) + glyph.shift_x, abs(x) + glyph.width + glyph.dx
|
||||
)
|
||||
if x == 0:
|
||||
if right is None:
|
||||
right = 0
|
||||
else:
|
||||
right = max(right, glyph.dx)
|
||||
position_x = x - glyph.width
|
||||
|
||||
elif self._label_direction == "TTB":
|
||||
if x == 0:
|
||||
if left is None:
|
||||
left = 0
|
||||
else:
|
||||
left = min(left, glyph.dx)
|
||||
if y == 0:
|
||||
top = min(top, -glyph.dy)
|
||||
|
||||
bottom = max(bottom, y + glyph.height, y + glyph.height + glyph.dy)
|
||||
right = max(
|
||||
right, x + glyph.width + glyph.dx, x + glyph.shift_x + glyph.dx
|
||||
)
|
||||
position_y = y + glyph.dy
|
||||
position_x = x - glyph.width // 2 + self._y_offset
|
||||
|
||||
elif self._label_direction == "UPR":
|
||||
if x == 0:
|
||||
if bottom is None:
|
||||
bottom = -glyph.dx
|
||||
|
||||
if y == 0: # first line, find the Ascender height
|
||||
bottom = min(bottom, -glyph.dy)
|
||||
left = min(left, x - glyph.height + self._y_offset)
|
||||
top = min(top, y - glyph.width - glyph.dx, y - glyph.shift_x)
|
||||
right = max(right, x + glyph.height, x + glyph.height - glyph.dy)
|
||||
position_y = y - glyph.width - glyph.dx
|
||||
position_x = x - glyph.height - glyph.dy + self._y_offset
|
||||
|
||||
elif self._label_direction == "DWR":
|
||||
if y == 0:
|
||||
if top is None:
|
||||
top = -glyph.dx
|
||||
top = min(top, -glyph.dx)
|
||||
if x == 0:
|
||||
left = min(left, -glyph.dy)
|
||||
left = min(left, x, x - glyph.dy - self._y_offset)
|
||||
bottom = max(bottom, y + glyph.width + glyph.dx, y + glyph.shift_x)
|
||||
right = max(right, x + glyph.height)
|
||||
position_y = y + glyph.dx
|
||||
position_x = x + glyph.dy - self._y_offset
|
||||
|
||||
if glyph.width > 0 and glyph.height > 0:
|
||||
face = TileGrid(
|
||||
glyph.bitmap,
|
||||
pixel_shader=self._palette,
|
||||
default_tile=glyph.tile_index,
|
||||
tile_width=glyph.width,
|
||||
tile_height=glyph.height,
|
||||
x=position_x,
|
||||
y=position_y,
|
||||
)
|
||||
|
||||
if self._label_direction == "UPR":
|
||||
face.transpose_xy = True
|
||||
face.flip_x = True
|
||||
if self._label_direction == "DWR":
|
||||
face.transpose_xy = True
|
||||
face.flip_y = True
|
||||
|
||||
if tilegrid_count < len(self._local_group):
|
||||
self._local_group[tilegrid_count] = face
|
||||
else:
|
||||
self._local_group.append(face)
|
||||
tilegrid_count += 1
|
||||
|
||||
if self._label_direction == "RTL":
|
||||
x = x - glyph.shift_x
|
||||
if self._label_direction == "TTB":
|
||||
if glyph.height < 2:
|
||||
y = y + glyph.shift_x
|
||||
else:
|
||||
y = y + glyph.height + 1
|
||||
if self._label_direction == "UPR":
|
||||
y = y - glyph.shift_x
|
||||
if self._label_direction == "DWR":
|
||||
y = y + glyph.shift_x
|
||||
if self._label_direction == "LTR":
|
||||
x = x + glyph.shift_x
|
||||
|
||||
i += 1
|
||||
|
||||
if self._label_direction == "LTR" and left is None:
|
||||
left = 0
|
||||
if self._label_direction == "RTL" and right is None:
|
||||
right = 0
|
||||
if self._label_direction == "TTB" and top is None:
|
||||
top = 0
|
||||
|
||||
while len(self._local_group) > tilegrid_count: # i:
|
||||
self._local_group.pop()
|
||||
|
||||
if self._label_direction == "RTL":
|
||||
# pylint: disable=invalid-unary-operand-type
|
||||
# type-checkers think left can be None
|
||||
self._bounding_box = (-left, top, left - right, bottom - top)
|
||||
if self._label_direction == "TTB":
|
||||
self._bounding_box = (left, top, right - left, bottom - top)
|
||||
if self._label_direction == "UPR":
|
||||
self._bounding_box = (left, top, right, bottom - top)
|
||||
if self._label_direction == "DWR":
|
||||
self._bounding_box = (left, top, right, bottom - top)
|
||||
if self._label_direction == "LTR":
|
||||
self._bounding_box = (left, top, right - left, bottom - top)
|
||||
|
||||
self._text = new_text
|
||||
|
||||
if self._background_color is not None:
|
||||
self._set_background_color(self._background_color)
|
||||
|
||||
def _reset_text(self, new_text: str) -> None:
|
||||
current_anchored_position = self.anchored_position
|
||||
self._update_text(str(self._replace_tabs(new_text)))
|
||||
self.anchored_position = current_anchored_position
|
||||
|
||||
def _set_font(self, new_font: FontProtocol) -> None:
|
||||
old_text = self._text
|
||||
current_anchored_position = self.anchored_position
|
||||
self._text = ""
|
||||
self._font = new_font
|
||||
self._height = self._font.get_bounding_box()[1]
|
||||
self._update_text(str(old_text))
|
||||
self.anchored_position = current_anchored_position
|
||||
|
||||
def _set_line_spacing(self, new_line_spacing: float) -> None:
|
||||
self._line_spacing = new_line_spacing
|
||||
self.text = self._text # redraw the box
|
||||
|
||||
def _set_text(self, new_text: str, scale: int) -> None:
|
||||
self._reset_text(new_text)
|
||||
|
||||
def _set_label_direction(self, new_label_direction: str) -> None:
|
||||
self._label_direction = new_label_direction
|
||||
self._update_text(str(self._text))
|
||||
|
||||
def _get_valid_label_directions(self) -> Tuple[str, ...]:
|
||||
return "LTR", "RTL", "UPR", "DWR", "TTB"
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
# SPDX-FileCopyrightText: 2023 Tim C
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text.outlined_label`
|
||||
====================================================
|
||||
|
||||
Subclass of BitmapLabel that adds outline color and stroke size
|
||||
functionalities.
|
||||
|
||||
* Author(s): Tim Cocks
|
||||
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
**Hardware:**
|
||||
|
||||
**Software and Dependencies:**
|
||||
|
||||
* Adafruit CircuitPython firmware for the supported boards:
|
||||
https://circuitpython.org/downloads
|
||||
|
||||
"""
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
import bitmaptools
|
||||
from displayio import Palette, Bitmap
|
||||
from adafruit_display_text import bitmap_label
|
||||
|
||||
try:
|
||||
from typing import Optional, Tuple, Union
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OutlinedLabel(bitmap_label.Label):
|
||||
"""
|
||||
OutlinedLabel - A BitmapLabel subclass that includes arguments and properties for specifying
|
||||
outline_size and outline_color to get drawn as a stroke around the text.
|
||||
|
||||
:param Union[Tuple, int] outline_color: The color of the outline stroke as RGB tuple, or hex.
|
||||
:param int outline_size: The size in pixels of the outline stroke.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
font,
|
||||
outline_color: Union[int, Tuple] = 0x999999,
|
||||
outline_size: int = 1,
|
||||
padding_top: Optional[int] = None,
|
||||
padding_bottom: Optional[int] = None,
|
||||
padding_left: Optional[int] = None,
|
||||
padding_right: Optional[int] = None,
|
||||
**kwargs
|
||||
):
|
||||
if padding_top is None:
|
||||
padding_top = outline_size + 0
|
||||
if padding_bottom is None:
|
||||
padding_bottom = outline_size + 2
|
||||
if padding_left is None:
|
||||
padding_left = outline_size + 0
|
||||
if padding_right is None:
|
||||
padding_right = outline_size + 0
|
||||
|
||||
super().__init__(
|
||||
font,
|
||||
padding_top=padding_top,
|
||||
padding_bottom=padding_bottom,
|
||||
padding_left=padding_left,
|
||||
padding_right=padding_right,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
_background_color = self._palette[0]
|
||||
_foreground_color = self._palette[1]
|
||||
_background_is_transparent = self._palette.is_transparent(0)
|
||||
self._palette = Palette(3)
|
||||
self._palette[0] = _background_color
|
||||
self._palette[1] = _foreground_color
|
||||
self._palette[2] = outline_color
|
||||
if _background_is_transparent:
|
||||
self._palette.make_transparent(0)
|
||||
|
||||
self._outline_size = outline_size
|
||||
self._stamp_source = Bitmap((outline_size * 2) + 1, (outline_size * 2) + 1, 3)
|
||||
self._stamp_source.fill(2)
|
||||
|
||||
self._bitmap = None
|
||||
|
||||
self._reset_text(
|
||||
font=font,
|
||||
text=self._text,
|
||||
line_spacing=self._line_spacing,
|
||||
scale=self.scale,
|
||||
)
|
||||
|
||||
def _add_outline(self):
|
||||
"""
|
||||
Blit the outline into the labels Bitmap. We will stamp self._stamp_source for each
|
||||
pixel of the foreground color but skip the foreground color when we blit.
|
||||
:return: None
|
||||
"""
|
||||
if hasattr(self, "_stamp_source"):
|
||||
for y in range(self.bitmap.height):
|
||||
for x in range(self.bitmap.width):
|
||||
if self.bitmap[x, y] == 1:
|
||||
try:
|
||||
bitmaptools.blit(
|
||||
self.bitmap,
|
||||
self._stamp_source,
|
||||
x - self._outline_size,
|
||||
y - self._outline_size,
|
||||
skip_dest_index=1,
|
||||
)
|
||||
except ValueError as value_error:
|
||||
raise ValueError(
|
||||
"Padding must be big enough to fit outline_size "
|
||||
"all the way around the text. "
|
||||
"Try using either larger padding sizes, or smaller outline_size."
|
||||
) from value_error
|
||||
|
||||
def _place_text(
|
||||
self,
|
||||
bitmap: Bitmap,
|
||||
text: str,
|
||||
font: FontProtocol,
|
||||
xposition: int,
|
||||
yposition: int,
|
||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||
# when copying glyph bitmaps (this is important for slanted text
|
||||
# where rectangular glyph boxes overlap)
|
||||
) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
Copy the glpyphs that represent the value of the string into the labels Bitmap.
|
||||
:param bitmap: The bitmap to place text into
|
||||
:param text: The text to render
|
||||
:param font: The font to render the text in
|
||||
:param xposition: x location of the starting point within the bitmap
|
||||
:param yposition: y location of the starting point within the bitmap
|
||||
:param skip_index: Color index to skip during rendering instead of covering up
|
||||
:return Tuple bounding_box: tuple with x, y, width, height values of the bitmap
|
||||
"""
|
||||
parent_result = super()._place_text(
|
||||
bitmap, text, font, xposition, yposition, skip_index=skip_index
|
||||
)
|
||||
|
||||
self._add_outline()
|
||||
|
||||
return parent_result
|
||||
|
||||
@property
|
||||
def outline_color(self):
|
||||
"""Color of the outline to draw around the text."""
|
||||
return self._palette[2]
|
||||
|
||||
@outline_color.setter
|
||||
def outline_color(self, new_outline_color):
|
||||
self._palette[2] = new_outline_color
|
||||
|
||||
@property
|
||||
def outline_size(self):
|
||||
"""Stroke size of the outline to draw around the text."""
|
||||
return self._outline_size
|
||||
|
||||
@outline_size.setter
|
||||
def outline_size(self, new_outline_size):
|
||||
self._outline_size = new_outline_size
|
||||
|
||||
self._padding_top = new_outline_size + 0
|
||||
self._padding_bottom = new_outline_size + 2
|
||||
self._padding_left = new_outline_size + 0
|
||||
self._padding_right = new_outline_size + 0
|
||||
|
||||
self._stamp_source = Bitmap(
|
||||
(new_outline_size * 2) + 1, (new_outline_size * 2) + 1, 3
|
||||
)
|
||||
self._stamp_source.fill(2)
|
||||
self._reset_text(
|
||||
font=self._font,
|
||||
text=self._text,
|
||||
line_spacing=self._line_spacing,
|
||||
scale=self.scale,
|
||||
)
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text.scrolling_label`
|
||||
====================================================
|
||||
|
||||
Displays text into a fixed-width label that scrolls leftward
|
||||
if the full_text is large enough to need it.
|
||||
|
||||
* Author(s): Tim Cocks
|
||||
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
**Hardware:**
|
||||
|
||||
**Software and Dependencies:**
|
||||
|
||||
* Adafruit CircuitPython firmware for the supported boards:
|
||||
https://circuitpython.org/downloads
|
||||
|
||||
"""
|
||||
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
import adafruit_ticks
|
||||
from adafruit_display_text import bitmap_label
|
||||
|
||||
try:
|
||||
from typing import Optional
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class ScrollingLabel(bitmap_label.Label):
|
||||
"""ScrollingLabel - A fixed-width label that will scroll to the left
|
||||
in order to show the full text if it's larger than the fixed-width.
|
||||
|
||||
:param font: The font to use for the label.
|
||||
:type: ~fontio.FontProtocol
|
||||
:param int max_characters: The number of characters that sets the fixed-width. Default is 10.
|
||||
:param str text: The full text to show in the label. If this is longer than
|
||||
``max_characters`` then the label will scroll to show everything.
|
||||
:param float animate_time: The number of seconds in between scrolling animation
|
||||
frames. Default is 0.3 seconds.
|
||||
:param int current_index: The index of the first visible character in the label.
|
||||
Default is 0, the first character. Will increase while scrolling."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
font: FontProtocol,
|
||||
max_characters: int = 10,
|
||||
text: Optional[str] = "",
|
||||
animate_time: Optional[float] = 0.3,
|
||||
current_index: Optional[int] = 0,
|
||||
**kwargs
|
||||
) -> None:
|
||||
super().__init__(font, **kwargs)
|
||||
self.animate_time = animate_time
|
||||
self._current_index = current_index
|
||||
self._last_animate_time = -1
|
||||
self.max_characters = max_characters
|
||||
|
||||
if text and text[-1] != " ":
|
||||
text = "{} ".format(text)
|
||||
self._full_text = text
|
||||
|
||||
self.update()
|
||||
|
||||
def update(self, force: bool = False) -> None:
|
||||
"""Attempt to update the display. If ``animate_time`` has elapsed since
|
||||
previews animation frame then move the characters over by 1 index.
|
||||
Must be called in the main loop of user code.
|
||||
|
||||
:param bool force: whether to ignore ``animation_time`` and force the update.
|
||||
Default is False.
|
||||
:return: None
|
||||
"""
|
||||
_now = adafruit_ticks.ticks_ms()
|
||||
if force or adafruit_ticks.ticks_less(
|
||||
self._last_animate_time + int(self.animate_time * 1000), _now
|
||||
):
|
||||
if len(self.full_text) <= self.max_characters:
|
||||
if self._text != self.full_text:
|
||||
super()._set_text(self.full_text, self.scale)
|
||||
self._last_animate_time = _now
|
||||
return
|
||||
|
||||
if self.current_index + self.max_characters <= len(self.full_text):
|
||||
_showing_string = self.full_text[
|
||||
self.current_index : self.current_index + self.max_characters
|
||||
]
|
||||
else:
|
||||
_showing_string_start = self.full_text[self.current_index :]
|
||||
_showing_string_end = "{}".format(
|
||||
self.full_text[
|
||||
: (self.current_index + self.max_characters)
|
||||
% len(self.full_text)
|
||||
]
|
||||
)
|
||||
|
||||
_showing_string = "{}{}".format(
|
||||
_showing_string_start, _showing_string_end
|
||||
)
|
||||
super()._set_text(_showing_string, self.scale)
|
||||
self.current_index += 1
|
||||
self._last_animate_time = _now
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def current_index(self) -> int:
|
||||
"""Index of the first visible character.
|
||||
|
||||
:return int: The current index
|
||||
"""
|
||||
return self._current_index
|
||||
|
||||
@current_index.setter
|
||||
def current_index(self, new_index: int) -> None:
|
||||
if self.full_text:
|
||||
self._current_index = new_index % len(self.full_text)
|
||||
else:
|
||||
self._current_index = 0
|
||||
|
||||
@property
|
||||
def full_text(self) -> str:
|
||||
"""The full text to be shown. If it's longer than ``max_characters`` then
|
||||
scrolling will occur as needed.
|
||||
|
||||
:return str: The full text of this label.
|
||||
"""
|
||||
return self._full_text
|
||||
|
||||
@full_text.setter
|
||||
def full_text(self, new_text: str) -> None:
|
||||
if new_text and new_text[-1] != " ":
|
||||
new_text = "{} ".format(new_text)
|
||||
if new_text != self._full_text:
|
||||
self._full_text = new_text
|
||||
self.current_index = 0
|
||||
self.update(True)
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""The full text to be shown. If it's longer than ``max_characters`` then
|
||||
scrolling will occur as needed.
|
||||
|
||||
:return str: The full text of this label.
|
||||
"""
|
||||
return self.full_text
|
||||
|
||||
@text.setter
|
||||
def text(self, new_text):
|
||||
self.full_text = new_text
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`adafruit_display_text.text_box`
|
||||
================================================================================
|
||||
|
||||
Text graphics handling for CircuitPython, including text boxes
|
||||
|
||||
|
||||
* Author(s): Tim Cocks
|
||||
|
||||
Implementation Notes
|
||||
--------------------
|
||||
|
||||
**Hardware:**
|
||||
|
||||
**Software and Dependencies:**
|
||||
|
||||
* Adafruit CircuitPython firmware for the supported boards:
|
||||
https://circuitpython.org/downloads
|
||||
|
||||
"""
|
||||
|
||||
__version__ = "3.2.2"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Text.git"
|
||||
|
||||
import displayio
|
||||
from micropython import const
|
||||
|
||||
from adafruit_display_text import wrap_text_to_pixels
|
||||
from adafruit_display_text import bitmap_label
|
||||
|
||||
try:
|
||||
from typing import Optional, Tuple
|
||||
from fontio import FontProtocol
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, duplicate-code
|
||||
class TextBox(bitmap_label.Label):
|
||||
"""
|
||||
TextBox has a constrained width and optionally height.
|
||||
You set the desired size when it's initialized it
|
||||
will automatically wrap text to fit it within the allotted
|
||||
size.
|
||||
|
||||
Left, Right, and Center alignment of the text within the
|
||||
box are supported.
|
||||
|
||||
:param font: The font to use for the TextBox.
|
||||
:param width: The width of the TextBox in pixels.
|
||||
:param height: The height of the TextBox in pixels.
|
||||
:param align: How to align the text within the box,
|
||||
valid values are ``ALIGN_LEFT``, ``ALIGN_CENTER``, ``ALIGN_RIGHT``.
|
||||
"""
|
||||
|
||||
ALIGN_LEFT = const(0)
|
||||
ALIGN_CENTER = const(1)
|
||||
ALIGN_RIGHT = const(2)
|
||||
|
||||
DYNAMIC_HEIGHT = const(-1)
|
||||
|
||||
def __init__(
|
||||
self, font: FontProtocol, width: int, height: int, align=ALIGN_LEFT, **kwargs
|
||||
) -> None:
|
||||
self._bitmap = None
|
||||
self._tilegrid = None
|
||||
self._prev_label_direction = None
|
||||
self._width = width
|
||||
|
||||
if height != TextBox.DYNAMIC_HEIGHT:
|
||||
self._height = height
|
||||
self.dynamic_height = False
|
||||
else:
|
||||
self.dynamic_height = True
|
||||
|
||||
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
|
||||
raise ValueError(
|
||||
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
|
||||
)
|
||||
self._align = align
|
||||
|
||||
self._padding_left = kwargs.get("padding_left", 0)
|
||||
self._padding_right = kwargs.get("padding_right", 0)
|
||||
|
||||
self.lines = wrap_text_to_pixels(
|
||||
kwargs.get("text", ""),
|
||||
self._width - self._padding_left - self._padding_right,
|
||||
font,
|
||||
)
|
||||
|
||||
super(bitmap_label.Label, self).__init__(font, **kwargs)
|
||||
|
||||
print(f"before reset: {self._text}")
|
||||
|
||||
self._text = "\n".join(self.lines)
|
||||
self._text = self._replace_tabs(self._text)
|
||||
self._original_text = self._text
|
||||
|
||||
# call the text updater with all the arguments.
|
||||
self._reset_text(
|
||||
font=font,
|
||||
text=self._text,
|
||||
line_spacing=self._line_spacing,
|
||||
scale=self.scale,
|
||||
)
|
||||
print(f"after reset: {self._text}")
|
||||
|
||||
def _place_text(
|
||||
self,
|
||||
bitmap: displayio.Bitmap,
|
||||
text: str,
|
||||
font: FontProtocol,
|
||||
xposition: int,
|
||||
yposition: int,
|
||||
skip_index: int = 0, # set to None to write all pixels, other wise skip this palette index
|
||||
# when copying glyph bitmaps (this is important for slanted text
|
||||
# where rectangular glyph boxes overlap)
|
||||
) -> Tuple[int, int, int, int]:
|
||||
# pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches
|
||||
|
||||
# placeText - Writes text into a bitmap at the specified location.
|
||||
#
|
||||
# Note: scale is pushed up to Group level
|
||||
original_xposition = xposition
|
||||
cur_line_index = 0
|
||||
cur_line_width = self._text_bounding_box(self.lines[0], self.font)[0]
|
||||
|
||||
if self.align == self.ALIGN_LEFT:
|
||||
x_start = original_xposition # starting x position (left margin)
|
||||
if self.align == self.ALIGN_CENTER:
|
||||
unused_space = self._width - cur_line_width
|
||||
x_start = original_xposition + unused_space // 2
|
||||
if self.align == self.ALIGN_RIGHT:
|
||||
unused_space = self._width - cur_line_width
|
||||
x_start = original_xposition + unused_space - self._padding_right
|
||||
|
||||
xposition = x_start # pylint: disable=used-before-assignment
|
||||
|
||||
y_start = yposition
|
||||
# print(f"start loc {x_start}, {y_start}")
|
||||
|
||||
left = None
|
||||
right = x_start
|
||||
top = bottom = y_start
|
||||
line_spacing = self._line_spacing
|
||||
|
||||
# print(f"cur_line width: {cur_line_width}")
|
||||
for char in text:
|
||||
if char == "\n": # newline
|
||||
cur_line_index += 1
|
||||
cur_line_width = self._text_bounding_box(
|
||||
self.lines[cur_line_index], self.font
|
||||
)[0]
|
||||
# print(f"cur_line width: {cur_line_width}")
|
||||
if self.align == self.ALIGN_LEFT:
|
||||
x_start = original_xposition # starting x position (left margin)
|
||||
if self.align == self.ALIGN_CENTER:
|
||||
unused_space = self._width - cur_line_width
|
||||
x_start = original_xposition + unused_space // 2
|
||||
if self.align == self.ALIGN_RIGHT:
|
||||
unused_space = self._width - cur_line_width
|
||||
x_start = original_xposition + unused_space - self._padding_right
|
||||
xposition = x_start
|
||||
|
||||
yposition = yposition + self._line_spacing_ypixels(
|
||||
font, line_spacing
|
||||
) # Add a newline
|
||||
|
||||
else:
|
||||
my_glyph = font.get_glyph(ord(char))
|
||||
|
||||
if my_glyph is None: # Error checking: no glyph found
|
||||
print("Glyph not found: {}".format(repr(char)))
|
||||
else:
|
||||
if xposition == x_start:
|
||||
if left is None:
|
||||
left = 0
|
||||
else:
|
||||
left = min(left, my_glyph.dx)
|
||||
|
||||
right = max(
|
||||
right,
|
||||
xposition + my_glyph.shift_x,
|
||||
xposition + my_glyph.width + my_glyph.dx,
|
||||
)
|
||||
if yposition == y_start: # first line, find the Ascender height
|
||||
top = min(top, -my_glyph.height - my_glyph.dy)
|
||||
bottom = max(bottom, yposition - my_glyph.dy)
|
||||
|
||||
glyph_offset_x = (
|
||||
my_glyph.tile_index * my_glyph.width
|
||||
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
|
||||
# for BDF loaded fonts, this should equal 0
|
||||
|
||||
y_blit_target = yposition - my_glyph.height - my_glyph.dy
|
||||
|
||||
# Clip glyph y-direction if outside the font ascent/descent metrics.
|
||||
# Note: bitmap.blit will automatically clip the bottom of the glyph.
|
||||
y_clip = 0
|
||||
if y_blit_target < 0:
|
||||
y_clip = -y_blit_target # clip this amount from top of bitmap
|
||||
y_blit_target = 0 # draw the clipped bitmap at y=0
|
||||
if self._verbose:
|
||||
print(
|
||||
'Warning: Glyph clipped, exceeds Ascent property: "{}"'.format(
|
||||
char
|
||||
)
|
||||
)
|
||||
|
||||
if (y_blit_target + my_glyph.height) > bitmap.height:
|
||||
if self._verbose:
|
||||
print(
|
||||
'Warning: Glyph clipped, exceeds descent property: "{}"'.format(
|
||||
char
|
||||
)
|
||||
)
|
||||
try:
|
||||
self._blit(
|
||||
bitmap,
|
||||
max(xposition + my_glyph.dx, 0),
|
||||
y_blit_target,
|
||||
my_glyph.bitmap,
|
||||
x_1=glyph_offset_x,
|
||||
y_1=y_clip,
|
||||
x_2=glyph_offset_x + my_glyph.width,
|
||||
y_2=my_glyph.height,
|
||||
skip_index=skip_index, # do not copy over any 0 background pixels
|
||||
)
|
||||
except ValueError:
|
||||
# ignore index out of bounds error
|
||||
break
|
||||
|
||||
xposition = xposition + my_glyph.shift_x
|
||||
|
||||
# bounding_box
|
||||
return left, top, right - left, bottom - top
|
||||
|
||||
def _reset_text(
|
||||
self,
|
||||
font: Optional[FontProtocol] = None,
|
||||
text: Optional[str] = None,
|
||||
line_spacing: Optional[float] = None,
|
||||
scale: Optional[int] = None,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
|
||||
|
||||
# Store all the instance variables
|
||||
if font is not None:
|
||||
self._font = font
|
||||
if line_spacing is not None:
|
||||
self._line_spacing = line_spacing
|
||||
|
||||
# if text is not provided as a parameter (text is None), use the previous value.
|
||||
if text is None:
|
||||
text = self._text
|
||||
|
||||
self._text = self._replace_tabs(text)
|
||||
print(f"inside reset_text text: {text}")
|
||||
|
||||
# Check for empty string
|
||||
if (text == "") or (
|
||||
text is None
|
||||
): # If empty string, just create a zero-sized bounding box and that's it.
|
||||
self._bounding_box = (
|
||||
0,
|
||||
0,
|
||||
0, # zero width with text == ""
|
||||
0, # zero height with text == ""
|
||||
)
|
||||
# Clear out any items in the self._local_group Group, in case this is an
|
||||
# update to the bitmap_label
|
||||
for _ in self._local_group:
|
||||
self._local_group.pop(0)
|
||||
|
||||
# Free the bitmap and tilegrid since they are removed
|
||||
self._bitmap = None
|
||||
self._tilegrid = None
|
||||
|
||||
else: # The text string is not empty, so create the Bitmap and TileGrid and
|
||||
# append to the self Group
|
||||
|
||||
# Calculate the text bounding box
|
||||
|
||||
# Calculate both "tight" and "loose" bounding box dimensions to match label for
|
||||
# anchor_position calculations
|
||||
(
|
||||
box_x,
|
||||
tight_box_y,
|
||||
x_offset,
|
||||
tight_y_offset,
|
||||
loose_box_y,
|
||||
loose_y_offset,
|
||||
) = self._text_bounding_box(
|
||||
text,
|
||||
self._font,
|
||||
) # calculate the box size for a tight and loose backgrounds
|
||||
|
||||
if self._background_tight:
|
||||
box_y = tight_box_y
|
||||
y_offset = tight_y_offset
|
||||
self._padding_left = 0
|
||||
self._padding_right = 0
|
||||
self._padding_top = 0
|
||||
self._padding_bottom = 0
|
||||
|
||||
else: # calculate the box size for a loose background
|
||||
box_y = loose_box_y
|
||||
y_offset = loose_y_offset
|
||||
|
||||
# Calculate the background size including padding
|
||||
tight_box_x = box_x
|
||||
box_x = box_x + self._padding_left + self._padding_right
|
||||
box_y = box_y + self._padding_top + self._padding_bottom
|
||||
|
||||
if self.dynamic_height:
|
||||
print(f"dynamic height, box_y: {box_y}")
|
||||
self._height = box_y
|
||||
|
||||
# Create the Bitmap unless it can be reused
|
||||
new_bitmap = None
|
||||
if (
|
||||
self._bitmap is None
|
||||
or self._bitmap.width != self._width
|
||||
or self._bitmap.height != self._height
|
||||
):
|
||||
new_bitmap = displayio.Bitmap(
|
||||
self._width, self._height, len(self._palette)
|
||||
)
|
||||
self._bitmap = new_bitmap
|
||||
else:
|
||||
self._bitmap.fill(0)
|
||||
|
||||
# Place the text into the Bitmap
|
||||
self._place_text(
|
||||
self._bitmap,
|
||||
text,
|
||||
self._font,
|
||||
self._padding_left - x_offset,
|
||||
self._padding_top + y_offset,
|
||||
)
|
||||
|
||||
if self._base_alignment:
|
||||
label_position_yoffset = 0
|
||||
else:
|
||||
label_position_yoffset = self._ascent // 2
|
||||
|
||||
# Create the TileGrid if not created bitmap unchanged
|
||||
if self._tilegrid is None or new_bitmap:
|
||||
self._tilegrid = displayio.TileGrid(
|
||||
self._bitmap,
|
||||
pixel_shader=self._palette,
|
||||
width=1,
|
||||
height=1,
|
||||
tile_width=self._width,
|
||||
tile_height=self._height,
|
||||
default_tile=0,
|
||||
x=-self._padding_left + x_offset,
|
||||
y=label_position_yoffset - y_offset - self._padding_top,
|
||||
)
|
||||
# Clear out any items in the local_group Group, in case this is an update to
|
||||
# the bitmap_label
|
||||
for _ in self._local_group:
|
||||
self._local_group.pop(0)
|
||||
self._local_group.append(
|
||||
self._tilegrid
|
||||
) # add the bitmap's tilegrid to the group
|
||||
|
||||
self._bounding_box = (
|
||||
self._tilegrid.x + self._padding_left,
|
||||
self._tilegrid.y + self._padding_top,
|
||||
tight_box_x,
|
||||
tight_box_y,
|
||||
)
|
||||
print(f"end of reset_text bounding box: {self._bounding_box}")
|
||||
|
||||
if (
|
||||
scale is not None
|
||||
): # Scale will be defined in local_group (Note: self should have scale=1)
|
||||
self.scale = scale # call the setter
|
||||
|
||||
# set the anchored_position with setter after bitmap is created, sets the
|
||||
# x,y positions of the label
|
||||
self.anchored_position = self._anchored_position
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
"""The height of the label determined from the bounding box."""
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""The width of the label determined from the bounding box."""
|
||||
return self._width
|
||||
|
||||
@width.setter
|
||||
def width(self, width: int) -> None:
|
||||
self._width = width
|
||||
self.text = self._text
|
||||
|
||||
@height.setter
|
||||
def height(self, height: int) -> None:
|
||||
if height != TextBox.DYNAMIC_HEIGHT:
|
||||
self._height = height
|
||||
self.dynamic_height = False
|
||||
else:
|
||||
self.dynamic_height = True
|
||||
self.text = self._text
|
||||
|
||||
@bitmap_label.Label.text.setter
|
||||
def text(self, text: str) -> None:
|
||||
self.lines = wrap_text_to_pixels(
|
||||
text, self._width - self._padding_left - self._padding_right, self.font
|
||||
)
|
||||
self._text = self._replace_tabs(text)
|
||||
self._original_text = self._text
|
||||
self._text = "\n".join(self.lines)
|
||||
|
||||
self._set_text(self._text, self.scale)
|
||||
|
||||
@property
|
||||
def align(self):
|
||||
"""Alignment of the text within the TextBox"""
|
||||
return self._align
|
||||
|
||||
@align.setter
|
||||
def align(self, align: int) -> None:
|
||||
if align not in (TextBox.ALIGN_LEFT, TextBox.ALIGN_CENTER, TextBox.ALIGN_RIGHT):
|
||||
raise ValueError(
|
||||
"Align must be one of: ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT"
|
||||
)
|
||||
self._align = align
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import board
|
||||
import displayio
|
||||
import vectorio
|
||||
import terminalio
|
||||
from adafruit_display_text import label
|
||||
|
||||
group = displayio.Group()
|
||||
board.DISPLAY.root_group = group
|
||||
|
||||
text = "Hello world"
|
||||
text_area = label.Label(terminalio.FONT, text=text)
|
||||
text_area.x = 10
|
||||
text_area.y = 10
|
||||
group.append(text_area)
|
||||
|
||||
palette = displayio.Palette(1)
|
||||
palette[0] = 0xff00ff
|
||||
|
||||
circle = vectorio.Circle(pixel_shader=palette, radius=5, x=5, y=5)
|
||||
group.append(circle)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#meowbit
|
||||
|
||||
import keypad
|
||||
import board
|
||||
|
||||
km = keypad.KeyMatrix(
|
||||
row_pins = (board.P0, board.P1, board.P2, board.P3),
|
||||
column_pins = (board.P4, board.P6, board.P8, board.P9) )
|
||||
|
||||
while True:
|
||||
event = km.events.get()
|
||||
if event:
|
||||
print(event.key_number, event.released)
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
# TO USE KEYPAD TO MANIPULATE DATA:
|
||||
#
|
||||
# A button: run/halt
|
||||
# B button: when halted, toggles address/data entry
|
||||
# Right button: when halted, single-steps
|
||||
#
|
||||
# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key)
|
||||
# Data entry: likewise. After you press the second digit, it will automatically go to the next address.
|
||||
|
||||
|
||||
import time
|
||||
import board
|
||||
import displayio
|
||||
import vectorio
|
||||
import terminalio # for font
|
||||
from adafruit_display_text import label
|
||||
import keypad
|
||||
from digitalio import DigitalInOut, Direction, Pull
|
||||
|
||||
|
||||
|
||||
|
||||
class TwoDigitHexInput:
|
||||
def __init__(self):
|
||||
self.digits = [0x0, 0x0]
|
||||
self.currentDigit = 0
|
||||
self.value = 0
|
||||
|
||||
def input(self, d):
|
||||
self.digits[self.currentDigit] = d
|
||||
self.value = (self.digits[0] * 16) + self.digits[1]
|
||||
print("INPUT", self.digits)
|
||||
self.currentDigit = 0 if self.currentDigit else 1
|
||||
|
||||
def clear(self):
|
||||
self.__init__()
|
||||
print(self.digits)
|
||||
|
||||
|
||||
class CPU:
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.IP = 254
|
||||
self.acc = 0
|
||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
||||
self.instruction = { 'opcode': False, 'operand': False }
|
||||
self.memory = False
|
||||
|
||||
|
||||
def load_memory(self, bytes):
|
||||
self.memory = bytes + bytearray(256 - len(bytes))
|
||||
print(type(self.memory))
|
||||
print('mem 254', self.memory[254])
|
||||
# print(self.memory)
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
def step(self):
|
||||
if self.IP >= 256:
|
||||
self.IP = 0
|
||||
print("IP:", self.IP)
|
||||
self.instruction['opcode'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.instruction['operand'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
||||
|
||||
print("instr:", self.instruction['opcode'], self.instruction['operand'])
|
||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
||||
print("acc:", self.acc)
|
||||
print("running:", self.running)
|
||||
print()
|
||||
# self.print_screen()
|
||||
print("byte 26 (keyboard):", self.memory[26])
|
||||
print()
|
||||
|
||||
def hlt(self, operand):
|
||||
self.running = False
|
||||
|
||||
def nop(self, operand):
|
||||
pass
|
||||
|
||||
def lda_lit(self, operand):
|
||||
self.acc = operand
|
||||
|
||||
def lda_mem(self, operand):
|
||||
self.acc = memory[operand]
|
||||
|
||||
def sta_lit(self, operand):
|
||||
memory[operand] = self.acc
|
||||
|
||||
def sta_mem(self, operand):
|
||||
memory[memory[operand]] = self.acc
|
||||
|
||||
def add_lit(self, operand):
|
||||
self.acc = self.acc + operand
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def add_mem(self, operand):
|
||||
self.acc = self.acc + self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_lit(self, operand):
|
||||
self.acc = self.acc - operand
|
||||
if self.acc < 0:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_mem(self, operand):
|
||||
self.acc = self.acc - self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def jmp_lit(self, operand):
|
||||
self.IP = operand
|
||||
|
||||
def jmp_mem(self, operand):
|
||||
self.IP = memory[operand]
|
||||
|
||||
def ske(self, operand):
|
||||
if self.flags['Eq']:
|
||||
self.IP += 2
|
||||
|
||||
def skz(self, operand):
|
||||
if self.flags['Z']:
|
||||
self.IP += 2
|
||||
|
||||
def skn(self, operand):
|
||||
if self.flags['N']:
|
||||
self.IP += 2
|
||||
|
||||
def skc(self, operand):
|
||||
if self.flags['C']:
|
||||
self.IP += 2
|
||||
|
||||
def cst(self, operand):
|
||||
self.flags['C'] = True
|
||||
|
||||
def ccl(self, operand):
|
||||
self.flags['C'] = False
|
||||
|
||||
nums2mnems = {
|
||||
0: hlt,
|
||||
1: nop,
|
||||
2: lda_lit,
|
||||
3: sta_lit,
|
||||
4: add_lit,
|
||||
5: sub_lit,
|
||||
6: jmp_lit,
|
||||
7: ske,
|
||||
8: skz,
|
||||
9: skn,
|
||||
10: skc,
|
||||
11: cst,
|
||||
12: ccl,
|
||||
16: hlt,
|
||||
17: nop,
|
||||
18: lda_mem,
|
||||
19: sta_mem,
|
||||
20: add_mem,
|
||||
21: sub_mem,
|
||||
22: jmp_mem,
|
||||
23: ske,
|
||||
24: skz,
|
||||
25: skn,
|
||||
26: skc,
|
||||
27: cst,
|
||||
28: ccl,
|
||||
}
|
||||
|
||||
|
||||
### MEOWBIT-SPECIFIC STUFF ###
|
||||
|
||||
# to list board features: print(board.__dir__)
|
||||
|
||||
btna = DigitalInOut(board.BTNA)
|
||||
btna.direction = Direction.INPUT
|
||||
btna.pull = Pull.UP # down doesn't work
|
||||
|
||||
btnb = DigitalInOut(board.BTNB)
|
||||
btnb.direction = Direction.INPUT
|
||||
btnb.pull = Pull.UP # down doesn't work
|
||||
|
||||
btnr = DigitalInOut(board.RIGHT)
|
||||
btnr.direction = Direction.INPUT
|
||||
btnr.pull = Pull.UP # down doesn't work
|
||||
|
||||
km = keypad.KeyMatrix(
|
||||
row_pins = (board.P0, board.P1, board.P2, board.P3),
|
||||
column_pins = (board.P4, board.P6, board.P8, board.P9) )
|
||||
|
||||
# This is global because that way you can update the text by just altering text_area.text
|
||||
displayGroup = displayio.Group()
|
||||
board.DISPLAY.root_group = displayGroup
|
||||
text_area = label.Label(terminalio.FONT, text="")
|
||||
text_area.x = 10
|
||||
text_area.y = 10
|
||||
displayGroup.append(text_area)
|
||||
|
||||
palette = displayio.Palette(1)
|
||||
palette[0] = 0xff00ff
|
||||
|
||||
|
||||
class Monitor:
|
||||
def __init__(self, cpu):
|
||||
self.cpu = cpu
|
||||
self.monitorMode = 'addressEntry' # or dataEntry
|
||||
self.monitorAddressInput = TwoDigitHexInput()
|
||||
self.monitorDataInput = TwoDigitHexInput()
|
||||
|
||||
def handleKeys(self):
|
||||
keypad_event = km.events.get()
|
||||
keyPressed = True if (keypad_event and keypad_event.released) else False
|
||||
key = keypad_event.key_number if keyPressed else False
|
||||
|
||||
if self.cpu.running:
|
||||
if btna.value == False:
|
||||
print("HALT PRESSED")
|
||||
self.cpu.running = False
|
||||
time.sleep(0.5) # lazy debounce
|
||||
# km.events.clear() # don't track keypresses from during the run
|
||||
|
||||
if keyPressed:
|
||||
self.cpu.memory[26] = key
|
||||
|
||||
elif not self.cpu.running:
|
||||
if btna.value == False:
|
||||
self.cpu.running = True
|
||||
print("\nSTARTING")
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if btnb.value == False:
|
||||
self.monitorMode = 'addressEntry' if self.monitorMode != 'addressEntry' else 'dataEntry'
|
||||
print("\nENTERING", self.monitorMode, "MODE")
|
||||
self.monitorDataInput.currentDigit = 0
|
||||
self.monitorAddressInput.currentDigit = 0
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if btnr.value == False:
|
||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
||||
# self.IP = self.monitorAddressInput.value
|
||||
self.cpu.step()
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if keypad_event and keypad_event.released:
|
||||
if self.monitorMode == 'addressEntry':
|
||||
self.monitorAddressInput.input(keypad_event.key_number)
|
||||
self.cpu.IP = self.monitorAddressInput.value
|
||||
print("MA", self.IP)
|
||||
|
||||
else:
|
||||
self.monitorDataInput.input(keypad_event.key_number)
|
||||
self.cpu.memory[self.IP] = self.monitorDataInput.value
|
||||
print("MD", self.monitorDataInput.value)
|
||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so go to the next addresss
|
||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
||||
print("ADVANCING")
|
||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
||||
|
||||
def printMonitor(self):
|
||||
text = "IP " + str(self.cpu.IP) + "\tDATA " + str(self.cpu.memory[self.cpu.IP]) + "\tACC " + str(self.cpu.acc) + "\nRunning: " + str(self.cpu.running)
|
||||
text_area.text = text
|
||||
|
||||
|
||||
def printScreen(self):
|
||||
for i in range(5):
|
||||
for j in range(5):
|
||||
memory_index = (i * 5) + j
|
||||
if self.cpu.memory[memory_index] > 0:
|
||||
print("#", end=" ")
|
||||
circle = vectorio.Circle(pixel_shader=palette, radius=8, x=(10 + (j * 20)), y=(40 + (i * 20)))
|
||||
displayGroup.append(circle)
|
||||
else:
|
||||
print("_", end=" ")
|
||||
print()
|
||||
|
||||
def run(self):
|
||||
self.cpu.start()
|
||||
t = time.time()
|
||||
while (time.time() - t) < 30:
|
||||
self.handleKeys()
|
||||
if self.cpu.running:
|
||||
self.cpu.step()
|
||||
self.printMonitor()
|
||||
# self.printScreen()
|
||||
time.sleep(0.5)
|
||||
print("timeout")
|
||||
print(self.cpu.memory)
|
||||
|
||||
|
||||
cpu = CPU()
|
||||
monitor = Monitor(cpu)
|
||||
|
||||
prog = '04 FF 04 01 14 01 00 00 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01'
|
||||
#prog = '00'
|
||||
program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
||||
# Add jmp at addr 254:
|
||||
program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600')
|
||||
cpu.load_memory(program_with_jump)
|
||||
|
||||
monitor.run()
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
# TO USE KEYPAD TO MANIPULATE DATA:
|
||||
#
|
||||
# A button: run/halt
|
||||
# B button: when halted, toggles address/data entry
|
||||
# Right button: when halted, single-steps
|
||||
#
|
||||
# Address entry: press the two digits for the address. It is entered immediately (there's no "enter" key)
|
||||
# Data entry: likewise. After you press the second digit, it will automatically go to the next address.
|
||||
|
||||
|
||||
import time
|
||||
import board
|
||||
import keypad
|
||||
import digitalio
|
||||
from tm1637_display import TM1637Display
|
||||
import board
|
||||
import busio # for led matrix
|
||||
from adafruit_ht16k33 import matrix
|
||||
|
||||
class TwoDigitHexInput:
|
||||
def __init__(self):
|
||||
self.digits = [0x0, 0x0]
|
||||
self.currentDigit = 0
|
||||
self.value = 0
|
||||
|
||||
def input(self, d):
|
||||
self.digits[self.currentDigit] = d
|
||||
self.value = (self.digits[0] * 16) + self.digits[1]
|
||||
print("INPUT", self.digits, "current digit: " + str(self.currentDigit), "value: " + str(self.value))
|
||||
self.currentDigit = 0 if self.currentDigit else 1
|
||||
|
||||
def clear(self):
|
||||
self.__init__()
|
||||
print(self.digits)
|
||||
|
||||
def set(self, n):
|
||||
self.value = n
|
||||
self.digits[0] = n >> 4
|
||||
self.digits[1] = n & 0xF
|
||||
|
||||
|
||||
|
||||
|
||||
class CPU:
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.IP = 254
|
||||
self.acc = 0
|
||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
||||
self.instruction = { 'opcode': False, 'operand': False }
|
||||
self.memory = False
|
||||
|
||||
|
||||
def load_memory(self, bytes):
|
||||
self.memory = bytes + bytearray(256 - len(bytes))
|
||||
# print(self.memory)
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
def step(self):
|
||||
if self.IP >= 255: # TODO CHECK
|
||||
self.IP = 0
|
||||
print("IP:", toHex(self.IP))
|
||||
self.instruction['opcode'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.instruction['operand'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
||||
|
||||
print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand']))
|
||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
||||
print("acc:", self.acc, "N:", self.flags['N'])
|
||||
print("running:", self.running)
|
||||
print()
|
||||
# self.print_screen()
|
||||
print("byte 26 (keyboard):", self.memory[26])
|
||||
print()
|
||||
|
||||
def hlt(self, operand):
|
||||
self.running = False
|
||||
|
||||
def nop(self, operand):
|
||||
pass
|
||||
|
||||
def lda_lit(self, operand):
|
||||
self.acc = operand
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def lda_mem(self, operand):
|
||||
self.acc = self.memory[operand]
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sta_lit(self, operand):
|
||||
self.memory[operand] = self.acc
|
||||
|
||||
def sta_mem(self, operand):
|
||||
self.memory[self.memory[operand]] = self.acc
|
||||
|
||||
def add_lit(self, operand):
|
||||
self.acc = self.acc + operand
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def add_mem(self, operand):
|
||||
self.acc = self.acc + self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_lit(self, operand):
|
||||
self.acc = self.acc - operand
|
||||
if self.acc < 0:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_mem(self, operand):
|
||||
self.acc = self.acc - self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def jmp_lit(self, operand):
|
||||
self.IP = operand
|
||||
|
||||
def jmp_mem(self, operand):
|
||||
self.IP = self.memory[operand]
|
||||
|
||||
def ske(self, operand): # FIXME
|
||||
# if self.flags['Eq']:
|
||||
# self.IP += 2
|
||||
if self.acc == operand:
|
||||
self.IP += 2
|
||||
|
||||
def skz(self, operand):
|
||||
if self.flags['Z']:
|
||||
self.IP += 2
|
||||
|
||||
def skn(self, operand):
|
||||
if self.flags['N']:
|
||||
self.IP += 2
|
||||
|
||||
def skc(self, operand):
|
||||
if self.flags['C']:
|
||||
self.IP += 2
|
||||
|
||||
def cst(self, operand):
|
||||
self.flags['C'] = True
|
||||
|
||||
def ccl(self, operand):
|
||||
self.flags['C'] = False
|
||||
|
||||
nums2mnems = {
|
||||
0: hlt, # x0
|
||||
1: nop, # x1
|
||||
2: lda_lit, # 02
|
||||
3: sta_lit, # 03
|
||||
4: add_lit, # 04
|
||||
5: sub_lit, # 05
|
||||
6: jmp_lit, # 06
|
||||
7: ske, # x7
|
||||
8: skz, # x8
|
||||
9: skn, # x9
|
||||
10: skc, # A
|
||||
11: cst, # B
|
||||
12: ccl, # C
|
||||
16: hlt, #
|
||||
17: nop, #
|
||||
18: lda_mem, # 12
|
||||
19: sta_mem, # 13
|
||||
20: add_mem, # 14
|
||||
21: sub_mem, # 15
|
||||
22: jmp_mem, # 16
|
||||
23: ske,
|
||||
24: skz,
|
||||
25: skn,
|
||||
26: skc,
|
||||
27: cst,
|
||||
28: ccl,
|
||||
}
|
||||
|
||||
|
||||
### PI PICO SPECIFIC STUFF ###
|
||||
|
||||
# to list board features: print(dir(board))
|
||||
|
||||
display_1 = TM1637Display(board.GP0, board.GP1, length=4)
|
||||
display_2 = TM1637Display(board.GP2, board.GP3, length=4)
|
||||
|
||||
i2c = busio.I2C(board.GP17, board.GP16) # scl, sda
|
||||
matrix = matrix.Matrix8x8(i2c)
|
||||
matrix.brightness = 1
|
||||
matrix.blink_rate = 0
|
||||
|
||||
keymatrix = keypad.KeyMatrix(
|
||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
||||
|
||||
keymap = {
|
||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addr",
|
||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"data" }
|
||||
|
||||
numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ]
|
||||
|
||||
def toHex(n):
|
||||
return "%0.2X" % n
|
||||
|
||||
class Monitor:
|
||||
def __init__(self, cpu):
|
||||
self.cpu = cpu
|
||||
self.monitorMode = 'addressEntry' # or dataEntry
|
||||
self.monitorAddressInput = TwoDigitHexInput()
|
||||
self.monitorDataInput = TwoDigitHexInput()
|
||||
|
||||
# In data entry mode, when a full byte is keyed in,
|
||||
# the next keypress advances to the next address and continues entering data there.
|
||||
# This variable tracks whether it's time to do that or not.
|
||||
self.advanceDataEntryNextPress = False
|
||||
|
||||
def handleKeys(self):
|
||||
keypad_event = keymatrix.events.get()
|
||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
||||
numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False
|
||||
|
||||
|
||||
if self.cpu.running:
|
||||
if key == "runhalt":
|
||||
print("HALT PRESSED")
|
||||
self.cpu.running = False
|
||||
time.sleep(0.5) # lazy debounce
|
||||
# km.events.clear() # don't track keypresses from during the run
|
||||
|
||||
if numericKeyPressed:
|
||||
self.cpu.memory[26] = int(key, 16)
|
||||
|
||||
elif not self.cpu.running:
|
||||
if key == "runhalt":
|
||||
self.cpu.running = True
|
||||
print("\nSTARTING")
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if key == "addr":
|
||||
self.monitorMode = 'addressEntry'
|
||||
print("\nENTERING", self.monitorMode, "MODE")
|
||||
self.monitorAddressInput.currentDigit = 0
|
||||
time.sleep(0.5) # lazy debounce
|
||||
if key == "data":
|
||||
self.monitorMode = 'dataEntry'
|
||||
print("\nENTERING", self.monitorMode, "MODE")
|
||||
self.monitorDataInput.clear()
|
||||
self.advanceDataEntryNextPress = False
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if key == "step":
|
||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
||||
# self.IP = self.monitorAddressInput.value
|
||||
self.cpu.step()
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if numericKeyPressed:
|
||||
if self.monitorMode == 'addressEntry':
|
||||
self.monitorAddressInput.input(int(key, 16))
|
||||
self.cpu.IP = self.monitorAddressInput.value
|
||||
print("MA", self.cpu.IP)
|
||||
|
||||
if self.monitorMode == 'dataEntry':
|
||||
if self.advanceDataEntryNextPress:
|
||||
print("ADVANCING")
|
||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
||||
# self.monitorDataInput.clear() # reset .currentDigit
|
||||
self.monitorDataInput.set(self.cpu.memory[self.cpu.IP])
|
||||
self.advanceDataEntryNextPress = False
|
||||
self.monitorDataInput.input(int(key, 16))
|
||||
self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value
|
||||
print("MD", self.monitorDataInput.value)
|
||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address
|
||||
self.advanceDataEntryNextPress = True
|
||||
|
||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
||||
|
||||
|
||||
def displayScreen(self):
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
matrix[x, y] = self.cpu.memory[x + (8*y)]
|
||||
|
||||
|
||||
|
||||
def run(self):
|
||||
#self.cpu.start()
|
||||
t = time.time()
|
||||
while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles
|
||||
self.handleKeys()
|
||||
display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP]))
|
||||
# display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP]))
|
||||
# display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc))
|
||||
display_2.print(toHex(self.cpu.acc))
|
||||
self.displayScreen()
|
||||
if self.cpu.running:
|
||||
self.cpu.step()
|
||||
# time.sleep(0.5) # TODO ?
|
||||
print("timeout")
|
||||
print(self.cpu.memory)
|
||||
|
||||
|
||||
cpu = CPU()
|
||||
monitor = Monitor(cpu)
|
||||
|
||||
# preamble = '00 ' * 64
|
||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES
|
||||
# offset = 64
|
||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00'
|
||||
#prog = '00'
|
||||
# program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
||||
|
||||
# Add jmp at addr 254:
|
||||
#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00
|
||||
# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64)
|
||||
|
||||
with open('test-multiply2.bin', 'rb') as file:
|
||||
program_bytes = bytearray(file.read())
|
||||
|
||||
cpu.load_memory(program_bytes)
|
||||
|
||||
monitor.run()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import time
|
||||
import board
|
||||
import neopixel
|
||||
|
||||
pixels = neopixel.NeoPixel(board.A3, 5*5, brightness=0.5, auto_write=True)
|
||||
|
||||
for i in range(50):
|
||||
pixels.fill((50, 0, 0)) # red
|
||||
time.sleep(0.5)
|
||||
pixels.fill((0,0,50)) # blue
|
||||
time.sleep(0.5)
|
||||
print(i)
|
||||
Binary file not shown.
|
|
@ -0,0 +1,181 @@
|
|||
# SPDX-FileCopyrightText: 2016 Damien P. George
|
||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries
|
||||
# SPDX-FileCopyrightText: 2019 Carter Nelson
|
||||
# SPDX-FileCopyrightText: 2019 Roy Hooper
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
`neopixel` - NeoPixel strip driver
|
||||
====================================================
|
||||
|
||||
* Author(s): Damien P. George, Scott Shawcroft, Carter Nelson, Rose Hooper
|
||||
"""
|
||||
|
||||
import sys
|
||||
import board
|
||||
import digitalio
|
||||
from neopixel_write import neopixel_write
|
||||
|
||||
import adafruit_pixelbuf
|
||||
|
||||
try:
|
||||
# Used only for typing
|
||||
from typing import Optional, Type
|
||||
from types import TracebackType
|
||||
import microcontroller
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
__version__ = "6.3.15"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel.git"
|
||||
|
||||
|
||||
# Pixel color order constants
|
||||
RGB = "RGB"
|
||||
"""Red Green Blue"""
|
||||
GRB = "GRB"
|
||||
"""Green Red Blue"""
|
||||
RGBW = "RGBW"
|
||||
"""Red Green Blue White"""
|
||||
GRBW = "GRBW"
|
||||
"""Green Red Blue White"""
|
||||
|
||||
|
||||
class NeoPixel(adafruit_pixelbuf.PixelBuf):
|
||||
"""
|
||||
A sequence of neopixels.
|
||||
|
||||
:param ~microcontroller.Pin pin: The pin to output neopixel data on.
|
||||
:param int n: The number of neopixels in the chain
|
||||
:param int bpp: Bytes per pixel. 3 for RGB and 4 for RGBW pixels.
|
||||
:param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full
|
||||
brightness
|
||||
:param bool auto_write: True if the neopixels should immediately change when set. If False,
|
||||
`show` must be called explicitly.
|
||||
:param str pixel_order: Set the pixel color channel order. The default is GRB if bpp is set
|
||||
to 3, otherwise GRBW is used as the default.
|
||||
|
||||
Example for Circuit Playground Express:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import neopixel
|
||||
from board import *
|
||||
|
||||
RED = 0x100000 # (0x10, 0, 0) also works
|
||||
|
||||
pixels = neopixel.NeoPixel(NEOPIXEL, 10)
|
||||
for i in range(len(pixels)):
|
||||
pixels[i] = RED
|
||||
|
||||
Example for Circuit Playground Express setting every other pixel red using a slice:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import neopixel
|
||||
from board import *
|
||||
import time
|
||||
|
||||
RED = 0x100000 # (0x10, 0, 0) also works
|
||||
|
||||
# Using ``with`` ensures pixels are cleared after we're done.
|
||||
with neopixel.NeoPixel(NEOPIXEL, 10) as pixels:
|
||||
pixels[::2] = [RED] * (len(pixels) // 2)
|
||||
time.sleep(2)
|
||||
|
||||
.. py:method:: NeoPixel.show()
|
||||
|
||||
Shows the new colors on the pixels themselves if they haven't already
|
||||
been autowritten.
|
||||
|
||||
The colors may or may not be showing after this function returns because
|
||||
it may be done asynchronously.
|
||||
|
||||
.. py:method:: NeoPixel.fill(color)
|
||||
|
||||
Colors all pixels the given ***color***.
|
||||
|
||||
.. py:attribute:: brightness
|
||||
|
||||
Overall brightness of the pixel (0 to 1.0)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pin: microcontroller.Pin,
|
||||
n: int,
|
||||
*,
|
||||
bpp: int = 3,
|
||||
brightness: float = 1.0,
|
||||
auto_write: bool = True,
|
||||
pixel_order: str = None
|
||||
):
|
||||
if not pixel_order:
|
||||
pixel_order = GRB if bpp == 3 else GRBW
|
||||
elif isinstance(pixel_order, tuple):
|
||||
order_list = [RGBW[order] for order in pixel_order]
|
||||
pixel_order = "".join(order_list)
|
||||
|
||||
self._power = None
|
||||
if (
|
||||
sys.implementation.version[0] >= 7
|
||||
and getattr(board, "NEOPIXEL", None) == pin
|
||||
):
|
||||
power = getattr(board, "NEOPIXEL_POWER_INVERTED", None)
|
||||
polarity = power is None
|
||||
if not power:
|
||||
power = getattr(board, "NEOPIXEL_POWER", None)
|
||||
if power:
|
||||
try:
|
||||
self._power = digitalio.DigitalInOut(power)
|
||||
self._power.switch_to_output(value=polarity)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
super().__init__(
|
||||
n, brightness=brightness, byteorder=pixel_order, auto_write=auto_write
|
||||
)
|
||||
|
||||
self.pin = digitalio.DigitalInOut(pin)
|
||||
self.pin.direction = digitalio.Direction.OUTPUT
|
||||
|
||||
def deinit(self) -> None:
|
||||
"""Blank out the NeoPixels and release the pin."""
|
||||
self.fill(0)
|
||||
self.show()
|
||||
self.pin.deinit()
|
||||
if self._power:
|
||||
self._power.deinit()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exception_type: Optional[Type[BaseException]],
|
||||
exception_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
):
|
||||
self.deinit()
|
||||
|
||||
def __repr__(self):
|
||||
return "[" + ", ".join([str(x) for x in self]) + "]"
|
||||
|
||||
@property
|
||||
def n(self) -> int:
|
||||
"""
|
||||
The number of neopixels in the chain (read-only)
|
||||
"""
|
||||
return len(self)
|
||||
|
||||
def write(self) -> None:
|
||||
""".. deprecated: 1.0.0
|
||||
|
||||
Use ``show`` instead. It matches Micro:Bit and Arduino APIs."""
|
||||
self.show()
|
||||
|
||||
def _transmit(self, buffer: bytearray) -> None:
|
||||
neopixel_write(self.pin, buffer)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Open the binary file in read-binary mode
|
||||
with open('test-multiply.bin', 'rb') as file:
|
||||
# Read the entire file contents
|
||||
binary_data = file.read()
|
||||
|
||||
# Convert the binary data to a string of hex bytes
|
||||
hex_string = binary_data.hex()
|
||||
|
||||
# Print the hex string
|
||||
print(hex_string)
|
||||
|
||||
|
||||
# Open the binary file in read-binary mode
|
||||
with open('test-multiply.bin', 'rb') as file:
|
||||
# Read the entire file contents into a bytearray
|
||||
byte_data = bytearray(file.read())
|
||||
|
||||
# Print the bytearray
|
||||
print(byte_data)
|
||||
|
||||
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
import board
|
||||
import busio
|
||||
from adafruit_ht16k33 import matrix
|
||||
i2c = busio.I2C(board.GP17, board.GP16) # scl, sda
|
||||
|
||||
matrix = matrix.Matrix8x8(i2c)
|
||||
matrix.fill(0) # Clear the matrix.
|
||||
matrix[0, 0] = 1
|
||||
matrix.brightness = 1
|
||||
matrix.blink_rate = 2
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
"""Example for pi pico. Blinking LED"""
|
||||
|
||||
import board
|
||||
import digitalio
|
||||
import time
|
||||
|
||||
led = digitalio.DigitalInOut(board.LED)
|
||||
led.direction = digitalio.Direction.OUTPUT
|
||||
|
||||
led.value = True
|
||||
|
||||
for i in range(50):
|
||||
led.value = not led.value
|
||||
time.sleep(0.5)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
"""Example for pi pico. Button-controlled LED.
|
||||
|
||||
Wiring: switch to ground pin on pico, and to pin 18.
|
||||
"""
|
||||
|
||||
import board
|
||||
import digitalio
|
||||
|
||||
led = digitalio.DigitalInOut(board.LED)
|
||||
led.direction = digitalio.Direction.OUTPUT
|
||||
|
||||
switch = digitalio.DigitalInOut(board.GP18)
|
||||
switch.direction = digitalio.Direction.INPUT
|
||||
switch.pull = digitalio.Pull.UP
|
||||
|
||||
while True:
|
||||
led.value = not switch.value
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# pi pico
|
||||
|
||||
import keypad
|
||||
import board
|
||||
|
||||
keymatrix = keypad.KeyMatrix(
|
||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
||||
|
||||
keymap = {
|
||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addrdata",
|
||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"NA" }
|
||||
|
||||
while True:
|
||||
event = keymatrix.events.get()
|
||||
if event:
|
||||
print(event.key_number, event.released, keymap[event.key_number])
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import board
|
||||
import keypad
|
||||
|
||||
keymatrix = keypad.KeyMatrix(
|
||||
row_pins = (board.GP5, board.GP6, board.GP7, board.GP8),
|
||||
column_pins = (board.GP9, board.GP10, board.GP11, board.GP12, board.GP13) )
|
||||
|
||||
keymap = {
|
||||
15:"0", 16:"1", 17:"2", 18:"3", 19:"runhalt",
|
||||
10:"4", 11:"5", 12:"6", 13:"7", 14:"step",
|
||||
5:"8", 6:"9", 7:"A", 8:"B", 9:"addr",
|
||||
0:"C", 1:"D", 2:"E", 3:"F", 4:"data" }
|
||||
|
||||
while True:
|
||||
keypad_event = keymatrix.events.get()
|
||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
||||
if key:
|
||||
print(key)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
"""Example for pi pico. Blinking LED"""
|
||||
|
||||
import board
|
||||
import digitalio
|
||||
import time
|
||||
from tm1637_display import TM1637Display
|
||||
|
||||
display_1 = TM1637Display(board.GP0, board.GP1, length=4)
|
||||
display_1.print("1234")
|
||||
|
||||
display_2 = TM1637Display(board.GP2, board.GP3, length=4)
|
||||
display_2.print("ABCD")
|
||||
|
||||
print("end")
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2013-2024, Kim Davies and contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import time
|
||||
|
||||
class CPU:
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.IP = 254
|
||||
self.acc = 0
|
||||
self.flags = { 'C': False, 'Z': False, 'N': False, 'Eq': False }
|
||||
self.instruction = { 'opcode': False, 'operand': False }
|
||||
self.memory = False
|
||||
|
||||
|
||||
def load_memory(self, bytes):
|
||||
self.memory = bytes + bytearray(256 - len(bytes))
|
||||
# print(self.memory)
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
def step(self):
|
||||
if self.IP >= 255: # TODO CHECK
|
||||
self.IP = 0
|
||||
print("IP:", toHex(self.IP))
|
||||
self.instruction['opcode'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.instruction['operand'] = self.memory[self.IP]
|
||||
self.IP = self.IP+1
|
||||
self.nums2mnems[self.instruction['opcode']](self, self.instruction['operand'])
|
||||
|
||||
print("instr:", toHex(self.instruction['opcode']), toHex(self.instruction['operand']))
|
||||
print("mnem:", self.nums2mnems[self.instruction['opcode']])
|
||||
print("acc:", self.acc, "N:", self.flags['N'])
|
||||
print("running:", self.running)
|
||||
print()
|
||||
# self.print_screen()
|
||||
print("byte 26 (keyboard):", self.memory[26])
|
||||
print()
|
||||
|
||||
def hlt(self, operand):
|
||||
self.running = False
|
||||
|
||||
def nop(self, operand):
|
||||
pass
|
||||
|
||||
def lda_lit(self, operand):
|
||||
self.acc = operand
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def lda_mem(self, operand):
|
||||
self.acc = self.memory[operand]
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sta_lit(self, operand):
|
||||
self.memory[operand] = self.acc
|
||||
|
||||
def sta_mem(self, operand):
|
||||
self.memory[self.memory[operand]] = self.acc
|
||||
|
||||
def add_lit(self, operand):
|
||||
self.acc = self.acc + operand
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def add_mem(self, operand):
|
||||
self.acc = self.acc + self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_lit(self, operand):
|
||||
self.acc = self.acc - operand
|
||||
if self.acc < 0:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def sub_mem(self, operand):
|
||||
self.acc = self.acc - self.memory[operand]
|
||||
if self.acc > 255:
|
||||
self.acc = self.acc % 256
|
||||
self.flags['C'] = True
|
||||
else:
|
||||
self.flags['C'] = False
|
||||
self.flags['Z'] = True if self.acc == 0 else False
|
||||
self.flags['Eq'] = True if self.acc == operand else False
|
||||
self.flags['N'] = True if self.acc > 127 else False
|
||||
|
||||
def jmp_lit(self, operand):
|
||||
self.IP = operand
|
||||
|
||||
def jmp_mem(self, operand):
|
||||
self.IP = self.memory[operand]
|
||||
|
||||
def ske(self, operand): # FIXME
|
||||
# if self.flags['Eq']:
|
||||
# self.IP += 2
|
||||
if self.acc == operand:
|
||||
self.IP += 2
|
||||
|
||||
def skz(self, operand):
|
||||
if self.flags['Z']:
|
||||
self.IP += 2
|
||||
|
||||
def skn(self, operand):
|
||||
if self.flags['N']:
|
||||
self.IP += 2
|
||||
|
||||
def skc(self, operand):
|
||||
if self.flags['C']:
|
||||
self.IP += 2
|
||||
|
||||
def cst(self, operand):
|
||||
self.flags['C'] = True
|
||||
|
||||
def ccl(self, operand):
|
||||
self.flags['C'] = False
|
||||
|
||||
nums2mnems = {
|
||||
0: hlt, # x0
|
||||
1: nop, # x1
|
||||
2: lda_lit, # 02
|
||||
3: sta_lit, # 03
|
||||
4: add_lit, # 04
|
||||
5: sub_lit, # 05
|
||||
6: jmp_lit, # 06
|
||||
7: ske, # x7
|
||||
8: skz, # x8
|
||||
9: skn, # x9
|
||||
10: skc, # A
|
||||
11: cst, # B
|
||||
12: ccl, # C
|
||||
16: hlt, #
|
||||
17: nop, #
|
||||
18: lda_mem, # 12
|
||||
19: sta_mem, # 13
|
||||
20: add_mem, # 14
|
||||
21: sub_mem, # 15
|
||||
22: jmp_mem, # 16
|
||||
23: ske,
|
||||
24: skz,
|
||||
25: skn,
|
||||
26: skc,
|
||||
27: cst,
|
||||
28: ccl,
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
numericKeys = [ "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" ]
|
||||
|
||||
def toHex(n):
|
||||
return "%0.2X" % n
|
||||
|
||||
class Monitor:
|
||||
def __init__(self, cpu):
|
||||
self.cpu = cpu
|
||||
self.monitorMode = 'addressEntry' # or dataEntry
|
||||
self.monitorAddressInput = TwoDigitHexInput()
|
||||
self.monitorDataInput = TwoDigitHexInput()
|
||||
|
||||
# In data entry mode, when a full byte is keyed in,
|
||||
# the next keypress advances to the next address and continues entering data there.
|
||||
# This variable tracks whether it's time to do that or not.
|
||||
self.advanceDataEntryNextPress = False
|
||||
|
||||
def handleKeys(self):
|
||||
keypad_event = keymatrix.events.get()
|
||||
keyPressed = True if (keypad_event and keypad_event.released ) else False
|
||||
key = keymap[keypad_event.key_number] if keyPressed else False
|
||||
numericKeyPressed = True if (keyPressed and (key in numericKeys)) else False
|
||||
|
||||
|
||||
if self.cpu.running:
|
||||
if key == "runhalt":
|
||||
print("HALT PRESSED")
|
||||
self.cpu.running = False
|
||||
time.sleep(0.5) # lazy debounce
|
||||
# km.events.clear() # don't track keypresses from during the run
|
||||
|
||||
if numericKeyPressed:
|
||||
self.cpu.memory[26] = int(key, 16)
|
||||
|
||||
elif not self.cpu.running:
|
||||
if key == "runhalt":
|
||||
self.cpu.running = True
|
||||
print("\nSTARTING")
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if key == "addr":
|
||||
self.monitorMode = 'addressEntry'
|
||||
print("\nENTERING", self.monitorMode, "MODE")
|
||||
self.monitorAddressInput.currentDigit = 0
|
||||
time.sleep(0.5) # lazy debounce
|
||||
if key == "data":
|
||||
self.monitorMode = 'dataEntry'
|
||||
print("\nENTERING", self.monitorMode, "MODE")
|
||||
self.monitorDataInput.clear()
|
||||
self.advanceDataEntryNextPress = False
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if key == "step":
|
||||
print("\nSINGLE STEP FROM MONITOR ADDR")
|
||||
# self.IP = self.monitorAddressInput.value
|
||||
self.cpu.step()
|
||||
time.sleep(0.5) # lazy debounce
|
||||
|
||||
if numericKeyPressed:
|
||||
if self.monitorMode == 'addressEntry':
|
||||
self.monitorAddressInput.input(int(key, 16))
|
||||
self.cpu.IP = self.monitorAddressInput.value
|
||||
print("MA", self.cpu.IP)
|
||||
|
||||
if self.monitorMode == 'dataEntry':
|
||||
if self.advanceDataEntryNextPress:
|
||||
print("ADVANCING")
|
||||
self.cpu.IP = (self.cpu.IP + 1) % 256
|
||||
# self.monitorDataInput.clear() # reset .currentDigit
|
||||
self.monitorDataInput.set(self.cpu.memory[self.cpu.IP])
|
||||
self.advanceDataEntryNextPress = False
|
||||
self.monitorDataInput.input(int(key, 16))
|
||||
self.cpu.memory[self.cpu.IP] = self.monitorDataInput.value
|
||||
print("MD", self.monitorDataInput.value)
|
||||
if self.monitorDataInput.currentDigit == 0: # that was the second keypress, so next keypress is for the next address
|
||||
self.advanceDataEntryNextPress = True
|
||||
|
||||
print("Acc", self.cpu.acc, "IP", self.cpu.IP, "Data", self.cpu.memory[self.cpu.IP], "\n")
|
||||
|
||||
|
||||
def displayScreen(self):
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
matrix[x, y] = self.cpu.memory[x + (8*y)]
|
||||
|
||||
|
||||
|
||||
def run(self):
|
||||
#self.cpu.start()
|
||||
t = time.time()
|
||||
while (time.time() - t) < 120: # TODO: add a time delta or sth maybe so this doesn't just burn cycles
|
||||
self.handleKeys()
|
||||
display_1.print(toHex(self.cpu.IP) + toHex(self.cpu.memory[self.cpu.IP]))
|
||||
# display_1.print(toHex(self.monitorAddressInput.value) + toHex(self.cpu.memory[self.cpu.IP]))
|
||||
# display_2.print(toHex(self.cpu.IP) + toHex(self.cpu.acc))
|
||||
display_2.print(toHex(self.cpu.acc))
|
||||
self.displayScreen()
|
||||
if self.cpu.running:
|
||||
self.cpu.step()
|
||||
# time.sleep(0.5) # TODO ?
|
||||
print("timeout")
|
||||
print(self.cpu.memory)
|
||||
|
||||
|
||||
cpu = CPU()
|
||||
monitor = Monitor(cpu)
|
||||
|
||||
# preamble = '00 ' * 64
|
||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 12 f0 05 41 08 00 06 40 00 00' # STRIPES
|
||||
# offset = 64
|
||||
# prog = preamble + '02 01 13 f0 12 f0 04 02 03 f0 05 08 09 00 04 09 03 f0 07 41 06' + toHex(offset) + '00 00'
|
||||
#prog = '00'
|
||||
# program_bytes = bytearray.fromhex(prog.replace(" ", ""))
|
||||
|
||||
# Add jmp at addr 254:
|
||||
#program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0600') # jump to addr 00
|
||||
# program_with_jump = program_bytes + bytearray(254 - len(program_bytes)) + bytearray.fromhex('0640') # jump to addr 0x40 (dec 64)
|
||||
|
||||
with open('test-multiply2.bin', 'rb') as file:
|
||||
program_bytes = bytearray(file.read())
|
||||
|
||||
cpu.load_memory(program_bytes)
|
||||
|
||||
monitor.run()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import cpu
|
||||
|
||||
c = cpu.CPU()
|
||||
|
||||
print(c)
|
||||
130
readme.md
130
readme.md
|
|
@ -1,130 +0,0 @@
|
|||
# Cardiograph Mark I — simulator for a paper 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```
|
||||
|
||||
## Instruction set
|
||||
|
||||
00 END
|
||||
01 STO lit# ; store ... mem[lit#] <- A
|
||||
02 STO addr ; store ... mem[mem[addr]] <- A
|
||||
03 LDA lit# ; load ... A <- lit#
|
||||
04 LDA addr ; load ... A <- mem[addr]
|
||||
05 ADD lit# ; add ... A <- A + lit# ... and un/set carry flag
|
||||
06 ADD addr ; add ... A <- A + mem[addr] ... and un/set carry flag
|
||||
07 SUB lit# ; sub ... A <- A - lit# ... and un/set carry flag
|
||||
08 SUB addr ; sub ... A <- A - mem[addr] ... and un/set carry flag
|
||||
09 HOP lit# ; hop ... skip next instruction if A == lit# ... when true: IP <- PC + 4
|
||||
0A HOP addr ; hop ... skip next instruction if A == addr ... when true: IP <- PC + 4
|
||||
0B JMP lit# ; jump ... IP <- lit#
|
||||
0C JMP addr ; jump ... IP <- addr
|
||||
0D FTG lit# ; toggle flag by number (see details below)
|
||||
0E FHP lit# ; flag hop ... skip next instruction if flag is set ... when true: IP <- PC + 4
|
||||
0F NOP ———— ; no operation
|
||||
|
||||
- Instructions are two bytes long:
|
||||
one byte for the opcode, one for the operand
|
||||
|
||||
|
||||
## Registers and Flags
|
||||
|
||||
- `A` - accumulator
|
||||
- `IP` - instruction pointer (aka program counter)
|
||||
- `FLAGS` - flags: **N**egative, **Z**ero, **O**verflow, **C**arry
|
||||
- in machine language, each flag is given a number:
|
||||
- N = 3
|
||||
Z = 2
|
||||
O = 1
|
||||
C = 0
|
||||
- (bitwise, `0000 = NZOC`)
|
||||
|
||||
## Memory map / Peripherals
|
||||
|
||||
- `00-0F` - display (4x4)
|
||||
- `10-19` - reserved for future use
|
||||
- `20 ` - keypad - value of the most recent keypress
|
||||
- `21 ` - pointer to display memory
|
||||
- `22 ` - pointer to keypad memory
|
||||
- `23-2F` - reserved for future use / variable storage
|
||||
- `30 ` - initial value for IP
|
||||
- `30-FF` - free
|
||||
|
||||
### Keypad
|
||||
|
||||
The value of the latest keypress on a hex keypad is stored at `$20`.
|
||||
(The keypad can also be relocated by changing the value of the pointer-to-keypad at `$22`.)
|
||||
|
||||
The keypad uses the same layout as the COSMAC VIP (and CHIP-8):
|
||||
|
||||
```
|
||||
1 2 3 C
|
||||
4 5 6 D
|
||||
7 8 9 E
|
||||
A 0 B F
|
||||
```
|
||||
The CPU simulator maps the following Qwerty keys onto those values:
|
||||
|
||||
```
|
||||
1 2 3 4
|
||||
Q W E R
|
||||
A S D F
|
||||
Z X C V
|
||||
```
|
||||
|
||||
## 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 *ADDR ; `*ADDR` is a magic value referencing the memory address
|
||||
; that the current line will store at after assembly
|
||||
|
||||
- Hexadecimal numbers are preceded by a `$`
|
||||
- Whitespace is ignored
|
||||
|
|
@ -1,32 +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 { machine } = require('os');
|
||||
|
||||
const mode = process.argv[2];
|
||||
const filename = process.argv[3];
|
||||
const inputFile_str = fs.readFileSync(filename, 'utf8');
|
||||
|
||||
let machineCode;
|
||||
|
||||
if (mode === "debug") {
|
||||
machineCode = assembler.assemble(inputFile_str, true);
|
||||
console.group("Machine code output");
|
||||
logMemory(machineCode);
|
||||
console.groupEnd();
|
||||
} else {
|
||||
machineCode = assembler.assemble(inputFile_str);
|
||||
let output = '';
|
||||
if (mode === 'runbin') { // print binary output
|
||||
machineCode.forEach((n) => output = `${output} ${num2bin(n)}`);
|
||||
} else { // print hex output
|
||||
machineCode.forEach((n) => output = `${output} ${num2hex(n)}`);
|
||||
}
|
||||
console.log(output);
|
||||
}
|
||||
30
run-cpu.js
30
run-cpu.js
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Run: `./run-cpu.js run assembly.asm`
|
||||
// Debug: `./run-cpu.js debug assembly.asm`
|
||||
// Run with single-stepping: `./run-cpu.js step assembly.asm`
|
||||
// Debug with single-stepping: `./run-cpu.js stepdebug assembly.asm`
|
||||
|
||||
const fs = require('fs');
|
||||
const computer = require('./cpu.js');
|
||||
const assembler = require('./assembler.js');
|
||||
const { logRunningHeader, logMemory } = require('./logging.js');
|
||||
|
||||
const mode = process.argv[2];
|
||||
|
||||
// console.log(`Reading ${filename}`);
|
||||
const filename = process.argv[3];
|
||||
const inputFile_str = fs.readFileSync(filename, 'utf8');
|
||||
|
||||
let machineCode = assembler.assemble(inputFile_str);
|
||||
if (mode === "debug") {
|
||||
logRunningHeader();
|
||||
computer.runProgram(machineCode, true);
|
||||
} else if (mode === "stepdebug") {
|
||||
logRunningHeader();
|
||||
computer.singleStepProgram(machineCode, true);
|
||||
} else if (mode === "step") {
|
||||
computer.singleStepProgram(machineCode, false);
|
||||
} else {
|
||||
computer.runProgram(machineCode, false, 200);
|
||||
}
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
// This is a sketch of a simulator for my paper computer,
|
||||
// made for two purposes:
|
||||
//
|
||||
// (1) figuring out the basic structure for a simple simulator
|
||||
// (2) testing some simple programs (hopefully)
|
||||
//
|
||||
// NOTA BENE: this simple version makes naive use of
|
||||
// Javascript numbers for opcodes, and a Javascript array
|
||||
// for the computer's "memory." This may cause some problems.
|
||||
|
||||
function CPU(mem) {
|
||||
this.memory = mem;
|
||||
this.running = false;
|
||||
this.instructionPointer = 0;
|
||||
this.carryFlag = 0;
|
||||
this.acc = 0;
|
||||
|
||||
this.instructions = {
|
||||
end: () => {
|
||||
console.log('END');
|
||||
this.running = false
|
||||
},
|
||||
|
||||
store_lit: (lit) => {
|
||||
console.log('STO lit#');
|
||||
this.memory[lit] = this.acc;
|
||||
log_table_with_title(this.memory, 'Current memory');
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
store_addr: (addr) => {
|
||||
console.log('STO addr');
|
||||
this.memory[this.memory[addr]] = this.acc;
|
||||
log_table_with_title(this.memory, 'Memory');
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
load_lit: (lit) => {
|
||||
console.log('LDA lit#');
|
||||
this.acc = lit;
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
load_addr: (addr) => {
|
||||
console.log('LDA addr');
|
||||
console.log('mem at addr: ', this.memory[addr]);
|
||||
this.acc = this.memory[addr];
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
add_lit: (lit) => {
|
||||
console.log("ADD lit");
|
||||
if ( (this.acc + lit) > 15 ) { this.carryFlag = 1; }
|
||||
this.acc = ((this.acc + lit) % 15);
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
add_addr: (addr) => {
|
||||
console.log("ADD addr");
|
||||
if ( (this.acc + this.memory[addr]) > 15 ) { this.carryFlag = 1; }
|
||||
this.acc = ((this.acc + this.memory[addr]) % 15);
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
sub_lit: (lit) => { // TODO: carry flag
|
||||
console.log("SUB lit");
|
||||
this.acc = this.acc - lit;
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
sub_addr: (addr) => { // TODO: carry flag
|
||||
console.log("SUB addr");
|
||||
this.acc = this.acc - this.memory[addr];
|
||||
this.instructionPointer = this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
hop_lit: (lit) => {
|
||||
console.log("HOP lit");
|
||||
console.log(" ↳ Memory at IP+1:", this.memory[this.instructionPointer+1]);
|
||||
if (this.acc === lit) {
|
||||
this.instructionPointer += 2;
|
||||
} else {
|
||||
this.instructionPointer += 1;
|
||||
}
|
||||
},
|
||||
|
||||
hop_addr: (addr) => {
|
||||
console.log("HOP addr");
|
||||
if (this.acc === this.memory[addr]) {
|
||||
this.instructionPointer += 2;
|
||||
} else {
|
||||
this.instructionPointer += 1;
|
||||
}
|
||||
},
|
||||
|
||||
jump_lit: (lit) => {
|
||||
console.log("JMP lit");
|
||||
this.instructionPointer = lit;
|
||||
},
|
||||
|
||||
jump_addr: (addr) => {
|
||||
console.log("JMP addr");
|
||||
this.instructionPointer = this.memory[addr];
|
||||
},
|
||||
|
||||
carry_clear: () => {
|
||||
console.log("CFC");
|
||||
this.carryFlag = 0;
|
||||
this.instructionPointer += 1;
|
||||
},
|
||||
|
||||
carry_hop: () => {
|
||||
console.log("CHP");
|
||||
if (this.carryFlag != 0) {
|
||||
this.instructionPointer += 2;
|
||||
} else {
|
||||
this.instructionPointer += 1;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.perform_operation = (opcode, arg) => {
|
||||
switch (opcode) {
|
||||
case 0:
|
||||
this.instructions.end(arg);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
this.instructions.store_lit(arg);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
this.instructions.store_addr(arg);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
this.instructions.load_lit(arg);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
this.instructions.load_addr(arg);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
this.instructions.add_lit(arg);
|
||||
break;
|
||||
|
||||
case 6:
|
||||
this.instructions.add_addr(arg);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
this.instructions.sub_lit(arg);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
this.instructions.sub_addr(arg);
|
||||
break;
|
||||
|
||||
case 9:
|
||||
this.instructions.hop_lit(arg);
|
||||
break;
|
||||
|
||||
case 10:
|
||||
this.instructions.hop_addr(arg);
|
||||
break;
|
||||
|
||||
case 11:
|
||||
this.instructions.jump_lit(arg);
|
||||
break;
|
||||
|
||||
case 12:
|
||||
this.instructions.jump_addr(arg);
|
||||
break;
|
||||
|
||||
case 13:
|
||||
this.instructions.carry_clear(arg);
|
||||
break;
|
||||
|
||||
case 14:
|
||||
this.instructions.carry_hop(arg);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error( `Invalid opcode: ${opcode} with argument ${arg}` );
|
||||
}
|
||||
}
|
||||
|
||||
this.run_program = () => {
|
||||
const initialMemory = JSON.parse(JSON.stringify(this.memory)); // Hack to make a copy-by-value -- https://stackoverflow.com/questions/18829099/copy-a-variables-value-into-another
|
||||
console.log();
|
||||
console.log("————————————————————————————————————————");
|
||||
let time = new Date();
|
||||
console.log( `Running at ${time.toLocaleTimeString('en-US')}` );
|
||||
console.log("————————————————————————————————————————");
|
||||
log_debug_state();
|
||||
|
||||
this.running = true;
|
||||
|
||||
for (let i = 1; i < 16; i++) {
|
||||
if ( this.running &&
|
||||
(this.instructionPointer < this.memory.length) ) {
|
||||
let op_arg_tuple = this.memory[this.instructionPointer];
|
||||
console.group("Proccessing instruction");
|
||||
console.log( op_arg_tuple );
|
||||
// console.log( `processing opcode ${op_arg_tuple[0]} with arg ${op_arg_tuple[1]}` );
|
||||
this.perform_operation(op_arg_tuple[0], op_arg_tuple[1]);
|
||||
log_debug_state();
|
||||
console.groupEnd("Processing instruction");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
memoryAtStart: initialMemory,
|
||||
memoryAtEnd: this.memory
|
||||
}
|
||||
}
|
||||
|
||||
log_debug_state = () => {
|
||||
console.log();
|
||||
console.group('CPU state');
|
||||
console.log( `Acc: ${this.acc} IP: ${this.instructionPointer} CF: ${this.carryFlag} ${this.running ? "running" : "halted" }` );
|
||||
console.log();
|
||||
console.groupEnd('CPU state');
|
||||
};
|
||||
};
|
||||
|
||||
log_table_with_title = (memory, tableTitle) => {
|
||||
console.log();
|
||||
console.group(tableTitle);
|
||||
console.table(memory);
|
||||
console.groupEnd(tableTitle);
|
||||
};
|
||||
|
||||
|
||||
// TESTS
|
||||
|
||||
let halt_and_catch_fire = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
];
|
||||
|
||||
let test_lda_sto = [
|
||||
[3, 8], // LDA lit
|
||||
[1, 5], // STO lit
|
||||
[4, 5], // LDA addr
|
||||
[2, 6], // STO addr
|
||||
[0, 0], // END
|
||||
];
|
||||
|
||||
let test_add_sub_nocarry = [
|
||||
[5, 6], // ADD lit ... acc = 6
|
||||
[7, 1], // SUB lit ... acc = 5
|
||||
[1, 8], // STO lit ... mem[8] = 5
|
||||
[6, 8], // ADD addr ... acc = 10
|
||||
[8, 8], // SUB addr ... acc = 5
|
||||
[0, 0], // END
|
||||
]
|
||||
let test_add_sub = [
|
||||
[5, 26], // ADD lit
|
||||
[0, 0], // END
|
||||
]
|
||||
|
||||
let test_hop = [
|
||||
[5, 8], // ADD lit ... acc = 8
|
||||
[9, 8], // HOP lit ... hop over next op if acc = 8
|
||||
[0, 0], // END ... (hopped over)
|
||||
[7, 8], // SUB lit ... acc = 0
|
||||
[0, 0]
|
||||
]
|
||||
|
||||
let test_jmp = [
|
||||
[11, 4], // 0
|
||||
[0, 0], // 1 ... END ... JMP'd over
|
||||
[0, 0], // 2
|
||||
[0, 0], // 3
|
||||
[5, 8], // 4 ... ADD lit ... acc = 8
|
||||
[0, 0], // 5 ... END
|
||||
]
|
||||
|
||||
let test_chp = [
|
||||
[5, 8], // ADD lit ... acc = 8
|
||||
[14, 0], // CHP ... shouldn't hop (CF = 0)
|
||||
[5, 1], // ADD lit ... acc = 9
|
||||
[5, 8], // ADD lit ... acc = 1 and CF = 1
|
||||
[14, 0], // CHP ... hop! (CF = 1)
|
||||
[0, 0], // END
|
||||
[7, 1], // SUB lit ... acc = 0
|
||||
[13, 0], // CFC ... CF = 0
|
||||
[0, 0], // END
|
||||
]
|
||||
|
||||
//let comp = new CPU(test_chp);
|
||||
|
||||
let comp = new CPU(test_lda_sto);
|
||||
let memory_snapshots = comp.run_program();
|
||||
log_table_with_title(memory_snapshots.memoryAtEnd, 'Memory after running');
|
||||
log_table_with_title(memory_snapshots.memoryAtStart, 'Memory before running');
|
||||
|
||||
// TODO: TEST HOP_addr
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
// NOTES:
|
||||
//
|
||||
// - instructions are two bytes long:
|
||||
// one byte for the opcode, one for the argument
|
||||
|
||||
// STATE
|
||||
|
||||
const CPU = {
|
||||
running: false,
|
||||
IP: 0,
|
||||
CF: 0,
|
||||
Acc: 0,
|
||||
memory: null,
|
||||
loadMemory: (data) => { // data: Uint8Array
|
||||
// TODO: check length of data
|
||||
CPU.memory = data;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT MODIFY STATE
|
||||
|
||||
const Instructions = {
|
||||
end: () => {
|
||||
console.log('END');
|
||||
CPU.running = false;
|
||||
},
|
||||
|
||||
store_lit: (lit) => {
|
||||
console.log('STO lit#');
|
||||
CPU.memory[lit] = CPU.Acc;
|
||||
logTableTitled(CPU.memory, 'Current memory');
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
store_addr: (addr) => {
|
||||
console.log('STO addr');
|
||||
CPU.memory[CPU.memory[addr]] = CPU.Acc;
|
||||
logTableTitled(CPU.memory, 'Memory');
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
load_lit: (lit) => {
|
||||
console.log('LDA lit#');
|
||||
CPU.Acc = lit;
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
load_addr: (addr) => {
|
||||
console.log('LDA addr');
|
||||
console.log('mem at addr: ', CPU.memory[addr]);
|
||||
CPU.Acc = CPU.memory[addr];
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
add_lit: (lit) => {
|
||||
console.log("ADD lit");
|
||||
let sum = CPU.Acc + lit;
|
||||
if (sum > 15) {
|
||||
CPU.CF = 1;
|
||||
CPU.Acc = (sum % 15) - 1;
|
||||
} else {
|
||||
CPU.CF = 0;
|
||||
CPU.Acc = sum;
|
||||
}
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
add_addr: (addr) => {
|
||||
console.log("ADD addr");
|
||||
let sum = CPU.Acc + CPU.memory[addr];
|
||||
if (sum > 15) {
|
||||
CPU.CF = 1;
|
||||
CPU.Acc = (sum % 15) - 1;
|
||||
} else {
|
||||
CPU.CF = 0;
|
||||
CPU.Acc = sum;
|
||||
}
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
sub_lit: (lit) => { // TODO: carry flag
|
||||
console.log("SUB lit");
|
||||
CPU.Acc = CPU.Acc - lit;
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
sub_addr: (addr) => { // TODO: carry flag
|
||||
console.log("SUB addr");
|
||||
CPU.Acc = CPU.Acc - CPU.memory[addr];
|
||||
CPU.IP = CPU.IP += 2;
|
||||
},
|
||||
|
||||
hop_lit: (lit) => {
|
||||
console.log("HOP lit");
|
||||
console.log(` ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`);
|
||||
if (CPU.Acc === lit) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
|
||||
hop_addr: (addr) => {
|
||||
console.log("HOP addr");
|
||||
if (CPU.Acc === CPU.memory[addr]) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
|
||||
jump_lit: (lit) => {
|
||||
console.log("JMP lit");
|
||||
CPU.IP = lit;
|
||||
},
|
||||
|
||||
jump_addr: (addr) => {
|
||||
console.log("JMP addr");
|
||||
CPU.IP = CPU.memory[addr];
|
||||
},
|
||||
|
||||
carry_clear: () => {
|
||||
console.log("CFC");
|
||||
CPU.CF = 0;
|
||||
CPU.IP += 2;
|
||||
},
|
||||
|
||||
carry_hop: () => {
|
||||
console.log("CHP");
|
||||
console.log(` ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`);
|
||||
console.table(CPU.memory);
|
||||
if (CPU.CF != 0) {
|
||||
CPU.IP += 4;
|
||||
} else {
|
||||
CPU.IP += 2;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const opcodes2mnemonics = {
|
||||
0: (arg) => Instructions.end(arg),
|
||||
1: (arg) => Instructions.store_lit(arg),
|
||||
2: (arg) => Instructions.store_addr(arg),
|
||||
3: (arg) => Instructions.load_lit(arg),
|
||||
4: (arg) => Instructions.load_addr(arg),
|
||||
5: (arg) => Instructions.add_lit(arg),
|
||||
6: (arg) => Instructions.add_addr(arg),
|
||||
7: (arg) => Instructions.sub_lit(arg),
|
||||
8: (arg) => Instructions.sub_addr(arg),
|
||||
9: (arg) => Instructions.hop_lit(arg),
|
||||
10: (arg) => Instructions.hop_addr(arg),
|
||||
11: (arg) => Instructions.jump_lit(arg),
|
||||
12: (arg) => Instructions.jump_addr(arg),
|
||||
13: (arg) => Instructions.carry_clear(arg),
|
||||
14: (arg) => Instructions.carry_hop(arg),
|
||||
};
|
||||
|
||||
function stepCPU() {
|
||||
console.group("Step CPU");
|
||||
let opcode = CPU.memory[CPU.IP];
|
||||
let argument = CPU.memory[CPU.IP+1];
|
||||
|
||||
console.log(`OP: ${opcode} ARG: ${argument}`);
|
||||
|
||||
let instruction = opcodes2mnemonics[opcode];
|
||||
instruction(argument);
|
||||
|
||||
logCPUState();
|
||||
console.groupEnd("Step CPU");
|
||||
}
|
||||
|
||||
function runCPU() {
|
||||
const initialMemory = JSON.parse(JSON.stringify(CPU.memory)); // Hack to make a copy-by-value -- https://stackoverflow.com/questions/18829099/copy-a-variables-value-into-another
|
||||
console.log();
|
||||
console.log("————————————————————————————————————————");
|
||||
let time = new Date();
|
||||
console.log( `Running at ${time.toLocaleTimeString('en-US')}` );
|
||||
console.log("————————————————————————————————————————");
|
||||
logCPUState();
|
||||
|
||||
CPU.running = true;
|
||||
for (let i = 0; i < 255; i++) { // FIXME: temporary limit as a lazy way to halt infinite loops
|
||||
if (!CPU.running) break;
|
||||
if (CPU.IP >= CPU.memory.length) break;
|
||||
stepCPU();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
|
||||
|
||||
function logCPUState() {
|
||||
console.log();
|
||||
console.group('CPU state');
|
||||
console.log( `Acc: ${CPU.Acc} IP: ${CPU.IP} CF: ${CPU.CF} ${CPU.running ? "running" : "halted" }` );
|
||||
console.log();
|
||||
console.groupEnd('CPU state');
|
||||
};
|
||||
|
||||
|
||||
// FUNCTIONS FOR DISPLAYING DATA
|
||||
|
||||
function num2hex(num) { return num.toString(16) };
|
||||
function hex2num(hex) { return parseInt(hex, 16) };
|
||||
|
||||
logTableTitled = (memory, tableTitle) => {
|
||||
console.log();
|
||||
console.group(tableTitle);
|
||||
console.table(memory);
|
||||
console.groupEnd(tableTitle);
|
||||
};
|
||||
|
||||
|
||||
// RUN IT !
|
||||
|
||||
const test_lda_sto = new Uint8Array([
|
||||
3, 17, // LDA lit ... Acc = 17
|
||||
1, 14, // STO lit ... @14 = Acc = 17
|
||||
3, 16, // LDA lit ... Acc = 16
|
||||
1, 15, // STO lit ... @15 = Acc = 16
|
||||
2, 15, // STO addr ... @[@15] = Acc = 16 ... mem[mem[addr]] <- Acc
|
||||
4, 0, // LDA addr ... Acc = @00 = 03
|
||||
0, 0, // END
|
||||
0, 0, // DATA
|
||||
]);
|
||||
|
||||
let test_add_sub_nocarry = new Uint8Array([
|
||||
5, 6, // ADD lit ... acc = 6
|
||||
7, 1, // SUB lit ... acc = 5
|
||||
1, 15, // STO lit ... mem[15] = 5
|
||||
6, 15, // ADD addr ... acc = 10
|
||||
8, 15, // SUB addr ... acc = 5
|
||||
0, 0, // END
|
||||
0, 0,
|
||||
0, 0,
|
||||
]);
|
||||
|
||||
let test_hop = new Uint8Array([
|
||||
5, 8, // ADD lit ... acc = 8
|
||||
9, 8, // HOP lit ... hop over next op if acc = 8
|
||||
0, 0, // END ... (hopped over)
|
||||
7, 8, // SUB lit ... acc = 0
|
||||
0, 0
|
||||
]);
|
||||
|
||||
let test_jmp = new Uint8Array([
|
||||
11, 8, // 0 ... JMP lit
|
||||
0, 0, // 2 ... END ... JMP'd over
|
||||
0, 0, // 4
|
||||
0, 0, // 6
|
||||
5, 8, // 8 ... ADD lit ... acc = 8
|
||||
0, 0, // 10 ... END
|
||||
]);
|
||||
|
||||
|
||||
const test_chp = new Uint8Array([
|
||||
5, 8, // ADD lit ... Acc = 8
|
||||
14, 0, // CHP ... shouldn't hop (CF = 0)
|
||||
5, 1, // ADD lit ... Acc = 9
|
||||
5, 8, // ADD lit ... Acc = 1 and CF = 1
|
||||
14, 0, // CHP ... hop! (CF = 1)
|
||||
0, 0, // END
|
||||
7, 1, // SUB lit ... Acc = 0
|
||||
13, 0, // CFC ... CF = 0
|
||||
0, 0, // END
|
||||
]);
|
||||
|
||||
//CPU.loadMemory(test_lda_sto);
|
||||
//CPU.loadMemory(test_add_sub_nocarry);
|
||||
//CPU.loadMemory(test_hop);
|
||||
CPU.loadMemory(test_jmp);
|
||||
//CPU.loadMemory(test_chp);
|
||||
runCPU();
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
const readline = require('readline');
|
||||
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
|
||||
if (process.stdin.setRawMode != null) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
process.stdin.on('keypress', (str, key) => {
|
||||
console.log(str)
|
||||
console.log(key)
|
||||
if (key.sequence === '\x03') process.exit();
|
||||
})
|
||||
|
||||
let i = 0;
|
||||
const loop = setInterval(async () => {
|
||||
console.log('loop #', i);
|
||||
if (i > 10) clearInterval(loop);
|
||||
i += 1;
|
||||
}, 250);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
const readlineSync = require('readline-sync');
|
||||
|
||||
let key = readlineSync.keyIn('? ')
|
||||
console.log(key);
|
||||
|
||||
/* This works without external dependencies,
|
||||
* but it's for a full line at a time
|
||||
const readline = require('readline/promises');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
async function getInput(prompt) {
|
||||
let input = await rl.question(prompt)
|
||||
console.log(input);
|
||||
console.log("Later");
|
||||
rl.close();
|
||||
}
|
||||
|
||||
getInput('?');
|
||||
*/
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
;; Fill display with $FF
|
||||
|
||||
LDA $00 ; Start of display
|
||||
STO $21 ; Pixel index
|
||||
|
||||
; store the $FF that we'll use to fill the screen
|
||||
LDA $FF
|
||||
STO $22
|
||||
|
||||
@copy-to-display
|
||||
LDA ($22) ; A = mem[$22] = $FF
|
||||
STO ($21) ; mem[mem[$21]] = A = $FF
|
||||
|
||||
; increment pixel index
|
||||
LDA ($21)
|
||||
ADD $01
|
||||
STO $21
|
||||
|
||||
; if CF is set, then the display is full and we're done
|
||||
FHP 0
|
||||
JMP @copy-to-display
|
||||
|
||||
END
|
||||
Loading…
Reference in New Issue