Files
FPGA-Neural/sim/flash_copy_engine_erase_tb.v
T
micheleandClaude Sonnet 5 97a21be240 feat: flash boot/persistence subsystem (SPI master, copy engine, CRC32 slot catalog)
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
2026-09-04 08:37:19 +02:00

177 lines
5.8 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_COPY_ENGINE TESTBENCH -- DIR_ERASE (F5's standalone
// FLASH_ERASE opcode primitive)
//
// Flash-side only (DIR_ERASE never touches PSRAM/Port D at all --
// d_req stays low throughout, checked explicitly below), so no
// PSRAM stack needed, mirroring F1's minimal testbench style.
//
// TEST 1 (happy path): a sector pre-poisoned with 0x33 is erased
// and read back (independent flash_model.mem[] peek) as all
// 0xFF -- §8.2.18 p.56.
// TEST 2 (adversarial §A.3): non-sector-aligned flash_addr -> err,
// no SE ever attempted (flash_model.v's own alignment guard,
// which would $fatal, never fires).
// TEST 3: an ADJACENT sector, deliberately left poisoned, must
// survive untouched -- confirms DIR_ERASE erases EXACTLY one
// sector, not a wider or narrower range.
// ================================================================
module tb;
localparam CLK_PERIOD = 12.5; // 80 MHz
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
wire mosi, miso, cs_n, sclk_w;
reg op_start;
reg [1:0] op_dir;
reg [23:0] flash_addr;
reg [22:0] psram_addr;
reg [23:0] len;
wire busy, done, err;
wire d_req, d_wr;
wire [22:0] d_addr;
wire signed [7:0] d_wdata;
reg signed [7:0] d_rdata;
reg d_ready;
localparam DIR_ERASE = 2'd2;
flash_copy_engine #(
.PSRAM_ADDR_WIDTH(23), .CLK_FREQ_MHZ(80), .SCLK_DIV(2)
) dut (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk_sim(sclk_w),
.op_start(op_start), .op_dir(op_dir),
.flash_addr(flash_addr), .psram_addr(psram_addr), .len(len),
.busy(busy), .done(done), .err(err),
.d_req(d_req), .d_wr(d_wr), .d_addr(d_addr), .d_wdata(d_wdata),
.d_rdata(d_rdata), .d_ready(d_ready)
);
flash_model #(.DEPTH(32'h0002_0000), .TIME_SCALE(100000)) dut_flash (
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
// Port D must NEVER be requested by DIR_ERASE.
integer d_req_count;
always @(posedge clk) if (d_req) d_req_count = d_req_count + 1;
integer errors;
integer i;
task automatic do_op(input [23:0] p_addr);
integer wd;
begin
@(posedge clk);
op_start <= 1'b1;
op_dir <= DIR_ERASE;
flash_addr <= p_addr;
psram_addr <= 23'h0;
len <= 24'h0;
@(posedge clk);
op_start <= 1'b0;
wd = 0;
while (!done) begin
@(posedge clk);
wd = wd + 1;
if (wd > 2_000_000) begin
$display("FATAL: do_op watchdog timeout");
$finish;
end
end
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
initial begin
errors = 0;
d_req_count = 0;
rst = 1'b1;
op_start = 1'b0; op_dir = DIR_ERASE; flash_addr = 24'h0;
psram_addr = 23'h0; len = 24'h0;
d_rdata = 8'sd0; d_ready = 1'b0;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
// ========================================================
// TEST 1: happy path
// ========================================================
$display("--- TEST 1 starting ---");
for (i = 0; i < 4096; i = i + 1)
dut_flash.mem[24'h00A000 + i] = 8'h33;
do_op(24'h00A000);
if (err) begin $display("FAIL: TEST1 unexpected err"); errors = errors + 1; end
for (i = 0; i < 4096; i = i + 1) begin
if (dut_flash.mem[24'h00A000 + i] !== 8'hFF) begin
$display("FAIL: TEST1 not fully erased at offset %0d, got=%02h", i, dut_flash.mem[24'h00A000+i]);
errors = errors + 1;
i = 4096;
end
end
// ========================================================
// TEST 2 (adversarial §A.3): unaligned flash_addr
// ========================================================
$display("--- TEST 2 starting ---");
do_op(24'h00A100); // not a multiple of 0x1000
if (!err) begin $display("FAIL: TEST2 expected err for unaligned erase, got none"); errors = errors + 1; end
// ========================================================
// TEST 3: adjacent sector untouched
// ========================================================
$display("--- TEST 3 starting ---");
for (i = 0; i < 4096; i = i + 1)
dut_flash.mem[24'h00B000 + i] = 8'h44; // sector right after the one erased in TEST1
do_op(24'h00A000); // re-erase the SAME sector as TEST1 (already 0xFF, harmless)
if (err) begin $display("FAIL: TEST3 unexpected err"); errors = errors + 1; end
for (i = 0; i < 4096; i = i + 1)
check_byte(dut_flash.mem[24'h00B000 + i], 8'h44, "TEST3 adjacent sector untouched");
if (d_req_count != 0) begin
$display("FAIL: DIR_ERASE issued %0d Port D request(s), expected 0", d_req_count);
errors = errors + 1;
end
// ========================================================
if (errors == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d error(s)", errors);
$finish;
end
initial begin
#50_000_000;
$display("FATAL: global simulation timeout");
$finish;
end
endmodule