Compare commits

..

175 Commits
v0.2.0 ... main

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 77a41d47c3 assembler - Change to a tidier approach, made possible by new Opter library 2023-09-02 20:55:45 -07:00
n loewen 101421c4c3 dbg - Add 'set level' function 2023-09-02 20:36:59 -07:00
n loewen d26bf39f05 assembler - Fix: move 'main' back to end 2023-09-02 20:34:54 -07:00
n loewen c8c188e55a assembler - Move 'main' up top 2023-09-02 16:45:38 -04:00
n loewen e25723fcc9 assembler - Change to make use of improvements in Opter 2023-09-02 16:42:11 -04:00
n loewen 9c1cf4abba opter - Rename 'argparser' to 'opter' 2023-09-02 15:40:16 -04:00
n loewen 2848588fc2 (infra) - gitignore - Ignore files with '.tmp.' in their name 2023-09-02 13:43:42 -04:00
n loewen b3d10a4197 assembler - Change to use new argument parsing library 2023-09-02 13:40:56 -04:00
n loewen 16f85d3b9f (tests) flag-overflow-2 - Fix: comment out date 2023-09-02 13:39:47 -04:00
n loewen 0b91a71575 (docs) readme - Change keypad reference markup to make it prettier 2023-08-31 13:20:10 -04:00
n loewen f9901d304b ?? submodule diff - idk what this is about 2023-08-29 22:32:36 -04:00
n loewen c84c86c160 cpu - Fix overflow flag! 2023-08-29 21:28:39 -04:00
n loewen 7b143ab000 (docs) readme - Update info on numeric types in assembly 2023-08-29 21:28:01 -04:00
n loewen 866f553346 assembler - Add feature: Allow binary numbers with prefix '0b' and hex numbers with prefix '0x' 2023-08-29 21:26:36 -04:00
n loewen eff9043665 cpu - Fix: Include '$' before hex in debug-peeking for indirect ops 2023-08-29 21:25:59 -04:00
n loewen 1fe582663b conversions - Create `conversions.js` and move `hex2num` et al. out of `logging.js` 2023-08-29 20:30:53 -04:00
n loewen 8bb3b9b43f conversions - Create `conversions.js` and move `hex2num` et al. out of `logging.js` 2023-08-29 20:30:44 -04:00
n loewen e9d721042b cardiograph - Remove old debug logging 2023-08-29 20:29:15 -04:00
n loewen 19784b56b6 dbg - Add 'none' debugging level, which turns off all output 2023-08-29 19:34:22 -04:00
n loewen 12d592c262 assembler - WIP - Add a 'return on stdout' mode that turns off all debug logging, and prints machine code output to stdout, so that output can be piped directly to the simulator 2023-08-29 19:32:43 -04:00
n loewen 5671314b10 (docs) readme - Add intro + re-arrange, to clarify relationship between Cardiograph and its constituent parts 2023-08-29 13:20:45 -04:00
n loewen a6e9966797 (docs) Readme - Update info on how to run the assembler and simulator 2023-08-29 13:08:48 -04:00
n loewen 8a38d6f831 (docs) readme - Update dependency list 2023-08-29 13:05:04 -04:00
n loewen be13802e1e assembler / cardiograph - Add shebangs 2023-08-29 13:04:38 -04:00
n loewen 45c8fe3bd2 assembler - Remove dbg log 2023-08-29 13:04:26 -04:00
n loewen 049d9367ac cardiograph - Change to take input from stdin! 2023-08-29 12:54:32 -04:00
n loewen 7faf190fe2 assembler / run-assembler - Make assembler get input from stdin! / Remove `run-assembler` 2023-08-29 12:54:06 -04:00
n loewen 41d7b7ba54 assembler - Rename 'debugInfo' to 'sourceInfo' and return it second in output - {machinecode, sourceinfo} 2023-08-29 12:52:48 -04:00
n loewen 14a32b9c27 dbg - Add feature: allow any number of arguments when logging 2023-08-29 12:51:28 -04:00
n loewen 8a19238612 dbg - Change to hardcoded functions instead of factory-generated ones, because VS Code type checking can't cope with the generated ones 2023-08-29 12:02:18 -04:00
n loewen 28b92b4980 assembler - Replace debugging with new debugging library 2023-08-29 11:51:53 -04:00
n loewen a2a79cea46 dbg - Create debugging library 2023-08-29 11:51:17 -04:00
n loewen b776951e81 logging - Fix bad alignment on rows with just one entry 2023-08-29 11:51:00 -04:00
n loewen d9212ab620 cardiograph - Move display-drawing so that it nests nicely inside a console.group() 2023-08-29 10:44:06 -04:00
n loewen 37bb92f296 cpu - Change to throw errors, instead of logging with console.error() 2023-08-29 10:42:36 -04:00
n loewen b08d9854c4 Update TODO/FIXME comments for increased clarity and accuracy 2023-08-29 10:04:18 -04:00
n loewen 74c6f83fcc machine config - Change CFG variable names to lowercase; Change to always access them as 'CFG.property' 2023-08-29 09:56:21 -04:00
n loewen ccc032b379 Move display functions to `io.js`, and re-implement keypad in `io.js` 2023-08-29 09:49:21 -04:00
n loewen efe20eabdf cpu - Rename back to 'cpu.js' 2023-08-29 09:30:33 -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
n loewen 5b52143ad0 Remove `run-cpu.js` (it is superseded by `cardiograph`) 2023-08-28 23:11:15 -04:00
n loewen fbda5ce927 cardiograph - WIP - Create minimal version of `cardiograph` file to act as CLI entrypoint to simulator 2023-08-28 23:09:51 -04:00
n loewen 18d77086df cpu - WIP - Finish refactoring into a library 2023-08-28 23:09:12 -04:00
n loewen 81617dfe42 Merge commit '49cb3171da959490277a8ea5b8b7a70ee8bf1783' into Re-architect 2023-08-28 23:06:31 -04:00
n loewen 49cb3171da Add gitignore 2023-08-28 23:05:39 -04:00
n loewen 03715a6a8e cpu - Rename to cpu.lib.js 2023-08-28 16:56:13 -04:00
n loewen b7d13087e6 cpu - WIP - Continue removing material that has to move elsewhere so that this can be treated as a library 2023-08-28 16:55:24 -04:00
n loewen b173d46cb6 cpu - Change structure of CPU object to group State and Debug info 2023-08-28 16:25:43 -04:00
n loewen ccebc6ec3d (notes) 2023-08-28 - Create note outlining plans to re-architect the simulator 2023-08-28 15:56:28 -04:00
n loewen 2d210303e6 (tests) WIP - Create a rough, partial sketch for an implementation of Conway's Game of Life 2023-08-28 15:31:57 -04:00
n loewen bf589411e7 Add `argparser` as a submodule 2023-08-28 09:18:51 -04:00
n loewen e09bd9a66f Remove old sketches 2023-08-28 09:12:26 -04:00
n loewen 1c8722c65e Move JavaScript source to new `/src` directory 2023-08-28 09:11:20 -04:00
n loewen 6545df9cfd cpu - WIP - Start re-configuring CLI interface, in order to allow passing clock speed as a CLI argument (while also keeping a defualt value in machine.config.js) 2023-08-28 09:10:21 -04:00
n loewen f335aba94d (issues) todos - Add "fix overflow flag" entry 2023-08-28 09:09:10 -04:00
n loewen 97e54d01cb (tests) move-pixel-with-keypad - Change keys to match new mapping / WASD 2023-08-26 16:45:14 -04:00
n loewen 29f25bda7a config / cpu - Add feature: Map the arrow keys to hex keypad, in addition to the larger Qwerty set 2023-08-26 16:44:30 -04:00
n loewen 872489c18d tests - Remove commented-out line 2023-08-26 14:58:29 +01:00
n loewen ac0fccf2e9 tests - finish WIP - Make a 'move a pixel with the keypad' program 2023-08-26 14:56:01 +01:00
n loewen 86d0b57c7a cpu - Fix: Make sure that the 'reached cycle limit' message displays in all output modes 2023-08-26 14:48:37 +01:00
n loewen 9f68bd3027 tests - WIP - Create 'move pixel with keypad' program 2023-08-26 14:35:49 +01:00
n loewen b290eb1568 cpu - Refactor CPU flags -- stop using a bitmask to make them easier to handle 2023-08-26 14:34:02 +01:00
n loewen ddab8f42c4 cpu / display - Fix/Refactor: Fix CPU status display so that it doesn't crash when JMPing to an address that isn't in the assembly source. Refactor CPU status display to streamline. 2023-08-26 14:29:43 +01:00
n loewen edad9ecbb8 cpu - Fix: Quit the process when the CPU halts 2023-08-26 13:26:26 +01:00
n loewen 41632b0a0f cpu - Change debug printout formatting 2023-08-26 13:25:09 +01:00
n loewen d0b620f1ad cpu - Fix incorrect flag numbering 2023-08-26 13:24:49 +01:00
n loewen 7cfad439f8 run-cpu / cpu - Change command-line interface to use more standard flags (--debug --step --pretty) 2023-08-26 12:57:27 +01:00
n loewen 4bb394f039 assembler - Fix: Count lines with only whitespace as blank 2023-08-26 11:35:12 +01:00
n loewen 1ce6757838 run-assembler - Remove unused import 2023-08-26 11:09:55 +01:00
n loewen dea1a445f6 logging - Fix memory printing function so that it works for arrays that start or end on odd address numbers 2023-08-26 11:07:38 +01:00
n loewen 902b218547 (tests) - draw-xy - Change selected pixel to (3, 3) 2023-08-25 11:06:34 +01:00
n loewen 0a3d474c43 issues - Move 'issues' directory out of 'notes' directory 2023-08-24 20:57:36 +01:00
n loewen 98c17cf925 display - Fix: Pretty display was displaying the wrong pixels 2023-08-24 17:06:09 +01:00
n loewen f612e7582a tests - Create routine for plotting a pixel at an (x, y) coordinate 2023-08-24 17:03:51 +01:00
n loewen 897749b108 cpu - Add feature: Quit after cycle limit reached 2023-08-24 16:35:21 +01:00
n loewen 2802623c4c assembler - Change error handling: Just quit, don't throw a JS error 2023-08-24 16:33:33 +01:00
n loewen 1bf2144a67 assembler - Fix 'Too many arguments' error so that it works for normal lines as well as lines with `*` (which might include an optional 2nd arg) 2023-08-24 16:32:29 +01:00
n loewen d30050a292 cpu - Add feature: 'Invalid opcode' error message 2023-08-24 16:31:11 +01:00
n loewen dbe630eb5e cpu - Fix debugging output on JMP / Add previous IP to state + add functions that abstract setting and incrementing the IP 2023-08-24 16:30:28 +01:00
n loewen d56eb34a44 assembler - Add feature: throw useful error on references to undefined constants 2023-08-24 16:01:56 +01:00
n loewen d6b55db381 assembler - Add feature: allow use of `*` with a numeric offset 2023-08-24 15:26:37 +01:00
n loewen dc78518b73 assembler - Fix test for argument-less operations / Remove unnecessary variable 2023-08-24 15:16:12 +01:00
n loewen 51bc13fe5b cpu - Change END to increment IP, to make debugging printout work when the only line in the program is an END 2023-08-24 15:13:49 +01:00
n loewen 0fca21dfab assembler - Change to better catch lines that are missing operands, and provide a useful error message 2023-08-24 15:00:33 +01:00
n loewen 9268b2c59a assembler - Fix: Make preparse start counting lines at 1, so that they match the numbers in your text editor 2023-08-24 14:42:45 +01:00
n loewen 621afa0553 (issues) todo - Add "Abandoned" section and re-insert 'return pure machine code from assembler' in there 2023-08-24 13:19:58 +01:00
n loewen 527d70fcdf (issues) todo - Remove 'return pure machine code from assembler' entry 2023-08-24 13:18:39 +01:00
n loewen 1dc535a71e assembler/cpu - Add feature: Pass debug info from assembler to cpu, so that cpu can print source line 2023-08-24 13:16:01 +01:00
n loewen f0632b0969 (issues) todos - Change entry to point to "execute JMP FF on startup" issue 2023-08-24 12:17:42 +01:00
n loewen 68ae53a245 (issues) issues - Add "execute JMP FF on startup" issue 2023-08-24 12:17:24 +01:00
n loewen aafeeb572b (issues) issues - Create 'issues' file for extended commentary on select tasks 2023-08-24 11:04:38 +01:00
n loewen 7074f0c5f5 (issues) todo - Reformat todo list 2023-08-24 11:04:05 +01:00
n loewen be8b1ef272 Remove 'scratch.js' file 2023-08-24 10:44:56 +01:00
n loewen 6e1a3faff2 (notes) issues - Create/Move: Create 'issues' directory / Move 'todo.md' to issues dir 2023-08-24 10:43:46 +01:00
n loewen 2ad6d46b5d (docs) readme - Change memory map to include initial IP + not-yet-implemented ROM with pointer-to-initial-IP 2023-08-24 08:18:45 +01:00
n loewen 6fc6e727d0 (notes) 2023-08-24 - Create note with brainstorming re: a more accurate start-up process (and memory/peripheral setup) + a todo list 2023-08-24 08:17:38 +01:00
n loewen f2e43888b5 assembler / machine config - Remove/Change: Remove pointer-to-keypad / Change initial IP 2023-08-24 08:16:56 +01:00
n loewen 78ac43bead assembler - Fix bug where the assembler attempted to handle indirect references to constants twice / Remove unused "assemble with optional args" function / Tidy up debugging printouts 2023-08-23 21:23:45 +01:00
n loewen ee1e899108 (tests) fill-display - Fix typo / Change constant names to increase clarity and make it easier to update if the flag IDs change again 2023-08-23 21:22:16 +01:00
n loewen 14770db506 (notes) todo - Add reminder to update CPU startup process / Remove old entries 2023-08-23 15:34:00 +01:00
n loewen 91853cd7e3 (notes) todo - Remove completed flag re-ordering task 2023-08-23 15:30:53 +01:00
n loewen 5f29c7c3eb cpu - Change flags from NZOC to ONZC 2023-08-23 15:30:06 +01:00
n loewen e1cc491e28 (docs) readme - Remove old table of instructions missed during previous change 2023-08-23 15:26:11 +01:00
n loewen ae3743926c (docs) readme - Change title: replace "paper computer" with "imaginary computer" 2023-08-23 15:15:43 +01:00
n loewen 944a0932f6 readme/notes - finish WIP - Change instruction set back to the original, but keep the improvements to the documentation 2023-08-23 15:12:33 +01:00
n loewen af52d4f373 (notes) - 2023-08-23 - Add a caveat about the current plan for the new ISA (requires 8-bit addressing) 2023-08-23 14:47:38 +01:00
n loewen 1d35f659ee readme - WIP - Change "Instruction Set" section to describe a new, revised architecture 2023-08-23 14:45:14 +01:00
n loewen c8d30e524a (notes) 2023-08-23 - Create dev note, thinking about re-arranging the ISA 2023-08-23 14:43:12 +01:00
n loewen c0f11f2b03 assembler - Finish WIP - Refactor extensively / Add a 'set the initial address to assemble at' feature / Add a feature that collects debug info that could be passed as output when running in debug mode 2023-08-22 13:03:52 +01:00
n loewen a766fd867c assembler - WIP - Continue refactoring everything, in support of 'set the initial IP to assemble to' feature, etc. (Everything is probably broken... but it's much closer) 2023-08-21 19:44:16 +01:00
n loewen 12273a6389 assembler - WIP - Continue refactoring everything, in support of 'set the initial IP to assemble to' feature, etc. (Everything is probably broken... but it's much closer) 2023-08-21 19:37:32 +01:00
n loewen f0e8664ab8 assembler - WIP - Continue refactoring everything, in support of 'set the initial IP to assemble to' feature, etc. (Everything is broken...) 2023-08-21 18:34:15 +01:00
n loewen 4481fc10d4 assembler - WIP - Start adding a 'set the initial IP to assembler to' feature + Start adding a 'return debug data as well as machine code' feature 2023-08-21 16:08:58 +01:00
n loewen a52336db07 (docs) readme - Fix keypad section, updating keypad address and removing pointer-to-keypad 2023-08-21 14:47:56 +01:00
n loewen 00ddf2c8fe (docs) todos - Add entry about re-ordering flags register 2023-08-21 14:46:43 +01:00
n loewen d48ead819f (docs) readme - Change memory map + Add section on CPU start-up
Memory map:
- Change to a 5x5 display
- Remove pointer to keypad (I can't see why you would want to relocate it)
- Add placeholder for bank-switching flag
- Remove `$30` as initial value of IP - that's covered (differently) by the new CPU start-up section
2023-08-21 14:46:25 +01:00
n loewen 97f1d02912 tests - Fix fill-display program: use new assembly syntax 2023-08-21 14:40:58 +01:00
n loewen eaa0597552 Merge branch '5x5-display' 2023-08-21 14:38:02 +01:00
n loewen efab770460 (notes) 2023-08-21 - Add section brainstorming a revised memory-map + initial Instruction Pointer 2023-08-21 14:34:20 +01:00
n loewen 209a03f281 (notes) todos - Remove JSDoc entry 2023-08-21 14:11:49 +01:00
n loewen 286875135f (notes) - Update to remove todo entry for previous commit 2023-08-21 12:31:20 +01:00
n loewen 5e1cf9e413 assembler - Change label for program counter from `*addr` to `*` 2023-08-21 12:30:31 +01:00
n loewen 9fa1bf5392 (docs) readme - Change keypad reference to a side-by-side layout 2023-08-21 10:42:36 +01:00
n loewen c5c64e5cd5 (docs) readme - Create a new 'peripherals' section (split up 'memory map/peripherals' section) 2023-08-21 10:37:51 +01:00
n loewen f421abaaf7 (notes) todos/dev note - Update todo list, copying items from today's dev note 2023-08-21 10:34:31 +01:00
n loewen 08fe395a5f (note) 2023-08-21 - Create dev note 2023-08-21 10:29:37 +01:00
n loewen c53bc14c36 (notes) 2023-08-17 - Create note / List todos + program ideas 2023-08-17 11:34:56 +01:00
n loewen 37578f038d (notes) 2023-08-16 - Update todo list to indicate that an item will be moved to the next note 2023-08-17 11:33:56 +01:00
n loewen 190592813d Merge branch 'main' into 5x5-display 2023-08-15 20:58:38 +01:00
n loewen 2cceaa71f2 Refactor: Rewrite fill-display:
- make more use of constants
- improve comments
2023-08-15 20:53:51 +01:00
n loewen 934f4c0f3f Docs: Update to-do list 2023-08-15 17:02:02 +01:00
n loewen 1baa0ded32 Docs: Add an abandoned interval-timer-based refactor of the main CPU loop to dev notes 2023-08-15 17:01:51 +01:00
n loewen c415056194 ! Feature: Change to a 5x5 display [breaking change] 2023-08-15 16:59:12 +01:00
75 changed files with 5375 additions and 2118 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
.vscode
*.tmp.*
node_modules
cardiograph.code-workspace
*venv*
*__pycache__

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "src/argparser"]
path = src/opter
url = https://git.nloewen.com/n/argv-parser.git
[submodule "src/python/opter-py"]
path = src/python/opter-py
url = https://git.nloewen.com/n/opter-py.git

View File

@ -1,228 +0,0 @@
const { logMemory, num2hex } = require('./logging.js');
const {
INITIAL_IP_ADDRESS,
DISPLAY_ADDR,
KEYPAD_ADDR,
POINTER_TO_DISPLAY,
POINTER_TO_KEYPAD
} = require('./machine.config.js');
// 1 = verbose
// 2 = what i'm currently focusing on
// 3 = always print
// 4 = silent
const DEBUG_LEVEL = 2;
let DEBUG = false; // Turn debugging on/off -- set by assemble()
exports.assemble = (str, debug = false) => {
DEBUG = debug;
return decodeInstructions(str);
}
// Configure pseudo-ops:
const POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND = '*addr';
const CONSTANT_PREFIX = '#';
const LABEL_PREFIX = '@';
const mnemonicsWithOptionalArgs = ['end', 'nop'];
const mnemonics2opcodes = {
end: { direct: 0, indirect: 0 },
sto: { direct: 1, indirect: 2 },
lda: { direct: 3, indirect: 4 },
add: { direct: 5, indirect: 6 },
sub: { direct: 7, indirect: 8 },
hop: { direct: 9, indirect: 10 },
jmp: { direct: 11, indirect: 12 },
ftg: { direct: 13, indirect: 13 },
fhp: { direct: 14, indirect: 14 },
nop: { direct: 15, indirect: 15 },
};
/**
* @param {String} line - One line of assembly to decode
**/
function decodeInstructions(line) {
let lines = line.split(/\n/); // returns an array of lines
let machineCode = new Array(INITIAL_IP_ADDRESS).fill(0);
machineCode[POINTER_TO_DISPLAY] = DISPLAY_ADDR;
machineCode[POINTER_TO_KEYPAD] = KEYPAD_ADDR;
let labels = {};
let constants = {};
let IP = INITIAL_IP_ADDRESS;
for (let i = 0; i < lines.length; i++) {
dbg(2, '');
dbgGroup(1, `Input line ${i}, IP ${num2hex(IP)}`);
dbg(3, `> ${lines[i]}`);
let line = stripWhitespaceFromEnds(stripComments(lines[i]));
// Handle blank lines
if (line.length === 0) {
dbg(3, `IP: $${num2hex(IP)}, new code: none`);
dbg(1, 'blank');
dbgGroupEnd(1, 'Input line');
continue;
}
// HANDLE OPS
// Handle label definitions
if (line.startsWith(LABEL_PREFIX)) {
let label = line.substring(1); // strip label prefix
if (label in labels) {
labels[label].pointsToByte = IP;
} else {
labels[label] = {
pointsToByte: IP,
bytesToReplace: [],
};
}
dbg(2, `pointsToByte: ${labels[label].pointsToByte}`);
dbg(2, `bytesToReplace: ${labels[label].bytesToReplace}`);
dbg(3, `IP: $${num2hex(IP)}, new code: none`);
dbgGroupEnd(1, 'Input line');
continue;
}
let op_arg_array = line.split(/\s+/); // split line into an array of [op, arg]
let opName = op_arg_array[0].toLowerCase();
let arg_str = op_arg_array[1];
let arg_num = null;
let addressingMode = 'direct'; // Must be "direct" or "indirect"
// Handle constant definitions
if (opName.startsWith(CONSTANT_PREFIX)) {
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
let constantName = opName.substring(1).toLowerCase(); // strip '>'
let constantValue = arg_str;
if (constantValue.toLowerCase() === POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND) {
constantValue = IP.toString();
}
constants[constantName] = constantValue;
dbg(2, `constants:`);
dbg(2, constants);
continue;
}
// Handle mnemonics without operands (eg END) ...
if (typeof arg_str === 'undefined') {
if (mnemonicsWithOptionalArgs.indexOf(opName) < 0) {
console.error(`Missing opcode: ${line}`);
throw new Error("Missing opcode");
}
arg_num = 0;
// HANDLE OPERANDS
// Handle references to labels
} else if (arg_str.startsWith(LABEL_PREFIX)) {
let label = arg_str.substring(1); // strip label prefix
arg_num = 0;
if (label in labels) {
dbg(1, `'${label}' already in labels object`);
labels[label].bytesToReplace.push(IP + 1);
} else {
dbg(1, `'${label}' NOT in labels object`);
labels[label] = {
bytesToReplace: [IP + 1],
};
}
dbg(2, `pointsToByte: ${labels[label].pointsToByte}`);
dbg(2, `bytesToReplace: ${labels[label].bytesToReplace}`);
// Handle references to the Instruction Pointer
} else if (arg_str.toLowerCase() === POINTER_TO_CURRENT_ADDR_PSEUDO_OPERAND) {
dbg(2, `operand references current address`);
arg_num = IP;
dbg(2, `arg_num: ${num2hex(arg_num)}`);
// Handle references to constants
} else if (arg_str.startsWith(CONSTANT_PREFIX)) {
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
arg_str = arg_str.substring(1).toLowerCase(); // strip '>'
dbg(2, `operand references '${arg_str}'`);
arg_str = constants[arg_str];
dbg(2, `arg_str from '${arg_str}'`);
// Handle references to constants in indirect mode
} else if (arg_str.startsWith(`(${CONSTANT_PREFIX}`)) {
addressingMode = "indirect";
arg_str = arg_str.replace(`(${CONSTANT_PREFIX}`, "");
arg_str = arg_str.replace(")", "");
// FIXME - a quick hack to get around problems caused by another use of lower-casing to sanitize input:
arg_str = arg_str.toLowerCase();
dbg(2, `INDY - operand references '${arg_str}'`);
arg_str = constants[arg_str];
// Handle indirect expressions
} else if (arg_str.startsWith("(")) {
addressingMode = "indirect";
arg_str = arg_str.replace("(", "");
arg_str = arg_str.replace(")", "");
}
// Handle numeric operands
if (arg_num === null) {
if (arg_str.startsWith("$")) {
// Handle hex
arg_str = arg_str.replace("$", "");
arg_num = hex2num(arg_str);
} else {
// Accept decimal i guess
arg_num = parseInt(arg_str);
}
}
// DECODE!
const op = mnemonics2opcodes[opName][addressingMode];
machineCode.push(op);
machineCode.push(arg_num);
dbg(3, `IP: $${num2hex(IP)}, new code: $${num2hex(op)} $${num2hex(arg_num)}`);
IP += 2;
dbgGroupEnd(1, 'Input line');
};
dbg(1, '');
dbgGroup(1, 'Memory before filling in label constants');
dbgExec(1, () => logMemory(new Uint8Array(machineCode)));
dbgGroupEnd(1, 'Memory before filling in label constants');
// Backfill label references
for (let k of Object.keys(labels)) {
dbgGroup(2, `${LABEL_PREFIX}${k}`);
let label = labels[k];
dbg(2, `pointsToByte: ${label.pointsToByte}`);
dbg(2, `bytesToReplace: ${label.bytesToReplace}`);
dbgGroupEnd(2, `label`);
for (let j = 0; j < label.bytesToReplace.length; j++) {
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
}
}
return new Uint8Array(machineCode);
}
function stripComments(line) {
return line.replace(/;.+/,"");
}
function stripWhitespaceFromEnds(line) {
line = line.replace(/^\s+/,"");
line = line.replace(/\s+$/,"");
return line;
}
function hex2num(hex) { return parseInt(hex, 16) };
// Debug helpers
const dbg = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.log(s) };
const dbgGroup = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.group(s) };
const dbgGroupEnd = (lvl, s) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) console.groupEnd() };
const dbgExec = (lvl, func) => { if (DEBUG && (lvl >= DEBUG_LEVEL)) func(); }

