diff --git a/hardware/v2/logs/experiments.log b/hardware/v2/logs/experiments.log
index 31fef19..6ce6e9a 100644
--- a/hardware/v2/logs/experiments.log
+++ b/hardware/v2/logs/experiments.log
@@ -2925,3 +2925,129 @@ decision: adopted as the operative Phase-0 BASELINE row (see
agreed time budget.
next_action: Phase 1 (N=8 timing fix) blocked on N_SLOTS=8 baseline;
in the meantime this N_SLOTS=4 baseline is committed to branch v21.
+
+EXP-0051 -- Two-physical-SDRAM-bank experiment (SIMULATION ONLY): does
+splitting weight-fetch (W) and activation/result (AR) traffic onto two
+independent physical SDRAM channels remove the memory-bound thrashing
+EXP-0049/0050 measured on the real board-level top? (2026-09-16)
+
+DATE: 2026-09-16
+CONTEXT: per decisions.log's own "next recommended step" note after
+EXP-0049/0050 closed Phase 0 (real board-top baseline, N=4 worst
+82.43MHz/N=8 worst 77.36MHz, both 8/8 PASS @ 64MHz -- see decisions.log
+for that specific number set, gathered in a prior pass of this same
+session) -- before spending effort on Phase 2 (85F retarget, N=16),
+verify whether the system is genuinely external-memory-bandwidth-bound
+(as tb_nms_dstress_sdram_unified.v's own instrumentation already
+strongly suggested: SDRAM controller port busy ~81.6% of all D-Stress
+cycles at BOTH N_SLOTS=4 (49927 cycles) and N_SLOTS=8 (49909 cycles) --
+see decisions.log/errors.log) by testing the "Fase 3" 1-vs-2-SDRAM-bank
+question from the brief's own original scope, at N=4/N=8, ahead of
+schedule. NOTE: STEP19/EXP-0048's own governing spec explicitly
+mandated a SINGLE physical SDRAM for the real board ("una SDRAM, anche
+se richiede un Memory Manager piu intelligente") -- this experiment
+does NOT propose reopening that decision for the real hardware/v2
+board (constraints/v2_board_top.lpf is untouched, still wires exactly
+one physical chip); it is scoped, per this session's own current
+brief, as SIMULATION-ONLY architecture exploration to inform whether a
+future board revision or a different RTL fix direction is worth
+pursuing at all.
+
+TOOLCHAIN (recorded per timing.log's own process recommendation after
+the EXP-0049/0050 Yosys-version discrepancy investigation): this
+session's OSS CAD Suite install at ~/tools_cache/oss-cad-suite/ reports
+`yosys -V` = "Yosys 0.69+59 (git sha1 d85872386-dirty)" -- the IDENTICAL
+commit hash already recorded for the EXP-0049/0050 session, confirming
+NO toolchain drift since that investigation closed (this experiment
+uses Verilator only, no synthesis/P&R was run). `verilator --version` =
+"Verilator 5.053 devel rev v5.052-85-g270c528af (mod)".
+
+METHOD: forked nms_neural_multiprocessor_sdram_unified.v (STEP19) into
+a new module, `nms_neural_multiprocessor_sdram_dualbank.v` --
+u_dataflow_core/u_arbiter/u_arbiter_wide all byte-for-byte unchanged;
+the single sdram_unified_backend.v instance is replaced by TWO
+instances of that SAME, unmodified module: u_sdram_backend_w (W port
+only, ar_req tied to 0) and u_sdram_backend_ar (AR port only, w_req
+tied to 0), each with its own sdram_controller.v and its own physical
+SDRAM pins. Safety of the permanent tie-off verified by inspection: an
+always-0 ar_req/w_req means S_AR_RD_WAIT/S_AR_WR_WAIT (resp. S_W_WAIT)
+are simply never entered -- no dead-state risk. Forked
+tb_nms_dstress_sdram_unified.v into `tb_nms_dstress_sdram_dualbank.v`
+(new files, both under hardware/v2/nms/{rtl,sim}/) -- identical D-Stress
+workload/golden-model/bit-exact verification; only the backdoor
+poke_byte/peek_byte targets change (weight pokes -> u_sdram_w.mem,
+activation/result pokes -> u_sdram_ar.mem, a split that already existed
+in the original testbench's own naming convention even when both
+pointed at the same array) and instrumentation now reports each bank's
+own sdram_controller.v busy%/req/ready/refresh counts separately, plus
+an "either bank busy" figure directly comparable to the single-bank
+sdram_busy_pct.
+command: `verilator --binary --timing -j 0 -Wno-fatal -GN_SLOTS_CFG=<4|8>
+ -GPFD_CFG=8 --top-module tb -Mdir
sim/tb_nms_dstress_sdram_
+ dualbank.v sim/sdram_model.v `, then run the resulting Vtb
+ binary. Baseline (single-bank) re-run first for direct comparison,
+ same command against the unmodified tb_nms_dstress_sdram_unified.v --
+ reproduced EXACTLY: N=4 49927 cycles/81.56% busy, N=8 49909
+ cycles/81.62% busy, both 256/256 bit-exact -- confirms this session's
+ toolchain/methodology matches the numbers already on record before
+ trusting the new dual-bank numbers below.
+
+RESULT (dual-bank, both 256/256 neurons bit-exact vs golden, data_ready
+PASS, zero functional regression):
+ N=4: total_cycles=45724 (vs single-bank 49927, a real but MODEST
+ 8.4% reduction). BANK W (weight-fetch): busy=35112/45724 (76.79%).
+ BANK AR (activation+result): busy=5375/45724 (11.76%). EITHER-bank-
+ busy=36391/45724 (79.59%) -- barely different from the single-bank
+ figure of 81.56%.
+ N=8: total_cycles=44980 (vs single-bank 49909, 9.9% reduction). BANK
+ W: busy=35057/44980 (77.94%). BANK AR: busy=5368/44980 (11.93%).
+ EITHER-bank-busy=35976/44980 (79.98%) -- again barely different
+ from the single-bank 81.62%.
+ Both configs: BANK W's own req/ready counts are near-identical
+ across N=4 and N=8 (2163 vs 2160) -- confirms weight-fetch traffic
+ volume itself does not grow much with N_SLOTS (same total tiles
+ processed either way), yet Bank W alone still saturates at ~77-78%
+ busy EVEN with a fully dedicated physical channel and zero AR
+ cross-traffic.
+
+INTERPRETATION (SURPRISING, disclosed honestly -- this is NOT the
+dramatic "thrashing disappears with 2 banks" result the hypothesis's
+naive framing might have predicted): the memory-bound hypothesis is
+CONFIRMED at the system level (~80% memory-port busy either way) but
+REFINED in a way that changes the recommended next step. Splitting
+traffic by CLASS (W vs AR) barely moves total_cycles (8-10%) because
+the AR path was never the dominant contention source in the first
+place (STEP17/EXP-0045 already showed AR at <=7.2% of all external-
+memory activity, confirmed again here: Bank AR sits at ~12% busy even
+with its own fully dedicated channel and zero contention). The real
+ceiling is BANK W's OWN throughput -- i.e. how fast a SINGLE weight-
+fetch channel (BURST_LEN=8, one sdram_controller.v transaction in
+flight at a time, W_ENTRIES=4 "other half" cache) can deliver 64-bit
+weight words to however many slots are requesting them -- not
+arbitration contention between logically-different traffic classes on
+one shared bus. Giving AR its own physical bank was, in effect, solving
+a problem that was not the binding one.
+decision: do NOT recommend a 2-physical-bank (W/AR split) board
+ revision on this evidence alone -- the ~8-10% cycle-count gain does
+ not obviously justify the doubled physical SDRAM pin count (74 vs 37
+ pins) for Phase 2's LFE5U-85F retarget, given the real bottleneck
+ visibly sits inside the weight-fetch channel itself, not in cross-
+ class contention. This does NOT close the memory-bandwidth question
+ -- it REDIRECTS it: the next diagnostic worth running before Phase 2
+ is characterizing what specifically caps Bank W's own ~77-78%
+ ceiling (single-transaction-in-flight controller design? W_ENTRIES=4
+ cache depth/hit rate under N=8 contention? BURST_LEN=8 granularity
+ vs per-tile fetch size?) and whether splitting WEIGHT traffic itself
+ across two banks (e.g. by slot-group, not by traffic class) would
+ fare differently -- that specific variant was NOT tested here and is
+ a real, disclosed gap, not assumed to also fail.
+next_action: report this refined finding to the user before choosing
+ between (a) a slot-group-split weight-bank experiment as a follow-up
+ to this same Fase-3 investigation, (b) a Bank-W-internal-only
+ optimization pass (cache depth, burst size, pipelining), or (c)
+ proceeding directly to Phase 2 (85F retarget + N=16) with the
+ memory-bandwidth ceiling accepted as a known, disclosed limitation
+ rather than something Phase 3 can cheaply remove. New files (not yet
+ used by the real board top, additive only): hardware/v2/nms/rtl/
+ nms_neural_multiprocessor_sdram_dualbank.v, hardware/v2/nms/sim/
+ tb_nms_dstress_sdram_dualbank.v.
diff --git a/hardware/v2/nms/rtl/nms_neural_multiprocessor_sdram_dualbank.v b/hardware/v2/nms/rtl/nms_neural_multiprocessor_sdram_dualbank.v
new file mode 100644
index 0000000..be37f3c
--- /dev/null
+++ b/hardware/v2/nms/rtl/nms_neural_multiprocessor_sdram_dualbank.v
@@ -0,0 +1,203 @@
+`timescale 1ns/1ps
+
+// ================================================================
+// Neural Memory System (NMS) -- EXPERIMENTAL two-physical-SDRAM-bank
+// variant, forked from nms_neural_multiprocessor_sdram_unified.v
+// (STEP19) to test one specific hypothesis before committing to
+// Phase 2/3 of the N=8-timing/85F-retarget/SDRAM-bank-sweep brief:
+// is this system's real bottleneck external-memory BANDWIDTH (one
+// shared physical SDRAM chip serialising ALL weight+activation+
+// result traffic through one sdram_controller.v instance), or
+// something else? See hardware/v2/logs/decisions.log (search
+// "memory-bound") and errors.log ERR-0030/ERR-0031 for the
+// measurement (tb_nms_dstress_sdram_unified.v: SDRAM controller port
+// busy ~81.6% of all cycles at BOTH N_SLOTS=4 and N_SLOTS=8) this
+// variant exists to stress-test.
+//
+// NOT a proposal to change the real V2 board (hardware/v2/constraints/
+// v2_board_top.lpf wires exactly ONE physical AS4C4M16SA-6TIN chip --
+// unchanged, untouched). This module is SIMULATION-side exploration
+// only: it duplicates sdram_unified_backend.v (byte-for-byte reused,
+// zero modification) into TWO independent instances --
+// u_sdram_backend_w : services ONLY the W (weight-fetch) port,
+// ar_req permanently tied low
+// u_sdram_backend_ar : services ONLY the AR (activation-fill +
+// result-writeback) port, w_req permanently
+// tied low
+// -- each with its OWN sdram_controller.v instance and its OWN set of
+// physical SDRAM pins, i.e. what a real two-physical-chip board
+// revision would look like. Tying ar_req/w_req permanently to 0 on
+// the respective instance is safe by inspection of sdram_unified_
+// backend.v's own state machine: with ar_req/ar_req_pending always 0,
+// S_AR_RD_WAIT/S_AR_WR_WAIT are simply never entered (and symmetrically
+// for w_req/S_W_WAIT) -- no dead-state risk, no latch ever set from a
+// permanently-0 input.
+//
+// u_dataflow_core, u_arbiter (AR, N_SLOTS+1 ports), and u_arbiter_wide
+// (W, N_SLOTS ports) are ALL byte-for-byte unchanged from the single-
+// bank wrapper -- only the final memory-side fanout changes.
+// ================================================================
+
+module nms_neural_multiprocessor_sdram_dualbank #(
+ parameter DATA_WIDTH = 8,
+ parameter P_IN = 8,
+ parameter ACC_WIDTH = 32,
+ parameter ADDR_WIDTH = 26,
+ parameter N_SLOTS = 2,
+ parameter N_NODES = 16,
+ parameter MAX_DEPS = 4,
+ parameter QUEUE_DEPTH = 8,
+ parameter MAX_TILES = 16,
+ parameter PREFETCH_DISTANCE = 8,
+ parameter CLK_FREQ_MHZ = 80
+)(
+ input wire clk,
+ input wire rst,
+
+ input wire reg_valid,
+ output wire reg_ready,
+ input wire [$clog2(N_NODES)-1:0] reg_node_id,
+ input wire [$clog2(MAX_DEPS+1)-1:0] reg_required,
+ input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids,
+ input wire [ADDR_WIDTH-1:0] reg_x_base,
+ input wire [ADDR_WIDTH-1:0] reg_w_base,
+ input wire [15:0] reg_n_tiles,
+ input wire [ADDR_WIDTH-1:0] reg_result_addr,
+
+ output wire data_ready,
+
+ // ---- Bank W: weight-fetch-only physical SDRAM chip ----
+ output wire sdram_w_cke,
+ output wire sdram_w_cs_n,
+ output wire sdram_w_ras_n,
+ output wire sdram_w_cas_n,
+ output wire sdram_w_we_n,
+ output wire [1:0] sdram_w_ba,
+ output wire [12:0] sdram_w_a,
+ inout wire [15:0] sdram_w_dq,
+ output wire [1:0] sdram_w_dqm,
+
+ // ---- Bank AR: activation-fill + result-writeback-only physical
+ // SDRAM chip ----
+ output wire sdram_ar_cke,
+ output wire sdram_ar_cs_n,
+ output wire sdram_ar_ras_n,
+ output wire sdram_ar_cas_n,
+ output wire sdram_ar_we_n,
+ output wire [1:0] sdram_ar_ba,
+ output wire [12:0] sdram_ar_a,
+ inout wire [15:0] sdram_ar_dq,
+ output wire [1:0] sdram_ar_dqm
+);
+
+ wire [N_SLOTS:0] slot_mem_req, slot_mem_wr;
+ wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr;
+ wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata, slot_mem_rdata;
+ wire [N_SLOTS:0] slot_mem_lb_n, slot_mem_ub_n;
+ wire [N_SLOTS:0] slot_mem_ready;
+
+ wire [N_SLOTS-1:0] wide_slot_mem_req;
+ wire [ADDR_WIDTH*N_SLOTS-1:0] wide_slot_mem_addr;
+ wire [64*N_SLOTS-1:0] wide_slot_mem_rdata;
+ wire [N_SLOTS-1:0] wide_slot_mem_ready;
+
+ nms_dataflow_core_sdram #(
+ .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),
+ .MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PREFETCH_DISTANCE)
+ ) u_dataflow_core (
+ .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),
+ .data_ready(data_ready),
+ .slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr),
+ .slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n),
+ .slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready),
+ .wide_slot_mem_req(wide_slot_mem_req), .wide_slot_mem_addr(wide_slot_mem_addr),
+ .wide_slot_mem_rdata(wide_slot_mem_rdata), .wide_slot_mem_ready(wide_slot_mem_ready)
+ );
+
+ // ---- AR: activation-fill (shared, 1 port) + per-slot result
+ // writeback (N_SLOTS ports), arbitrated exactly as before ----
+ wire arb_m_req, arb_m_wr;
+ wire [ADDR_WIDTH-1:0] arb_m_addr;
+ wire [15:0] arb_m_wdata;
+ wire arb_m_lb_n, arb_m_ub_n;
+ wire [15:0] arb_m_rdata;
+ wire arb_m_ready;
+
+ slot_mem_arbiter #(
+ .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS+1)
+ ) u_arbiter (
+ .clk(clk), .rst(rst),
+ .s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr),
+ .s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n),
+ .s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
+ .m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
+ .m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
+ .m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
+ );
+
+ // ---- W: weight fetch (N_SLOTS ports), arbitrated exactly as
+ // before -- weight fetch never writes, same tie-off convention
+ // as STEP16-19 ----
+ wire [N_SLOTS-1:0] wide_s_wr = {N_SLOTS{1'b0}};
+ wire [64*N_SLOTS-1:0] wide_s_wdata = {(64*N_SLOTS){1'b0}};
+ wire [N_SLOTS-1:0] wide_s_lb_n = {N_SLOTS{1'b0}};
+ wire [N_SLOTS-1:0] wide_s_ub_n = {N_SLOTS{1'b0}};
+
+ wire wide_arb_m_req, wide_arb_m_wr;
+ wire [ADDR_WIDTH-1:0] wide_arb_m_addr;
+ wire [63:0] wide_arb_m_wdata;
+ wire wide_arb_m_lb_n, wide_arb_m_ub_n;
+ wire [63:0] wide_arb_m_rdata;
+ wire wide_arb_m_ready;
+
+ slot_mem_arbiter_wide #(
+ .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS), .DATA_WIDTH(64)
+ ) u_arbiter_wide (
+ .clk(clk), .rst(rst),
+ .s_req(wide_slot_mem_req), .s_wr(wide_s_wr), .s_addr(wide_slot_mem_addr),
+ .s_wdata(wide_s_wdata), .s_lb_n(wide_s_lb_n), .s_ub_n(wide_s_ub_n),
+ .s_rdata(wide_slot_mem_rdata), .s_ready(wide_slot_mem_ready),
+ .m_req(wide_arb_m_req), .m_wr(wide_arb_m_wr), .m_addr(wide_arb_m_addr), .m_wdata(wide_arb_m_wdata),
+ .m_lb_n(wide_arb_m_lb_n), .m_ub_n(wide_arb_m_ub_n),
+ .m_rdata(wide_arb_m_rdata), .m_ready(wide_arb_m_ready)
+ );
+
+ // ---- Bank W: dedicated physical SDRAM backend, W port only, AR
+ // port permanently idle (tied off -- never issues an AR-side
+ // physical transaction, see header note on why this is safe) ----
+ sdram_unified_backend #(
+ .ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ)
+ ) u_sdram_backend_w (
+ .clk(clk), .rst(rst),
+ .w_req(wide_arb_m_req), .w_addr(wide_arb_m_addr),
+ .w_rdata(wide_arb_m_rdata), .w_ready(wide_arb_m_ready),
+ .ar_req(1'b0), .ar_wr(1'b0), .ar_addr({ADDR_WIDTH{1'b0}}),
+ .ar_wdata(16'h0), .ar_lb_n(1'b1), .ar_ub_n(1'b1),
+ .ar_rdata(), .ar_ready(),
+ .sdram_cke(sdram_w_cke), .sdram_cs_n(sdram_w_cs_n), .sdram_ras_n(sdram_w_ras_n),
+ .sdram_cas_n(sdram_w_cas_n), .sdram_we_n(sdram_w_we_n),
+ .sdram_ba(sdram_w_ba), .sdram_a(sdram_w_a), .sdram_dq(sdram_w_dq), .sdram_dqm(sdram_w_dqm)
+ );
+
+ // ---- Bank AR: dedicated physical SDRAM backend, AR port only, W
+ // port permanently idle ----
+ sdram_unified_backend #(
+ .ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ)
+ ) u_sdram_backend_ar (
+ .clk(clk), .rst(rst),
+ .w_req(1'b0), .w_addr({ADDR_WIDTH{1'b0}}),
+ .w_rdata(), .w_ready(),
+ .ar_req(arb_m_req), .ar_wr(arb_m_wr), .ar_addr(arb_m_addr), .ar_wdata(arb_m_wdata),
+ .ar_lb_n(arb_m_lb_n), .ar_ub_n(arb_m_ub_n),
+ .ar_rdata(arb_m_rdata), .ar_ready(arb_m_ready),
+ .sdram_cke(sdram_ar_cke), .sdram_cs_n(sdram_ar_cs_n), .sdram_ras_n(sdram_ar_ras_n),
+ .sdram_cas_n(sdram_ar_cas_n), .sdram_we_n(sdram_ar_we_n),
+ .sdram_ba(sdram_ar_ba), .sdram_a(sdram_ar_a), .sdram_dq(sdram_ar_dq), .sdram_dqm(sdram_ar_dqm)
+ );
+
+endmodule
diff --git a/hardware/v2/nms/sim/tb_nms_dstress_sdram_dualbank.v b/hardware/v2/nms/sim/tb_nms_dstress_sdram_dualbank.v
new file mode 100644
index 0000000..d30123a
--- /dev/null
+++ b/hardware/v2/nms/sim/tb_nms_dstress_sdram_dualbank.v
@@ -0,0 +1,576 @@
+`timescale 1ns/1ps
+
+// ================================================================
+// FPGA-Neural V2 -- EXPERIMENTAL two-physical-SDRAM-bank D-Stress
+// benchmark, forked from tb_nms_dstress_sdram_unified.v (STEP19's
+// own official D-Stress regression) with ONLY the memory-side
+// substitution needed to instantiate nms_neural_multiprocessor_sdram_
+// dualbank.v instead of the single-bank nms_neural_multiprocessor_
+// sdram_unified.v -- two independent sdram_model.v instances (u_sdram_w
+// for weight-fetch traffic, u_sdram_ar for activation-fill+result-
+// writeback traffic), each behind its own sdram_unified_backend.v
+// instance and own sdram_controller.v.
+//
+// PURPOSE (see hardware/v2/logs/decisions.log, search "memory-bound"):
+// tb_nms_dstress_sdram_unified.v measured the single shared SDRAM
+// controller port at ~81.6% busy at BOTH N_SLOTS=4 (49927 cycles) and
+// N_SLOTS=8 (49909 cycles) -- i.e. total D-Stress cycle count barely
+// changes with more parallelism, consistent with a memory-bandwidth-
+// bound system, not a compute- or Fmax-bound one. This testbench
+// tests that hypothesis directly: if splitting weight traffic and
+// activation/result traffic onto two INDEPENDENT physical channels
+// (removing the single-controller serialisation point) meaningfully
+// reduces total_cycles and/or busy%, that confirms memory bandwidth
+// (not Fmax, not arbitration logic) as the real ceiling -- independent
+// of whether Phase 2 (85F retarget) or Phase 1 (RTL Fmax work) ever
+// happens.
+//
+// Identical workload (D-Stress only, 256 neurons x 16 tiles), golden
+// model, and bit-exact verification method as the single-bank
+// original -- ONLY the memory-side instantiation and the poke_byte/
+// peek_byte backing-array targets (now split: weight pokes target
+// u_sdram_w.mem, activation/result pokes target u_sdram_ar.mem --
+// this mapping already existed in the single-bank testbench's own
+// naming convention, poke_byte_weight/peek_byte_weight vs poke_byte/
+// peek_byte, even though both pointed at the same physical array
+// before) change.
+// ================================================================
+module tb #(
+ parameter N_SLOTS_CFG = 2,
+ parameter PFD_CFG = 8
+);
+
+ localparam ADDR_WIDTH = 26;
+ localparam DATA_WIDTH = 8;
+ localparam P_IN = 8;
+ localparam ACC_WIDTH = 32;
+ localparam N_NODES = 1024;
+ localparam MAX_DEPS = 8;
+ localparam QUEUE_DEPTH = 8;
+ localparam NODE_IDW = $clog2(N_NODES);
+ localparam CLK_PERIOD = 12.5; // 80 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;
+
+ // ---- Bank W: weight-only physical SDRAM ----
+ wire sdram_w_cke, sdram_w_cs_n, sdram_w_ras_n, sdram_w_cas_n, sdram_w_we_n;
+ wire [1:0] sdram_w_ba;
+ wire [12:0] sdram_w_a;
+ wire [15:0] sdram_w_dq;
+ wire [1:0] sdram_w_dqm;
+
+ // ---- Bank AR: activation+result-only physical SDRAM ----
+ wire sdram_ar_cke, sdram_ar_cs_n, sdram_ar_ras_n, sdram_ar_cas_n, sdram_ar_we_n;
+ wire [1:0] sdram_ar_ba;
+ wire [12:0] sdram_ar_a;
+ wire [15:0] sdram_ar_dq;
+ wire [1:0] sdram_ar_dqm;
+
+ nms_neural_multiprocessor_sdram_dualbank #(
+ .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
+ .N_SLOTS(N_SLOTS_CFG), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
+ .MAX_TILES(16), .PREFETCH_DISTANCE(PFD_CFG), .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),
+ .sdram_w_cke(sdram_w_cke), .sdram_w_cs_n(sdram_w_cs_n), .sdram_w_ras_n(sdram_w_ras_n),
+ .sdram_w_cas_n(sdram_w_cas_n), .sdram_w_we_n(sdram_w_we_n),
+ .sdram_w_ba(sdram_w_ba), .sdram_w_a(sdram_w_a), .sdram_w_dq(sdram_w_dq), .sdram_w_dqm(sdram_w_dqm),
+ .sdram_ar_cke(sdram_ar_cke), .sdram_ar_cs_n(sdram_ar_cs_n), .sdram_ar_ras_n(sdram_ar_ras_n),
+ .sdram_ar_cas_n(sdram_ar_cas_n), .sdram_ar_we_n(sdram_ar_we_n),
+ .sdram_ar_ba(sdram_ar_ba), .sdram_ar_a(sdram_ar_a), .sdram_ar_dq(sdram_ar_dq), .sdram_ar_dqm(sdram_ar_dqm)
+ );
+
+ sdram_model #(.CLK_FREQ_MHZ(80)) u_sdram_w (
+ .clk(clk), .cke(sdram_w_cke), .cs_n(sdram_w_cs_n), .ras_n(sdram_w_ras_n),
+ .cas_n(sdram_w_cas_n), .we_n(sdram_w_we_n), .ba(sdram_w_ba), .a(sdram_w_a),
+ .dq(sdram_w_dq), .dqm(sdram_w_dqm)
+ );
+
+ sdram_model #(.CLK_FREQ_MHZ(80)) u_sdram_ar (
+ .clk(clk), .cke(sdram_ar_cke), .cs_n(sdram_ar_cs_n), .ras_n(sdram_ar_ras_n),
+ .cas_n(sdram_ar_cas_n), .we_n(sdram_ar_we_n), .ba(sdram_ar_ba), .a(sdram_ar_a),
+ .dq(sdram_ar_dq), .dqm(sdram_ar_dqm)
+ );
+
+ // ============================================================
+ // Backdoor access -- activations/results go to u_sdram_ar.mem
+ // (AR port), weights go to u_sdram_w.mem (W port). This is the
+ // ONLY functional difference from the single-bank testbench's
+ // own poke_byte/peek_byte (u_sdram.mem) vs poke_byte_weight/
+ // peek_byte_weight (also u_sdram.mem before this fork).
+ // ============================================================
+ task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input signed [7:0] val);
+ reg [24:0] word_addr;
+ begin
+ word_addr = byte_addr[ADDR_WIDTH-1:1];
+ if (byte_addr[0] == 1'b0) u_sdram_ar.mem[word_addr][7:0] = val;
+ else u_sdram_ar.mem[word_addr][15:8] = val;
+ end
+ endtask
+
+ function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr);
+ reg [24:0] word_addr;
+ begin
+ word_addr = byte_addr[ADDR_WIDTH-1:1];
+ peek_byte = (byte_addr[0] == 1'b0) ? u_sdram_ar.mem[word_addr][7:0] : u_sdram_ar.mem[word_addr][15:8];
+ end
+ endfunction
+
+ task automatic poke_byte_weight(input [ADDR_WIDTH-1:0] byte_addr, input signed [7:0] val);
+ reg [24:0] word_addr;
+ begin
+ word_addr = byte_addr[ADDR_WIDTH-1:1];
+ if (byte_addr[0] == 1'b0) u_sdram_w.mem[word_addr][7:0] = val;
+ else u_sdram_w.mem[word_addr][15:8] = val;
+ end
+ endtask
+
+ function automatic signed [7:0] peek_byte_weight(input [ADDR_WIDTH-1:0] byte_addr);
+ reg [24:0] word_addr;
+ begin
+ word_addr = byte_addr[ADDR_WIDTH-1:1];
+ peek_byte_weight = (byte_addr[0] == 1'b0) ? u_sdram_w.mem[word_addr][7:0] : u_sdram_w.mem[word_addr][15:8];
+ end
+ endfunction
+
+ function automatic signed [7:0] relu_sat(input integer acc);
+ begin
+ if (acc <= 0) relu_sat = 8'sd0;
+ else if (acc > 127) relu_sat = 8'sd127;
+ else relu_sat = acc[7:0];
+ end
+ endfunction
+
+ task automatic register_node(
+ input [NODE_IDW-1:0] nid,
+ input [$clog2(MAX_DEPS+1)-1:0] required,
+ input [MAX_DEPS*NODE_IDW-1:0] producer_ids_packed,
+ 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 = producer_ids_packed;
+ 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
+
+ reg measure_en;
+ integer total_cycles;
+ integer psram_busy_cycles;
+ integer ni;
+ genvar gi;
+
+ reg [N_SLOTS_CFG-1:0] slot_busy_bit;
+ reg [N_SLOTS_CFG-1:0] slot_tile_bit;
+ integer slot_busy_cycles [0:N_SLOTS_CFG-1];
+ integer slot_tiles_delivered [0:N_SLOTS_CFG-1];
+
+ generate
+ for (gi = 0; gi < N_SLOTS_CFG; gi = gi + 1) begin : GEN_SLOT_MON
+ always @(*) begin
+ slot_busy_bit[gi] = (u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.state != 3'd0);
+ slot_tile_bit[gi] = u_nmp.u_dataflow_core.GEN_SLOT[gi].mm_operand_valid &&
+ u_nmp.u_dataflow_core.GEN_SLOT[gi].mm_operand_ready;
+ end
+ end
+ endgenerate
+
+ integer active_count;
+ integer active_hist [0:4];
+ integer useful_mac_cycles;
+ integer first_tile_cyc;
+ integer last_tile_cyc;
+ integer any_tile_bit;
+
+ // ---- Per-bank SDRAM controller-port instrumentation (real
+ // signals on EACH bank's own sdram_controller.v instance) ----
+ integer sdram_req_count_w, sdram_ready_count_w, sdram_busy_cycles_w, sdram_refresh_count_w;
+ integer sdram_req_count_ar, sdram_ready_count_ar, sdram_wr_count_ar, sdram_busy_cycles_ar, sdram_refresh_count_ar;
+ integer either_busy_cycles; // cycles where AT LEAST ONE bank's controller is busy
+ reg sdram_prev_refwait_w, sdram_prev_refwait_ar;
+
+ initial begin
+ active_hist[0]=0; active_hist[1]=0; active_hist[2]=0; active_hist[3]=0; active_hist[4]=0;
+ useful_mac_cycles = 0; first_tile_cyc = -1; last_tile_cyc = -1;
+ sdram_req_count_w=0; sdram_ready_count_w=0; sdram_busy_cycles_w=0; sdram_refresh_count_w=0;
+ sdram_req_count_ar=0; sdram_ready_count_ar=0; sdram_wr_count_ar=0; sdram_busy_cycles_ar=0; sdram_refresh_count_ar=0;
+ either_busy_cycles=0;
+ sdram_prev_refwait_w=1'b0; sdram_prev_refwait_ar=1'b0;
+ end
+
+ always @(posedge clk) begin
+ if (measure_en) begin
+ active_count = slot_busy_bit[0];
+ for (ni = 1; ni < N_SLOTS_CFG; ni = ni + 1) active_count = active_count + slot_busy_bit[ni];
+ active_hist[active_count] <= active_hist[active_count] + 1;
+
+ any_tile_bit = slot_tile_bit[0];
+ for (ni = 1; ni < N_SLOTS_CFG; ni = ni + 1) any_tile_bit = any_tile_bit | slot_tile_bit[ni];
+ for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1)
+ if (slot_tile_bit[ni]) useful_mac_cycles <= useful_mac_cycles + 1;
+ if (any_tile_bit) begin
+ if (first_tile_cyc < 0) first_tile_cyc <= total_cycles;
+ last_tile_cyc <= total_cycles;
+ end
+
+ // ---- Bank W ----
+ if (u_nmp.u_sdram_backend_w.u_sdram_ctrl.req) sdram_req_count_w <= sdram_req_count_w + 1;
+ if (u_nmp.u_sdram_backend_w.u_sdram_ctrl.ready) sdram_ready_count_w <= sdram_ready_count_w + 1;
+ if (u_nmp.u_sdram_backend_w.u_sdram_ctrl.busy) sdram_busy_cycles_w <= sdram_busy_cycles_w + 1;
+ sdram_prev_refwait_w <= (u_nmp.u_sdram_backend_w.u_sdram_ctrl.state == 5'd9);
+ if (u_nmp.u_sdram_backend_w.u_sdram_ctrl.state == 5'd9 && !sdram_prev_refwait_w)
+ sdram_refresh_count_w <= sdram_refresh_count_w + 1;
+
+ // ---- Bank AR ----
+ if (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.req) begin
+ sdram_req_count_ar <= sdram_req_count_ar + 1;
+ if (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.wr) sdram_wr_count_ar <= sdram_wr_count_ar + 1;
+ end
+ if (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.ready) sdram_ready_count_ar <= sdram_ready_count_ar + 1;
+ if (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.busy) sdram_busy_cycles_ar <= sdram_busy_cycles_ar + 1;
+ sdram_prev_refwait_ar <= (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.state == 5'd9);
+ if (u_nmp.u_sdram_backend_ar.u_sdram_ctrl.state == 5'd9 && !sdram_prev_refwait_ar)
+ sdram_refresh_count_ar <= sdram_refresh_count_ar + 1;
+
+ if (u_nmp.u_sdram_backend_w.u_sdram_ctrl.busy || u_nmp.u_sdram_backend_ar.u_sdram_ctrl.busy)
+ either_busy_cycles <= either_busy_cycles + 1;
+ end
+ end
+
+ task automatic report_step17_instrumentation;
+ real active_pct [0:4];
+ real util_pct, startup_cycles, drain_cycles;
+ real busy_pct_w, busy_pct_ar, either_busy_pct;
+ integer kk;
+ begin
+ $display(" ---- cycle decomposition ----");
+ for (kk = 0; kk <= N_SLOTS_CFG; kk = kk + 1) begin
+ active_pct[kk] = (total_cycles > 0) ? (100.0*active_hist[kk]/total_cycles) : 0.0;
+ $display(" active_slots=%0d: %0d cycles (%0.2f%%)", kk, active_hist[kk], active_pct[kk]);
+ end
+ util_pct = (total_cycles > 0) ? (100.0*useful_mac_cycles/(total_cycles*1.0*N_SLOTS_CFG)) : 0.0;
+ $display(" useful_mac_cycles (slot-tile-delivery events, summed)=%0d (%0.2f%% of total_cycles*N_SLOTS)", useful_mac_cycles, util_pct);
+ startup_cycles = (first_tile_cyc >= 0) ? (1.0*first_tile_cyc) : 0.0;
+ drain_cycles = (last_tile_cyc >= 0) ? (1.0*(total_cycles - last_tile_cyc)) : 0.0;
+ $display(" startup (cycles before first tile delivered anywhere)=%0.0f", startup_cycles);
+ $display(" drain (cycles after last tile delivered, until job completion)=%0.0f", drain_cycles);
+ $display(" ---- DUAL-BANK SDRAM effectiveness ----");
+ busy_pct_w = (total_cycles > 0) ? (100.0*sdram_busy_cycles_w/total_cycles) : 0.0;
+ busy_pct_ar = (total_cycles > 0) ? (100.0*sdram_busy_cycles_ar/total_cycles) : 0.0;
+ either_busy_pct = (total_cycles > 0) ? (100.0*either_busy_cycles/total_cycles) : 0.0;
+ $display(" BANK W (weight-fetch): req=%0d ready=%0d busy=%0d/%0d (%0.2f%%) refresh=%0d",
+ sdram_req_count_w, sdram_ready_count_w, sdram_busy_cycles_w, total_cycles, busy_pct_w, sdram_refresh_count_w);
+ $display(" BANK AR (activation+result): req=%0d ready=%0d wr=%0d busy=%0d/%0d (%0.2f%%) refresh=%0d",
+ sdram_req_count_ar, sdram_ready_count_ar, sdram_wr_count_ar, sdram_busy_cycles_ar, total_cycles, busy_pct_ar, sdram_refresh_count_ar);
+ $display(" EITHER bank busy=%0d/%0d (%0.2f%%) (compare directly vs single-bank's own sdram_busy_pct)",
+ either_busy_cycles, total_cycles, either_busy_pct);
+ end
+ endtask
+
+ reg [N_SLOTS_CFG-1:0] slot_could_present_act;
+ reg [N_SLOTS_CFG-1:0] slot_weight_blocking;
+ reg [N_SLOTS_CFG-1:0] slot_stalled_this_tile;
+ reg [31:0] prev_tile_idx [0:N_SLOTS_CFG-1];
+ integer weight_stall_cycles [0:N_SLOTS_CFG-1];
+ integer tiles_prefetched_clean [0:N_SLOTS_CFG-1];
+ integer tiles_consumed_total [0:N_SLOTS_CFG-1];
+ wire [31:0] slot_tile_idx_w [0:N_SLOTS_CFG-1];
+
+ generate
+ for (gi = 0; gi < N_SLOTS_CFG; gi = gi + 1) begin : GEN_SLOT_PF_MON
+ assign slot_tile_idx_w[gi] = {16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx};
+ always @(*) begin
+ slot_could_present_act[gi] =
+ ({{16{1'b0}}, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx} <
+ {16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.n_tiles_reg}) &&
+ ({{16{1'b0}}, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx} <
+ {16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.usable_act});
+ slot_weight_blocking[gi] =
+ slot_could_present_act[gi] &&
+ !(u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx <
+ u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.wgt_ready_count) &&
+ !u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.operand_valid;
+ end
+ end
+ endgenerate
+
+ always @(posedge clk) begin
+ if (measure_en) begin
+ for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1) begin
+ if (prev_tile_idx[ni] != slot_tile_idx_w[ni]) begin
+ slot_stalled_this_tile[ni] <= 1'b0;
+ prev_tile_idx[ni] <= slot_tile_idx_w[ni];
+ end else if (slot_weight_blocking[ni]) begin
+ slot_stalled_this_tile[ni] <= 1'b1;
+ weight_stall_cycles[ni] <= weight_stall_cycles[ni] + 1;
+ end
+ if (slot_tile_bit[ni]) begin
+ tiles_consumed_total[ni] <= tiles_consumed_total[ni] + 1;
+ if (!slot_stalled_this_tile[ni])
+ tiles_prefetched_clean[ni] <= tiles_prefetched_clean[ni] + 1;
+ end
+ end
+ end
+ end
+
+ integer jobs_allocated, jobs_completed, wakeups;
+ integer waiting_sum, ready_sum, dispatched_sum, sample_count;
+ reg sample_occupancy;
+ integer scan_i;
+ integer waiting_now, ready_now, dispatched_now;
+
+ always @(posedge clk) begin
+ if (measure_en) begin
+ total_cycles <= total_cycles + 1;
+ if (u_nmp.u_arbiter.owner != 0) psram_busy_cycles <= psram_busy_cycles + 1;
+ for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1) begin
+ if (slot_busy_bit[ni]) slot_busy_cycles[ni] <= slot_busy_cycles[ni] + 1;
+ if (slot_tile_bit[ni]) slot_tiles_delivered[ni] <= slot_tiles_delivered[ni] + 1;
+ end
+ if (u_nmp.u_dataflow_core.dm_ready_valid && u_nmp.u_dataflow_core.dm_ready_ready)
+ jobs_allocated <= jobs_allocated + 1;
+ if (u_nmp.u_dataflow_core.dir_job_out_done)
+ jobs_completed <= jobs_completed + 1;
+ if (u_nmp.u_dataflow_core.dm_producer_done_valid)
+ wakeups <= wakeups + 1;
+
+ if (sample_occupancy) begin
+ waiting_now = 0; ready_now = 0; dispatched_now = 0;
+ for (scan_i = 0; scan_i < N_NODES; scan_i = scan_i + 1) begin
+ case (u_nmp.u_dataflow_core.u_dep_mgr.node_state[scan_i])
+ 2'd1: waiting_now = waiting_now + 1;
+ 2'd2: ready_now = ready_now + 1;
+ 2'd3: dispatched_now = dispatched_now + 1;
+ default: ;
+ endcase
+ end
+ waiting_sum <= waiting_sum + waiting_now;
+ ready_sum <= ready_sum + ready_now;
+ dispatched_sum <= dispatched_sum + dispatched_now;
+ sample_count <= sample_count + 1;
+ end
+ end
+ end
+
+ task automatic reset_instrumentation(input do_sample_occupancy);
+ integer k;
+ begin
+ active_hist[0]=0; active_hist[1]=0; active_hist[2]=0; active_hist[3]=0; active_hist[4]=0;
+ useful_mac_cycles = 0; first_tile_cyc = -1; last_tile_cyc = -1;
+ sdram_req_count_w=0; sdram_ready_count_w=0; sdram_busy_cycles_w=0; sdram_refresh_count_w=0;
+ sdram_req_count_ar=0; sdram_ready_count_ar=0; sdram_wr_count_ar=0; sdram_busy_cycles_ar=0; sdram_refresh_count_ar=0;
+ either_busy_cycles=0;
+ total_cycles = 0; psram_busy_cycles = 0;
+ jobs_allocated = 0; jobs_completed = 0; wakeups = 0;
+ waiting_sum = 0; ready_sum = 0; dispatched_sum = 0; sample_count = 0;
+ sample_occupancy = do_sample_occupancy;
+ for (k = 0; k < N_SLOTS_CFG; k = k + 1) begin
+ slot_busy_cycles[k] = 0;
+ slot_tiles_delivered[k] = 0;
+ weight_stall_cycles[k] = 0;
+ tiles_prefetched_clean[k] = 0;
+ tiles_consumed_total[k] = 0;
+ slot_stalled_this_tile[k] = 1'b0;
+ prev_tile_idx[k] = 32'hFFFFFFFF;
+ end
+ end
+ endtask
+
+ task automatic report_instrumentation(input [255:0] label, input integer n_neurons_completed);
+ integer k, total_tiles;
+ integer total_weight_stall_cycles, total_tiles_consumed_all, total_tiles_prefetched_clean;
+ real avg_waiting, avg_ready, avg_dispatched;
+ real psram_util, sustained_mac_per_cycle, wallclock_us;
+ real processor_utilization, weight_stall_pct, prefetch_effectiveness_pct;
+ begin
+ total_tiles = 0;
+ for (k = 0; k < N_SLOTS_CFG; k = k + 1) total_tiles = total_tiles + slot_tiles_delivered[k];
+ avg_waiting = (sample_count > 0) ? (1.0*waiting_sum/sample_count) : 0.0;
+ avg_ready = (sample_count > 0) ? (1.0*ready_sum/sample_count) : 0.0;
+ avg_dispatched = (sample_count > 0) ? (1.0*dispatched_sum/sample_count) : 0.0;
+ psram_util = (total_cycles > 0) ? (100.0*psram_busy_cycles/total_cycles) : 0.0;
+ sustained_mac_per_cycle = (total_cycles > 0) ? (1.0*total_tiles*P_IN/total_cycles) : 0.0;
+ wallclock_us = total_cycles * CLK_PERIOD / 1000.0;
+ $display("---- BENCHMARK REPORT: %0s ----", label);
+ $display(" total_cycles=%0d wallclock_us=%0.3f", total_cycles, wallclock_us);
+ $display(" neurons_completed=%0d tiles_delivered(real)=%0d", n_neurons_completed, total_tiles);
+ $display(" jobs_allocated=%0d jobs_completed=%0d dependency_wakeups=%0d", jobs_allocated, jobs_completed, wakeups);
+ $display(" shared AR (activation+result) arbiter-side utilization: %0.1f%% (%0d/%0d busy cycles)", psram_util, psram_busy_cycles, total_cycles);
+ for (k = 0; k < N_SLOTS_CFG; k = k + 1)
+ $display(" slot %0d: busy=%0d/%0d (%0.1f%%) tiles=%0d", k, slot_busy_cycles[k], total_cycles,
+ (total_cycles>0)?(100.0*slot_busy_cycles[k]/total_cycles):0.0, slot_tiles_delivered[k]);
+ if (sample_count > 0)
+ $display(" dependency_manager avg occupancy (sampled every measured cycle): waiting=%0.2f ready=%0.2f dispatched=%0.2f", avg_waiting, avg_ready, avg_dispatched);
+ else
+ $display(" dependency_manager occupancy: NOT SAMPLED for this workload");
+ $display(" DERIVED: sustained end-to-end MAC/cycle = %0.4f (real tiles*%0d / real total_cycles)", sustained_mac_per_cycle, P_IN);
+ if (n_neurons_completed > 0)
+ $display(" DERIVED: cycles/neuron = %0.2f", 1.0*total_cycles/n_neurons_completed);
+ if (total_tiles > 0)
+ $display(" DERIVED: cycles/tile = %0.2f", 1.0*total_cycles/total_tiles);
+
+ total_weight_stall_cycles = 0; total_tiles_consumed_all = 0; total_tiles_prefetched_clean = 0;
+ for (k = 0; k < N_SLOTS_CFG; k = k + 1) begin
+ total_weight_stall_cycles = total_weight_stall_cycles + weight_stall_cycles[k];
+ total_tiles_consumed_all = total_tiles_consumed_all + tiles_consumed_total[k];
+ total_tiles_prefetched_clean = total_tiles_prefetched_clean + tiles_prefetched_clean[k];
+ end
+ processor_utilization = (total_cycles > 0) ? (100.0*total_tiles/(total_cycles*1.0)) : 0.0;
+ weight_stall_pct = (total_cycles > 0) ? (100.0*total_weight_stall_cycles/(total_cycles*N_SLOTS_CFG*1.0)) : 0.0;
+ prefetch_effectiveness_pct = (total_tiles_consumed_all > 0) ?
+ (100.0*total_tiles_prefetched_clean/(total_tiles_consumed_all*1.0)) : 0.0;
+ $display(" [STEP11] PFD=%0d weight_stall_cycles(sum,all slots)=%0d (%0.2f%% of total_cycles*N_SLOTS)",
+ PFD_CFG, total_weight_stall_cycles, weight_stall_pct);
+ $display(" [STEP11] tiles_consumed=%0d tiles_prefetched_clean(zero weight-block before consumption)=%0d",
+ total_tiles_consumed_all, total_tiles_prefetched_clean);
+ $display(" [STEP11] DERIVED: prefetch_effectiveness = %0.2f%%", prefetch_effectiveness_pct);
+ $display(" [STEP11] DERIVED: processor_utilization reference sustained_mac_per_cycle=%0.4f", sustained_mac_per_cycle);
+ end
+ endtask
+
+ integer errors, tests;
+
+ task automatic run_dense_layer(
+ input [255:0] label,
+ input integer n_neurons,
+ input integer n_tiles_count,
+ input [NODE_IDW-1:0] node_base,
+ input [ADDR_WIDTH-1:0] x_base,
+ input [ADDR_WIDTH-1:0] w_base,
+ input [ADDR_WIDTH-1:0] res_base,
+ input sample_occ
+ );
+ integer n, t, k, len, acc;
+ reg signed [7:0] xv, wv, golden, real_y;
+ reg [MAX_DEPS*NODE_IDW-1:0] no_deps;
+ integer completed, wd2;
+ begin
+ len = n_tiles_count * P_IN;
+ no_deps = {(MAX_DEPS*NODE_IDW){1'b0}};
+
+ for (k = 0; k < len; k = k + 1)
+ poke_byte(x_base + k, ((k % 8) + 1));
+
+ reset_instrumentation(sample_occ);
+ measure_en = 1'b1;
+
+ for (n = 0; n < n_neurons; n = n + 1) begin
+ acc = 0;
+ for (t = 0; t < n_tiles_count; t = t + 1) begin
+ for (k = 0; k < P_IN; k = k + 1) begin
+ xv = peek_byte(x_base + t*P_IN + k);
+ wv = (((n + t*P_IN + k) % 8) + 1);
+ poke_byte_weight(w_base + n*len + t*P_IN + k, wv);
+ acc = acc + xv*wv;
+ end
+ end
+ golden = relu_sat(acc);
+ poke_byte(res_base + n, 8'sd0);
+ register_node(node_base + n[NODE_IDW-1:0], 0, no_deps,
+ x_base, w_base + n*len, n_tiles_count[15:0], res_base + n);
+ if ((n % 32) == 0) begin
+ $display(" [%0s] registered %0d/%0d", label, n+1, n_neurons);
+ $fflush;
+ end
+ end
+ $display(" [%0s] all %0d neurons registered, waiting for completion...", label, n_neurons);
+ $fflush;
+
+ completed = 0; wd2 = 0;
+ while (completed < n_neurons && wd2 < 2000000) begin
+ @(posedge clk);
+ wd2 = wd2 + 1;
+ completed = jobs_completed;
+ if ((wd2 % 20000) == 0) begin
+ $display(" [%0s] watchdog %0d: completed=%0d/%0d total_cycles=%0d", label, wd2, completed, n_neurons, total_cycles);
+ $fflush;
+ end
+ end
+ repeat(5) @(posedge clk);
+ measure_en = 1'b0;
+
+ tests = tests + 1;
+ if (completed < n_neurons) begin
+ $display("FAIL %0s: only %0d/%0d neurons completed within watchdog", label, completed, n_neurons);
+ errors = errors + 1;
+ end else begin : check_block
+ integer local_errors;
+ local_errors = 0;
+ for (n = 0; n < n_neurons; n = n + 1) begin
+ acc = 0;
+ for (t = 0; t < n_tiles_count; t = t + 1)
+ for (k = 0; k < P_IN; k = k + 1)
+ acc = acc + peek_byte(x_base + t*P_IN + k) * peek_byte_weight(w_base + n*len + t*P_IN + k);
+ golden = relu_sat(acc);
+ real_y = peek_byte(res_base + n);
+ if (real_y !== golden) begin
+ $display("FAIL %0s neuron %0d: real=%0d golden=%0d", label, n, real_y, golden);
+ local_errors = local_errors + 1;
+ end
+ end
+ if (local_errors == 0)
+ $display("PASS %0s: all %0d neurons bit-exact vs golden", label, n_neurons);
+ else
+ errors = errors + 1;
+ end
+ report_instrumentation(label, n_neurons);
+ report_step17_instrumentation;
+ end
+ endtask
+
+ 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;
+ measure_en = 0;
+ repeat(5) @(posedge clk);
+ rst = 0;
+
+ $display("========================================");
+ $display("NMS D-Stress benchmark (EXPERIMENTAL DUAL-BANK SDRAM: independent W and AR physical chips) -- N_SLOTS_CFG=%0d PFD_CFG=%0d", N_SLOTS_CFG, PFD_CFG);
+ $display("========================================");
+
+ wait (u_nmp.u_sdram_backend_w.u_sdram_ctrl.state == u_nmp.u_sdram_backend_w.u_sdram_ctrl.S_IDLE &&
+ u_nmp.u_sdram_backend_ar.u_sdram_ctrl.state == u_nmp.u_sdram_backend_ar.u_sdram_ctrl.S_IDLE);
+ @(posedge clk);
+
+ run_dense_layer("D-Stress", 256, 16, 16'd400, 26'h200000, 26'h010000, 26'h300000, 1'b0);
+
+ repeat (4) @(posedge clk);
+ if (u_nmp.data_ready !== 1'b1) begin
+ $display("FAIL data_ready: expected 1 after graph completion, got %b", u_nmp.data_ready);
+ errors = errors + 1;
+ end else begin
+ $display("PASS data_ready: correctly asserted after graph completion");
+ end
+
+ $display("========================================");
+ if (errors == 0)
+ $display("ALL %0d WORKLOAD SUITES PASSED (N_SLOTS_CFG=%0d, PFD_CFG=%0d, DUAL-BANK SDRAM)", tests, N_SLOTS_CFG, PFD_CFG);
+ else
+ $display("FAILED: %0d/%0d workload suite(s) had errors -- see messages above", errors, tests);
+ $display("========================================");
+ $finish;
+ end
+
+endmodule