Files
FPGA-Neural/rtl/spi_engine.v
T
micheleandClaude Sonnet 5 55c827bedf feat: PSRAM page-mode reads + graph engine (Type #2) + real pinout/IRQ pins
PSRAM page-mode read burst support in psram_controller.v: enables the
ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register
software-access sequence at boot (disabled by default on the real
chip), then keeps CE#/OE# asserted after a read so a same-page
continuation only pays tAPA (20ns) instead of a full tAA (70ns)
random access, with automatic tCEM-safe session closing. Only a WRITE
closes the page -- byte-enable changes do not, since
int8_memory_access.v alternates them on nearly every access and an
early implementation attempt that treated them as a close condition
measured a real regression (53.25->61.25 cycles/edge) before being
corrected (53.25->37.53 cycles/edge, +42% gather bandwidth).
sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement
(with a real Verilog same-timestep event-ordering race found and
fixed via a #0 sync) so the regression proves real timing compliance,
not just data correctness. New sim/psram_page_mode_tb.v; full 26-file
regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on
the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and
65.13MHz (P8) -- still under the 80MHz target but not regressed, with
the critical path confirmed (not assumed) to remain entirely inside
neuron_parallel's accumulate chain, never psram_controller.

Also includes this session's other already-validated work: the graph
engine (Type #2 sparse-graph network: act_buffer, graph_engine,
netasm host assembler), real CABGA381 pinout (.lpf, place&route
verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing
closure logs -- all previously uncommitted, documented in WORKLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
2026-09-03 17:12:05 +02:00

756 lines
29 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_ENGINE - opcode/protocol FSM + register bank
//
// Implements the v1 draft protocol in docs/FPGA-NeuralNetwork-Engine.md
// §8.1 on top of the byte-level interface exposed by spi_slave.v.
// One opcode byte per CS-low transaction (§8.1 framing).
//
// IMPORTANT (see rtl/spi_slave.v for the full contract):
// - tx_byte is driven COMBINATIONALLY from current state, so it is
// always correct whenever spi_slave.v prefetches it (at cs_fell
// and at every byte boundary) -- no explicit reaction needed.
// - Any stateful pointer (RAM address, response byte index) is
// advanced on rx_valid, which fires exactly once per REAL byte
// transferred -- never on tx_byte_req, which fires one extra
// "phantom" time after the last byte of a transaction.
//
// RAM byte-level master port uses the same convention as
// neuron_memory.v's external mem_* port (byte address, byte data,
// req/ready handshake) so it can share an arbiter + int8_memory_access
// + memory_interface chain with neuron_memory at the top level.
//
// v1 LIMITATION (documented, not yet solved): WRITE_RAM/READ_RAM
// have no backpressure to the SPI master. Each received/produced
// byte must be fully processed by this engine before the next
// SCLK-driven byte boundary arrives, i.e. the host must not clock
// RAM-touching commands faster than one RAM transaction (a handful
// of `clk` cycles) per SPI byte period. This is a reasonable
// constraint for bulk-loading weights/bias/input at initialization,
// not a real-time path.
// ================================================================
module spi_engine #(
parameter ADDR_WIDTH = 23,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8,
parameter N_TOTAL = 4096 // Phase G5: graph (Type #2) activation buffer depth, exposed via READ_CONFIG
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Byte-level interface from/to spi_slave.v
// ------------------------------------------------------------
input wire [7:0] rx_byte,
input wire rx_valid,
input wire cs_start,
input wire cs_end,
output wire [7:0] tx_byte,
input wire tx_byte_req, // unused on purpose, see header note
// ------------------------------------------------------------
// RAM byte-level master port (byte address, byte data)
// ------------------------------------------------------------
output reg ram_req,
output reg ram_wr,
output reg [ADDR_WIDTH-1:0] ram_addr,
output reg signed [7:0] ram_wdata,
input wire signed [7:0] ram_rdata,
input wire ram_ready,
// ------------------------------------------------------------
// neuron_memory control
// ------------------------------------------------------------
output reg [ADDR_WIDTH-1:0] x_base,
output reg [ADDR_WIDTH-1:0] w_base,
output reg [ADDR_WIDTH-1:0] bias_addr,
output reg [1:0] activation,
output reg [15:0] n_inputs_real,
output reg [15:0] n_neurons_real,
output reg nm_start,
input wire nm_busy,
input wire nm_done, // one-cycle pulse
input wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus,
output reg nm_soft_rst,
// ------------------------------------------------------------
// layer_sequencer control (Phase 5: RUN_NETWORK opcode)
// ------------------------------------------------------------
output reg [ADDR_WIDTH-1:0] table_base,
output reg [ADDR_WIDTH-1:0] buf_a_base,
output reg [ADDR_WIDTH-1:0] buf_b_base,
output reg run_start, // one-cycle pulse
output reg [7:0] run_num_layers,
input wire seq_busy,
input wire seq_done, // one-cycle pulse
// ------------------------------------------------------------
// graph_engine control (Phase G5: Type #2 network, dispatched by
// RUN_NETWORK alongside layer_sequencer based on `net_type`; the
// top level routes the `run_start` pulse above to whichever of
// layer_sequencer/graph_engine is selected -- see
// rtl/spi_neuron_top.v)
// ------------------------------------------------------------
output reg [7:0] net_type, // 0x01=dense(#1) 0x02=graph(#2), default dense
output reg [15:0] num_neurons_graph, // SET_BASE sel 9
output reg [15:0] n_out, // SET_BASE sel 10
input wire graph_busy,
input wire graph_done, // one-cycle pulse
input wire graph_err, // sticky guard-violation flag (§7)
// ------------------------------------------------------------
// Host attention signal (active-HIGH here; spi_neuron_top.v
// inverts it to drive the physical active-low DATA_READY_N pin).
// Mirrors STATUS.bit1 exactly -- same sticky-until-STATUS-read
// latch (status_done_sticky below), so the host can either poll
// STATUS or watch this pin (or both: reading STATUS clears the
// pin too, they are the same flip-flop). IRQ_N is NOT driven
// from here -- see spi_neuron_top.v, it comes straight from
// graph_engine's own `err` (STATUS.bit2), a separate condition.
// ------------------------------------------------------------
output wire data_ready
);
// ============================================================
// OPCODES (docs §8.1 -- values are draft/example, see header)
// ============================================================
localparam OP_NOP = 8'h00;
localparam OP_WRITE_RAM = 8'h01;
localparam OP_READ_RAM = 8'h02;
localparam OP_RESET = 8'h0F;
localparam OP_SET_BASE = 8'h10;
localparam OP_SET_NET_TYPE = 8'h11;
localparam OP_START = 8'h20;
localparam OP_STATUS = 8'h21;
localparam OP_READ_OUTPUT = 8'h22;
localparam OP_RUN_NETWORK = 8'h23;
localparam OP_READ_CONFIG = 8'h30;
// SET_BASE selector values
localparam SEL_X_BASE = 8'h00;
localparam SEL_W_BASE = 8'h01;
localparam SEL_BIAS_ADDR = 8'h02;
localparam SEL_TABLE_BASE = 8'h03;
localparam SEL_BUF_A_BASE = 8'h04;
localparam SEL_BUF_B_BASE = 8'h05;
localparam SEL_ACTIVATION = 8'h06;
localparam SEL_N_INPUTS = 8'h07;
localparam SEL_N_NEURONS = 8'h08;
localparam SEL_NUM_NEURONS_GRAPH = 8'h09;
localparam SEL_N_OUT = 8'h0A;
// net_type register values (§5)
localparam NET_TYPE_DENSE = 8'h01;
localparam NET_TYPE_GRAPH = 8'h02;
// READ_CONFIG capability flags (byte 10, bit0)
localparam GRAPH_SUPPORTED = 1'b1;
// ============================================================
// STATES
// ============================================================
localparam ST_OPCODE = 4'd0;
localparam ST_SETBASE_SEL = 4'd1;
localparam ST_ADDR = 4'd2; // 3 bytes, MSB first
localparam ST_LEN = 4'd3; // 2 bytes, MSB first
localparam ST_WRITE_DATA = 4'd4;
localparam ST_WRITE_ISSUE = 4'd5;
localparam ST_WRITE_WAIT = 4'd6;
localparam ST_READ_ISSUE = 4'd7;
localparam ST_READ_WAIT = 4'd8;
localparam ST_READ_DATA = 4'd9;
localparam ST_RESP = 4'd10; // STATUS / READ_OUTPUT / READ_CONFIG
localparam ST_IGNORE = 4'd11;
localparam ST_RUNNET = 4'd12; // RUN_NETWORK: 1 payload byte (num_layers)
localparam ST_SET_NET_TYPE = 4'd13; // SET_NET_TYPE: 1 payload byte (type)
reg [3:0] state;
reg [7:0] opcode;
// Generic byte-position counter for ADDR (0..2) / LEN (0..1)
reg [1:0] byte_pos;
reg [23:0] addr_acc; // 3-byte accumulator, byte address
reg [15:0] len_acc; // 2-byte accumulator, transfer length
reg [15:0] len_remaining;
reg [ADDR_WIDTH-1:0] cur_addr;
reg pending_write;
reg [7:0] pending_wdata;
reg [7:0] cur_read_byte;
reg [3:0] resp_index; // response byte index (max needed: 8, READ_CONFIG)
reg [3:0] resp_len; // total bytes for the current response opcode
// ============================================================
// STICKY STATUS.done LATCH
//
// neuron_memory.done is a one-cycle pulse; STATUS must hold it
// until the host actually reads STATUS (or issues RESET), or a
// slow SPI poll would almost certainly miss it. See docs §8.1.
// ============================================================
reg status_done_sticky;
// Physical DATA_READY pin: the exact same flip-flop as
// STATUS.bit1, just also wired straight to a pin. No separate
// latch/clear logic needed -- it clears exactly when STATUS.bit1
// does (host reads STATUS with the sticky bit set, or RESET).
assign data_ready = status_done_sticky;
// net_mode: set while a RUN_NETWORK (multi-layer, dense) job is
// in flight (from the accepted opcode until layer_sequencer's
// final seq_done), so STATUS.done latches on the sequencer's
// seq_done rather than on each intermediate per-layer nm_done
// pulse -- see done_event below. graph_mode is the analogous
// flag for a RUN_NETWORK job dispatched to graph_engine instead
// (net_type == graph); the two are mutually exclusive by
// construction (RUN_NETWORK sets exactly one of them, §5).
reg net_mode;
reg graph_mode;
wire busy_all = nm_busy | seq_busy | graph_busy;
wire done_event = graph_mode ? graph_done : (net_mode ? seq_done : nm_done);
wire status_read_now = (state == ST_RESP) && (opcode == OP_STATUS) && rx_valid;
// status_snapshot: the STATUS byte is latched once, when the
// OP_STATUS opcode itself is accepted (ST_OPCODE, below), not
// read live/combinationally throughout ST_RESP. Without this,
// a done_event landing WHILE a STATUS response byte is already
// mid-transmission races the clear-on-read logic: the host can
// end up shifting out a stale pre-done byte while this engine
// simultaneously treats the sticky bit as "delivered" and
// clears it -- silently dropping the done transition forever
// (found via sim/spi_neuron_top_runnetwork_tb.v: a done_event
// landing mid-poll during continuous STATUS polling reproduces
// this every time). Freezing the byte at opcode-accept time and
// gating the clear on what was ACTUALLY snapshotted (below)
// closes the race: a done_event that lands too late to make it
// into this snapshot is simply reported on the next poll
// instead of being lost.
//
// bit2 = graph_err (§7 guard violation), snapshotted the same
// way -- graph_engine.err is itself sticky until rst or the next
// graph run_start, so no separate clear-on-read latch is needed
// for it here (unlike status_done_sticky).
reg [7:0] status_snapshot;
always @(posedge clk) begin
if (rst) begin
status_done_sticky <= 1'b0;
end else if (nm_soft_rst) begin
status_done_sticky <= 1'b0;
end else if (done_event) begin
status_done_sticky <= 1'b1;
end else if (status_read_now && status_snapshot[1]) begin
status_done_sticky <= 1'b0;
end
end
// ============================================================
// tx_byte: fully combinational, always reflects "the byte to
// send right now" for the current state/response index. This
// is what spi_slave.v prefetches via tx_byte_req -- see the
// module header for why this must not depend on tx_byte_req.
// ============================================================
reg [7:0] tx_byte_comb;
always @(*) begin
tx_byte_comb = 8'h00;
case (state)
ST_READ_DATA: tx_byte_comb = cur_read_byte;
ST_RESP: begin
case (opcode)
OP_STATUS: tx_byte_comb = status_snapshot;
OP_READ_OUTPUT: begin
if (resp_index < N_NEURONS)
tx_byte_comb = y_bus[resp_index*DATA_WIDTH +: DATA_WIDTH];
else
tx_byte_comb = 8'h00;
end
OP_READ_CONFIG: begin
case (resp_index)
4'd0: tx_byte_comb = ADDR_WIDTH[7:0];
4'd1: tx_byte_comb = N_INPUTS[15:8];
4'd2: tx_byte_comb = N_INPUTS[7:0];
4'd3: tx_byte_comb = N_NEURONS[7:0];
4'd4: tx_byte_comb = PARALLEL[7:0];
4'd5: tx_byte_comb = DATA_WIDTH[7:0];
4'd6: tx_byte_comb = 8'h00; // protocol version 0x0001, high byte
4'd7: tx_byte_comb = 8'h01; // protocol version 0x0001, low byte
4'd8: tx_byte_comb = N_TOTAL[15:8]; // Phase G5: graph activation buffer depth
4'd9: tx_byte_comb = N_TOTAL[7:0];
4'd10: tx_byte_comb = {7'b0, GRAPH_SUPPORTED}; // capability flags, bit0=graph
default: tx_byte_comb = 8'h00;
endcase
end
default: tx_byte_comb = 8'h00;
endcase
end
default: tx_byte_comb = 8'h00;
endcase
end
assign tx_byte = tx_byte_comb;
// ============================================================
// MAIN FSM
// ============================================================
always @(posedge clk) begin
if (rst) begin
state <= ST_OPCODE;
opcode <= 8'h00;
byte_pos <= 2'd0;
addr_acc <= 24'h0;
len_acc <= 16'h0;
len_remaining <= 16'h0;
cur_addr <= {ADDR_WIDTH{1'b0}};
pending_write <= 1'b0;
pending_wdata <= 8'h00;
cur_read_byte <= 8'h00;
resp_index <= 4'd0;
resp_len <= 4'd0;
status_snapshot <= 8'h00;
ram_req <= 1'b0;
ram_wr <= 1'b0;
ram_addr <= {ADDR_WIDTH{1'b0}};
ram_wdata <= 8'sd0;
x_base <= {ADDR_WIDTH{1'b0}};
w_base <= {ADDR_WIDTH{1'b0}};
bias_addr <= {ADDR_WIDTH{1'b0}};
activation <= 2'd1; // ACT_RELU, matches neuron_parallel's own default
n_inputs_real <= N_INPUTS[15:0];
n_neurons_real <= N_NEURONS[15:0];
nm_start <= 1'b0;
nm_soft_rst <= 1'b0;
table_base <= {ADDR_WIDTH{1'b0}};
buf_a_base <= {ADDR_WIDTH{1'b0}};
buf_b_base <= {ADDR_WIDTH{1'b0}};
run_start <= 1'b0;
run_num_layers <= 8'h00;
net_mode <= 1'b0;
net_type <= NET_TYPE_DENSE; // default after RESET (§5): Type #1 tests need never emit SET_NET_TYPE
num_neurons_graph <= 16'h0;
n_out <= 16'h0;
graph_mode <= 1'b0;
end else begin
// --------------------------------------------------
// Default pulses
// --------------------------------------------------
ram_req <= 1'b0;
nm_start <= 1'b0;
nm_soft_rst <= 1'b0;
run_start <= 1'b0;
if (seq_done) begin
net_mode <= 1'b0;
end
if (graph_done) begin
graph_mode <= 1'b0;
end
if (cs_end) begin
// End of transaction: always return to opcode wait,
// regardless of where we were (defensive: a short
// or malformed transaction cannot wedge the engine).
state <= ST_OPCODE;
end else begin
case (state)
// =============================================
// OPCODE
// =============================================
ST_OPCODE: begin
if (rx_valid) begin
opcode <= rx_byte;
byte_pos <= 2'd0;
case (rx_byte)
OP_WRITE_RAM, OP_READ_RAM: begin
addr_acc <= 24'h0;
state <= ST_ADDR;
end
OP_SET_BASE: begin
state <= ST_SETBASE_SEL;
end
OP_START: begin
if (!busy_all)
nm_start <= 1'b1;
state <= ST_IGNORE;
end
OP_RUN_NETWORK: begin
state <= ST_RUNNET;
end
OP_RESET: begin
nm_soft_rst <= 1'b1;
net_mode <= 1'b0;
graph_mode <= 1'b0;
net_type <= NET_TYPE_DENSE;
state <= ST_IGNORE;
end
OP_SET_NET_TYPE: begin
state <= ST_SET_NET_TYPE;
end
OP_STATUS: begin
resp_index <= 4'd0;
resp_len <= 4'd1;
status_snapshot <= {5'b0, graph_err, status_done_sticky, busy_all};
state <= ST_RESP;
end
OP_READ_OUTPUT: begin
resp_index <= 4'd0;
resp_len <= N_NEURONS[3:0];
state <= ST_RESP;
end
OP_READ_CONFIG: begin
resp_index <= 4'd0;
resp_len <= 4'd11;
state <= ST_RESP;
end
default: begin // OP_NOP and unknown opcodes
state <= ST_IGNORE;
end
endcase
end
end
// =============================================
// SET_BASE: 1 selector byte, then 3 addr bytes
// =============================================
ST_SETBASE_SEL: begin
if (rx_valid) begin
addr_acc <= 24'h0;
// Reuse `len_acc[7:0]` as a 1-byte stash
// for the selector between states.
len_acc[7:0] <= rx_byte;
state <= ST_ADDR;
end
end
// =============================================
// ADDR: 3 bytes, MSB first
// Shared by WRITE_RAM / READ_RAM / SET_BASE.
// =============================================
ST_ADDR: begin
if (rx_valid) begin
addr_acc <= {addr_acc[15:0], rx_byte};
if (byte_pos == 2'd2) begin
byte_pos <= 2'd0;
if (opcode == OP_SET_BASE) begin
case (len_acc[7:0])
SEL_X_BASE: x_base <= {addr_acc[15:0], rx_byte};
SEL_W_BASE: w_base <= {addr_acc[15:0], rx_byte};
SEL_BIAS_ADDR: bias_addr <= {addr_acc[15:0], rx_byte};
SEL_TABLE_BASE: table_base <= {addr_acc[15:0], rx_byte};
SEL_BUF_A_BASE: buf_a_base <= {addr_acc[15:0], rx_byte};
SEL_BUF_B_BASE: buf_b_base <= {addr_acc[15:0], rx_byte};
SEL_ACTIVATION: activation <= rx_byte[1:0]; // low 2 bits of the low addr byte
SEL_N_INPUTS: n_inputs_real <= {addr_acc[7:0], rx_byte}; // low 2 of the 3 addr bytes, BE
SEL_N_NEURONS: n_neurons_real <= {addr_acc[7:0], rx_byte}; // low 2 of the 3 addr bytes, BE
SEL_NUM_NEURONS_GRAPH: num_neurons_graph <= {addr_acc[7:0], rx_byte}; // Phase G5, graph mode
SEL_N_OUT: n_out <= {addr_acc[7:0], rx_byte}; // Phase G5, graph mode
default: ; // reserved selector: ignored
endcase
state <= ST_IGNORE;
end else begin
cur_addr <= {addr_acc[15:0], rx_byte};
len_acc <= 16'h0;
state <= ST_LEN;
end
end else begin
byte_pos <= byte_pos + 2'd1;
end
end
end
// =============================================
// LEN: 2 bytes, MSB first (WRITE_RAM / READ_RAM)
// =============================================
ST_LEN: begin
if (rx_valid) begin
len_acc <= {len_acc[7:0], rx_byte};
if (byte_pos == 2'd1) begin
len_remaining <= {len_acc[7:0], rx_byte};
byte_pos <= 2'd0;
if ({len_acc[7:0], rx_byte} == 16'h0) begin
state <= ST_IGNORE;
end else if (opcode == OP_WRITE_RAM) begin
state <= ST_WRITE_DATA;
end else begin // OP_READ_RAM
state <= ST_READ_ISSUE;
end
end else begin
byte_pos <= byte_pos + 2'd1;
end
end
end
// =============================================
// WRITE_RAM: accept one data byte, write it,
// repeat for len_remaining bytes.
// =============================================
ST_WRITE_DATA: begin
if (rx_valid) begin
pending_wdata <= rx_byte;
state <= ST_WRITE_ISSUE;
end
end
ST_WRITE_ISSUE: begin
ram_req <= 1'b1;
ram_wr <= 1'b1;
ram_addr <= cur_addr;
ram_wdata <= $signed(pending_wdata);
state <= ST_WRITE_WAIT;
end
ST_WRITE_WAIT: begin
if (ram_ready) begin
cur_addr <= cur_addr + 1'b1;
len_remaining <= len_remaining - 16'd1;
if (len_remaining == 16'd1)
state <= ST_IGNORE;
else
state <= ST_WRITE_DATA;
end
end
// =============================================
// READ_RAM: prefetch one byte, serve it, repeat.
// =============================================
ST_READ_ISSUE: begin
ram_req <= 1'b1;
ram_wr <= 1'b0;
ram_addr <= cur_addr;
state <= ST_READ_WAIT;
end
ST_READ_WAIT: begin
if (ram_ready) begin
cur_read_byte <= ram_rdata[7:0];
state <= ST_READ_DATA;
end
end
ST_READ_DATA: begin
// rx_valid marks that the response byte
// currently on tx_byte has been shifted out
// and a (dummy) MOSI byte was received in
// exchange -- advance to the next one.
if (rx_valid) begin
cur_addr <= cur_addr + 1'b1;
len_remaining <= len_remaining - 16'd1;
if (len_remaining == 16'd1)
state <= ST_IGNORE;
else
state <= ST_READ_ISSUE;
end
end
// =============================================
// RUN_NETWORK: 1 payload byte (num_layers, dense
// only -- ignored for graph, whose neuron count
// comes from SET_BASE sel 9 instead so this
// opcode's framing stays byte-identical for both
// net_type values, §5), then pulse run_start and
// dispatch to layer_sequencer or graph_engine
// based on net_type. The top level routes this
// single run_start pulse to whichever engine
// net_type selects (see rtl/spi_neuron_top.v).
// No-op (ignored, like OP_START) if the compute
// engine is already busy in any form.
// =============================================
ST_RUNNET: begin
if (rx_valid) begin
if (!busy_all) begin
run_start <= 1'b1;
run_num_layers <= rx_byte;
if (net_type == NET_TYPE_GRAPH)
graph_mode <= 1'b1;
else
net_mode <= 1'b1;
end
state <= ST_IGNORE;
end
end
// =============================================
// SET_NET_TYPE: 1 payload byte (§5)
// =============================================
ST_SET_NET_TYPE: begin
if (rx_valid) begin
net_type <= rx_byte;
state <= ST_IGNORE;
end
end
// =============================================
// STATUS / READ_OUTPUT / READ_CONFIG response
// =============================================
ST_RESP: begin
if (rx_valid) begin
if (resp_index == resp_len - 4'd1)
state <= ST_IGNORE;
else
resp_index <= resp_index + 4'd1;
end
end
// =============================================
// IGNORE: transaction's meaningful bytes are
// done; ignore anything else until cs_end.
// =============================================
ST_IGNORE: begin
// intentionally empty
end
default: begin
state <= ST_OPCODE;
end
endcase
end
end
end
endmodule