PTC-701 R1.7 / McDonalds
Information
The PTC-701 system ROM contains a stack-based bytecode interpreter that executes application programs stored in the banked app ROM. For general hardware information, see the General Information section.
The interpreter dispatch loop is copied from ROM (0x0500) into RAM at 0x6100 for execution. It uses a 16-bit evaluation stack (pointed to by RD, located at 0x61FF) and variables stored in RAM at 0x6000+.
Opcodes are single bytes. Bit 7 selects between two dispatch methods:
- Low opcodes (0x00–0x7F): Handler address looked up from a 128-entry table at ROM 0x0100–0x01FF
- High opcodes (0x80–0xFF): Handler address partially encoded inline in the bytecode stream
Each handler's first byte is an entry-point selector loaded into R3, allowing multiple opcodes to share handler code with different setup paths. This compact design fits a complete virtual machine (VM) in ~1.5K of native code.
Architecture
| CPU | CDP1802 @ 2.4576 MHz |
| System ROM | 16K at 0x0000–0x3FFF |
| App ROM | 32K, bank-switched at 0x4000–0x5FFF |
| RAM | 8K at 0x6000–0x7FFF |
| Interpreter loop | RAM at 0x6100 (copied from ROM 0x0500) |
| Dispatch table | ROM at 0x0100–0x01FF (128 entries × 2 bytes) |
| Data stack | RAM 0x61FF (RD pointer, grows downward, 16-bit values) |
| Variables | RAM 0x6000+ (indexed by bytecode operand) |
| Return stack | RAM 0x625F (R2 pointer, grows downward, saves/restores RC) |
| Bytecode PC | Register RC (walks through banked app ROM) |
Register Usage
| R0 | Inline dispatch via address 0x00FE |
| R1 | Interrupt register 0x0D00 |
| R2 | Return stack (0x625F, saves/restores RC for CALL/RETURN) |
| R3 | Handler entry point selector |
| R9 | Data pointer (X register target in some handlers) |
| RB | Handler address / temporary pointer |
| RC | Bytecode program counter |
| RD | Data stack pointer (RD.1 = 0x61, grows downward) |
| RE | Dispatch table index (RE.1 = 0x01) |
| RF | Interpreter PC |
Definitions
| kk | 8-bit constant / variable index / mask value |
| bb | 8-bit signed branch offset |
| aaaa | 16-bit absolute RAM address |
| [aaaa] | Value at RAM address aaaa (16-bit) |
| mmmm | Second 16-bit RAM address (used when an instruction takes two inline addresses) |
| TOS | Top of data stack (16-bit value at M(RD)/M(RD+1)) |
| NOS | Next on stack (16-bit value below TOS) |
| 3OS | Third on stack (16-bit value below NOS) |
| [RP] | ROM Pointer — indirect read from app ROM via banked pointer at M(60CA/60CB). Auto-advances after read. |
| VA | Fixed 16-bit variable at M(6056/6057) |
| VB | Fixed 16-bit variable at M(6058/6059) |
| VC | Fixed 16-bit variable at M(605A/605B) |
| VD | Fixed 16-bit variable at M(605C/605D) |
| VE | Fixed 16-bit variable at M(605E/605F) |
| 0..9, A..F | Hexadecimal digits |
Note: All binary operations (TOS = NOS op TOS) pop both operands and push the result unless otherwise noted.
Note: All byte loads are zero-extended to 16-bit before pushing to the data stack.
Bank Address Encoding
The PTC-701 uses a 3-byte encoded address format for all banked ROM/RAM access. Opcodes LDBNK (0x39), STBNK (0x5A), LDBNK (0x5B), and SETBK (0x6B) all decode addresses using this scheme. Byte 0 bit 7 selects between two memory windows:
Window 1 (byte 0 bit 7 = 1) - bank register 0x3FA2, address range 0x4000–0x5FFF:
bank = ((byte0 << 3) | 0x80) + (byte1 >> 5) addr_high = (byte1 & 0x1F) + 0x40 addr_low = byte2
Writes bank value to M(604F) and hardware register M(3FA2).
Window 2 (byte 0 bit 7 = 0) - bank registers 0x3FC0–0x3FC3, address range 0x8000–0xFFFF:
raw = (byte0 << 1) | (byte1 >> 7) M(604E) = raw − 1 bank_reg[n] = M(604E) × 4 + n (n = 0..3, written to 0x3FC0–0x3FC3) read_addr = ((byte1 | 0x80) << 8) | byte2
Only writes bank registers when M(604E) changes from its previous value.
On exit, all bank operations restore the home bank from M(60C9).
Handler Dispatch
Dispatch Table at ROM 0x0100
The handler address table occupies ROM 0x0100–0x01FF and contains 128 entries (one per low opcode 0x00–0x7F). Each entry is 2 bytes stored as high byte first, low byte second at address 0x0100 + (opcode × 2).
The interpreter dispatch loop uses register RE (with RE.1 pre-set to 0x01) as a pointer into this table. After fetching an opcode and shifting left (SHL), the result is placed in RE.0, giving the table offset directly. Two reads via RE retrieve the 16-bit handler address into RB:
Both low opcodes (0x00–0x7F) and high opcodes (0x80–0xFF / SYS) use the same dispatch chain. The SYS path computes the handler address differently (from the opcode itself and the following byte) but then follows the identical pattern: LDA RB reads the first byte at the handler as an entry-point selector, loads it into R3.0, and executes via SEP R3. SYS handlers therefore use the same entry-point mechanism as low opcodes.
; From the dispatch table lookup at ROM 0x0504: ; (this routine is copied to RAM 0x6100 during startup) PLO RE ; RE.0 = (opcode << 1) - table offset LDA RE ; D = M(0x01xx) = handler address high byte, RE++ PHI RB ; RB.1 = handler high LDN RE ; D = M(0x01xx+1) = handler address low byte PLO RB ; RB.0 = handler low -> RB = full handler address
Unused opcode slots contain 0x0000 or 0xFFFF. Valid entries point to handler code in the system ROM (typically pages 0x05–0x21). The first byte at each handler address is not executed as an instruction - it is consumed by the dispatch loop as the entry-point selector for R3.
Example: opcode 0x00 (ADD) → table offset = 0x00 × 2 = 0x00 → reads ROM at 0x0100/0x0101 = 0x05/0x6B → handler at 0x056B. First byte there is 0x06 (entry-point selector), so handler body starts at 0x056C.
Dispatch Chain Notation
The Dispatch column shows the execution chain from the handler address to the actual code. The first byte at each handler address is an entry-point selector - it is loaded into R3.0 and causes a jump to address 0x02xx. Some entry points perform a second dispatch by reading the next handler byte, creating a two-level chain shown as XX·YY→ZZZZ.
black – traced orange – partly traced goldenrod – disassembled red – guess
Entry-Point Routines (page 0x02)
Reached via three mechanisms: 1st = first-byte dispatch (entry selector at handler address), 2nd = second-stage via entry A4 or 03 (LDA RB→PLO R3), R0 = SEP R0 trampoline (0x00FE→LDA RB→PLO R3→SEP R3).
| Byte | Address | Via | Function |
| 00 | 0200 | R0 | SEX R6 trampoline: sets X=R6, reads next handler byte as second dispatch (LDA RB→PLO R3). Used by entry 0x94 to make 0x0285 read from M(R6) instead of M(RC). |
| 03 | 0203 | 1st | MUL setup: SEX RB, then two-level dispatch via LDA RB→PLO R3 to second-stage routine 0xB7 to pop operands. Used by MUL (0x06). |
| 06 | 0206 | 1st | Pop TOS into R9, set X=RD, return to handler body |
| 0B | 020B | 1st | Direct execute: immediately returns to handler body (SEP RB). No parameter or stack setup. |
| 0C | 020C | 1st | Add inline constant to TOS: reads 2-byte constant (low, high) from handler body via LDA RB, adds to TOS via ADD/ADC. Used by opcodes 0x03 (+1), 0x04 (+2), 0x27 (-1). |
| 15 | 0215 | R0 | CALL via computed address: push RC to return stack (R2), set RC = RB (handler address) |
| 20 | 0220 | 1st, R0 | Push constant from handler body: reads 2 bytes via LDA RB (high, low), pushes as 16-bit value onto data stack. Used by PUSH 0000–FFFF (opcodes 0x72–0x76). |
| 28 | 0228 | 1st | Immediate return: SEP RF. Returns to interpreter loop without any action. Used by NOOP (0x7F). |
| 29 | 0229 | 1st, R0 | Push zero: pushes 16-bit 0x0000 onto data stack (uses GHI R0 where R0.1=0x00) |
| 2B | 022B | 1st, R0 | Push one (0x0001): GHI RE (=0x01); DEC RD; STR RD; GHI R0 (=0x00); DEC RD; STR RD; SEP RB. Used by LOOP (0x0C) as increment=1, and by ISLT (0x4D) as true result. |
| 32 | 0232 | 1st, R0 | Indirect call via data stack: pop address from TOS, DEC to get entry byte, read entry selector, dispatch to handler at that address |
| 39 | 0239 | R0 | Byte copy loop: DEC R8; LDA RA; STR R9; INC R9; GLO R8; BNZ (loop). Copies R8.0 bytes from M(RA) to M(R9). Used by MCPY (0x1A) and MCPY3 (0x38). |
| 3D | 023D | R0 | Byte copy loop (mid-entry): GLO R8; BNZ → LDA RA; STR R9; INC R9; DEC R8; GLO R8; BNZ (loop). Enters the 0x39 copy loop at the count-check, skipping the initial DEC R8. Used by MCOPY (0x1A) for count.high passes, STBNK (0x5A), and LDBNK (0x5B). |
| 50 | 0250 | R0 | Fill loop: GLO R8; BNZ; DEC R8; GLO RA; STR R9; INC R9; repeat until R8.0==0. Fills R8.0 bytes at M(R9) with RA.0. Used by FILL (0x46) via secondary dispatch from SEP R0→0x00FE→LDA RB(=0x50)→PLO R3→SEP R3. |
| 54 | 0254 | R0 | Store RA to var A: LDI 56; PLO R6 (R6=0x6056); SEX R6; GLO RA; STXD; GHI RA; STXD; SEP RB. Stores 16-bit RA to var A (0x6056/57). Variant of 0x59 but uses X=R6 instead of X=RD. |
| 59 | 0259 | R0 | Push RA to data stack: SEX RD; GLO RA; STXD; GHI RA; STXD; SEP RB. Used by ISLT (0x4D) to re-push NOS, and by LDVCD (0x37, when X=R6 stores to VC). |
| 5A | 025A | R0 | Store RA via STXD×2 (no SEX): GLO RA; STXD; GHI RA; STXD; SEP RB. Entry within 0x59 body, skips SEX RD. Target depends on current X register. Used by LDVCD (0x37, X=R6→stores to VC). |
| 5F | 025F | R0 | Store R9 via STXD×2 (no SEX): GLO R9; STXD; GHI R9; STXD; SEP RB. Entry within 0x60 body, skips SEX RD. Target depends on current X register. |
| 60 | 0260 | R0 | Push R9 to data stack: SEX RD; GLO R9; STXD; GHI R9; STXD; SEP RB. Used by ISLT (0x4D) to re-push TOS. |
| 66 | 0266 | 1st, R0 | Pop TOS→R9: SEX RD, pops 16-bit TOS into R9 (high then low via LDXA×2), returns to handler body. Used by ROMCPY, DISP. |
| 67 | 0267 | R0 | Pop TOS→R9 (no SEX): LDXA→PHI R9; LDXA→PLO R9; SEP RB. Entry within 0x66 body, skips SEX RD. Target depends on current X register. |
| 6C | 026C | 1st, R0 | Pop TOS into R8: SEX RD; LDXA→PHI R8; LDXA→PLO R8; SEP RB. Used by TOBCD (0x48), FMBCD (0x49), STBNK (0x5A), LDBNK (0x5B). |
| 6D | 026D | R0 | Pop TOS into R8 (no SEX): LDXA→PHI R8; LDXA→PLO R8; SEP RB. Entry within 0x6C body, skips SEX RD. Target depends on current X register. |
| 72 | 0272 | 1st | Load R9 from M(6051/52) and RA from M(6053/54) (ROMCPY post-state: dest ptr, src ptr), return to handler body |
| 78 | 0278 | 1st | Pop 3 values: TOS→R8, NOS→R9, 3OS→RA (with NBR padding between pairs); sets X=RD; returns to handler body (FILL, NEG, LOAD, COMP) |
| 7E | 027E | 1st, R0 | Pop TOS→R9, NOS→RA: SEX RD; then falls into 0x7F body (LDXA→PHI R9; LDXA→PLO R9; NBR; LDXA→PHI RA; LDXA→PLO RA; NBR; SEP RB). Reads from data stack (X=RD). Used by ADDM (0x05), SWAP (0x20), ISLT (0x4D), SUB24 (0x6E). |
| 7F | 027F | 2nd | Read two 16-bit values into R9 and RA (no SEX): LDXA→PHI R9; LDXA→PLO R9; NBR; LDXA→PHI RA; LDXA→PLO RA; NBR; SEP RB. Entry within 0x7E body, skips SEX RD. Source depends on current X register. Used by MCPY3 (0x58) and ADD24 (0x59) inline-address variants (X=RC via entry 0xA4). |
| 84 | 0284 | 1st, R0 | Pop TOS into RA: SEX RD; then falls into 0x85 body (LDXA→PHI RA; LDXA→PLO RA; NBR; SEP RB). Reads from data stack (X=RD). Used by LD TOS,[TOS], LD [TOS],NOS, CLRB, SETB, etc. |
| 85 | 0285 | 2nd, R0 | Read 16-bit into RA (no SEX): LDXA→PHI RA; LDXA→PLO RA; NBR; SEP RB. Entry within 0x84 body, skips SEX RD. Source depends on current X register. As second-stage routine (via A4·85, X=RC): reads 2 bytes from bytecode stream into RA. |
| 8A | 028A | 1st, R0 | SEX RD: set X=RD (data stack), return to handler body. TOS at M(RD). |
| 8B | 028B | 1st | Direct execute: immediately returns to handler body (SEP RB). No parameter or stack setup. |
| 8C | 028C | 1st | OVER setup: INC RD×2 (skip TOS), reads NOS into R9.1 (high) and D (low), DEC RD×2 (back to original), SEP RB to handler body which pushes copy as new TOS. |
| 8E | 028E | 1st | Read TOS into R9, move RD down by 2, return to handler body. Used by DUP (0x1E). |
| 94 | 0294 | 1st | Variable indirect: SEX RB, reads R6.0 from handler body, second dispatch via 0x00 then third via next byte. Used by STRCP, NUMCP, NUMCZ, PUSH VE, PUSH [VE].0, PUSH [VE], POP [VE]. |
| 99 | 0299 | R0 | Add M(RA) to RA (16-bit with carry): SEX RA; GLO RA; ADD; PLO RA; GHI RA; ADCI 00; PHI RA; SEP RB. Used by ADD TOS,[TOS] (0x35). |
| 9A | 029A | R0 | Add M(X) to RA (16-bit): GLO RA; ADD; PLO RA; GHI RA; ADCI 00; PHI RA; SEP RB. Entry within 0x99 body (skips SEX RA). Adds value at current X to RA with carry. Used by FMHEX (0x4B), TOHEX (0x4C). |
| A1 | 02A1 | R0 | Dual-purpose entry. First call (via SEP R0→PLO R3→SEP R3): lands at 0x02A1 = SEP RB, returns immediately to handler body (sets R3.0=A1 for later). Subsequent calls (via SEP R3 from handler loop): R3 resumes at 0x02A2 = BR 9A → jumps to 0x029A (GLO RA; ADD; PLO RA; GHI RA; ADCI 00; PHI RA; SEP RB). Adds M(X) to RA with carry, returns. Used by CKSUM (0x47): first call sets up R3, then loop calls SEP R3 repeatedly for the add step (SEX RA set once before loop). |
| A4 | 02A4 | 1st | SEX RC trampoline: sets X=RC (bytecode stream), reads next handler byte as second dispatch |
| A7 | 02A7 | 2nd, R0 | Read 16-bit from M(60xx) and push to data stack: LDXA→PLO R6 (R6=60xx); LDA R6→PLO RE (high byte); DEC RD; LDN R6→STR RD (push low); DEC RD; GLO RE→STR RD (push high); SEP RB. Used by LD TOS,[60bb] and MUL result retrieval. |
| B2 | 02B2 | 2nd | Read single byte from M(60xx), zero-extend to 16-bit, push to data stack: LDXA→PLO R6 (R6=60xx); LDN R6→D (byte value); GHI R0→D=0x00; BR 02AA; PLO RE (RE.0=0x00); DEC RD; LDN R6→STR RD (push low byte); DEC RD; GLO RE→STR RD (push high=0x00); SEP RB. Used by LD TOS.0,[60bb] (opcode 0x40). |
| B7 | 02B7 | 2nd, R0 | Pop 16-bit from data stack and store to M(60xx): LDXA→PLO R6 (R6=60xx); LDA RD→STR R6 (high byte to M(60xx)); INC R6; LDA RD→STR R6 (low byte to M(60xx+1)); SEP RB. Used by LD [60bb],TOS and MUL operand setup. |
| BF | 02BF | 2nd | Pop TOS and store low byte only to M(60xx): LDXA→PLO R6 (R6=60xx); INC RD (skip TOS high); BR 02BC; LDA RD→D (read TOS low, RD advances past); STR R6 (store to M(60xx)); SEP RB. Used by LD [60bb],TOS.0 (opcode 0x41). |
| CA | 02CA | 1st, R0 | Handler body setup: reads 2 bytes from handler body (LDA RB) into R6.0 and R7.0, then SEP RB. Used by LCD, DELAY. |
| CF | 02CF | R0 | Set bits in HW register: SEX R6; LDA RB (mask); OR; STR R6; STR R7; SEP RB. ORs mask byte from handler body with M(R6) (RAM shadow), stores result to both M(R6) and M(R7) (HW register). |
| D5 | 02D5 | R0 | Clear bits in HW register: SEX R6; LDA RB (mask); XRI FF; AND; STR R6; STR R7; SEP RB. Inverts mask, ANDs with M(R6) (RAM shadow), stores result to both M(R6) and M(R7) (HW register). |
| E8 | 02E8 | R0 | Jump to 0x03A7: ROM data field decoder, processes bytes at M(RA), extracts bit fields, returns via SEP RB |
| EB | 02EB | R0 | Jump to 0x03DA: banked address decoder (see Bank Address Encoding). Decodes 3-byte address at M(RA) into physical address and bank select value. Returns via SEP RB. |
| EE | 02EE | R0? | Jump to 0x046B: serial handshake routine (TBC). Tests EF1/EF3/Q, manages serial state. Returns via SEP RB. |
| F1 | 02F1 | R0? | Jump to 0x04AE: serial handshake routine (TBC). Tests EF1/EF2/Q, manages serial state. Returns via SEP RB. |
| F4 | 02F4 | R0 | Jump to 0x0451: bank register update. Computes bank values from M(604E) and writes to 0x3FC0–0x3FC3 (window 2 bank select registers). Returns via SEP RB. |
| F7 | 02F7 | R0 | Jump to 0x042C: page advance. If RA.1 < 0x60 (ROM window 1): increment bank at M(604F), write to M(3FA2), reset RA.1 = 0x40. If RA.1 == 0x00 (ROM window 2): increment M(604E), update bank regs 0x3FC0–0x3FC3, reset RA.1 = 0x80. Returns via SEP RB. |
| FA | 02FA | R0 | Jump to 0x0300: MUL loop (shift-and-add multiply). Shifts product left, shifts multiplier left (MSB→DF), adds multiplicand when DF=1. Loops R8.0 iterations. Returns via LBR 020B (SEP RB). Used by MUL (0x06). |
| FD | 02FD | R0 | Jump to 0x0347: DIV loop (restoring division). Shifts quotient/dividend left, trial-subtracts divisor, restores on borrow. Loops R8.0 iterations. Returns via LBR 020B (SEP RB). Used by DIV (0x07). |
Syntax
Instructions
| Opcode | Mnemonic | Parameter | Forth | Handler | Dispatch | Definition |
|---|---|---|---|---|---|---|
| 00 | ADD | TOS, NOS | + | 056B | 06→056C | TOS = NOS + TOS (16-bit add) |
| 01 | SHL | TOS | 2* | 0572 | 06→0573 | TOS = TOS × 2 (16-bit shift left) |
| 02 | SUB | TOS, NOS | - | 0577 | 06→0578 | TOS = NOS − TOS (16-bit subtract) |
| 03 | ADD | TOS, 1 | 1+ | 057E | 0C→0581 | TOS = TOS + 1 |
| 04 | ADD | TOS, 2 | 2+ | 0582 | 0C→0585 | TOS = TOS + 2 |
| 05 | ADD | [TOS], NOS | +! | 058A | 7E→058B | M(TOS) = M(TOS) + NOS (16-bit add in-place). Pops both. |
| 06 | MUL | TOS, NOS | * | 09F8 | 03·B7→09FB | TOS = NOS × TOS (16-bit result, overflow discarded) |
| 07 | DIV | TOS, NOS | /MOD | 0A23 | 29→0A24 | TOS = NOS / TOS (quotient), NOS = NOS mod TOS (remainder). 16-bit unsigned division. Pushes two results. |
| 08bb | JZ | aaaa | ?BRANCH | 059D | 8A→059E | Pop TOS; if TOS == 0, jump to aaaa (forward, aaaa = PC + 1 + bb) |
| 09bb | JZ | aaaa | ?BRANCH | 05D5 | 8A→05D6 | Pop TOS; if TOS == 0, jump to aaaa (backward, aaaa = PC + 1 + bb - 256) |
| 0Abb | JP | aaaa | BRANCH | 05A5 | 8B→05A6 | Unconditional jump to aaaa (forward, aaaa = PC + 1 + bb) |
| 0Bbb | JP | aaaa | BRANCH | 05DD | 8B→05DE | Unconditional jump backward (aaaa = PC + 1 + bb − 256) |
| 0Cbb | LOOP | aaaa | (LOOP) | 05AF | 2B→05B0 | Increment 16-bit counter at top of return stack (SP+0/1) by 1. Compare with limit at SP+2/3. If counter > limit (signed): branch backward to aaaa. Otherwise: exit loop. |
| 0Dbb | LOOP | TOS, aaaa | (+LOOP) | 05B1 | 0B→05B2 | Same as LOOP but increment by TOS (variable step) instead of 1. Branch backward to aaaa. |
| 0E | LOOP | FRAME | (DO) | 05E7 | 7E→05E8 | Push loop frame to return stack: TOS→SP+0/1 (counter), NOS→SP+2/3 (limit). Pops both from data stack. |
| 0F | LOOP | CLR | LEAVE | 05F2 | 0B→05F3 | Clear loop frame limit: zeroes SP+2/3 (limit). Does not modify counter or pop the frame. |
| 10 | EXEC | TOS | EXECUTE | 05FE | 32→05FF | Call handler at M(TOS). Pops TOS, dispatches to target. |
| 11 | RET | EXIT | 060F | 8B→0610 | Return from subroutine: pop RC from return stack | |
| 12 | AND | TOS, NOS | AND | 0615 | 06→0616 | TOS = NOS AND TOS (16-bit bitwise AND) |
| 13 | ISZ | TOS | 0= | 0627 | 8A→0628 | TOS = (TOS == 0) ? 1 : 0. |
| 14 | ISNEG | TOS | 0< | 0635 | 8B→0636 | TOS = (TOS < 0) ? 1 : 0. Tests bit 7 of TOS high byte. |
| 15 | LD | TOS, [TOS] | @ | 0661 | 84→0662 | TOS = M(TOS):M(TOS+1). Load 16-bit value from M(TOS). |
| 16 | LD | [TOS], NOS | ! | 0676 | 84→0677 | M(TOS) = NOS (store 16-bit NOS to M(TOS), pops both) |
| 17 | LD | TOS.0, [TOS] | C@ | 068A | 84→068B | TOS.0 = M(TOS) (load single byte from M(TOS)) |
| 19 | LD | [TOS], NOS.0 | C! | 0694 | 84→0695 | M(TOS) = NOS.low. Store low byte of NOS to M(TOS), pops both. |
| 1A | MCPY | [NOS], [3OS] | CMOVE | 0699 | 78→069A | Memory copy: pops 3 values (TOS=count, NOS=dest, 3OS=source); copies full 16-bit count bytes from source to dest |
| 1C | DROP | DROP | 06A5 | 8B→06A6 | Drop TOS (discard top of stack) | |
| 1D | OVER | OVER | 06A9 | 8C→06AA | Copy NOS to TOS (push copy of second stack value on top) | |
| 1E | DUP | DUP | 06AD | 8E→06AE | Duplicate TOS (push copy of top value) | |
| 1F | DUPNZ | ?DUP | 06B3 | 8A→06B4 | Duplicate TOS if nonzero (push copy); if TOS == 0, no change | |
| 20 | SWAP | SWAP | 06BE | 7E→06BF | Exchange TOS and NOS on data stack (swap top two 16-bit values) | |
| 21 | ROT | ROT | 06C6 | 78→06C7 | Rotate 3OS item to TOS (Forth ROT). Before: TOS=A, NOS=B, 3OS=C → After: TOS=C, NOS=A, 3OS=B | |
| 22aaaa | PUSH | aaaa | LIT aaaa | 06D0 | 8B→06D1 | Push 16-bit immediate value aaaa onto data stack |
| 23kk | PUSH | kk | LIT kk | 06D5 | 8B→06D6 | Push 8-bit constant kk onto data stack |
| 24 | PUSH | R | R@ | 06DF | 0B→06E0 | Read 16-bit value from top of return stack (R2), push to data stack (return stack unchanged) |
| 25 | POPR | R> | 06EA | 8B→06EB | Pop 16-bit value from return stack (R2), push to data stack | |
| 26 | PUSHR | >R | 06F1 | 84→06F2 | Pop TOS from data stack, push to return stack (R2) | |
| 27 | SUB | TOS, 1 | 1- | 0586 | 0C→0589 | TOS = TOS − 1 |
| 29 | NAVBK | NAVBK | 11A0 | 2B→11A1 | Conditional bank navigation. Reads byte at M(60CA/CB): if bit 7 = 1, returns immediately (already at target). If bit 7 = 0, follows linked-list of forward offsets through app ROM (lower 7 bits = offset to next record) until finding a record with bit 7 = 1, then updates M(60CA/CB). Same as SETBK phase 1 only; no phase 2 traversal, no bank select write. | |
| 2A | LD | TOS.0, [RP] | C@+ | 1280 | 8A→1281 | Load byte from app ROM via [RP], push to data stack. Advance pointer by 1. |
| 2B | STRCP | STRCP | 07F5 | 94→07F9 | String copy with space padding: copies VD bytes from M(VC) to M(VA), pads (VB-VD) spaces to fill field width VB | |
| 2C | NUMCP | NUMCP | 081D | 94→0821 | Numeric copy with zero padding: pads (VB-VD) zero bytes at M(VA) first, then copies VD bytes from M(VC). Right-aligns in field width VB. | |
| 2D | NUMCZ | NUMCZ | 0815 | 94→0819 | Numeric copy with '0' padding: pads (VB-VD) ASCII '0' bytes at M(VA), then copies VD bytes from M(VC). Right-aligns in field width VB. | |
| 2E | LD | [TOS], 0 | 0 C! | 0875 | 84→0876 | M(TOS) = 0x00. Pops TOS. |
| 2F | LD | [TOS], 1 | 1 C! | 0879 | 84→087A | M(TOS) = 0x01. Pops TOS. |
| 30 | ADD | [TOS], NOS.0 | C+! | 087D | 84→087E | M(TOS) = M(TOS) + NOS.low (byte add). Pops both. |
| 31 | MCPY3 | [TOS], [NOS] | CMOVE3 | 088B | 7E→088C | Copy 3 bytes from NOS (source) to TOS (dest). Pops both. |
| 32 | ADD24 | [NOS], [TOS] | D+24 | 089C | 7E→089D | 24-bit (3-byte) add: M(NOS) = M(NOS) + M(TOS), big-endian. Pops both addresses. |
| 33 | OR | TOS, NOS | OR | 0892 | 06→0893 | TOS = NOS OR TOS (16-bit bitwise OR) |
| 34 | COMP | COMPARE | 08C8 | 78→08C9 | Multi-byte compare: compares TOS.0 bytes at NOS against 3OS. Returns 0 (equal), 1 (NOS > 3OS), or FFFF (NOS < 3OS) | |
| 35 | ADD | TOS, [TOS] | +@ | 08E2 | 84→08E3 | TOS = TOS + M(TOS) (16-bit add with carry) |
| 36 | LD24 | [TOS], NOS | !24 | 08AE | 84→08AF | Store NOS as 24-bit (zero-extended) at M(TOS): M(TOS)=0x00, M(TOS+1)=NOS.1, M(TOS+2)=NOS.0. Pops both. |
| 37 | LDVCD | [TOS] | LDVCD | 07E3 | 84→07E4 | VD = M(TOS), VC = TOS+1 (address); pops TOS |
| 38 | MCPY | [TOS], [NOS] | CMOVE | 08E8 | 7E→08E9 | Copy M(NOS) bytes from NOS+1 to TOS; count is first byte at source address. Pops both. |
| 39 | LDBNK | [TOS] | C@BK | 10BA | 84→10BB | Read 1 byte from banked ROM at 3-byte encoded address (see Bank Address Encoding). Pushes byte to TOS. Restores home bank on exit. |
| 3Aaaaa | LD | TOS, [aaaa] | @ aaaa | 065D | A4·85→065F | TOS = [aaaa] (load 16-bit, push to stack) |
| 3Baaaa | LD | [aaaa], TOS | ! aaaa | 0673 | A4·85→0675 | [aaaa] = TOS (pop 16-bit, store to address) |
| 3Caaaa | LD | TOS.0, [aaaa] | C@ aaaa | 0686 | A4·85→0688 | TOS = [aaaa] (load byte, push) |
| 3Daaaa | LD | [aaaa], TOS.0 | C! aaaa | 0691 | A4·85→0693 | [aaaa] = TOS (pop, store low byte only) |
| 3Ebb | LD | TOS, [60bb] | @ 60bb | 0B2D | A4·A7→0B2F | Load 16-bit from variable at 0x60bb, push to stack |
| 3Fbb | LD | [60bb], TOS | ! 60bb | 0B33 | A4·B7→0B35 | Pop TOS (16-bit), store to variable at 0x60bb |
| 40bb | LD | TOS.0, [60bb] | C@ 60bb | 0B2A | A4·B2→0B2C | Load 1 byte from variable at 0x60bb, push to stack |
| 41bb | LD | [60bb], TOS.0 | C! 60bb | 0B30 | A4·BF→0B32 | Pop TOS, store low byte to variable at 0x60bb |
| 42kk | LD | [60BD], kk | C! kk | 0B36 | 94→0B39 | Store immediate byte kk to fixed address 0x60BD |
| 43 | DISP | DISPLAY | 072F | 66→0730 | Character insertion into display buffer. Pops TOS as dest position. Inserts formatted chars from var A into cursor positions (0x5F markers) in buffer at dest. | |
| 44 | LCD | LCD | 1848 | CA→184B | Full HD44780 display refresh. Writes 2×32 chars from RAM buffer 0x6080–0x60BF to LCD. Each byte translated via table at 0x68xx. | |
| 45 | DELAY | DELAY | 1A9C | CA→1A9F | Software delay. Duration controlled by M(6009): if 0, exits immediately; if nonzero, delays proportional to value. No I/O. | |
| 46 | FILL | FILL | 0884 | 78→0885 | Fill memory: pops 3 values (TOS=count, NOS=dest addr, 3OS=fill byte); fills count.low bytes at dest with fill.low | |
| 47 | CKSUM | CHECKSUM | 08F1 | 84→08F2 | Checksum: pops TOS (start address) and NOS (count). Loops count times: adds byte at current address to the address itself, then advances. Pushes final address. If count == 0, pushes TOS unchanged. | |
| 48 | TOBCD | TOBCD | 0907 | 6C→0908 | Binary to packed BCD (double-dabble algorithm). Pops TOS as byte count N. Converts N binary bytes at M(6038..6037+N) to packed BCD at M(60(37−N)..6037). No stack result. | |
| 49 | FMBCD | FMBCD | 0959 | 6C→095A | Packed BCD to binary (reverse double-dabble algorithm). Pops TOS as byte count N. Converts BCD at M(60(38−N)..6037) to binary at M(6038..6037+N). Inverse of TOBCD. No stack result. | |
| 4B | FMHEX | FMHEX | 0A72 | 94→0A76 | ASCII hex to nibble. Converts VD hex chars from M(VC) (right-to-left) into VB nibble bytes at M(VA). Zero-pads remaining output. Uses vars VA–VD. | |
| 4C | TOHEX | TOHEX | 0AC1 | 94→0AC5 | Binary to ASCII hex. Converts VD bytes from M(VC) to VB hex chars at M(VA) (right-to-left, 2 chars per byte). Inverse of FMHEX. Uses vars VA–VD. | |
| 4D | ISLT | < | 06F8 | 7E→06F9 | TOS = (TOS < NOS) ? 1 : 0 (16-bit unsigned) | |
| 4E | ISGT | > | 0700 | 06→0701 | TOS = (TOS > NOS) ? 1 : 0 (16-bit unsigned) | |
| 4F | ISEQ | = | 070F | 06→0710 | TOS = (TOS == NOS) ? 1 : 0 (16-bit) | |
| 52kk | PUSH | VE, kk | VE+ kk | 0647 | 94→064B | TOS = VE + kk. VE unchanged. |
| 53kk | PUSH | [VE].0, kk | VE+C@ kk | 067D | 94→0681 | TOS = M(VE + kk) (load byte). VE unchanged. |
| 55kk | PUSH | [VE], kk | VE+@ kk | 0654 | 94→0658 | TOS = M(VE + kk) : M(VE + kk + 1) (load 16-bit word, big-endian). VE unchanged. |
| 56kk | POP | [VE], kk | VE+! kk | 066A | 94→066E | M(VE + kk) = TOS.0 (store low byte). VE unchanged. |
| 57aaaakk | LDAND | [aaaa], kk | LDAND aaaa kk | 061C | A4·85→061E | TOS = [aaaa] & kk (load byte, AND with mask, push) |
| 58aaaammmm | MCPY3 | [aaaa], [mmmm] | CMOVE3 aaaa mmmm | 0888 | A4·7F→088A | Copy 3 bytes from [mmmm] to [aaaa]. Fixed count of 3. |
| 59aaaammmm | ADD24 | [mmmm], [aaaa] | D+24 mmmm aaaa | 0899 | A4·7F→089B | [mmmm] = [mmmm] + [aaaa] — 24-bit (3-byte) big-endian add. |
| 5Aaaaammmm | STBNK | [aaaa], mmmm | !BK aaaa mmmm | 156E | 6C→156F | Banked write: copy TOS bytes from RAM at mmmm to banked address at [aaaa] (see Bank Address Encoding). Pops TOS (count). |
| 5Baaaammmm | LDBNK | aaaa, [mmmm] | C@BK aaaa mmmm | 15CC | 6C→15CD | Banked read: copy TOS bytes from banked address at [mmmm] to RAM at aaaa (see Bank Address Encoding). Pops TOS (count). |
| 5D | LD | VC, TOS | VC ! | 0B3C | 8B→0B3D | Pop TOS (16-bit), store to VC |
| 5E | LD | TOS, VC | VC @ | 0B10 | 8A→0B11 | Load 16-bit from VC, push to stack |
| 5F | LD | VD, TOS | VD ! | 0B41 | 8B→0B42 | Pop TOS (16-bit), store to VD |
| 60 | LD | TOS, VD | VD @ | 0B15 | 8A→0B16 | Load 16-bit from VD, push to stack |
| 61 | LD | VA, TOS | VA ! | 0B46 | 8B→0B47 | Pop TOS (16-bit), store to VA |
| 62 | LD | TOS, VA | VA @ | 0B1A | 8A→0B1B | Load 16-bit from VA, push to stack |
| 63 | LD | VB, TOS | VB ! | 0B4B | 8B→0B4C | Pop TOS (16-bit), store to VB |
| 64 | LD | TOS, VB | VB @ | 0B1F | 8A→0B20 | Load 16-bit from VB, push to stack |
| 66aaaa | CALLI | [aaaa] | EXECUTE | 05FF | A4·85→0601 | Indirect call: reads 16-bit target from table at aaaa + TOS×2, calls handler at that address. Pops TOS (index). |
| 67 | LD | VE, [RP] | @+VE! | 12A8 | 8A→12A9 | VE = M(RP) : M(RP+1). Load 16-bit word from app ROM, store to VE, advance pointer by 2. |
| 6A | LD | TOS, [RP] | @+ | 12A0 | 8A→12A1 | Load 16-bit word from app ROM via [RP], push to stack, advance pointer by 2 |
| 6B | SETBK | SETBK | 11A2 | 29→11A3 | Full bank navigation. Phase 1: reads byte at M(60CA/CB): if bit 7 = 0, follows linked-list of forward offsets through app ROM (lower 7 bits = offset to next record) until finding a record with bit 7 = 1; updates M(60CA/CB). If bit 7 is already set, the pointer is unchanged. Phase 2: reads pointer from M(60C0/C1/C2), traverses a second linked list in banked ROM from that address (same offset mechanism), until a record with bit 7 = 1 is found. Restores home bank on exit. No data transfer. | |
| 6E | SUB24 | [NOS], [TOS] | D-24 | 09B6 | 7E→09B7 | M(NOS) = M(NOS) − M(TOS) — 24-bit (3-byte) subtract, big-endian. Pops both addresses. |
| 6F | ADD24 | [NOS], TOS | D+24 | 0A4D | 7E→0A4E | M(NOS) = M(NOS) + TOS — Add 16-bit TOS to 24-bit (3-byte) value at M(NOS), big-endian. Pops both. |
| 70 | SUB24 | [NOS], TOS | D-24 | 0A63 | 7E→0A64 | M(NOS) = M(NOS) − TOS — Subtract 16-bit TOS from 24-bit (3-byte) value at M(NOS), big-endian. Pops both. |
| 72 | PUSH | 0000 | LIT 0000 | 212E | 20 | Push 0x0000 (constant in handler body) |
| 73 | PUSH | 0001 | LIT 0001 | 2132 | 20 | Push 0x0001 (constant in handler body) |
| 74 | PUSH | 0002 | LIT 0002 | 2136 | 20 | Push 0x0002 (constant in handler body) |
| 75 | PUSH | 0003 | LIT 0003 | 213A | 20 | Push 0x0003 (constant in handler body) |
| 76 | PUSH | FFFF | LIT FFFF | 213E | 20 | Push 0xFFFF (constant in handler body) |
| 7F | NOOP | NOOP | 0BE3 | 28→0BE4 | No operation | |
| 80kk–FFkk | SYS | aaaa | SYS aaaa | - | - | System dispatch to target address aaaa. Address calculation: aaaa = (kk << 8) | ((opcode & 0x7F) << 1). Only even target addresses are reachable. Note that kk is a program memory page value; multiple opcodes can target the same page of code. Example: opcode DD 07 → kk = 07, kk << 8 = 0x0700, (opcode & 0x7F) = 0x5D, 0x5D << 1 = 0xBA, address = 0x0700 | 0xBA = 0x07BA |
SYS Targets
| Opcode | Mnemonic | Handler | Dispatch | Function |
|---|---|---|---|---|
| CA 05 | SYS 0594 | 0594 | 8A→0595 | SHR TOS — 16-bit logical shift right |
| A0 06 | SYS 0640 | 0640 | 06→0641 | XOR: TOS = NOS XOR TOS (16-bit bitwise XOR) |
| 91 07 | SYS 0722 | 0722 | 84→0723 | Average: TOS = (TOS + NOS) / 2 (16-bit add then shift right) |
| CC 07 | SYS 0798 | 0798 | 8B→0799 | String index: compute address VC + VD, load byte, push to stack |
| DD 07 | SYS 07BA | 07BA | 72→07BB | String strip: strip bit 7 from each char in display buffer using ROMCPY state pointers and var D count |
| A9 08 | SYS 0852 | 0852 | 94→0853 | Variable-indirect compare setup: load params from vars, fall through to COMP |
| DC 08 | SYS 08B8 | 08B8 | 7E→08B9 | COMP3: 3-byte compare of M(TOS) vs M(NOS). Returns 0/1/FFFF. |
| DF 08 | SYS 08BE | 08BE | 94→08BF | Variable-indirect COMP: load count from var, addresses from stack, compare bytes |
| D3 09 | SYS 09A6 | 09A6 | 78→09A7 | Multi-byte add: M(NOS) += M(TOS), N bytes (big-endian, N in R8.0) |
| E0 09 | SYS 09C0 | 09C0 | 78→09C1 | Multi-byte subtract: M(NOS) -= M(TOS), N bytes (big-endian, N in R8.0) |
| E8 09 | SYS 09D0 | 09D0 | 6C→09D1 | Multi-byte negate: zero-subtract N bytes at address on stack |
| F1 09 | SYS 09E2 | 09E2 | 6C→09E3 | Clear and prepare BCD workspace (fill computed area with zeros) |
| 89 0A | SYS 0A12 | 0A12 | 6C→0A13 | BCD shift/multiply operation (shift left with BCD correction) |
| A3 0A | SYS 0A46 | 0A46 | 84→0A47 | ADD24 variant: add 16-bit TOS to 24-bit value at computed address |
| AE 0A | SYS 0A5C | 0A5C | 84→0A5D | SUB24 variant: subtract 16-bit TOS from 24-bit value at computed address |
| AB 0B | SYS 0B56 | 0B56 | 66→0B57 | Indexed address load: compute address from TOS + M(TOS), store to var A |
| F2 0B | SYS 0BE4 | 0BE4 | 8B→0BE5 | VE block setup: load VE, set R9=VE, RA=VE+6, R8=VE+3 for block operation |
| 96 0C | SYS 0C2C | 0C2C | 84→0C2D | Display buffer operation: manipulate buffer at 0x62xx using TOS as parameter |
| C2 0C | SYS 0C84 | 0C84 | 7E→0C85 | Indexed byte store: store NOS.0 at address (TOS + M(TOS)) |
| B7 0E | SYS 0E6E | 0E6E | 8B→0E6F | CALL native: save RC to return stack, set up native 1802 execution context |
| CD 0E | SYS 0E9A | 0E9A | 8A→0E9B | Context switch: save interpreter state, transfer to native execution at computed address |
| 8D 0F | SYS 0F1A | 0F1A | 84→0F1B | Bank state save: store bank/address to M(6050-54) from TOS |
| 9C 0F | SYS 0F38 | 0F38 | 84→0F39 | Bank state restore: load bank/address from M(6050-54), store to TOS address |
| C9 0F | SYS 0F92 | 0F92 | 8B→0F93 | ROM pointer advance: read TOS as offset, advance ROM pointer M(60CA/CB) |
| DE 0F | SYS 0FBC | 0FBC | 8B→0FBD | Store TOS to M(60C4/C5), copy M(60B7-B9) to M(60BA-BC) working area |
| 85 10 | SYS 100A | 100A | 8B→100B | Copy M(60B7-B9) base to M(60BA-BC) working area, decode bank address |
| B0 10 | SYS 1060 | 1060 | 8B→1061 | Bank navigation: traverse linked list in banked ROM via M(60CB/BC) |
| C3 10 | SYS 1086 | 1086 | 8B→1087 | Bank base setup: copy M(60B7-B9) to working area, traverse linked list |
| DB 10 | SYS 10B6 | 10B6 | 84→10B7 | LDBNK variant: read 1 byte from banked ROM at 3-byte address, push to stack |
| E9 10 | SYS 10D2 | 10D2 | 8B→10D3 | Banked read via M(60CA/CB): decode bank, read sequential bytes from ROM |
| A7 12 | SYS 124E | 124E | 8B→124F | Record interpreter: reads bytes sequentially from app ROM via [RP]. Each byte (bit 7 set) is decoded via dispatch table at ROM page 0x20, selecting sub-handlers. Self-repeating (backs up RC by 2). |
| EB 12 | SYS 12D6 | 12D6 | 8B→12D7 | Bank base copy: copy M(60B7-B9) to M(60BA-BC) and decode bank for subsequent access |
| 8E 13 | SYS 131C | 131C | 66→131D | ROMCPY: block copy from banked ROM to RAM. Pops TOS (dest address), NOS (offset). Adds offset to 24-bit base at M(60B7/B8/B9), decodes bank (see Bank Address Encoding), copies data to dest. |
| 90 13 | SYS 1320 | 1320 | 8B→1321 | ROMCPY to fixed dest: block copy from banked ROM to RAM at 0x6260 via M(60CA/CB) pointer |
| C1 13 | SYS 1382 | 1382 | 8B→1383 | BANKPTR: compute banked ROM address from base + TOS offset (see Bank Address Encoding), store pointer to M(60C0/C1/C2). No data transfer. |
| D8 13 | SYS 13B0 | 13B0 | 8B→13B1 | Banked block read (SEQ): set Q, read block from banked ROM via M(60CA/CB) to RAM |
| DA 13 | SYS 13B4 | 13B4 | 8B→13B5 | Banked block read (REQ): reset Q, read block from banked ROM via M(60CA/CB) to RAM |
| DF 13 | SYS 13BE | 13BE | 8B→13BF | Banked block read (REQ variant): reset Q, read block with alternate pointer setup |
| 97 15 | SYS 152E | 152E | 84→152F | Banked decode + copy: decode bank from TOS, copy source address to var A |
| A2 15 | SYS 1544 | 1544 | 72→1545 | Banked block write (variant 1): pop count and address, write to banked ROM/RAM |
| BF 15 | SYS 157E | 157E | 6C→157F | Banked decode + read: pop count, decode bank address, copy bytes to RAM |
| C8 15 | SYS 1590 | 1590 | 84→1591 | Banked store: decode bank from TOS, write data to banked address |
| D0 15 | SYS 15A0 | 15A0 | 72→15A1 | Banked block write (variant 2): pop count and address, write to banked ROM/RAM with state save |
| E9 15 | SYS 15D2 | 15D2 | 6C→15D3 | Banked block read with SEQ: pop count, decode bank, copy bytes with Q set |
| FF 15 | SYS 15FE | 15FE | - | Bank deselect and state restore (shared exit routine) |
| 9D 16 | SYS 163A | 163A | 94→163B | Banked byte read via var: decode bank from variable, read byte from banked address |
| B4 16 | SYS 1668 | 1668 | 6C→1669 | Banked multi-byte read: pop count, decode bank, read block to RAM via LCD char table |
| E9 16 | SYS 16D2 | 16D2 | 84→16D3 | Banked sequential read: decode bank from TOS, read bytes with page-crossing detection |
| A2 17 | SYS 1744 | 1744 | CA→1747 | Set bit 7 of HW register M(3FA1) (RAM shadow M(6001)) |
| A6 17 | SYS 174C | 174C | CA→174F | Clear bit 7 of HW register M(3FA1) (RAM shadow M(6001)) |
| AA 17 | SYS 1754 | 1754 | CA→1757 | Set bit 6 of HW register M(3FA1) (RAM shadow M(6001)) |
| AE 17 | SYS 175C | 175C | CA→175F | Clear bit 6 of HW register M(3FA1) (RAM shadow M(6001)) |
| B7 17 | SYS 176E | 176E | 8A→176F | Test bits 0-1 of M(6060): push 1 if both set, else push 0 |
| CA 17 | SYS 1794 | 1794 | 8A→1795 | Test EF1: push 1 if EF1 active, else push 0 |
| DB 17 | SYS 17B6 | 17B6 | CA→17B9 | Write M(6022) to HW register M(3F60) |
| DE 17 | SYS 17BC | 17BC | CA→17BF | Write M(6021) to HW register M(3F62) |
| E1 17 | SYS 17C2 | 17C2 | CA→17C5 | Write M(601F):M(6020) to HW registers M(3F82):M(3F83) |
| E9 17 | SYS 17D2 | 17D2 | CA→17D5 | Clear bit 3 of HW register M(3FA1) (RAM shadow M(6001)) |
| EE 17 | SYS 17DC | 17DC | CA→17DF | Clear bit 3 of HW register M(3FA1) with alternate setup |
| FC 17 | SYS 17F8 | 17F8 | CA→17FB | Conditional clear bit 3 of HW register M(3FA1): test M(6001), clear if condition met |
| 86 18 | SYS 180C | 180C | CA→180F | Shift TOS.0 left 5 bits, write to HW register via M(3FA0) |
| 90 18 | SYS 1820 | 1820 | CA→1823 | Set bit 7 of HW register M(3FF0) (RAM shadow M(6064)) |
| 94 18 | SYS 1828 | 1828 | CA→182B | Clear bit 7 of HW register M(3FF0) (RAM shadow M(6064)) |
| A4 18 | SYS 1848 | 1848 | CA→184B | LCD refresh: write 2×32 chars from display buffer 0x6080–0x60BF to HD44780 |
| CC 18 | SYS 1898 | 1898 | CA→189B | Keyboard scan: scans 6 ports 0x3F20–0x3F25. Returns key code, or 0x0000 if no key. |
| A6 19 | SYS 194C | 194C | 29→194D | Push 0x0000 (no-op return: entry 0x29 pushes zero, returns immediately) |
| A7 19 | SYS 194E | 194E | 8A→194F | Set bank window: store TOS.0 to M(604E), select bank page |
| AC 19 | SYS 1958 | 1958 | CA→195B | Set bank register: write TOS.0 with bit 7 set to shadow M(604F), write masked value to M(3FA2) |
| B2 19 | SYS 1964 | 1964 | 8B→1965 | Reset status: write 0x10 to M(3FA0), clear R0.1 |
| BC 19 | SYS 1978 | 1978 | CA→197B | Clear bit 2 of HW register M(3FA0), clear R5.0, reset interrupt state |
| C4 19 | SYS 1988 | 1988 | CA→198B | Write 0x02 to HW register M(3F63) (RAM shadow M(6067)), configure serial/timer |
| D7 19 | SYS 19AE | 19AE | 8A→19AF | Context restore: reload R2, RD, RC from saved state at M(601B-1F) |
| E2 19 | SYS 19C4 | 19C4 | 8A→19C5 | Serial RX poll: test EF4; if active → receive byte and push result; if inactive → push 0x0000 (no data) |
| F0 19 | SYS 19E0 | 19E0 | CA→19E3 | Clear HW register M(3F0E): write 0 via entry 0xCA with R6=A7, R7=0E |
| DB 1A | SYS 1AB6 | 1AB6 | 6C→1AB7 | Serial TX (SEQ): set Q, pop TOS as byte count, transmit bytes from data stack |
| DE 1A | SYS 1ABC | 1ABC | 6C→1ABD | Serial TX (REQ): reset Q, pop TOS as byte count, transmit bytes from data stack |
| 86 1B | SYS 1B0C | 1B0C | 6C→1B0D | Serial block TX (SEQ): set Q, pop count, transmit block from banked address |
| 88 1B | SYS 1B10 | 1B10 | 6C→1B11 | Serial block TX (REQ): reset Q, pop count, transmit block from banked address |
| BA 1B | SYS 1B74 | 1B74 | 8A→1B75 | Re-init interpreter: set R6.1=0x80, R2.1=0x81, RD.1=0x82, RF.1=0x05 |
| C1 1B | SYS 1B82 | 1B82 | CA→1B85 | Set bits in HW register M(3FF0): OR TOS.0 (masked to bits 0-1) with M(6064), write to M(3FF0) |
| D0 1B | SYS 1BA0 | 1BA0 | CA→1BA3 | Clear bit 0 of HW register 0x3FF1 (RAM shadow at 0x6065) |
| E0 1B | SYS 1BC0 | 1BC0 | 6C→1BC1 | LCD character table update: pop count, write character mappings to RAM 0x68xx lookup table |
| B8 1C | SYS 1C70 | 1C70 | CA→1C73 | Clear bit 4 of HW register M(3F80) (RAM shadow M(6066)) |
| BD 1C | SYS 1C7A | 1C7A | 6C→1C7B | Display buffer init: pop count, initialize display character buffer at 0x60xx |
| E4 1D | SYS 1DC8 | 1DC8 | 6C→1DC9 | INP 4 read: input from port 4, process result into display/state buffer |
| C3 1E | SYS 1E86 | 1E86 | 8A→1E87 | Multi-dispatch: set RE.0=2, branch to sub-handler based on TOS value |
| 8A 1F | SYS 1F14 | 1F14 | 8A→1F15 | Set R5.0 = 0x04 (mode/state configuration) |
CALLI Targets (Traced)
CALLI (opcode 0x66) performs indirect calls through function pointer tables. Each table entry is a 2-byte handler address. The handler at addr−1 contains an entry byte (same dispatch mechanism as the record interpreter).
Table at 0x2812
| Index | Table addr | Target | Entry | Definition |
|---|---|---|---|---|
| 10 | 2832 | 0AC2 | 94 | Packed hex-to-ASCII: converts byte from source to 2 ASCII hex chars at dest buffer |
Record Interpreter (SYS 124E)
SYS 124E implements a third level of interpretation. It reads structured data bytes from the app ROM and dispatches sub-handlers via a lookup table at ROM page 0x20. The record interpreter is self-repeating (backs up RC by 2 each iteration) and processes bytes until a CALL entry transfers control elsewhere.
Entry Byte Convention
The record interpreter uses a different address convention than the main bytecode interpreter:
| Interpreter | Table stores | Entry byte at | Mechanism |
| Main bytecode (0x6100) | Handler address (= entry byte location) | stored address | LDA RB reads entry, RB advances to body |
| Record interpreter (entry 0x32) | Body address (= code start) | stored address − 1 | DEC RB; LDA RB reads entry at addr−1, RB advances to body |
Both dispatch through the same entry-point mechanism. The entry byte at target−1 determines whether the sub-handler is native 1802 code (entry 0x8B) or a bytecode CALL (entry 0x15).
Record Opcodes (ROM page 0x20 dispatch table)
| Byte | SHL | Table | Target | Entry | Definition |
|---|---|---|---|---|---|
| 82 | 04 | 2004 | 1039 | 8B | Field decode + arithmetic: copy pointer to working area, decode fields, subtract var 0x60B8/B9, store result to 0x60C4/C5 |
| 90 | 20 | 2020 | 237F | 15 | CALL to bytecode: push RC to return stack, set RC = 0x237F. RET returns to continue record processing. |
| 94 | 28 | 2028 | 145D | 8B | REQ + multi-byte ROM→RAM copy: reset Q, read bytes from app ROM, store to computed RAM address |
| 9E | 3C | 203C | 1453 | 8B | SEQ + multi-byte ROM→RAM copy: set Q, read bytes from app ROM, store to computed RAM address |
Interpreter Dispatch Loop (RAM at 0x6100)
The following native CDP1802 code is copied from ROM 0x0500 into RAM 0x6100 at startup. It forms the fetch-decode-execute cycle of the VM:
6100: LDA RC ; fetch next bytecode, RC++ 6101: SHL ; bit 7 -> DF, remaining bits x 2 = table index 6102: BDF 610E ; if bit 7 was set -> high opcode path 6104: PLO RE ; RE.0 = table index (low opcodes) 6105: LDA RE ; read handler addr high from table at 0x01xx 6106: PHI RB ; RB.1 = handler high 6107: LDN RE ; read handler addr low from table 6108: PLO RB ; RB.0 = handler low 6109: LDA RB ; first byte at handler = entry-point selector 610A: PLO R3 ; R3.0 = entry point 610B: SEP R3 ; transfer control to handler 610C: BR 6100 ; <- handler returns here via SEP RF, loop back 610E: PLO RB ; high opcode: RB.0 = SHL result (partial addr) 610F: LDA RC ; read next byte = handler addr high page 6110: PHI RB ; RB.1 = inline high byte 6111: LDA RB ; first byte at handler = entry-point selector 6112: PLO R3 ; R3.0 = entry point 6113: SEP R3 ; transfer control to handler 6114: BR 6100 ; loop back 6116: LBR 051A ; escape to ROM (interpreter exit/reinit)
Memory Map (Bytecode Variables)
For the complete memory map and RAM organisation, see the Memory section.
Hardware I/O Registers
For the complete I/O register map, see the I/O Map section.
Startup Trace
For the complete annotated cold boot trace, see PTC-701 Startup Boot Trace.
Notes
- This is a stack-based bytecode VM, unlike the register-based GPL/Chip-8 family. Operations pop operands from the data stack and push results back.
- Opcode encoding: low opcodes (0x00–0x7F) are 1–4 bytes (opcode + optional parameters). High opcodes (0x80–0xFF) are always 2 bytes: a SYS call to a computed even address.
- Fixed constant pushes (opcodes 0x03, 0x04, 0x27) read their values from the handler body in ROM, not from the bytecode stream. They are single-byte opcodes despite pushing 16-bit values.
- The interpreter has no V registers (unlike GPL/Chip-8). Variables are stored as 16-bit words in RAM and accessed by index.
- Bank switching is used to read application data from the 32K ROM through the 8K window at 0x4000–0x5FFF. The bytecode PC (RC) walks through this banked region.
- SYS can target system ROM, banked app ROM (bank-dependent!), or RAM - making the bytecode capable of dispatching to any handler address.
- Many opcodes remain unverified or only partially understood. The mnemonics above are best-effort deductions from handler code patterns and are subject to revision.
- The PTC bytecode language is significantly more complex than the GPL family - it has ~100 low opcodes plus 128 possible SYS targets vs ~16 opcodes for GPL variants.
- The system implements three levels of interpretation: (1) native CDP1802 code in system ROM, (2) the PTC bytecode interpreter (opcodes 0x00–0xFF), and (3) a “record interpreter” (SYS 124E) that processes structured data bytes from the app ROM. Each record byte dispatches to sub-handlers (at 0x1039, 0x1453, 0x145D, etc.) via a lookup table at ROM page 0x20. The record interpreter is self-repeating and exits via a CALL entry that transfers to new bytecode.