Files
FPGA-Neural/hardware/v2/nms/sim/tb_sdram_controller.v
T
micheleandClaude Sonnet 5 8d83d97bde feat: SDRAM 8MB->64MB upgrade (AS4C32M16SB-7BIN) + N_SLOTS=8 support
Memory upgrade, at the user's own explicit request: Alliance Memory
AS4C4M16SA-6TIN (64Mbit/8MB) -> AS4C32M16SB-7BIN (512Mbit/64MB, 54-ball
TFBGA), the largest same-family SDR SDRAM Alliance Memory offers.
Real-datasheet-driven (whole AS4C4M16SA/AS4C8M16SA/AS4C16M16SA/
AS4C32M16SA family investigated): 13 row bits (was 12, one new FPGA
pin sdram_a[12]/ball F1), 10 column bits (was 8), real -7-grade AC
timing (tRCD/tRP improved to 15ns, tREFI halved to 7.8us for the
doubled row count). sdram_controller.v and sdram_model.v gained real
ROW_BITS/COL_BITS/BANK_BITS parameters (was hardcoded 12/8/2).

ADDR_WIDTH widened 23->26 bits across the live instantiation tree.
This required a real SPI protocol change (spi_host_bridge.v): a 26-bit
byte address no longer fits in 3 bytes -- every address field widened
3->4 bytes (WRITE_JOB 15->18 payload bytes, WRITE_MEM/READ_MEM header
5->6 bytes).

Found and fixed two real timing regressions via nextpnr-ecp5 P&R
(not assumed): neural_director.v's own runtime-indexed demux write
(ERR-0027, was silently synthesizing an extra MULT18X18D) and
nms_activation_fill_ctrl_v3.v's own linear N_SLOTS-wide max-scan
(ERR-0028, became dominant at N_SLOTS=8) -- both replaced with
constant-indexed/tree-based equivalents, bit-exact same behavior,
confirmed via full D-Stress N=2/4/8 regression (identical cycle
counts). N_SLOTS=4 now fully closes timing at 64MHz (8/8 seeds);
N_SLOTS=8 significantly improved but not yet fully reliable (5/8
seeds) -- honestly disclosed, not claimed complete.

Full regression re-verified: sdram_controller (461/461, 18 configs),
tb_sdram_boundary (21/21), D-Stress N=2/4/8 (bit-exact), spi_host_bridge
(18/18), board-level SPI smoke test (11/11), unified backend (40/40).

See hardware/v2/docs/MEMORY_UPGRADE_64MB_N8.md for the full
investigation, and errors.log/decisions.log (ERR-0027, ERR-0028,
DEC-0039) for the complete root-cause writeups.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-07 00:20:53 +02:00

231 lines
9.4 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 = 64
);
// AS4C32M16SA-7TIN (64MB): 13 row bits (A0-A12), 10 col bits
// (A0-A9), 2 bank bits (BA0-BA1) -- see sdram_controller.v's own
// header for the full datasheet cross-reference.
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS;
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 [BANK_BITS-1:0] sdram_ba;
wire [ROW_BITS-1: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),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) 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),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) 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({ADDR_WIDTH{1'b0}}, 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, 13'd0, 10'd0}, 16'h2000);
check_word({2'b00, 13'd1, 10'd0}, 16'h2001);
check_word({2'b00, 13'd100, 10'd0}, 16'h2002);
// ---- F: bank change ----
check_word({2'b00, 13'd5, 10'd0}, 16'h3000);
check_word({2'b01, 13'd5, 10'd0}, 16'h3001);
check_word({2'b10, 13'd5, 10'd0}, 16'h3002);
check_word({2'b11, 13'd5, 10'd0}, 16'h3003);
// ---- I: address limits (AS4C32M16SA: 8192 rows, 1024 cols) ----
check_word({2'b00, 13'd0, 10'd0}, 16'h4000); // row 0, col 0
check_word({2'b11, 13'd8191, 10'(1024-BURST_LEN)}, 16'h4001); // max bank/row, last valid burst-aligned col
check_word({2'b00, 13'd8191, 10'd0}, 16'h4002);
check_word({2'b11, 13'd0, 10'd0}, 16'h4003);
// ---- H: pseudo-random pattern ----
for (i = 0; i < 32; i = i + 1) begin
rnd_addr = ($random(seed) % (4*8192*1024/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 = 25'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