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
148 lines
4.1 KiB
Verilog
148 lines
4.1 KiB
Verilog
`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
|