Follow-up to EXP-0051: built sdram_controller_pipelined.v, remapping addr->bank to low-order bits (today's weight region always maps to bank 0) and adding a shadow-slot ACTIVATE lookahead so a different-bank request can start its tRCD wait during the current transaction's tail. Phase A (isolated tb_sdram_controller_pipelined.v, 38/38 bit-exact, independently re-verified this session): mechanism works, saves exactly 2 cycles (tRCD) per different-bank back-to-back pair, matching the theoretical ceiling derived before measuring (CAS_LATENCY+BURST_LEN are serial on the shared data bus regardless of bank, so more than tRCD/tRP was never on the table). Phase B (integration, tb_nms_dstress_sdram_pipelined.v, independently rebuilt/rerun): N=4 49760 cycles (-0.33% vs baseline), N=8 49755 (-0.31%) -- both 256/256 bit-exact. Root cause of the gap: the W port's request/ready protocol is one-at-a-time, so a second, different-bank request is essentially never already pending while the first is still in flight, so the mechanism rarely triggers in the real system even though it's correct when directly stimulated. Not integrated into production; kept as additive reference for a possible future arbiter/backend pipelined-dispatch rewrite (out of scope here, larger and riskier). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHENedK76onD2Vtc2CMjej
274 lines
12 KiB
Verilog
274 lines
12 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// Isolated correctness + cycle-savings regression for
|
|
// sdram_controller_pipelined.v, forked from tb_sdram_controller.v's
|
|
// own idiom (same do_transaction task style, same sdram_model.v DUT
|
|
// pairing). Adds what the original testbench cannot exercise (it
|
|
// always waits for `busy` to clear before issuing the next request):
|
|
// deliberately pulsing a SECOND req WHILE the controller is still
|
|
// mid-transaction, to test the new shadow-pipeline slot.
|
|
//
|
|
// Covers:
|
|
// 1) same correctness battery as the original (write->read,
|
|
// sequential, all 4 banks, address limits, pseudo-random) --
|
|
// using the ORIGINAL wait-for-ready protocol throughout, so this
|
|
// also proves the re-sliced address decomposition (header note
|
|
// (1) in sdram_controller_pipelined.v) is a correct bijection.
|
|
// 2) DIFFERENT-bank early injection: issue a second request for a
|
|
// different bank while the first is still in S_CAS_WAIT, verify
|
|
// both results bit-exact AND that the combined cycle count is
|
|
// LOWER than 2x the serial baseline.
|
|
// 3) SAME-bank consecutive (both via the normal wait-for-ready
|
|
// protocol): must cost exactly the same as the original
|
|
// controller, no regression.
|
|
// 4) refresh spanning an early-injected interleave: run enough
|
|
// interleaved pairs to cross >=1 real tREFI interval, watch for
|
|
// any "VIOLATION"/"WARNING" from sdram_model.v.
|
|
// ============================================================
|
|
module tb #(
|
|
parameter BURST_LEN = 8,
|
|
parameter CLK_FREQ_MHZ = 80
|
|
);
|
|
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;
|
|
localparam ALIGN_BITS = (BURST_LEN<=1) ? 0 : $clog2(BURST_LEN);
|
|
|
|
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_pipelined #(
|
|
.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;
|
|
|
|
// ---- helper: which bank a given flat word address maps to under
|
|
// the PIPELINED decomposition (must match sdram_controller_
|
|
// pipelined.v's own addr_bank wire exactly) ----
|
|
function automatic [BANK_BITS-1:0] bank_of;
|
|
input [ADDR_WIDTH-1:0] a;
|
|
begin
|
|
bank_of = a[ALIGN_BITS +: BANK_BITS];
|
|
end
|
|
endfunction
|
|
|
|
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, wpat;
|
|
integer elapsed;
|
|
|
|
task automatic check_word(input [ADDR_WIDTH-1:0] a, input [15:0] pattern);
|
|
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 bank=%0d: got=%h expected=%h", a, bank_of(a), got, wpat);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS addr=%0d bank=%0d: burst=%0d bit-exact, cycles=%0d", a, bank_of(a), BURST_LEN, elapsed);
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// issue a request THIS cycle without waiting for busy/ready --
|
|
// the caller is responsible for knowing this is safe (shadow slot
|
|
// free, or accepting fallback-to-req_pending semantics otherwise)
|
|
task automatic issue_req_now(input t_wr, input [ADDR_WIDTH-1:0] t_addr, input [16*BURST_LEN-1:0] t_wdata);
|
|
begin
|
|
@(posedge clk);
|
|
req = 1'b1; wr = t_wr; addr = t_addr; wdata = t_wdata;
|
|
@(posedge clk);
|
|
req = 1'b0;
|
|
end
|
|
endtask
|
|
|
|
task automatic wait_ready(output [16*BURST_LEN-1:0] t_rdata, output integer t_cyc_at_ready);
|
|
begin
|
|
// always advance at least one cycle first -- otherwise two
|
|
// back-to-back calls can both observe the SAME still-high
|
|
// `ready` pulse from the previous call's own exit cycle
|
|
// (a single-cycle-wide pulse level-checked with no
|
|
// intervening clock edge looks identical to a fresh one).
|
|
@(posedge clk);
|
|
while (!ready) @(posedge clk);
|
|
t_rdata = rdata;
|
|
t_cyc_at_ready = cyc;
|
|
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;
|
|
repeat(5) @(posedge clk);
|
|
rst = 0;
|
|
while (busy) @(posedge clk);
|
|
|
|
$display("=== TEST 1: correctness battery (original wait-for-ready protocol) ===");
|
|
check_word({ADDR_WIDTH{1'b0}}, 16'hA5A5);
|
|
for (i = 0; i < 8; i = i + 1)
|
|
check_word(i*BURST_LEN, 16'h1000 + i);
|
|
// all 4 banks (bank now comes from LOW bits above the burst
|
|
// alignment -- addr values chosen so bank_of() sweeps 0..3)
|
|
for (i = 0; i < 4; i = i + 1)
|
|
check_word((i << ALIGN_BITS) + (100 << (ALIGN_BITS+BANK_BITS)), 16'h2000 + i);
|
|
// pseudo-random
|
|
for (i = 0; i < 24; i = i + 1) begin
|
|
rnd_addr = ($random(seed) % ((1<<ADDR_WIDTH)/BURST_LEN)) * BURST_LEN;
|
|
check_word(rnd_addr, 16'h3000 + i);
|
|
end
|
|
$display(" TEST 1: %0d/%0d passed so far", tests-errors, tests);
|
|
|
|
$display("=== TEST 2: SAME-bank consecutive, original protocol -- must match baseline 16-ish cycles/txn, no regression ===");
|
|
begin : test2
|
|
integer c_a, c_b;
|
|
reg [16*BURST_LEN-1:0] junk;
|
|
do_transaction(1'b1, (5 << ALIGN_BITS), {(16*BURST_LEN){1'b1}}, junk, c_a);
|
|
do_transaction(1'b0, (5 << ALIGN_BITS), {(16*BURST_LEN){1'b0}}, junk, c_b);
|
|
$display(" same-bank sequential write/read cycles: %0d / %0d (informational, expect ~identical to original controller's own measured cost)", c_a, c_b);
|
|
end
|
|
|
|
$display("=== TEST 3: DIFFERENT-bank early injection -- measure real cycle savings ===");
|
|
begin : test3
|
|
reg [ADDR_WIDTH-1:0] addr_bank0, addr_bank1;
|
|
reg [16*BURST_LEN-1:0] wpat0, wpat1, rd0, rd1;
|
|
integer t0, cyc_ready0, cyc_ready1, k;
|
|
addr_bank0 = (10 << ALIGN_BITS); // bank 0
|
|
addr_bank1 = (10 << ALIGN_BITS) + (1 << ALIGN_BITS); // bank 1 (adjacent word block)
|
|
if (bank_of(addr_bank0) == bank_of(addr_bank1)) begin
|
|
$display("FAIL TEST3 setup: addr_bank0/addr_bank1 landed on the SAME bank (%0d) -- test address choice is wrong", bank_of(addr_bank0));
|
|
errors = errors + 1;
|
|
end else begin
|
|
for (k = 0; k < BURST_LEN; k = k + 1) begin
|
|
wpat0[k*16 +: 16] = 16'h4000 + k[15:0];
|
|
wpat1[k*16 +: 16] = 16'h5000 + k[15:0];
|
|
end
|
|
// pre-seed both locations via the safe, sequential protocol
|
|
do_transaction(1'b1, addr_bank0, wpat0, got, elapsed);
|
|
do_transaction(1'b1, addr_bank1, wpat1, got, elapsed);
|
|
|
|
// now the REAL measurement: issue read A, wait until
|
|
// we're inside S_CAS_WAIT (shadow-capturable), inject
|
|
// read B for the OTHER bank, then measure total elapsed
|
|
// from A's issue to B's ready.
|
|
@(posedge clk);
|
|
while (busy) @(posedge clk);
|
|
t0 = cyc;
|
|
issue_req_now(1'b0, addr_bank0, {(16*BURST_LEN){1'b0}});
|
|
while (dut.state !== 13) @(posedge clk); // S_CAS_WAIT == 5'd13
|
|
if (dut.pipe_valid !== 1'b0)
|
|
$display(" (note) shadow slot already occupied when attempting injection -- unexpected for this test");
|
|
issue_req_now(1'b0, addr_bank1, {(16*BURST_LEN){1'b0}});
|
|
if (dut.pipe_valid !== 1'b1) begin
|
|
$display("FAIL TEST3: pipe_valid did not get set after different-bank injection during S_CAS_WAIT");
|
|
errors = errors + 1;
|
|
end
|
|
wait_ready(rd0, cyc_ready0);
|
|
wait_ready(rd1, cyc_ready1);
|
|
|
|
tests = tests + 1;
|
|
if (rd0 !== wpat0 || rd1 !== wpat1) begin
|
|
$display("FAIL TEST3 data: rd0=%h (exp %h) rd1=%h (exp %h)", rd0, wpat0, rd1, wpat1);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS TEST3 data: both banks bit-exact");
|
|
end
|
|
$display(" TEST3 timing: total cycles A-issue -> B-ready = %0d (serial baseline for 2 back-to-back BURST_LEN=%0d transactions is ~%0d; savings expected ~tRCD per pipelined pair, NOT a multiple-x speedup -- see sdram_controller_pipelined.v header)",
|
|
cyc_ready1 - t0, BURST_LEN, 2*(1+2+(BURST_LEN==1?0:3+1)+ (BURST_LEN>1?BURST_LEN-1:0) +2));
|
|
end
|
|
end
|
|
|
|
$display("=== TEST 4: refresh spanning interleaved traffic (watch for VIOLATION/WARNING above) ===");
|
|
begin : test4
|
|
integer t0b, cyc_r0, cyc_r1, j;
|
|
reg [ADDR_WIDTH-1:0] ba0, ba1;
|
|
reg [16*BURST_LEN-1:0] rr0, rr1;
|
|
for (j = 0; j < 60; j = j + 1) begin
|
|
ba0 = ((j*3) << ALIGN_BITS);
|
|
ba1 = ((j*3+1) << ALIGN_BITS);
|
|
if (bank_of(ba0) == bank_of(ba1)) ba1 = ba1 + (1 << ALIGN_BITS);
|
|
do_transaction(1'b1, ba0, {(16*BURST_LEN){16'hAA55}}, got, elapsed);
|
|
do_transaction(1'b1, ba1, {(16*BURST_LEN){16'h55AA}}, got, elapsed);
|
|
@(posedge clk);
|
|
while (busy) @(posedge clk);
|
|
t0b = cyc;
|
|
issue_req_now(1'b0, ba0, {(16*BURST_LEN){1'b0}});
|
|
while (dut.state !== 13 && dut.state !== 7) @(posedge clk); // S_CAS_WAIT or back to S_IDLE (refresh could have won)
|
|
if (dut.state === 13 && !dut.pipe_valid)
|
|
issue_req_now(1'b0, ba1, {(16*BURST_LEN){1'b0}});
|
|
wait_ready(rr0, cyc_r0);
|
|
if (dut.pipe_valid || dut.state != 7)
|
|
wait_ready(rr1, cyc_r1);
|
|
end
|
|
$display(" TEST4: 60 interleaved read pairs completed (spans real tREFI at CLK_FREQ_MHZ=%0d) -- check log above for VIOLATION/WARNING", CLK_FREQ_MHZ);
|
|
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_pipelined, BURST_LEN=%0d, CLK_FREQ_MHZ=%0d)", BURST_LEN, CLK_FREQ_MHZ);
|
|
$finish;
|
|
end
|
|
endmodule
|