Files
FPGA-Neural/sim/flash_slot_manager_tb.v
T
micheleandClaude Sonnet 5 b029e3d95a fix: make flash SPI bus electrically independent, drop USRMCLK/CCLK reuse (Phase F7)
The flash subsystem's SCLK previously reused the boot config-SPI's CCLK
pad via the ECP5 USRMCLK primitive to save one pin. This made the
"exclusive flash bus" claim misleading (SCLK still depended on the
config engine's own pad electrically) and carried an unresolved
verification gap (USRMCLKTS pad-enable timing never checked against
the primary Lattice sysCONFIG Usage Guide).

flash_sclk is now a genuine 4th ordinary GPIO pin (E3, bank 7), added
purely additively to the real .lpf (git diff: one new line, no existing
ball moved). The flash bus is now 4 fully independent wires
(sclk/mosi/miso/cs_n), zero pins shared with any ECP5 config primitive
-- confirmed by the full-system synthesis reporting USRMCLK 0/1 (0%)
utilisation.

All 33 project testbenches re-run clean after the port rename (no
functional change, only sclk_sim -> sclk). Full-system real synthesis
re-verified: 0 constraint errors, Fmax 67.91MHz (up slightly from
66.68MHz, same critical path, not a regression).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-04 10:16:31 +02:00

448 lines
18 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_SLOT_MANAGER TESTBENCH -- Phase F4
//
// Real stack: mem_arbiter -> int8_memory_access -> memory_interface
// -> psram_controller -> psram_model on the PSRAM side (same as
// F2/F3's testbenches), flash_model.v on the flash side.
// flash_slot_manager owns Port D exclusively in this testbench (its
// own internal flash_copy_engine's traffic plus its own
// catalog-staging traffic, muxed internally -- see the module's own
// header); Port A is driven directly by this testbench to seed/
// verify PSRAM content, mirroring F2/F3's own convention.
//
// Independent oracle (§A.1): tools/flash_catalog/oracle.py. Exact
// expected bytes/CRC used below were generated by running:
// python3 -c "from tools.flash_catalog.oracle import pack_entry, crc32; ..."
// (see WORKLOG.md's F4 entry for the precise invocation and output)
// -- not hand-derived from this RTL's own algorithm.
//
// TEST 1 (CAT_WRITE_SLOT + persist + CAT_READ round-trip):
// registers slot 2 (offset=0x001000, length=0, type=0x01,
// valid=0 since no data yet). Checks (a) the raw persisted flash
// bytes against the oracle's pack_entry(...) output byte-for-
// byte (independent oracle, not the RTL's own decode), and
// (b) a fresh CAT_READ (simulating a reboot) correctly
// reconstructs the same on-chip entry.
//
// TEST 2 (SAVE_SLOT, independent CRC oracle): seeds PSRAM with a
// known 32-byte pattern, SAVE_SLOTs it into slot 2, and checks
// the persisted catalog entry's raw bytes against the oracle's
// pack_entry(..., valid=True, data=pattern) output -- including
// the CRC32 field, computed by a completely independent
// implementation (Python zlib), not by reading rtl/crc32.v back.
//
// TEST 3 (LOAD_SLOT, byte-exact + CRC accepted): loads slot 2 back
// into a different PSRAM region, checks byte-exact content and
// err==0 (valid CRC).
//
// TEST 4 (adversarial §A.3, CRC corrotto -> invalido): after
// TEST 2/3's successful save, ONE flash byte belonging to slot
// 2's data is corrupted directly (hierarchical poke into
// flash_model.mem[], independent of the RTL under test) and
// LOAD_SLOT is retried -- must report err==1 (CRC mismatch).
//
// TEST 5 (adversarial §A.3, slot mai salvato -> invalido):
// LOAD_SLOT on a slot that was CAT_WRITE_SLOT'd (registered)
// but never SAVE_SLOT'd -- must report err==1 immediately, with
// no flash/PSRAM transaction attempted at all (checked via a
// PSRAM sentinel at the target address surviving untouched).
//
// TEST 6 (adversarial §A.3, power-loss simulato): a SAVE_SLOT's
// underlying Sector Erase is aborted mid-flight using
// flash_model.v's own documented power-loss hook (forcing
// `pending_se` and `busy` low via hierarchical reference before
// the erase's commit loop runs -- see flash_model.v's header).
// The target sector was pre-poisoned with a DIFFERENT pattern
// than the one being saved, so the abort leaves stale bytes
// un-erased; flash_copy_engine's WIP poll (fooled by the forced
// `busy=0`) proceeds to "successfully" finish the whole
// SAVE_SLOT and marks the catalog valid -- but with a CRC
// computed from the INTENDED data, which no longer matches the
// ACTUAL (corrupted, AND-of-poison-and-intended) flash bytes.
// A subsequent LOAD_SLOT must therefore report err==1 -- this
// is the mechanism (CRC over real committed bytes, not trust in
// a completion signal) that makes "regione invalida rilevata"
// work even when the underlying op silently didn't do what it
// claimed.
// ================================================================
module tb;
localparam CLK_PERIOD = 12.5; // 80 MHz
localparam ADDR_WIDTH = 23;
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_code;
reg [3:0] slot_id;
reg [23:0] new_offset, new_length;
reg [7:0] new_type;
reg [ADDR_WIDTH-1:0] ext_psram_addr;
reg [23:0] ext_length;
wire busy, done, err;
reg [3:0] cat_read_sel;
wire [23:0] cat_out_offset, cat_out_length;
wire [7:0] cat_out_type;
wire cat_out_valid;
wire [31:0] cat_out_crc;
localparam OP_CAT_READ = 2'd0;
localparam OP_CAT_WRITE_SLOT = 2'd1;
localparam OP_LOAD_SLOT = 2'd2;
localparam OP_SAVE_SLOT = 2'd3;
wire d_req, d_wr;
wire [ADDR_WIDTH-1:0] d_addr;
wire signed [7:0] d_wdata;
wire signed [7:0] d_rdata;
wire d_ready;
reg a_req, a_wr;
reg [ADDR_WIDTH-1:0] a_addr;
reg signed [7:0] a_wdata;
wire signed [7:0] a_rdata;
wire a_ready;
flash_slot_manager #(
.PSRAM_ADDR_WIDTH(ADDR_WIDTH),
.CLK_FREQ_MHZ(80),
.SCLK_DIV(2),
.CATALOG_PSRAM_ADDR(23'h000000)
) dut (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n),
.sclk(sclk_w),
.op_start(op_start), .op_code(op_code), .slot_id(slot_id),
.new_offset(new_offset), .new_length(new_length), .new_type(new_type),
.ext_psram_addr(ext_psram_addr), .ext_length(ext_length),
.busy(busy), .done(done), .err(err),
.cat_read_sel(cat_read_sel),
.cat_out_offset(cat_out_offset), .cat_out_length(cat_out_length),
.cat_out_type(cat_out_type), .cat_out_valid(cat_out_valid), .cat_out_crc(cat_out_crc),
.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)
);
// ------------------------------------------------------------
// Real PSRAM stack
// ------------------------------------------------------------
wire arb_req, arb_wr;
wire [ADDR_WIDTH-1:0] arb_addr;
wire signed [7:0] arb_wdata;
wire signed [7:0] arb_rdata;
wire arb_ready;
mem_arbiter #(.ADDR_WIDTH(ADDR_WIDTH)) u_arbiter (
.clk(clk), .rst(rst),
.a_req(a_req), .a_wr(a_wr), .a_addr(a_addr), .a_wdata(a_wdata),
.a_rdata(a_rdata), .a_ready(a_ready),
.b_req(1'b0), .b_wr(1'b0), .b_addr({ADDR_WIDTH{1'b0}}), .b_wdata(8'sd0),
.b_rdata(), .b_ready(),
.c_req(1'b0), .c_wr(1'b0), .c_addr({ADDR_WIDTH{1'b0}}), .c_wdata(8'sd0),
.c_rdata(), .c_ready(),
.d_req(d_req), .d_wr(d_wr), .d_addr(d_addr), .d_wdata(d_wdata),
.d_rdata(d_rdata), .d_ready(d_ready),
.m_req(arb_req), .m_wr(arb_wr), .m_addr(arb_addr), .m_wdata(arb_wdata),
.m_rdata(arb_rdata), .m_ready(arb_ready)
);
wire i8_req, i8_wr;
wire [ADDR_WIDTH-1:0] i8_addr;
wire [15:0] i8_wdata;
wire i8_lb_n, i8_ub_n;
wire [15:0] i8_rdata;
wire i8_ready;
int8_memory_access #(.ADDR_WIDTH(ADDR_WIDTH)) u_i8 (
.clk(clk), .rst(rst),
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
.rdata(arb_rdata), .ready(arb_ready),
.mem_req(i8_req), .mem_wr(i8_wr), .mem_addr(i8_addr), .mem_wdata(i8_wdata),
.mem_lb_n(i8_lb_n), .mem_ub_n(i8_ub_n),
.mem_rdata(i8_rdata), .mem_ready(i8_ready)
);
wire mi_req, mi_wr;
wire [ADDR_WIDTH-1:0] mi_addr;
wire [15:0] mi_wdata;
wire mi_lb_n, mi_ub_n;
wire [15:0] mi_rdata;
wire mi_ready;
memory_interface #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16)) u_mi (
.clk(clk), .rst(rst),
.req(i8_req), .wr(i8_wr), .addr(i8_addr), .wdata(i8_wdata),
.lb_n(i8_lb_n), .ub_n(i8_ub_n),
.rdata(i8_rdata), .ready(i8_ready),
.mem_req(mi_req), .mem_wr(mi_wr), .mem_addr(mi_addr), .mem_wdata(mi_wdata),
.mem_lb_n(mi_lb_n), .mem_ub_n(mi_ub_n),
.mem_rdata(mi_rdata), .mem_ready(mi_ready)
);
wire [ADDR_WIDTH-1:0] psram_a;
wire [15:0] psram_dq;
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
psram_controller #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16), .CLK_FREQ_MHZ(80)) u_psram_ctrl (
.clk(clk), .rst(rst),
.mem_req(mi_req), .mem_wr(mi_wr), .mem_addr(mi_addr), .mem_wdata(mi_wdata),
.mem_lb_n(mi_lb_n), .mem_ub_n(mi_ub_n),
.mem_rdata(mi_rdata), .mem_ready(mi_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(16), .DEPTH(16384)) u_psram (
.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)
);
// ============================================================
// Helper tasks
// ============================================================
integer errors;
task automatic do_op(
input [1:0] p_code,
input [3:0] p_slot,
input [23:0] p_new_offset,
input [23:0] p_new_length,
input [7:0] p_new_type,
input [ADDR_WIDTH-1:0] p_ext_psram_addr,
input [23:0] p_ext_length
);
integer wd;
begin
@(posedge clk);
op_start <= 1'b1;
op_code <= p_code;
slot_id <= p_slot;
new_offset <= p_new_offset;
new_length <= p_new_length;
new_type <= p_new_type;
ext_psram_addr <= p_ext_psram_addr;
ext_length <= p_ext_length;
@(posedge clk);
op_start <= 1'b0;
wd = 0;
while (!done) begin
@(posedge clk);
wd = wd + 1;
if (wd > 5_000_000) begin
$display("FATAL: do_op watchdog timeout");
$finish;
end
end
end
endtask
task automatic psram_read_byte(input [ADDR_WIDTH-1:0] a, output [7:0] v);
begin
@(posedge clk);
a_req <= 1'b1;
a_wr <= 1'b0;
a_addr <= a;
@(posedge clk);
a_req <= 1'b0;
while (!a_ready) @(posedge clk);
v = a_rdata;
@(posedge clk);
end
endtask
task automatic psram_write_byte(input [ADDR_WIDTH-1:0] a, input [7:0] v);
begin
@(posedge clk);
a_req <= 1'b1;
a_wr <= 1'b1;
a_addr <= a;
a_wdata <= $signed(v);
@(posedge clk);
a_req <= 1'b0;
while (!a_ready) @(posedge clk);
@(posedge clk);
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
integer i;
reg [7:0] rb;
reg [7:0] exp_entry [0:15];
initial begin
errors = 0;
rst = 1'b1;
op_start = 1'b0;
op_code = OP_CAT_READ;
slot_id = 4'h0;
new_offset = 24'h0; new_length = 24'h0; new_type = 8'h0;
ext_psram_addr = {ADDR_WIDTH{1'b0}};
ext_length = 24'h0;
cat_read_sel = 4'h0;
a_req = 1'b0; a_wr = 1'b0; a_addr = {ADDR_WIDTH{1'b0}}; a_wdata = 8'sd0;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
// ========================================================
// TEST 1: CAT_WRITE_SLOT + persist + CAT_READ round-trip
// ========================================================
$display("--- TEST 1 starting ---");
do_op(OP_CAT_WRITE_SLOT, 4'd2, 24'h001000, 24'd0, 8'h01, {ADDR_WIDTH{1'b0}}, 24'h0);
if (err) begin $display("FAIL: TEST1 unexpected err"); errors = errors + 1; end
// Independent oracle: python3 pack_entry(offset=0x1000, length=0,
// type=1, valid=False, data=b"") = 00100000000001000000000000000000
exp_entry[0]=8'h00; exp_entry[1]=8'h10; exp_entry[2]=8'h00; exp_entry[3]=8'h00;
exp_entry[4]=8'h00; exp_entry[5]=8'h00; exp_entry[6]=8'h01; exp_entry[7]=8'h00;
exp_entry[8]=8'h00; exp_entry[9]=8'h00; exp_entry[10]=8'h00; exp_entry[11]=8'h00;
exp_entry[12]=8'h00; exp_entry[13]=8'h00; exp_entry[14]=8'h00; exp_entry[15]=8'h00;
for (i = 0; i < 16; i = i + 1)
check_byte(dut_flash.mem[24'h000000 + 2*16 + i], exp_entry[i], "TEST1 persisted catalog entry vs oracle");
do_op(OP_CAT_READ, 4'h0, 24'h0, 24'h0, 8'h0, {ADDR_WIDTH{1'b0}}, 24'h0);
cat_read_sel = 4'd2;
#1;
if (cat_out_offset !== 24'h001000 || cat_out_length !== 24'd0 ||
cat_out_type !== 8'h01 || cat_out_valid !== 1'b0) begin
$display("FAIL: TEST1 CAT_READ reconstruction mismatch: offset=%06h length=%0d type=%02h valid=%b",
cat_out_offset, cat_out_length, cat_out_type, cat_out_valid);
errors = errors + 1;
end
// ========================================================
// TEST 2/3: SAVE_SLOT (independent CRC oracle) + LOAD_SLOT
// ========================================================
$display("--- TEST 2/3 starting ---");
for (i = 0; i < 32; i = i + 1)
psram_write_byte(23'h003000 + i, 8'h60 + i[7:0]);
do_op(OP_SAVE_SLOT, 4'd2, 24'h0, 24'h0, 8'h0, 23'h003000, 24'd32);
if (err) begin $display("FAIL: TEST2 unexpected err"); errors = errors + 1; end
// Independent oracle: python3 pack_entry(offset=0x1000, length=32,
// type=1, valid=True, data=bytes(0x60..0x7F))
// = 001000000020010139a6f63100000000
exp_entry[0]=8'h00; exp_entry[1]=8'h10; exp_entry[2]=8'h00; exp_entry[3]=8'h00;
exp_entry[4]=8'h00; exp_entry[5]=8'h20; exp_entry[6]=8'h01; exp_entry[7]=8'h01;
exp_entry[8]=8'h39; exp_entry[9]=8'ha6; exp_entry[10]=8'hf6; exp_entry[11]=8'h31;
exp_entry[12]=8'h00; exp_entry[13]=8'h00; exp_entry[14]=8'h00; exp_entry[15]=8'h00;
for (i = 0; i < 16; i = i + 1)
check_byte(dut_flash.mem[24'h000000 + 2*16 + i], exp_entry[i], "TEST2 persisted catalog entry (SAVE_SLOT) vs oracle");
for (i = 0; i < 32; i = i + 1)
check_byte(dut_flash.mem[24'h001000 + i], 8'h60 + i[7:0], "TEST2 slot data in flash vs oracle pattern");
do_op(OP_LOAD_SLOT, 4'd2, 24'h0, 24'h0, 8'h0, 23'h004000, 24'h0);
if (err) begin $display("FAIL: TEST3 unexpected err (valid CRC)"); errors = errors + 1; end
for (i = 0; i < 32; i = i + 1) begin
psram_read_byte(23'h004000 + i, rb);
check_byte(rb, 8'h60 + i[7:0], "TEST3 LOAD_SLOT byte-exact");
end
// ========================================================
// TEST 4 (adversarial §A.3): CRC corrotto -> invalido
// ========================================================
$display("--- TEST 4 starting ---");
dut_flash.mem[24'h001005] = dut_flash.mem[24'h001005] ^ 8'h01; // flip one bit of slot data
do_op(OP_LOAD_SLOT, 4'd2, 24'h0, 24'h0, 8'h0, 23'h005000, 24'h0);
if (!err) begin $display("FAIL: TEST4 expected err for corrupted CRC, got none"); errors = errors + 1; end
dut_flash.mem[24'h001005] = dut_flash.mem[24'h001005] ^ 8'h01; // restore for later tests
// ========================================================
// TEST 5 (adversarial §A.3): slot mai salvato -> invalido
// ========================================================
$display("--- TEST 5 starting ---");
do_op(OP_CAT_WRITE_SLOT, 4'd5, 24'h008000, 24'd0, 8'h02, {ADDR_WIDTH{1'b0}}, 24'h0);
psram_write_byte(23'h006000, 8'h5A); // sentinel
do_op(OP_LOAD_SLOT, 4'd5, 24'h0, 24'h0, 8'h0, 23'h006000, 24'h0);
if (!err) begin $display("FAIL: TEST5 expected err for never-saved slot, got none"); errors = errors + 1; end
psram_read_byte(23'h006000, rb);
check_byte(rb, 8'h5A, "TEST5 sentinel untouched (no transaction attempted)");
// ========================================================
// TEST 6 (adversarial §A.3): power-loss simulato durante
// l'erase di una SAVE_SLOT.
// ========================================================
$display("--- TEST 6 starting ---");
do_op(OP_CAT_WRITE_SLOT, 4'd7, 24'h009000, 24'd0, 8'h01, {ADDR_WIDTH{1'b0}}, 24'h0);
for (i = 0; i < 4096; i = i + 1)
dut_flash.mem[24'h009000 + i] = 8'h77; // poison, different from the intended save
for (i = 0; i < 16; i = i + 1)
psram_write_byte(23'h007000 + i, 8'hD0 + i[7:0]); // intended (different) data
fork
do_op(OP_SAVE_SLOT, 4'd7, 24'h0, 24'h0, 8'h0, 23'h007000, 24'd16);
begin
@(posedge dut_flash.pending_se);
dut_flash.pending_se = 1'b0;
dut_flash.busy = 1'b0;
end
join
if (err) begin $display("FAIL: TEST6 unexpected err on the (fooled) SAVE_SLOT itself"); errors = errors + 1; end
do_op(OP_LOAD_SLOT, 4'd7, 24'h0, 24'h0, 8'h0, 23'h008100, 24'h0);
if (!err) begin $display("FAIL: TEST6 expected err for power-loss-corrupted slot, got none"); errors = errors + 1; end
// ========================================================
if (errors == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d error(s)", errors);
$finish;
end
initial begin
#200_000_000;
$display("FATAL: global simulation timeout");
$finish;
end
endmodule