Compare commits
44 Commits
sketch-low
...
main
| 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 | |
|
|
0b91a71575 | |
|
|
144ae8de6c | |
|
|
dee5b4afd4 | |
|
|
e6883fbc65 | |
|
|
072d2ccdb5 | |
|
|
9f4a67770f |
|
|
@ -2,4 +2,6 @@
|
|||
.vscode
|
||||
*.tmp.*
|
||||
node_modules
|
||||
cardiograph.code-workspace
|
||||
cardiograph.code-workspace
|
||||
*venv*
|
||||
*__pycache__
|
||||
|
|
|
|||
|
|
@ -1,3 +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
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Cardiograph issues
|
||||
|
||||
## Open
|
||||
|
||||
### #1 - Improve CLI interface
|
||||
|
||||
I'm thinking of an interface like this...
|
||||
|
||||
$ ./cpu.js -mc code.bin
|
||||
$ ./cpu.js code.asm
|
||||
$ ./cpu.js --debug code.asm
|
||||
|
||||
Full list of flags I want:
|
||||
|
||||
-d --debug
|
||||
-s --singlestep
|
||||
-p --prettydisplay
|
||||
-mc --machinecode
|
||||
|
||||
### #2 - Startup: Execute `JMP $FF`
|
||||
|
||||
See [2023-08-24](../notes/2023-08-24--dev-notes.md#cpu-start-up)
|
||||
|
||||
... say that there's a 1-byte ROM at $FF.
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FE` - free
|
||||
- `FF ` - ROM (unwriteable) - pointer to initial IP
|
||||
|
||||
- store `$1D` at `$FF`
|
||||
- make CPU execute `JMP $FF` on startup
|
||||
- make ROM unwriteable
|
||||
|
||||
More step-by-step:
|
||||
|
||||
- Change memory from a Uint8Array to a regular array,
|
||||
and make every entry { number | { type: 'ROM', value: number }}
|
||||
- Store ROM as an object in machine.config.js
|
||||
- Load ROM data into memory at CPU startup (`startCPU(RAM, ROM)`)
|
||||
|
||||
|
||||
## Closed
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# To do — Summary
|
||||
|
||||
This is a quick todo list.
|
||||
|
||||
For extended commentary, see [issues](issues.md).
|
||||
|
||||
## Open
|
||||
|
||||
### Todo
|
||||
|
||||
- Finish WIP on run-cli arg parsing
|
||||
- Pass CYCLE_COUNT as a cli arg
|
||||
|
||||
- (cpu) !! Fix overflow flag
|
||||
- Add a flag for bank-switching to the ~zero-page
|
||||
- Remove run-scripts and add the ability to run `./cpu.js` and `./assembler.js` directly -- cf. [#1](issues.md#1---improve-cli-interface)
|
||||
- [fix] (cpu) Make single-stepping work with simulated keypad
|
||||
|
||||
### Features
|
||||
|
||||
- (cpu) allow arrow keys, too
|
||||
- [fix] (cpu) Execute `JMP $FF` on startup / Implement ROM — see [#2](issues.md#2---startup-execute-jmp-ff)
|
||||
- (assembler) Validate labels
|
||||
- (assembler) Extract debugging to its own module
|
||||
- (cpu) Consider adding a VIP-style keypad-based machine code monitor
|
||||
- (cpu) Add a mode that prints the display as text, but still animates
|
||||
- (cpu) Allow running pre-compiled machine code
|
||||
- (cpu) DRY out addition and subtraction
|
||||
- [Extended system (secret bonus operations)](../notes/2023-08-07--dev-notes.md)
|
||||
- (research) Review CHIP-8
|
||||
- read about the spec / ISA
|
||||
- read these, and add them to the bibliography:
|
||||
- Steve Losh: https://stevelosh.com/blog/2016/12/chip8-input/
|
||||
- https://tonisagrista.com/blog/2021/chip8-spec/
|
||||
|
||||
### Documentation
|
||||
|
||||
- Improve docs for flags register
|
||||
|
||||
### Testing
|
||||
|
||||
- Display (hex) numbers
|
||||
- Greater than
|
||||
- Minimal LOGO-ish interpreter for turtle graphics
|
||||
|
||||
|
||||
## Closed
|
||||
|
||||
- 2023-08-26 - [fix] (logging) - 'undefined operand' situation is caused by assembling to an initial IP of $1C, which is an odd number
|
||||
- (assembler) Pass asm line thru to cpu to print when debugging
|
||||
|
||||
|
||||
## Abandoned
|
||||
|
||||
- (assembler) Return pure machine code when printing to stdout (and not in debug mode)
|
||||
|
|
@ -173,7 +173,7 @@ function handleConstantDefinitions(op, arg, IP, constants) {
|
|||
* 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 {{ sourceInfo: Object, machineCode: Array }};
|
||||
* @return {{ sourceAnnotations: Object, machineCode: Array }};
|
||||
**/
|
||||
// TODO rename?
|
||||
function decodeInstructions(source) {
|
||||
|
|
@ -203,7 +203,7 @@ function decodeInstructions(source) {
|
|||
/** @type {Array<number>} - Assembled source code, as an array of bytes **/
|
||||
let machineCode = new Array(IP).fill(0);
|
||||
|
||||
let sourceInfo = {};
|
||||
let sourceAnnotations = {};
|
||||
|
||||
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
|
||||
machineCode[CFG.pointerToDisplay] = CFG.displayAddr;
|
||||
|
|
@ -343,7 +343,7 @@ function decodeInstructions(source) {
|
|||
machineCode[IP] = decodedOp;
|
||||
machineCode[IP + 1] = decodedArg;
|
||||
|
||||
sourceInfo[IP] = {
|
||||
sourceAnnotations[IP] = {
|
||||
lineNumber: line.number,
|
||||
source: line.source,
|
||||
address: IP,
|
||||
|
|
@ -381,7 +381,7 @@ function decodeInstructions(source) {
|
|||
}
|
||||
}
|
||||
|
||||
return { 'machineCode': machineCode, 'sourceInfo': sourceInfo };
|
||||
return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations };
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -440,21 +440,20 @@ function assemble(inputFilename, outputToFile, includeMetadata, outputFilename=n
|
|||
const dbg = new DBG('nitpick');
|
||||
|
||||
// Handle command-line options...
|
||||
const opts = new Opter(process.argv);
|
||||
opts.synonymize('-d', '--debug');
|
||||
opts.requireOption('-i', 'Input file required (-i prog.asm)');
|
||||
opts.requireOptionArgument('-i', 1, 1, 'Input file required (-i prog.asm)');
|
||||
opts.requireOptionArgument('-o', 1, 1, 'Missing output file name (-o prog.asm)');
|
||||
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.opts.i[0];
|
||||
const outputToFile = opts.contains('-o');
|
||||
let outputWithMetadata = opts.contains('--debug');
|
||||
const inputFilename = opts.in[0];
|
||||
let outputWithAnnotations = 'annotate' in opts;
|
||||
|
||||
// Assemble...!
|
||||
if (outputToFile) {
|
||||
const outputFilename = opts.opts.o[0];
|
||||
assemble(inputFilename, outputToFile, outputWithMetadata, outputFilename);
|
||||
if ('out' in opts) {
|
||||
const outputFilename = opts.out[0];
|
||||
assemble(inputFilename, true, outputWithAnnotations, outputFilename);
|
||||
} else {
|
||||
dbg.setLevel('none');
|
||||
assemble(inputFilename, outputToFile, outputWithMetadata);
|
||||
assemble(inputFilename, false, outputWithAnnotations);
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
#!/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');
|
||||
|
|
@ -15,25 +18,32 @@ let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit);
|
|||
main();
|
||||
|
||||
async function main() {
|
||||
const input = await readPipedStdin();
|
||||
const opter = new Opter();
|
||||
opter.addOption('-i', '--in', false, true, 1);
|
||||
const opts = opter.parse(process.argv);
|
||||
|
||||
const debuggable = true; // FIXME - Get this as a command line flag instead
|
||||
// if debuggable === true, the input is JSON {sourceInfo, machineCode}
|
||||
// otherwise, the input is a string of space-separated numbers
|
||||
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 sourceInfo = null;
|
||||
let sourceAnnotations = null;
|
||||
|
||||
if (!debuggable) {
|
||||
code = new Uint8Array(input.split(' '));
|
||||
} else {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
sourceAnnotations = parsedInput.sourceAnnotations;
|
||||
code = new Uint8Array(parsedInput.machineCode);
|
||||
sourceInfo = parsedInput.sourceInfo;
|
||||
} catch (error) {
|
||||
if (error.name === 'SyntaxError') {
|
||||
code = new Uint8Array(input.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
cpu.loadMemory(code);
|
||||
if (debuggable) { cpu.loadSourceInfo(sourceInfo); }
|
||||
if (sourceAnnotations !== null) { cpu.loadSourceAnnotations(sourceAnnotations); }
|
||||
|
||||
cpu.onCycleEnd(tick);
|
||||
cpu.onCycleEnd(logCPUState);
|
||||
|
|
@ -56,8 +66,8 @@ async function tick() {
|
|||
|
||||
function logCPUState() {
|
||||
let lineInfo = null;
|
||||
if (cpu.dbg.sourceInfo) {
|
||||
lineInfo = cpu.dbg.sourceInfo[cpu.dbg.previousIP];
|
||||
if (cpu.dbg.sourceAnnotations) {
|
||||
lineInfo = cpu.dbg.sourceAnnotations[cpu.dbg.previousIP];
|
||||
}
|
||||
console.group(`Step ${cpu.dbg.cycleCounter}`);
|
||||
console.log();
|
||||
|
|
@ -40,7 +40,7 @@ module.exports = class CPU {
|
|||
poke() { return; } // TODO - implement Poke
|
||||
|
||||
/** @param {Array} info **/ // TODO - document type for 'sourceInfo'
|
||||
loadSourceInfo(info) {
|
||||
loadSourceAnnotations(info) {
|
||||
this.dbg.sourceInfo = info;
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "paper-computer",
|
||||
"name": "cardiograph",
|
||||
"scripts": {
|
||||
"jsdoc": "./node_modules/.bin/jsdoc"
|
||||
},
|
||||
|
|
@ -8,35 +8,77 @@ Cardiograph is an imaginary computer. It has three main components:
|
|||
|
||||
## Simulator
|
||||
|
||||
### Run assembler
|
||||
### Dependencies
|
||||
Cardiograph is an imaginary computer. It has three main components:
|
||||
|
||||
```./assembler.js source_code.asm [output.txt]```
|
||||
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
|
||||
2. an input-output processor, *IO*
|
||||
3. a display, *Graph*
|
||||
|
||||
By default, the output is written to `out.txt`. It is saved as a string of space-separated decimal numbers.
|
||||
|
||||
|
||||
### Run simulator
|
||||
|
||||
```./cardiograph.js < machine_code.txt```
|
||||
## 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
|
||||
|
||||
- `A` - accumulator
|
||||
- `IP` - instruction pointer (aka program counter)
|
||||
- `FLAGS` - flags: **O**verflow, **N**egative, **Z**ero, **C**arry
|
||||
- in machine language, each flag is given a number:
|
||||
- O = 3
|
||||
N = 2
|
||||
Z = 1
|
||||
C = 0
|
||||
- (bitwise, `0000 = ONZC`)
|
||||
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
|
||||
|
|
@ -65,7 +107,7 @@ Hex Mnem. Operand Effect
|
|||
```
|
||||
|
||||
- Instructions are two bytes long:
|
||||
one byte for the opcode, one for the operand
|
||||
one byte for the opcode, one for the operand
|
||||
|
||||
|
||||
#### Effects on memory, flags, registers
|
||||
|
|
@ -101,6 +143,9 @@ 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 `;`
|
||||
|
|
@ -133,13 +178,18 @@ Put differently: it starts executing instructions at the address contained in `$
|
|||
|
||||
## Cardiograph memory map
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FE` - free
|
||||
- `FF ` - ROM (unwriteable) pointer to initial IP (not yet implemented)
|
||||
| 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
|
||||
|
||||
|
|
@ -149,20 +199,12 @@ The value of the latest keypress on a hex keypad is stored at `$1B`.
|
|||
|
||||
The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set:
|
||||
|
||||
```
|
||||
1 2 3 C 1 2 3 4
|
||||
4 5 6 D Q W E R
|
||||
7 8 9 E A S D F
|
||||
A 0 B F Z X C V
|
||||
|
||||
hex pad simulator
|
||||
```
|
||||
`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 ← ↓ →
|
||||
|
||||
hex pad simulator
|
||||
```
|
||||
` ` `5` ` ` = ` ` `↑` ` `
|
||||
`7` `8` `9` = `←` `↓` `→`
|
||||
|
|
@ -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,16 +0,0 @@
|
|||
# Dev notes — 2023-08-17
|
||||
|
||||
## Goals for today
|
||||
|
||||
- [ ] *ADDR - Add relative offsets - `*ADDR +1`, `$ADDR -2`
|
||||
- [ ] ? Add keypad visualization to simulator
|
||||
1. Display qwerty-to-VIP mapping for reference
|
||||
2. Highlight the most recent keypress on there
|
||||
- [ ] Notes re: ROM and/or tape loader
|
||||
|
||||
- Programming ideas:
|
||||
- Keypad display
|
||||
1. Light pixel corresponding to most recent keypress
|
||||
2. Display character corresponging to most recent keypress
|
||||
- Draw dot at (x, y)
|
||||
- Move dot around display using keypad
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# Dev notes - 2023-08-21
|
||||
|
||||
## Goals for today
|
||||
|
||||
- graphic design
|
||||
- [x] graphics: 80 col card template
|
||||
- see below
|
||||
- printing
|
||||
- [x] print: paper tape template
|
||||
- [x] print: colour coding forms
|
||||
- [x] print: 80 col card template
|
||||
|
||||
- [ ] see code/design commentary below...
|
||||
|
||||
- (consider python (microbit) version of simulator)
|
||||
- maybe ask for e's input
|
||||
|
||||
## Memory map re-think
|
||||
|
||||
### CPU start-up
|
||||
|
||||
When starting up, the CPU executes a `JMP $FF`.
|
||||
|
||||
Put differently: it starts executing instructions at the address contained in `$FF`.
|
||||
|
||||
### Cardiograph memory map
|
||||
|
||||
```
|
||||
00-19 - display (5x5)
|
||||
1A - pointer to display memory
|
||||
1B - keypad: value of latest key pressed
|
||||
1C - reserved for future use (bank switching flag)
|
||||
1D-FE - free
|
||||
```
|
||||
|
||||
## References re: where do CPU instruction pointers start, and how are they set?
|
||||
|
||||
- ["Memory Map Requirements", in *6502 PRIMER: Building your own 6502 computer*](http://wilsonminesco.com/6502primer/MemMapReqs.html)
|
||||
- "Reset (RST): When the 6502's RST input gets pulled low and then brought back high, the 6502 starts its reset process, and gets the address to start executing program instructions from $FFFC-FFFD. Notice it does not start executing at address $FFFC, but reads it to get the beginning address of the routine where it should start executing. That routine will normally have to be in ROM."
|
||||
- [What address does the x86 begin executing at?](https://stackoverflow.com/questions/4004493/what-address-does-the-x86-begin-executing-at)
|
||||
- "The `cs` (code selector) register is set to `0xffff` and `ip` (instruction pointer) is set to `0x0000`."
|
||||
- [Why is the first BIOS instruction located at 0xFFFFFFF0 ("top" of RAM)?](https://superuser.com/questions/988473/why-is-the-first-bios-instruction-located-at-0xfffffff0-top-of-ram) (x86)
|
||||
|
||||
## Imported notes from earlier
|
||||
|
||||
### 2023-08-18 cardiograph loose thoughts
|
||||
|
||||
- use binary encoded punch cards for cardiog progs in machine code
|
||||
- try making a microbit based emulator
|
||||
- (microbit + hex keypad)
|
||||
- (machine code monitor like VIP…)
|
||||
- (+ tape input??)
|
||||
- a4 template with full size 80 col card
|
||||
- snake
|
||||
- [/] add simulator todo: pass asm line thru to cpu to print when debugging
|
||||
- asm: create a second array that stores every line with code (nor blank or comment only lines) + its line number
|
||||
- cpu: accept an optional debugging array, print line # and statement
|
||||
- readme:
|
||||
- [x] readme: rename (or split up?) mem map / peripherals section
|
||||
- [x] ? readme: put 2 keypad charts side by side (they would fit on my phone)
|
||||
- [/] see paper notes on mem map
|
||||
|
||||
|
||||
## 2023-08-19
|
||||
|
||||
[/] reconsider ISA order in light of supercat comment here
|
||||
- [Why didn't the 6503 have increment/decrement opcodes for A?](https://retrocomputing.stackexchange.com/questions/13023/why-didnt-the-6502-have-increment-decrement-opcodes-for-a)
|
||||
|
||||
- [/] look at use of `*` or `.` in assembly
|
||||
- [What does "jmp *" mean in 6502 assembly?](https://retrocomputing.stackexchange.com/questions/7998/what-does-jmp-mean-in-6502-assembly)
|
||||
|
||||
|
||||
## 2023-07-17 - cardiograph - worksheet for hand-assembling code… + other docs
|
||||
|
||||
- [ ] docs/graphics: machine code quick ref (ops + short explanations + mnems)
|
||||
- [ ] docs/graphics: assembly quick ref (as above, plus assembler-specific syntax like constants)
|
||||
- [ ] docs/graphics: worksheet for hand-assembling
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
# Dev notes — 2023-08-23
|
||||
|
||||
## Problems to solve
|
||||
|
||||
The outline below / in the new README has some appeal, but it makes each instruction 8 bits long, which would require altering the simulator to support 8 bit addressing/storage...
|
||||
|
||||
Do I want to do this??
|
||||
|
||||
Or maybe go with the simpler "just swap NOP and FHP" plan...
|
||||
|
||||
**→ Ok i'm going to bail on this for now; the current set is easier to work with and nicer to teach. It was good to learn about and think about this, and maybe it will come back later, but for now it feels like adding this complexity would be contrary to my goals of maximum simplicity and rapid learnability.**
|
||||
|
||||
## Instruction set layout notes
|
||||
|
||||
### Reference: 6502
|
||||
|
||||
[The 6502 Instruction Set Decoded](https://llx.com/Neil/a2/opcodes.html)
|
||||
|
||||
> Most instructions that explicitly reference memory locations have bit patterns of the form aaabbbcc. The aaa and cc bits determine the opcode, and the bbb bits determine the addressing mode.
|
||||
|
||||
## CHUMP reference
|
||||
|
||||
from David Feinberg, "A Simple and Affordable TTL Processor for the Classroom":
|
||||
|
||||
> The CHUMP instruction set features seven key operations, each of which comes in two flavors: constant and memory. For example, there is an ADD command for adding a constant to the accumulator, and another ADD for adding a value from memory to the accumulator. The 4-bit constant portion of the instruction is ignored by the seven memory commands. Table 1 describes the seven constant commands. The corresponding memory commands operate similarly on a memory value, and have a 1 in the op-code's low-order bit.
|
||||
>
|
||||
> For example, the following program increments the value in RAM location 2 repeatedly. Used properly, every READ command should be followed by a memory command, and every memory command should be preceded by a READ command.
|
||||
|
||||
> ```0: 10000010 READ 2
|
||||
> 1: 00010000 LOAD IT
|
||||
> 2: 00100001 ADD 1
|
||||
> 3: 01100010 STORETO 2
|
||||
> 4: 10100000 GOTO 0
|
||||
>```
|
||||
|
||||
Constant instructions:
|
||||
|
||||
dec bin
|
||||
00 0000 Load
|
||||
02 0010 Add
|
||||
04 0100 Subtract
|
||||
06 0110 Store To
|
||||
08 1000 Read
|
||||
10 1010 GOTO
|
||||
12 1100 If Zero
|
||||
|
||||
Memory instructions (I think):
|
||||
|
||||
dec bin
|
||||
01 0001 Load
|
||||
03 0011 Add
|
||||
05 0101 Subtract
|
||||
07 0111 Store To
|
||||
09 1001 Read
|
||||
11 1011 GOTO
|
||||
13 1101 If Zero
|
||||
|
||||
## Current Cardiograph
|
||||
|
||||
```
|
||||
hex bin
|
||||
00 0000 END *
|
||||
01 0001 STO lit#
|
||||
02 0010 STO addr
|
||||
03 0011 LDA lit#
|
||||
04 0100 LDA addr
|
||||
05 0101 ADD lit#
|
||||
06 0110 ADD addr
|
||||
07 0111 SUB lit#
|
||||
08 1000 SUB addr
|
||||
09 1001 HOP lit#
|
||||
0A 1010 HOP addr
|
||||
0B 1011 JMP lit#
|
||||
0C 1100 JMP addr
|
||||
0D 1101 FTG lit#
|
||||
0E 1110 FHP lit# *
|
||||
0F 1111 NOP ———— *
|
||||
```
|
||||
so the least significant bit indicates the addressing mode (0 = direct, 1 = indirect)
|
||||
|
||||
except for three exceptions: END, FHP, and NOP
|
||||
|
||||
## Possible Cardiograph revisions
|
||||
|
||||
If NOP swaps with FHP, then a 1 in the least significant bit always indicates literal addressing:
|
||||
|
||||
```
|
||||
0000 END
|
||||
0001 STO lit#
|
||||
0010 STO addr
|
||||
0011 LDA lit#
|
||||
0100 LDA addr
|
||||
0101 ADD lit#
|
||||
0110 ADD addr
|
||||
0111 SUB lit#
|
||||
1000 SUB addr
|
||||
1001 HOP lit#
|
||||
1010 HOP addr
|
||||
1011 JMP lit#
|
||||
1100 JMP addr
|
||||
1101 FTG lit#
|
||||
1110 NOP
|
||||
1111 FHP lit#
|
||||
```
|
||||
|
||||
Or we could use 8 bits, and use one of the upper 4 to group instructions:
|
||||
|
||||
```
|
||||
00 0000 0000 END
|
||||
01 0000 0001 NOP
|
||||
... ... NOP
|
||||
0F 0000 1111 NOP
|
||||
|
||||
10 0001 0000 STO lit#
|
||||
11 0001 0001 STO addr
|
||||
12 0001 0010 LDA lit#
|
||||
13 0001 0011 LDA addr
|
||||
14 0001 0100 ADD lit#
|
||||
15 0001 0101 ADD addr
|
||||
16 0001 0110 SUB lit#
|
||||
17 0001 0111 SUB addr
|
||||
18 0001 1000 HOP lit#
|
||||
19 0001 1001 HOP addr
|
||||
1A 0001 1010 JMP lit#
|
||||
1B 0001 1011 JMP addr
|
||||
1C 0001 1100 FTG lit#
|
||||
1D 0001 1101 FTG addr
|
||||
1E 0001 1110 FHP lit#
|
||||
1F 0001 1111 FHP addr
|
||||
```
|
||||
|
||||
```
|
||||
gggg iii a
|
||||
|
||||
g: group
|
||||
i: instruction
|
||||
a: addressing mode (for group 0)
|
||||
```
|
||||
|
||||
- makes the use of the LSB for direct/indirect addressing perfectly consistent for group `0001`
|
||||
- makes room for indirect `FTG` and `FHP` (but those still don't seem very useful)
|
||||
|
||||
...
|
||||
|
||||
But if I want to be able to add more groups later, something like `gg aa iiii` might be better...
|
||||
|
||||
```
|
||||
hex bin group mode op
|
||||
|
||||
00 0000 0000 0 -- END
|
||||
01 0000 0001 0 -- NOP
|
||||
|
||||
50 0101 0000 1 direct STO
|
||||
51 0101 0001 1 direct LDA
|
||||
52 0101 0010 1 direct ADD
|
||||
53 0101 0011 1 direct SUB
|
||||
54 0101 0100 1 direct HOP
|
||||
55 0101 0101 1 direct JMP
|
||||
56 0101 0110 1 direct FTG
|
||||
57 0101 0111 1 direct FHP
|
||||
|
||||
60 0110 0000 1 indirect STO
|
||||
61 0110 0001 1 indirect LDA
|
||||
62 0110 0010 1 indirect ADD
|
||||
63 0110 0011 1 indirect SUB
|
||||
64 0110 0100 1 indirect HOP
|
||||
65 0110 0101 1 indirect JMP
|
||||
66 0110 0110 1 indirect FTG
|
||||
67 0110 0111 1 indirect FHP
|
||||
```
|
||||
|
||||
~~**let's do that!**~~
|
||||
|
||||
(but for now i'm going to skip indirect FTG and FHP out of laziness)
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
# Dev notes — 2023-08-24
|
||||
|
||||
## CPU start-up
|
||||
|
||||
Thinking about booting up...
|
||||
how would the "pointer to display memory" etc. get initialized?
|
||||
|
||||
They could be in ROM, but then they're not re-locatable.
|
||||
|
||||
Maybe there's a start-up routine in ROM that sets them up?
|
||||
|
||||
[cf. C64 bank switching](https://www.c64-wiki.com/wiki/Bank_Switching)
|
||||
|
||||
The C64 uses I/O lines on the CPU (+ others) to signal to the PLA that it's time to switch ROM banks in or out.
|
||||
|
||||
Maybe I need something like that?
|
||||
|
||||
~~A simplified approach might be to put the pointer-to-display and ...~~
|
||||
|
||||
Current memory map:
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FF` - free
|
||||
|
||||
Thinking about changing it for better bank-switching:
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `20 ` - initial IP
|
||||
- ` ` - free
|
||||
- `FC ` - I/O controller - pointer to display memory
|
||||
- `FD ` - I/O controller - reserved for future use (bank switching flag)
|
||||
- `FE ` - I/O controller - keypad: value of latest key pressed
|
||||
- `FF ` - ROM - pointer to initial IP
|
||||
|
||||
Ah so actually the issue is that the CPU needs to be paired with an I/O controller,
|
||||
and that needs to know where to look to find these pointers/where to put the keypad info.
|
||||
|
||||
And I think that can just be hand-waved away for now?
|
||||
|
||||
### Here's the plan
|
||||
|
||||
**But I got started on this because I was trying to work out how FF gets loaded with the initial IP, and I think that's still a question.**
|
||||
|
||||
***(And maybe some way to switch ROM in and out would be good for the secret-advanced-mode, since it would be great to have a ROM pre-loaded with a set of convenient utility routines.)***
|
||||
|
||||
I think maybe we just leave the IP hardcoded for now; say that there's a 1-byte ROM at $FF.
|
||||
|
||||
- `00-19` - display (5x5)
|
||||
- `1A ` - pointer to display memory
|
||||
- `1B ` - keypad: value of latest key pressed
|
||||
- `1C ` - reserved for future use (bank switching flag)
|
||||
- `1D ` - initial IP
|
||||
- `1D-FE` - free
|
||||
- `FF ` - ROM (unwriteable) - pointer to initial IP
|
||||
|
||||
- [ ] store `$1D` at `$FF`
|
||||
- [ ] make CPU execute `JMP $FF` on startup
|
||||
- [ ] make ROM unwriteable
|
||||
|
||||
More step-by-step:
|
||||
|
||||
- Change memory from a Uint8Array to a regular array,
|
||||
and make every entry { number | { type: 'ROM', value: number }}
|
||||
- Store ROM as an object in machine.config.js
|
||||
- Load ROM data into memory at CPU startup (`startCPU(RAM, ROM)`)
|
||||
|
||||
(And continue to handwave away how that RAM already contains data, for now...)
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Check that we're starting execution at $1D now
|
||||
- [ ] Programming
|
||||
- [ ] Subroutine stack
|
||||
- [ ] Conway's Life
|
||||
- [ ] ? bank switching
|
||||
- [ ] Keypad handling
|
||||
- [ ] Start-up process described above
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Dev notes — 2023-08-28
|
||||
|
||||
Two current goals:
|
||||
|
||||
## 1. Implement a new CLI interface
|
||||
|
||||
- New arg-parser
|
||||
- Allow setting clock speed with a command line flag
|
||||
|
||||
|
||||
## 2. Re-architect
|
||||
|
||||
- Move the bulk of cpu.js to cpu.lib.js
|
||||
- Add a way to register functions to be called each cycle
|
||||
- Create a new 'cardiograph.js'
|
||||
- imports cpu.lib.js
|
||||
- registers functions for logging, updating display
|
||||
- (split those apart)
|
||||
- provides terminal interface
|
||||
- get machine code input from stdin (or like, file stream?)
|
||||
- `./assembler.js | ./cardiograph.js
|
||||
- see below re: changes to assembler
|
||||
- Assembler
|
||||
- Move CLI interface into assembler.js and delete run-assembler.js
|
||||
- Add a --json flag, which causes the assembler to print the json of {machineCode, debugInfo} on stdin
|
||||
- Otherwise, print hex/bin to stdin
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 4dec08afbc0c96e454dd532ed9cfa16a4a1ddd51
|
||||
Loading…
Reference in New Issue