Compare commits

..

44 Commits

Author SHA1 Message Date
n loewen ceb6019a1c Merge branch 'rearchitect' 2025-04-17 10:29:20 +01:00
n loewen 353b7d7fdc Add __pycache__ to gitignore 2025-04-17 10:24:03 +01:00
n loewen 82c0283b25 Add python WIP 2025-04-17 10:23:35 +01:00
n loewen 3261b6c97a Catch git up to current work eek 2025-04-17 10:21:56 +01:00
n loewen 98b19ce936 Fix bugs, play with flag behaviour, write some new test programs 2025-03-29 08:35:27 +00:00
n loewen 4c605e92c5 Move meowbit CPU to its own dir + get screen going 2025-03-29 08:34:42 +00:00
n loewen 6bea93308c Get 8x8 led matrix working 2025-03-25 17:04:25 +00:00
n loewen cf68271440 Get monitor keypad working 2025-03-25 15:22:25 +00:00
n loewen e82a429d5e Start work on pi pico version of CPU simulator 2025-03-20 10:12:25 +00:00
n loewen 6f164294e5 Get it running again by default + displaying on meowbit screen 2025-03-08 15:20:53 +00:00
n loewen 3fd770ab53 Add a little comment explaining how keypad entry workds 2025-03-08 12:26:35 +00:00
n loewen f21e57cafe Improve keypad monitor entry 2025-03-08 12:24:10 +00:00
n loewen d5ded67b79 Get basic keypad entry working 2025-03-08 11:18:33 +00:00
n loewen fd4ca3e8c8 Switch to circuitpython. Get some basics working... 2025-03-04 19:24:26 +00:00
n loewen fa504685d2 Start working on keyboard input - works when running the simulator, but no 'monitor mode' yet 2025-03-03 20:35:46 +00:00
n loewen 6e58241288 Implement remaining instructions + basic 5x5 display printout 2025-03-03 19:52:12 +00:00
n loewen 61361c5f3f Get simulator to run a few instructions 2025-02-28 12:30:11 +00:00
n loewen 7e842bd7a6 Merge old work on laptop with new python work 2025-02-28 10:53:42 +00:00
n loewen 34e2c24b88 Start working on micropython simulator for 2025 version of cardiograph 2025-02-28 10:48:47 +00:00
n loewen f3f6a58a65 Merge branch 'rearchitect' 2023-09-23 21:23:16 -07:00
n loewen 35d164b0a7 (docs) readme - Change 'keypad' section to use the nicer formatting from the main branch 2023-09-23 21:18:59 -07:00
n loewen 62b7396ab6 (docs) readme - Change memory map to use a nice table 2023-09-23 21:15:12 -07:00
n loewen 9bd88aa8bc (docs) readme - Add todo in 'start-up' section, pointing out that the simulator currently doesn't follow the spec here 2023-09-23 20:52:31 -07:00
n loewen b2933a50a0 (docs) readme - Change 'register and flags' section to improve style and clarity 2023-09-23 20:47:16 -07:00
n loewen 83e980b685 (docs) readme - Fix typo 2023-09-23 20:33:06 -07:00
n loewen 4854ce34fa Change name to 'cardiograph' in package.json 2023-09-23 20:31:34 -07:00
n loewen 92a619fded (docs) readme - Update simulator usage info 2023-09-23 20:29:44 -07:00
n loewen ae587a0712 assembler - Change variables name to 'outputWithAnnotations' to match the change made in the previous commit 2023-09-23 20:29:05 -07:00
n loewen f802420799 Rename 'sourceInfo' variables (etc) to 'sourceAnnotations' 2023-09-23 20:23:44 -07:00
n loewen 3e32cb97e1 cardiograph - Change to programmatically infer whether the input includes annotations or not 2023-09-23 20:20:42 -07:00
n loewen 91cba57aa1 cardiograph - Change to read command line options using Opter 2023-09-23 19:33:58 -07:00
n loewen 98bfa50269 opter - Update 2023-09-23 19:16:34 -07:00
n loewen 9c82265a88 assembler - Change to use new Opter interface 2023-09-23 19:09:56 -07:00
n loewen 63eb4a9500 opter - Update to latest 2023-09-23 19:09:28 -07:00
n loewen 51c64cc615 opter - Update to newest version 2023-09-23 18:30:58 -07:00
n loewen 98fa9a4ab7 Remove 'issues' dir, since they moved to the 'notes' branch 2023-09-10 21:31:21 -07:00
n loewen 93f88560a2 (notes) - Remove notes from this branch, since they moved to their own 2023-09-10 21:28:01 -07:00
n loewen b0996d30c3 opter - Update to latest version 2023-09-04 18:04:35 -07:00
n loewen 0b91a71575 (docs) readme - Change keypad reference markup to make it prettier 2023-08-31 13:20:10 -04:00
n loewen 144ae8de6c (docs) readme - Add intro + re-arrange, to clarify relationship between Cardiograph and its constituent parts 2023-08-29 09:26:09 -04:00
n loewen dee5b4afd4 Remove 'issues' dir - it lives in the 'issues' branch now 2023-08-29 09:15:21 -04:00
n loewen e6883fbc65 (notes) Remove all notes - they live in the 'notes' branch now 2023-08-29 08:46:09 -04:00
n loewen 072d2ccdb5 (notes) 2023-08-20 - Create note with list of short-term goals 2023-08-29 08:26:17 -04:00
n loewen 9f4a67770f (notes) - 2023-08-28 - Update todos 2023-08-29 08:25:56 -04:00
71 changed files with 3770 additions and 1213 deletions

4
.gitignore vendored
View File

@ -2,4 +2,6 @@
.vscode
*.tmp.*
node_modules
cardiograph.code-workspace
cardiograph.code-workspace
*venv*
*__pycache__

