Files
FPGA-Neural/rtl/mem_arbiter.v
T
michele 233d6ff7fb feat: complete Phase 5 multi-layer network (RUN_NETWORK) + fix STATUS race
Wires the already-present layer_sequencer.v into the SPI stack:

- spi_engine.v: RUN_NETWORK opcode (0x23) + SET_BASE selectors for
  table_base/buf_a_base/buf_b_base; STATUS.busy/done extended to
  track the sequencer (seq_busy/seq_done) alongside neuron_memory
  directly, so done latches on the last layer only.
- spi_neuron_top.v: instantiates layer_sequencer, muxes
  neuron_memory's control inputs between it (while seq_busy) and
  spi_engine's direct-drive path (legacy single-layer mode), wires
  the sequencer's own RAM master to mem_arbiter's Port C.

Found and fixed a real race while writing the end-to-end test: STATUS's
sticky/clear-on-read done bit read its value live/combinationally
during transmission and cleared unconditionally on any STATUS read.
A done_event landing mid-transmission of a STATUS response byte could
be silently dropped -- the host would receive a stale byte while the
sticky bit was cleared regardless, hanging any host polling STATUS in
a loop. Present since Phase 4, not RUN_NETWORK-specific; only
surfaced under this test's continuous polling. Fixed by latching a
status_snapshot at opcode-accept time and gating the clear on what
was actually transmitted.

Tests: spi_engine_tb.v gains RUN_NETWORK/SET_BASE opcode tests (K/L);
new layer_sequencer_tb.v unit-tests the sequencer FSM directly
(descriptor table, ping-pong buffer addressing, byte-exact copy-out);
new spi_neuron_top_runnetwork_tb.v drives a real 2-layer network over
simulated SPI end to end (real neuron_memory + PSRAM, hand-computed
expected output) and confirms the legacy single-layer path still
works afterward. All existing testbenches still pass.
2026-09-02 19:47:36 +02:00

190 lines
5.6 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 three 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)
// Port C: layer_sequencer.v (Phase 5: descriptor reads + output
// buffer writes between layers)
//
// Fixed priority B > C > A when more than one requests on the same
// idle cycle (an in-progress inference is treated as more
// time-critical than the sequencer's own bookkeeping, which in turn
// is treated as more time-critical than a newly-arriving manual SPI
// RAM access). In normal operation B and C are temporally disjoint
// anyway -- neuron_memory only requests while running, and
// layer_sequencer only requests in the gaps between layers -- so
// this priority mostly matters for the edge case of a manual
// WRITE_RAM/READ_RAM arriving while a Phase 5 run is in progress.
// Once a port is granted, the arbiter holds ownership until that
// single transaction's m_ready pulse, then releases -- all three
// masters 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,
// ------------------------------------------------------------
// Port C - layer_sequencer
// ------------------------------------------------------------
input wire c_req,
input wire c_wr,
input wire [ADDR_WIDTH-1:0] c_addr,
input wire signed [7:0] c_wdata,
output reg signed [7:0] c_rdata,
output reg c_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;
localparam SEL_C = 2'd3;
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;
c_rdata <= 8'sd0;
c_ready <= 1'b0;
end else begin
m_req <= 1'b0;
a_ready <= 1'b0;
b_ready <= 1'b0;
c_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 (c_req) begin
owner <= SEL_C;
m_req <= 1'b1;
m_wr <= c_wr;
m_addr <= c_addr;
m_wdata <= c_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
SEL_C: begin
if (m_ready) begin
c_rdata <= m_rdata;
c_ready <= 1'b1;
owner <= SEL_NONE;
end
end
default: begin
owner <= SEL_NONE;
end
endcase
end
end
endmodule