feat(v2): M8 PSRAM integration - real V1 backend shared across concurrent slots

neural_multiprocessor.v wraps dataflow_core.v (M7, unmodified) around
the real, unmodified V1 PSRAM backend chain (int8_memory_access ->
memory_interface -> psram_controller), funneling N_SLOTS independent
Memory Backend Interface ports through a new generic N-port arbiter
(slot_mem_arbiter.v) inspired by (not copied from) V1's own
mem_arbiter.v.

Real concurrent-slot simulation immediately surfaced a genuine bug
(ERR-0008): memory_manager/prefetch_engine's byte-level backend
protocol is fire-and-forget (a single-cycle mem_req pulse with no
accept handshake) - correct for M4's direct 1:1 connection, but a
naive arbiter silently drops a pulse arriving while the shared bus is
owned by another slot, hanging that slot forever. Fixed with a
per-port pending-request latch, the same "queue, don't drop" idiom
already used by memory_manager's own pf_pending register (ERR-0006).

Verified (Verilator): 4/4 PASS with 2 slots genuinely contending for
one real PSRAM port (444 cycles). No regression on M4's own
testbench. Real synthesis + nextpnr-ecp5 P&R (no harness needed - real
PSRAM pins keep the top-level at 157 pins): 0 problems, Fmax 142.45
MHz, PASS at 80MHz.

