Overhaul the Intel 4004 instruction set to match the real opcode map, adding missing instructions (NOP, FIM, FIN, JIN, ISZ, KBP, DCL, WR0-3, RD0-3) and refining two-byte instruction handling with proper M1/M2 data phases. Rewrite the CPU fetch-execute cycle to correctly process two-byte opcodes across separate clock cycles. Improve Intel 4001 ROM timing with M1/M2 nibble phase tracking and an A3 wait counter so the ROM drives data only when the CPU expects it. Fix state-machine transition helpers in Intel 4002 and 4003 to capture the old state before processing events, preventing incorrect exit/entry callbacks. Add non-propagating pin driver methods to prevent deadlocks caused by bidirectional pin connections during signal propagation. Add Fibonacci integration test and opcode fix plan document. Update all README files to reflect the new instruction set.
1251 lines
45 KiB
Markdown
1251 lines
45 KiB
Markdown
# Intel 4004 Opcode Table Fix — Design Document
|
|
|
|
## 1. Overview
|
|
|
|
The current implementation in [`intel_4004.rs`](src/components/cpu/intel_4004.rs) has a **completely incorrect opcode mapping**. The decoder assigns wrong instructions to nearly every opcode range. This document provides the authoritative reference for rewriting the opcode decoder, instruction enum, and execution logic.
|
|
|
|
### Current Issues Summary
|
|
|
|
| Opcode Range | Current Mapping | Correct Mapping |
|
|
|---|---|---|
|
|
| 0x00-0x0F | LD/XCH by register | NOP + LD R (duplicate of 0xA0-0xAF) |
|
|
| 0x10-0x1F | ADD/SUB by register | JCN condition, address (2-byte) |
|
|
| 0x20-0x2F | ADDC/SBC by register | FIM/SRC by register pair |
|
|
| 0x30-0x3F | JCN condition | FIN/JIN by register pair |
|
|
| 0x40-0x4F | LDM immediate | JUN address (2-byte) |
|
|
| 0x50-0x5F | WRM/WMP | JMS address (2-byte) |
|
|
| 0x60-0x6F | WRR/WPM | INC R |
|
|
| 0x70-0x7F | Individual I/O instructions | ISZ R, address (2-byte) |
|
|
| 0x80-0x8F | JUN high nibble | ADD R with carry |
|
|
| 0x90-0x9F | JUN low nibble | SUB R with borrow |
|
|
| 0xA0-0xAF | JMS high nibble | LD R (into accumulator) |
|
|
| 0xB0-0xBF | JMS low nibble | XCH R (exchange with register) |
|
|
| 0xC0-0xCF | INC R | BBL data (return from subroutine + load ACC) |
|
|
| 0xD0-0xDF | INC R | LDM data (load immediate into ACC) |
|
|
| 0xE0-0xEF | Not mapped | I/O and RAM instructions |
|
|
| 0xF0-0xFC | Duplicates of accumulator ops | Accumulator group instructions |
|
|
| 0xFD-0xFF | — | Invalid / unused |
|
|
|
|
---
|
|
|
|
## 2. Complete Opcode Table (All 256 Entries)
|
|
|
|
### Legend
|
|
- **Size**: 1 = one-byte (8 cycles), 2 = two-byte (16 cycles)
|
|
- **R** = register number 0-15
|
|
- **r** = register pair number 0-7 (even register index)
|
|
|
|
### 2.1 NOP and Machine Instructions
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x00 | NOP | 1 | No operation |
|
|
|
|
### 2.2 JCN — Jump Conditional (2-byte)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x10 | JCN 0, addr | 2 | Jump if condition 0 (always false — never jumps) |
|
|
| 0x11 | JCN 1, addr | 2 | Jump if TEST = 1 |
|
|
| 0x12 | JCN 2, addr | 2 | Jump if Carry = 1 |
|
|
| 0x13 | JCN 3, addr | 2 | Jump if Carry=1 OR TEST=1 |
|
|
| 0x14 | JCN 4, addr | 2 | Jump if ACC = 0 |
|
|
| 0x15 | JCN 5, addr | 2 | Jump if ACC=0 OR TEST=1 |
|
|
| 0x16 | JCN 6, addr | 2 | Jump if ACC=0 OR Carry=1 |
|
|
| 0x17 | JCN 7, addr | 2 | Jump if ACC=0 OR Carry=1 OR TEST=1 |
|
|
| 0x18 | JCN 8, addr | 2 | Jump unconditional (invert of always false) |
|
|
| 0x19 | JCN 9, addr | 2 | Jump if TEST = 0 |
|
|
| 0x1A | JCN A, addr | 2 | Jump if Carry = 0 |
|
|
| 0x1B | JCN B, addr | 2 | Jump if Carry=0 AND TEST=0 |
|
|
| 0x1C | JCN C, addr | 2 | Jump if ACC ≠ 0 |
|
|
| 0x1D | JCN D, addr | 2 | Jump if ACC≠0 AND TEST=0 |
|
|
| 0x1E | JCN E, addr | 2 | Jump if Carry=0 AND ACC≠0 |
|
|
| 0x1F | JCN F, addr | 2 | Jump if Carry=0 AND ACC≠0 AND TEST=0 |
|
|
|
|
**Byte 2**: 8-bit address within current 256-byte page.
|
|
**Full address**: `(PC & 0xF00) | byte2`
|
|
|
|
### 2.3 FIM / SRC — Register Pair Operations
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x20 | FIM R0, data | 2 | Fetch immediate to R0/R1 |
|
|
| 0x21 | SRC R0 | 1 | Send register control from R0/R1 |
|
|
| 0x22 | FIM R2, data | 2 | Fetch immediate to R2/R3 |
|
|
| 0x23 | SRC R1 | 1 | Send register control from R2/R3 |
|
|
| 0x24 | FIM R4, data | 2 | Fetch immediate to R4/R5 |
|
|
| 0x25 | SRC R2 | 1 | Send register control from R4/R5 |
|
|
| 0x26 | FIM R6, data | 2 | Fetch immediate to R6/R7 |
|
|
| 0x27 | SRC R3 | 1 | Send register control from R6/R7 |
|
|
| 0x28 | FIM R8, data | 2 | Fetch immediate to R8/R9 |
|
|
| 0x29 | SRC R4 | 1 | Send register control from R8/R9 |
|
|
| 0x2A | FIM R10, data | 2 | Fetch immediate to R10/R11 |
|
|
| 0x2B | SRC R5 | 1 | Send register control from R10/R11 |
|
|
| 0x2C | FIM R12, data | 2 | Fetch immediate to R12/R13 |
|
|
| 0x2D | SRC R6 | 1 | Send register control from R12/R13 |
|
|
| 0x2E | FIM R14, data | 2 | Fetch immediate to R14/R15 |
|
|
| 0x2F | SRC R7 | 1 | Send register control from R14/R15 |
|
|
|
|
**FIM Byte 2**: 8-bit data. High nibble → first register (even), Low nibble → second register (odd).
|
|
|
|
### 2.4 FIN / JIN — Register Pair Indirect Operations
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x30 | FIN R0 | 2 | Fetch indirect to R0/R1 from ROM |
|
|
| 0x31 | JIN R0 | 1 | Jump indirect to address in R0/R1 |
|
|
| 0x32 | FIN R2 | 2 | Fetch indirect to R2/R3 from ROM |
|
|
| 0x33 | JIN R1 | 1 | Jump indirect to address in R2/R3 |
|
|
| 0x34 | FIN R4 | 2 | Fetch indirect to R4/R5 from ROM |
|
|
| 0x35 | JIN R2 | 1 | Jump indirect to address in R4/R5 |
|
|
| 0x36 | FIN R6 | 2 | Fetch indirect to R6/R7 from ROM |
|
|
| 0x37 | JIN R3 | 1 | Jump indirect to address in R6/R7 |
|
|
| 0x38 | FIN R8 | 2 | Fetch indirect to R8/R9 from ROM |
|
|
| 0x39 | JIN R4 | 1 | Jump indirect to address in R8/R9 |
|
|
| 0x3A | FIN R10 | 2 | Fetch indirect to R10/R11 from ROM |
|
|
| 0x3B | JIN R5 | 1 | Jump indirect to address in R10/R11 |
|
|
| 0x3C | FIN R12 | 2 | Fetch indirect to R12/R13 from ROM |
|
|
| 0x3D | JIN R6 | 1 | Jump indirect to address in R12/R13 |
|
|
| 0x3E | FIN R14 | 2 | Fetch indirect to R14/R15 from ROM |
|
|
| 0x3F | JIN R7 | 1 | Jump indirect to address in R14/R15 |
|
|
|
|
**FIN**: Reads ROM at address pointed to by register pair, stores result in register pair.
|
|
**JIN**: Sets PC to 12-bit address formed by register pair (even reg = high 6 bits, odd reg = low 6 bits). See Section 8.
|
|
|
|
### 2.5 JUN — Jump Unconditional (2-byte)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x40 | JUN addr | 2 | Jump to address (high nibble 0) |
|
|
| 0x41 | JUN addr | 2 | Jump to address (high nibble 1) |
|
|
| 0x42 | JUN addr | 2 | Jump to address (high nibble 2) |
|
|
| 0x43 | JUN addr | 2 | Jump to address (high nibble 3) |
|
|
| 0x44 | JUN addr | 2 | Jump to address (high nibble 4) |
|
|
| 0x45 | JUN addr | 2 | Jump to address (high nibble 5) |
|
|
| 0x46 | JUN addr | 2 | Jump to address (high nibble 6) |
|
|
| 0x47 | JUN addr | 2 | Jump to address (high nibble 7) |
|
|
| 0x48 | JUN addr | 2 | Jump to address (high nibble 8) |
|
|
| 0x49 | JUN addr | 2 | Jump to address (high nibble 9) |
|
|
| 0x4A | JUN addr | 2 | Jump to address (high nibble A) |
|
|
| 0x4B | JUN addr | 2 | Jump to address (high nibble B) |
|
|
| 0x4C | JUN addr | 2 | Jump to address (high nibble C) |
|
|
| 0x4D | JUN addr | 2 | Jump to address (high nibble D) |
|
|
| 0x4E | JUN addr | 2 | Jump to address (high nibble E) |
|
|
| 0x4F | JUN addr | 2 | Jump to address (high nibble F) |
|
|
|
|
**Full address**: `((byte1 & 0x0F) << 8) | byte2`
|
|
|
|
### 2.6 JMS — Jump to Subroutine (2-byte)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x50 | JMS addr | 2 | Jump to subroutine (high nibble 0) |
|
|
| 0x51 | JMS addr | 2 | Jump to subroutine (high nibble 1) |
|
|
| 0x52 | JMS addr | 2 | Jump to subroutine (high nibble 2) |
|
|
| 0x53 | JMS addr | 2 | Jump to subroutine (high nibble 3) |
|
|
| 0x54 | JMS addr | 2 | Jump to subroutine (high nibble 4) |
|
|
| 0x55 | JMS addr | 2 | Jump to subroutine (high nibble 5) |
|
|
| 0x56 | JMS addr | 2 | Jump to subroutine (high nibble 6) |
|
|
| 0x57 | JMS addr | 2 | Jump to subroutine (high nibble 7) |
|
|
| 0x58 | JMS addr | 2 | Jump to subroutine (high nibble 8) |
|
|
| 0x59 | JMS addr | 2 | Jump to subroutine (high nibble 9) |
|
|
| 0x5A | JMS addr | 2 | Jump to subroutine (high nibble A) |
|
|
| 0x5B | JMS addr | 2 | Jump to subroutine (high nibble B) |
|
|
| 0x5C | JMS addr | 2 | Jump to subroutine (high nibble C) |
|
|
| 0x5D | JMS addr | 2 | Jump to subroutine (high nibble D) |
|
|
| 0x5E | JMS addr | 2 | Jump to subroutine (high nibble E) |
|
|
| 0x5F | JMS addr | 2 | Jump to subroutine (high nibble F) |
|
|
|
|
**Full address**: `((byte1 & 0x0F) << 8) | byte2`
|
|
Pushes current PC to stack before jumping.
|
|
|
|
### 2.7 INC — Increment Register
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x60 | INC R0 | 1 | Increment register 0 |
|
|
| 0x61 | INC R1 | 1 | Increment register 1 |
|
|
| 0x62 | INC R2 | 1 | Increment register 2 |
|
|
| 0x63 | INC R3 | 1 | Increment register 3 |
|
|
| 0x64 | INC R4 | 1 | Increment register 4 |
|
|
| 0x65 | INC R5 | 1 | Increment register 5 |
|
|
| 0x66 | INC R6 | 1 | Increment register 6 |
|
|
| 0x67 | INC R7 | 1 | Increment register 7 |
|
|
| 0x68 | INC R8 | 1 | Increment register 8 |
|
|
| 0x69 | INC R9 | 1 | Increment register 9 |
|
|
| 0x6A | INC R10 | 1 | Increment register 10 |
|
|
| 0x6B | INC R11 | 1 | Increment register 11 |
|
|
| 0x6C | INC R12 | 1 | Increment register 12 |
|
|
| 0x6D | INC R13 | 1 | Increment register 13 |
|
|
| 0x6E | INC R14 | 1 | Increment register 14 |
|
|
| 0x6F | INC R15 | 1 | Increment register 15 |
|
|
|
|
Register = `opcode & 0x0F`. Wraps from 15 to 0 (4-bit).
|
|
|
|
### 2.8 ISZ — Increment and Skip if Not Zero (2-byte)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x70 | ISZ R0, addr | 2 | Increment R0, jump if not zero |
|
|
| 0x71 | ISZ R1, addr | 2 | Increment R1, jump if not zero |
|
|
| 0x72 | ISZ R2, addr | 2 | Increment R2, jump if not zero |
|
|
| 0x73 | ISZ R3, addr | 2 | Increment R3, jump if not zero |
|
|
| 0x74 | ISZ R4, addr | 2 | Increment R4, jump if not zero |
|
|
| 0x75 | ISZ R5, addr | 2 | Increment R5, jump if not zero |
|
|
| 0x76 | ISZ R6, addr | 2 | Increment R6, jump if not zero |
|
|
| 0x77 | ISZ R7, addr | 2 | Increment R7, jump if not zero |
|
|
| 0x78 | ISZ R8, addr | 2 | Increment R8, jump if not zero |
|
|
| 0x79 | ISZ R9, addr | 2 | Increment R9, jump if not zero |
|
|
| 0x7A | ISZ R10, addr | 2 | Increment R10, jump if not zero |
|
|
| 0x7B | ISZ R11, addr | 2 | Increment R11, jump if not zero |
|
|
| 0x7C | ISZ R12, addr | 2 | Increment R12, jump if not zero |
|
|
| 0x7D | ISZ R13, addr | 2 | Increment R13, jump if not zero |
|
|
| 0x7E | ISZ R14, addr | 2 | Increment R14, jump if not zero |
|
|
| 0x7F | ISZ R15, addr | 2 | Increment R15, jump if not zero |
|
|
|
|
Register = `opcode & 0x0F`.
|
|
**Byte 2**: 8-bit address within current page.
|
|
**Full address**: `(PC & 0xF00) | byte2`
|
|
|
|
### 2.9 ADD — Add Register to Accumulator with Carry
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x80 | ADD R0 | 1 | Add R0 to ACC with carry |
|
|
| 0x81 | ADD R1 | 1 | Add R1 to ACC with carry |
|
|
| 0x82 | ADD R2 | 1 | Add R2 to ACC with carry |
|
|
| 0x83 | ADD R3 | 1 | Add R3 to ACC with carry |
|
|
| 0x84 | ADD R4 | 1 | Add R4 to ACC with carry |
|
|
| 0x85 | ADD R5 | 1 | Add R5 to ACC with carry |
|
|
| 0x86 | ADD R6 | 1 | Add R6 to ACC with carry |
|
|
| 0x87 | ADD R7 | 1 | Add R7 to ACC with carry |
|
|
| 0x88 | ADD R8 | 1 | Add R8 to ACC with carry |
|
|
| 0x89 | ADD R9 | 1 | Add R9 to ACC with carry |
|
|
| 0x8A | ADD R10 | 1 | Add R10 to ACC with carry |
|
|
| 0x8B | ADD R11 | 1 | Add R11 to ACC with carry |
|
|
| 0x8C | ADD R12 | 1 | Add R12 to ACC with carry |
|
|
| 0x8D | ADD R13 | 1 | Add R13 to ACC with carry |
|
|
| 0x8E | ADD R14 | 1 | Add R14 to ACC with carry |
|
|
| 0x8F | ADD R15 | 1 | Add R15 to ACC with carry |
|
|
|
|
`ACC = (ACC + R + Carry) & 0x0F`, `Carry = (ACC + R + Carry) > 0x0F`
|
|
|
|
### 2.10 SUB — Subtract Register from Accumulator with Borrow
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0x90 | SUB R0 | 1 | Subtract R0 from ACC with borrow |
|
|
| 0x91 | SUB R1 | 1 | Subtract R1 from ACC with borrow |
|
|
| 0x92 | SUB R2 | 1 | Subtract R2 from ACC with borrow |
|
|
| 0x93 | SUB R3 | 1 | Subtract R3 from ACC with borrow |
|
|
| 0x94 | SUB R4 | 1 | Subtract R4 from ACC with borrow |
|
|
| 0x95 | SUB R5 | 1 | Subtract R5 from ACC with borrow |
|
|
| 0x96 | SUB R6 | 1 | Subtract R6 from ACC with borrow |
|
|
| 0x97 | SUB R7 | 1 | Subtract R7 from ACC with borrow |
|
|
| 0x98 | SUB R8 | 1 | Subtract R8 from ACC with borrow |
|
|
| 0x99 | SUB R9 | 1 | Subtract R9 from ACC with borrow |
|
|
| 0x9A | SUB R10 | 1 | Subtract R10 from ACC with borrow |
|
|
| 0x9B | SUB R11 | 1 | Subtract R11 from ACC with borrow |
|
|
| 0x9C | SUB R12 | 1 | Subtract R12 from ACC with borrow |
|
|
| 0x9D | SUB R13 | 1 | Subtract R13 from ACC with borrow |
|
|
| 0x9E | SUB R14 | 1 | Subtract R14 from ACC with borrow |
|
|
| 0x9F | SUB R15 | 1 | Subtract R15 from ACC with borrow |
|
|
|
|
`ACC = (ACC - R - (1 - Carry)) & 0x0F`, `Carry = (ACC - R - (1 - Carry)) >= 0`
|
|
Note: Carry = 1 means no borrow, Carry = 0 means borrow occurred.
|
|
|
|
### 2.11 LD — Load Register into Accumulator
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0xA0 | LD R0 | 1 | Load R0 into ACC |
|
|
| 0xA1 | LD R1 | 1 | Load R1 into ACC |
|
|
| 0xA2 | LD R2 | 1 | Load R2 into ACC |
|
|
| 0xA3 | LD R3 | 1 | Load R3 into ACC |
|
|
| 0xA4 | LD R4 | 1 | Load R4 into ACC |
|
|
| 0xA5 | LD R5 | 1 | Load R5 into ACC |
|
|
| 0xA6 | LD R6 | 1 | Load R6 into ACC |
|
|
| 0xA7 | LD R7 | 1 | Load R7 into ACC |
|
|
| 0xA8 | LD R8 | 1 | Load R8 into ACC |
|
|
| 0xA9 | LD R9 | 1 | Load R9 into ACC |
|
|
| 0xAA | LD R10 | 1 | Load R10 into ACC |
|
|
| 0xAB | LD R11 | 1 | Load R11 into ACC |
|
|
| 0xAC | LD R12 | 1 | Load R12 into ACC |
|
|
| 0xAD | LD R13 | 1 | Load R13 into ACC |
|
|
| 0xAE | LD R14 | 1 | Load R14 into ACC |
|
|
| 0xAF | LD R15 | 1 | Load R15 into ACC |
|
|
|
|
`ACC = R[opcode & 0x0F]`
|
|
|
|
### 2.12 XCH — Exchange Accumulator with Register
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0xB0 | XCH R0 | 1 | Exchange ACC with R0 |
|
|
| 0xB1 | XCH R1 | 1 | Exchange ACC with R1 |
|
|
| 0xB2 | XCH R2 | 1 | Exchange ACC with R2 |
|
|
| 0xB3 | XCH R3 | 1 | Exchange ACC with R3 |
|
|
| 0xB4 | XCH R4 | 1 | Exchange ACC with R4 |
|
|
| 0xB5 | XCH R5 | 1 | Exchange ACC with R5 |
|
|
| 0xB6 | XCH R6 | 1 | Exchange ACC with R6 |
|
|
| 0xB7 | XCH R7 | 1 | Exchange ACC with R7 |
|
|
| 0xB8 | XCH R8 | 1 | Exchange ACC with R8 |
|
|
| 0xB9 | XCH R9 | 1 | Exchange ACC with R9 |
|
|
| 0xBA | XCH R10 | 1 | Exchange ACC with R10 |
|
|
| 0xBB | XCH R11 | 1 | Exchange ACC with R11 |
|
|
| 0xBC | XCH R12 | 1 | Exchange ACC with R12 |
|
|
| 0xBD | XCH R13 | 1 | Exchange ACC with R13 |
|
|
| 0xBE | XCH R14 | 1 | Exchange ACC with R14 |
|
|
| 0xBF | XCH R15 | 1 | Exchange ACC with R15 |
|
|
|
|
Swap `ACC` and `R[opcode & 0x0F]`.
|
|
|
|
### 2.13 LDM — Load Immediate into Accumulator
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0xD0 | LDM 0 | 1 | Load 0 into ACC |
|
|
| 0xD1 | LDM 1 | 1 | Load 1 into ACC |
|
|
| 0xD2 | LDM 2 | 1 | Load 2 into ACC |
|
|
| 0xD3 | LDM 3 | 1 | Load 3 into ACC |
|
|
| 0xD4 | LDM 4 | 1 | Load 4 into ACC |
|
|
| 0xD5 | LDM 5 | 1 | Load 5 into ACC |
|
|
| 0xD6 | LDM 6 | 1 | Load 6 into ACC |
|
|
| 0xD7 | LDM 7 | 1 | Load 7 into ACC |
|
|
| 0xD8 | LDM 8 | 1 | Load 8 into ACC |
|
|
| 0xD9 | LDM 9 | 1 | Load 9 into ACC |
|
|
| 0xDA | LDM A | 1 | Load A into ACC |
|
|
| 0xDB | LDM B | 1 | Load B into ACC |
|
|
| 0xDC | LDM C | 1 | Load C into ACC |
|
|
| 0xDD | LDM D | 1 | Load D into ACC |
|
|
| 0xDE | LDM E | 1 | Load E into ACC |
|
|
| 0xDF | LDM F | 1 | Load F into ACC |
|
|
|
|
`ACC = opcode & 0x0F`
|
|
|
|
### 2.14 I/O and RAM Instructions (0xE0-0xEF)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0xE0 | WRM | 1 | Write ACC to RAM main memory character |
|
|
| 0xE1 | WMP | 1 | Write ACC to RAM output port |
|
|
| 0xE2 | WRR | 1 | Write ACC to ROM output port |
|
|
| 0xE3 | WPM | 1 | Write ACC to program memory (special, for 4008/4009) |
|
|
| 0xE4 | WR0 | 1 | Write ACC to RAM status character 0 |
|
|
| 0xE5 | WR1 | 1 | Write ACC to RAM status character 1 |
|
|
| 0xE6 | WR2 | 1 | Write ACC to RAM status character 2 |
|
|
| 0xE7 | WR3 | 1 | Write ACC to RAM status character 3 |
|
|
| 0xE8 | SBM | 1 | Subtract RAM main memory character from ACC with borrow |
|
|
| 0xE9 | RDM | 1 | Read RAM main memory character to ACC |
|
|
| 0xEA | RDR | 1 | Read ROM input port to ACC |
|
|
| 0xEB | ADM | 1 | Add RAM main memory character to ACC with carry |
|
|
| 0xEC | RD0 | 1 | Read RAM status character 0 |
|
|
| 0xED | RD1 | 1 | Read RAM status character 1 |
|
|
| 0xEE | RD2 | 1 | Read RAM status character 2 |
|
|
| 0xEF | RD3 | 1 | Read RAM status character 3 |
|
|
|
|
### 2.15 Accumulator Group Instructions (0xF0-0xFC)
|
|
|
|
| Opcode | Mnemonic | Size | Description |
|
|
|--------|----------|------|-------------|
|
|
| 0xF0 | CLB | 1 | Clear both — ACC and Carry |
|
|
| 0xF1 | CLC | 1 | Clear carry |
|
|
| 0xF2 | IAC | 1 | Increment accumulator |
|
|
| 0xF3 | CMC | 1 | Complement carry |
|
|
| 0xF4 | CMA | 1 | Complement accumulator |
|
|
| 0xF5 | RAL | 1 | Rotate ACC left through carry |
|
|
| 0xF6 | RAR | 1 | Rotate ACC right through carry |
|
|
| 0xF7 | TCC | 1 | Transmit carry to ACC and clear ACC |
|
|
| 0xF8 | TCS | 1 | Transmit carry set — set ACC to 9 if carry, 10 if not |
|
|
| 0xF9 | STC | 1 | Set carry |
|
|
| 0xFA | DAA | 1 | Decimal adjust accumulator |
|
|
| 0xFB | KBP | 1 | Keyboard process |
|
|
| 0xFC | DCL | 1 | Designate command line |
|
|
|
|
### 2.16 Invalid/Unused Opcodes
|
|
|
|
| Opcode Range | Status |
|
|
|--------------|--------|
|
|
| 0x01-0x0F | **LD R** — duplicate of 0xA0-0xAF. Functionally identical. |
|
|
| 0xC0-0xCF | **BBL data** — valid one-byte instruction (return from subroutine + load ACC). See Section 10. |
|
|
| 0xD0-0xDF | **LDM data** — valid (see Section 2.13) |
|
|
| 0xFD-0xFF | Invalid / unused |
|
|
|
|
> **Note on 0x01-0x0F**: The Intel 4004 datasheet shows LD at both 0x01-0x0F and 0xA0-0xAF. Both should be decoded identically as `LD R` where `R = opcode & 0x0F`.
|
|
|
|
---
|
|
|
|
## 3. Correct Rust Instruction Enum
|
|
|
|
```rust
|
|
/// Intel 4004 instruction set enumeration
|
|
/// Organized by opcode range for clarity
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum Instruction {
|
|
// === No operation ===
|
|
Nop, // 0x00
|
|
|
|
// === Register load (duplicated at 0x01-0x0F and 0xA0-0xAF) ===
|
|
Ld(u8), // LD R: Load register R into ACC (R = 0..15)
|
|
|
|
// === Two-byte instructions ===
|
|
Jcn(u8, u8), // 0x10-0x1F: JCN condition, addr_lo
|
|
// condition = byte1 & 0x0F
|
|
// addr_lo = byte2
|
|
// full_addr = (PC & 0xF00) | addr_lo
|
|
|
|
Fim(u8, u8), // 0x20,0x22,...,0x2E: FIM Rpair, data
|
|
// Rpair = (byte1 >> 1) & 0x07 (register pair 0-7)
|
|
// data = byte2
|
|
// R[Rpair*2] = (data >> 4) & 0x0F
|
|
// R[Rpair*2+1] = data & 0x0F
|
|
|
|
Src(u8), // 0x21,0x23,...,0x2F: SRC Rpair
|
|
// Rpair = (byte1 >> 1) & 0x07
|
|
|
|
Fin(u8, u8), // 0x30,0x32,...,0x3E: FIN Rpair, data (data from ROM)
|
|
// Rpair = (byte1 >> 1) & 0x07
|
|
// data = byte2 (read from ROM at address in register pair)
|
|
|
|
Jin(u8), // 0x31,0x33,...,0x3F: JIN Rpair
|
|
// Rpair = (byte1 >> 1) & 0x07
|
|
|
|
Jun(u16), // 0x40-0x4F: JUN addr
|
|
// addr = ((byte1 & 0x0F) << 8) | byte2
|
|
|
|
Jms(u16), // 0x50-0x5F: JMS addr
|
|
// addr = ((byte1 & 0x0F) << 8) | byte2
|
|
|
|
Isz(u8, u8), // 0x70-0x7F: ISZ R, addr_lo
|
|
// R = byte1 & 0x0F
|
|
// addr_lo = byte2
|
|
// full_addr = (PC & 0xF00) | addr_lo
|
|
|
|
// === One-byte register operations ===
|
|
Inc(u8), // 0x60-0x6F: INC R (R = 0..15)
|
|
Add(u8), // 0x80-0x8F: ADD R with carry (R = 0..15)
|
|
Sub(u8), // 0x90-0x9F: SUB R with borrow (R = 0..15)
|
|
Xch(u8), // 0xB0-0xBF: XCH R (R = 0..15)
|
|
|
|
// === One-byte immediate ===
|
|
Ldm(u8), // 0xD0-0xDF: LDM data (data = 0..15)
|
|
|
|
// === I/O and RAM instructions ===
|
|
Wrm, // 0xE0: Write ACC to RAM main memory
|
|
Wmp, // 0xE1: Write ACC to RAM output port
|
|
Wrr, // 0xE2: Write ACC to ROM output port
|
|
Wpm, // 0xE3: Write ACC to program memory
|
|
Wr0, // 0xE4: Write ACC to RAM status character 0
|
|
Wr1, // 0xE5: Write ACC to RAM status character 1
|
|
Wr2, // 0xE6: Write ACC to RAM status character 2
|
|
Wr3, // 0xE7: Write ACC to RAM status character 3
|
|
Sbm, // 0xE8: Subtract RAM main memory from ACC with borrow
|
|
Rdm, // 0xE9: Read RAM main memory to ACC
|
|
Rdr, // 0xEA: Read ROM input port to ACC
|
|
Adm, // 0xEB: Add RAM main memory to ACC with carry
|
|
Rd0, // 0xEC: Read RAM status character 0
|
|
Rd1, // 0xED: Read RAM status character 1
|
|
Rd2, // 0xEE: Read RAM status character 2
|
|
Rd3, // 0xEF: Read RAM status character 3
|
|
|
|
// === Accumulator group ===
|
|
Clb, // 0xF0: Clear both (ACC and Carry)
|
|
Clc, // 0xF1: Clear carry
|
|
Iac, // 0xF2: Increment accumulator
|
|
Cmc, // 0xF3: Complement carry
|
|
Cma, // 0xF4: Complement accumulator
|
|
Ral, // 0xF5: Rotate ACC left through carry
|
|
Rar, // 0xF6: Rotate ACC right through carry
|
|
Tcc, // 0xF7: Transmit carry and clear ACC
|
|
Tcs, // 0xF8: Transmit carry set
|
|
Stc, // 0xF9: Set carry
|
|
Daa, // 0xFA: Decimal adjust accumulator
|
|
Kbp, // 0xFB: Keyboard process
|
|
Dcl, // 0xFC: Designate command line
|
|
|
|
// === BBL (Branch Back and Load) ===
|
|
Bbl(u8), // 0xC0-0xCF: BBL data (return from subroutine + load ACC)
|
|
// data = opcode & 0x0F
|
|
// One-byte instruction. See Section 10.
|
|
|
|
// === Invalid ===
|
|
Invalid, // Unused opcodes (0xFD-0xFF)
|
|
}
|
|
```
|
|
|
|
> **Note on BBL**: BBL is confirmed at opcode range 0xC0-0xCF. It is a one-byte instruction where the 4-bit immediate value is encoded in the low nibble of the opcode itself (similar to LDM). The instruction pops the return address from the stack and loads the accumulator with the immediate value.
|
|
|
|
---
|
|
|
|
## 4. Correct `decode_instruction()` Pseudocode
|
|
|
|
```
|
|
function decode_instruction(opcode: u8) -> Instruction:
|
|
match opcode:
|
|
// NOP
|
|
0x00 => Nop
|
|
|
|
// LD R (duplicate range — same as 0xA0-0xAF)
|
|
0x01..=0x0F => Ld(opcode & 0x0F)
|
|
|
|
// JCN — two-byte, condition in low nibble
|
|
0x10..=0x1F => Jcn(opcode & 0x0F, 0) // addr filled by byte 2 fetch
|
|
|
|
// FIM (even) / SRC (odd)
|
|
0x20..=0x2F =>
|
|
if opcode & 0x01 == 0:
|
|
Fim((opcode >> 1) & 0x07, 0) // data filled by byte 2 fetch
|
|
else:
|
|
Src((opcode >> 1) & 0x07)
|
|
|
|
// FIN (even) / JIN (odd)
|
|
0x30..=0x3F =>
|
|
if opcode & 0x01 == 0:
|
|
Fin((opcode >> 1) & 0x07) // data from ROM filled by byte 2
|
|
else:
|
|
Jin((opcode >> 1) & 0x07)
|
|
|
|
// JUN — two-byte
|
|
0x40..=0x4F => Jun(0) // addr filled by byte 2 fetch
|
|
|
|
// JMS — two-byte
|
|
0x50..=0x5F => Jms(0) // addr filled by byte 2 fetch
|
|
|
|
// INC R
|
|
0x60..=0x6F => Inc(opcode & 0x0F)
|
|
|
|
// ISZ — two-byte
|
|
0x70..=0x7F => Isz(opcode & 0x0F, 0) // addr filled by byte 2 fetch
|
|
|
|
// ADD R (with carry)
|
|
0x80..=0x8F => Add(opcode & 0x0F)
|
|
|
|
// SUB R (with borrow)
|
|
0x90..=0x9F => Sub(opcode & 0x0F)
|
|
|
|
// LD R (primary range)
|
|
0xA0..=0xAF => Ld(opcode & 0x0F)
|
|
|
|
// XCH R
|
|
0xB0..=0xBF => Xch(opcode & 0x0F)
|
|
|
|
// BBL (if confirmed) or Invalid
|
|
0xC0..=0xCF => Bbl(opcode & 0x0F)
|
|
// OR Invalid if 0xC0-0xCF is truly unused
|
|
|
|
// Invalid
|
|
0xC0..=0xCF => Invalid // (use this if BBL is not at this range)
|
|
|
|
// LDM data
|
|
0xD0..=0xDF => Ldm(opcode & 0x0F)
|
|
|
|
// I/O and RAM instructions
|
|
0xE0 => Wrm
|
|
0xE1 => Wmp
|
|
0xE2 => Wrr
|
|
0xE3 => Wpm
|
|
0xE4 => Wr0
|
|
0xE5 => Wr1
|
|
0xE6 => Wr2
|
|
0xE7 => Wr3
|
|
0xE8 => Sbm
|
|
0xE9 => Rdm
|
|
0xEA => Rdr
|
|
0xEB => Adm
|
|
0xEC => Rd0
|
|
0xED => Rd1
|
|
0xEE => Rd2
|
|
0xEF => Rd3
|
|
|
|
// Accumulator group
|
|
0xF0 => Clb
|
|
0xF1 => Clc
|
|
0xF2 => Iac
|
|
0xF3 => Cmc
|
|
0xF4 => Cma
|
|
0xF5 => Ral
|
|
0xF6 => Rar
|
|
0xF7 => Tcc
|
|
0xF8 => Tcs
|
|
0xF9 => Stc
|
|
0xFA => Daa
|
|
0xFB => Kbp
|
|
0xFC => Dcl
|
|
|
|
// Invalid/unused
|
|
0xFD..=0xFF => Invalid
|
|
|
|
_ => Invalid
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Correct `execute_instruction()` Pseudocode
|
|
|
|
```
|
|
function execute_instruction():
|
|
match current_op:
|
|
Nop:
|
|
// No operation, just advance PC
|
|
PC.inc()
|
|
|
|
Ld(reg):
|
|
ACC = index_registers[reg]
|
|
PC.inc()
|
|
|
|
Xch(reg):
|
|
temp = ACC
|
|
ACC = index_registers[reg]
|
|
index_registers[reg] = temp
|
|
PC.inc()
|
|
|
|
Add(reg):
|
|
result = ACC + index_registers[reg] + (if carry then 1 else 0)
|
|
carry = result > 0x0F
|
|
ACC = result & 0x0F
|
|
PC.inc()
|
|
|
|
Sub(reg):
|
|
// Subtract with borrow: ACC = ACC - R - (1 - Carry)
|
|
borrow = if not carry then 1 else 0
|
|
result = ACC - index_registers[reg] - borrow
|
|
carry = result >= 0 // carry=1 means no borrow
|
|
ACC = result & 0x0F
|
|
PC.inc()
|
|
|
|
Inc(reg):
|
|
index_registers[reg] = (index_registers[reg] + 1) & 0x0F
|
|
PC.inc()
|
|
|
|
Ldm(data):
|
|
ACC = data & 0x0F
|
|
PC.inc()
|
|
|
|
// === Two-byte instructions (operand already fetched) ===
|
|
|
|
Jcn(condition, addr_lo):
|
|
full_addr = (PC.value() & 0xF00) | addr_lo
|
|
should_jump = evaluate_jcn_condition(condition)
|
|
if should_jump:
|
|
PC.set(full_addr)
|
|
else:
|
|
PC.inc() // skip past byte 2 (PC was already inc'd past byte 1)
|
|
|
|
Fim(rpair, data):
|
|
even_reg = rpair * 2
|
|
odd_reg = rpair * 2 + 1
|
|
index_registers[even_reg] = (data >> 4) & 0x0F
|
|
index_registers[odd_reg] = data & 0x0F
|
|
PC.inc() // past byte 2
|
|
|
|
Src(rpair):
|
|
// Send register pair contents to address bus for I/O
|
|
even_reg = rpair * 2
|
|
odd_reg = rpair * 2 + 1
|
|
// The 8-bit value from the register pair is sent to RAM/ROM
|
|
// for addressing: high nibble from even reg, low nibble from odd reg
|
|
// This selects a specific RAM character or ROM port
|
|
rom_port = index_registers[even_reg] // high nibble selects chip
|
|
ram_addr_low = index_registers[odd_reg] // low nibble selects word
|
|
PC.inc()
|
|
|
|
Fin(rpair):
|
|
// Read ROM at address in register pair, store in register pair
|
|
even_reg = rpair * 2
|
|
odd_reg = rpair * 2 + 1
|
|
rom_addr = (index_registers[even_reg] << 4) | index_registers[odd_reg]
|
|
// Read 8 bits from ROM at rom_addr
|
|
// (In practice, this reads from the current ROM page)
|
|
// data = ROM[PC_page_base + rom_addr] -- simplified
|
|
// index_registers[even_reg] = (data >> 4) & 0x0F
|
|
// index_registers[odd_reg] = data & 0x0F
|
|
PC.inc()
|
|
|
|
Jin(rpair):
|
|
even_reg = rpair * 2
|
|
odd_reg = rpair * 2 + 1
|
|
addr = (index_registers[even_reg] << 4) | index_registers[odd_reg]
|
|
// Note: JIN uses 8 bits from register pair as page offset
|
|
// Full address = (PC & 0xF00) | addr (stays on current page)
|
|
// OR: Full address = (index_registers[even_reg] as high 6 bits,
|
|
// index_registers[odd_reg] as low 6 bits)
|
|
// See Section 8 for detailed discussion
|
|
PC.set((PC.value() & 0xF00) | addr)
|
|
// Note: JIN does NOT advance PC normally — it sets PC
|
|
|
|
Jun(addr):
|
|
PC.set(addr)
|
|
// PC already past byte 2 from fetch cycle
|
|
|
|
Jms(addr):
|
|
// Push return address to stack
|
|
if stack_pointer < 3:
|
|
stack[stack_pointer] = PC // PC points past the 2-byte instruction
|
|
stack_pointer += 1
|
|
PC.set(addr)
|
|
|
|
Isz(reg, addr_lo):
|
|
index_registers[reg] = (index_registers[reg] + 1) & 0x0F
|
|
if index_registers[reg] != 0:
|
|
full_addr = (PC.value() & 0xF00) | addr_lo
|
|
PC.set(full_addr)
|
|
else:
|
|
PC.inc() // past byte 2
|
|
|
|
Bbl(data):
|
|
// Return from subroutine and load ACC
|
|
if stack_pointer > 0:
|
|
stack_pointer -= 1
|
|
PC = stack[stack_pointer]
|
|
ACC = data & 0x0F
|
|
|
|
// === I/O and RAM instructions ===
|
|
|
|
Wrm:
|
|
// Write ACC to RAM main memory at current SRC address
|
|
// ram_write(current_ram_addr, ACC)
|
|
PC.inc()
|
|
|
|
Wmp:
|
|
// Write ACC to RAM output port
|
|
// ram_output(ACC)
|
|
PC.inc()
|
|
|
|
Wrr:
|
|
// Write ACC to ROM output port
|
|
// rom_output(ACC)
|
|
PC.inc()
|
|
|
|
Wpm:
|
|
// Write ACC to program memory (special)
|
|
// rom_write(ACC)
|
|
PC.inc()
|
|
|
|
Wr0:
|
|
// Write ACC to RAM status character 0
|
|
PC.inc()
|
|
|
|
Wr1:
|
|
// Write ACC to RAM status character 1
|
|
PC.inc()
|
|
|
|
Wr2:
|
|
// Write ACC to RAM status character 2
|
|
PC.inc()
|
|
|
|
Wr3:
|
|
// Write ACC to RAM status character 3
|
|
PC.inc()
|
|
|
|
Sbm:
|
|
// Subtract RAM main memory from ACC with borrow
|
|
// ram_data = ram_read(current_ram_addr)
|
|
// borrow = if not carry then 1 else 0
|
|
// result = ACC - ram_data - borrow
|
|
// carry = result >= 0
|
|
// ACC = result & 0x0F
|
|
PC.inc()
|
|
|
|
Rdm:
|
|
// Read RAM main memory to ACC
|
|
// ACC = ram_read(current_ram_addr)
|
|
PC.inc()
|
|
|
|
Rdr:
|
|
// Read ROM input port to ACC
|
|
// ACC = rom_read_input()
|
|
PC.inc()
|
|
|
|
Adm:
|
|
// Add RAM main memory to ACC with carry
|
|
// ram_data = ram_read(current_ram_addr)
|
|
// result = ACC + ram_data + (if carry then 1 else 0)
|
|
// carry = result > 0x0F
|
|
// ACC = result & 0x0F
|
|
PC.inc()
|
|
|
|
Rd0:
|
|
// Read RAM status character 0 to ACC
|
|
// ACC = ram_read_status(0)
|
|
PC.inc()
|
|
|
|
Rd1:
|
|
// Read RAM status character 1 to ACC
|
|
PC.inc()
|
|
|
|
Rd2:
|
|
// Read RAM status character 2 to ACC
|
|
PC.inc()
|
|
|
|
Rd3:
|
|
// Read RAM status character 3 to ACC
|
|
PC.inc()
|
|
|
|
// === Accumulator group ===
|
|
|
|
Clb:
|
|
ACC = 0
|
|
carry = false
|
|
PC.inc()
|
|
|
|
Clc:
|
|
carry = false
|
|
PC.inc()
|
|
|
|
Iac:
|
|
result = ACC + 1
|
|
carry = result > 0x0F
|
|
ACC = result & 0x0F
|
|
PC.inc()
|
|
|
|
Cmc:
|
|
carry = not carry
|
|
PC.inc()
|
|
|
|
Cma:
|
|
ACC = (~ACC) & 0x0F
|
|
PC.inc()
|
|
|
|
Ral:
|
|
new_carry = (ACC & 0x08) != 0
|
|
ACC = ((ACC << 1) | (if carry then 1 else 0)) & 0x0F
|
|
carry = new_carry
|
|
PC.inc()
|
|
|
|
Rar:
|
|
new_carry = (ACC & 0x01) != 0
|
|
ACC = (ACC >> 1) | (if carry then 0x08 else 0)
|
|
carry = new_carry
|
|
PC.inc()
|
|
|
|
Tcc:
|
|
// Transmit carry to ACC and clear
|
|
ACC = if carry then 1 else 0
|
|
carry = false
|
|
PC.inc()
|
|
|
|
Tcs:
|
|
// Set ACC to 9 if carry was set, 10 if not
|
|
// Used for BCD subtraction correction
|
|
ACC = if carry then 9 else 10
|
|
carry = false
|
|
PC.inc()
|
|
|
|
Stc:
|
|
carry = true
|
|
PC.inc()
|
|
|
|
Daa:
|
|
// Decimal adjust accumulator
|
|
if ACC > 9 or carry:
|
|
ACC = ACC + 6
|
|
if ACC > 0x0F:
|
|
carry = true
|
|
ACC = ACC & 0x0F
|
|
PC.inc()
|
|
|
|
Kbp:
|
|
// Keyboard process — converts 1-of-N coded key to binary
|
|
// If ACC = 0: ACC = 0
|
|
// If ACC = 1: ACC = 0
|
|
// If ACC = 2: ACC = 1
|
|
// If ACC = 4: ACC = 2
|
|
// If ACC = 8: ACC = 3
|
|
// Otherwise: ACC = ACC (error)
|
|
match ACC:
|
|
0 => ACC = 0
|
|
1 => ACC = 0
|
|
2 => ACC = 1
|
|
4 => ACC = 2
|
|
8 => ACC = 3
|
|
_ => {} // error, leave as is
|
|
PC.inc()
|
|
|
|
Dcl:
|
|
// Designate command line — set RAM bank from ACC
|
|
ram_bank = ACC & 0x07 // 3 bits for bank selection
|
|
PC.inc()
|
|
|
|
Invalid:
|
|
// Treat as NOP — advance PC
|
|
PC.inc()
|
|
```
|
|
|
|
---
|
|
|
|
## 6. One-Byte vs Two-Byte Instruction Classification
|
|
|
|
### One-Byte Instructions (8 clock cycles)
|
|
|
|
| Opcode Range | Instruction |
|
|
|---|---|
|
|
| 0x00 | NOP |
|
|
| 0x01-0x0F | LD R (duplicate) |
|
|
| 0x21, 0x23, 0x25, 0x27, 0x29, 0x2B, 0x2D, 0x2F | SRC Rpair |
|
|
| 0x31, 0x33, 0x35, 0x37, 0x39, 0x3B, 0x3D, 0x3F | JIN Rpair |
|
|
| 0x60-0x6F | INC R |
|
|
| 0x80-0x8F | ADD R |
|
|
| 0x90-0x9F | SUB R |
|
|
| 0xA0-0xAF | LD R |
|
|
| 0xB0-0xBF | XCH R |
|
|
| 0xD0-0xDF | LDM data |
|
|
| 0xC0-0xCF | BBL data |
|
|
| 0xE0-0xEF | I/O and RAM instructions |
|
|
| 0xF0-0xFC | Accumulator group |
|
|
|
|
### Two-Byte Instructions (16 clock cycles)
|
|
|
|
| Opcode Range | Instruction | Byte 2 Usage |
|
|
|---|---|---|
|
|
| 0x10-0x1F | JCN condition, addr | 8-bit page address |
|
|
| 0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E | FIM Rpair, data | 8-bit immediate data |
|
|
| 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E | FIN Rpair | (reads from ROM) |
|
|
| 0x40-0x4F | JUN addr | Low 8 bits of 12-bit address |
|
|
| 0x50-0x5F | JMS addr | Low 8 bits of 12-bit address |
|
|
| 0x70-0x7F | ISZ R, addr | 8-bit page address |
|
|
|
|
### Quick Detection Function
|
|
|
|
```
|
|
function is_two_byte_instruction(opcode: u8) -> bool:
|
|
// JCN: 0x10-0x1F
|
|
if opcode >= 0x10 && opcode <= 0x1F: return true
|
|
|
|
// FIM (even opcodes in 0x20-0x2F)
|
|
if opcode >= 0x20 && opcode <= 0x2E && (opcode & 0x01) == 0: return true
|
|
|
|
// FIN (even opcodes in 0x30-0x3F)
|
|
if opcode >= 0x30 && opcode <= 0x3E && (opcode & 0x01) == 0: return true
|
|
|
|
// JUN: 0x40-0x4F
|
|
if opcode >= 0x40 && opcode <= 0x4F: return true
|
|
|
|
// JMS: 0x50-0x5F
|
|
if opcode >= 0x50 && opcode <= 0x5F: return true
|
|
|
|
// ISZ: 0x70-0x7F
|
|
if opcode >= 0x70 && opcode <= 0x7F: return true
|
|
|
|
return false
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Two-Byte Instruction Fetch in `update()` Cycle
|
|
|
|
### Current Problem
|
|
|
|
The current implementation uses a flawed "high nibble / low nibble" approach for JUN and JMS, treating them as two separate 1-byte instructions (`JunHigh`/`JunLow`, `JmsHigh`/`JmsLow`). This is **incorrect** — JUN and JMS are single 2-byte instructions where:
|
|
- Byte 1 contains the opcode AND the high 4 bits of the address
|
|
- Byte 2 contains the low 8 bits of the address
|
|
|
|
### Correct Fetch State Machine
|
|
|
|
The instruction fetch should use a state machine with an additional state for "waiting for byte 2":
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> FetchByte1
|
|
FetchByte1 --> Decode: byte1 ready
|
|
Decode --> Execute: one-byte instruction
|
|
Decode --> FetchByte2: two-byte instruction
|
|
FetchByte2 --> Execute: byte2 ready
|
|
Execute --> FetchByte1: execution complete
|
|
```
|
|
|
|
### Pseudocode for `update()` Fetch Logic
|
|
|
|
```
|
|
enum InstructionPhase:
|
|
FetchByte1, // Fetching first byte of instruction
|
|
FetchByte2, // Fetching second byte of 2-byte instruction
|
|
Execute, // Executing decoded instruction
|
|
|
|
struct Intel4004:
|
|
// ... existing fields ...
|
|
instruction_phase: InstructionPhase
|
|
byte1: u8, // First byte of instruction
|
|
byte2: u8, // Second byte (for 2-byte instructions)
|
|
current_op: Instruction // Decoded instruction
|
|
|
|
function update():
|
|
// ... clock edge detection, memory state machine ...
|
|
|
|
match instruction_phase:
|
|
FetchByte1:
|
|
if memory_state == DriveData:
|
|
byte1 = read_data_bus()
|
|
PC.inc()
|
|
|
|
if is_two_byte_instruction(byte1):
|
|
// Decode partially, then fetch byte 2
|
|
current_op = decode_instruction(byte1)
|
|
instruction_phase = FetchByte2
|
|
else:
|
|
// One-byte instruction: decode and execute
|
|
current_op = decode_instruction(byte1)
|
|
instruction_phase = Execute
|
|
|
|
FetchByte2:
|
|
if memory_state == DriveData:
|
|
byte2 = read_data_bus()
|
|
PC.inc()
|
|
|
|
// Complete the instruction with byte2 data
|
|
current_op = complete_two_byte(current_op, byte2)
|
|
instruction_phase = Execute
|
|
|
|
Execute:
|
|
execute_instruction()
|
|
instruction_phase = FetchByte1
|
|
|
|
function complete_two_byte(partial_op: Instruction, byte2: u8) -> Instruction:
|
|
match partial_op:
|
|
Jcn(condition, _):
|
|
return Jcn(condition, byte2)
|
|
|
|
Fim(rpair, _):
|
|
return Fim(rpair, byte2)
|
|
|
|
Fin(rpair):
|
|
// FIN reads from ROM at address in register pair
|
|
// byte2 is already read from ROM by the fetch hardware
|
|
// Store byte2 into register pair
|
|
return FinWithData(rpair, byte2)
|
|
|
|
Jun(_):
|
|
addr = ((byte1 & 0x0F) << 8) | byte2
|
|
return Jun(addr)
|
|
|
|
Jms(_):
|
|
addr = ((byte1 & 0x0F) << 8) | byte2
|
|
return Jms(addr)
|
|
|
|
Isz(reg, _):
|
|
return Isz(reg, byte2)
|
|
|
|
_:
|
|
return partial_op // Should not happen
|
|
```
|
|
|
|
### Key Points for Implementation
|
|
|
|
1. **PC is incremented after each byte fetch** — once after byte1, once after byte2
|
|
2. **JUN/JMS address encoding**: Byte 1 low nibble = address bits 11-8, Byte 2 = address bits 7-0
|
|
3. **JCN/ISZ address encoding**: Byte 2 = address bits 7-0 within current page. Full address = `(PC_before_fetch & 0xF00) | byte2`
|
|
4. **FIM data encoding**: Byte 2 high nibble → even register, low nibble → odd register
|
|
5. **FIN**: The second byte is read from ROM at the address formed by the register pair contents. The CPU fetches it from the data bus like any other byte, but the ROM address comes from the register pair, not from PC.
|
|
|
|
### Important: Save PC Page Before Fetch
|
|
|
|
For JCN and ISZ, the page address must be captured **before** the PC is incremented past byte 1:
|
|
|
|
```
|
|
// In FetchByte1, before incrementing PC:
|
|
saved_page = PC.value() & 0xF00
|
|
|
|
// Later, when computing full address:
|
|
full_addr = saved_page | byte2
|
|
```
|
|
|
|
---
|
|
|
|
## 8. JCN Condition Decoding Logic
|
|
|
|
The JCN instruction has a 4-bit condition field. The conditions use a combination of three tests:
|
|
|
|
- **Bit 0 (0x01)**: TEST input pin
|
|
- **Bit 1 (0x02)**: Carry flag
|
|
- **Bit 2 (0x04)**: Accumulator zero
|
|
- **Bit 3 (0x08)**: Invert flag
|
|
|
|
### Decoding Algorithm
|
|
|
|
```
|
|
function evaluate_jcn_condition(condition: u8) -> bool:
|
|
test_pin = read_test_pin() // Current TEST pin state
|
|
carry_flag = self.carry // Current carry flag
|
|
acc_zero = (self.accumulator == 0) // ACC zero check
|
|
|
|
// Compute the base condition (before invert)
|
|
// Conditions are OR-ed together based on which bits are set
|
|
base = false
|
|
if (condition & 0x01) != 0: base = base || test_pin
|
|
if (condition & 0x02) != 0: base = base || carry_flag
|
|
if (condition & 0x04) != 0: base = base || acc_zero
|
|
|
|
// Apply invert (bit 3)
|
|
if (condition & 0x08) != 0:
|
|
return !base
|
|
else:
|
|
return base
|
|
```
|
|
|
|
### Condition Code Table
|
|
|
|
| Condition | Binary | Meaning |
|
|
|---|---|---|
|
|
| 0x0 | 0000 | Always false (never jump) |
|
|
| 0x1 | 0001 | TEST = 1 |
|
|
| 0x2 | 0010 | Carry = 1 |
|
|
| 0x3 | 0011 | Carry=1 OR TEST=1 |
|
|
| 0x4 | 0100 | ACC = 0 |
|
|
| 0x5 | 0101 | ACC=0 OR TEST=1 |
|
|
| 0x6 | 0110 | ACC=0 OR Carry=1 |
|
|
| 0x7 | 0111 | ACC=0 OR Carry=1 OR TEST=1 |
|
|
| 0x8 | 1000 | Always true (NOT false) |
|
|
| 0x9 | 1001 | TEST = 0 (NOT TEST=1) |
|
|
| 0xA | 1010 | Carry = 0 (NOT Carry=1) |
|
|
| 0xB | 1011 | Carry=0 AND TEST=0 (NOT of Carry=1 OR TEST=1) |
|
|
| 0xC | 1100 | ACC ≠ 0 (NOT ACC=0) |
|
|
| 0xD | 1101 | ACC≠0 AND TEST=0 |
|
|
| 0xE | 1110 | Carry=0 AND ACC≠0 |
|
|
| 0xF | 1111 | Carry=0 AND ACC≠0 AND TEST=0 |
|
|
|
|
### Key Insight: De Morgan's Law
|
|
|
|
The invert bit applies De Morgan's law. When bit 3 is set:
|
|
- `NOT (A OR B OR C)` becomes `NOT A AND NOT B AND NOT C`
|
|
- So condition 0xB (1011) = NOT (Carry=1 OR TEST=1) = Carry=0 AND TEST=0
|
|
|
|
---
|
|
|
|
## 9. SRC Register Pair Addressing
|
|
|
|
### Register Pair Concept
|
|
|
|
The 16 index registers (R0-R15) are organized as 8 register pairs:
|
|
- Pair 0: R0 (high nibble) + R1 (low nibble)
|
|
- Pair 1: R2 (high nibble) + R3 (low nibble)
|
|
- Pair 2: R4 (high nibble) + R5 (low nibble)
|
|
- Pair 3: R6 (high nibble) + R7 (low nibble)
|
|
- Pair 4: R8 (high nibble) + R9 (low nibble)
|
|
- Pair 5: R10 (high nibble) + R11 (low nibble)
|
|
- Pair 6: R12 (high nibble) + R13 (low nibble)
|
|
- Pair 7: R14 (high nibble) + R15 (low nibble)
|
|
|
|
### Extracting Register Pair Number from Opcode
|
|
|
|
For FIM/SRC/FIN/JIN, the register pair number is encoded in bits 4-1 of the opcode:
|
|
|
|
```
|
|
pair_number = (opcode >> 1) & 0x07
|
|
even_register = pair_number * 2
|
|
odd_register = pair_number * 2 + 1
|
|
```
|
|
|
|
### SRC Operation
|
|
|
|
The SRC instruction sends the 8-bit value from a register pair to the address bus:
|
|
- **Even register** → high nibble (chip select / ROM port)
|
|
- **Odd register** → low nibble (word select within chip)
|
|
|
|
This 8-bit value is used by subsequent I/O instructions (WRM, WMP, WRR, RDM, RDR, etc.) to select the target device and address.
|
|
|
|
### JIN Operation
|
|
|
|
JIN uses the register pair to form a jump target address:
|
|
- The 8-bit value from the pair is used as a page offset
|
|
- Full address = `(current_PC & 0xF00) | (R[even] << 4) | R[odd]`
|
|
|
|
> **Note**: Some sources describe JIN as using all 12 bits from a register pair. However, since each register is only 4 bits wide, a pair provides only 8 bits. The upper 4 bits of the address come from the current PC page.
|
|
|
|
### FIM Operation
|
|
|
|
FIM loads an 8-bit immediate value into a register pair:
|
|
```
|
|
data = byte2
|
|
R[even] = (data >> 4) & 0x0F // High nibble → even register
|
|
R[odd] = data & 0x0F // Low nibble → odd register
|
|
```
|
|
|
|
### FIN Operation
|
|
|
|
FIN reads 8 bits from ROM and stores in a register pair:
|
|
```
|
|
// The ROM address is formed from the register pair contents
|
|
rom_addr = (R[even] << 4) | R[odd]
|
|
// Read 2 nibbles from ROM at current page + rom_addr offset
|
|
data = ROM[(PC & 0xF00) | rom_addr] // Simplified
|
|
R[even] = (data >> 4) & 0x0F
|
|
R[odd] = data & 0x0F
|
|
```
|
|
|
|
---
|
|
|
|
## 10. BBL Instruction Clarification
|
|
|
|
The BBL (Branch Back and Load) instruction is a **one-byte instruction** at opcode range 0xC0-0xCF. The 4-bit immediate value is encoded in the low nibble of the opcode (similar to LDM). It:
|
|
|
|
1. Pops the return address from the stack
|
|
2. Loads the accumulator with the 4-bit immediate value from the low nibble of the opcode
|
|
|
|
```
|
|
Bbl(data):
|
|
if stack_pointer > 0:
|
|
stack_pointer -= 1
|
|
PC = stack[stack_pointer]
|
|
ACC = data & 0x0F
|
|
PC.inc() // advance past the return address
|
|
```
|
|
|
|
> **Confirmed**: BBL is at 0xC0-0xCF and is a one-byte instruction. The immediate is in the opcode's low nibble, not a second byte.
|
|
|
|
---
|
|
|
|
## 11. Instruction Execution Semantics Notes
|
|
|
|
### ADD vs. Add-with-Carry
|
|
|
|
The Intel 4004 ADD instruction (0x80-0x8F) **always adds with carry**. There is no separate "add without carry" instruction. The carry flag is both an input and output.
|
|
|
|
### SUB vs. Subtract-with-Borrow
|
|
|
|
Similarly, SUB (0x90-0x9F) **always subtracts with borrow**. The carry flag acts as the inverse of borrow:
|
|
- Carry = 1 → no borrow occurred
|
|
- Carry = 0 → borrow occurred
|
|
|
|
To subtract without borrow, set carry to 1 first (using STC) before executing SUB.
|
|
|
|
### TCS Semantics
|
|
|
|
TCS (Transmit Carry Set) is used for BCD correction after subtraction:
|
|
- If carry = 1 (no borrow): ACC = 9
|
|
- If carry = 0 (borrow occurred): ACC = 10
|
|
|
|
This value is then added to correct the BCD result.
|
|
|
|
### KBP (Keyboard Process)
|
|
|
|
Converts a 1-of-N encoded input to binary:
|
|
| ACC Input | ACC Output |
|
|
|-----------|------------|
|
|
| 0000 (0) | 0000 (0) |
|
|
| 0001 (1) | 0000 (0) |
|
|
| 0010 (2) | 0001 (1) |
|
|
| 0100 (4) | 0010 (2) |
|
|
| 1000 (8) | 0011 (3) |
|
|
| Other | Unchanged (error) |
|
|
|
|
---
|
|
|
|
## 12. Implementation Checklist
|
|
|
|
When rewriting [`intel_4004.rs`](src/components/cpu/intel_4004.rs), the following changes are needed:
|
|
|
|
1. **Replace the `Instruction` enum** with the correct one from Section 3
|
|
2. **Rewrite `decode_instruction()`** using the correct opcode mapping from Section 4
|
|
3. **Rewrite `execute_instruction()`** with correct semantics from Section 5
|
|
4. **Add `is_two_byte_instruction()` helper** as described in Section 6
|
|
5. **Refactor `update()` fetch logic** to use FetchByte1/FetchByte2/Execute states from Section 7
|
|
6. **Remove `JunHigh`/`JunLow`/`JmsHigh`/`JmsLow`** — these are artifacts of the wrong design
|
|
7. **Remove `pending_operand` field** — replaced by the FetchByte2 state
|
|
8. **Implement correct JCN condition decoding** from Section 8
|
|
9. **Implement correct SRC register pair addressing** from Section 9
|
|
10. **Add missing instructions**: FIM, FIN, JIN, SRC, BBL, KBP, DCL, RD0-RD3, WR0-WR3
|
|
11. **Remove non-existent instructions**: AddC, SubC, Dad, Jnt, JntInvert
|
|
12. **Fix ADD/SUB semantics** — they always use carry/borrow
|
|
13. **Fix TCC semantics** — should set ACC = carry value, not just clear
|
|
14. **Fix TCS semantics** — should set ACC = 9 if carry, 10 if not
|
|
15. **Update all tests** to use correct opcodes
|
|
16. **Mark 0xFD-0xFF as invalid** (not 0xFA-0xFF — DAA/KBP/DCL are at 0xFA-0xFC)
|