The instruction set, all 174 of them
I’ve never written the instruction set down in one place. It lives in microcode_gen.py as a table of control-word rows, and the assembler and the oracle both read it from there, so the ISA has been whatever that file said. This entry is the readable version. The opcodes, byte lengths and T-state counts below are dumped from that table, so if this page and the machine disagree the page is stale.
The shape of it
8-bit data, 16-bit addresses. Three registers: A is the accumulator and the ALU’s first operand, B is the ALU’s second operand, C is scratch. B:C doubles as an index pair with C as the low half. There’s a 16-bit stack pointer, a program counter, and an 8-LED output latch called OB, which is the only way anything gets out of the machine besides the serial card.
Every instruction is a list of microcode rows in a 24-bit control word across three AT28C64Bs (U9, U15, U23). T0 is the fetch every instruction shares, then the rows run until one carries END. So a 2 T-state instruction is fetch plus one row.
ISA size: 174 opcodes, 82 free. The high nibble of the opcode is its family, which is why I can read a byte off the bus and know roughly what it is.
How to read a row
- Hex is the opcode byte.
- B is total length in bytes, operands included. Multi-byte operands are low byte first.
- T is the T-state count including the fetch.
- Flags:
holdmeans the instruction doesn’t touch them.CZVNmeans all four update.ZNmeans it was a logic op: Z and N are valid, C is undefined because the ‘382 doesn’t specify carry out for logic functions and I’ve never scoped it. Don’t branch on C after AND, OR, XOR, CLR, SET or TST. - Notes call out
clobbers Borclobbers Cwhere a register you didn’t name gets overwritten,settlewhere one T-state is an idle pad, andunrunwhere the microcode is burned but the instruction has never executed on the hardware.
Flags update on any instruction that uses the ALU and hold across everything else. Loads, moves, stores and stack traffic don’t touch them. A Z set by AND still stands at a JNZ two instructions later.
The settle pads are on 23 instructions, added 2026-08-27 for a fault that turned out to be the ground return. Each one costs a T-state. Pulling them and re-running the soak is phase H work.
Addressing modes
| Mode | Syntax | Count | How it works |
|---|---|---|---|
| Implied | CLR | 85 | Operands named by the opcode. |
| Immediate | LDAI n | 25 | One operand byte after the opcode. |
| Absolute | LDA addr | 25 | Two operand bytes, low first, loaded into MAR. |
| Indexed | LDAX | 20 | Address is B:C. C is the low half. No carry from C into B. |
| SP-relative | LDAS | 13 | Address is SP. Can’t share the stack with CALLs while doing this. |
| Memory-indirect | LDAM ptr | 6 | The pointer lives in memory. Operand names where the pointer is. Clobbers C, B survives. |
Memory-indirect instructions are five bytes because the assembler emits the address twice, ptr and ptr+1. MAR loads only from the bus and has no increment, so the microcode reads the low byte through C, re-points MAR at the high byte, parks it in MDR and replays it.
Rules you find out the hard way
- Set SP first. The ‘169 counters have no clear, so SP is random at power-up.
LXISPbefore any push, pop, CALL or SP-relative access. Nothing warns you. - OB is never cleared. The output latch has no reset and holds whatever the last program left. Write a known-wrong value first (
LDAI 0xFF; OUT) so a missing result shows up as missing instead of stale. - RET destroys C. C holds half the return address while RET rebuilds it. Wrap it in
PUSHC/POPCif it has to survive a call. - One branch polarity. Every conditional is “taken if the flag is zero”. To branch on a flag being set, jump over a JMP. Six bytes.
- Carry is NOT-borrow. SUB runs with carry-in set, so C=1 means A >= B unsigned and C=0 means A < B.
JNCafterCMPreads as “jump if less than”. - C is the low half of B:C. Always. A swapped pair is an off-by-256 that a write-then-read through the same pointer won’t show. Read the cell back by absolute address.
0x0x Control
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 00 | NOP | 1 | 2 | nothing | hold | 0x00 is what blank RAM reads, so a PC that runs into it slides instead of stopping. First ran 2026-09-08. |
| 01 | RST | 1 | 2 | PC <- 0x0000 | hold | unrun. Clears the PC and nothing else. |
0x1x Immediate loads
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 11 | LDAI n | 2 | 2 | A <- n | hold | |
| 12 | LDBI n | 2 | 2 | B <- n | hold | The 0xCx forms do LDBI plus ALU in one instruction. |
| 13 | LDCI n | 2 | 2 | C <- n | hold | Doesn’t survive a CALL. |
| 14 | LXISP addr | 3 | 3 | SP <- addr | hold | Required before any stack access. |
| 15 | LXIL n | 2 | 2 | SP low <- n | hold | |
| 16 | LXIH n | 2 | 2 | SP high <- n | hold |
0x2x Memory, absolute and SP-relative
A peripheral card is memory. The I/O window is 0x4000-0x7FFF in 2K slots, so LDA 0x4000 reads the DIP switch.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 21 | LDA addr | 3 | 4 | A <- [addr] | hold | |
| 22 | STA addr | 3 | 4 | [addr] <- A | hold | |
| 23 | LDB addr | 3 | 4 | B <- [addr] | hold | |
| 24 | LDC addr | 3 | 4 | C <- [addr] | hold | |
| 25 | STB addr | 3 | 4 | [addr] <- B | hold | |
| 26 | STC addr | 3 | 4 | [addr] <- C | hold | |
| 27 | LDAS | 1 | 4 | A <- [SP] | hold | Read a stack slot without popping. |
| 28 | STAS | 1 | 4 | [SP] <- A | hold | |
| 29 | LDBS | 1 | 4 | B <- [SP] | hold | |
| 2A | STBS | 1 | 4 | [SP] <- B | hold | |
| 2B | LDCS | 1 | 4 | C <- [SP] | hold | |
| 2C | STCS | 1 | 4 | [SP] <- C | hold |
0x3x Flow
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 31 | JMP addr | 3 | 4 | PC <- addr | hold | |
| 32 | JNZ addr | 3 | 4 | jump if Z = 0 | hold | |
| 33 | CALL addr | 3 | 12 | push return, PC <- addr | hold | Pushes PC+1, the address of its own operand bytes. RET steps over them. |
| 34 | RET | 1 | 14 | pop PC, step over 2 bytes | hold | clobbers C. |
| 35 | JMPX | 1 | 4 | PC <- B:C | hold | Computed jump. |
| 36 | JMPSP | 1 | 4 | PC <- SP | hold | |
| 37 | JNC addr | 3 | 4 | jump if C = 0 | hold | After CMP: jump if A < B unsigned. |
| 38 | JZX | 1 | 4 | jump to B:C if Z = 0 | hold | |
| 39 | JCX | 1 | 4 | jump to B:C if C = 0 | hold | |
| 3A | JMPM ptr | 5 | 10 | PC <- [ptr] | hold | clobbers C. Vector table in RAM. |
| 3B | JZM ptr | 5 | 10 | jump via [ptr] if Z = 0 | hold | clobbers C. |
| 3C | JCM ptr | 5 | 10 | jump via [ptr] if C = 0 | hold | clobbers C. |
| 3D | LDAM ptr | 5 | 10 | A <- [[ptr]] | hold | clobbers C. |
| 3E | LDBM ptr | 5 | 10 | B <- [[ptr]] | hold | clobbers C. |
| 3F | STAM ptr | 5 | 10 | [[ptr]] <- A | hold | clobbers C. |
0x4x ALU
Two 74F382s with ripple carry. A and B feed the ALU from shadow latches that reload on every A or B load, so you never load them yourself. SHL, INR, DCR and NOT get to the ALU by staging a constant in B, so all four destroy it.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 40 | BIT | 1 | 2 | A AND B, discarded | ZN | Mask test. |
| 41 | ADD | 1 | 2 | A <- A + B | CZVN | |
| 42 | SUB | 1 | 2 | A <- A - B | CZVN | |
| 43 | AND | 1 | 2 | A <- A AND B | ZN | |
| 44 | OR | 1 | 2 | A <- A OR B | ZN | |
| 45 | XOR | 1 | 2 | A <- A XOR B | ZN | |
| 46 | CLR | 1 | 2 | A <- 0x00 | ZN | One byte, shorter than LDAI 0. |
| 47 | SET | 1 | 2 | A <- 0xFF | ZN | |
| 48 | BSUB | 1 | 2 | A <- B - A | CZVN | |
| 49 | CMP | 1 | 2 | A - B, discarded | CZVN | Flags update even with no destination. |
| 4A | CMPB | 1 | 2 | B - A, discarded | CZVN | |
| 4B | TST | 1 | 2 | A OR A, discarded | ZN | Zero test on A alone. |
| 4C | SHL | 1 | 4 | A <- A + A | CZVN | settle, clobbers B. |
| 4D | INR | 1 | 4 | A <- A + 1 | CZVN | settle, clobbers B. B staged with 0xFF, A - 0xFF. |
| 4E | DCR | 1 | 4 | A <- A - 1 | CZVN | settle, clobbers B. A + 0xFF. |
| 4F | NOT | 1 | 4 | A <- NOT A | ZN | settle, clobbers B. A XOR 0xFF. |
0x5x Output
OB latches whatever is on the internal bus. Sourcing it from A was a microcode convention, so anything that can drive the bus can go to the display. Every variant except OUT and OUTI is unrun: the self-test reads OB, so it can’t judge an instruction whose only effect is writing OB.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 51 | OUT | 1 | 2 | OB <- A | hold | |
| 52 | OUTB | 1 | 2 | OB <- B | hold | unrun |
| 53 | OUTC | 1 | 2 | OB <- C | hold | unrun |
| 54 | OUTSPL | 1 | 2 | OB <- SP low | hold | unrun |
| 55 | OUTSPH | 1 | 2 | OB <- SP high | hold | unrun |
| 56 | OUTPCL | 1 | 2 | OB <- PC low | hold | unrun |
| 57 | OUTPCH | 1 | 2 | OB <- PC high | hold | unrun |
| 58 | OUTI n | 2 | 2 | OB <- n | hold | |
| 59 | OUTM addr | 3 | 4 | OB <- [addr] | hold | unrun |
| 5A | OUTMX | 1 | 4 | OB <- [B:C] | hold | unrun |
| 5B | OUTMS | 1 | 4 | OB <- [SP] | hold | unrun |
0x6x Stack and pointers
Empty-descending. SP points at the next free slot. Push stores then decrements, pop increments then loads. Every stack access copies SP into MAR first, which is where the extra two T-states go.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 60 | PUSHPCH | 1 | 5 | push PC high | hold | unrun |
| 61 | PUSHA | 1 | 5 | [SP] <- A, SP– | hold | |
| 62 | POPA | 1 | 5 | SP++, A <- [SP] | hold | |
| 63 | PUSHB | 1 | 5 | [SP] <- B, SP– | hold | |
| 64 | POPB | 1 | 5 | SP++, B <- [SP] | hold | |
| 65 | PUSHC | 1 | 5 | [SP] <- C, SP– | hold | |
| 66 | POPC | 1 | 5 | SP++, C <- [SP] | hold | |
| 67 | INXSP | 1 | 2 | SP <- SP + 1 | hold | The only 16-bit increment in the machine. |
| 68 | DCXSP | 1 | 2 | SP <- SP - 1 | hold | |
| 69 | SPHL | 1 | 3 | SP <- B:C | hold | |
| 6A | HLSP | 1 | 3 | B:C <- SP | hold | |
| 6B | PUSHSPL | 1 | 5 | push SP low | hold | |
| 6C | POPSPL | 1 | 5 | pop SP low | hold | |
| 6D | PUSHSPH | 1 | 5 | push SP high | hold | |
| 6E | POPSPH | 1 | 5 | pop SP high | hold | |
| 6F | PUSHPCL | 1 | 5 | push PC low | hold |
0x7x Indexed through B:C
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 71 | LDAX | 1 | 4 | A <- [B:C] | hold | |
| 72 | STAX | 1 | 4 | [B:C] <- A | hold | |
| 73 | LDBX | 1 | 4 | B <- [B:C] | hold | Overwrites the pointer’s high half. |
| 74 | STBX | 1 | 4 | [B:C] <- B | hold | |
| 75 | LDCX | 1 | 4 | C <- [B:C] | hold | Overwrites the pointer’s low half. Linked-list walk. |
| 76 | STCX | 1 | 4 | [B:C] <- C | hold | |
| 77 | MVIX n | 2 | 6 | [B:C] <- n | hold | Fill through the pointer without touching A. |
| 78 | STADDX | 1 | 4 | [B:C] <- A + B | CZVN | |
| 79 | STSUBX | 1 | 4 | [B:C] <- A - B | CZVN | |
| 7A | STBSUBX | 1 | 4 | [B:C] <- B - A | CZVN | |
| 7B | STANDX | 1 | 4 | [B:C] <- A AND B | ZN | |
| 7C | STORX | 1 | 4 | [B:C] <- A OR B | ZN | |
| 7D | STXORX | 1 | 4 | [B:C] <- A XOR B | ZN |
0x8x Register and pointer moves
One row past the fetch. Cheapest instructions in the machine.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| 81 | MOVAB | 1 | 2 | A <- B | hold | |
| 82 | MOVAC | 1 | 2 | A <- C | hold | C can’t be an ALU operand, so this is how C reaches the ALU. |
| 83 | MOVBA | 1 | 2 | B <- A | hold | |
| 84 | MOVBC | 1 | 2 | B <- C | hold | |
| 85 | MOVCA | 1 | 2 | C <- A | hold | |
| 86 | MOVCB | 1 | 2 | C <- B | hold | |
| 87 | MOVASPL | 1 | 2 | A <- SP low | hold | |
| 88 | MOVASPH | 1 | 2 | A <- SP high | hold | |
| 89 | MOVSPLA | 1 | 2 | SP low <- A | hold | |
| 8A | MOVSPHA | 1 | 2 | SP high <- A | hold | |
| 8B | MOVAPCL | 1 | 2 | A <- PC low | hold | Tapped at the counters’ Q outputs. |
| 8C | MOVAPCH | 1 | 2 | A <- PC high | hold | unrun |
0x9x Reserved
Held empty for opcodes that need hardware the machine doesn’t have yet: SHR, MOV A,FLAGS, ADC, SBB, and the branch families that need CW22/CW23 landed.
0xAx Pointer save and restore, immediate stores, computed output
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| A0 | LDSPL addr | 3 | 4 | SP low <- [addr] | hold | |
| A1 | LDSPH addr | 3 | 4 | SP high <- [addr] | hold | |
| A2 | STSPL addr | 3 | 4 | [addr] <- SP low | hold | |
| A3 | STSPH addr | 3 | 4 | [addr] <- SP high | hold | |
| A4 | STPCL addr | 3 | 4 | [addr] <- PC low | hold | unrun |
| A5 | STPCH addr | 3 | 4 | [addr] <- PC high | hold | unrun |
| A6 | MVI addr, n | 4 | 6 | [addr] <- n | hold | Got a settle row on 2026-09-05 after PROG_test5 caught it writing a mix of the old byte and the new one. |
| A7 | MVIS n | 2 | 6 | [SP] <- n | hold | |
| A8 | OUTADD | 1 | 2 | OB <- A + B | CZVN | unrun |
| A9 | OUTSUB | 1 | 2 | OB <- A - B | CZVN | unrun |
| AA | OUTBSUB | 1 | 2 | OB <- B - A | CZVN | unrun |
| AB | OUTAND | 1 | 2 | OB <- A AND B | ZN | unrun |
| AC | OUTOR | 1 | 2 | OB <- A OR B | ZN | unrun |
| AD | OUTXOR | 1 | 2 | OB <- A XOR B | ZN | unrun |
0xBx B and C to and from the pointers
With the ALU these let a routine do arithmetic on SP, or borrow SP as a second index register and hand it back.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| B0 | MOVBSPL | 1 | 2 | B <- SP low | hold | |
| B1 | MOVBSPH | 1 | 2 | B <- SP high | hold | |
| B2 | MOVCSPL | 1 | 2 | C <- SP low | hold | |
| B3 | MOVCSPH | 1 | 2 | C <- SP high | hold | |
| B4 | MOVSPLB | 1 | 2 | SP low <- B | hold | |
| B5 | MOVSPHB | 1 | 2 | SP high <- B | hold | |
| B6 | MOVSPLC | 1 | 2 | SP low <- C | hold | |
| B7 | MOVSPHC | 1 | 2 | SP high <- C | hold | |
| B8 | MOVBPCL | 1 | 2 | B <- PC low | hold | |
| B9 | MOVBPCH | 1 | 2 | B <- PC high | hold | unrun |
| BA | MOVCPCL | 1 | 2 | C <- PC low | hold | |
| BB | MOVCPCH | 1 | 2 | C <- PC high | hold | unrun |
0xCx ALU with an immediate
Two bytes where LDBI plus the ALU op costs three. The constant comes in through B, so every one of these overwrites B unless B is the destination. Every row here carries a settle pad, which is why they’re 4 T-states and not 3.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| C0 | ADI n | 2 | 4 | A <- A + n | CZVN | settle, clobbers B |
| C1 | ADI_B n | 2 | 4 | B <- A + n | CZVN | settle |
| C2 | ADI_C n | 2 | 4 | C <- A + n | CZVN | settle, clobbers B |
| C3 | SUI n | 2 | 4 | A <- A - n | CZVN | settle, clobbers B |
| C4 | SUI_B n | 2 | 4 | B <- A - n | CZVN | settle |
| C5 | SUI_C n | 2 | 4 | C <- A - n | CZVN | settle, clobbers B |
| C6 | BSUI n | 2 | 4 | A <- n - A | CZVN | settle, clobbers B |
| C7 | BSUI_B n | 2 | 4 | B <- n - A | CZVN | settle |
| C8 | BSUI_C n | 2 | 4 | C <- n - A | CZVN | settle, clobbers B |
| C9 | ANI n | 2 | 4 | A <- A AND n | ZN | settle, clobbers B |
| CA | ANI_B n | 2 | 4 | B <- A AND n | ZN | settle |
| CB | ANI_C n | 2 | 4 | C <- A AND n | ZN | settle, clobbers B |
| CC | ORI n | 2 | 4 | A <- A OR n | ZN | settle, clobbers B |
| CD | ORI_B n | 2 | 4 | B <- A OR n | ZN | settle |
| CE | ORI_C n | 2 | 4 | C <- A OR n | ZN | settle, clobbers B |
| CF | XRI n | 2 | 4 | A <- A XOR n | ZN | settle, clobbers B |
0xDx Immediate ALU continued, and results into B or C
Where the result lands decides what survives. Into B, it becomes the next op’s second operand. Into C, neither operand is touched.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| D0 | XRI_B n | 2 | 4 | B <- A XOR n | ZN | settle |
| D1 | XRI_C n | 2 | 4 | C <- A XOR n | ZN | settle, clobbers B |
| D2 | CPI n | 2 | 4 | A - n, discarded | CZVN | settle, clobbers B. CPI n; JNC is compare-and-branch in four bytes. |
| D3 | ADD_B | 1 | 2 | B <- A + B | CZVN | |
| D4 | ADD_C | 1 | 2 | C <- A + B | CZVN | |
| D5 | SUB_B | 1 | 2 | B <- A - B | CZVN | |
| D6 | SUB_C | 1 | 2 | C <- A - B | CZVN | |
| D7 | BSUB_B | 1 | 2 | B <- B - A | CZVN | |
| D8 | BSUB_C | 1 | 2 | C <- B - A | CZVN | |
| D9 | AND_B | 1 | 2 | B <- A AND B | ZN | |
| DA | AND_C | 1 | 2 | C <- A AND B | ZN | |
| DB | OR_B | 1 | 2 | B <- A OR B | ZN | |
| DC | OR_C | 1 | 2 | C <- A OR B | ZN | |
| DD | XOR_B | 1 | 2 | B <- A XOR B | ZN | |
| DE | XOR_C | 1 | 2 | C <- A XOR B | ZN | |
| DF | CPX | 1 | 2 | A XOR B, discarded | ZN | Equality test, both operands intact. |
0xEx Compute and store
A MAR load and an ALU row that writes RAM. The result never sits in a register.
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| E0 | STADD addr | 3 | 4 | [addr] <- A + B | CZVN | |
| E1 | STSUB addr | 3 | 4 | [addr] <- A - B | CZVN | |
| E2 | STBSUB addr | 3 | 4 | [addr] <- B - A | CZVN | |
| E3 | STAND addr | 3 | 4 | [addr] <- A AND B | ZN | |
| E4 | STOR addr | 3 | 4 | [addr] <- A OR B | ZN | |
| E5 | STXOR addr | 3 | 4 | [addr] <- A XOR B | ZN | |
| E6 | STADDS | 1 | 4 | [SP] <- A + B | CZVN | |
| E7 | STSUBS | 1 | 4 | [SP] <- A - B | CZVN | |
| E8 | STBSUBS | 1 | 4 | [SP] <- B - A | CZVN | |
| E9 | STANDS | 1 | 4 | [SP] <- A AND B | ZN | |
| EA | STORS | 1 | 4 | [SP] <- A OR B | ZN | |
| EB | STXORS | 1 | 4 | [SP] <- A XOR B | ZN |
0xFx Halt
| Hex | Mnemonic | B | T | Operation | Flags | Notes |
|---|---|---|---|---|---|---|
| FF | HALT | 1 | 2 | T-counter stops | hold | 0xFF so an erased EEPROM halts instead of running. Same byte fills unused microcode slots and parks the bus. |
Opcode map
256 slots. 174 assigned, 16 reserved (family 9), 66 free. Free slots are the gaps you can see in the tables above: 0x02-0x10, 0x17-0x20, 0x2D-0x30, 0x50, 0x5C-0x5F, 0x70, 0x7E-0x80, 0x8D-0x9F, 0xAE-0xAF, 0xBC-0xBF, 0xEC-0xFE.
What has actually run
The tables describe what’s burned. What has been witnessed on the hardware is a narrower claim.
- Burned images in the repo: U9 0xC52B, U15 0xDCD1, U23 0xCB8E, CRC-16 pinned in the host tests so a reburn is deliberate. The U15 in the socket is 0x45F9, which adds an OUT settle row from 2026-09-08 that fixed nothing and comes back out on the next burn.
PROG_isaruns 147 subtests, one per instruction it can observe through OB, and reads 0xB4 at 1.024 MHz.PROG_isasoakrepeats them 64 times per run and read 0x00 on 50 runs in a row, 470,400 subtest executions.- RET, OUT, OUTI and HALT have no subtest because the harness is built from them. Every coverage image since phase C runs all four. NOP first ran on 2026-09-08 in the OUT-before-RET chase. That’s 152 instructions witnessed.
- 22 have never executed: RST, MOVAPCH, MOVBPCH, MOVCPCH, PUSHPCH, STPCL, STPCH, and the fifteen OUT variants (OUTB, OUTC, OUTSPL, OUTSPH, OUTPCL, OUTPCH, OUTM, OUTMX, OUTMS, OUTADD, OUTSUB, OUTBSUB, OUTAND, OUTOR, OUTXOR). The OUT ones can’t be self-tested because their only effect is the display. The rest just don’t have an image yet.
Next
- Phase H: pull the 23 settle pads and the OUT settle row, reburn U9 and U15, rerun
PROG_isasoak. If it stays at 0x00 the pads were never needed. - Scope the ‘382 carry out after a logic op so the ZN rows can say what C actually does.
- Decide which of the fifteen OUT variants earn their slot. Something has to watch the LEDs to test them, so probably a camera or a second latch on the bus.