docs(v4.0.0): DATASHEET-F18.md -- the machine: registers, stacks, memory, instruction word, opcodes, control unit, faults, I/O

As built in the golden model (v4/src), with what is not yet as intended listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-15 06:20:25 -04:00
co-authored by Claude Opus 5.5
parent 952df0f44a
commit 1ed56b93b4
+317
View File
@@ -0,0 +1,317 @@
# StarForth v4 F18 Machine — Datasheet
The machine: registers, stacks, memory, instruction word, opcodes, control unit, faults and I/O.
This sheet says what the machine **is**. Nothing above the machine is here: no FORTH, no
interpreter, no compiler, no dictionary. Those are source, in the capsule's trailer
(`MACHINE.md`).
**Source of truth.** The golden model in C: `v4/src/` and `v4/include/v4/`, as built on
2026-10-15. The FPGA machine will match it instruction for instruction (`JUSTIFICATION.md` §10,
step 3: Verilator co-simulation in lockstep). Every statement here is checked against the model's
code, cited by file. Where the model is not yet what was intended, the sheet says so (§13).
Design history: `DECOMPOSITION.md` §1 (the ISA) and §3 (decisions D-1 to D-19). Where the two
differ, the model and this sheet follow the later ruling.
---
## 1. Summary
| | |
|---|---|
| Architecture | Two-stack machine derived from Chuck Moore's F18 (GA144 node) |
| Instruction set | 32 opcodes, 5 bits each |
| Instruction word | 32 bits: six 5-bit slots + 2 spare bits, at every cell width |
| Cell (data) width | 32 or 64 bits, a build parameter |
| Addressing | Word-addressed. An address counts cells. |
| Registers | P, A, B; T, S (top of the data stack); R (top of the return stack) |
| Data stack | T, S + ring. 10 deep on a mesh node, 32 on the host node. Counted and guarded. |
| Return stack | R + ring. 9 deep on a mesh node, 32 on the host node. Counted and guarded. |
| Memory | RAM, a build parameter (1024 words mesh, 16384 host) |
| I/O | Memory-mapped: ports, console, stack-depth registers, error register |
| Faults | 6 kinds, vectored through a 6-word table |
| Side effects | Per-opcode heat, per-call-target heat, anti-clock. No instruction cost. |
## 2. Build parameters
| Parameter | Symbol | Values | Where |
|---|---|---|---|
| Cell width | `V4_CELL_BITS` | 32, 64 | `cell.h` |
| RAM words | `V4_NODE_WORDS` | default 1024; host node 16384 | `node.h`, `v4/Makefile` `HOST_WORDS` |
| Data ring | `V4_DATA_RING` | default 8 (depth 10); host 30 (depth 32) | `stack.h`, `v4/Makefile` |
| Return ring | `V4_RET_RING` | default 8 (depth 9); host 31 (depth 32) | `stack.h`, `v4/Makefile` |
| Ports | `V4_PORTS` | default 8 | `node.h` |
The host node is the same machine with more memory and deeper stacks (D-17). Nothing else
differs.
## 3. Registers
| Reg | Width | Holds | Visible to code through |
|---|---|---|---|
| **P** | cell | Program counter: the address of the next instruction word, or of the next `@p`/`!p` cell | `@p`, `!p`, `ex`, branches, `;` |
| **A** | cell | Address register; also the low half of the `+*` product | `a`, `a!`, `@`, `@+`, `!`, `!+`, `+*` |
| **B** | cell | Address register | `b!`, `@b`, `!b` |
| **T** | cell | Top of the data stack | every data opcode |
| **S** | cell | Second on the data stack | `over`, `+`, `and`, `xor`, `+*` |
| **R** | cell | Top of the return stack | `;`, `ex`, `call`, `next`, `unext`, `push`, `pop` |
| **I** | 32 bits | The instruction word being executed, and the slot within it | not visible. Held by the control unit (`exec.c`). |
T and S are the top two places of the data stack, and R is the top place of the return stack.
They are not copies of stack cells (`node.h`, `stack.h`). There is no stack pointer register
and no flags register. Conditions are tested on T directly (`if`, `-if`).
## 4. Stacks
**Data stack** (`v4_dstack`, `stack.h`): T, S, then a ring of `V4_DATA_RING` cells.
- A push moves S into the ring, T into S, and the new value into T.
- A pop does the reverse.
**Return stack** (`v4_rstack`): R, then a ring of `V4_RET_RING` cells, the same way.
**Counted and guarded (D-16).** Each stack counts what it holds, from 0 to its depth. Before
every opcode the control unit checks two things:
- the stacks hold what the opcode takes, including a T or S it only reads;
- the stacks have room for what it leaves.
If either check fails, the opcode does nothing and the machine faults (§9). The ring's wrap is
never seen by a running program.
**Not addressable.** There is no address for any stack cell. A program sees only the two depth
registers (§10.3).
## 5. Memory
- **Word-addressed (D-1).** An address names a cell, so `CELLS` is the identity.
- A byte address is 4 × word + 0..3. That is a software convention used by the block format
and strings. The machine itself has no byte access.
- **RAM:** addresses 0 to `V4_NODE_WORDS − 1`.
- **Out of range (D-14).** Every address a running program uses is checked before it is used:
- P, for an instruction fetch, an `@p` fetch or a `!p` store;
- A, for `@ @+ ! !+`;
- B, for `@b !b`.
An address outside RAM is an address fault (§9). No access is made, and A and B are left
unchanged.
- **I/O addresses** (§10) sit inside the address space. A fetch from or store to one goes to
the device, not to RAM. The exceptions are NODE-ERROR, which writes RAM and then traps, and
the console receive addresses, where a store goes to RAM.
- **ROM.** Designed, not built: `MACHINE.md` §2, and plan Task 1. It is a read-only range at
`V4_NODE_WORDS` and above. It can be fetched and executed. A store there is an address fault.
A reset keeps it.
## 6. Instruction word
```
31 27 26 22 21 17 16 12 11 7 6 2 1 0
+--------+--------+--------+--------+--------+--------+----+
| slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | xx |
+--------+--------+--------+--------+--------+--------+----+
```
- **32 bits at every cell width (D-9).** On a 64-bit machine the instruction word is the low 32
bits of the cell, and the high half is ignored. Compiled code is identical at 32 and 64.
Literals and data are a full cell.
- **Slots execute left to right.**
- **A branch** (`jump call next if -if`) takes its target from every bit to the right of its
slot, the 2 spare bits included. The target replaces the same low bits of P, and the bits of
P above them are kept. Branches are page-relative, as on the F18.
| Branch in slot | Address bits | Reach |
|---|---|---|
| 0 | 27 | 128M words |
| 1 | 22 | 4M words |
| 2 | 17 | 128K words |
| 3 | 12 | 4K words |
| 4, 5 | 7, 2 | outside the spec. The model does not trap it; it applies the 7- or 2-bit target. |
- **The spare bits** are never executed. They are address bits for a branch in any slot.
- **The low bits of a non-branch slot** are ignored by that opcode.
## 7. Opcodes
"Takes" and "leaves" are what the control unit checks before the opcode runs (§4). "d" is the
data stack, "r" is the return stack.
| Op | Name | Effect | Takes | Leaves room for |
|---|---|---|---|---|
| 00 | `;` | P ← R, pop R | r 1 | |
| 01 | `ex` | swap P and R | r 1 | |
| 02 | `jump a` | P ← a | | |
| 03 | `call a` | push P to R; P ← a; +1 heat of a | | r 1 |
| 04 | `unext` | if R ≠ 0: R ← R−1, and the same word runs again from slot 0, with no refetch and P unchanged. Else pop R and go on to the next slot. | r 1 | |
| 05 | `next a` | if R ≠ 0: R ← R−1, P ← a. Else pop R. | r 1 | |
| 06 | `if a` | if T = 0: P ← a. T is not popped. | d 1 | |
| 07 | `-if a` | if T ≥ 0 (sign bit clear): P ← a. T is not popped. | d 1 | |
| 08 | `@p` | push the cell at P; P ← P+1 | | d 1 |
| 09 | `@+` | push the cell at A; A ← A+1 | | d 1 |
| 0A | `@b` | push the cell at B | | d 1 |
| 0B | `@` | push the cell at A | | d 1 |
| 0C | `!p` | cell at P ← T, pop; P ← P+1 | d 1 | |
| 0D | `!+` | cell at A ← T, pop; A ← A+1 | d 1 | |
| 0E | `!b` | cell at B ← T, pop | d 1 | |
| 0F | `!` | cell at A ← T, pop | d 1 | |
| 10 | `+*` | if A bit 0: T ← T+S (the carry out is not kept, D-3). Then T:A shift right one bit: T keeps its sign bit, T's bit 0 goes into A's top bit. S is not popped. | d 2 | |
| 11 | `2*` | T ← T << 1 | d 1 | |
| 12 | `2/` | T ← T >> 1, arithmetic | d 1 | |
| 13 | `inv` | T ← ~T (F18 `-`) | d 1 | |
| 14 | `+` | T ← S + T, pop. Wraps. | d 2 | |
| 15 | `and` | T ← S & T, pop | d 2 | |
| 16 | `xor` | T ← S ^ T, pop (F18 `or`) | d 2 | |
| 17 | `drop` | pop T | d 1 | |
| 18 | `dup` | push T | d 1 | d 1 |
| 19 | `pop` | pop R, push it onto the data stack | r 1 | d 1 |
| 1A | `over` | push S | d 2 | d 1 |
| 1B | `a` | push A | | d 1 |
| 1C | `nop` | nothing | | |
| 1D | `push` | pop T, push it onto R | d 1 | r 1 |
| 1E | `b!` | B ← T, pop | d 1 | |
| 1F | `a!` | A ← T, pop | d 1 | |
**Arithmetic.** All arithmetic wraps, two's complement, at the cell width. There is no carry
flag, no overflow flag and no trap.
**Names.** Two names differ from Moore's: `inv` is F18 `-`, and `xor` is F18 `or`. No opcode
name is a FORTH-79 word name (`opcode.h`).
## 8. Control unit
One step (`v4_exec_step_word`, `exec.c`):
1. **Stopped or blocked?** If the machine has stopped (§9) or is blocked at a port or at Do
(§10.4), nothing happens.
2. **Resume.** If the machine was blocked part-way through a word and has since been served, it
continues from the saved word and slot. A fault while it was blocked cancels the resume.
3. **Fetch.**
- P is checked against memory (§5).
- If P is a port, the word is whatever the neighbour writes there, and P does not advance.
This is how a newborn node runs code sent to it.
- Otherwise the word is the cell at P, and P ← P+1.
4. **Slots, from 0 (or the resumed slot) to 5.** For each:
- Check the stacks (§4). Then run the opcode. Then count +1 heat for the opcode and +1 on the
anti-clock (§11). An opcode that faults does not retire. A raised error retires (§9).
- If the opcode was a fetch from a port with nothing written yet, the machine blocks before
the opcode, and it will resume at this slot.
- If it faulted, the rest of the word is abandoned, and P is the handler.
- If it stored to a port or to Do, the machine blocks after the opcode, and it will resume at
the next slot.
- If it was `unext` with R ≠ 0, the same word runs again from slot 0.
- If it was a branch, `;` or `ex`, the word ends. Nothing to the right of a branch executes,
taken or not.
5. The step returns the number of opcodes retired.
**`@p` and `!p` in an `unext` loop** walk the cells after the loop word, because P is not moved
by the restart. This is the F18's behaviour.
## 9. Faults
| Kind | Code | Cause |
|---|---|---|
| Address | 0 | An address outside memory (§5). Also a store to a read-only port address (§10.4). |
| Data overflow | 1 | A push onto a full data stack |
| Data underflow | 2 | The data stack holds less than the opcode takes |
| Return overflow | 3 | A push onto a full return stack |
| Return underflow | 4 | The return stack holds less than the opcode takes |
| Raised | 5 | A non-zero store to NODE-ERROR (§10.5) |
**On a fault** (`v4_node_fault`, `node.c`):
- The kind and, for an address fault, the address are recorded, and the fault count goes up.
- The return stack is emptied. The data stack is emptied too, except for a raised error, which
keeps it.
- P ← fault table + kind.
The **fault table** is 6 words of RAM, one per kind, each normally a jump to that kind's
handler. A handler does not return to the faulting program.
**With no fault table** the machine **stops**: no further step does anything until it is reset
or a table is attached.
## 10. Memory-mapped I/O
Each device is placed at an address by whoever owns the machine. In the model this is done by an
`attach` call (`node.h`). Fixed addresses are D-4, deferred to the mesh and the RTL (§13).
`v4_node_reset` detaches every device.
### 10.1 Console transmit — CONSOLE-TX
A store sends the low 8 bits as one character. RAM is not written. A fetch reads RAM.
### 10.2 Console receive — CONSOLE-RX, CONSOLE-STATUS
- A fetch from **STATUS** gives −1 if a character is pending and 0 if none is. It changes
nothing.
- A fetch from **RX** takes the next character, 0–255, or gives −1 if there is none.
- Neither address blocks. A program that must wait polls STATUS.
- A store to either is an ordinary RAM store.
- An instruction or literal fetch at these addresses always reads RAM.
### 10.3 Stack depth — DSTACK-DEPTH, RSTACK-DEPTH (D-16)
- A fetch gives that stack's depth. For the data stack, the fetch's own push is not counted.
- A store empties that stack. For the data stack, the value stored is taken off first.
### 10.4 Ports (`DECOMPOSITION.md` §6, `MESH.md` 4.1, 7d.3)
At the port base address:
| Offset | Name | Fetch | Store |
|---|---|---|---|
| 0 … PORTS−1 | port k | Blocks until the neighbour on k writes, then gives that word | Blocks until the neighbour on k has taken the word |
| PORTS | any | Blocks until a neighbour on any port writes | address fault |
| PORTS+1 | which | The port the last "any" read came from | address fault |
| PORTS+2 | writers | bit k: the neighbour on k is waiting to write here | — |
| PORTS+3 | readers | bit k: the neighbour on k is waiting to read. Bit PORTS+k: port k has anything on it at all. | — |
| PORTS+4 | Wire | what was stored | the port the next operation is about |
| PORTS+5 | A | what was stored | an operand |
| PORTS+6 | B | what was stored | an operand |
| PORTS+7 | Do | the operation number | **Blocks** until the operation is done (whole messages) |
| PORTS+8 | How | how the last operation went | address fault |
| PORTS+9 | Have | bit k: a message is waiting on wire k | address fault |
A read port also works as an execution source: if P is a port, the machine executes what
arrives (§8, step 3). A newborn node (`v4_node_born`) has P at "any" and blocks there, empty,
until a neighbour sends it code.
### 10.5 Error register — NODE-ERROR (D-18)
- A store of a non-zero value writes RAM, then raises a fault of kind 5. The value stored is
recorded as the fault address.
- A store of 0 is an ordinary store, which is how the flag is cleared.
- Not attached, it is plain RAM.
## 11. Side effects: heat and the anti-clock
Not instructions. They cost nothing (`DECOMPOSITION.md` §1.4, `heat.h`):
- Every retired opcode adds 1 to that opcode's heat counter, `HEAT-OP[0..31]`, and 1 to the
**anti-clock**.
- Every `call` adds 1 to its target's heat counter, `HEAT-CALL[target]`.
- Counters are 64 bits at every cell width (`JUSTIFICATION.md` §5).
They are not yet memory-mapped: the §7 addresses wait on D-4.
## 12. Reset and birth
**`v4_node_reset`:**
- P, A and B are set to 0.
- Both stacks are emptied, and every RAM cell is set to 0.
- Every device is detached, and the fault table is detached.
**`v4_node_born(base)`:** a reset, then ports at `base`, then P = `base + PORTS` ("any"). The
machine blocks there, empty, until it is sent code.
## 13. Not yet as intended
These are reported, not hidden:
1. **Per-call-target heat** is the model's interim structure: `heat.h` says "in the meantime",
and D-6 is deferred. It is a fixed 4096-entry table, and a call to a target at address 4096
or above is not counted. This is the stand-in recorded 2026-10-05. It is not the intended mechanism,
and it is not to be extended.
2. **Device addresses are attached by the owner, not fixed.** D-4 (the memory map) is deferred.
The FPGA needs fixed addresses.
3. **The ROM range** is designed (`MACHINE.md`) and not built.
4. **`stack.h`'s opening comment** still says there is no overflow detection "and there never
will be". D-16 revised that, and the header carries a revision note above it. The code
counts and guards (§4). The comment's first paragraph is out of date.
5. **Branches in slots 4 and 5** are outside the spec but not trapped (§6).
## 14. What is not in the machine
No FORTH, no interpreter, no compiler, no assembler-in-hardware, no dictionary, no names, no
vocabularies, no boot policy. The ROM (`MACHINE.md` §2) is F18 code the machine runs. It is not
part of this sheet's hardware. Everything above the opcodes arrives as source.