docs: draft SPI protocol v1 for Phase 4 (opcodes, register map)

Phase 4 (SPI Interface) only had a high-level conceptual sequence
(RESET/CONFIGURE/LOAD.../START/WAIT/READ) with no concrete opcodes,
framing, or register map -- not enough to start RTL from. Added
docs/FPGA-NeuralNetwork-Engine.md §8.1 with a concrete v1 draft:

- SPI Mode 0, MSB-first, one opcode byte per CS-low transaction.
- Explicit length field on WRITE_RAM/READ_RAM (chosen over
  CS-edge-delimited streaming: simpler controller, just a byte
  counter).
- READ_CONFIG opcode exposing N_INPUTS/N_NEURONS/PARALLEL/
  ADDR_WIDTH/DATA_WIDTH at runtime, so one host firmware build can
  target different bitstreams.
- RESET kept as its own opcode (0x0F), distinct from NOP.
- STATUS.done documented as required to be a STICKY, clear-on-read
  bit in the SPI register bank: neuron_memory.done is a one-cycle
  pulse that a slow SPI poll would almost certainly miss otherwise.

Opcode values themselves are marked explicitly as draft/example,
not frozen -- only the framing rules and the two decisions above are
meant to stick going into Phase 4 RTL work.

No RTL or testbench changes in this commit; design-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
This commit is contained in:
2026-09-02 15:04:37 +02:00
co-authored by Claude Sonnet 5
parent 661363f637
commit 87efce3d9b
2 changed files with 103 additions and 0 deletions
+87
View File
@@ -383,6 +383,87 @@ WAIT FOR DONE
READ OUTPUT
```
## 8.1 SPI Protocol v1 (draft, 2026-09-02)
Concrete opcode-level draft of the section above, written before any
Phase 4 RTL. Opcode values and the exact set of commands are
illustrative/example at this stage, not frozen — the framing rules
(MSB-first, explicit length, sticky STATUS.done) and the two
decisions already made (explicit length field over CS-delimited
streaming; a runtime READ_CONFIG command) are the parts intended to
stick; the opcode table itself is expected to be revised as Phase 4
RTL work starts.
**Physical layer:** SPI Mode 0 (CPOL=0, CPHA=0), MSB-first, single
SPI for v1 (Dual SPI is a future extension per §8, not addressed
here). The FPGA is always SPI slave. One command per CS-low period;
byte 0 of every transaction is the opcode.
**Multi-byte fields** are big-endian (most significant byte first).
Byte addresses are `ADDR_WIDTH`-bit (22 bits today, from
`rtl/neuron_memory.v`), carried in a 3-byte field with the top 2 bits
reserved as 0.
**Length is explicit**, not CS-edge-delimited: `WRITE_RAM`/`READ_RAM`
carry a 2-byte length field, so the SPI controller only needs a byte
counter, not CS-edge detection mid-transfer.
### Opcode table
| Opcode | Name | Payload (host → FPGA) | Response (FPGA → host) | Function |
|---|---|---|---|---|
| 0x00 | NOP | — | — | No operation (idle/dummy clocking) |
| 0x01 | WRITE_RAM | addr(3B) + len(2B) + `len` data bytes | — | Write a block into PSRAM (X, weights, bias, network params) |
| 0x02 | READ_RAM | addr(3B) + len(2B) | `len` data bytes | Read a block back from PSRAM |
| 0x0F | RESET | — | — | Synchronous reset pulse to the compute engine (`neuron_memory`) and clears the STATUS latch below. Does **not** erase PSRAM contents. Kept as a distinct opcode from NOP. |
| 0x10 | SET_BASE | sel(1B) + addr(3B) | — | Sets `x_base`(sel=0) / `w_base`(sel=1) / `bias_addr`(sel=2) |
| 0x20 | START | — | — | Pulses `start`; ignored (no-op) if `busy=1` |
| 0x21 | STATUS | — | 1 byte | bit0=`busy` (live), bit1=`done` (**sticky, clear-on-read**), bits7:2 reserved=0 |
| 0x22 | READ_OUTPUT | — | `N_NEURONS` bytes | `y_bus`, neuron-major (byte 0 = neuron 0) |
| 0x30 | READ_CONFIG | — | 8 bytes | Hardware config record, see below |
**Why STATUS.done is sticky / clear-on-read:** in `rtl/neuron_memory.v`
`done` is a single-cycle pulse (asserted for exactly one clock in
`STATE_WAIT_N`, deasserted the next cycle). A host polling over SPI
— orders of magnitude slower than the FPGA clock — would almost
certainly miss a raw one-cycle pulse. The SPI register bank must
therefore latch `done` into a sticky bit on the pulse, and clear it
when the host issues `STATUS` (or `RESET`), not sample the raw
`neuron_memory.done` signal directly. `busy` has no such problem
(it is level-held for the whole computation) and can be read live.
**READ_CONFIG payload** (fixed 8 bytes, lets one host firmware build
work across different bitstreams without recompiling):
| Byte(s) | Field | Source |
|---|---|---|
| 0 | `ADDR_WIDTH` (bits) | `neuron_memory.ADDR_WIDTH` |
| 12 | `N_INPUTS` (16-bit BE) | `neuron_memory.N_INPUTS` |
| 3 | `N_NEURONS` | `neuron_memory.N_NEURONS` |
| 4 | `PARALLEL` | `neuron_memory.PARALLEL` |
| 5 | `DATA_WIDTH` (bits) | `neuron_memory.DATA_WIDTH` |
| 67 | protocol version (16-bit BE) | `0x0001` for this spec |
**Example session** (fills in the conceptual sequence above with
concrete opcodes):
```text
RESET -> 0x0F
READ_CONFIG -> 0x30 (host learns N_INPUTS/N_NEURONS/...)
WRITE_RAM (weights) -> 0x01 ...
WRITE_RAM (biases) -> 0x01 ...
SET_BASE (X/W/BIAS) -> 0x10 x3
WRITE_RAM (input X) -> 0x01 ...
START -> 0x20
poll STATUS -> 0x21 (until done bit set; clears on this read)
READ_OUTPUT -> 0x22
```
Not yet decided / explicitly out of scope for v1: Dual SPI framing,
a CRC/checksum on transfers (SPI is assumed reliable for a
board-level trace in v1), and multi-layer sequencing commands (that
belongs to Phase 5, once intermediate buffers exist).
---
# 9. Dedicated FPGA RAM
@@ -671,6 +752,12 @@ Implement:
- input/output protocol;
- status and control.
- [x] Protocol/opcode set drafted — see §8.1 SPI Protocol v1
- [ ] SPI controller RTL (physical layer: shift register, CS/clock sync)
- [ ] Register bank RTL (SET_BASE, sticky STATUS, READ_CONFIG constants)
- [ ] RAM access passthrough RTL (WRITE_RAM/READ_RAM -> memory_interface)
- [ ] Testbench (SPI master BFM + full stack, mirroring neuron_memory_tb.v style)
## Phase 5 — Multi-Layer Network
Implement: