`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