391
cpu.js
View File

@ -1,391 +0,0 @@
const readline = require('readline');
const readlineSync = require('readline-sync');
const {
INITIAL_IP_ADDRESS,
CYCLE_LIMIT,
KEYPAD_ADDR,
KEY_MAP,
} = require('./machine.config');
const {
num2hex,
num2bin_4bit,
} = require('./logging.js');
const display = require('./display.js');
// STATE
const CPU = {
// Core state
running: false,
IP: INITIAL_IP_ADDRESS,
FLAGS: 0, // A bit field! 0000 = NZOC
Acc: 0,
memory: null,
// Functions that update core state
/** @param {Uint8Array} data */
loadMemory: (data) => {
CPU.memory = new Uint8Array(256);
CPU.memory.set(data, 0);
},
setFlagNegative: () => { CPU.FLAGS |= 8 },
setFlagZero: () => { CPU.FLAGS |= 4 },
setFlagOverflow: () => { CPU.FLAGS |= 2 },
setFlagCarry: () => { CPU.FLAGS |= 1 },
unsetFlagNegative: () => { CPU.FLAGS &= ~8 },
unsetFlagZero: () => { CPU.FLAGS &= ~4 },
unsetFlagOverflow: () => { CPU.FLAGS &= ~2 },
unsetFlagCarry: () => { CPU.FLAGS &= ~1 },
updateFlagZero: () => {
if (CPU.Acc === 0) {
CPU.setFlagZero();
} else {
CPU.unsetFlagZero();
}
},
updateFlagNegative: () => {
CPU.Acc & 128 ? CPU.setFlagNegative : CPU.unsetFlagNegative
},
// Debug info
currentInstruction: {
opcode: null,
operand: null,
mnemonic: null,
},
cycleCounter: 0,
}
// FUNCTIONS THAT MODIFY STATE
const Instructions = {
end: () => {
CPU.currentInstruction.mnemonic = 'END';
CPU.running = false;
},
store_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'STO lit';
CPU.memory[lit] = CPU.Acc;
CPU.IP = CPU.IP += 2;
},
store_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'STO addr';
CPU.memory[CPU.memory[addr]] = CPU.Acc;
CPU.IP = CPU.IP += 2;
},
load_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'LDA lit';
CPU.Acc = lit;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
load_addr: (addr) => {
CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`;
CPU.Acc = CPU.memory[addr];
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
add_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'ADD lit';
// Calculate sum
let sum = CPU.Acc + lit;
if (sum > 255) {
CPU.setFlagCarry();
sum = (sum % 255) - 1;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
add_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'ADD addr';
// Calculate sum
let sum = CPU.Acc + CPU.memory[addr];
if (sum > 255) {
CPU.setFlagCarry();
sum = (sum % 255) - 1;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
sub_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'SUB lit';
// Calculate sum
let sum = CPU.Acc - lit;
if (sum < 0) {
CPU.setFlagCarry();
sum = sum + 256;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (lit & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
sub_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'SUB addr';
// Calculate sum
let sum = CPU.Acc - CPU.memory[addr];
if (sum < 0) {
CPU.setFlagCarry();
sum = sum + 256;
} else {
CPU.unsetFlagCarry();
}
// Calculate overflow flag status
let bitSixCarry = 0;
if ((CPU.Acc & 64) && (addr & 64)) { bitSixCarry = 1; }
let overflow = bitSixCarry ^ (CPU.FLAGS & 1)
if (overflow) {
CPU.setFlagOverflow();
} else {
CPU.unsetFlagOverflow();
}
CPU.Acc = sum;
CPU.updateFlagNegative();
CPU.updateFlagZero();
CPU.IP = CPU.IP += 2;
},
hop_lit: (lit) => {
CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
if (CPU.Acc === lit) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
hop_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'HOP addr';
if (CPU.Acc === CPU.memory[addr]) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
jump_lit: (lit) => {
CPU.currentInstruction.mnemonic = 'JMP lit';
CPU.IP = lit;
},
jump_addr: (addr) => {
CPU.currentInstruction.mnemonic = 'JMP addr';
CPU.IP = CPU.memory[addr];
},
flag_toggle: (flagNum) => {
CPU.currentInstruction.mnemonic = 'FTG';
let mask = null;
if (flagNum === 0) { mask = 1; }
if (flagNum === 1) { mask = 2; }
if (flagNum === 2) { mask = 4; }
if (flagNum === 3) { mask = 8; }
if (mask === null) { throw new Error('Invalid flag number'); }
CPU.FLAGS = CPU.FLAGS ^= mask;
CPU.IP += 2;
},
flag_hop: (flagNum) => {
CPU.currentInstruction.mnemonic = `FHP; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`;
let mask = null;
if (flagNum === 0) { mask = 1; }
if (flagNum === 1) { mask = 2; }
if (flagNum === 2) { mask = 4; }
if (flagNum === 3) { mask = 8; }
if (mask === null) { throw new Error('Invalid flag number'); }
if (CPU.FLAGS & mask) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
no_op: () => {
CPU.currentInstruction.mnemonic = `NOP`;
CPU.IP += 2;
},
}
const opcodes2mnemonics = {
0: (operand) => Instructions.end(),
1: (operand) => Instructions.store_lit(operand),
2: (operand) => Instructions.store_addr(operand),
3: (operand) => Instructions.load_lit(operand),
4: (operand) => Instructions.load_addr(operand),
5: (operand) => Instructions.add_lit(operand),
6: (operand) => Instructions.add_addr(operand),
7: (operand) => Instructions.sub_lit(operand),
8: (operand) => Instructions.sub_addr(operand),
9: (operand) => Instructions.hop_lit(operand),
10: (operand) => Instructions.hop_addr(operand),
11: (operand) => Instructions.jump_lit(operand),
12: (operand) => Instructions.jump_addr(operand),
13: (operand) => Instructions.flag_toggle(operand),
14: (operand) => Instructions.flag_hop(operand),
15: (operand) => Instructions.no_op(),
};
/**
* Load code into memory and set CPU state to "running"
* @param {Uint8Array} code - Machine code to load
**/
function startCPU(code) {
CPU.loadMemory(code);
CPU.cycleCounter = 0;
CPU.running = true;
// FIXME: This conflicts with single-stepping
// (you can single-step, but keys aren't passed
// through to the Cardiograph)
//
// -> The fix is maybe to remove readlineSync,
// and instead stash the keypress into a buffer variable.*
// Then have the stepping function check that buffer,
// and then clear the buffer, each time it runs.
//
// * If it's in the set of keys that are relevant
// to single-stepping.
// Start listening for keypresses...
readline.emitKeypressEvents(process.stdin);
if (process.stdin.setRawMode != null) {
process.stdin.setRawMode(true);
}
process.stdin.on('keypress', (str, key) => { // TODO: is it possible to turn this off again?
if (key.sequence === '\x03') process.exit();
str = str.toUpperCase();
if (str in KEY_MAP) {
CPU.memory[KEYPAD_ADDR] = KEY_MAP[str];
}
});
}
/**
* Execute just the next instruction in memory
**/
async function stepCPU(debug = false) {
if (CYCLE_LIMIT) { // Temporary limit as a lazy way to halt infinite loops
if (CPU.cycleCounter > CYCLE_LIMIT) {
console.warn('HALTING - reached cycle limit');
CPU.running = false;
}
}
if (CPU.running) {
if (CPU.IP >= CPU.memory.length) {
console.error('HALTING - IP greater than memory size');
CPU.running = false;
} else {
CPU.currentInstruction.opcode = CPU.memory[CPU.IP];
CPU.currentInstruction.operand = CPU.memory[CPU.IP+1];
let executeInstruction = opcodes2mnemonics[CPU.currentInstruction.opcode];
executeInstruction(CPU.currentInstruction.operand);
CPU.cycleCounter += 1;
}
}
logCPUState(debug);
}
/**
* @param {Uint8Array} code - Machine code to run
* @param {Boolean} [debug] - Enable/disable debugging printouts
* @param {Number} [clockSpeed] - CPU clock speed in milliseconds
**/
exports.runProgram = (code, debug = false, clockSpeed = 100) => {
startCPU(code);
// Animate the output by pausing between steps
const loop = setInterval(async () => {
stepCPU(debug);
if (!CPU.running) clearInterval(loop);
}, clockSpeed);
}
/**
* @param {Uint8Array} code - Machine code to run
* @param {Boolean} [debug] - Enable/disable debugging printouts
**/
exports.singleStepProgram = (code, debug = false) => {
startCPU(code);
while (CPU.running) {
stepCPU(debug);
// FIXME: this prevents exiting with Ctrl+C:
let key = readlineSync.keyIn('S to step, Q to quit > ', {
limit: ['s', 'S', 'q', 'Q'],
});
if (key.toLowerCase() === 'q') { process.exit(); }
console.log();
}
}
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
/**
* @param {Boolean} [debug] - Enable/disable debugging printouts
**/
function logCPUState(debug = false) {
console.group(`Step`);
if (!debug) console.clear();
if (debug) {
display.printDisplay(CPU.memory);
} else {
display.prettyPrintDisplay(CPU.memory);
}
console.log();
console.log('Mnemonic:', CPU.currentInstruction.mnemonic);
console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`);
console.log();
console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} NZOC: ${num2bin_4bit(CPU.FLAGS)}`);
console.log(`KEY: $${num2hex(CPU.memory[KEYPAD_ADDR])}  ${CPU.running ? "running" : "halted" }`);
console.log();
console.groupEnd();
};

View File

@ -1,30 +0,0 @@
const { POINTER_TO_DISPLAY } = require('./machine.config');
const { num2hex } = require('./logging.js');
/**
* Print the contents of display memory as hex number
* @param {Uint8Array} mem - CPU memory
**/
const printDisplay = (mem) => {
const disp = mem[POINTER_TO_DISPLAY];
for (let i = disp; i < disp + 16; i += 4) {
console.log(`${num2hex(mem[i])} ${num2hex(mem[i+1])} ${num2hex(mem[i+2])} ${num2hex(mem[i+3])}`);
}
}
/**
* Print the contents of display memory using black and white emoji circles
* @param {Uint8Array} mem - CPU memory
**/
const prettyPrintDisplay = (mem) => {
const disp = mem[POINTER_TO_DISPLAY];
const num2pic = (n) => n > 0 ? '⚫' : '⚪';
for (let i = disp; i < disp + 16; i += 4) {
console.log(`${num2pic(mem[i])}${num2pic(mem[i+1])}${num2pic(mem[i+2])}${num2pic(mem[i+3])}`);
}
}
module.exports = {
"printDisplay": printDisplay,
"prettyPrintDisplay": prettyPrintDisplay,
}

459
javascript-old/assembler.js Executable file
View File

@ -0,0 +1,459 @@
#!/usr/bin/env node
const fs = require('fs');
const Opter = require('./opter/opter.js');
const { logMemory } = require('./logging.js');
const { num2hex, hex2num, bin2num } = require('./conversions.js');
const DBG = require('./dbg.js');
const CFG = require('./machine.config.js');
/** Configure pseudo-ops **/
const ASM_IP_LABEL = '*';
const ASM_CONSTANT_PREFIX = '#';
const ASM_LABEL_PREFIX = '@';
/** Configure mnemonics **/
const mnemonicsWithOptionalArgs = ['end', 'nop'];
const mnemonics2opcodes = {
end: { direct: 0, indirect: 0 },
sto: { direct: 1, indirect: 2 },
lda: { direct: 3, indirect: 4 },
add: { direct: 5, indirect: 6 },
sub: { direct: 7, indirect: 8 },
hop: { direct: 9, indirect: 10 },
jmp: { direct: 11, indirect: 12 },
ftg: { direct: 13, indirect: 13 },
fhp: { direct: 14, indirect: 14 },
nop: { direct: 15, indirect: 15 },
};
/**
* @typedef {('code'|'comment'|'blank')} SourceLineType
**/
/**
* @typedef {Object} SourceLineInfo
* @property {number} number - line number
* @property {string} source - source text
* @property {string} sanitized - source text, with comments and whitespace removed
* @property {SourceLineType} type - line type
* @property {string} [operation] - For code: the first non-whitespace chunk
* @property {string} [argument] - For code: the second non-whitespace chunk, if there is one
* @property {string} [extraArgument] - For code: the third non-whitespace chunk, if there is one
**/
/**
* @param {string} source
* @returns {Array<SourceLineInfo>}
**/
function preparseSourceCode(source) {
let lines = source.split(/\n/); // returns an array of lines
const isLineBlank = (l) => { return stripWhitespaceFromEnds(l).length === 0 ? true : false };
const isLineComment = (l) => { return stripWhitespaceFromEnds(l).startsWith(';') };
/**
* @param {string} l
* @returns {SourceLineType}
**/
const getLineType = (l) => {
if (isLineBlank(l)) return 'blank';
if (isLineComment(l)) return 'comment';
return 'code';
}
return lines.map((line, index) => {
dbg.nit(` in: ${line}`);
let info = {
number: index + 1,
source: line,
sanitized: stripWhitespaceFromEnds(stripComments(line)),
type: getLineType(line),
};
dbg.nit(`${info.number} - ${info.type}: ${info.sanitized}`);
dbg.nit(``);
if (info.type === 'code') {
const op_arg_array = info.sanitized.split(/\s+/); // split line into an array of [op, arg, extra_arg]
if (op_arg_array[0] !== 'undefined') {
info.operation = op_arg_array[0];
}
if (op_arg_array.length === 2) {
info.argument = op_arg_array[1];
}
if (op_arg_array.length === 3) {
info.argument = op_arg_array[1];
info.extraArgument = op_arg_array[2];
}
// If there's too many arguments, throw an error
// NB. there's a special case:
// lines with the ASM_IP_LABEL can take an extra argument
let maxArgs = 2;
if (op_arg_array.length > 2 && op_arg_array[1].startsWith(ASM_IP_LABEL)) {
maxArgs = 3;
}
if (op_arg_array.length > maxArgs) {
console.error();
console.error(`Error: Too many arguments`);
console.error(` at line ${info.number}`);
process.exit();
}
}
return info;
});
}
/**
* @param {string} arg
* @returns {number}
**/
function decodeNumericOp(arg) {
if (arg.startsWith("$")) return hex2num(arg.replace("$", ""));
if (arg.startsWith("0x")) return hex2num(arg.replace("0x", ""));
if (arg.startsWith("0b")) return bin2num(arg.replace("0b", ""));
return parseInt(arg);
}
/**
* @param {string} op
* @param {object} labels // TODO - document labels object
* @param {number} IP
* @returns {Array<string>} - array of labels
**/
function handleLabelDefinition(op, IP, labels) {
let label = op.substring(1); // strip label prefix
if (label in labels) {
labels[label].pointsToByte = IP;
} else {
labels[label] = {
pointsToByte: IP,
bytesToReplace: [],
};
}
dbg.nit(` Label definition:`);
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`);
dbg.nit(` IP: $${num2hex(IP)}, new code: none`);
dbg.nitGroupEnd('Input line');
return labels;
}
/**
* @param {string} op
* @param {string} arg
* @param {number} IP
* @returns {Array<string>} - array of constants
**/
function handleConstantDefinitions(op, arg, IP, constants) {
let constantName = op.substring(1); // strip '>'
let constantValue = arg;
if (constantValue === ASM_IP_LABEL) {
constantValue = IP.toString();
}
constants[constantName] = constantValue;
dbg.nit('');
dbg.nit(`Constants:`);
dbg.nit(constants);
dbg.nit('');
return constants;
}
/**
* Assemble source code.
*
* If the source doesn't explicitly set an address to assemble to,
* it will be assembled to the default intial value of the IP,
* as specified in `machine.config.js`.
* @param {string} source - Assembly source to decode
* @return {{ sourceAnnotations: Object, machineCode: Array }};
**/
// TODO rename?
function decodeInstructions(source) {
dbg.nit('Pre-parsing...');
let lines = preparseSourceCode(source);
dbg.nit('');
dbg.nit('Done pre-parsing.');
dbg.nit('');
dbg.nit('Assembling...');
// Figure out where to start assembly...
/** @type {number} IP - Destination addr for the next line **/
let IP;
// Check if the source code explicitly sets an address to assemble at
// by including a `* [addr]` as the first (non-blank, non-comment) line
let idOfFirstLineWithCode = lines.findIndex((el) => el.type === 'code');
if (lines[idOfFirstLineWithCode].operation.startsWith(ASM_IP_LABEL)) {
IP = parseInt(lines[idOfFirstLineWithCode].argument);
} else {
IP = CFG.initialIP;
}
// Initialize arrays to collect assembled code
/** @type {Array<number>} - Assembled source code, as an array of bytes **/
let machineCode = new Array(IP).fill(0);
let sourceAnnotations = {};
// Initialize memory-mapped IO -- TODO this should probably be in the CPU, not here
machineCode[CFG.pointerToDisplay] = CFG.displayAddr;
// Initialize arrays that collect code references that
// have to be revisited after our first pass through the source
let labels = {};
let constants = {};
// Decode line by line...
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// dbg(2, `line info:`);
// dbg(2, line);
if (line.type === 'code') {
const op = line.operation;
if (typeof line.argument === 'undefined') {
// If this isn't a label definition,
// or one of the ops with optional arguments,
// then it's an error
if (!line.operation.startsWith('@')) {
if (mnemonicsWithOptionalArgs.indexOf(line.operation.toLowerCase()) < 0) {
console.error('');
console.error(`Error: Missing operand ${line.source}`);
console.error(` at line ${line.number}`);
process.exit();
} else {
// It *is* one of the special optional-arg ops
// So let's fill in the implicit operand with $00
line.argument = '0';
}
}
}
// *** Decode special operations ***
// Opcodes - Handle label definitions
if (op.startsWith(ASM_LABEL_PREFIX)) {
labels = handleLabelDefinition(op, IP, labels);
continue;
}
// Opcodes - Handle constant definitions
if (op.startsWith(ASM_CONSTANT_PREFIX)) {
constants = handleConstantDefinitions(op, line.argument, IP, constants);
continue;
}
// Opcodes - Handle setting value of IP
if (op.startsWith(ASM_IP_LABEL)) {
IP = parseInt(line.argument);
continue;
}
// *** Decode regular operations ***
/** @type {number|null} decodedOp **/
let decodedOp = null;
/** @type {number|null} decodedArg **/
let decodedArg = null;
/** @typedef {'direct'|'indirect'} AddressingMode **/
let addressingMode = 'direct';
// Now that it can't be a label or a constant, normalize the opcode
line.operation = line.operation.toLowerCase();
// Operands - Handle references to labels
if (line.argument.startsWith(ASM_LABEL_PREFIX)) {
let label = line.argument.substring(1); // strip label prefix
if (label in labels) {
dbg.nit(`'${label}' already in labels object`);
labels[label].bytesToReplace.push(IP + 1);
} else {
dbg.nit(`'${label}' NOT in labels object`);
labels[label] = {
bytesToReplace: [IP + 1],
};
}
dbg.nit(`Label reference:`);
dbg.nit(` Points to byte: ${labels[label].pointsToByte}`);
dbg.nit(` Bytes to replace: ${labels[label].bytesToReplace}`);
decodedArg = 0; // Return 0 for operand for now -- we'll replace it later
}
// Operands - Handle references to the Instruction Pointer
if (line.argument === ASM_IP_LABEL) {
dbg.nit(` References current IP - ${IP}`);
if (typeof line.extraArgument === 'undefined') {
decodedArg = IP;
} else {
decodedArg = IP + decodeNumericOp(line.extraArgument);
}
}
// Operands - Handle references to constants
if (line.argument.startsWith(ASM_CONSTANT_PREFIX)) {
dbg.nit(`References '${line.argument}'`);
if (typeof constants[line.argument.substring(1)] === 'undefined') {
console.error();
console.error(`Error: Undefined constant '${line.argument}'`);
console.error(` at line ${line.number}`);
process.exit();
}
decodedArg = decodeNumericOp(constants[line.argument.substring(1)]); // substring(1) strips '>'
}
// Operands - Handle references to constants in indirect mode
if (line.argument.startsWith(`(${ASM_CONSTANT_PREFIX}`)) {
addressingMode = "indirect";
dbg.nit(`(Indirectly) References '${line.argument}'`);
let constName = line.argument.replace(`(${ASM_CONSTANT_PREFIX}`, "");
constName = constName.replace(")", "");
decodedArg = decodeNumericOp(constants[constName]);
}
// Operands - Handle indirect expressions
if (decodedArg === null && line.argument.startsWith("(")) {
addressingMode = "indirect";
let indyTemp = line.argument.replace("(", "").replace(")", "");
decodedArg = decodeNumericOp(indyTemp);
}
// Decode regular opcodes
if (decodedOp === null) {
decodedOp = mnemonics2opcodes[line.operation][addressingMode];
}
// Decode regular operands
if (decodedArg === null) {
decodedArg = decodeNumericOp(line.argument);
}
machineCode[IP] = decodedOp;
machineCode[IP + 1] = decodedArg;
sourceAnnotations[IP] = {
lineNumber: line.number,
source: line.source,
address: IP,
machine: [decodedOp, decodedArg]
};
dbg.i();
dbg.i(`Line ${line.number}: ${line.source}`);
if (line.argument) {
dbg.i(` Asm operation: ${line.operation.toUpperCase()} ${line.argument}`);
} else if (line.operation) {
dbg.i(` Asm operation: ${line.operation.toUpperCase()}`);
}
dbg.i(` Machine code: $${num2hex(decodedOp)} $${num2hex(decodedArg)}`);
dbg.i(` IP: $${num2hex(IP)}`);
IP += 2;
};
}
dbg.nit('');
dbg.nitGroup('Memory before filling in label constants');
dbg.nitExec(() => logMemory(new Uint8Array(machineCode)));
dbg.nitGroupEnd();
// Backfill label references
for (let k of Object.keys(labels)) {
dbg.nitGroup(`${ASM_LABEL_PREFIX}${k}`);
let label = labels[k];
dbg.nit(`Points to byte: ${label.pointsToByte}`);
dbg.nit(`Bytes to replace: ${label.bytesToReplace}`);
dbg.nitGroupEnd();
for (let j = 0; j < label.bytesToReplace.length; j++) {
machineCode[label.bytesToReplace[j]] = label.pointsToByte;
}
}
return { 'machineCode': machineCode, 'sourceAnnotations': sourceAnnotations };
}
/**
* @param {string} line
* @returns {string}
**/
function stripComments(line) {
return line.replace(/;.+/,"");
}
/**
* @param {string} line
* @returns {string}
**/
function stripWhitespaceFromEnds(line) {
line = line.replace(/^\s+/,"");
line = line.replace(/\s+$/,"");
return line;
}
/**
* Assemble source code into machine code.
* If 'includeMetadata' is true, a JSON object containing
* both machine code and metadata is written to the output file.
* Otherwise, a string of decimal numbers is written.
* @arg {string} inputFilename File containing code to assemble
* @arg {boolean} outputToFile If false, output is on stdout
* @arg {boolean} includeMetadata Include metadata when writing output to a file? (for use when debugging using the simulator)
* @arg {string} [outputFilename] Output file for machine code (and optional metadata)
**/
function assemble(inputFilename, outputToFile, includeMetadata, outputFilename=null) {
const sourceCode = fs.readFileSync(inputFilename, 'utf8');
const out = decodeInstructions(sourceCode);
if (includeMetadata) {
const debugJSON = JSON.stringify(out);
if (outputToFile) {
fs.writeFileSync(outputFilename, debugJSON);
} else {
console.log(debugJSON);
}
} else {
const asciiMachineCode = out.machineCode.toString().replace(/,/g, ' ');
if (outputToFile) {
fs.writeFileSync(outputFilename, asciiMachineCode);
} else {
console.log(asciiMachineCode);
}
}
}
/** MAIN **/
// Initialize debugger...
const dbg = new DBG('nitpick');
// Handle command-line options...
const opter = new Opter();
opter.addOption('-a', '--annotate');
opter.addOption('-i', '--in', true, true, 1);
opter.addOption('-o', '--out', false, true, 1);
let opts = opter.parse(process.argv);
const inputFilename = opts.in[0];
let outputWithAnnotations = 'annotate' in opts;
// Assemble...!
if ('out' in opts) {
const outputFilename = opts.out[0];
assemble(inputFilename, true, outputWithAnnotations, outputFilename);
} else {
dbg.setLevel('none');
assemble(inputFilename, false, outputWithAnnotations);
}

