feat: complete Phase 4 SPI RTL (engine, arbiter, top) + real-RAM e2e test
Implements the rest of the SPI interface (docs §8.1) on top of spi_slave.v from the previous commit: - rtl/spi_engine.v: opcode FSM + register bank, all 8 opcodes (NOP, WRITE_RAM, READ_RAM, RESET, SET_BASE, START, STATUS, READ_OUTPUT, READ_CONFIG). tx_byte is driven combinationally from live state (not reactively on tx_byte_req), applying the prefetch-vs-consume contract documented on spi_slave.v. STATUS.done is a sticky, clear-on-read latch. RAM master port uses the same byte-level convention as neuron_memory.v's external mem_* port. - rtl/mem_arbiter.v: fixed-priority (neuron_memory > spi_engine) grant-and-forward arbiter sharing one byte-level memory port between spi_engine's WRITE_RAM/READ_RAM and neuron_memory's own X/W/bias reads during a run. - rtl/spi_neuron_top.v: full integration -- spi_slave -> spi_engine -> mem_arbiter -> a single shared int8_memory_access -> memory_interface -> psram_controller -> PSRAM pins. neuron_memory's rst is global rst OR'd with the RESET opcode's soft-reset pulse. The host has no direct electrical path to the RAM, only through this chain. Testing: - sim/spi_engine_tb.v: 10 tests (one per opcode + WRITE_RAM/READ_RAM, START idle-vs-busy, STATUS sticky/clear-on-read, extra-MOSI-bytes- ignored, back-to-back transactions) against a synthetic 2-cycle- latency RAM model, isolating the opcode FSM from PSRAM timing. Found and fixed two testbench-only bugs (RTL needed no change): the same delta-zero clock-edge race as spi_slave_tb.v (blocking `nm_done=1` landing on the same sim time as a posedge -- fixed via negedge-based pulsing) and a missing RAM sentinel initialization. - sim/spi_neuron_top_tb.v: end-to-end test against the **real** psram_model.v (not a mock) -- RESET/READ_CONFIG/WRITE_RAM/ READ_RAM/SET_BASE/START/STATUS/READ_OUTPUT all driven purely over simulated SPI. 3/3 scenarios (sum, saturation, ReLU) pass on the first attempt; confirms the arbiter and shared byte<->word bridge are correct against real PSRAM timing, not just a synthetic mock. Real-toolchain verification (Yosys + nextpnr-ecp5 + ecppack): spi_slave.v and spi_engine.v synthesize clean and comfortably clear 80 MHz in isolation (403 MHz / 191 MHz, no DSP usage). The full spi_neuron_top.v integration, however, does NOT meet 80 MHz (~52-56 MHz depending on PARALLEL) -- the critical path is entirely inside neuron_parallel.v's existing saturation comparator (no contribution from the new SPI/arbiter logic), but its routed delay is ~57% worse than in the isolated benchmark due to placement/ routing congestion once SPI + PSRAM logic shares the fabric with it, not resource exhaustion (2% DSP usage). Documented as a Phase 4/7 finding in docs/FPGA-NeuralNetwork-Engine.md -- a floorplanning/ pipelining problem for Phase 7, not a functional-correctness issue (verified independently in simulation against real PSRAM timing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ================================================================
|
||||
// MEM_ARBITER
|
||||
//
|
||||
// Arbitrates a single shared byte-level memory master port (feeding
|
||||
// a shared int8_memory_access -> memory_interface -> psram_controller
|
||||
// chain) between two byte-level requesters:
|
||||
//
|
||||
// Port A: spi_engine.v (WRITE_RAM / READ_RAM opcodes)
|
||||
// Port B: neuron_memory.v (its own X/W/bias reads during a run)
|
||||
//
|
||||
// Fixed priority B > A when both request on the same idle cycle
|
||||
// (an in-progress inference is treated as more time-critical than a
|
||||
// newly-arriving SPI RAM access). Once a port is granted, the
|
||||
// arbiter holds ownership until that single transaction's m_ready
|
||||
// pulse, then releases -- both A and B already issue `req` as a
|
||||
// clean one-cycle pulse (matching int8_memory_access's own
|
||||
// contract), so a simple grant-and-forward design is sufficient;
|
||||
// no request queuing/pipelining is needed.
|
||||
// ================================================================
|
||||
|
||||
module mem_arbiter #(
|
||||
parameter ADDR_WIDTH = 22
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Port A - spi_engine
|
||||
// ------------------------------------------------------------
|
||||
|
||||
input wire a_req,
|
||||
input wire a_wr,
|
||||
input wire [ADDR_WIDTH-1:0] a_addr,
|
||||
input wire signed [7:0] a_wdata,
|
||||
output reg signed [7:0] a_rdata,
|
||||
output reg a_ready,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Port B - neuron_memory
|
||||
// ------------------------------------------------------------
|
||||
|
||||
input wire b_req,
|
||||
input wire b_wr,
|
||||
input wire [ADDR_WIDTH-1:0] b_addr,
|
||||
input wire signed [7:0] b_wdata,
|
||||
output reg signed [7:0] b_rdata,
|
||||
output reg b_ready,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Shared master port
|
||||
// ------------------------------------------------------------
|
||||
|
||||
output reg m_req,
|
||||
output reg m_wr,
|
||||
output reg [ADDR_WIDTH-1:0] m_addr,
|
||||
output reg signed [7:0] m_wdata,
|
||||
|
||||
input wire signed [7:0] m_rdata,
|
||||
input wire m_ready
|
||||
);
|
||||
|
||||
localparam SEL_NONE = 2'd0;
|
||||
localparam SEL_A = 2'd1;
|
||||
localparam SEL_B = 2'd2;
|
||||
|
||||
reg [1:0] owner;
|
||||
|
||||
always @(posedge clk) begin
|
||||
|
||||
if (rst) begin
|
||||
|
||||
owner <= SEL_NONE;
|
||||
|
||||
m_req <= 1'b0;
|
||||
m_wr <= 1'b0;
|
||||
m_addr <= {ADDR_WIDTH{1'b0}};
|
||||
m_wdata <= 8'sd0;
|
||||
|
||||
a_rdata <= 8'sd0;
|
||||
a_ready <= 1'b0;
|
||||
|
||||
b_rdata <= 8'sd0;
|
||||
b_ready <= 1'b0;
|
||||
|
||||
end else begin
|
||||
|
||||
m_req <= 1'b0;
|
||||
a_ready <= 1'b0;
|
||||
b_ready <= 1'b0;
|
||||
|
||||
case (owner)
|
||||
|
||||
SEL_NONE: begin
|
||||
|
||||
if (b_req) begin
|
||||
|
||||
owner <= SEL_B;
|
||||
m_req <= 1'b1;
|
||||
m_wr <= b_wr;
|
||||
m_addr <= b_addr;
|
||||
m_wdata <= b_wdata;
|
||||
|
||||
end else if (a_req) begin
|
||||
|
||||
owner <= SEL_A;
|
||||
m_req <= 1'b1;
|
||||
m_wr <= a_wr;
|
||||
m_addr <= a_addr;
|
||||
m_wdata <= a_wdata;
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
SEL_A: begin
|
||||
|
||||
if (m_ready) begin
|
||||
a_rdata <= m_rdata;
|
||||
a_ready <= 1'b1;
|
||||
owner <= SEL_NONE;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
SEL_B: begin
|
||||
|
||||
if (m_ready) begin
|
||||
b_rdata <= m_rdata;
|
||||
b_ready <= 1'b1;
|
||||
owner <= SEL_NONE;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
default: begin
|
||||
owner <= SEL_NONE;
|
||||
end
|
||||
|
||||
endcase
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,554 @@
|
||||
`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 = 22,
|
||||
parameter DATA_WIDTH = 8,
|
||||
parameter N_INPUTS = 32,
|
||||
parameter N_NEURONS = 1,
|
||||
parameter PARALLEL = 8
|
||||
)(
|
||||
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 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
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// 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_START = 8'h20;
|
||||
localparam OP_STATUS = 8'h21;
|
||||
localparam OP_READ_OUTPUT = 8'h22;
|
||||
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;
|
||||
|
||||
// ============================================================
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
wire status_read_now = (state == ST_RESP) && (opcode == OP_STATUS) && rx_valid;
|
||||
|
||||
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 (nm_done) begin
|
||||
status_done_sticky <= 1'b1;
|
||||
end else if (status_read_now) 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 = {6'b0, status_done_sticky, nm_busy};
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
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}};
|
||||
|
||||
nm_start <= 1'b0;
|
||||
nm_soft_rst <= 1'b0;
|
||||
|
||||
end else begin
|
||||
|
||||
// --------------------------------------------------
|
||||
// Default pulses
|
||||
// --------------------------------------------------
|
||||
ram_req <= 1'b0;
|
||||
nm_start <= 1'b0;
|
||||
nm_soft_rst <= 1'b0;
|
||||
|
||||
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 (!nm_busy)
|
||||
nm_start <= 1'b1;
|
||||
state <= ST_IGNORE;
|
||||
end
|
||||
|
||||
OP_RESET: begin
|
||||
nm_soft_rst <= 1'b1;
|
||||
state <= ST_IGNORE;
|
||||
end
|
||||
|
||||
OP_STATUS: begin
|
||||
resp_index <= 4'd0;
|
||||
resp_len <= 4'd1;
|
||||
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'd8;
|
||||
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};
|
||||
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
|
||||
|
||||
// =============================================
|
||||
// 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
|
||||
@@ -0,0 +1,268 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ================================================================
|
||||
// SPI_NEURON_TOP
|
||||
//
|
||||
// Full Phase 3 + Phase 4 integration: SPI host interface (spi_slave
|
||||
// + spi_engine, docs §8.1) driving neuron_memory.v (Phase 3,
|
||||
// N_NEURONS>=1) through a shared PSRAM (memory_interface +
|
||||
// psram_controller), arbitrated between spi_engine's own RAM access
|
||||
// (WRITE_RAM/READ_RAM opcodes) and neuron_memory's own X/W/bias
|
||||
// reads during a run.
|
||||
//
|
||||
// neuron_memory's own `rst` is the global reset OR'd with the
|
||||
// RESET opcode's soft-reset pulse from spi_engine, so a host can
|
||||
// recover the compute engine over SPI without a physical reset
|
||||
// (RAM contents are untouched either way).
|
||||
// ================================================================
|
||||
|
||||
module spi_neuron_top #(
|
||||
parameter ADDR_WIDTH = 22,
|
||||
parameter DATA_WIDTH = 8,
|
||||
parameter N_INPUTS = 32,
|
||||
parameter N_NEURONS = 1,
|
||||
parameter PARALLEL = 8,
|
||||
parameter ACC_WIDTH = 32,
|
||||
parameter MEM_DATA_WIDTH = 16,
|
||||
parameter CLK_FREQ_MHZ = 80
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// SPI host interface
|
||||
// ------------------------------------------------------------
|
||||
|
||||
input wire sclk,
|
||||
input wire mosi,
|
||||
output wire miso,
|
||||
input wire cs_n,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// PSRAM physical interface
|
||||
// ------------------------------------------------------------
|
||||
|
||||
output wire [ADDR_WIDTH-1:0] psram_a,
|
||||
inout wire [MEM_DATA_WIDTH-1:0] psram_dq,
|
||||
output wire psram_ce_n,
|
||||
output wire psram_oe_n,
|
||||
output wire psram_we_n,
|
||||
output wire psram_lb_n,
|
||||
output wire psram_ub_n,
|
||||
output wire psram_zz_n
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// SPI PHYSICAL LAYER
|
||||
// ============================================================
|
||||
|
||||
wire [7:0] rx_byte;
|
||||
wire rx_valid;
|
||||
wire cs_start;
|
||||
wire cs_end;
|
||||
wire [7:0] tx_byte;
|
||||
wire tx_byte_req;
|
||||
|
||||
spi_slave u_spi_slave (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
||||
|
||||
.rx_byte(rx_byte), .rx_valid(rx_valid),
|
||||
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
|
||||
|
||||
.cs_active(), .cs_start(cs_start), .cs_end(cs_end)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// SPI PROTOCOL ENGINE
|
||||
// ============================================================
|
||||
|
||||
wire spi_ram_req;
|
||||
wire spi_ram_wr;
|
||||
wire [ADDR_WIDTH-1:0] spi_ram_addr;
|
||||
wire signed [7:0] spi_ram_wdata;
|
||||
wire signed [7:0] spi_ram_rdata;
|
||||
wire spi_ram_ready;
|
||||
|
||||
wire [ADDR_WIDTH-1:0] x_base;
|
||||
wire [ADDR_WIDTH-1:0] w_base;
|
||||
wire [ADDR_WIDTH-1:0] bias_addr;
|
||||
|
||||
wire nm_start;
|
||||
wire nm_busy;
|
||||
wire nm_done;
|
||||
|
||||
wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
|
||||
|
||||
wire nm_soft_rst;
|
||||
|
||||
spi_engine #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH),
|
||||
.DATA_WIDTH(DATA_WIDTH),
|
||||
.N_INPUTS(N_INPUTS),
|
||||
.N_NEURONS(N_NEURONS),
|
||||
.PARALLEL(PARALLEL)
|
||||
) u_spi_engine (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.rx_byte(rx_byte), .rx_valid(rx_valid),
|
||||
.cs_start(cs_start), .cs_end(cs_end),
|
||||
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
|
||||
|
||||
.ram_req(spi_ram_req), .ram_wr(spi_ram_wr),
|
||||
.ram_addr(spi_ram_addr), .ram_wdata(spi_ram_wdata),
|
||||
.ram_rdata(spi_ram_rdata), .ram_ready(spi_ram_ready),
|
||||
|
||||
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
|
||||
|
||||
.nm_start(nm_start), .nm_busy(nm_busy), .nm_done(nm_done),
|
||||
.y_bus(y_bus),
|
||||
|
||||
.nm_soft_rst(nm_soft_rst)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// NEURON MEMORY
|
||||
//
|
||||
// rst is the global reset OR'd with the SPI RESET opcode pulse.
|
||||
// ============================================================
|
||||
|
||||
wire nm_rst = rst | nm_soft_rst;
|
||||
|
||||
wire nm_ram_req;
|
||||
wire nm_ram_wr;
|
||||
wire [ADDR_WIDTH-1:0] nm_ram_addr;
|
||||
wire signed [7:0] nm_ram_wdata;
|
||||
wire signed [7:0] nm_ram_rdata;
|
||||
wire nm_ram_ready;
|
||||
|
||||
neuron_memory #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH),
|
||||
.DATA_WIDTH(DATA_WIDTH),
|
||||
.N_INPUTS(N_INPUTS),
|
||||
.N_NEURONS(N_NEURONS),
|
||||
.PARALLEL(PARALLEL),
|
||||
.ACC_WIDTH(ACC_WIDTH)
|
||||
) u_neuron_memory (
|
||||
.clk(clk), .rst(nm_rst),
|
||||
.start(nm_start),
|
||||
|
||||
.mem_req(nm_ram_req), .mem_wr(nm_ram_wr),
|
||||
.mem_addr(nm_ram_addr), .mem_wdata(nm_ram_wdata),
|
||||
.mem_rdata(nm_ram_rdata), .mem_ready(nm_ram_ready),
|
||||
|
||||
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
|
||||
|
||||
.y_bus(y_bus), .busy(nm_busy), .done(nm_done)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// SHARED MEMORY ARBITER
|
||||
// ============================================================
|
||||
|
||||
wire arb_req;
|
||||
wire arb_wr;
|
||||
wire [ADDR_WIDTH-1:0] arb_addr;
|
||||
wire signed [7:0] arb_wdata;
|
||||
wire signed [7:0] arb_rdata;
|
||||
wire arb_ready;
|
||||
|
||||
mem_arbiter #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH)
|
||||
) u_arbiter (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.a_req(spi_ram_req), .a_wr(spi_ram_wr),
|
||||
.a_addr(spi_ram_addr), .a_wdata(spi_ram_wdata),
|
||||
.a_rdata(spi_ram_rdata), .a_ready(spi_ram_ready),
|
||||
|
||||
.b_req(nm_ram_req), .b_wr(nm_ram_wr),
|
||||
.b_addr(nm_ram_addr), .b_wdata(nm_ram_wdata),
|
||||
.b_rdata(nm_ram_rdata), .b_ready(nm_ram_ready),
|
||||
|
||||
.m_req(arb_req), .m_wr(arb_wr),
|
||||
.m_addr(arb_addr), .m_wdata(arb_wdata),
|
||||
.m_rdata(arb_rdata), .m_ready(arb_ready)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// BYTE <-> WORD BRIDGE (shared, single instance)
|
||||
// ============================================================
|
||||
|
||||
wire i8_mem_req;
|
||||
wire i8_mem_wr;
|
||||
wire [ADDR_WIDTH-1:0] i8_mem_addr;
|
||||
wire [MEM_DATA_WIDTH-1:0] i8_mem_wdata;
|
||||
wire i8_mem_lb_n;
|
||||
wire i8_mem_ub_n;
|
||||
|
||||
wire [MEM_DATA_WIDTH-1:0] i8_mem_rdata;
|
||||
wire i8_mem_ready;
|
||||
|
||||
int8_memory_access #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH)
|
||||
) u_int8_access (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
|
||||
.rdata(arb_rdata), .ready(arb_ready),
|
||||
|
||||
.mem_req(i8_mem_req), .mem_wr(i8_mem_wr),
|
||||
.mem_addr(i8_mem_addr), .mem_wdata(i8_mem_wdata),
|
||||
.mem_lb_n(i8_mem_lb_n), .mem_ub_n(i8_mem_ub_n),
|
||||
|
||||
.mem_rdata(i8_mem_rdata), .mem_ready(i8_mem_ready)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// MEMORY INTERFACE / PSRAM CONTROLLER
|
||||
// ============================================================
|
||||
|
||||
wire [MEM_DATA_WIDTH-1:0] psram_mem_rdata;
|
||||
wire psram_mem_ready;
|
||||
|
||||
wire psram_mem_req;
|
||||
wire psram_mem_wr;
|
||||
wire [ADDR_WIDTH-1:0] psram_mem_addr;
|
||||
wire [MEM_DATA_WIDTH-1:0] psram_mem_wdata;
|
||||
wire psram_mem_lb_n;
|
||||
wire psram_mem_ub_n;
|
||||
|
||||
memory_interface #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH),
|
||||
.DATA_WIDTH(MEM_DATA_WIDTH)
|
||||
) u_memory_if (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.req(i8_mem_req), .wr(i8_mem_wr), .addr(i8_mem_addr), .wdata(i8_mem_wdata),
|
||||
.lb_n(i8_mem_lb_n), .ub_n(i8_mem_ub_n),
|
||||
|
||||
.rdata(i8_mem_rdata), .ready(i8_mem_ready),
|
||||
|
||||
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
|
||||
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
|
||||
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
|
||||
|
||||
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready)
|
||||
);
|
||||
|
||||
psram_controller #(
|
||||
.ADDR_WIDTH(ADDR_WIDTH),
|
||||
.DATA_WIDTH(MEM_DATA_WIDTH),
|
||||
.CLK_FREQ_MHZ(CLK_FREQ_MHZ)
|
||||
) u_psram_ctrl (
|
||||
.clk(clk), .rst(rst),
|
||||
|
||||
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
|
||||
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
|
||||
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
|
||||
|
||||
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready),
|
||||
|
||||
.psram_a(psram_a), .psram_dq(psram_dq),
|
||||
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
|
||||
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
|
||||
);
|
||||
|
||||
endmodule
|
||||
Reference in New Issue
Block a user