\chapter{Memory subsystem} \label{ch:mem} \section{Memory chain} The compute engine works with addresses and data at the \emph{byte} level (INT8), while the PSRAM is a 16-bit word device. Three cascaded modules realize the conversion and the physical access: \begin{center} \begin{tikzpicture}[font=\scriptsize,node distance=8mm] \node[fnblockD,minimum width=30mm,minimum height=12mm](nm){byte-level master\\{\scriptsize \code{neuron\_memory} / \code{spi\_engine} / \code{layer\_sequencer}}}; \node[fnblockT,right=10mm of nm,minimum width=28mm,minimum height=12mm](ia){\code{int8\_memory\_access}\\{\scriptsize byte $\leftrightarrow$ 16-bit word}}; \node[fnblock,right=10mm of ia,minimum width=26mm,minimum height=12mm](mi){\code{memory\_interface}\\{\scriptsize IDLE/WAIT FSM}}; \node[fnblockA,below=9mm of mi,minimum width=26mm,minimum height=12mm](pc){\code{psram\_controller}\\{\scriptsize async 70\,ns physical bus}}; \node[fnblock,left=10mm of pc,minimum width=26mm,minimum height=12mm](ps){PSRAM\\{\scriptsize 8\,MB 4M$\times$16}}; \draw[fnbus] (nm)--node[fnlbl,above]{req/wr/addr}(ia); \draw[fnbus] (ia)--node[fnlbl,above]{16-bit}(mi); \draw[fnbus] (mi)--(pc); \draw[fnbus] (pc)--node[fnlbl,above]{DQ/A/ctrl}(ps); \end{tikzpicture} \end{center} \section{\texttt{int8\_memory\_access} --- byte/word conversion} Converts the INT8 interface (byte address) into the word interface. The byte address is divided by two (\code{addr>>1}) to obtain the word address; the least significant bit selects the byte: \begin{itemize} \item \code{addr[0]=0} $\to$ low byte: \code{lb\_n=0}, \code{ub\_n=1}, data on DQ[7:0]; \item \code{addr[0]=1} $\to$ high byte: \code{lb\_n=1}, \code{ub\_n=0}, data on DQ[15:8]. \end{itemize} On read it extracts the correct byte from \code{mem\_rdata}. The FSM has two states (IDLE, WAIT) and returns \code{ready} as a one-cycle pulse. \section{\texttt{memory\_interface} --- handshake} Two-state FSM that serializes a single transaction: in IDLE, on the \code{req} request, it latches \code{wr/addr/wdata/lb\_n/ub\_n} and emits a one-cycle \code{mem\_req} pulse toward the controller; in WAIT it waits for \code{mem\_ready}, captures \code{rdata} on read and asserts \code{ready}. It guarantees the ``one transaction at a time'' contract. \section{\texttt{psram\_controller} --- physical bus} Asynchronous parallel PSRAM bus controller, with support for the chip's read \textbf{page mode} (\S~\ref{sec:pagemode}). The main state machine is: \begin{center} \begin{tikzpicture}[font=\scriptsize] \node[fnstate](init) at (0,0){INIT}; \node[fnstate](idle) at (3.2,0){IDLE}; \node[fnstate](read) at (7,2.7){READ}; \node[fnstate](popen) at (11,2.7){PAGE\\OPEN}; \node[fnstate](write) at (7,-2.7){WRITE}; \node[fnstate](ww) at (11,-2.7){WRITE\\WAIT}; \draw[fnarrow] (init)--node[fnlbl,above]{INIT\_CYCLES + CR load}(idle); \draw[fnarrow] (idle)--node[fnlbl,above,sloped]{req \& !wr}(read); \draw[fnarrow] (idle)--node[fnlbl,below,sloped]{req \& wr}(write); \draw[fnarrow] (read)--node[fnlbl,above]{ready}(popen); \draw[fnarrowT] (popen) to[bend left=25] node[fnlbl,below]{req \& !wr}(read); \draw[fnarrow] (popen) to[bend right=20] node[fnlbl,above,sloped]{req \& wr}(write); \draw[fnarrow] (popen) to[out=-100,in=15,looseness=1.15] node[fnlbl,pos=0.55]{tCEM timeout}(idle); \draw[fnarrow] (write)--node[fnlbl,above]{ACCESS\_CYCLES}(ww); \draw[fnarrow] (ww) to[out=160,in=-70] node[fnlbl,pos=0.5,left]{ready}(idle); \end{tikzpicture} \end{center} From INIT the controller automatically goes through a configuration-register load sub-sequence (\code{STATE\_CR\_INIT}, 4 steps) before reaching IDLE for the first time --- see \S~\ref{sec:pagemode}. The PAGE~OPEN~$\to$~WRITE transition (bottom-right arrow) internally passes through two transit micro-states, \code{STATE\_PAGE\_CLOSE} and \code{STATE\_PAGE\_REOPEN} (one cycle each): the first forces CE\#/OE\# high for at least one cycle before the controller starts driving the data bus, avoiding contention with the PSRAM's still-active output ($\geq t_{HZ}$); the second restarts the already-latched transaction exactly as IDLE would. They are not drawn as separate nodes to keep the figure readable. \subsection{Timing} \begin{fnspec}[Timing formulas] $\text{ACCESS\_CYCLES}=\lceil (70\times \text{CLK\_FREQ\_MHZ})/1000\rceil$ \quad (random-access latency, $t_{AA}$/$t_{RC}$ = 70~ns)\\[3pt] $\text{PAGE\_CYCLES}=\lceil (20\times \text{CLK\_FREQ\_MHZ})/1000\rceil$ \quad (same-page continuation, $t_{APA}$/$t_{PC}$ = 20~ns)\\[3pt] $\text{INIT\_CYCLES}=150\times \text{CLK\_FREQ\_MHZ}$ \quad (power-up initialization, $t_{PU}$ = 150~\textmu s)\\[3pt] $\text{PAGE\_TIMEOUT\_CYCLES}=\lceil (6000\times \text{CLK\_FREQ\_MHZ})/1000\rceil$ \quad (automatic page close, safety margin under $t_{CEM}$ = 8~\textmu s) \end{fnspec} The data bus is tri-state driven: \code{psram\_dq = dq\_oe ? dq\_out : Z}. On read \code{dq\_oe=0}; on write \code{dq\_oe=1} during the \code{we\_n} pulse. A WRITE\_WAIT state keeps \code{ce\_n/lb\_n/ub\_n} active for the final hold before release. \begin{fnwarn}[This is not QSPI] This is a classic asynchronous-SRAM interface, \textbf{not} QSPI: most commercial serial/QSPI ``PSRAM'' parts are not compatible with this controller without a rewrite. See ch.~\ref{ch:hw} for the recommended part (parallel ISSI). \end{fnwarn} \section{Read page mode} \label{sec:pagemode} The recommended chip (ch.~\ref{ch:hw}) is ``asynchronous/\textbf{page mode}'': once an initial random access at $t_{AA}$~=~70~ns has been done, further reads inside the same 16-word page (address bits above \code{A[3]} unchanged) only cost $t_{APA}$/$t_{PC}$~=~20~ns, because CE\#/OE\# stay asserted and only the address bus changes. Page mode is \textbf{disabled by default} at power-up (bit~7 of the configuration register, CR~=~\texttt{0x0070} by default) and must be explicitly enabled. \begin{itemize} \item \textbf{Enable at boot}: right after INIT, the controller runs the datasheet's ``software-access sequence'' (2 dummy reads + 2 writes, \texttt{0x0000} unlock then real CR \texttt{0x00F0} = default with the Page bit set) at the chip's highest address --- it reuses exactly the same READ/WRITE logic as every other transaction, so it goes through the same timing checks. \item \textbf{Page bursts}: after a READ the controller no longer closes CE\#/OE\# (PAGE~OPEN state). A following read in the same page only waits PAGE\_CYCLES; a read crossing into a different page still avoids a CE\# toggle but pays a full ACCESS\_CYCLES for that one word (any change at \code{A[4]} or above requires a new $t_{AA}$). A counter closes the page before the $t_{CEM}$ limit with a safety margin. \item \textbf{Only a WRITE closes the page.} Changes to \code{lb\_n}/\code{ub\_n} do \emph{not} close it: \code{int8\_memory\_access} alternates these signals on nearly every access (byte-granular access over the 16-bit bus), so treating them as a close condition --- the first implementation attempt --- made the real workload \emph{slower}, not faster (measured: 53.25$\to$61.25 cycles/edge on \code{graph\_engine}'s gather); removed, corrected to 53.25$\to$37.53 cycles/edge (bandwidth +42\%, \S~\ref{sec:bandwidth}). \end{itemize} \begin{fnwarn}[No benefit without a sequential pattern] Page mode only speeds up accesses that stay in the same page (or nearly) while the controller is waiting for a new request with the page still open. Isolated, scattered accesses (a random address every time) still pay a full ACCESS\_CYCLES, plus a small close/reopen overhead if preceded by a WRITE or a $t_{CEM}$ timeout: it is not a universal win, it depends on the caller's access pattern. \end{fnwarn} Real Fmax (\code{nextpnr-ecp5}, ch.~\ref{ch:impl}) on the integrated \code{spi\_neuron\_top} system with Type~\#2 enabled: \textbf{75.73~MHz} at \code{PARALLEL}=2 (was 55.59~MHz before page mode was added) and \textbf{65.13~MHz} at \code{PARALLEL}=8, both still FAIL against the 80~MHz target but not regressed. The critical path stays, in both cases, entirely inside \code{u\_graph\_engine.u\_neuron} (the \code{mac8}/\code{neuron\_parallel} accumulate chain, ch.~\ref{ch:impl}) --- \code{psram\_controller} never appears in the critical path despite page mode's resource growth. \section{Address map and conventions} The addressing space is \code{ADDR\_WIDTH}=23~bits (\emph{byte} address), for a full 8~MB. The regions do not have hardwired addresses: their bases are registers set by the host via \op{SET\_BASE} (single-layer path) or read from the descriptor table (multi-layer path). \begin{tabularx}{\textwidth}{L{3.2cm} L{3.4cm} Y} \toprule \rowh \thd{Region} & \thd{Base} & \thd{Content / convention} \\ \midrule Input $X$ & \code{x\_base} & Shared input vector, read once per invocation. \\ \rowa Weights $W$ & \code{w\_base} & Neuron-major: weights of neuron $n$ at \code{w\_base + n*N\_INPUTS} bytes. \\ Bias & \code{bias\_addr} & One byte per neuron: bias of neuron $n$ at \code{bias\_addr + n}. \\ \rowa Descriptor table & \code{table\_base} & \code{N\_LAYERS} 11-byte entries (ch.~\ref{ch:seq}). \\ Ping-pong buffers A/B & \code{buf\_a\_base} / \code{buf\_b\_base} & Intermediate outputs between layers. \\ \bottomrule \end{tabularx} \subsection{PSRAM physical addressing} The recommended PSRAM is 4M$\times$16 (8~MB), which requires a 22-bit word address (A0--A21). \code{int8\_memory\_access} computes \code{addr>>1}, turning the 23-bit byte address into a 22-bit word address that maps exactly onto A0--A21; bit~22 of \code{psram\_a} is therefore always 0 and 22 real address lines remain on the PCB. \section{Bandwidth} \label{sec:bandwidth} Measured on \code{graph\_engine}'s edge-list gather (ch.~\ref{ch:grafo}), by difference between two graph sizes to isolate the per-edge cost from the fixed per-neuron overhead (\code{sim/graph\_engine\_bandwidth\_tb.v}): \begin{tabularx}{\textwidth}{L{5.2cm} Y Y Y} \toprule \rowh \thd{} & \thd{Before (no page mode)} & \thd{After (page mode)} & \thd{$\Delta$} \\ \midrule Cycles/edge & 53.25 & 37.53 & $-29.5\%$ \\ \rowa Bandwidth @80\,MHz & 6.01\,MB/s & 8.53\,MB/s & $+41.9\%$ \\ Bandwidth @16\,MHz\textsuperscript{*} & 1.20\,MB/s & 1.71\,MB/s & $+41.9\%$ \\ \bottomrule \end{tabularx} \textsuperscript{*}recommended real oscillator (ch.~\ref{ch:hw}). The model still remains memory-bound by construction: \code{neuron\_memory} reads $X$ once and re-reads $W$/bias for each neuron (ch.~\ref{ch:seq}), one neuron at a time; page mode reduces the per-byte cost of a sequential access, it does not eliminate the access pattern itself.