Arbitration policy is fixed lowest-index priority, not fairness-
balanced (DEC-0010) - consistent with every other "simplest correct
policy first" scheduling choice in this roadmap, revisited only if
M9's real measurement shows starvation matters.

Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0010)/
experiments (EXP-0009)/errors (ERR-0008)/development.log, ROADMAP.md
updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-05 15:13:11 +02:00
co-authored by Claude Sonnet 5
parent 77baa8fc16
commit 6cff2c8a7c
12 changed files with 732 additions and 1 deletions
+190
View File
@@ -0,0 +1,190 @@
`timescale 1ns/1ps
// ============================================================
// M8 testbench (docs/v2-description.md §15/§16/§20):
// neural_multiprocessor.v -- dataflow_core.v (M7, unmodified) sharing
// the REAL, UNMODIFIED V1 PSRAM backend chain (int8_memory_access ->
// memory_interface -> psram_controller -> real psram_model) across
// N_SLOTS=2 concurrent memory_manager instances for the first time,
// through the new slot_mem_arbiter.v (M8).
//
// Same DAG shape as tb_dataflow_core.v (M7's own test), REPLACING the
// per-slot behavioral memories with the single real PSRAM chain --
// this is the actual M8 measurement: does real arbitration/contention
// across genuinely-concurrent slots work correctly against real PSRAM
// timing (not an idealized 2-cycle behavioral model)?
//
// node0 (x=2,w=3,8in -> acc=48) --+
// +--> node2 (x=1,w=5,8in -> acc=40)
// node1 (x=1,w=1,8in -> acc=8) --+
//
// node0 and node1 are registered back-to-back with NO dependencies,
// so both are dispatched to the two available slots essentially
// simultaneously -- both memory_manager instances will genuinely
// contend for the one real PSRAM port at the same time, exercising
// slot_mem_arbiter.v's arbitration for real (not just in isolation).
//
// Verified with Verilator (decisions.log DEC-0004).
// ============================================================
module tb;
localparam ADDR_WIDTH = 23;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
localparam N_SLOTS = 2;
localparam N_NODES = 8;
localparam MAX_DEPS = 4;
localparam QUEUE_DEPTH = 4;
localparam NODE_IDW = $clog2(N_NODES);
localparam PSRAM_DATA_WIDTH = 16;
localparam CLK_PERIOD = 12.5; // 80 MHz, matches psram_controller's CLK_FREQ_MHZ
reg clk, rst;
initial begin clk = 1'b0; forever #(CLK_PERIOD/2.0) clk = ~clk; end
reg reg_valid;
wire reg_ready;
reg [NODE_IDW-1:0] reg_node_id;
reg [$clog2(MAX_DEPS+1)-1:0] reg_required;
reg [MAX_DEPS*NODE_IDW-1:0] reg_producer_ids;
reg [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr;
reg [15:0] reg_n_tiles;
wire [ADDR_WIDTH-1:0] psram_a;
wire [PSRAM_DATA_WIDTH-1:0] psram_dq;
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
neural_multiprocessor #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.PSRAM_DATA_WIDTH(PSRAM_DATA_WIDTH), .CLK_FREQ_MHZ(80)
) u_nmp (
.clk(clk), .rst(rst),
.reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id),
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
.reg_result_addr(reg_result_addr),
.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)
);
psram_model #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH), .DEPTH(16384)) u_psram (
.clk(clk), .a(psram_a), .dq(psram_dq),
.ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n),
.lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n)
);
task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] val);
reg [ADDR_WIDTH-2:0] word_addr;
begin
word_addr = byte_addr[ADDR_WIDTH-1:1];
if (byte_addr[0] == 1'b0)
u_psram.mem[word_addr][7:0] = val;
else
u_psram.mem[word_addr][15:8] = val;
end
endtask
function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr);
reg [ADDR_WIDTH-2:0] word_addr;
begin
word_addr = byte_addr[ADDR_WIDTH-1:1];
peek_byte = (byte_addr[0] == 1'b0) ? u_psram.mem[word_addr][7:0] : u_psram.mem[word_addr][15:8];
end
endfunction
task automatic register_node(
input [NODE_IDW-1:0] nid,
input [$clog2(MAX_DEPS+1)-1:0] required,
input [NODE_IDW-1:0] p0, input [NODE_IDW-1:0] p1,
input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb,
input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr
);
begin
@(posedge clk);
reg_node_id = nid;
reg_required = required;
reg_producer_ids = {NODE_IDW*MAX_DEPS{1'b0}};
reg_producer_ids[0*NODE_IDW +: NODE_IDW] = p0;
reg_producer_ids[1*NODE_IDW +: NODE_IDW] = p1;
reg_x_base = xb; reg_w_base = wb; reg_n_tiles = nt; reg_result_addr = resaddr;
reg_valid = 1'b1;
while (!reg_ready) @(posedge clk);
@(posedge clk);
reg_valid = 1'b0;
end
endtask
integer errors, tests;
integer i, wd;
initial begin
errors = 0; tests = 0;
rst = 1; reg_valid = 0; reg_node_id = 0; reg_required = 0; reg_producer_ids = 0;
reg_x_base = 0; reg_w_base = 0; reg_n_tiles = 0; reg_result_addr = 0;
repeat(5) @(posedge clk);
rst = 0;
// Real PSRAM power-up sequence (~150us @ 80MHz) -- same
// requirement/convention as tb_memory_manager.v (M4).
wait (u_nmp.u_psram_ctrl.state == u_nmp.u_psram_ctrl.STATE_IDLE);
@(posedge clk);
for (i = 0; i < 8; i = i + 1) begin
poke_byte(23'h10+i, 8'sd2); poke_byte(23'h20+i, 8'sd3); // node0: x=2,w=3
poke_byte(23'h30+i, 8'sd1); poke_byte(23'h40+i, 8'sd1); // node1: x=1,w=1
poke_byte(23'h50+i, 8'sd1); poke_byte(23'h60+i, 8'sd5); // node2: x=1,w=5
end
poke_byte(23'h70, 8'sd0); poke_byte(23'h71, 8'sd0); poke_byte(23'h72, 8'sd0);
// node0, node1: no dependencies -- dispatched back-to-back, so
// BOTH slots start genuinely concurrent PSRAM traffic through
// the shared arbiter at essentially the same time.
register_node(0, 0, 0, 0, 23'h10, 23'h20, 16'd1, 23'h70);
register_node(1, 0, 0, 0, 23'h30, 23'h40, 16'd1, 23'h71);
register_node(2, 2, 0, 1, 23'h50, 23'h60, 16'd1, 23'h72);
tests = tests + 1;
wd = 0;
while ((peek_byte(23'h70)==0 || peek_byte(23'h71)==0) && wd < 20000) begin
if (peek_byte(23'h72) !== 8'sd0) begin
$display("FAIL: node2 completed before both node0 and node1 finished");
errors = errors + 1;
end
@(posedge clk); wd = wd + 1;
end
$display("PASS: node2 did not complete before both its dependencies did (checked every cycle up to wd=%0d)", wd);
wd = 0;
while (peek_byte(23'h72)==0 && wd < 20000) begin @(posedge clk); wd = wd + 1; end
repeat(10) @(posedge clk);
tests = tests + 3;
if (peek_byte(23'h70) !== 8'sd48) begin
$display("FAIL node0: result=%0d expected 48", peek_byte(23'h70));
errors = errors + 1;
end else $display("PASS node0: result=48 via real PSRAM + shared arbiter");
if (peek_byte(23'h71) !== 8'sd8) begin
$display("FAIL node1: result=%0d expected 8", peek_byte(23'h71));
errors = errors + 1;
end else $display("PASS node1: result=8 via real PSRAM + shared arbiter (concurrent with node0)");
if (peek_byte(23'h72) !== 8'sd40) begin
$display("FAIL node2: result=%0d expected 40", peek_byte(23'h72));
errors = errors + 1;
end else $display("PASS node2: result=40, dispatched only after BOTH producers genuinely completed, real PSRAM end-to-end");
$display("========================================");
if (errors == 0)
$display("ALL %0d TESTS PASSED (neural_multiprocessor, real V1 PSRAM chain shared across N_SLOTS=%0d via slot_mem_arbiter)", tests, N_SLOTS);
else
$display("FAILED: %0d/%0d test(s) had errors -- see messages above", errors, tests);
$display("========================================");
$finish;
end
endmodule