Files
FPGA-Neural/sim/psram_page_mode_tb.v
T
micheleandClaude Sonnet 5 55c827bedf feat: PSRAM page-mode reads + graph engine (Type #2) + real pinout/IRQ pins
PSRAM page-mode read burst support in psram_controller.v: enables the
ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register
software-access sequence at boot (disabled by default on the real
chip), then keeps CE#/OE# asserted after a read so a same-page
continuation only pays tAPA (20ns) instead of a full tAA (70ns)
random access, with automatic tCEM-safe session closing. Only a WRITE
closes the page -- byte-enable changes do not, since
int8_memory_access.v alternates them on nearly every access and an
early implementation attempt that treated them as a close condition
measured a real regression (53.25->61.25 cycles/edge) before being
corrected (53.25->37.53 cycles/edge, +42% gather bandwidth).
sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement
(with a real Verilog same-timestep event-ordering race found and
fixed via a #0 sync) so the regression proves real timing compliance,
not just data correctness. New sim/psram_page_mode_tb.v; full 26-file
regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on
the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and
65.13MHz (P8) -- still under the 80MHz target but not regressed, with
the critical path confirmed (not assumed) to remain entirely inside
neuron_parallel's accumulate chain, never psram_controller.

Also includes this session's other already-validated work: the graph
engine (Type #2 sparse-graph network: act_buffer, graph_engine,
netasm host assembler), real CABGA381 pinout (.lpf, place&route
verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing
closure logs -- all previously uncommitted, documented in WORKLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
2026-09-03 17:12:05 +02:00

544 lines
19 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// PSRAM PAGE MODE TEST
//
// Exercises the page-mode burst-read path added to
// rtl/psram_controller.v: the configuration-register load at
// power-up, fast same-page read continuations (tAPA instead of
// tAA) including byte-enable changes between words (the pattern
// int8_memory_access.v actually produces -- LB#/UB# alternate on
// nearly every access), page-boundary crossing within an open
// session, and the one condition that closes the page (a WRITE).
//
// sim/psram_model.v enforces real datasheet timing ($fatal on any
// violation), including the new page-mode tAPA/tAA continuation
// check -- so a passing run here is a real proof that the RTL
// waits the correct number of cycles, not just that data compares
// equal.
// ================================================================
module tb;
localparam ADDR_WIDTH = 23;
localparam DATA_WIDTH = 16;
localparam CLK_FREQ_MHZ = 80;
localparam CLK_PERIOD = 12.5; // 80 MHz
reg clk;
reg rst;
reg mem_req;
reg mem_wr;
reg [ADDR_WIDTH-1:0] mem_addr;
reg [DATA_WIDTH-1:0] mem_wdata;
reg mem_lb_n;
reg mem_ub_n;
wire [DATA_WIDTH-1:0] mem_rdata;
wire mem_ready;
wire [ADDR_WIDTH-1:0] psram_a;
wire [DATA_WIDTH-1:0] psram_dq;
wire psram_ce_n;
wire psram_oe_n;
wire psram_we_n;
wire psram_lb_n;
wire psram_ub_n;
wire psram_zz_n;
// ============================================================
// Expected cycle counts (must match the RTL's own formulas)
// ============================================================
localparam integer ACCESS_CYCLES = ((70 * CLK_FREQ_MHZ) + 999) / 1000;
localparam integer PAGE_CYCLES = ((20 * CLK_FREQ_MHZ) + 999) / 1000;
// ============================================================
// DUT
// ============================================================
psram_controller #(
.ADDR_WIDTH (ADDR_WIDTH),
.DATA_WIDTH (DATA_WIDTH),
.CLK_FREQ_MHZ (CLK_FREQ_MHZ)
) dut (
.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),
.psram_a (psram_a),
.psram_dq (psram_dq),
.psram_ce_n(psram_ce_n),
.psram_oe_n(psram_oe_n),
.psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n),
.psram_ub_n(psram_ub_n),
.psram_zz_n(psram_zz_n)
);
psram_model #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(4096)
) memory (
.clk (clk),
.a (psram_a),
.dq (psram_dq),
.ce_n (psram_ce_n),
.oe_n (psram_oe_n),
.we_n (psram_we_n),
.lb_n (psram_lb_n),
.ub_n (psram_ub_n),
.zz_n (psram_zz_n)
);
// ============================================================
// Clock
// ============================================================
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
initial begin
$dumpfile("sim/psram_page_mode.vcd");
$dumpvars(0, tb);
end
// ============================================================
// CE# pulse counter -- counts how many times psram_ce_n rises
// (i.e. how many times a session actually closed), so tests
// can check that a page really stayed open (or really closed)
// without hand-parsing the DUT's internal state.
// ============================================================
integer ce_close_count;
integer ce_close_before_burst;
always @(posedge psram_ce_n)
ce_close_count = ce_close_count + 1;
// ============================================================
// Helpers
// ============================================================
real req_time;
real latency_ns;
task write_word;
input [ADDR_WIDTH-1:0] addr;
input [DATA_WIDTH-1:0] data;
begin
@(posedge clk);
mem_addr <= addr;
mem_wdata <= data;
mem_wr <= 1'b1;
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b0;
mem_req <= 1'b1;
@(posedge clk);
mem_req <= 1'b0;
wait (mem_ready);
@(posedge clk);
end
endtask
// Full-word read, records latency (mem_req assertion -> mem_ready)
// in latency_ns for the caller to inspect.
task read_word;
input [ADDR_WIDTH-1:0] addr;
input [DATA_WIDTH-1:0] expected;
begin
@(posedge clk);
mem_addr <= addr;
mem_wr <= 1'b0;
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b0;
mem_req <= 1'b1;
req_time = $realtime;
@(posedge clk);
mem_req <= 1'b0;
wait (mem_ready);
latency_ns = $realtime - req_time;
if (mem_rdata !== expected) begin
$display("READ addr=0x%06x FAIL got=0x%04x expected=0x%04x",
addr, mem_rdata, expected);
$fatal;
end
$display("READ addr=0x%06x data=0x%04x latency=%0.1fns PASS",
addr, mem_rdata, latency_ns);
@(posedge clk);
end
endtask
task read_word_be;
input [ADDR_WIDTH-1:0] addr;
input lb;
input ub;
input [DATA_WIDTH-1:0] expected;
begin
@(posedge clk);
mem_addr <= addr;
mem_wr <= 1'b0;
mem_lb_n <= lb;
mem_ub_n <= ub;
mem_req <= 1'b1;
req_time = $realtime;
@(posedge clk);
mem_req <= 1'b0;
wait (mem_ready);
latency_ns = $realtime - req_time;
if (mem_rdata !== expected) begin
$display("READ(BE) addr=0x%06x FAIL got=0x%04x expected=0x%04x",
addr, mem_rdata, expected);
$fatal;
end
$display("READ(BE) addr=0x%06x LB#=%b UB#=%b data=0x%04x latency=%0.1fns PASS",
addr, lb, ub, mem_rdata, latency_ns);
@(posedge clk);
end
endtask
task expect_ce_closes;
input integer expected_count;
input [8*48-1:0] label;
begin
if (ce_close_count !== expected_count) begin
$display("CE# close-count FAIL (%0s): got=%0d expected=%0d",
label, ce_close_count, expected_count);
$fatal;
end else begin
$display("CE# close-count OK (%0s): %0d", label, ce_close_count);
end
end
endtask
// ============================================================
// Test
// ============================================================
integer i;
reg [DATA_WIDTH-1:0] page_data [0:15];
initial begin
mem_req = 1'b0;
mem_wr = 1'b0;
mem_addr = 0;
mem_wdata = 0;
mem_lb_n = 1'b1;
mem_ub_n = 1'b1;
ce_close_count = 0;
rst = 1'b1;
repeat (5) @(posedge clk);
rst = 1'b0;
$display("");
$display("========================================");
$display("PSRAM PAGE MODE TEST");
$display("%0d MHz -- ACCESS_CYCLES=%0d PAGE_CYCLES=%0d",
CLK_FREQ_MHZ, ACCESS_CYCLES, PAGE_CYCLES);
$display("========================================");
$display("");
// Wait through STATE_INIT + the CR software-access-sequence
// (2 dummy reads + 2 writes at the top address) -- if the
// sequence violates any read/write timing the strict
// psram_model will $fatal before we ever get here.
wait (dut.state == dut.STATE_IDLE);
$display("PSRAM init + CR page-mode enable sequence complete");
$display("");
// Reset the close-counter here: boot (reset release + the
// 4-step CR sequence) legitimately toggles CE# several
// times and that's not what the test below is checking.
ce_close_count = 0;
// ========================================================
// Fill one 16-word page (addresses share bits above A[3])
// plus one word in the next page, for boundary testing.
// ========================================================
for (i = 0; i < 16; i = i + 1) begin
page_data[i] = 16'hA000 + i[15:0];
write_word(23'h000100 + i, page_data[i]);
end
write_word(23'h000110, 16'hB000); // first word of the NEXT page
expect_ce_closes(17, "after 17 writes");
// ========================================================
// Same-page sequential reads: first word pays full tAA,
// every following word in the same page must be a fast
// PAGE_CYCLES continuation with CE# never toggling.
// ========================================================
$display("");
$display("---- same-page sequential read burst ----");
read_word(23'h000100, page_data[0]);
if (latency_ns < ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: first word of a fresh page was faster than tAA (%0.1fns < %0.1fns)",
latency_ns, ACCESS_CYCLES * CLK_PERIOD);
$fatal;
end
// Opening a page (a completed read) does NOT close CE# --
// that's the whole point, the session stays open.
expect_ce_closes(17, "page opened, CE# still low");
for (i = 1; i < 16; i = i + 1) begin
read_word(23'h000100 + i, page_data[i]);
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: same-page word %0d did not use the fast path (%0.1fns >= tAA %0.1fns)",
i, latency_ns, ACCESS_CYCLES * CLK_PERIOD);
$fatal;
end
if (latency_ns > PAGE_CYCLES * CLK_PERIOD + CLK_PERIOD) begin
$display("FAIL: same-page word %0d slower than expected (%0.1fns)",
i, latency_ns);
$fatal;
end
end
// CE# must NOT have toggled again across the whole 16-word
// burst -- it should still be exactly one open session.
expect_ce_closes(17, "still one open session after 16-word burst");
$display("PAGE MODE BURST SPEEDUP CONFIRMED");
$display("");
// ========================================================
// Page-boundary crossing while CE# stays open: a READ to a
// different page still doesn't need to close CE# (only a
// WRITE does) -- but that one word pays the full tAA per
// the datasheet rule ("any change in addresses A[4] or
// higher initiates a new tAA access time"), then
// continuations in the new page are fast again.
// ========================================================
$display("---- page-boundary crossing (still open) ----");
read_word(23'h000110, 16'hB000);
if (latency_ns < ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: page-crossing word was not full tAA (%0.1fns)", latency_ns);
$fatal;
end
expect_ce_closes(17, "page crossing did not toggle CE#");
// A WRITE, unlike a READ, always closes the page -- and it
// costs two CE# pulses: one to close the read session that
// was open, one for the write's own transaction.
write_word(23'h000111, 16'hB001);
expect_ce_closes(19, "write after page crossing closes the session");
read_word(23'h000111, 16'hB001);
expect_ce_closes(19, "fresh read reopens its own session (no close)");
read_word(23'h000112, 16'h0000); // untouched location -> reset value
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: second word of new page-crossing session should be fast");
$fatal;
end
expect_ce_closes(19, "same-page continuation after the crossing");
$display("");
// ========================================================
// A WRITE mid-session must close the page.
// ========================================================
$display("---- write closes an open page ----");
// Still the SAME open session as above (page 0x11) -- a
// READ to a different page (0x10) is just another
// page-miss continuation, not a close.
read_word(23'h000100, page_data[0]);
expect_ce_closes(19, "page-miss read continuation (still open)");
read_word(23'h000101, page_data[1]); // fast continuation
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: continuation before the write should be fast");
$fatal;
end
expect_ce_closes(19, "still open before the write");
write_word(23'h000200, 16'hC0DE);
expect_ce_closes(21, "write forces a close (2 pulses: close + write)");
read_word(23'h000200, 16'hC0DE);
expect_ce_closes(21, "post-write read opens a fresh session (no close)");
$display("");
// ========================================================
// Byte-enable changes must NOT close the page.
//
// int8_memory_access.v alternates LB#/UB# on essentially
// every access (byte-granular reads over the 16-bit PSRAM
// bus, addr[0] selects the byte) -- this is the actual
// real-world access pattern (e.g. graph_engine's edge-list
// gather), so it must stay on the fast page-hit path, not
// force a close on every single byte.
// ========================================================
$display("---- byte-enable changes stay on the fast path ----");
read_word(23'h000102, page_data[2]);
expect_ce_closes(21, "page-miss read continuation, still open");
read_word_be(23'h000102, 1'b0, 1'b1, {8'h00, page_data[2][7:0]});
expect_ce_closes(21, "LB# only -- still open, still fast");
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: byte-enable-only change should stay on the fast path");
$fatal;
end
read_word_be(23'h000102, 1'b1, 1'b0, {page_data[2][15:8], 8'h00});
expect_ce_closes(21, "UB# only -- still open, still fast");
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: byte-enable-only change should stay on the fast path");
$fatal;
end
read_word_be(23'h000103, 1'b0, 1'b1, {8'h00, page_data[3][7:0]});
expect_ce_closes(21, "new address + byte-enable change together -- still fast");
if (latency_ns >= ACCESS_CYCLES * CLK_PERIOD) begin
$display("FAIL: address+byte-enable change together should stay fast");
$fatal;
end
$display("");
// ========================================================
// tCEM safety timeout: an open page with no further
// requests must close itself well before the 8us CE#-low
// refresh limit (PAGE_TIMEOUT_CYCLES, ~6us of margin) --
// not just "eventually", but on its own, unprompted.
// ========================================================
$display("---- tCEM idle timeout closes an unattended open page ----");
read_word(23'h000104, page_data[4]); // still page 0x10, still open
expect_ce_closes(21, "same page continuation, waiting idle now");
repeat (dut.PAGE_TIMEOUT_CYCLES + 4) @(posedge clk);
expect_ce_closes(22, "idle page auto-closed by the tCEM timeout");
read_word(23'h000104, page_data[4]); // must still work correctly
expect_ce_closes(22, "fresh session opened, not yet closed again");
$display("");
// ========================================================
// tCEM budget mid-burst: a long run of back-to-back
// same-page HITS (never idle, never a write) must still be
// split before the limit -- not just the idle-timeout case
// above. sim/psram_model.v independently enforces the real
// 8us tCEM hard limit ($fatal on violation); this is a real
// safety net, not a rubber stamp, so a passing run here is
// genuine proof the RTL splits the burst in time, with
// margin, not just "in simulation it happened to work".
// ========================================================
$display("---- tCEM budget forces a split mid-burst (never idle) ----");
for (i = 0; i < 16; i = i + 1) begin
page_data[i] = 16'hC000 + i[15:0];
write_word(23'h000500 + i, page_data[i]);
end
read_word(23'h000500, page_data[0]); // opens page 0x50
ce_close_before_burst = ce_close_count;
// 2*PAGE_TIMEOUT_CYCLES/16 round trips through this 16-word
// page comfortably crosses PAGE_TIMEOUT_CYCLES worth of
// STATE_READ time (each hit costs PAGE_CYCLES inside
// STATE_READ, plus this task's own idle cycles between
// requests, both counted by hold_cycles) while never once
// idling long enough on its own to hit the separate
// idle-timeout path above -- this is the "busy" case.
for (i = 0; i < 2 * dut.PAGE_TIMEOUT_CYCLES; i = i + 1)
read_word(23'h000500 + (i % 16), page_data[i % 16]);
if (ce_close_count <= ce_close_before_burst) begin
$display("FAIL: expected at least one mid-burst split, CE# never toggled (before=%0d after=%0d)",
ce_close_before_burst, ce_close_count);
$fatal;
end
$display("CE# split during long burst confirmed: %0d -> %0d closes",
ce_close_before_burst, ce_close_count);
$display("(no tCEM $fatal from psram_model.v -- split happened with margin)");
$display("");
// ========================================================
// Data-integrity stress: random-ish scattered pages,
// mixing writes and page-local read bursts.
// ========================================================
$display("---- mixed stress: scattered pages + local bursts ----");
for (i = 0; i < 64; i = i + 1) begin
reg [ADDR_WIDTH-1:0] base;
reg [DATA_WIDTH-1:0] val;
integer j;
base = ((i * 8191) ^ (i << 6)) & 23'h000FF0; // page-aligned
val = (i * 733) ^ 16'h5A5A;
for (j = 0; j < 4; j = j + 1)
write_word(base + j, val + j[15:0]);
for (j = 0; j < 4; j = j + 1)
read_word(base + j, val + j[15:0]);
end
$display("");
$display("========================================");
$display("PSRAM PAGE MODE TEST PASSED");
$display("========================================");
$display("");
$finish;
end
endmodule