FASE #1 hardware freeze for FPGA-Neural V2, N4/P8, single external SDRAM (Alliance Memory AS4C4M16SA-6TIN) serving weights, activations, and results through one physical sdram_controller.v instance. Removes the PSRAM dependency (hardware/v1/rtl/psram_controller.v + memory_interface.v) from the V2 physical path entirely -- V1 itself remains fully unmodified, the golden reference. New RTL: sdram_unified_backend.v (2-way W/AR arbitration over one SDRAM controller, real per-byte DQM write masking added to sdram_controller.v for correct single-byte result writes with no read-modify-write), nms_neural_multiprocessor_sdram_unified.v (the frozen top-level). Two real bugs found and fixed via full-system testing before being accepted (ERR-0023): a deadlock and an off-by-one data-shift bug in the new arbitration logic. Real results: N=4 and N=2 D-Stress bit-exact (256/256 neurons), 40 real AUTO REFRESH events interleaved with zero corruption, real Yosys+nextpnr-ecp5 synthesis/P&R for LFE5U-45F-8CABGA381 (149/245 TRELLIS_IO, a real 45-pin reduction from the prior dual-memory design). Timing is MARGINAL (1/8 P&R seeds >=80MHz), reported honestly rather than masked by the best seed. Real, sourced ball-level pinout for the SDRAM bus + clk/rst (39/149 signals, P&R-verified) using the official Lattice ECP5U-45 pinout CSV found on disk during this step's own pre-commit review -- corrects an earlier draft that wrongly assumed no real pinout data was available. Chip readiness: NO. Real, disclosed blockers remain (no physical host interface exists yet -- the RTL's own reg_* ports are a 110-pin raw test-harness bus; clock source/PLL decision; power/configuration component selection) -- see hardware/v2/docs/{HARDWARE_FREEZE, CHIP_READINESS,OPEN_ITEMS}.md for the complete, itemized status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
247 lines
11 KiB
Verilog
247 lines
11 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// NMS STEP15 (continuation) -- bit-exact + timing regression for
|
|
// psram_controller_dual32.v: real weight_prefetch_engine_wide.v
|
|
// (STEP14, UNMODIFIED) at MEM_DATA_WIDTH=32, driving the REAL dual-
|
|
// chip 32-bit controller against TWO real psram_model.v instances,
|
|
// with the real production nms_weight_packed.v SRAM as the fill
|
|
// target -- same bit-exact methodology as tb_weight_prefetch_wide.v
|
|
// (STEP14), byte-level pattern (t*8+k)%251, per-tile read-back
|
|
// verification.
|
|
//
|
|
// Covers: bit-exact data (lane ordering: chip0=low16/chip1=high16 of
|
|
// each 32-bit word), page-hit/open/close/boundary behavior (inherited
|
|
// unmodified from the real psram_controller.v, exercised identically
|
|
// per physical chip), reset behavior, back-to-back transactions
|
|
// (n_tiles edge cases), and real cycles/tile timing (expected ~9).
|
|
// ============================================================
|
|
module tb;
|
|
parameter ADDR_WIDTH = 23;
|
|
parameter DATA_WIDTH = 8;
|
|
parameter P_IN = 8;
|
|
parameter MAX_TILES = 512;
|
|
parameter PFD = 600;
|
|
localparam TIW = $clog2(MAX_TILES);
|
|
localparam CNTW = $clog2(MAX_TILES+1);
|
|
localparam CLK_PERIOD = 12.5; // 80MHz, matches every real-PSRAM benchmark in this project
|
|
|
|
reg clk = 0;
|
|
always #(CLK_PERIOD/2.0) clk = ~clk;
|
|
reg rst;
|
|
|
|
reg job_active;
|
|
reg [ADDR_WIDTH-1:0] w_base;
|
|
reg [15:0] n_tiles;
|
|
reg [CNTW-1:0] consumed_count;
|
|
|
|
wire wgt_fill_we;
|
|
wire [TIW-1:0] wgt_fill_addr;
|
|
wire [DATA_WIDTH*P_IN-1:0] wgt_fill_data;
|
|
wire [CNTW-1:0] ready_count;
|
|
|
|
wire mem_req;
|
|
wire [ADDR_WIDTH-1:0] mem_addr;
|
|
wire [31:0] mem_rdata;
|
|
wire mem_ready;
|
|
|
|
weight_prefetch_engine_wide #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH),
|
|
.MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PFD), .MEM_DATA_WIDTH(32)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
.job_active(job_active), .w_base(w_base), .n_tiles(n_tiles),
|
|
.consumed_count(consumed_count),
|
|
.wgt_fill_we(wgt_fill_we), .wgt_fill_addr(wgt_fill_addr), .wgt_fill_data(wgt_fill_data),
|
|
.ready_count(ready_count),
|
|
.mem_req(mem_req), .mem_addr(mem_addr), .mem_rdata(mem_rdata), .mem_ready(mem_ready)
|
|
);
|
|
|
|
// weight fetch never writes -- tie the real controller's write-
|
|
// side inputs to constants (this engine has no write path, same
|
|
// as its own real 16-bit counterpart weight_prefetch_engine.v)
|
|
wire mem_wr = 1'b0;
|
|
wire [31:0] mem_wdata = 32'h0;
|
|
wire [1:0] mem_lb_n = 2'b00, mem_ub_n = 2'b00; // always both bytes of both chips
|
|
|
|
wire [ADDR_WIDTH-1:0] p0_a, p1_a;
|
|
wire [15:0] p0_dq, p1_dq;
|
|
wire p0_ce_n, p0_oe_n, p0_we_n, p0_lb_n, p0_ub_n, p0_zz_n;
|
|
wire p1_ce_n, p1_oe_n, p1_we_n, p1_lb_n, p1_ub_n, p1_zz_n;
|
|
wire lane_sync_error;
|
|
|
|
psram_controller_dual32 #(.ADDR_WIDTH(ADDR_WIDTH), .CLK_FREQ_MHZ(80)) u_dual (
|
|
.clk(clk), .rst(rst),
|
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr), .mem_wdata(mem_wdata),
|
|
.mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
|
|
.mem_rdata(mem_rdata), .mem_ready(mem_ready), .lane_sync_error(lane_sync_error),
|
|
.psram0_a(p0_a), .psram0_dq(p0_dq),
|
|
.psram0_ce_n(p0_ce_n), .psram0_oe_n(p0_oe_n), .psram0_we_n(p0_we_n),
|
|
.psram0_lb_n(p0_lb_n), .psram0_ub_n(p0_ub_n), .psram0_zz_n(p0_zz_n),
|
|
.psram1_a(p1_a), .psram1_dq(p1_dq),
|
|
.psram1_ce_n(p1_ce_n), .psram1_oe_n(p1_oe_n), .psram1_we_n(p1_we_n),
|
|
.psram1_lb_n(p1_lb_n), .psram1_ub_n(p1_ub_n), .psram1_zz_n(p1_zz_n)
|
|
);
|
|
|
|
psram_model #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16), .DEPTH(131072)) u_psram0 (
|
|
.clk(clk), .a(p0_a), .dq(p0_dq),
|
|
.ce_n(p0_ce_n), .oe_n(p0_oe_n), .we_n(p0_we_n),
|
|
.lb_n(p0_lb_n), .ub_n(p0_ub_n), .zz_n(p0_zz_n)
|
|
);
|
|
psram_model #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16), .DEPTH(131072)) u_psram1 (
|
|
.clk(clk), .a(p1_a), .dq(p1_dq),
|
|
.ce_n(p1_ce_n), .oe_n(p1_oe_n), .we_n(p1_we_n),
|
|
.lb_n(p1_lb_n), .ub_n(p1_ub_n), .zz_n(p1_zz_n)
|
|
);
|
|
|
|
// real production weight SRAM, N_SLOTS=1, fed by the fill port
|
|
reg wgt_rd_en;
|
|
reg [TIW-1:0] wgt_rd_addr;
|
|
wire signed [DATA_WIDTH*P_IN-1:0] wgt_rd_data;
|
|
nms_weight_packed #(.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .N_SLOTS(1), .MAX_TILES(MAX_TILES)) u_sram (
|
|
.clk(clk), .rst(rst),
|
|
.fill_we(wgt_fill_we), .fill_addr_flat(wgt_fill_addr), .fill_data_flat(wgt_fill_data),
|
|
.rd_en(wgt_rd_en), .rd_addr_flat(wgt_rd_addr), .rd_data_flat(wgt_rd_data)
|
|
);
|
|
|
|
// Poke the SAME byte-level pattern as tb_weight_prefetch_wide.v
|
|
// (STEP14): tile t, lane k -> (t*8+k) % 251. weight_prefetch_
|
|
// engine_wide.v's own mem_addr is a byte address; the real dual32
|
|
// controller's own mem_addr is a WORD address (4 bytes/word at
|
|
// 32-bit) -- poke directly into each chip's own byte-addressable
|
|
// backing array via the SAME byte-address convention psram_model.v
|
|
// itself uses elsewhere in this project (word_addr = byte_addr>>1
|
|
// PER CHIP, since each chip is still a 16-bit device internally;
|
|
// for the dual32 mapping, chip0 holds bits[15:0] of 32-bit word
|
|
// W=byte_addr>>2, chip1 holds bits[31:16]).
|
|
task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] val);
|
|
reg [ADDR_WIDTH-1:0] word32_addr;
|
|
reg [1:0] byte_in_word32;
|
|
begin
|
|
word32_addr = byte_addr >> 2;
|
|
byte_in_word32 = byte_addr[1:0];
|
|
case (byte_in_word32)
|
|
2'd0: u_psram0.mem[word32_addr][7:0] = val;
|
|
2'd1: u_psram0.mem[word32_addr][15:8] = val;
|
|
2'd2: u_psram1.mem[word32_addr][7:0] = val;
|
|
2'd3: u_psram1.mem[word32_addr][15:8] = val;
|
|
endcase
|
|
end
|
|
endtask
|
|
|
|
integer errors, tests;
|
|
|
|
task automatic fill_pattern(input [ADDR_WIDTH-1:0] base, input integer count);
|
|
integer t, k;
|
|
begin
|
|
for (t = 0; t < count; t = t + 1)
|
|
for (k = 0; k < P_IN; k = k + 1)
|
|
poke_byte(base + t*P_IN + k, (t*8+k) % 251);
|
|
end
|
|
endtask
|
|
|
|
reg freeze_consumer;
|
|
always @(posedge clk) begin
|
|
if (rst || !job_active) consumed_count <= {CNTW{1'b0}};
|
|
else if (!freeze_consumer && consumed_count < ready_count) consumed_count <= consumed_count + 1'b1;
|
|
end
|
|
|
|
integer cyc;
|
|
always @(posedge clk) if (!rst) cyc <= cyc + 1;
|
|
reg trace_on;
|
|
always @(posedge clk) begin
|
|
if (trace_on && mem_req)
|
|
$display(" [%0d] mem_req addr=%0h (chip_word_addr=%0h) wr=%0d", cyc, mem_addr, u_dual.chip_word_addr, mem_wr);
|
|
if (trace_on && mem_ready)
|
|
$display(" [%0d] mem_ready rdata=%08h (rdata0=%04h rdata1=%04h)", cyc, mem_rdata, u_dual.rdata0, u_dual.rdata1);
|
|
if (trace_on && wgt_fill_we)
|
|
$display(" [%0d] wgt_fill_we addr=%0d data=%016h", cyc, wgt_fill_addr, wgt_fill_data);
|
|
end
|
|
|
|
task automatic run_job(input [ADDR_WIDTH-1:0] base, input integer count, input integer watchdog);
|
|
integer wd, t, k;
|
|
reg [7:0] expected;
|
|
begin
|
|
w_base = base; n_tiles = count[15:0];
|
|
job_active = 1'b1;
|
|
wd = 0;
|
|
while (ready_count < count[CNTW-1:0] && wd < watchdog) begin @(posedge clk); wd = wd + 1; end
|
|
@(posedge clk); #1;
|
|
tests = tests + 1;
|
|
if (ready_count !== count[CNTW-1:0]) begin
|
|
$display("FAIL n_tiles=%0d: ready_count=%0d expected=%0d (watchdog=%0d)", count, ready_count, count, wd);
|
|
errors = errors + 1;
|
|
end else begin
|
|
for (t = 0; t < count; t = t + 1) begin
|
|
wgt_rd_addr = t[TIW-1:0]; wgt_rd_en = 1'b1;
|
|
@(posedge clk); @(posedge clk); #1;
|
|
for (k = 0; k < P_IN; k = k + 1) begin
|
|
expected = (t*8+k) % 251;
|
|
if (wgt_rd_data[k*DATA_WIDTH +: DATA_WIDTH] !== expected) begin
|
|
$display("FAIL n_tiles=%0d tile=%0d lane=%0d: got=%0d expected=%0d",
|
|
count, t, k, wgt_rd_data[k*DATA_WIDTH +: DATA_WIDTH], expected);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
end
|
|
if (lane_sync_error) begin
|
|
$display("FAIL n_tiles=%0d: lane_sync_error latched -- chips diverged", count);
|
|
errors = errors + 1;
|
|
end
|
|
$display("PASS n_tiles=%0d: ready_count=%0d, all tiles bit-exact, lane_sync_error=0 (cycles=%0d)",
|
|
count, ready_count, wd);
|
|
end
|
|
job_active = 1'b0;
|
|
repeat(3) @(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
integer t0, t1;
|
|
initial begin
|
|
errors = 0; tests = 0; cyc = 0; freeze_consumer = 0;
|
|
rst = 1; job_active = 0; w_base = 0; n_tiles = 0; consumed_count = 0; wgt_rd_en = 0; wgt_rd_addr = 0; trace_on = 0;
|
|
repeat(5) @(posedge clk);
|
|
rst = 0;
|
|
|
|
// real ~150us power-up wait, both chips (matches every other
|
|
// real-PSRAM testbench in this project) -- forgetting this
|
|
// was an earlier test-setup bug in this file (all requests
|
|
// issued during STATE_INIT/STATE_CR_INIT are simply never
|
|
// accepted), not an RTL defect.
|
|
wait (u_dual.u_ctrl0.state == u_dual.u_ctrl0.STATE_IDLE);
|
|
wait (u_dual.u_ctrl1.state == u_dual.u_ctrl1.STATE_IDLE);
|
|
@(posedge clk);
|
|
|
|
fill_pattern(23'h60000, MAX_TILES);
|
|
trace_on = 1'b1;
|
|
|
|
// edge cases (STEP11-14 convention): 0,1,2,MAX_TILES-1,MAX_TILES,
|
|
// back-to-back jobs (reset-free), matching prior discipline --
|
|
// "counter-width bug at value 16" class explicitly re-tested here.
|
|
run_job(23'h60000, 0, 500);
|
|
run_job(23'h60000, 1, 500);
|
|
run_job(23'h60000, 2, 500);
|
|
run_job(23'h60000, 15, 2000);
|
|
run_job(23'h60000, 16, 2000);
|
|
run_job(23'h60000, MAX_TILES-1, 8000);
|
|
|
|
// timed run for the real cycles/tile measurement (unconstrained
|
|
// consumer, freeze_consumer=1, so ready_count races ahead as
|
|
// fast as the real dual-chip physical interface allows)
|
|
freeze_consumer = 1'b1;
|
|
w_base = 23'h60000; n_tiles = MAX_TILES[15:0];
|
|
job_active = 1'b1;
|
|
t0 = cyc;
|
|
wait (ready_count == MAX_TILES[CNTW-1:0]);
|
|
t1 = cyc;
|
|
$display("REAL DUAL-CHIP 32-BIT TIMING: MAX_TILES=%0d total_cycles=%0d cycles/tile=%0.4f lane_sync_error=%0d",
|
|
MAX_TILES, t1-t0, (t1-t0)/(1.0*MAX_TILES), lane_sync_error);
|
|
job_active = 1'b0;
|
|
freeze_consumer = 1'b0;
|
|
|
|
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
|
|
if (errors == 0) $display("ALL TESTS PASSED (tb_psram_dual32)");
|
|
$finish;
|
|
end
|
|
endmodule
|