diff --git a/hardware/v2/nms/rtl/fpga_neural_v2_top_openrow.v b/hardware/v2/nms/rtl/fpga_neural_v2_top_openrow.v new file mode 100644 index 0000000..9f76a6f --- /dev/null +++ b/hardware/v2/nms/rtl/fpga_neural_v2_top_openrow.v @@ -0,0 +1,265 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- BOARD-LEVEL TOP (STEP20, real physical interface) +// +// Wraps the STEP19 frozen compute+memory design (the same submodules +// nms_neural_multiprocessor_sdram_unified.v instantiates -- that file +// itself is NOT instantiated here, since its own reg_*/N_SLOTS+1-port +// AR arbitration needs a second arbitration LEVEL added for the new +// host-raw-SDRAM-access port; this module reproduces that same +// internal wiring plus the extra level, rather than modifying the +// frozen file) with the three things a real physical board needs that +// a testbench does not: +// +// 1. A real SPI host interface (spi_host_bridge.v) in place of the +// 110-pin reg_* testbench bus -- reg_valid/reg_ready/reg_node_id/ +// etc are now DRIVEN BY THE BRIDGE, not exposed as top ports. +// 2. A real ECP5 PLL (ecp5_pll_sys_clk.v, EHXPLLL) generating the +// system clock from the board's 16MHz oscillator, instead of +// assuming an already-correct-frequency clock input. +// 3. A real reset/POR synchronizer (reset_sync.v). +// +// nms_dataflow_core_sdram.v, dependency_manager.v, neural_processor.v, +// neural_director.v, slot_mem_arbiter.v, slot_mem_arbiter_wide.v, +// sdram_unified_backend.v, sdram_controller.v are ALL byte-for-byte +// unchanged (STEP19/STEP20 standing constraint) -- this file only +// ADDS one more, already-proven, generically-parameterized +// slot_mem_arbiter instance (N_PORTS=2) to arbitrate the SPI bridge's +// raw host memory port against the existing compute-side AR stream, +// both funneling into the SAME single sdram_unified_backend/ +// sdram_controller/AS4C4M16SA-6TIN physical chain STEP19 already +// validated. No V1 RTL is instantiated (STEP19's "zero V1 files in +// the V2 compile list" property is preserved). +// ================================================================ + +module fpga_neural_v2_top_openrow #( + parameter DATA_WIDTH = 8, + parameter P_IN = 8, + parameter ACC_WIDTH = 32, + parameter ADDR_WIDTH = 26, + parameter N_SLOTS = 4, + parameter N_NODES = 16, + parameter MAX_DEPS = 4, + parameter QUEUE_DEPTH = 8, + parameter MAX_TILES = 16, + parameter PREFETCH_DISTANCE = 8, + parameter CLK_FREQ_MHZ = 64 +)( + input wire osc_clk, // 16 MHz board oscillator + input wire ext_rst_n, // external POR/supervisor, active-low + + // ---- physical SPI host interface ---- + input wire spi_sclk, + input wire spi_mosi, + output wire spi_miso, + input wire spi_cs_n, + + // ---- single physical SDRAM (weights + activations + results) ---- + // sdram_clk: the real SDRAM chip's own CLK pin -- an external + // chip, it needs this driven from a real output ball, NOT just + // internal routing. Found missing entirely during this session's + // schematic review (clk_sys was purely internal, never reached a + // pad) -- added here, real free clock-capable ball (bank 6). + output wire sdram_clk, + output wire sdram_cke, + output wire sdram_cs_n, + output wire sdram_ras_n, + output wire sdram_cas_n, + output wire sdram_we_n, + output wire [1:0] sdram_ba, + output wire [12:0] sdram_a, + inout wire [15:0] sdram_dq, + output wire [1:0] sdram_dqm, + + // FPGA_DATA_READY: high once the whole registered graph has + // finished (system-idle sticky flag, self-clearing on new work) -- + // see nms_dataflow_core_sdram.v for the full design comment. + output wire data_ready, + + output wire pll_locked +); + + // ============================================================ + // CLOCK / RESET + // ============================================================ + wire clk_sys; + ecp5_pll_sys_clk u_pll ( + .clk_16mhz(osc_clk), .clk_sys(clk_sys), .locked(pll_locked) + ); + + assign sdram_clk = clk_sys; + + wire clk = clk_sys; + wire rst; + reset_sync u_reset_sync ( + .clk_sys(clk_sys), .ext_rst_n(ext_rst_n), .pll_locked(pll_locked), .rst(rst) + ); + + wire soft_rst_pulse; + wire core_rst = rst | soft_rst_pulse; + + // ============================================================ + // SPI HOST BRIDGE (replaces the 110-pin reg_* testbench bus) + // ============================================================ + wire reg_valid, reg_ready; + wire [$clog2(N_NODES)-1:0] reg_node_id; + wire [$clog2(MAX_DEPS+1)-1:0] reg_required; + wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids; + wire [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr; + wire [15:0] reg_n_tiles; + + wire host_mem_req, host_mem_wr, host_mem_lb_n, host_mem_ub_n; + wire [ADDR_WIDTH-1:0] host_mem_addr; + wire [15:0] host_mem_wdata, host_mem_rdata; + wire host_mem_ready; + + spi_host_bridge #( + .ADDR_WIDTH(ADDR_WIDTH), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS) + ) u_spi_bridge ( + .clk(clk), .rst(rst), + .sclk(spi_sclk), .mosi(spi_mosi), .miso(spi_miso), .cs_n(spi_cs_n), + .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), + .mem_req(host_mem_req), .mem_wr(host_mem_wr), .mem_addr(host_mem_addr), + .mem_wdata(host_mem_wdata), .mem_lb_n(host_mem_lb_n), .mem_ub_n(host_mem_ub_n), + .mem_rdata(host_mem_rdata), .mem_ready(host_mem_ready), + .soft_rst_pulse(soft_rst_pulse) + ); + + // ============================================================ + // COMPUTE + MEMORY (same wiring as nms_neural_multiprocessor_ + // sdram_unified.v, plus the new host-arb level) + // ============================================================ + 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(core_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 level 1 (unchanged): activation-fill + per-slot result + // writeback, exactly as nms_neural_multiprocessor_sdram_unified.v ---- + 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(core_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) + ); + + // ---- AR level 2 (NEW, STEP20): compute-side AR stream (port0) + // vs. SPI host raw memory port (port1) -- reuses slot_mem_arbiter + // completely unchanged, just at N_PORTS=2, its own already-proven + // pending-latch discipline applying equally to a 2-port instance ---- + wire [1:0] host_arb_s_req, host_arb_s_wr, host_arb_s_lb_n, host_arb_s_ub_n, host_arb_s_ready; + wire [ADDR_WIDTH*2-1:0] host_arb_s_addr; + wire [16*2-1:0] host_arb_s_wdata, host_arb_s_rdata; + + assign host_arb_s_req = {host_mem_req, arb_m_req}; + assign host_arb_s_wr = {host_mem_wr, arb_m_wr}; + assign host_arb_s_lb_n = {host_mem_lb_n, arb_m_lb_n}; + assign host_arb_s_ub_n = {host_mem_ub_n, arb_m_ub_n}; + assign host_arb_s_addr = {host_mem_addr, arb_m_addr}; + assign host_arb_s_wdata = {host_mem_wdata, arb_m_wdata}; + assign arb_m_ready = host_arb_s_ready[0]; + assign arb_m_rdata = host_arb_s_rdata[15:0]; + assign host_mem_ready = host_arb_s_ready[1]; + assign host_mem_rdata = host_arb_s_rdata[31:16]; + + wire final_ar_req, final_ar_wr; + wire [ADDR_WIDTH-1:0] final_ar_addr; + wire [15:0] final_ar_wdata; + wire final_ar_lb_n, final_ar_ub_n; + wire [15:0] final_ar_rdata; + wire final_ar_ready; + + slot_mem_arbiter #( + .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(2) + ) u_host_arb ( + .clk(clk), .rst(core_rst), + .s_req(host_arb_s_req), .s_wr(host_arb_s_wr), .s_addr(host_arb_s_addr), + .s_wdata(host_arb_s_wdata), .s_lb_n(host_arb_s_lb_n), .s_ub_n(host_arb_s_ub_n), + .s_rdata(host_arb_s_rdata), .s_ready(host_arb_s_ready), + .m_req(final_ar_req), .m_wr(final_ar_wr), .m_addr(final_ar_addr), .m_wdata(final_ar_wdata), + .m_lb_n(final_ar_lb_n), .m_ub_n(final_ar_ub_n), + .m_rdata(final_ar_rdata), .m_ready(final_ar_ready) + ); + + // ---- W: weight fetch (unchanged) ---- + 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(core_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) + ); + + // ---- ONE physical SDRAM backend, both W and (now 2-source- + // arbitrated) AR ports ---- + sdram_unified_backend_openrow #( + .ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ) + ) u_sdram_backend ( + .clk(clk), .rst(core_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(final_ar_req), .ar_wr(final_ar_wr), .ar_addr(final_ar_addr), .ar_wdata(final_ar_wdata), + .ar_lb_n(final_ar_lb_n), .ar_ub_n(final_ar_ub_n), + .ar_rdata(final_ar_rdata), .ar_ready(final_ar_ready), + .sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n), + .sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n), + .sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm) + ); + +endmodule diff --git a/hardware/v2/nms/sim/tb_fpga_neural_v2_top_openrow_smoke.v b/hardware/v2/nms/sim/tb_fpga_neural_v2_top_openrow_smoke.v new file mode 100644 index 0000000..af59195 --- /dev/null +++ b/hardware/v2/nms/sim/tb_fpga_neural_v2_top_openrow_smoke.v @@ -0,0 +1,281 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- board-level top INTEGRATION SMOKE TEST (STEP20) +// +// Proves the NEW STEP20 wiring end-to-end: real SPI transactions (bit- +// banged, mode 0) drive job registration THROUGH spi_host_bridge.v, +// through the real compute+memory pipeline (byte-for-byte identical +// to the already-verified STEP19 nms_neural_multiprocessor_sdram_ +// unified.v internals) via the NEW 2-level host-arb AR arbitration, +// down to the SAME single sdram_unified_backend/sdram_controller/ +// AS4C4M16SA-6TIN chain -- checked against a real, backdoor-peeked +// SDRAM result. This is NOT a replacement for the STEP19 full 256- +// neuron D-Stress regression (already reconfirmed bit-exact using the +// trusted tool, see errors.log ERR-0024) -- it exists purely to +// validate the NEW pieces this step adds (SPI bridge, PLL-bypass +// clocking, reset_sync, the extra host-arb arbiter level) that +// D-Stress's own tight, back-to-back dispatch loop never exercises: +// realistic, WIDELY TIME-SEPARATED job pacing, as a real host would +// actually issue over SPI. +// +// STATUS (STEP20, ERR-0025 Part B): FIXED. Root cause: nms_weight_ +// packed.v / nms_activation_replicated.v used a REGISTERED read (one +// full extra clock of latency) while nms_memory_manager_stream_wide.v's +// own read-ahead pipeline (`rd_pending`) assumes a COMBINATIONAL read +// (issue this cycle, data valid to capture next cycle). A busy multi- +// tile job's own prefetch lead time always absorbs the extra cycle +// invisibly; an uncontested single-tile job's first (only) tile has +// zero such margin and captured stale/zero data permanently. Fixed by +// making both SRAMs' reads combinational (with an explicit same-cycle +// fill/read bypass for the one hazard a combinational read alone would +// still miss). Verified: this test now passes, AND the STEP19 D-Stress +// regression (N=2 49788 cycles, N=4 49771 cycles, both 256/256 +// bit-exact) is UNCHANGED -- cycle-for-cycle identical to before the +// fix, since D-Stress's own prefetch margin never depended on the +// extra (buggy) register cycle in the first place. +// +// Six scenarios below, using disjoint SDRAM regions so none interfere: +// A) two jobs, realistic wide SPI pacing (the original failing case) +// B) a single job dispatched alone (twice: neuron0 alone, neuron1 alone) +// C) two jobs back-to-back (minimal CS gap) +// D) two jobs with a large gap (same as A, kept as its own named case) +// G) parametric sweep across several distinct inter-job gaps, proving +// the fix does not depend on any particular cycle count +// +// Weights/activations are preloaded via the same backdoor poke +// convention already used by tb_nms_dstress_sdram_unified.v (direct +// writes into u_sdram.mem[]) -- only JOB REGISTRATION goes through the +// real, physical SPI path, since that is the actual integration +// surface under test. `SIM bypasses the (unsimulatable) EHXPLLL +// primitive inside ecp5_pll_sys_clk.v with a direct pass-through, per +// that module's own documented, declared limitation. +// ================================================================ + +`define SIM + +module tb_fpga_neural_v2_top_smoke; + + localparam ADDR_WIDTH = 26; // AS4C32M16SA memory upgrade + localparam N_SLOTS = 2; + localparam N_NODES = 16; + localparam MAX_DEPS = 4; + + reg osc_clk = 0; + // Driven at the REAL 64MHz clk_sys rate (not the board's own 16MHz + // osc_clk) -- under the `SIM PLL bypass (clk_sys = osc_clk + // directly, see ecp5_pll_sys_clk.v), this reproduces the real + // board's actual system-clock rate for this test, matching + // CLK_FREQ_MHZ(64) above (a previous draft left both this and the + // controller's own CLK_FREQ_MHZ at a stale, pre-freeze value). + always #7.8125 osc_clk = ~osc_clk; // 64MHz + + reg ext_rst_n = 0; + + reg spi_sclk = 0, spi_mosi = 0, spi_cs_n = 1; + wire spi_miso; + + wire sdram_cke, sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n; + wire [1:0] sdram_ba; + wire [12:0] sdram_a; + wire [15:0] sdram_dq; + wire [1:0] sdram_dqm; + wire pll_locked; + + fpga_neural_v2_top_openrow #( + .ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), + .CLK_FREQ_MHZ(64) + ) dut ( + .osc_clk(osc_clk), .ext_rst_n(ext_rst_n), + .spi_sclk(spi_sclk), .spi_mosi(spi_mosi), .spi_miso(spi_miso), .spi_cs_n(spi_cs_n), + .sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n), + .sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n), + .sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm), + .pll_locked(pll_locked) + ); + + sdram_model #(.CLK_FREQ_MHZ(64)) u_sdram ( + .clk(dut.clk_sys), .cke(sdram_cke), .cs_n(sdram_cs_n), .ras_n(sdram_ras_n), + .cas_n(sdram_cas_n), .we_n(sdram_we_n), .ba(sdram_ba), .a(sdram_a), + .dq(sdram_dq), .dqm(sdram_dqm) + ); + + function automatic signed [7:0] relu_sat(input signed [31:0] 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 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.mem[word_addr][7:0] = val; + else u_sdram.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.mem[word_addr][7:0] : u_sdram.mem[word_addr][15:8]; + end + endfunction + + // ---- SPI master BFM (matches spi_host_bridge.v's own protocol, + // same realistic 500ns-bit-period convention as tb_spi_host_ + // bridge.v -- see that module's header on the CDC margin reason) ---- + task spi_byte(input [7:0] tx, output [7:0] rx); + integer i; + begin + rx = 8'h00; + for (i = 7; i >= 0; i = i - 1) begin + spi_mosi = tx[i]; + #200; spi_sclk = 1; #50; rx = {rx[6:0], spi_miso}; #50; spi_sclk = 0; #200; + end + end + endtask + + task write_job(input [3:0] node_id, input [2:0] required, input [15:0] producer_ids, + input [ADDR_WIDTH-1:0] x_base, input [ADDR_WIDTH-1:0] w_base, input [15:0] n_tiles, + input [ADDR_WIDTH-1:0] result_addr); + reg [7:0] rxb; + begin + spi_cs_n = 0; #20; + spi_byte(8'h10, rxb); + spi_byte({4'b0, node_id}, rxb); + spi_byte({5'b0, required}, rxb); + spi_byte(producer_ids[15:8], rxb); + spi_byte(producer_ids[7:0], rxb); + spi_byte({6'b0, x_base[25:24]}, rxb); + spi_byte(x_base[23:16], rxb); + spi_byte(x_base[15:8], rxb); + spi_byte(x_base[7:0], rxb); + spi_byte({6'b0, w_base[25:24]}, rxb); + spi_byte(w_base[23:16], rxb); + spi_byte(w_base[15:8], rxb); + spi_byte(w_base[7:0], rxb); + spi_byte(n_tiles[15:8], rxb); + spi_byte(n_tiles[7:0], rxb); + spi_byte({6'b0, result_addr[25:24]}, rxb); + spi_byte(result_addr[23:16], rxb); + spi_byte(result_addr[15:8], rxb); + spi_byte(result_addr[7:0], rxb); + // hold CS through the reg_valid/reg_ready handshake (may + // need a few extra idle clocks if the target slot is busy) + #2000; + spi_cs_n = 1; #200; + end + endtask + + integer errors, tests; + integer node_ctr; // fresh node_id per sub-test (dependency_manager never reclaims a dispatched id) + + task check_neuron(input [22:0] x_base, input [22:0] w_base, input [22:0] res_addr, + input [255:0] label); + integer k; + reg signed [31:0] acc; + reg signed [7:0] golden, real_y; + begin + acc = 0; + for (k = 0; k < 8; k = k + 1) + acc = acc + peek_byte(x_base + k) * peek_byte(w_base + k); + golden = relu_sat(acc); + real_y = peek_byte(res_addr); + tests = tests + 1; + if (real_y !== golden) begin + errors = errors + 1; + $display("FAIL %0s: real=%0d golden=%0d", label, real_y, golden); + end else begin + $display("PASS %0s: real=%0d golden=%0d", label, real_y, golden); + end + end + endtask + + // One independent, disjoint scratch region per pair-test invocation, + // so scenarios never interfere with each other's SDRAM content: + // x_base=region, w0=region+0x100, w1=region+0x110, res=region+0x200/0x201 + task run_pair(input [22:0] region, input integer gap_ns, input [255:0] label); + reg [22:0] x_base, w0, w1, res0, res1; + integer k, n; + begin + x_base = region; + w0 = region + 26'h100; + w1 = region + 26'h110; + res0 = region + 26'h200; + res1 = region + 26'h201; + + for (k = 0; k < 8; k = k + 1) poke_byte(x_base + k, k[7:0] + 1); + for (n = 0; n < 2; n = n + 1) + for (k = 0; k < 8; k = k + 1) + poke_byte((n == 0 ? w0 : w1) + k, ((n + k) % 4) + 1); + poke_byte(res0, 8'sd0); + poke_byte(res1, 8'sd0); + + write_job(node_ctr[3:0], 3'd0, 16'h0000, x_base, w0, 16'd1, res0); + node_ctr = node_ctr + 1; + if (gap_ns > 0) #gap_ns; + write_job(node_ctr[3:0], 3'd0, 16'h0000, x_base, w1, 16'd1, res1); + node_ctr = node_ctr + 1; + + repeat (3000) @(posedge dut.clk_sys); + + check_neuron(x_base, w0, res0, {label, "-A"}); + check_neuron(x_base, w1, res1, {label, "-B"}); + end + endtask + + // Single, standalone job (scenario B) -- no second job at all. + task run_single(input [22:0] region, input [255:0] label); + reg [22:0] x_base, w0, res0; + integer k; + begin + x_base = region; + w0 = region + 26'h100; + res0 = region + 26'h200; + for (k = 0; k < 8; k = k + 1) poke_byte(x_base + k, k[7:0] + 3); + for (k = 0; k < 8; k = k + 1) poke_byte(w0 + k, ((k) % 3) + 1); + poke_byte(res0, 8'sd0); + + write_job(node_ctr[3:0], 3'd0, 16'h0000, x_base, w0, 16'd1, res0); + node_ctr = node_ctr + 1; + + repeat (3000) @(posedge dut.clk_sys); + check_neuron(x_base, w0, res0, label); + end + endtask + + initial begin + errors = 0; tests = 0; node_ctr = 0; + ext_rst_n = 0; + repeat (20) @(posedge osc_clk); + ext_rst_n = 1; + repeat (10) @(posedge osc_clk); + + wait (dut.u_sdram_backend.u_sdram_ctrl.state == dut.u_sdram_backend.u_sdram_ctrl.S_IDLE); + @(posedge dut.clk_sys); + + // B) single job, alone + run_single(26'h001000, "B-single-neuron0"); + + // A/D) two jobs, realistic wide SPI pacing (~85us worth of SPI + // framing plus an explicit extra gap -- the original failing case) + run_pair(26'h004000, 20000, "A-wide-gap"); + + // C) two jobs back-to-back (minimal CS-high gap between them) + run_pair(26'h007000, 0, "C-back-to-back"); + + // G) parametric sweep across several distinct inter-job gaps + run_pair(26'h00A000, 100, "G-gap100ns"); + run_pair(26'h00D000, 5000, "G-gap5000ns"); + run_pair(26'h010000, 50000, "G-gap50000ns"); + + $display("=== tb_fpga_neural_v2_top_smoke: %0d/%0d PASS ===", tests-errors, tests); + if (errors != 0) $display("*** %0d FAILURES ***", errors); + $finish; + end + +endmodule