3
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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);
}

View File

@ -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();

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
{
"name": "paper-computer",
"name": "cardiograph",
"scripts": {
"jsdoc": "./node_modules/.bin/jsdoc"
},

View File

@ -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`   =   `←` `↓` `→`

View File

@ -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!

View File

@ -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?

View File

@ -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?

View File

@ -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)

View File

@ -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 lets pretend it doesnt 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…
; wouldnt it be great to have a “hop if neg” op…
; do we have to just subtract numbers until we get 0?
; no!
; heres an approach thats 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 isnt quite it… we also need to chexk if we hit 0 by just deceementinf and if so retuen 0
jmp @jump_table

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"))

View File

@ -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

View File

@ -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"

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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])

View File

@ -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)

View File

@ -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")

View File

@ -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.

164
python/cpu.py Normal file
View File

@ -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,
}

124
python/simulator-scrap.py Normal file
View File

@ -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()

5
python/simulator.py Normal file
View File

@ -0,0 +1,5 @@
import cpu
c = cpu.CPU()
print(c)

@ -1 +0,0 @@
Subproject commit 4dec08afbc0c96e454dd532ed9cfa16a4a1ddd51

View File

@ -1,198 +0,0 @@
/* CPU sketch */
class CPU {
constructor () {
this.reset();
}
IP = 0;
A = 0;
flags = 0b00000000;
instruction = 0x00;
operand = 0x00;
//What do I want for interrupts…?
// interruptRequest = 0;
// TODO: can it be a simple setup with fixed instruction cycle length pls?
_instructionCycleStep = 0;
_decodedOp = {};
_calculatedIP = 0;
tickClock () {
console.log('cpu tick');
if (connections.cpuWait) return false;
switch (this._instructionCycleStep) {
// Fetch - instruction
case 0:
// initiate read
connections.addressBus = this.IP;
connections.memoryReadSignal = 1;
this._calculatedIP += 2;
break;
case 1:
// complete read
connections.memoryReadSignal = 0;
this.instruction = connections.dataBus;
break;
// Fetch - operand
case 2:
// initiate read
connections.addressBus = this.IP;
connections.memoryReadSignal = 1;
break;
case 3:
// complete read
connections.memoryReadSignal = 0;
this.operand = connections.dataBus;
break;
// Decode
case 4:
// decode
this._decode();
break;
// Execute
case 5:
this._ops[this._decodedOp]();
break;
case 4:
// execute more
break;
}
this._instructionCycleStep = (this._instructionCycleStep + 1) % 4;
}
reset () {
this.IP = 0xFE;
this.A = 0;
this.flags = 0b00000000;
connections.dataBus = 0b00000000;
this._instructionCycleStep = 0;
this.debug.running = true;
}
_decode (instruction) {
/*
// gg aa iiii
let group = 0b11000000 & instruction;
let mode = 0b00110000 & instruction;
let instr = 0b00001111 & instruction;
console.log(group, mode, instr);
*/
let mode = (0b00110000 & instruction) === 16 ? 'indirect' : 'direct';
const instrs = {
0x00 : 'END',
0x01 : 'NOP',
0x50 : 'STO_indirect',
0x51 : 'LDA_indirect',
0x52 : 'ADD_indirect',
0x53 : 'SUB_indirect',
0x54 : 'HOP_indirect',
0x55 : 'JMP_indirect',
0x56 : 'FTG_indirect',
0x57 : 'FHP_indirect',
0x60 : 'STO_direct',
0x61 : 'LDA_direct',
0x62 : 'ADD_direct',
0x63 : 'SUB_direct',
0x64 : 'HOP_direct',
0x65 : 'JMP_direct',
0x66 : 'FTG_direct',
0x67 : 'FHP_direct',
};
// return { instruction: instrs[instruction], mode: mode };
this._decodedOp = instruction;
}
_ops = {
JMP_direct: (lit) => {
this.debug.currentMnemonic = 'JMP lit';
this._calculatedIP = lit;
},
JMP_indirect: (addr) => {
this.debug.currentMnemonic = 'JMP addr';
this._calculatedIP = addr;
},
}
debug = {
// TODO include the rest of the stuff that I have here in the current version
running: false,
}
}
class Memory {
// ROM format: { addr: nn, data: nn }
constructor (sizeInBytes, ROM) {
// memory format: [ { data: nn, type: str } ]
this.memory = new Array(sizeInBytes);
ROM.forEach((byte) => {
this.memory[byte.address] = { data: byte.data, type: 'ROM' };
})
}
tickClock () {
if (connections.memoryReadSignal) this._read();
if (connections.memoryWriteSignal) this._write();
}
_read () {
connections.dataBus = this.memory[connections.addressBus].data;
}
_write () {
if (this.memory[connections.addressBus].type === 'ROM') throw new Error('Attempted write to ROM');
this.memory[connections.addressBus].data = connections.dataBus;
}
}
/** DMA controller for screen, keypad... **/
class IO {
// TODO…
constructor (connections) {
// ...
}
}
class Connections {
dataBus = 0;
addressBus = 0;
memoryReadSignal = 0;
memoryWriteSignal = 0;
cpuWait = 0;
}
// let rom = // TODO read in…
const rom = [
{ address: 0xFE, data: 0x55, },
{ address: 0xFF, data: 0x00, },
{ address: 0x00, data: 0x51, },
{ address: 0x01, data: 0x0F, },
{ address: 0x02, data: 0x62, },
{ address: 0x03, data: 0x01, },
{ address: 0x04, data: 0x00, },
];
let connections = new Connections();
let memory = new Memory(256, rom);
let cpu = new CPU();
let intervalTimer = setInterval( () => {
cpu.tickClock();
memory.tickClock();
}, 500);
console.log(intervalTimer);