Table of Contents
Appendix 4: Instruction Set Architecture
ISA Overview
The SD-8516 instruction set is organized into four tiers. Numbering is canonical in decimal (as defined in arch.h and arch.ts); hex is given alongside for convenience.
- Tier 1 - Core behaves like a RISC instruction set: small, orthogonal, and complete on its own. Target this first. Everything above it can be synthesized from the core if you have to.
- Tier 2 - Extended are quality-of-life instructions for hand assemblers.
ADD reg, immis the canonical example: you do not need it (load the immediate andADD), but it is provided for convenience.MULis in the same spirit, since you can loop withADD. - Tier 3 - CISC are heavier, mostly VAX- and 680×0-flavored instructions: block memory, queue management, character scanning, jump tables. Each one folds a small loop into a single fetch-execute.
- Tier 4 - Acceleration are instructions added primarily to speed up a specific consumer: the LLVM backend (
MOVSX/MOVZX, indexed addressing), the Forth system (LSTEPand friends), or the hardware (PPU/ASU). They exist to remove instructions from a hot path, not to add expressiveness.
A handful of opcodes carry aliases (e.g. JC/JAE, BC/BAE, CMPC/CMPC3); both names assemble to the same byte.
Deprecated since the previous revision: CASE3, CASEB, PAB, UAB, and conditional JMPR.
Tier 1: Core (RISC)
A small, sufficient core. If you only ever target these, you can write any program.
| # | hex | Mnemonic | Example | Description | Flags |
| 0 | $00 | LD_IMM | LDA $5 | Load register with an immediate value | |
| 1 | $01 | LD_MEM | LDA [$5] | Load register from memory (absolute) | |
| 2 | $02 | LD_REG | LDA [X] | Load register from memory (register pointer) | |
| 3 | $03 | LD_IMM8 | LDAL $5 | Load byte register with an immediate | |
| 4 | $04 | LD_MEM8 | LDAL [$5] | Load byte from memory (absolute) | |
| 5 | $05 | LD_REG8 | LDAL [X] | Load byte from memory (register pointer) | |
| 6 | $06 | ST_MEM | STA [$10] | Store register to memory (absolute) | |
| 7 | $07 | ST_REG | STA [X] | Store register to memory (register pointer) | |
| 9 | $09 | MOV | MOV Y, A | Copy register to register (same width) | |
| 11 | $0B | PUSH | PUSH A | Push register onto stack | |
| 12 | $0C | POP | POP Y | Pop stack into register | |
| 15 | $0F | PUSHF | PUSHF | Push flags | |
| 16 | $10 | POPF | POPF | Pop flags | * |
| 23 | $17 | INC | INC X | Increment register by 1 (any width) | |
| 24 | $18 | DEC | DEC Y | Decrement register by 1 (any width) | |
| 30 | $1E | ADD | ADD X, Y | X = X + Y | |
| 31 | $1F | SUB | SUB X, Y | X = X - Y | |
| 50 | $32 | AND | AND dst, src | Bitwise AND | |
| 51 | $33 | OR | OR dst, src | Bitwise OR | |
| 52 | $34 | XOR | XOR dst, src | Bitwise XOR | |
| 53 | $35 | NOT | NOT reg | Bitwise NOT (invert all bits) | |
| 70 | $46 | SHL | SHL A | Shift left | Z N C |
| 71 | $47 | SHR | SHR A | Shift right | Z N C |
| 90 | $5A | CMP | CMP A, B | Compare (subtract, discard result) | Z N C V |
| 91 | $5B | CMP_IMM | CMP A, $1 | Compare against immediate | Z N C V |
| 100 | $64 | JMP | JMP @label | Unconditional jump | |
| 101 | $65 | JZ | JZ @label | Jump if zero | |
| 102 | $66 | JNZ | JNZ @label | Jump if not zero | |
| 105 | $69 | JC | JC @label | Jump if carry set (alias JAE) | |
| 106 | $6A | JNC | JNC @label | Jump if carry clear (alias JNAE) | |
| 130 | $82 | CALL | CALL @label | Call subroutine (push IP, jump) | |
| 133 | $85 | RET | RET | Return from subroutine (pop IP) | |
| 134 | $86 | INT | INT $10 | Software interrupt | |
| 135 | $87 | RTI | RTI | Return from interrupt | * |
| 182 | $B6 | SETF | SETF $80 | Set bits in the flags register | * |
| 183 | $B7 | CLRF | CLRF $80 | Clear bits in the flags register | * |
| 184 | $B8 | TESTF | TESTF $80 | Non-destructive AND against flags | Z C |
| 254 | $FE | NOP | NOP | No operation | |
| 255 | $FF | HALT | HALT | Halt CPU (sets HALT flag) |
Although this is intended as a RISC core, there are some extras here, primarily LD-8bit hot path, which is done to speed up execution of 8 bit loads by about 30%. Additionally, INC, DEC and CALL/RET are semi-RISC because you can ADD reg, #1 or PUSH ptr0 and JMP (then POP IP to return). INT and RTI are suspect here, as well as CMP_IMM (you can just load into a register and use register compare). But these instructions are so common that it is difficult to justify not including them. The core set is intended to be minimalist, but not obtusely so.
POPF however, is a strong canidate for removal. It would really only be needed for a kind of protected mode, interrupt mode or kernal mode, and we dont use it currently.
Finally, it may be interesting to fuse CMP and JZ. Imagine, any CMP and conditional jump could be a CJMP. ex. CJZ A, B, address – if they're equal, then we jump. One instruction. We save a dispatch and a byte.
Consider:
- No CMP exists in isolation
- No JZ exists without responding to a change in the Z flag.
- CJZ A, A can test for zero after a load if we really need one.
Further investigate:
- Does the cost of flag calculation outweight dispatch for how often a CMP occurrs?
Tier 2: Extended (Quality of Life)
Conveniences for the hand assembler. None of these add capability the core lacks; they trade an opcode for fewer instructions in common patterns.
| # | hex | Mnemonic | Example | Description | Flags |
| 10 | $0A | XCHG | XCHG X, Y | Swap two registers | |
| 13 | $0D | PUSHA | PUSHA | Push all registers | |
| 14 | $0E | POPA | POPA | Pop all registers | * |
| 17 | $11 | PUSH2 | PUSH2 A, B | Push 2 registers | |
| 18 | $12 | POP2 | POP2 A, B | Pop 2 registers | |
| 19 | $13 | PUSH3 | PUSH3 A, B, X | Push 3 registers | |
| 20 | $14 | POP3 | POP3 A, B, X | Pop 3 registers | |
| 21 | $15 | PUSH4 | PUSH4 A,B,X,Y | Push 4 registers | |
| 22 | $16 | POP4 | POP4 A,B,X,Y | Pop 4 registers | |
| 25 | $19 | ST_PD | STA [-, BLX] | Pre-decrement store (alias DPUSH) | |
| 26 | $1A | LD_FS | LDA [BLX, +] | Load + forward step (alias DPOP) | |
| 27 | $1B | ST_FS | STA [BLX, +] | Store + forward step | |
| 32 | $20 | MUL | MUL X, Y | X = X * Y | |
| 33 | $21 | DIV | DIV X, Y | X = X / Y (unsigned) | |
| 34 | $22 | MOD | MOD X, Y | X = X % Y (unsigned) | |
| 35 | $23 | ADD_REG_IMM | ADD X, $1234 | X = X + immediate | |
| 36 | $24 | SUB_REG_IMM | SUB X, $ABCD | X = X - immediate | |
| 37 | $25 | MUL_REG_IMM | MUL X, $100 | X = X * immediate | |
| 38 | $26 | DIV_REG_IMM | DIV X, $10 | X = X / immediate (unsigned) | |
| 39 | $27 | MOD_REG_IMM | MOD X, $FF | X = X % immediate (unsigned) | |
| 40 | $28 | ADDC | ADDC X, Y | X = X + Y + carry | |
| 41 | $29 | SUBC | SUBC X, Y | X = X - Y - borrow | |
| 42 | $2A | ADDC_REG_IMM | ADDC X, $5 | X = X + immediate + carry | |
| 43 | $2B | SUBC_REG_IMM | SUBC X, $1 | X = X - immediate - borrow | |
| 54 | $36 | TEST | TEST dst, src | Non-destructive AND | |
| 55 | $37 | AND_IMM | AND dst, #imm | Bitwise AND with immediate | |
| 56 | $38 | OR_IMM | OR dst, #imm | Bitwise OR with immediate | |
| 57 | $39 | XOR_IMM | XOR dst, #imm | Bitwise XOR with immediate | |
| 72 | $48 | SHLC | SHLC A | Shift left through carry | Z N C |
| 73 | $49 | SHRC | SHRC A | Shift right through carry | Z N C |
| 74 | $4A | ROL | ROL A | Rotate left | Z N C |
| 75 | $4B | ROR | ROR A | Rotate right | Z N C |
| 76 | $4C | ROLC | ROLC A | Rotate left through carry | Z N C |
| 77 | $4D | RORC | RORC A | Rotate right through carry | Z N C |
| 80 | $50 | SHL_X | SHL_X A, C | Shift left by count (confirm semantics) | Z N C |
| 81 | $51 | SHR_X | SHR_X A, C | Shift right by count (confirm semantics) | Z N C |
| 103 | $67 | JN | JN @label | Jump if negative | |
| 104 | $68 | JNN | JNN @label | Jump if not negative | |
| 107 | $6B | JO | JO @label | Jump if overflow (alias JV) | |
| 108 | $6C | JNO | JNO @label | Jump if no overflow (alias JNV) | |
| 110 | $6E | JMPR | JMPR X | Jump to address in register | |
| 111 | $6F | JL | JL @label | Jump if less (signed) | |
| 112 | $70 | JLE | JLE @label | Jump if less or equal (signed) | |
| 113 | $71 | JG | JG @label | Jump if greater (signed) | |
| 114 | $72 | JGE | JGE @label | Jump if greater or equal (signed) | |
| 115 | $73 | JA | JA @label | Jump if above (unsigned) | |
| 116 | $74 | JNA | JNA @label | Jump if not above (unsigned) | |
| 120 | $78 | BZ | BZ @near | Branch if zero (2-byte relative, +/-127) | |
| 121 | $79 | BNZ | BNZ @near | Branch if not zero | |
| 122 | $7A | BC | BC @near | Branch if carry (alias BAE) | |
| 123 | $7B | BNC | BNC @near | Branch if no carry (alias BNAE) | |
| 124 | $7C | BA | BA @near | Branch if above (unsigned) | |
| 125 | $7D | BNA | BNA @near | Branch if not above (unsigned) | |
| 126 | $7E | BG | BG @near | Branch if greater (signed) | |
| 127 | $7F | BGE | BGE @near | Branch if greater or equal (signed) | |
| 128 | $80 | BL | BL @near | Branch if less (signed) | |
| 129 | $81 | BLE | BLE @near | Branch if less or equal (signed) | |
| 160 | $A0 | SEZ | SEZ | Set zero flag | Z |
| 161 | $A1 | SEN | SEN | Set negative flag | N |
| 162 | $A2 | SEC | SEC | Set carry flag | C |
| 163 | $A3 | SEV | SEV | Set overflow flag | V |
| 164 | $A4 | SEE | SEE | Set exception/extra flag | E |
| 165 | $A5 | SEF | SEF | Set F (free) flag | F |
| 166 | $A6 | SEB | SEB | Set B (bonus) flag | B |
| 167 | $A7 | SEU | SEU | Set U (user) flag | U |
| 168 | $A8 | SED | SED | Set debug (trace) flag | D |
| 169 | $A9 | SEI | SEI | Set interrupt-enable flag | I |
| 170 | $AA | SSI | SSI | Set sound-interrupt flag | S |
| 171 | $AB | CLZ | CLZ | Clear zero flag | Z |
| 172 | $AC | CLN | CLN | Clear negative flag | N |
| 173 | $AD | CLC | CLC | Clear carry flag | C |
| 174 | $AE | CLV | CLV | Clear overflow flag | V |
| 175 | $AF | CLE | CLE | Clear exception/extra flag | E |
| 176 | $B0 | CLF | CLF | Clear F flag | F |
| 177 | $B1 | CLB | CLB | Clear B flag | B |
| 178 | $B2 | CLU | CLU | Clear U flag | U |
| 179 | $B3 | CLD | CLD | Clear debug flag | D |
| 180 | $B4 | CLI | CLI | Clear interrupt-enable flag | I |
| 181 | $B5 | CSI | CSI | Clear sound-interrupt flag | S |
Tier 3: CISC (VAX-style)
Heavier instructions that fold a loop into one fetch-execute. Mostly VAX- and 680×0-inspired; useful for the kernal and for hand-written string/queue code.
| # | hex | Mnemonic | Example | Description | Flags |
| 140 | $8C | MEMCOPY | MEMCOPY src, dst, n | Copy n bytes ptr to ptr | |
| 141 | $8D | SCAN | SCAN ELM, reg | Scan memory for a 1-4 byte needle | Z |
| 142 | $8E | CMPC | CMPC ELM, FLD, C | Compare characters (alias CMPC3) | Z C |
| 143 | $8F | SKPC | SKPC ELM, AL | Skip characters (anti-scan) | |
| 144 | $90 | SKPC_IMM | SKPC ELM, $20 | Skip characters (immediate needle) | |
| 145 | $91 | INSQUE | INSQUE reg, reg | Insert into queue | |
| 146 | $92 | INSQUE_PTR | INSQUE reg, [reg] | Insert into queue (pointer form) | |
| 147 | $93 | REMQUE | REMQUE reg | Remove from queue | Z |
| 148 | $94 | SCANQUE | SCANQUE H, N, O | Scan a queue, matching a field (reg offset) | Z |
| 149 | $95 | SCANQUE_IMM | SCANQUE H, N, #O | Scan a queue, matching a field (imm offset) | Z |
| 200 | $C8 | CASE | CASE AL, #limit | Inline jump table; CALL table[selector] | V ER |
| 202 | $CA | CASETAB | CASETAB ELM, AL, #limit | Jump table at a base address | V ER |
| 203 | $CB | CVTAN | CVTAN AL | Convert ASCII '0'-'Z' to number 0-35 | O C |
| 204 | $CC | CVTNA | CVTNA AL | Convert number 0-35 to ASCII '0'-'Z' | O C |
Tier 4: Acceleration (LLVM / Forth / Hardware)
Instructions added to remove work from a specific hot path rather than to add expressiveness. Each one has a primary consumer noted below.
Canidates for inclusion: LEA, fused CMP-Bcc and CMP-Jcc, conditional move, LD_IDX16
| # | hex | Mnemonic | Example | Description | Consumer | Flags |
| 28 | $1C | MOVSX | MOVSX A, AL | Move with sign-extend (dst wider than src) | LLVM | |
| 29 | $1D | MOVZX | MOVZX A, AL | Move with zero-extend (dst wider than src) | LLVM | |
| 210 | $D2 | LD_IDXI | LDA [BLX + #4] | Indexed load, signed byte immediate -128..127 | LLVM | |
| 211 | $D3 | LD_IDXR | LDA [BLX + X] | Indexed load, register offset | LLVM | |
| 212 | $D4 | ST_IDXI | STA [BLX + #4] | Indexed store, signed byte immediate | LLVM | |
| 213 | $D5 | ST_IDXR | STA [BLX + X] | Indexed store, register offset | LLVM | |
| 220 | $DC | PPIXEL | PPIXEL X, Y, C | PPU draw pixel | PPU | |
| 221 | $DD | PLINE | PLINE | PPU draw line | PPU | |
| 222 | $DE | PRECT | PRECT | PPU draw rectangle | PPU | |
| 223 | $DF | PRECTF | PRECTF | PPU draw filled rectangle | PPU | |
| 224 | $E0 | PCIRC | PCIRC | PPU draw circle | PPU | |
| 225 | $E1 | PCIRCF | PCIRCF | PPU draw filled circle | PPU | |
| 226 | $E2 | PCLEAR | PCLEAR | PPU clear screen | PPU | |
| 227 | $E3 | PBLIT | PBLIT | PPU draw tile (blit) | PPU | |
| 228 | $E4 | PTIMER | PTIMER | PPU timer function | PPU | |
| 229 | $E5 | PNTIMER | PNTIMER | PPU timer function (variant) | PPU | |
| 240 | $F0 | LSTEPM | LSTEPM | In-memory loop step at [FLX] | Forth | Z |
| 241 | $F1 | TTOS | TTOS | Test top of stack | Forth | Z |
| 242 | $F2 | LIT | LIT | Push AB, load next 4 bytes into AB | Forth | |
| 243 | $F3 | LSTEP | LSTEP A, @label | Decrement-and-branch (SOB / DBcc style) | Forth | Z |
| 251 | $FB | CNOP | CNOP | Instruction-count / timer probe | System | |
| 252 | $FC | YIELD | YIELD | Yield thread priority | System | Y |
| 253 | $FD | BREAK | BREAK | Breakpoint (reserved) | System |
Dictionary
Detailed notes per instruction. Linkable anchors are provided for cross-reference from the tables above.
#0 $00 LD_IMM
Load register with an immediate value. A = $5 (16/24/32-bit depending on register width).
#1 $01 LD_MEM
Load register from memory (absolute address). A = [$5].
#2 $02 LD_REG
Load register from memory (register-indirect pointer). A = [X].
#3 $03 LD_IMM8
Load byte register with an immediate value. AL = $5.
#4 $04 LD_MEM8
Load byte from memory (absolute). AL = [$5].
#5 $05 LD_REG8
Load byte from memory (register-indirect). AL = [X].
#6 $06 ST_MEM
Store register to memory (absolute). [$10] = A.
#7 $07 ST_REG
Store register to memory (register-indirect). [X] = A.
#9 $09 MOV dst, src
Register-to-register copy ex. Y = A. MOV requires source and destination of equal width; this is enforced at assemble time. A move that changes width is not a MOV – use MOVSX/MOVZX to widen. The assembler rejects a mismatched-width MOV rather than guessing which bytes you meant. Mismatched MOV is undefined at execution time.
#11 $0B PUSH
Push register onto stack. SP -= width; [SP] = A.
#12 $0C POP
Pop stack into register. Y = [SP]; SP += width.
#15 $0F PUSHF
Push flags register onto stack.
#16 $10 POPF
Pop stack into flags register. *
#23 $17 INC
Increment register by 1 (any width). X += 1. Sets Z/N flags.
#24 $18 DEC
Decrement register by 1 (any width). Y -= 1. Sets Z/N flags.
#25 ST_PD / #26 LD_FS / #27 ST_FS
Auto-stepping addressing. ST_PD pre-decrements the pointer then stores (STA [-, BLX]), giving a push-style write. LD_FS loads then advances the pointer (LDA [BLX, +]), and ST_FS stores then advances. These collapse the pointer bookkeeping of a copy loop into the access itself.
#28 $1C MOVSX dst, src / #29 $1D MOVZX dst, src
Move with sign-extend (MOVSX) or zero-extend (MOVZX). The source is read at its own width and written across the full destination, with the high bytes filled from the source's sign bit (MOVSX) or with zero (MOVZX). The destination must be strictly wider than the source; this is checked at assemble time. Both fully define the destination, so unlike a byte ALU op there is no partial-register hazard, and the in-place overlapping case (MOVSX A, AL) is well defined.
These exist to collapse the compiler's extend sequence: without a native sign-extend, widening a signed char to int took three ops (AND/XOR/SUB implementing the (x ^ 0x80) - 0x80 identity). MOVSX does it in one. Integer promotion makes signed widening pervasive in C, so this is a recurring code-size win. MOVZX similarly drops the masking immediate of an AND $FF style zero-extend.
#30 $1E ADD
Add registers. X = X + Y. Updates Z N C V.
#31 $1F SUB
Subtract registers. X = X - Y. Updates Z N C V.
#50 $32 AND
Bitwise AND. dst = dst & src. Updates Z N.
#51 $33 OR
Bitwise OR. dst = dst | src. Updates Z N.
#52 $34 XOR
Bitwise XOR. dst = dst ^ src. Updates Z N.
#53 $35 NOT
Bitwise NOT (invert all bits). reg = ~reg. Updates Z N.
#70 $46 SHL
Shift left. A «= count. Updates Z N C.
#71 $47 SHR
Shift right (logical). A »= count. Updates Z N C.
#80 SHL_X / #81 SHR_X
Shift by a variable count rather than by one. The exact operand form (count in a register vs. immediate) and whether the count is masked should be confirmed against the current emulator handler before relying on this entry.
#90 $64 CMP
Compare registers (subtract, discard result). tmp = A - B. Updates Z N C V.
#91 $5B CMP_IMM
Compare register against immediate. tmp = A - $1. Updates Z N C V.
#100 $64 JMP
Unconditional jump. IP = @label.
#101 $65 JZ
Jump if zero. if (Z) IP = @label.
#102 $66 JNZ
Jump if not zero. if (!Z) IP = @label.
#105 $69 JC
Jump if carry set (alias JAE for unsigned >=). if (C) IP = @label.
#106 $6A JNC
Jump if carry clear (alias JNAE for unsigned <). if (!C) IP = @label.
#130 $82 CALL addr / CALL reg / #131 $83 CALLR reg
CALL pushes the return address (the instruction after the call) and jumps to the target. CALLR takes a register operand and dispatches on its width: 16-bit and 8-bit registers combine with the current bank, 24-bit registers give a full address, and a 32-bit pair (e.g. AB) is narrowed to 24 bits by discarding the high byte. This makes 32-bit register pairs usable as function pointers.
#120-#129 Branches (BZ, BNZ, BC, BNC, BA, BNA, BG, BGE, BL, BLE)
Two-byte relative branches with a signed 8-bit displacement (range -128..+127 from the instruction). They mirror the absolute jumps but encode shorter and stay position-independent, so they are preferred for tight local loops. BC/BAE and BNC/BNAE are aliases. For targets outside +/-127, use the absolute jumps.
#140 $8C MEMCOPY src, dst, reg
Copy reg bytes from src to dst. Handles overlap. A fast MMU operation –
where a string copy is otherwise a tight LDA/STA/JNZ loop, this is a single opcode when you know the length.
#141 $8D SCAN ELM, reg
Scan from ELM for the value in reg. An 8-bit register searches for a byte; a wider register searches for that many bytes. Useful for finding keyword candidates and for computing string length (scan for zero, subtract the start pointer).
#142 $8E CMPC ELM, FLD, C (alias CMPC3)
Non-zero byte compare, useful for strings. Compares up to C characters; C returns either the index of the first mismatch or the matched length. Sets ZERO on a full match; otherwise CARRY distinguishes the -1 / +1 ordering.
CMPC allows early termination when byte_a == 0 – the C-string “begins with” semantics. If both strings reach a terminator at the same position with all prior bytes equal, the loop exits matched (Z=1, C=1). Only byte_a is tested because by that point byte_a == byte_b is already proven, so byte_a == 0 implies byte_b == 0. This lets CMPC do double duty: fixed-length compare and null-terminated strcmp in one instruction.
#143 $8F SKPC ELM, reg / #144 $90 SKPC_IMM ELM, #imm
Anti-scan: advance while the needle is found, stopping at the first non-matching position. A word-or-wider needle steps by needle width. Most often used to skip spaces: SKPC ELM, $20 leaves ELM just past the last space.
#145 INSQUE / #146 INSQUE_PTR / #147 REMQUE
Doubly-linked queue primitives. INSQUE inserts an element (register or pointer form); REMQUE removes one.
REMQUE's Z-flag semantic differs from the VAX and is worth a kernal-side note. The VAX used V for “the queue was already empty” (an error condition). SD-8516 uses Z for “the queue is now empty after this removal” (a state condition):
- VAX: V=1 means “you tried to remove from an empty queue.”
- SD-8516: Z=1 means “you removed the last item; the queue is now empty.”
Both are useful, but they answer different questions.
#148 $94 SCANQUE H, N, O / #149 $95 SCANQUE_IMM H, N, #O
Three operands: head, needle, and offset. SCANQUE_IMM takes the offset as an immediate byte; SCANQUE takes it in a register (allowing offsets beyond +255).
SCANQUE combines queue traversal with a structured-field match in one fetch-execute cycle – the inner loop of every lookup in a hash chain, free list, or PCB table. The immediate form is what you use 99% of the time (the field offset is known when you write CRUD for a struct); the register form exists so generic kernel queue helpers can take “field offset” as a parameter.
#160 SEZ / #161 SEN / #162 SEC / #163 SEV
Set the zero, negative, carry, and overflow flags respectively. Useful when you are certain no intervening operation disturbs the flag – for example, carry survives POP and MOV, so it is a common error/clear channel out of a routine (JC @error).
#164 $A4 SEE
Set the Exception flag. Used by some interrupts and the system to signal an error, but free for your own use otherwise. Think of it as an “Extra” flag.
#165 $A5 SEF
Set the F (“free”) flag. Used for “first-statement” in BASIC; otherwise safe for machine-language use.
#166 $A6 SEB
Set the B (“bonus”) flag. Used for BREAK in BASIC; otherwise safe for machine-language use.
#167 $A7 SEU
Set the User flag. Not used by the system; reserved for the programmer.
#168 $A8 SED
Set Debug. Emits trace messages while on; slows the system considerably. Trace output may be compiled out in some builds.
#169 $A9 SEI
Enable/disable interrupts. With interrupts off, INT will not fire.
#170 $AA SSI
Set the Sound Interrupt flag. Managed by the KERNAL; semi-reserved and may be removed.
#200 $C8 CASE selector, #limit
Inline jump table. Indexes an address from the table that follows the instruction and CALLs it. limit (immediate, 0-255) is the table length. If selector > limit the instruction silently falls through without calling; if you need to detect that, have your handlers return a result code whose default means “no handler ran.” Table format: [addr][addr]….
CASE reads from the instruction stream as data – the inline table is structured bytes, not fetched-and-decoded instructions, the same idea as LD_IMM's operand pushed further. Two derived addresses do the bookkeeping:
IP + 3 * sel: the table slot holding the target (read as a 24-bit address).IP + 3 * lim + 3: the first byte past the table (the return point, and the
IP destination on out-of-bounds).
Both are masked to 24 bits because IP arithmetic wraps in a 24-bit space.
Out-of-bounds sets V and ER and falls through rather than trapping (unlike VAX CASE, which raises an exception). The policy is “error recoverable”: software decides whether to handle it, e.g. a JV some_handler after the CASE. No other flags are touched, which is convenient if the selector came from an arithmetic op whose flags you still want.
A single-entry CASE acts as a CALLZ: call if the register is zero, otherwise not.
#202 $CA CASETAB base, selector, #limit
Jump table held at a base address rather than inline. Indexes an address from the table at base by selector, bounded by limit, and dispatches to it.
#203 $CB CVTAN reg8
Convert ASCII to number. Maps '0'-'Z' to 0-35. Sets Overflow if the character is not a decimal digit (0-9) and Carry if it is not a hex digit (0-15). Also a fast digit test: MOV reg8, AL then CVTAN reg8 lets you branch with JO/JNO (JV/JNV). Designed for zoned decimal, and works for zoned hex.
#204 $CC CVTNA reg8
Convert number to ASCII, the inverse of CVTAN. Maps 0-35 to '0'-'Z'. Sets Overflow if outside 0-9 and Carry if outside 0-15.
#210 LD_IDXI / #211 LD_IDXR / #212 ST_IDXI / #213 ST_IDXR
Indexed load/store: a 24-bit pointer register plus a displacement. The I forms take a signed byte immediate (-128..+127); the R forms take a register offset. These back the compiler's frame-slot and struct-field access ([ptr + disp]). The pointer base must be a 24-bit register; the effective address reads the base at its true width before applying the offset.
#220-#229 PPU accelerators
Delegate a drawing or timing primitive to the PPU: PPIXEL (pixel), PLINE (line), PRECT / PRECTF (rectangle, outline / filled), PCIRC / PCIRCF (circle, outline / filled), PCLEAR (clear screen), PBLIT (tile blit), and PTIMER / PNTIMER (timer functions). Example: PPIXEL X, Y, C draws a pixel at (X, Y) in color C.
#240 $F0 LSTEPM
In-memory loop step, a Forth accelerator. Increments the 4-byte loop counter at [FLX], compares it to the 4-byte limit at [FLX+4], and sets Z when they match. This enables a very tight count-from-X-to-Y loop at the cost of an 8-byte counter/limit in memory:
LDFLX $F000 ; 8 byte loop data start
LDA #1 ; start at 1
STA [FLX] ; write starting counter at first four bytes
LDA #1000 ; go until 1000
STA [FLX+4] ; write finish (until) counter at second four bytes
loop:
LSTEPM ; INC N1, if N1==N2 set Z=1
JNZ @loop
Unlike a DEC loop it counts upward and supports an arbitrary start. The generalized form of this is LSTEP which counts downwards instead.
Only used by Forth. It is recommended to use LSTEP instead; this instruction is a canidate for removal.
#241 $F1 TTOS
Test top of stack (Forth). CD = AB; AB = [ELY]; ELY += 4; Z = (CD == 0).
#242 $F2 LIT
Push literal (Forth). Pushes AB to the stack and loads the next 4 bytes of the instruction stream into AB. Encoding: [242, b0, b1, b2, b3].
#243 $F3 LSTEP reg, @addr
Loop step, the generalized Forth accelerator: a decrement-and-branch in one opcode, inspired by the VAX SOB (subtract one and branch) and the 68000 DBcc.
LDC #1000 ; loop range
loop:
LSTEP C, @loop
