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=0and 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.