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
219 lines
8.8 KiB
Verilog
219 lines
8.8 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// NMS STEP16 Phase 3 -- isolated correctness regression for
|
|
// sdram_controller.v against the real-timing-checked sdram_model.v.
|
|
//
|
|
// Covers all nine required scenarios:
|
|
// A) write -> read, single word
|
|
// B) sequential addresses (many consecutive tiles)
|
|
// C/D) burst length 4 / 8 (parametrized, compiled separately)
|
|
// E) row change (same bank, different row)
|
|
// F) bank change (different bank)
|
|
// G) refresh during activity (long-running test forces >=1 real
|
|
// periodic AUTO REFRESH to interleave with real transactions)
|
|
// H) pseudo-random address pattern
|
|
// I) addresses at the memory's own limits (row 0/4095, bank 0/3,
|
|
// col 0/(BURST_LEN-aligned near 255))
|
|
// ============================================================
|
|
module tb #(
|
|
parameter BURST_LEN = 4,
|
|
parameter CLK_FREQ_MHZ = 166
|
|
);
|
|
localparam ADDR_WIDTH = 22;
|
|
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
|
|
|
|
reg clk = 0;
|
|
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
|
|
reg rst;
|
|
|
|
reg req, wr;
|
|
reg [ADDR_WIDTH-1:0] addr;
|
|
reg [16*BURST_LEN-1:0] wdata;
|
|
reg [2*BURST_LEN-1:0] wmask;
|
|
wire [16*BURST_LEN-1:0] rdata;
|
|
wire ready, busy;
|
|
|
|
wire sdram_cke, sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n;
|
|
wire [1:0] sdram_ba;
|
|
wire [11:0] sdram_a;
|
|
wire [15:0] sdram_dq;
|
|
wire [1:0] sdram_dqm;
|
|
|
|
sdram_controller #(.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH)) dut (
|
|
.clk(clk), .rst(rst),
|
|
.req(req), .wr(wr), .addr(addr), .wdata(wdata), .wmask(wmask), .rdata(rdata), .ready(ready), .busy(busy),
|
|
.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)
|
|
);
|
|
|
|
sdram_model #(.CLK_FREQ_MHZ(CLK_FREQ_MHZ)) mem (
|
|
.clk(clk), .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)
|
|
);
|
|
|
|
integer errors, tests;
|
|
integer cyc;
|
|
always @(posedge clk) if (!rst) cyc <= cyc + 1;
|
|
reg trace_on;
|
|
reg [4:0] state_prev;
|
|
always @(posedge clk) begin
|
|
if (trace_on && dut.state !== state_prev)
|
|
$display(" [%0d] state->%0d busy=%0d ready=%0d req=%0d burst_idx=%0d",
|
|
cyc, dut.state, busy, ready, req, dut.burst_idx);
|
|
state_prev <= dut.state;
|
|
end
|
|
|
|
// one full burst transaction: issue req, wait for ready, return
|
|
// elapsed cycles and rdata via output args (Verilog tasks use
|
|
// output ports for this)
|
|
task automatic do_transaction(
|
|
input t_wr,
|
|
input [ADDR_WIDTH-1:0] t_addr,
|
|
input [16*BURST_LEN-1:0] t_wdata,
|
|
output [16*BURST_LEN-1:0] t_rdata,
|
|
output integer t_cycles
|
|
);
|
|
integer t0;
|
|
begin
|
|
@(posedge clk);
|
|
while (busy) @(posedge clk);
|
|
t0 = cyc;
|
|
req = 1'b1; wr = t_wr; addr = t_addr; wdata = t_wdata;
|
|
@(posedge clk);
|
|
req = 1'b0;
|
|
while (!ready) @(posedge clk);
|
|
t_rdata = rdata;
|
|
t_cycles = cyc - t0;
|
|
end
|
|
endtask
|
|
|
|
reg [16*BURST_LEN-1:0] got, expect_pattern;
|
|
integer elapsed;
|
|
|
|
task automatic check_word(input [ADDR_WIDTH-1:0] a, input [15:0] pattern);
|
|
reg [16*BURST_LEN-1:0] wpat;
|
|
integer k;
|
|
begin
|
|
for (k = 0; k < BURST_LEN; k = k + 1)
|
|
wpat[k*16 +: 16] = pattern + k[15:0];
|
|
do_transaction(1'b1, a, wpat, got, elapsed);
|
|
do_transaction(1'b0, a, {(16*BURST_LEN){1'b0}}, got, elapsed);
|
|
tests = tests + 1;
|
|
if (got !== wpat) begin
|
|
$display("FAIL addr=%0d: got=%h expected=%h", a, got, wpat);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS addr=%0d: burst=%0d bit-exact, cycles=%0d", a, BURST_LEN, elapsed);
|
|
end
|
|
end
|
|
endtask
|
|
|
|
integer seed;
|
|
integer i;
|
|
reg [ADDR_WIDTH-1:0] rnd_addr;
|
|
|
|
initial begin
|
|
errors = 0; tests = 0; cyc = 0; seed = 32'hC0FFEE;
|
|
rst = 1; req = 0; wr = 0; addr = 0; wdata = 0; wmask = 0; trace_on = 0;
|
|
repeat(5) @(posedge clk);
|
|
rst = 0;
|
|
while (busy) @(posedge clk); // real power-up/init sequence
|
|
|
|
// ---- A: write -> read single ----
|
|
check_word(22'd0, 16'hA5A5);
|
|
|
|
// ---- B: sequential addresses ----
|
|
trace_on = 1'b1;
|
|
for (i = 0; i < 3; i = i + 1)
|
|
check_word(i*BURST_LEN, 16'h1000 + i);
|
|
trace_on = 1'b0;
|
|
for (i = 3; i < 16; i = i + 1)
|
|
check_word(i*BURST_LEN, 16'h1000 + i);
|
|
|
|
// ---- E: row change (same bank 0, different row) ----
|
|
check_word({2'b00, 12'd0, 8'd0}, 16'h2000);
|
|
check_word({2'b00, 12'd1, 8'd0}, 16'h2001);
|
|
check_word({2'b00, 12'd100, 8'd0}, 16'h2002);
|
|
|
|
// ---- F: bank change ----
|
|
check_word({2'b00, 12'd5, 8'd0}, 16'h3000);
|
|
check_word({2'b01, 12'd5, 8'd0}, 16'h3001);
|
|
check_word({2'b10, 12'd5, 8'd0}, 16'h3002);
|
|
check_word({2'b11, 12'd5, 8'd0}, 16'h3003);
|
|
|
|
// ---- I: address limits ----
|
|
check_word({2'b00, 12'd0, 8'd0}, 16'h4000); // row 0, col 0
|
|
check_word({2'b11, 12'd4095, 8'(256-BURST_LEN)}, 16'h4001); // max bank/row, last valid burst-aligned col
|
|
check_word({2'b00, 12'd4095, 8'd0}, 16'h4002);
|
|
check_word({2'b11, 12'd0, 8'd0}, 16'h4003);
|
|
|
|
// ---- H: pseudo-random pattern ----
|
|
for (i = 0; i < 32; i = i + 1) begin
|
|
rnd_addr = ($random(seed) % (4*4096*256/BURST_LEN)) * BURST_LEN;
|
|
check_word(rnd_addr, 16'h5000 + i);
|
|
end
|
|
|
|
// ---- G: refresh during activity -- run enough back-to-back
|
|
// transactions to span well past one real tREFI interval
|
|
// (2605 cycles @166MHz), confirming the controller correctly
|
|
// interleaves periodic AUTO REFRESH with real read/write
|
|
// traffic with zero data loss/corruption ----
|
|
for (i = 0; i < 400; i = i + 1)
|
|
check_word((i*7 % (4*4096*256/BURST_LEN))*BURST_LEN, 16'h6000 + i);
|
|
|
|
// ---- J: real DQM byte-write masking (STEP19 -- the single-
|
|
// SDRAM unified memory subsystem needs true byte-addressable
|
|
// writes for result writeback; verify the controller's own
|
|
// per-burst-word wmask correctly masks OUT the bytes it's told
|
|
// to mask (memory retains its old value there) and writes
|
|
// through the bytes it's told to write, for every burst word
|
|
// position, not just word 0 ----
|
|
begin : test_j
|
|
reg [16*BURST_LEN-1:0] full_pat, masked_pat, readback;
|
|
reg [2*BURST_LEN-1:0] m;
|
|
integer w, elapsed_j;
|
|
reg [ADDR_WIDTH-1:0] addr_j;
|
|
addr_j = 22'd50000;
|
|
// seed a known full pattern first (no masking)
|
|
for (w = 0; w < BURST_LEN; w = w + 1) full_pat[w*16 +: 16] = 16'h7000 + w[15:0];
|
|
wmask = {(2*BURST_LEN){1'b0}};
|
|
do_transaction(1'b1, addr_j, full_pat, got, elapsed);
|
|
|
|
// now write a DIFFERENT pattern but mask OUT every other
|
|
// word (odd word indices), so only even words should
|
|
// actually change
|
|
for (w = 0; w < BURST_LEN; w = w + 1) masked_pat[w*16 +: 16] = 16'h8000 + w[15:0];
|
|
m = {(2*BURST_LEN){1'b0}};
|
|
for (w = 1; w < BURST_LEN; w = w + 2) m[w*2 +: 2] = 2'b11; // mask both bytes of odd words
|
|
wmask = m;
|
|
do_transaction(1'b1, addr_j, masked_pat, got, elapsed_j);
|
|
wmask = {(2*BURST_LEN){1'b0}};
|
|
|
|
do_transaction(1'b0, addr_j, {(16*BURST_LEN){1'b0}}, readback, elapsed);
|
|
tests = tests + 1;
|
|
begin : check_j
|
|
integer ok; reg [15:0] exp_w, got_w;
|
|
ok = 1;
|
|
for (w = 0; w < BURST_LEN; w = w + 1) begin
|
|
got_w = readback[w*16 +: 16];
|
|
exp_w = (w % 2 == 0) ? masked_pat[w*16 +: 16] : full_pat[w*16 +: 16];
|
|
if (got_w !== exp_w) begin
|
|
$display("FAIL J-mask word%0d: got=%h expected=%h (masked-write correctness)", w, got_w, exp_w);
|
|
ok = 0;
|
|
end
|
|
end
|
|
if (ok) $display("PASS J-mask addr=%0d: byte-masked write bit-exact, cycles=%0d", addr_j, elapsed_j);
|
|
else errors = errors + 1;
|
|
end
|
|
end
|
|
|
|
$display("=== %0d/%0d tests, %0d errors (BURST_LEN=%0d, CLK_FREQ_MHZ=%0d) ===",
|
|
tests-errors, tests, errors, BURST_LEN, CLK_FREQ_MHZ);
|
|
if (errors == 0) $display("ALL TESTS PASSED (tb_sdram_controller, BURST_LEN=%0d, CLK_FREQ_MHZ=%0d)", BURST_LEN, CLK_FREQ_MHZ);
|
|
$finish;
|
|
end
|
|
endmodule
|