Adds FPGA-exclusive access to the onboard W25Q128JV SPI NOR flash for weights/bias/network persistence, layered as spi_flash_master (raw SPI, USRMCLK-driven) -> flash_copy_engine (flash<->PSRAM streaming, erase- before-write, Page Program loop) -> flash_slot_manager (16-slot catalog with CRC32), exposed via 8 new SPI opcodes (0x40-0x47). Fixes two pre-existing bugs found during bring-up: a psram_controller.v request lost during power-up, and a one-cycle-pulse race in the PSRAM arbiter request handshake. Full simulation + real Yosys/nextpnr-ecp5 synthesis verification (0 errors, Fmax 66.68MHz) in WORKLOG.md and docs/FPGA-Neural-Flash-Subsystem-Verification.md. Also updates docs/pinout to reflect the 56-signal real .lpf (3 new flash pins) and documents the WRITE_RAM/READ_RAM host backpressure risk found while testing this subsystem. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
339 lines
12 KiB
Verilog
339 lines
12 KiB
Verilog
`timescale 1ns/1ps
|
|
// Compile with -DSIMULATION (see rtl/spi_flash_master.v header: this
|
|
// exposes sclk_sim as a normal port in place of the real USRMCLK
|
|
// primitive, which has no Icarus simulation model).
|
|
|
|
// ================================================================
|
|
// SPI_FLASH_MASTER TESTBENCH (Phase F1)
|
|
//
|
|
// Drives rtl/spi_flash_master.v against sim/flash_model.v (an
|
|
// independent behavioral model of the datasheet's write rules, see
|
|
// its own header) and checks:
|
|
//
|
|
// TEST 1 (RDID, happy path): oracle is the datasheet's own
|
|
// manufacturer/type/capacity bytes (EF/40/18h -- see
|
|
// flash_model.v's header for the plain-vs-DTR JEDEC-ID note),
|
|
// hardcoded here independently of both the RTL and the model's
|
|
// internal constant (this testbench does not read
|
|
// flash_model.v's localparam, it states the expected value
|
|
// itself, from the same citation).
|
|
//
|
|
// TEST 2 (READ, known block, INDEPENDENT oracle): a pattern is
|
|
// planted directly into flash_model's memory array via
|
|
// hierarchical reference (dut_flash.mem[...]) -- NOT written
|
|
// through spi_flash_master -- then read back through the DUT
|
|
// and compared byte-exact. This is the true independent-oracle
|
|
// test per WORKLOG.md §A.1: the expected data was never
|
|
// produced by the RTL under test.
|
|
//
|
|
// TEST 3 (WREN+PP+RDSR-poll+READ round-trip): exercises the
|
|
// write-side primitives (not yet independent of the RTL's own
|
|
// write path -- a round-trip self-consistency check, distinct
|
|
// from TEST 2's independence, and explicitly noted as such).
|
|
// Confirms AND-only programming into an erased (0xFF) region
|
|
// reproduces the exact bytes written, and that RDSR's BUSY bit
|
|
// is observed high during the modeled tPP delay and clears
|
|
// after.
|
|
//
|
|
// TEST 4 (WREN+SE+RDSR-poll+READ): confirms sector erase drives
|
|
// the target sector back to all-0xFF, verified against a region
|
|
// TEST 3 deliberately left non-0xFF first (so this test cannot
|
|
// pass by coincidence on an already-blank region).
|
|
//
|
|
// TEST 5 (negative, §A.3): an opcode this design never uses
|
|
// (0xAB, Release Power-down / Device ID -- a real, harmless
|
|
// W25Q128JV instruction, but NOT one spi_flash_master or
|
|
// flash_model implement) is issued. Requirement: the
|
|
// transaction completes (`done` pulses within a bounded cycle
|
|
// count) rather than hanging -- a master that can wedge on an
|
|
// opcode it doesn't specifically recognize is broken by
|
|
// construction, since it always just shifts bits regardless of
|
|
// opcode semantics.
|
|
// ================================================================
|
|
|
|
module tb;
|
|
|
|
localparam CLK_PERIOD = 12.5; // 80 MHz, matches CLK_FREQ_MHZ default
|
|
|
|
reg clk;
|
|
reg rst;
|
|
|
|
initial begin
|
|
clk = 1'b0;
|
|
forever #(CLK_PERIOD / 2.0) clk = ~clk;
|
|
end
|
|
|
|
// ------------------------------------------------------------
|
|
// DUT <-> flash_model physical wiring
|
|
// ------------------------------------------------------------
|
|
wire mosi;
|
|
wire miso;
|
|
wire cs_n;
|
|
wire sclk_w;
|
|
|
|
reg start;
|
|
reg [7:0] opcode;
|
|
reg has_addr;
|
|
reg [23:0] addr;
|
|
reg [1:0] dir;
|
|
reg [15:0] n_data;
|
|
|
|
wire wdata_req;
|
|
reg [7:0] wdata;
|
|
reg wdata_valid;
|
|
|
|
wire rdata_valid;
|
|
wire [7:0] rdata;
|
|
reg rdata_ack;
|
|
|
|
wire busy;
|
|
wire done;
|
|
|
|
localparam DIR_NONE = 2'd0;
|
|
localparam DIR_WRITE = 2'd1;
|
|
localparam DIR_READ = 2'd2;
|
|
|
|
spi_flash_master #(
|
|
.CLK_FREQ_MHZ(80),
|
|
.SCLK_DIV(2)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
|
|
.mosi(mosi), .miso(miso), .cs_n(cs_n),
|
|
.sclk_sim(sclk_w),
|
|
|
|
.start(start), .opcode(opcode), .has_addr(has_addr), .addr(addr),
|
|
.dir(dir), .n_data(n_data),
|
|
|
|
.wdata_req(wdata_req), .wdata(wdata), .wdata_valid(wdata_valid),
|
|
|
|
.rdata_valid(rdata_valid), .rdata(rdata), .rdata_ack(rdata_ack),
|
|
|
|
.busy(busy), .done(done)
|
|
);
|
|
|
|
flash_model #(
|
|
.DEPTH(32'h0002_0000),
|
|
.TIME_SCALE(100000) // see flash_model.v header: MAX datasheet timing / 100000
|
|
) dut_flash (
|
|
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
|
|
);
|
|
|
|
// ------------------------------------------------------------
|
|
// Bookkeeping
|
|
// ------------------------------------------------------------
|
|
integer errors;
|
|
reg [7:0] rbuf [0:511];
|
|
reg [7:0] wbuf [0:511];
|
|
|
|
// ============================================================
|
|
// do_cmd: drive one full command through the DUT's byte-level
|
|
// handshake, servicing wdata_req/rdata_valid as needed. Reads
|
|
// land in rbuf[0..n_data-1]; writes are sourced from wbuf.
|
|
// ============================================================
|
|
integer k;
|
|
integer wd_cyc;
|
|
|
|
task automatic do_cmd(
|
|
input [7:0] p_opcode,
|
|
input p_has_addr,
|
|
input [23:0] p_addr,
|
|
input [1:0] p_dir,
|
|
input [15:0] p_n
|
|
);
|
|
begin
|
|
@(posedge clk);
|
|
start <= 1'b1;
|
|
opcode <= p_opcode;
|
|
has_addr <= p_has_addr;
|
|
addr <= p_addr;
|
|
dir <= p_dir;
|
|
n_data <= p_n;
|
|
@(posedge clk);
|
|
start <= 1'b0;
|
|
|
|
k = 0;
|
|
wd_cyc = 0;
|
|
while (!done) begin
|
|
@(posedge clk);
|
|
|
|
if (wdata_req) begin
|
|
wdata_valid <= 1'b1;
|
|
wdata <= wbuf[k];
|
|
end else begin
|
|
wdata_valid <= 1'b0;
|
|
end
|
|
|
|
if (wdata_valid) begin
|
|
k = k + 1;
|
|
end
|
|
|
|
if (rdata_valid) begin
|
|
rbuf[k] = rdata;
|
|
k = k + 1;
|
|
rdata_ack <= 1'b1;
|
|
end else begin
|
|
rdata_ack <= 1'b0;
|
|
end
|
|
|
|
wd_cyc = wd_cyc + 1;
|
|
if (wd_cyc > 200000) begin
|
|
$display("FATAL: do_cmd watchdog timeout (opcode=%02h) at t=%0t", p_opcode, $time);
|
|
$finish;
|
|
end
|
|
end
|
|
@(posedge clk);
|
|
rdata_ack <= 1'b0;
|
|
wdata_valid <= 1'b0;
|
|
end
|
|
endtask
|
|
|
|
task automatic check_byte(input [7:0] got, input [7:0] exp, input [255:0] label);
|
|
begin
|
|
if (got !== exp) begin
|
|
$display("FAIL: %0s got=%02h exp=%02h", label, got, exp);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
localparam [7:0] OP_WREN = 8'h06;
|
|
localparam [7:0] OP_READ = 8'h03;
|
|
localparam [7:0] OP_PP = 8'h02;
|
|
localparam [7:0] OP_SE = 8'h20;
|
|
localparam [7:0] OP_RDSR1 = 8'h05;
|
|
localparam [7:0] OP_RDID = 8'h9F;
|
|
|
|
task automatic wait_wip_clear;
|
|
begin
|
|
rbuf[0] = 8'hFF;
|
|
while (rbuf[0][0] == 1'b1) begin
|
|
do_cmd(OP_RDSR1, 1'b0, 24'h0, DIR_READ, 16'd1);
|
|
end
|
|
end
|
|
endtask
|
|
|
|
integer i;
|
|
|
|
initial begin
|
|
errors = 0;
|
|
rst = 1'b1;
|
|
start = 1'b0;
|
|
opcode = 8'h00;
|
|
has_addr = 1'b0;
|
|
addr = 24'h0;
|
|
dir = DIR_NONE;
|
|
n_data = 16'd0;
|
|
wdata_valid = 1'b0;
|
|
wdata = 8'h00;
|
|
rdata_ack = 1'b0;
|
|
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
repeat (5) @(posedge clk);
|
|
|
|
// ========================================================
|
|
// TEST 1: RDID happy path
|
|
// ========================================================
|
|
do_cmd(OP_RDID, 1'b0, 24'h0, DIR_READ, 16'd3);
|
|
check_byte(rbuf[0], 8'hEF, "TEST1 RDID manufacturer");
|
|
check_byte(rbuf[1], 8'h40, "TEST1 RDID memtype");
|
|
check_byte(rbuf[2], 8'h18, "TEST1 RDID capacity");
|
|
|
|
// ========================================================
|
|
// TEST 2: READ, known block, planted independently of the
|
|
// RTL (hierarchical poke into the flash model's own array).
|
|
// ========================================================
|
|
for (i = 0; i < 16; i = i + 1)
|
|
dut_flash.mem[24'h001000 + i] = 8'hA0 + i[7:0];
|
|
|
|
do_cmd(OP_READ, 1'b1, 24'h001000, DIR_READ, 16'd16);
|
|
for (i = 0; i < 16; i = i + 1)
|
|
check_byte(rbuf[i], 8'hA0 + i[7:0], "TEST2 READ known block");
|
|
|
|
// ========================================================
|
|
// TEST 3: WREN + PP (16 bytes) + RDSR poll + READ round-trip.
|
|
// Target region 0x002000 starts erased (0xFF, from
|
|
// flash_model's own reset init) -- programming a pattern
|
|
// with some cleared bits must read back exactly that
|
|
// pattern (AND-with-0xFF is a no-op, so this alone doesn't
|
|
// yet prove AND-only; TEST 3b below does).
|
|
// ========================================================
|
|
for (i = 0; i < 16; i = i + 1)
|
|
wbuf[i] = 8'h10 + i[7:0];
|
|
|
|
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
|
|
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
|
|
|
|
wait_wip_clear;
|
|
|
|
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
|
|
for (i = 0; i < 16; i = i + 1)
|
|
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3 PP/READ round-trip");
|
|
|
|
// TEST 3b: program the SAME region again with a value that
|
|
// tries to SET a bit the first program cleared (0x10 has
|
|
// bit4 set high already at 0x10=00010000; try programming
|
|
// 0xFF over it, which per the AND-only rule must leave 0x10
|
|
// unchanged, NOT flip it to 0xFF). This is the datasheet
|
|
// rule TEST 3 alone cannot distinguish from a naive
|
|
// "programming just overwrites" implementation.
|
|
for (i = 0; i < 16; i = i + 1)
|
|
wbuf[i] = 8'hFF;
|
|
|
|
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
|
|
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
|
|
wait_wip_clear;
|
|
|
|
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
|
|
for (i = 0; i < 16; i = i + 1)
|
|
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3b PP AND-only (program cannot set bits)");
|
|
|
|
// ========================================================
|
|
// TEST 4: WREN + SE (sector containing 0x002000) + RDSR
|
|
// poll + READ, confirming erase -> all-FF. The region was
|
|
// deliberately left non-FF by TEST 3/3b above, so this
|
|
// cannot pass by coincidence.
|
|
// ========================================================
|
|
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
|
|
do_cmd(OP_SE, 1'b1, 24'h002000, DIR_NONE, 16'd0);
|
|
|
|
wait_wip_clear;
|
|
|
|
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
|
|
for (i = 0; i < 16; i = i + 1)
|
|
check_byte(rbuf[i], 8'hFF, "TEST4 SE erase -> 0xFF");
|
|
|
|
// ========================================================
|
|
// TEST 5 (negative, §A.3): unsupported/unrecognized opcode
|
|
// (0xABh, a real W25Q128JV instruction -- Release
|
|
// Power-down/Device ID -- that neither spi_flash_master nor
|
|
// flash_model implement any special-case for). Requirement:
|
|
// do_cmd's watchdog must NOT fire -- the transaction has to
|
|
// complete (`done` pulses) purely from the master's own
|
|
// fixed bit-count/opcode-agnostic shifting, proving the
|
|
// master cannot wedge on an opcode it doesn't recognize.
|
|
// ========================================================
|
|
do_cmd(8'hAB, 1'b0, 24'h0, DIR_READ, 16'd1);
|
|
$display("TEST5 (illegal/unsupported opcode 0xAB): completed without watchdog timeout, got=%02h (undefined/don't-care by design)", rbuf[0]);
|
|
|
|
// ========================================================
|
|
if (errors == 0)
|
|
$display("ALL TESTS PASSED");
|
|
else
|
|
$display("FAILED: %0d error(s)", errors);
|
|
|
|
$finish;
|
|
end
|
|
|
|
// Safety net: absolute simulation timeout independent of the
|
|
// per-command watchdog above (catches a hang between commands,
|
|
// e.g. in the top-level initial block itself).
|
|
initial begin
|
|
#50_000_000;
|
|
$display("FATAL: global simulation timeout");
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|