Design Rationale & Microarchitecture

This page explains the whys — the decisions you cannot see in the RTL, including the failures that led to them.

Why a register-driven controller with programmable frames?

The project started with a hard-wired SQI backend plus a 4 KiB direct-mapped cache as prefetch (tag archive/cache-xip-ref). That direction was deliberately abandoned:

cache + fixed backend (old)

FIFO + programmable frames (new)

second chip (flash)

new backend needed

just a different register setup

area cost

4 KiB tag+data RAM

32-byte FIFO

sequential fetch

cache-line fill

streaming without line boundaries

random access

cache hit possible

always a stream restart

software model

implicit

documented CSR model

For the main use case — sequential code execution from external memory — the stream prefetch is practically on par with the cache at a fraction of the complexity. Random data accesses are slower but correct. The register-driven CSR model (control/frame/status registers with trigger rules and an abort flow) follows the programming model established by commercial MCU quad-SPI peripherals, so the software patterns are well-proven and documented.

The job as central abstraction

qspi_cmd_seq knows neither registers nor bus windows — it executes exactly one qspi_job_t. Indirect mode and memory-mapped mode therefore share the same, once-verified datapath; qspi_regs and qspi_mmap are pure job factories. Arbitration is trivial because CCR.FMODE makes the sources mutually exclusive (no locking, no priorities).

Phase FSM (qspi_cmd_seq)

        start                 tick
 StIdle ─────► StCsLow(CS↓) ─────► StInstr ─► StAddr ─► StAlt ─► StDummy ─► StData
   ▲                                  │  (each phase: *MODE=00 → skip;           │
   │                                  │   abort_i → abort at a byte boundary)    │
   │            CSHT ticks            ▼                                          │
   └───────── StCsht ◄──── StCsHold(CS↑) ◄───────────────────────────────────────┘
                                             (data done ∨ abort)

Each phase reads its *MODE field and works byte by byte: one order to qspi_shift per byte, with arbitrary time allowed in between. This is the design’s most important simplification — it is protocol-legal because static SPI slaves only react to edges (SCK pauses with CS low are invisible). It makes three problems trivial that would otherwise need a tightly coupled pipeline:

  • TX underrun (indirect write, FIFO empty): simply start no new byte, SCK rests.

  • RX backpressure (FIFO full): the transfer pauses; hysteresis: resumes at ≥ 4 bytes free (avoids stutter at the full boundary).

  • Turnaround/dummy: its own shift operation with oe=0 and N clocks.

The dummy phase counts SCK clocks (not bytes) because the target chips need different granularity (SRAM SQI: 2 clocks; flash 0xEB: 4; 0x0B/0x6B: 8).

Shift unit: registered I/O and the first-bit trap

qspi_shift updates outputs on the falling and samples on the rising edge (mode 0). The first bit of an output is driven at the start of the operation (not at the first edge) — otherwise the device samples an invalid bit on the first rising edge. Between operations the unit holds the last drive values (harmless, no edges) but releases the lanes on dummy/input/CS end.

An RX byte is captured into its own register on done before the FIFO push (registered, +1 cycle) runs — the next shift order may overwrite the shift register immediately. The backpressure threshold accounts for 1 byte of headroom (free ≥ 2 instead of ≥ 1).

One FIFO instead of four

TX data, RX data, prefetch and mmap write staging share one 32-byte FIFO with a 4-byte bus port and a 1-byte serial port. Possible because all four uses are mutually exclusive in time (FMODE + BUSY). The price is muxing in qspi_top and the software rule “push exactly DLR+1 bytes”; the gain: one memory, one verification, and the prefetch buffer comes “for free”. Depth 32 covers one 23LC1024 page (32 B) and ≥ 8 prefetch words.

mmap frontend: stream semantics

qspi_mmap tracks the chip address of the FIFO head (head). A read at head is a hit (pop word, head += 4); anything else is a jump: abort (byte boundary!), FIFO flush, new unlimited read job. Why unlimited? The sequencer then needs no notion of length for XIP, and backpressure naturally limits the lookahead to the FIFO depth. Consequence: BUSY stays 1 permanently during mmap operation — documented, and the reason for the mandatory abort flow on mode changes. A wait counter in the serve state detects an externally aborted stream (software ABORT despite mmap) and restarts it instead of hanging.

mmap writes stage their 1–4 bytes in the (freshly flushed) FIFO and run as an ordinary write job via WCCR. A second CCR instead of bit-fiddling in one: read and write frames differ on real chips almost always (SRAM: dummy only on read; flash: no write at all) — and WCCR.DMODE=00 doubles as the write-protect semantics.

Deliberate feature-set boundaries

Not implemented: interrupts (TCF/TEF/…), FIFO threshold flag, automatic status polling, DMA, dual-SPI in the controller datapath (the models support it), instruction-only-once optimisation (SIOO), DDR, mmap timeout, CDC for SCK > clk/4. Each of these is a candidate for a later extension; the register map leaves room at the customary spots.