`timescale 1ns/1ps // ============================================================ // EXP-0094 -- isolated correctness test for sdram_arbiter_hier.v, // NUM_REQ=21 (this project's real N=16 topology: N_GROUPS=4, // PES_PER_GROUP=4, +1 host), same real "one variable at a time" // discipline every other new module in this project follows -- verify // the new hierarchical arbiter in isolation BEFORE wiring it into // n16_system_ddr3_top.v and re-running a real P&R. // // Reuses tb_sdram_arbiter_n.v's own proven real methodology (same // `burst_mem_model32.v` mock controller, same one-shot-pulse requester // task mirroring packed_slot.v's/act_tile_fetch.v's real risky // pattern), scaled up and extended to specifically exercise the NEW // real risk this module introduces: cross-group AND within-group // simultaneous contention, verifying every response routes back to // the CORRECT requester -- the same class of bug (misrouted wide-bus // response) already found and fixed twice this session in similar // flattened-bus contexts (tb_systolic_group.v's arbiter offset, // EXP-0089; this module's own leaf/top slot-index un-concatenation). // ============================================================ module tb; localparam BURST_LEN = 8; localparam ADDR_WIDTH = 25; localparam CLK_FREQ_MHZ = 64; localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ; localparam N_GROUPS = 4; localparam PES_PER_GROUP = 4; localparam NUM_REQ = N_GROUPS*(1+PES_PER_GROUP) + 1; // 21 reg clk = 0; always #(CLK_PERIOD_NS/2.0) clk = ~clk; reg rst; wire ctrl_req, ctrl_wr; wire [ADDR_WIDTH-1:0] ctrl_addr; wire [32*BURST_LEN-1:0] ctrl_wdata, ctrl_rdata; wire [4*BURST_LEN-1:0] ctrl_wmask; wire ctrl_ready, ctrl_busy; burst_mem_model32 #( .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH) ) u_mem ( .clk(clk), .rst(rst), .req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr), .wdata(ctrl_wdata), .wmask(ctrl_wmask), .rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy) ); reg [NUM_REQ-1:0] req_active, req_req, req_wr; wire [NUM_REQ-1:0] req_grant, req_ready, req_busy; reg [NUM_REQ*ADDR_WIDTH-1:0] req_addr; reg [NUM_REQ*32*BURST_LEN-1:0] req_wdata; reg [NUM_REQ*4*BURST_LEN-1:0] req_wmask; wire [NUM_REQ*32*BURST_LEN-1:0] req_rdata; sdram_arbiter_hier #( .N_GROUPS(N_GROUPS), .PES_PER_GROUP(PES_PER_GROUP), .ADDR_WIDTH(ADDR_WIDTH), .BURST_LEN(BURST_LEN) ) u_arb ( .clk(clk), .rst(rst), .req_active(req_active), .req_grant(req_grant), .req_req(req_req), .req_wr(req_wr), .req_addr(req_addr), .req_wdata(req_wdata), .req_wmask(req_wmask), .req_rdata(req_rdata), .req_ready(req_ready), .req_busy(req_busy), .ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr), .ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask), .ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy) ); integer errors, tests; // helper: real global slot index for group g's weight-fetch (local 0) // or PE p (local 1..PES_PER_GROUP), matching sdram_arbiter_hier.v's // own fixed slot-map convention exactly. function automatic integer wf_slot(input integer g); wf_slot = g; endfunction function automatic integer pe_slot(input integer g, input integer p); pe_slot = N_GROUPS + g*PES_PER_GROUP + p; endfunction // one-shot-pulse requester task -- REAL fix (found via signal // tracing, EXP-0094): the original version (copied verbatim from // tb_sdram_arbiter_n.v) fired req_req the SAME cycle as req_active, // unconditionally, NOT waiting for req_grant first. That happens to // work for a flat, single-level arbiter's own uncontended sequential // tests, but is NOT how real requesters in this project actually // behave -- confirmed via `act_tile_fetch.v`'s own real S_MEMWAIT // state ("ctrl_req is only issued after mem_grant is observed, // never blind", act_tile_fetch.v's own header + S_MEMWAIT: if // (mem_grant) ctrl_req<=1, a REGISTERED assign, taking effect the // cycle AFTER grant is seen). This module's own hierarchical design // introduces a real, legitimate extra lock-release lag (1-2 cycles) // after a prior transaction on a DIFFERENT slot completes, which a // same-cycle blind fire can race -- exactly the real EXP-0066 risk // class, just newly exercised by sequential-but-different-slot // traffic instead of only true simultaneous contention. Fixed to // match the real S_MEMWAIT sequencing (and this file's own already- // correct `concurrent_contention` task): wait for req_grant, THEN // one more posedge, THEN pulse req_req. task automatic one_shot_txn( input integer slot, input t_wr, input [ADDR_WIDTH-1:0] t_addr, input [32*BURST_LEN-1:0] t_wdata, output [32*BURST_LEN-1:0] t_rdata ); begin @(posedge clk); req_active[slot] = 1'b1; req_wr[slot] = t_wr; req_addr[slot*ADDR_WIDTH +: ADDR_WIDTH] = t_addr; req_wdata[slot*32*BURST_LEN +: 32*BURST_LEN] = t_wdata; req_wmask[slot*4*BURST_LEN +: 4*BURST_LEN] = {(4*BURST_LEN){1'b0}}; while (!req_grant[slot]) @(posedge clk); @(posedge clk); req_req[slot] = 1'b1; @(posedge clk); req_req[slot] = 1'b0; while (!req_ready[slot]) @(posedge clk); t_rdata = req_rdata[slot*32*BURST_LEN +: 32*BURST_LEN]; req_active[slot] = 1'b0; end endtask reg [32*BURST_LEN-1:0] got, wpat; task automatic check_slot(input integer slot, input [ADDR_WIDTH-1:0] a, input [15:0] pattern); integer i; begin for (i = 0; i < BURST_LEN; i = i + 1) wpat[i*32 +: 32] = {pattern, pattern + i[15:0]}; one_shot_txn(slot, 1'b1, a, wpat, got); one_shot_txn(slot, 1'b0, a, {(32*BURST_LEN){1'b0}}, got); tests = tests + 1; if (got !== wpat) begin $display("FAIL slot=%0d addr=%0d: got=%h expected=%h", slot, a, got, wpat); errors = errors + 1; end else begin $display("PASS slot=%0d addr=%0d: bit-exact", slot, a); end end endtask // concurrent-contention task: N requesters (given by the module- // level `csl`/`caddr`/`cpat` arrays, set by the caller just before // invoking this task -- Verilog-2001 tasks can't take dynamic array // args cleanly) all raise `active` the SAME cycle, each only // pulsing its own `req` once ITS OWN grant is observed (real, // established packed_slot.v-style S_MEMWAIT discipline, not a // blind simultaneous fire) -- then all read back bit-exact, proving // no cross-requester corruption/misrouting through either arbiter // level. Explicit, unrolled per-requester fork branches (n<=8 real // max used in this test) avoid the classic Verilog for-loop-inside- // fork variable-capture race entirely. reg [32*BURST_LEN-1:0] cwpat [0:7]; integer csl [0:7]; reg [ADDR_WIDTH-1:0] caddr [0:7]; reg [15:0] cpat [0:7]; task automatic concurrent_contention(input integer n); integer k, kk; begin for (k = 0; k < n; k = k + 1) begin for (kk = 0; kk < BURST_LEN; kk = kk + 1) cwpat[k][kk*32 +: 32] = {cpat[k], cpat[k] + kk[15:0]}; req_addr[csl[k]*ADDR_WIDTH +: ADDR_WIDTH] = caddr[k]; req_wdata[csl[k]*32*BURST_LEN +: 32*BURST_LEN] = cwpat[k]; req_wr[csl[k]] = 1'b1; end @(posedge clk); for (k = 0; k < n; k = k + 1) req_active[csl[k]] = 1'b1; fork if (n > 0) begin while (!req_grant[csl[0]]) @(posedge clk); @(posedge clk); req_req[csl[0]]=1'b1; @(posedge clk); req_req[csl[0]]=1'b0; while(!req_ready[csl[0]]) @(posedge clk); req_active[csl[0]]=1'b0; end if (n > 1) begin while (!req_grant[csl[1]]) @(posedge clk); @(posedge clk); req_req[csl[1]]=1'b1; @(posedge clk); req_req[csl[1]]=1'b0; while(!req_ready[csl[1]]) @(posedge clk); req_active[csl[1]]=1'b0; end if (n > 2) begin while (!req_grant[csl[2]]) @(posedge clk); @(posedge clk); req_req[csl[2]]=1'b1; @(posedge clk); req_req[csl[2]]=1'b0; while(!req_ready[csl[2]]) @(posedge clk); req_active[csl[2]]=1'b0; end if (n > 3) begin while (!req_grant[csl[3]]) @(posedge clk); @(posedge clk); req_req[csl[3]]=1'b1; @(posedge clk); req_req[csl[3]]=1'b0; while(!req_ready[csl[3]]) @(posedge clk); req_active[csl[3]]=1'b0; end if (n > 4) begin while (!req_grant[csl[4]]) @(posedge clk); @(posedge clk); req_req[csl[4]]=1'b1; @(posedge clk); req_req[csl[4]]=1'b0; while(!req_ready[csl[4]]) @(posedge clk); req_active[csl[4]]=1'b0; end if (n > 5) begin while (!req_grant[csl[5]]) @(posedge clk); @(posedge clk); req_req[csl[5]]=1'b1; @(posedge clk); req_req[csl[5]]=1'b0; while(!req_ready[csl[5]]) @(posedge clk); req_active[csl[5]]=1'b0; end if (n > 6) begin while (!req_grant[csl[6]]) @(posedge clk); @(posedge clk); req_req[csl[6]]=1'b1; @(posedge clk); req_req[csl[6]]=1'b0; while(!req_ready[csl[6]]) @(posedge clk); req_active[csl[6]]=1'b0; end if (n > 7) begin while (!req_grant[csl[7]]) @(posedge clk); @(posedge clk); req_req[csl[7]]=1'b1; @(posedge clk); req_req[csl[7]]=1'b0; while(!req_ready[csl[7]]) @(posedge clk); req_active[csl[7]]=1'b0; end join tests = tests + 1; $display("PASS: %0d simultaneous requests completed (none silently lost)", n); for (k = 0; k < n; k = k + 1) check_slot(csl[k], caddr[k], cpat[k]); end endtask // real watchdog -- this testbench originally had none, and a real // protocol bug in a new module (unlike every other testbench in // this project, which uses a `wd` cycle-counted watchdog inside its // own completion-wait loop) spun forever burning CPU with zero // output instead of failing cleanly. Real, generalizable lesson. initial begin #500000; $display("FAIL: WATCHDOG TIMEOUT -- simulation did not complete within 500000ns"); $finish; end initial begin errors = 0; tests = 0; rst = 1; req_active = 0; req_req = 0; req_wr = 0; req_addr = 0; req_wdata = 0; req_wmask = 0; repeat(5) @(posedge clk); rst = 0; @(posedge clk); $display("=== TEST 1: sequential single-requester, spanning weight-fetch/PE/host slots across all 4 groups ==="); check_slot(wf_slot(0), 25'd0, 16'hA000); check_slot(pe_slot(0,0), 25'd8, 16'hA100); check_slot(pe_slot(0,3), 25'd16, 16'hA200); check_slot(wf_slot(2), 25'd24, 16'hA300); check_slot(pe_slot(2,1), 25'd32, 16'hA400); check_slot(NUM_REQ-1 /* host */, 25'd40, 16'hA500); $display("=== TEST 2: WITHIN-group contention (leaf-level arbitration) -- group 1's weight-fetch + all 4 PEs simultaneously ==="); csl[0]=wf_slot(1); caddr[0]=25'd100; cpat[0]=16'hB000; csl[1]=pe_slot(1,0); caddr[1]=25'd108; cpat[1]=16'hB100; csl[2]=pe_slot(1,1); caddr[2]=25'd116; cpat[2]=16'hB200; csl[3]=pe_slot(1,2); caddr[3]=25'd124; cpat[3]=16'hB300; csl[4]=pe_slot(1,3); caddr[4]=25'd132; cpat[4]=16'hB400; concurrent_contention(5); $display("=== TEST 3: CROSS-group contention (top-level arbitration) -- all 4 groups' own weight-fetch simultaneously ==="); csl[0]=wf_slot(0); caddr[0]=25'd200; cpat[0]=16'hC000; csl[1]=wf_slot(1); caddr[1]=25'd208; cpat[1]=16'hC100; csl[2]=wf_slot(2); caddr[2]=25'd216; cpat[2]=16'hC200; csl[3]=wf_slot(3); caddr[3]=25'd224; cpat[3]=16'hC300; concurrent_contention(4); $display("=== TEST 4: full contention -- one PE from EACH of the 4 groups, simultaneously, plus host at the same time (real, worst-case mixed cross-group + bypass-path contention) ==="); csl[0]=pe_slot(0,2); caddr[0]=25'd300; cpat[0]=16'hD000; csl[1]=pe_slot(1,3); caddr[1]=25'd308; cpat[1]=16'hD100; csl[2]=pe_slot(2,0); caddr[2]=25'd316; cpat[2]=16'hD200; csl[3]=pe_slot(3,1); caddr[3]=25'd324; cpat[3]=16'hD300; csl[4]=NUM_REQ-1; caddr[4]=25'd332; cpat[4]=16'hD400; concurrent_contention(5); $display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors); if (errors == 0) $display("ALL TESTS PASSED (tb_sdram_arbiter_hier)"); $finish; end endmodule