99
javascript-old/cardiograph.js Executable file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env node
const fs = require('fs');
const DBG = require('./dbg.js');
const Opter = require('./opter/opter.js');
const { num2hex, bool2bit } = require('./conversions.js');
const CFG = require('./machine.config.js');
const CPU = require('./cpu.js');
const io = require('./io.js');
/** SETUP **/
const dbg = new DBG('nitpick');
let cpu = new CPU(CFG.initialIP, CFG.defaultCycleLimit);
main();
async function main() {
const opter = new Opter();
opter.addOption('-i', '--in', false, true, 1);
const opts = opter.parse(process.argv);
let input = null;
if ('in' in opts) { // Read from file
input = fs.readFileSync(opts.in[0], 'utf8');
} else { // Read from stdin
input = await readPipedStdin();
}
let code = null;
let sourceAnnotations = null;
try {
const parsedInput = JSON.parse(input);
sourceAnnotations = parsedInput.sourceAnnotations;
code = new Uint8Array(parsedInput.machineCode);
} catch (error) {
if (error.name === 'SyntaxError') {
code = new Uint8Array(input.split(' '));
}
}
cpu.loadMemory(code);
if (sourceAnnotations !== null) { cpu.loadSourceAnnotations(sourceAnnotations); }
cpu.onCycleEnd(tick);
cpu.onCycleEnd(logCPUState);
cpu.start();
io.getKeypadInput(cpu);
cpu.step();
}
async function tick() {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
await sleep(100);
cpu.step();
if (!cpu.running) {
console.log('Halted');
process.exit();
}
}
function logCPUState() {
let lineInfo = null;
if (cpu.dbg.sourceAnnotations) {
lineInfo = cpu.dbg.sourceAnnotations[cpu.dbg.previousIP];
}
console.group(`Step ${cpu.dbg.cycleCounter}`);
console.log();
io.showDisplay(cpu.memory, true); // FIXME - display - allow printing hex as well as pretty-printing
console.log();
if (lineInfo) {
console.log(`Line ${lineInfo.lineNumber}: ${lineInfo.source}`);
console.log();
}
console.log('Mnemonic:', cpu.dbg.currentMnemonic);
console.log(`Machine: $${num2hex(cpu.instruction.opcode)} $${num2hex(cpu.instruction.operand)}`);
console.log();
console.log(`IP: $${num2hex(cpu.IP)} Acc: $${num2hex(cpu.acc)} ONZC ${bool2bit(cpu.flags.O)}${bool2bit(cpu.flags.N)}${bool2bit(cpu.flags.Z)}${bool2bit(cpu.flags.C)}`);
console.log(`KEY: ${io.readKeyMem(cpu.memory)} ${cpu.running ? "running" : "halted" }`);
console.log();
console.groupEnd();
};
async function readPipedStdin() {
// https://wellingguzman.com/notes/node-pipe-input
return new Promise(function (resolve, reject) {
const stdin = process.stdin;
stdin.setEncoding('utf8');
let data = '';
stdin.on('data', function (chunk) { data += chunk; });
stdin.on('end', function () { resolve(data); });
stdin.on('error', reject);
});
}

