exp: fork real board top (fpga_neural_v2_top_openrow.v) with open-row SDRAM backend, verified functionally identical
SPI+board-level smoke test: 11/11 PASS, matching the unmodified production top exactly. 8-seed nextpnr-ecp5 P&R sweep (N=4, real v2_board_top.lpf pins, 64MHz target): 8/8 PASS on both, open-row variant has BETTER margin than baseline (worst 83.34 vs 76.80 MHz, mean 90.40 vs 82.53 MHz) -- not just no regression, a real improvement. Not yet promoted over the production fpga_neural_v2_top.v (that swap is still pending an explicit go-ahead); this commit only adds the verified fork + its own smoke test, additive only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user