View File

@ -0,0 +1,48 @@
/**
* @param {number} num
* @returns {string}
*/
const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0");
/**
* @param {string} hex
* @returns {number}
*/
const hex2num = (hex) => parseInt(hex, 16);
/**
* Convert a number to binary, padded to 8 bits
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
* @param {number} num
* @returns {string} binary representation of the input
**/
const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0");
/**
* Convert a number to binary, padded to 4 bits
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
* @param {number} num
* @returns {string} binary representation of the input
**/
const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0");
/**
* @param {string} bin
* @returns {number}
*/
const bin2num = (bin) => parseInt(bin, 2)
/**
* @param {Boolean} bool
* @returns {0|1}
**/
const bool2bit = (bool) => bool ? 1 : 0;
module.exports = {
"num2hex": num2hex,
"hex2num": hex2num,
"num2bin": num2bin,
"num2bin_4bit": num2bin_4bit,
"bin2num": bin2num,
"bool2bit": bool2bit,
}

320
javascript-old/cpu.js Normal file
View File

@ -0,0 +1,320 @@
const { num2hex } = require('./conversions.js');
module.exports = class CPU {
/**
* @arg {number} initialIP
**/
constructor(initialIP, cycleLimit) {
this.running = false;
this.IP = initialIP;
this.acc = 0;
this.flags = {'C': false, 'Z': false, 'N': false, 'O': false};
this.flagNums = {0: 'C', 1: 'Z', 2: 'N', 3: 'O'};
this.instruction = { opcode: null, operand: null };
this.memory = null;
this._cycleLimit = cycleLimit;
this.dbg = {
sourceInfo: null,
currentMnemonic: null,
previousIP: initialIP,
cycleCounter: 0,
}
}
/** Public interface **/
/**
* @param {Uint8Array} machineCode
**/
loadMemory(machineCode) {
this.memory = new Uint8Array(256);
this.memory.set(machineCode, 0);
}
peek() { return; } // TODO - implement Peek
poke() { return; } // TODO - implement Poke
/** @param {Array} info **/ // TODO - document type for 'sourceInfo'
loadSourceAnnotations(info) {
this.dbg.sourceInfo = info;
}
/** Set CPU state to "state.running" **/
start() {
this.running = true;
}
/** Execute the next instruction in memory **/
step() {
this._cycleStartCallbacks.forEach((fn) => fn());
if (this.IP >= this.memory.length) {
this.running = false;
throw new Error('HALTING - IP greater than memory size');
} else {
this.instruction.opcode = this.memory[this.IP];
this.instruction.operand = this.memory[this.IP+1];
let mnem = this._nums2mnems[this.instruction.opcode];
let op = this._ops[mnem];
if (typeof op === 'undefined') { this._failInvalidOpcode(); }
op(this.instruction.operand);
this.dbg.cycleCounter += 1;
}
// Temporary limit as a lazy way to halt infinite loops
if ((this._cycleLimit > 0) && this.dbg.cycleCounter >= this._cycleLimit) {
this.running = false;
throw new Error(' HALTING - reached cycle limit');
}
this._cycleEndCallbacks.forEach((fn) => fn());
if (!this.running) process.exit();
}
/** Private methods **/
_incrementIP(offset) {
this.dbg.previousIP = this.IP;
this.IP = this.IP + offset;
}
_setIP(address) {
this.dbg.previousIP = this.IP;
this.IP = address;
}
_updateFlagZero() {
this.flags.Z = this.acc === 0;
}
_updateFlagNegative() {
if (this.acc & 128)
{ this.flags.N = true; }
else
{ this.flags.N = false; }
}
/** Hooks **/
/** @type Array<Function> **/
_cycleStartCallbacks = [];
/** @type Array<Function> **/
_cycleEndCallbacks = [];
/** @param {function} fn **/
onCycleStart(fn) { this._cycleStartCallbacks.push(fn) };
/** @param {function} fn **/
onCycleEnd(fn) { this._cycleEndCallbacks.push(fn) };
_ops = {
end: () => {
this.dbg.currentMnemonic = 'END';
this.running = false;
this._incrementIP(2);
},
store_lit: (lit) => {
this.dbg.currentMnemonic = 'STO lit';
this.memory[lit] = this.acc;
this._incrementIP(2);
},
store_addr: (addr) => {
this.dbg.currentMnemonic = `STO addr; @addr: $${num2hex(this.memory[addr])}`;
this.memory[this.memory[addr]] = this.acc;
this._incrementIP(2);
},
load_lit: (lit) => {
this.dbg.currentMnemonic = 'LDA lit';
this.acc = lit;
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
load_addr: (addr) => {
this.dbg.currentMnemonic = `LDA addr; @ addr: $${num2hex(this.memory[addr])}`;
this.acc = this.memory[addr];
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
add_lit: (lit) => {
this.dbg.currentMnemonic = 'ADD lit';
const [sum, carry, overflow] = sumCarryOverflow(this.acc, lit);
this.acc = sum;
this.flags.C = carry;
this.flags.O = overflow;
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
add_addr: (addr) => {
this.dbg.currentMnemonic = `ADD addr; @ addr: $${num2hex(this.memory[addr])}`;
const [sum, carry, overflow] = sumCarryOverflow(this.acc, this.memory[addr]);
this.acc = sum;
this.flags.C = carry;
this.flags.O = overflow;
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
sub_lit: (lit) => {
this.dbg.currentMnemonic = 'SUB lit';
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, lit);
this.acc = difference;
this.flags.C = carry;
this.flags.O = overflow;
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
sub_addr: (addr) => {
this.dbg.currentMnemonic = `SUB addr; @ addr: $${num2hex(this.memory[addr])}`;
const [difference, carry, overflow] = differenceCarryOverflow(this.acc, this.memory[addr]);
this.acc = difference;
this.flags.C = carry;
this.flags.O = overflow;
this._updateFlagNegative();
this._updateFlagZero();
this._incrementIP(2);
},
hop_lit: (lit) => {
this.dbg.currentMnemonic = `HOP lit; IP+2: $${this.memory[this.IP+2]}, IP+3: $${this.memory[this.IP+3]}`;
if (this.acc === lit) {
this._incrementIP(4);
} else {
this._incrementIP(2);
}
},
hop_addr: (addr) => {
this.dbg.currentMnemonic = 'HOP addr';
if (this.acc === this.memory[addr]) {
this._incrementIP(4);
} else {
this._incrementIP(2);
}
},
jump_lit: (lit) => {
this.dbg.currentMnemonic = 'JMP lit';
this._setIP(lit);
},
jump_addr: (addr) => {
this.dbg.currentMnemonic = 'JMP addr';
this._setIP(this.memory[addr]);
},
flag_toggle: (flagNum) => {
if (flagNum === null) {
let info = this.dbg.sourceInfo[this.IP];
throw new Error(`Invalid flag number: '${flagNum}' on line ${info.lineNumber}: ${info.source}`);
}
const flagName = this.flagNums[flagNum];
this.dbg.currentMnemonic = `FTG ${flagName}`;
this.flags[flagName] = !this.flags[flagName];
this._incrementIP(2);
},
flag_hop: (flagNum) => {
if (flagNum === null) {
console.error('Invalid flag number');
process.exit();
}
const flagName = this.flagNums[flagNum];
this.dbg.currentMnemonic =
`FHP ${flagName}; IP+2: ${this.memory[this.IP+2]}, IP+3: ${this.memory[this.IP+3]}`;
if (this.flags[this.flagNums[flagNum]]) {
this._incrementIP(4);
} else {
this._incrementIP(2);
}
},
no_op: () => {
this.dbg.currentMnemonic = `NOP`;
this._incrementIP(2);
},
}
_nums2mnems = {
0: "end",
1: "store_lit",
2: "store_addr",
3: "load_lit",
4: "load_addr",
5: "add_lit",
6: "add_addr",
7: "sub_lit",
8: "sub_addr",
9: "hop_lit",
10: "hop_addr",
11: "jump_lit",
12: "jump_addr",
13: "flag_toggle",
14: "flag_hop",
15: "no_op",
}
_failInvalidOpcode() {
let info = this.dbg.sourceInfo[this.dbg.previousIP];
console.error();
console.error(`Error: Invalid opcode`);
console.error(` Executing $${num2hex(info.machine[0])} $${num2hex(info.machine[1])}`);
console.error(` from line ${info.lineNumber}: ${info.source}`);
process.exit();
}
}
/**
* @arg {number} n1
* @arg {number} n2
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
**/
function sumCarryOverflow(n1, n2) {
let sum = n1 + n2;
let carry = false;
if (sum > 255) {
carry = true;
sum = (sum % 255) - 1;
}
let n1_bit6 = (n1 & 64) === 64; // Bit 6 is the 64s place
let n2_bit6 = (n2 & 64) === 64; // 64 & n == 64 where n >= 64
let carryIntoLastBit = n1_bit6 && n2_bit6;
console.log('c_in', carryIntoLastBit, 'c_out', carry);
let overflow = carryIntoLastBit != carry;
return [sum, carry, overflow];
}
/**
* @arg {number} n1
* @arg {number} n2
* @returns {[number, boolean, boolean]} [sum, carry, overflow]
**/
function differenceCarryOverflow(n1, n2) {
// https://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
// > SBC simply takes the ones complement of the second value and then performs an ADC.
//
// https://stackoverflow.com/a/8966863
// > The signed overflow flag value, however, must be the same for both A-B and A+(-B) because it depends on whether or not the result has the correct sign bit and in both cases the sign bit will be the same.
return sumCarryOverflow(n1, -n2);
}

113
javascript-old/dbg.js Normal file
View File

@ -0,0 +1,113 @@
module.exports = class DBG {
/**
* @param ${'none'|'warn'|'info'|'debug'|'nitpick'} [level='info']
**/
constructor(level = 'info') {
this.setLevel(level);
}
_levels = ['nitpick', 'debug', 'info', 'warn', 'none'];
setLevel(level) {
if (this._levels.includes(level)) {
this._level = level;
} else {
throw new Error(`'${level}' is not a valid debug level`);
}
}
/** @param {any} s **/
warn = (s='', ...z) => {
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
console.log(s, ...z);
}
/** @param {any} s **/
i = (s='', ...z) => {
if (this._lvl2num('info') < this._lvl2num(this._level)) return
console.log(s, ...z);
}
/** @param {any} s **/
d = (s='', ...z) => {
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
console.log(s, ...z);
}
/** @param {any} s **/
nit = (s='', ...z) => {
if (this._lvl2num('nitpick') < this._lvl2num(this._level)) return
console.log(s, ...z);
}
warnGroup = (s) => {
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
console.group(s);
}
infoGroup = (s) => {
if (this._lvl2num('info') < this._lvl2num(this._level)) return
console.group(s);
}
debugGroup = (s) => {
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
console.group(s);
}
nitGroup = (s) => {
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
console.group(s);
}
warnGroupEnd = (s) => {
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
console.groupEnd();
}
infoGroupEnd = (s) => {
if (this._lvl2num('info') < this._lvl2num(this._level)) return
console.group();
}
debugGroupEnd = (s) => {
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
console.group();
}
nitGroupEnd = (s) => {
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
console.group();
}
warnExec = (fn) => {
if (this._lvl2num('warn') < this._lvl2num(this._level)) return
fn();
}
infoExec = (fn) => {
if (this._lvl2num('info') < this._lvl2num(this._level)) return
fn();
}
debugExec = (fn) => {
if (this._lvl2num('debug') < this._lvl2num(this._level)) return
fn();
}
nitExec = (fn) => {
if (this._lvl2num('nit') < this._lvl2num(this._level)) return
fn();
}
_lvl2num(lvl) {
return 1 + this._levels.findIndex(l => l === lvl);
}
}
/* TEST
const dbg = new DBG('nitpick');
dbg.warnGroup('w');
dbg.warn('warn');
dbg.warnGroupEnd();
dbg.iGroup('i');
dbg.i('info');
dbg.iGroupEnd();
dbg.dGroup('d');
dbg.d('debug');
dbg.dGroupEnd();
dbg.nitGroup('n');
dbg.nit('nitpick');
dbg.nitGroupEnd();
*/

44
javascript-old/io.js Normal file
View File

@ -0,0 +1,44 @@
const readline = require('readline');
const CFG = require('./machine.config.js');
const { num2hex } = require('./logging.js');
function readKeyMem(mem) {
return mem[CFG.keypadAddr];
}
function getKeypadInput(cpu) {
readline.emitKeypressEvents(process.stdin);
if (process.stdin.setRawMode != null) {
process.stdin.setRawMode(true);
}
process.stdin.on('keypress', (str, key) => {
if (key.sequence === '\x03') process.exit();
let name = key.name.toUpperCase();
if (name in CFG.keyMap) {
cpu.memory[CFG.keypadAddr] = CFG.keyMap[name];
}
});
}
/**
* Print the contents of display memory
* by default, each pixel is shown as a hex number
* @param {Uint8Array} mem - CPU memory
* @param {Boolean} pretty - Display pixels using black and white emoji circles
**/
function showDisplay(mem, pretty=false) {
const disp = mem[CFG.pointerToDisplay];
const num2pic = (n) => n > 0 ? '⚫' : '⚪';
let fmt = (n) => num2hex(n);
if (pretty) fmt = (n) => num2pic(n);
for (let i = disp; i < disp + 25; i += 5) {
console.log(`${fmt(mem[i])} ${fmt(mem[i+1])} ${fmt(mem[i+2])} ${fmt(mem[i+3])} ${fmt(mem[i+4])}`);
}
}
module.exports = {
"showDisplay": showDisplay,
"getKeypadInput": getKeypadInput,
"readKeyMem": readKeyMem,
}

46
javascript-old/logging.js Normal file
View File

@ -0,0 +1,46 @@
let { num2hex } = require('./conversions.js');
/**
* Display a table of memory locations.
* Call with [start] and [end] indices to display a range.
* @param {Uint8Array} mem - Memory to display
* @param {number} [start] - A start-index, in decimal
* @param {number} [end] - An end-index, in decimal
**/
const logMemory = (mem, start=0, end=mem.length) => {
let top1 = `┌─────────┬────────┬─────────┐`;
let top2 = `│ addrs │ opcode │ operand │`;
let top3 = `├─────────┼────────┼─────────┤`;
let blnk = `│ │ │ │`;
let bot1 = `└─────────┴────────┴─────────┘`;
console.log(`${top1}\n${top2}\n${top3}`);
for (let i = start; i < mem.length; i +=2) {
let operand = mem[i+1];
if (typeof operand === 'undefined') {
console.log(`${num2hex(i)} ${num2hex(i+1)}${num2hex(mem[i])} │ │`);
} else {
console.log(`${num2hex(i)} ${num2hex(i+1)}${num2hex(mem[i])}${num2hex(operand)}`);
}
// Add a blank row every 4 lines:
let rowNum = i - start + 2; // Not actually the row number...
if ((rowNum % 8 === 0)
&& (i < (mem.length - 2))) {
console.log(blnk);
}
}
console.log(bot1);
}
const logRunningHeader = () => {
console.log();
let time = new Date();
console.log( `┌─────────────────────┐`);
console.log( `│ Running at ${time.toLocaleTimeString('en-GB')}` );
console.log( `└─────────────────────┘`);
}
module.exports = {
"logMemory": logMemory,
"logRunningHeader": logRunningHeader,
}

View File

@ -1,14 +1,13 @@
module.exports = {
"INITIAL_IP_ADDRESS": 48,
"initialIP": 29,
// Use these in CPU:
"DISPLAY_ADDR": 0,
"KEYPAD_ADDR": 32,
// Store the `X_ADDR`s at these addresses when assembling:
"POINTER_TO_DISPLAY": 33,
"POINTER_TO_KEYPAD": 34,
"displayAddr": 0,
"keypadAddr": 27,
// Store the `DISPLAY_ADDR` at this address when assembling:
"pointerToDisplay": 26,
"KEY_MAP": {
"keyMap": {
// Same layout as COSMAC VIP / CHIP-8
// (This object maps qwerty keys to hex keys
// so that they are arranged in the same layout
@ -17,10 +16,16 @@ module.exports = {
'Q':'4', 'W':'5', 'E':'6', 'R':'D',
'A':'7', 'S':'8', 'D':'9', 'F':'E',
'Z':'A', 'X':'0', 'C':'B', 'V':'F',
// Include conventional arrow keys
'UP': '5',
'LEFT': '7',
'DOWN': '8',
'RIGHT': '9',
},
// max number of times to step the CPU,
// to stop endless loops
// 0 = infinite
"CYCLE_LIMIT": 256,
"defaultCycleLimit": 2048,
}

View File

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

210
javascript-old/readme.md Normal file
View File

@ -0,0 +1,210 @@
# Cardiograph Mark I — simulator for an imaginary computer
Cardiograph is an imaginary computer. It has three main components:
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
2. an input-output processor, *IO*
3. a display, *Graph*
## Simulator
### Dependencies
Cardiograph is an imaginary computer. It has three main components:
1. the CPU, *Card* (short for 'Completely Analogue Risc Machine')
2. an input-output processor, *IO*
3. a display, *Graph*
## Simulator
### Dependencies
- Node.js
### Quick examples
Assemble and run:
```./assembler.js -i <source.asm> | ./cardiograph.js```
Assemble to a file:
```./assembler.js -i <source.asm> -o <machinecode.out>```
Run from a file:
```./cardiograph.js -i <machinecode.out>```
### Assembler: assembler.js
```
Usage: ./assembler.js [-a] -i <input-file> [-o <output-file>]
-a, --annotate Output code with debugging annotations
-i, --in <file> Assembly-language input
-o, --out <file> Machine-code output
```
- If an output file is not provided, the output is printed to stdout
- If the `annotate` flag is not set, the machine code is returned as a string of space-separated decimal numbers
### Simulator: cardiograph.js
```
Usage: ./cardiograph.js [-i <file>]
-i, --in <file> Machine-code input
```
- If an input file is not provided, the input is read from stdin
## CPU
### Registers and Flags
There are three registers:
1. **A**, an 8-bit accumulator
2. **IP**, an 8-bit instruction pointer (aka program counter)
3. **flags**, a 4-bit flag register
The four flags are **O**verflow, **N**egative, **Z**ero, and **C**arry.
(Overflow is the high bit and carry is the low bit.)
In decimal:
| O | N | Z | C |
|---|---|---|---|
| 3 | 2 | 1 | 0 |
### Instruction set
#### Operations
```
Hex Mnem. Operand Effect
00 END (ignored) Halt CPU
01 STO literal # mem[lit#] = A
02 STO address mem[mem[addr]] = A
03 LDA literal # A = lit#
04 LDA address A = addr
05 ADD literal # A = A + lit#
06 ADD address A = A + mem[addr]
07 SUB literal # A = A - lit#
08 SUB address A = A - mem[addr]
09 HOP literal # If A == lit#, skip next op (IP += 4)
0A HOP address If A == mem[addr], skip next instruction (IP += 4)
0B JMP literal # IP = lit#
0C JMP address IP = mem[addr]
0D FTG literal # Toggle flag, where flag number == lit#
0E FHP literal # Skip next op if flag is set, where flag number == lit#
0F NOP (ignored) None
```
- Instructions are two bytes long:
one byte for the opcode, one for the operand
#### Effects on memory, flags, registers
```
op mem flags IP
END +2
NOP +2
STO w +2
LDA r NZ +2
ADD ONZC +2
SUB ONZC +2
HOP +2/+4
JMP arg
FTG ONZC +2
FHP ONZC +2/+4
STO r,w +2
LDA r,r NZ +2
ADD r ONZC +2
SUB r ONZC +2
HOP r +2/+4
JMP r arg
FTG r ONZC +2
FHP r ONZC +2/+4
```
### Start-up
When starting up, the CPU executes a `JMP $FF`.
Put differently: it starts executing instructions at the address contained in `$FF`.
<mark>TODO: currently the simulator doesn't actually do this</mark>
### Assembly language
ADD $01 ; comments follow a `;`
ADD $FF ; this is direct addressing
ADD ($CC) ; this is indirect addressing
END ; END and NOP don't require operands
; (the assembler will fill in a default value of 0)
@subroutine ; create a label
ADD $01 ; (it must be on the line before the code it names)
ADD $02
JMP @subroutine ; use a label as operand
; the label will be replaced with
; the address of the label
#foo $FF ; define a constant
; (must be defined before it is referenced)
ADD #foo ; use a constant as an operand
LDA * ; `*` is a special label referencing the memory address
; where the current line will be stored after assembly
- Prefix hexadecimal numbers with `$` (or `0x`)
- Prefix binary numbers with `0b`
- Whitespace is ignored
## Cardiograph memory map
| Address | Used for... |
|----------|-----------------------------------------------|
| 00 to 19 | display (5x5) |
| 1A | pointer to display memory |
| 1B | keypad: value of latest key pressed |
| 1C | reserved for future use (bank switching flag) |
| 1D | initial IP |
| 1D to FE | free |
| FF | * ROM (unwriteable) pointer to initial IP |
\* Not implemented yet
## Cardiograph peripherals
### Keypad
The value of the latest keypress on a hex keypad is stored at `$1B`.
The keypad uses the same layout as the COSMAC VIP (and CHIP-8). The CPU simulator maps those keys onto a Qwerty set:
`1` `2` `3` `C`   =   `1` `2` `3` `4`
`4` `5` `6` `D`   =   `Q` `W` `E` `R`
`7` `8` `9` `E`   =   `A` `S` `D` `F`
`A` `0` `B` `F`   =   `Z` `X` `C` `V`
The arrow keys are also mapped onto the hex keypad:
` ` `5` ` `   =   ` ` `↑` ` `
`7` `8` `9`   =   `←` `↓` `→`

View File

@ -0,0 +1,69 @@
; Routine for drawing at (x, y) coordinates
#zeroflag 1
; *** Set your desired (x, y) here: ***
#input_x 2
#input_y 2
; Set up some handy shortcuts
#x $FA
#y $FB
#return_addr_ptr $FE
; Main:
LDA #input_x
STO #x
LDA #input_y
STO #y
LDA * 6 ; acc = current address + 6 (LDA, STO, JMP = 6)
STO #return_addr_ptr
JMP @getxy
LDA $FF
STO ($FD)
END
;; Convert a pair of (x, y) coords
;; to the address of a pixel on the display
;;
;; Call with:
;; - x in #x
;; - y in #y
;; - return address in #return_addr_ptr
;;
;; Returns: pixel address in $FD
@getxy
#gxy_px $FD
; stash x...
LDA (#x)
STO #gxy_px
; check if this is row 0...
LDA (#y)
FHP #zeroflag
JMP @getxy_loop
JMP (#return_addr_ptr) ; if row 0, we're done
@getxy_loop
LDA (#gxy_px)
ADD 5 ; add 5 to get to the next row
STO #gxy_px
LDA (#y) ; decrement y (it's acting as a loop counter)...
SUB 1
STO #y
FHP #zeroflag
JMP @getxy_loop
JMP (#return_addr_ptr)
;; Main variables:
;; F8
;; F9
;; FA - x coord
;; FB - y coord
;; FC - gxy temp
;; FD - gxy temp
;; FE - Return address for subroutine

View File

@ -0,0 +1,26 @@
;; Fill display with $FF
; updated for 5x5 display
#Zero 1
#px_ptr $F0
#fill $F1
; Initialize variables...
LDA $00 ; (Address for the first px on the display)
STO #px_ptr ; Pointer to current px
LDA $FF ; ($FF is 'on', $00 is 'off')
STO #fill ; Stash value to fill with
@paint
LDA (#fill) ; (A = mem[fill] = $FF)
STO (#px_ptr); Paint pixel (mem[mem[*px]] = A = $FF)
LDA (#px_ptr) ; Increment pixel pointer...
ADD $01
STO #px_ptr
LDA (#px_ptr) ; Test whether to loop or not...
SUB $19 ; if *px - $19 == 0, we've reached the end
FHP #Zero
JMP @paint
END

View File

@ -0,0 +1,63 @@
;; Test behaviour of flags during addition and subtraction
;; with a focus on the Overflow flag
;; 2023-08-29
; http://teaching.idallen.com/dat2343/11w/notes/040_overflow.txt:
;
; > 1. If the sum of two numbers with the sign bits off yields a result number
; with the sign bit on, the "overflow" flag is turned on.
; >
; > 0100 + 0100 = 1000 (overflow flag is turned on)
; >
; > 2. If the sum of two numbers with the sign bits on yields a result number
; > with the sign bit off, the "overflow" flag is turned on.
; >
; > 1000 + 1000 = 0000 (overflow flag is turned on)
; >
; > Otherwise, the overflow flag is turned off.
; > * 0100 + 0001 = 0101 (overflow flag is turned off)
; > * 0110 + 1001 = 1111 (overflow flag is turned off)
; > * 1000 + 0001 = 1001 (overflow flag is turned off)
; > * 1100 + 1100 = 1000 (overflow flag is turned off)
;; Check simple addition and subtraction
; LDA 1
; STO 0
; LDA 1
; ADD 1
; LDA 1
; ADD (0)
; LDA 3
; SUB 1
; LDA 3
; SUB (0)
;; Check zero flag, negative flag
; LDA 0
; LDA 255
;; Check overflow flag
LDA 0b01000000
ADD 0b01000000 ; 10000000 ; Overflow flag is on
LDA 0b10000000
ADD 0b10000000 ; 00000000 ; Overflow flag is on
; > * 0100 + 0001 = 0101 (overflow flag is turned off)
; > * 0110 + 1001 = 1111 (overflow flag is turned off)
; > * 1000 + 0001 = 1001 (overflow flag is turned off)
; > * 1100 + 1100 = 1000 (overflow flag is turned off)
LDA 0b01000000
ADD 0b00010000 ; 01010000 ; overflow off
LDA 0b01100000
ADD 0b10010000 ; 11110000 ; overflow off
LDA 0b10000000
ADD 0b00010000 ; 10010000 ; overflow off
LDA 0b11000000
ADD 0b11000000 ; 10000000 ; overflow off

View File

@ -7,7 +7,7 @@
#LOOPCOUNT $80
#Z 2 ; the zero flag is #2
#Z 1 ; the zero flag is #1
#keypad $20 ; magic memory location containing latest key pressed
#loopIter $FF ; address of loop iterator
#iterPx $03 ; where to display iterator

View File

@ -0,0 +1,37 @@
;; Conway's Game of Life
; n loewen & Elizabeth Pankratz
; 2023-08-23 -
; Flag numbers for easier reference
#Carry 0
#Zero 1
#live_colour $FF
#dead_colour $00
#top_left $00
#top_right $04
#bot_left $14
#bot_right $18
#px_ptr $00
#live_neighbours_ptr $FF
; start of code
* $1D
@loop
LDA (#px_ptr)
; do something...
; increment pixel pointer
LDA (#px_ptr)
STA #px_ptr
JMP @loop
@check_for_tl_corner
LDA (#px_ptr)
; choose a memory location to stash result. 0=false, 1=true
HOP

View File

@ -0,0 +1,164 @@
; Draw a pixel, and move it when a key is pressed
; 2023-08-26
#flagZ 1
#flagN 2
#keypad $1B ; contains latest key pressed
; Starting (x, y) coordinates
#input_x 0
#input_y 0
; Some handy shortcuts
#x $FA
#y $FB
#px_addr $FD ; holds return value from @xy2id
#return_addr_ptr $FE
; Main variables:
; F8
; F9 - xy2id temp
; FA - x coord
; FB - y coord
; FC - xy2id temp
; FD - xy2id return value / xy2id temp
; FE - Return address for subroutine
@setup
LDA #input_x
STO #x
LDA #input_y
STO #y
LDA @update
STO #return_addr_ptr
JMP @xy2id
@update
; draw pixel
LDA $FF
STO (#px_addr)
; determine direction
#up 5
#left 7
#down 8
#right 9
; test up
lda (#keypad)
sub #up
ftg #flagZ
fhp #flagZ
jmp @up
; test left
lda (#keypad)
sub #left
ftg #flagZ
fhp #flagZ
jmp @left
; test right
lda (#keypad)
sub #right
ftg #flagZ
fhp #flagZ
jmp @right
; test down
lda (#keypad)
sub #down
ftg #flagZ
fhp #flagZ
jmp @down
;; no key pressed...
jmp @stay_put
@up
lda (#y)
sub 1
ftg #flagN
fhp #flagN
jmp @stay_put
sto #y
jmp @xy2id
@left
lda (#x)
sub 1
ftg #flagN
fhp #flagN
jmp @stay_put
sto #x
jmp @xy2id
@right
lda (#x)
sub 4
ftg #flagZ
fhp #flagZ
jmp @stay_put
lda (#x)
add 1
sto #x
jmp @xy2id
@down
lda (#y)
sub 4
ftg #flagZ
fhp #flagZ
jmp @stay_put
lda (#y)
add 1
sto #y
jmp @xy2id
@stay_put
; draw pixel
LDA $FF
STO (#px_addr)
; TODO
; END
;; Convert a pair of (x, y) coords
;; to the address of a pixel on the display
;;
;; Call with:
;; - x in #x
;; - y in #y
;; - return address in #return_addr_ptr
;;
;; Returns:
;; - pixel address in #px_addr
@xy2id
; stash x, y...
#xy2id_y $FC
#xy2id_x $F9
LDA (#y)
STO #xy2id_y
LDA (#x)
STO #xy2id_x
STO #px_addr
; check if this is row 0...
LDA (#xy2id_y)
FHP #flagZ
JMP @xy2id_loop
JMP (#return_addr_ptr) ; if row 0, we're done
@xy2id_loop
LDA (#px_addr)
ADD 5 ; add 5 to get to the next row
STO #px_addr
LDA (#xy2id_y) ; decrement y (it's acting as a loop counter) ...
SUB 1
STO #xy2id_y
FHP #flagZ
JMP @xy2id_loop
JMP (#return_addr_ptr)

View File

@ -1,9 +1,11 @@
;; Test referencing address of line being assembled
NOP ; Push the const below to a later address
#initAddr *ADDR
* 30
LDA *ADDR
NOP ; Push the const below to a later address
#initAddr *
LDA *
STO $25
FHP 0 ; hop if carry set
JMP @setCarry

View File

@ -1,83 +0,0 @@
/**
* Display a table of memory locations.
* Call with [start] and [end] indices to display a range.
* @param {Uint8Array} mem - Memory to display
* @param {number} [start] - A start-index, in decimal
* @param {number} [end] - An end-index, in decimal
**/
const logMemory = (mem, start=0, end=mem.length) => {
// This function can only handle
// an even number of array entries
if ((start % 2) === 1) { start -= 1; }
if ((end % 2) === 1) { end += 1; }
mem = mem.slice(start, end);
console.log(`┌────────┬────────┬─────────┐`);
console.log(`│ addr │ opcode │ operand │`);
console.log(`├────────┼────────┼─────────┤`);
//for (let i = 0; i < mem.length; i += 2) {
for (let i = start; i < mem.length; i +=2) {
console.log(`${num2hex(i)}${num2hex(mem[i])}${num2hex(mem[i+1])}`);
// Add a blank row every 4 lines:
if (((i + 2) % 8) === 0) {
if ((i < (mem.length - 2))) {
console.log(`│ │ │ │`);
}
}
}
console.log(`└────────┴────────┴─────────┘`);
}
const logRunningHeader = () => {
console.log();
let time = new Date();
console.log( `┌─────────────────────┐`);
console.log( `│ Running at ${time.toLocaleTimeString('en-GB')}` );
console.log( `└─────────────────────┘`);
}
/**
* @param {number} num
* @returns {string}
*/
const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0");
/**
* @param {string} hex
* @returns {number}
*/
const hex2num = (hex) => parseInt(hex, 16);
/**
* Convert a number to binary, padded to 8 bits
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
* @param {number} num
* @returns {string} binary representation of the input
**/
const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0");
/**
* Convert a number to binary, padded to 4 bits
* See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript
* @param {number} num
* @returns {string} binary representation of the input
**/
const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0");
/**
* @param {string} bin
* @returns {number}
*/
const bin2num = (bin) => parseInt(bin, 2)
module.exports = {
"logMemory": logMemory,
"logRunningHeader": logRunningHeader,
"num2hex": num2hex,
"hex2num": hex2num,
"num2bin": num2bin,
"num2bin_4bit": num2bin_4bit,
"bin2num": bin2num,
}

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

@ -1,72 +0,0 @@
# To do
## Research
- [ ] Learn how the C64's PC gets initialized (what is the initial value? how is it set?)
- [ ] Review CHIP-8
- read about the spec / ISA
- read these, and add them to the bibliography:
- Steve Losh: https://stevelosh.com/blog/2016/12/chip8-input/
- https://tonisagrista.com/blog/2021/chip8-spec/
## Documentation
- [ ] Improve docs for flags register
- [ ] Play with JSDoc
## Design
- [ ] Add a flag for bank-switching to the ~zero-page
- [ ] Move the initial IP value, to eliminate the gap between it and the keypad pointer
- [ ] Consider adding a VIP-style keypad-based machine code monitor
### For consideration
- [ ] Subroutine stack
- [ ] [Extended system (secret bonus operations)](2023-08-07--dev-notes.md)
## Programming
- [ ] Write some more complex test programs
- [ ] Display (hex) numbers
- [ ] Greater than
- [ ] Minimal LOGO-ish interpreter for turtle graphics
## Run-scripts
- [ ] Replace with running `./cpu.js` and `./assembler.js` diretly
## Simulator
### Assembler
- [ ] Validate labels
- [ ] Return pure machine code when printing to stdout (and not in debug mode)
### CPU
- [ ] Add a mode that prints the display as text, but still animates
- [ ] Make single-stepping work with simulated keypad
- [ ] Allow running pre-compiled machine code
I'm thinking of an interface like this...
$ ./cpu.js -mc code.bin
$ ./cpu.js code.asm
$ ./cpu.js --debug code.asm
Full list of flags I want:
-d --debug
-s --singlestep
-p --prettydisplay
-mc --machinecode
### Possible under-the-hood improvements
- [ ] Do a proper binary version... (or lose the typed arrays?)
- [ ] Extract debugging to its own module
- [ ] DRY out addition and subtraction

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)

130
readme.md
View File

@ -1,130 +0,0 @@
# Cardiograph Mark I — simulator for a paper computer
## Dependencies
- Node.js
- readline-sync
## Run
### Assemble
Hex output:
```./run-assembler run source_code.asm```
Binary output:
```./run-assembler runbin source_code.asm```
Verbose debugging output (hex):
```./run-assembler debug source_code.asm```
### Assemble and run
With animated display of screen memory:
```./run-cpu run source_code.asm```
With verbose debugging output:
```./run-cpu debug source_code.asm```
With single stepping + pretty-printed display:
```./run-cpu step source_code.asm```
With single stepping + verbose debugging output:
```./run-cpu stepdebug source_code.asm```
## Instruction set
00 END
01 STO lit# ; store ... mem[lit#] <- A
02 STO addr ; store ... mem[mem[addr]] <- A
03 LDA lit# ; load ... A <- lit#
04 LDA addr ; load ... A <- mem[addr]
05 ADD lit# ; add ... A <- A + lit# ... and un/set carry flag
06 ADD addr ; add ... A <- A + mem[addr] ... and un/set carry flag
07 SUB lit# ; sub ... A <- A - lit# ... and un/set carry flag
08 SUB addr ; sub ... A <- A - mem[addr] ... and un/set carry flag
09 HOP lit# ; hop ... skip next instruction if A == lit# ... when true: IP <- PC + 4
0A HOP addr ; hop ... skip next instruction if A == addr ... when true: IP <- PC + 4
0B JMP lit# ; jump ... IP <- lit#
0C JMP addr ; jump ... IP <- addr
0D FTG lit# ; toggle flag by number (see details below)
0E FHP lit# ; flag hop ... skip next instruction if flag is set ... when true: IP <- PC + 4
0F NOP ———— ; no operation
- Instructions are two bytes long:
one byte for the opcode, one for the operand
## Registers and Flags
- `A` - accumulator
- `IP` - instruction pointer (aka program counter)
- `FLAGS` - flags: **N**egative, **Z**ero, **O**verflow, **C**arry
- in machine language, each flag is given a number:
- N = 3
Z = 2
O = 1
C = 0
- (bitwise, `0000 = NZOC`)
## Memory map / Peripherals
- `00-0F` - display (4x4)
- `10-19` - reserved for future use
- `20 ` - keypad - value of the most recent keypress
- `21 ` - pointer to display memory
- `22 ` - pointer to keypad memory
- `23-2F` - reserved for future use / variable storage
- `30 ` - initial value for IP
- `30-FF` - free
### Keypad
The value of the latest keypress on a hex keypad is stored at `$20`.
(The keypad can also be relocated by changing the value of the pointer-to-keypad at `$22`.)
The keypad uses the same layout as the COSMAC VIP (and CHIP-8):
```
1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
```
The CPU simulator maps the following Qwerty keys onto those values:
```
1 2 3 4
Q W E R
A S D F
Z X C V
```
## Assembly language
ADD $01 ; comments follow a `;`
ADD $FF ; this is direct addressing
ADD ($CC) ; this is indirect addressing
END ; END and NOP don't require operands
; (the assembler will fill in a default value of 0)
@subroutine ; create a label
ADD $01 ; (it must be on the line before the code it names)
ADD $02
JMP @subroutine ; use a label as operand
; the label will be replaced with
; the address of the label
#foo $FF ; define a constant
; (must be defined before it is referenced)
ADD #foo ; use a constant as an operand
LDA *ADDR ; `*ADDR` is a magic value referencing the memory address
; that the current line will store at after assembly
- Hexadecimal numbers are preceded by a `$`
- Whitespace is ignored

View File

@ -1,32 +0,0 @@
#!/usr/bin/env node
// Run with hex output: `./run-assembler.js run assembly.asm`
// Run with binary output: `./run-assembler.js runbin assembly.asm`
// Debug: `./run-assembler.js debug assembly.asm`
const fs = require('fs');
const assembler = require('./assembler.js');
const { logMemory, num2hex, num2bin } = require('./logging.js');
const { machine } = require('os');
const mode = process.argv[2];
const filename = process.argv[3];
const inputFile_str = fs.readFileSync(filename, 'utf8');
let machineCode;
if (mode === "debug") {
machineCode = assembler.assemble(inputFile_str, true);
console.group("Machine code output");
logMemory(machineCode);
console.groupEnd();
} else {
machineCode = assembler.assemble(inputFile_str);
let output = '';
if (mode === 'runbin') { // print binary output
machineCode.forEach((n) => output = `${output} ${num2bin(n)}`);
} else { // print hex output
machineCode.forEach((n) => output = `${output} ${num2hex(n)}`);
}
console.log(output);
}

View File

@ -1,30 +0,0 @@
#!/usr/bin/env node
// Run: `./run-cpu.js run assembly.asm`
// Debug: `./run-cpu.js debug assembly.asm`
// Run with single-stepping: `./run-cpu.js step assembly.asm`
// Debug with single-stepping: `./run-cpu.js stepdebug assembly.asm`
const fs = require('fs');
const computer = require('./cpu.js');
const assembler = require('./assembler.js');
const { logRunningHeader, logMemory } = require('./logging.js');
const mode = process.argv[2];
// console.log(`Reading ${filename}`);
const filename = process.argv[3];
const inputFile_str = fs.readFileSync(filename, 'utf8');
let machineCode = assembler.assemble(inputFile_str);
if (mode === "debug") {
logRunningHeader();
computer.runProgram(machineCode, true);
} else if (mode === "stepdebug") {
logRunningHeader();
computer.singleStepProgram(machineCode, true);
} else if (mode === "step") {
computer.singleStepProgram(machineCode, false);
} else {
computer.runProgram(machineCode, false, 200);
}

View File

@ -1,300 +0,0 @@
// This is a sketch of a simulator for my paper computer,
// made for two purposes:
//
// (1) figuring out the basic structure for a simple simulator
// (2) testing some simple programs (hopefully)
//
// NOTA BENE: this simple version makes naive use of
// Javascript numbers for opcodes, and a Javascript array
// for the computer's "memory." This may cause some problems.
function CPU(mem) {
this.memory = mem;
this.running = false;
this.instructionPointer = 0;
this.carryFlag = 0;
this.acc = 0;
this.instructions = {
end: () => {
console.log('END');
this.running = false
},
store_lit: (lit) => {
console.log('STO lit#');
this.memory[lit] = this.acc;
log_table_with_title(this.memory, 'Current memory');
this.instructionPointer = this.instructionPointer += 1;
},
store_addr: (addr) => {
console.log('STO addr');
this.memory[this.memory[addr]] = this.acc;
log_table_with_title(this.memory, 'Memory');
this.instructionPointer = this.instructionPointer += 1;
},
load_lit: (lit) => {
console.log('LDA lit#');
this.acc = lit;
this.instructionPointer = this.instructionPointer += 1;
},
load_addr: (addr) => {
console.log('LDA addr');
console.log('mem at addr: ', this.memory[addr]);
this.acc = this.memory[addr];
this.instructionPointer = this.instructionPointer += 1;
},
add_lit: (lit) => {
console.log("ADD lit");
if ( (this.acc + lit) > 15 ) { this.carryFlag = 1; }
this.acc = ((this.acc + lit) % 15);
this.instructionPointer = this.instructionPointer += 1;
},
add_addr: (addr) => {
console.log("ADD addr");
if ( (this.acc + this.memory[addr]) > 15 ) { this.carryFlag = 1; }
this.acc = ((this.acc + this.memory[addr]) % 15);
this.instructionPointer = this.instructionPointer += 1;
},
sub_lit: (lit) => { // TODO: carry flag
console.log("SUB lit");
this.acc = this.acc - lit;
this.instructionPointer = this.instructionPointer += 1;
},
sub_addr: (addr) => { // TODO: carry flag
console.log("SUB addr");
this.acc = this.acc - this.memory[addr];
this.instructionPointer = this.instructionPointer += 1;
},
hop_lit: (lit) => {
console.log("HOP lit");
console.log(" ↳ Memory at IP+1:", this.memory[this.instructionPointer+1]);
if (this.acc === lit) {
this.instructionPointer += 2;
} else {
this.instructionPointer += 1;
}
},
hop_addr: (addr) => {
console.log("HOP addr");
if (this.acc === this.memory[addr]) {
this.instructionPointer += 2;
} else {
this.instructionPointer += 1;
}
},
jump_lit: (lit) => {
console.log("JMP lit");
this.instructionPointer = lit;
},
jump_addr: (addr) => {
console.log("JMP addr");
this.instructionPointer = this.memory[addr];
},
carry_clear: () => {
console.log("CFC");
this.carryFlag = 0;
this.instructionPointer += 1;
},
carry_hop: () => {
console.log("CHP");
if (this.carryFlag != 0) {
this.instructionPointer += 2;
} else {
this.instructionPointer += 1;
}
},
};
this.perform_operation = (opcode, arg) => {
switch (opcode) {
case 0:
this.instructions.end(arg);
break;
case 1:
this.instructions.store_lit(arg);
break;
case 2:
this.instructions.store_addr(arg);
break;
case 3:
this.instructions.load_lit(arg);
break;
case 4:
this.instructions.load_addr(arg);
break;
case 5:
this.instructions.add_lit(arg);
break;
case 6:
this.instructions.add_addr(arg);
break;
case 7:
this.instructions.sub_lit(arg);
break;
case 8:
this.instructions.sub_addr(arg);
break;
case 9:
this.instructions.hop_lit(arg);
break;
case 10:
this.instructions.hop_addr(arg);
break;
case 11:
this.instructions.jump_lit(arg);
break;
case 12:
this.instructions.jump_addr(arg);
break;
case 13:
this.instructions.carry_clear(arg);
break;
case 14:
this.instructions.carry_hop(arg);
break;
default:
console.error( `Invalid opcode: ${opcode} with argument ${arg}` );
}
}
this.run_program = () => {
const initialMemory = JSON.parse(JSON.stringify(this.memory)); // Hack to make a copy-by-value -- https://stackoverflow.com/questions/18829099/copy-a-variables-value-into-another
console.log();
console.log("————————————————————————————————————————");
let time = new Date();
console.log( `Running at ${time.toLocaleTimeString('en-US')}` );
console.log("————————————————————————————————————————");
log_debug_state();
this.running = true;
for (let i = 1; i < 16; i++) {
if ( this.running &&
(this.instructionPointer < this.memory.length) ) {
let op_arg_tuple = this.memory[this.instructionPointer];
console.group("Proccessing instruction");
console.log( op_arg_tuple );
// console.log( `processing opcode ${op_arg_tuple[0]} with arg ${op_arg_tuple[1]}` );
this.perform_operation(op_arg_tuple[0], op_arg_tuple[1]);
log_debug_state();
console.groupEnd("Processing instruction");
}
}
return {
memoryAtStart: initialMemory,
memoryAtEnd: this.memory
}
}
log_debug_state = () => {
console.log();
console.group('CPU state');
console.log( `Acc: ${this.acc} IP: ${this.instructionPointer} CF: ${this.carryFlag}  ${this.running ? "running" : "halted" }` );
console.log();
console.groupEnd('CPU state');
};
};
log_table_with_title = (memory, tableTitle) => {
console.log();
console.group(tableTitle);
console.table(memory);
console.groupEnd(tableTitle);
};
// TESTS
let halt_and_catch_fire = [
[0, 0],
[1, 0],
];
let test_lda_sto = [
[3, 8], // LDA lit
[1, 5], // STO lit
[4, 5], // LDA addr
[2, 6], // STO addr
[0, 0], // END
];
let test_add_sub_nocarry = [
[5, 6], // ADD lit ... acc = 6
[7, 1], // SUB lit ... acc = 5
[1, 8], // STO lit ... mem[8] = 5
[6, 8], // ADD addr ... acc = 10
[8, 8], // SUB addr ... acc = 5
[0, 0], // END
]
let test_add_sub = [
[5, 26], // ADD lit
[0, 0], // END
]
let test_hop = [
[5, 8], // ADD lit ... acc = 8
[9, 8], // HOP lit ... hop over next op if acc = 8
[0, 0], // END ... (hopped over)
[7, 8], // SUB lit ... acc = 0
[0, 0]
]
let test_jmp = [
[11, 4], // 0
[0, 0], // 1 ... END ... JMP'd over
[0, 0], // 2
[0, 0], // 3
[5, 8], // 4 ... ADD lit ... acc = 8
[0, 0], // 5 ... END
]
let test_chp = [
[5, 8], // ADD lit ... acc = 8
[14, 0], // CHP ... shouldn't hop (CF = 0)
[5, 1], // ADD lit ... acc = 9
[5, 8], // ADD lit ... acc = 1 and CF = 1
[14, 0], // CHP ... hop! (CF = 1)
[0, 0], // END
[7, 1], // SUB lit ... acc = 0
[13, 0], // CFC ... CF = 0
[0, 0], // END
]
//let comp = new CPU(test_chp);
let comp = new CPU(test_lda_sto);
let memory_snapshots = comp.run_program();
log_table_with_title(memory_snapshots.memoryAtEnd, 'Memory after running');
log_table_with_title(memory_snapshots.memoryAtStart, 'Memory before running');
// TODO: TEST HOP_addr

View File

@ -1,274 +0,0 @@
// NOTES:
//
// - instructions are two bytes long:
// one byte for the opcode, one for the argument
// STATE
const CPU = {
running: false,
IP: 0,
CF: 0,
Acc: 0,
memory: null,
loadMemory: (data) => { // data: Uint8Array
// TODO: check length of data
CPU.memory = data;
},
}
// FUNCTIONS THAT MODIFY STATE
const Instructions = {
end: () => {
console.log('END');
CPU.running = false;
},
store_lit: (lit) => {
console.log('STO lit#');
CPU.memory[lit] = CPU.Acc;
logTableTitled(CPU.memory, 'Current memory');
CPU.IP = CPU.IP += 2;
},
store_addr: (addr) => {
console.log('STO addr');
CPU.memory[CPU.memory[addr]] = CPU.Acc;
logTableTitled(CPU.memory, 'Memory');
CPU.IP = CPU.IP += 2;
},
load_lit: (lit) => {
console.log('LDA lit#');
CPU.Acc = lit;
CPU.IP = CPU.IP += 2;
},
load_addr: (addr) => {
console.log('LDA addr');
console.log('mem at addr: ', CPU.memory[addr]);
CPU.Acc = CPU.memory[addr];
CPU.IP = CPU.IP += 2;
},
add_lit: (lit) => {
console.log("ADD lit");
let sum = CPU.Acc + lit;
if (sum > 15) {
CPU.CF = 1;
CPU.Acc = (sum % 15) - 1;
} else {
CPU.CF = 0;
CPU.Acc = sum;
}
CPU.IP = CPU.IP += 2;
},
add_addr: (addr) => {
console.log("ADD addr");
let sum = CPU.Acc + CPU.memory[addr];
if (sum > 15) {
CPU.CF = 1;
CPU.Acc = (sum % 15) - 1;
} else {
CPU.CF = 0;
CPU.Acc = sum;
}
CPU.IP = CPU.IP += 2;
},
sub_lit: (lit) => { // TODO: carry flag
console.log("SUB lit");
CPU.Acc = CPU.Acc - lit;
CPU.IP = CPU.IP += 2;
},
sub_addr: (addr) => { // TODO: carry flag
console.log("SUB addr");
CPU.Acc = CPU.Acc - CPU.memory[addr];
CPU.IP = CPU.IP += 2;
},
hop_lit: (lit) => {
console.log("HOP lit");
console.log(` ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`);
if (CPU.Acc === lit) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
hop_addr: (addr) => {
console.log("HOP addr");
if (CPU.Acc === CPU.memory[addr]) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
jump_lit: (lit) => {
console.log("JMP lit");
CPU.IP = lit;
},
jump_addr: (addr) => {
console.log("JMP addr");
CPU.IP = CPU.memory[addr];
},
carry_clear: () => {
console.log("CFC");
CPU.CF = 0;
CPU.IP += 2;
},
carry_hop: () => {
console.log("CHP");
console.log(` ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`);
console.table(CPU.memory);
if (CPU.CF != 0) {
CPU.IP += 4;
} else {
CPU.IP += 2;
}
},
}
const opcodes2mnemonics = {
0: (arg) => Instructions.end(arg),
1: (arg) => Instructions.store_lit(arg),
2: (arg) => Instructions.store_addr(arg),
3: (arg) => Instructions.load_lit(arg),
4: (arg) => Instructions.load_addr(arg),
5: (arg) => Instructions.add_lit(arg),
6: (arg) => Instructions.add_addr(arg),
7: (arg) => Instructions.sub_lit(arg),
8: (arg) => Instructions.sub_addr(arg),
9: (arg) => Instructions.hop_lit(arg),
10: (arg) => Instructions.hop_addr(arg),
11: (arg) => Instructions.jump_lit(arg),
12: (arg) => Instructions.jump_addr(arg),
13: (arg) => Instructions.carry_clear(arg),
14: (arg) => Instructions.carry_hop(arg),
};
function stepCPU() {
console.group("Step CPU");
let opcode = CPU.memory[CPU.IP];
let argument = CPU.memory[CPU.IP+1];
console.log(`OP: ${opcode} ARG: ${argument}`);
let instruction = opcodes2mnemonics[opcode];
instruction(argument);
logCPUState();
console.groupEnd("Step CPU");
}
function runCPU() {
const initialMemory = JSON.parse(JSON.stringify(CPU.memory)); // Hack to make a copy-by-value -- https://stackoverflow.com/questions/18829099/copy-a-variables-value-into-another
console.log();
console.log("————————————————————————————————————————");
let time = new Date();
console.log( `Running at ${time.toLocaleTimeString('en-US')}` );
console.log("————————————————————————————————————————");
logCPUState();
CPU.running = true;
for (let i = 0; i < 255; i++) { // FIXME: temporary limit as a lazy way to halt infinite loops
if (!CPU.running) break;
if (CPU.IP >= CPU.memory.length) break;
stepCPU();
};
}
// FUNCTIONS THAT PULL INFO FROM STATE TO DISPLAY
function logCPUState() {
console.log();
console.group('CPU state');
console.log( `Acc: ${CPU.Acc} IP: ${CPU.IP} CF: ${CPU.CF}  ${CPU.running ? "running" : "halted" }` );
console.log();
console.groupEnd('CPU state');
};
// FUNCTIONS FOR DISPLAYING DATA
function num2hex(num) { return num.toString(16) };
function hex2num(hex) { return parseInt(hex, 16) };
logTableTitled = (memory, tableTitle) => {
console.log();
console.group(tableTitle);
console.table(memory);
console.groupEnd(tableTitle);
};
// RUN IT !
const test_lda_sto = new Uint8Array([
3, 17, // LDA lit ... Acc = 17
1, 14, // STO lit ... @14 = Acc = 17
3, 16, // LDA lit ... Acc = 16
1, 15, // STO lit ... @15 = Acc = 16
2, 15, // STO addr ... @[@15] = Acc = 16 ... mem[mem[addr]] <- Acc
4, 0, // LDA addr ... Acc = @00 = 03
0, 0, // END
0, 0, // DATA
]);
let test_add_sub_nocarry = new Uint8Array([
5, 6, // ADD lit ... acc = 6
7, 1, // SUB lit ... acc = 5
1, 15, // STO lit ... mem[15] = 5
6, 15, // ADD addr ... acc = 10
8, 15, // SUB addr ... acc = 5
0, 0, // END
0, 0,
0, 0,
]);
let test_hop = new Uint8Array([
5, 8, // ADD lit ... acc = 8
9, 8, // HOP lit ... hop over next op if acc = 8
0, 0, // END ... (hopped over)
7, 8, // SUB lit ... acc = 0
0, 0
]);
let test_jmp = new Uint8Array([
11, 8, // 0 ... JMP lit
0, 0, // 2 ... END ... JMP'd over
0, 0, // 4
0, 0, // 6
5, 8, // 8 ... ADD lit ... acc = 8
0, 0, // 10 ... END
]);
const test_chp = new Uint8Array([
5, 8, // ADD lit ... Acc = 8
14, 0, // CHP ... shouldn't hop (CF = 0)
5, 1, // ADD lit ... Acc = 9
5, 8, // ADD lit ... Acc = 1 and CF = 1
14, 0, // CHP ... hop! (CF = 1)
0, 0, // END
7, 1, // SUB lit ... Acc = 0
13, 0, // CFC ... CF = 0
0, 0, // END
]);
//CPU.loadMemory(test_lda_sto);
//CPU.loadMemory(test_add_sub_nocarry);
//CPU.loadMemory(test_hop);
CPU.loadMemory(test_jmp);
//CPU.loadMemory(test_chp);
runCPU();

View File

@ -1,20 +0,0 @@
const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
if (process.stdin.setRawMode != null) {
process.stdin.setRawMode(true);
}
process.stdin.on('keypress', (str, key) => {
console.log(str)
console.log(key)
if (key.sequence === '\x03') process.exit();
})
let i = 0;
const loop = setInterval(async () => {
console.log('loop #', i);
if (i > 10) clearInterval(loop);
i += 1;
}, 250);

View File

@ -1,23 +0,0 @@
const readlineSync = require('readline-sync');
let key = readlineSync.keyIn('? ')
console.log(key);
/* This works without external dependencies,
* but it's for a full line at a time
const readline = require('readline/promises');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function getInput(prompt) {
let input = await rl.question(prompt)
console.log(input);
console.log("Later");
rl.close();
}
getInput('?');
*/

View File

@ -1,23 +0,0 @@
;; Fill display with $FF
LDA $00 ; Start of display
STO $21 ; Pixel index
; store the $FF that we'll use to fill the screen
LDA $FF
STO $22
@copy-to-display
LDA ($22) ; A = mem[$22] = $FF
STO ($21) ; mem[mem[$21]] = A = $FF
; increment pixel index
LDA ($21)
ADD $01
STO $21
; if CF is set, then the display is full and we're done
FHP 0
JMP @copy-to-display
END