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
430 lines
17 KiB
Verilog
430 lines
17 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// FLASH_COPY_ENGINE TESTBENCH -- Phase F3 (SAVE direction, PSRAM -> flash)
|
|
//
|
|
// Same real stack as sim/flash_copy_engine_load_tb.v (mem_arbiter ->
|
|
// int8_memory_access -> memory_interface -> psram_controller ->
|
|
// psram_model on the PSRAM side, flash_model.v on the flash side),
|
|
// exercising DIR_SAVE this time.
|
|
//
|
|
// TEST 1 (round-trip byte-exact, self-consistency): a pattern is
|
|
// written into PSRAM via arbiter Port A (host-style,
|
|
// WRITE_RAM-equivalent), SAVEd to flash, then loaded BACK from
|
|
// flash into a DIFFERENT PSRAM region via DIR_LOAD (already
|
|
// independently verified in F2) and compared byte-exact against
|
|
// the original. This is a round-trip check (not fully
|
|
// independent of the RTL under test, since LOAD is used as part
|
|
// of the oracle) -- flagged as such, same honesty standard as
|
|
// F1/F2. TEST 1b below supplies the genuinely independent half.
|
|
//
|
|
// TEST 1b (independent oracle): after TEST 1's SAVE, the actual
|
|
// flash_model.mem[] bytes at the target sector are read directly
|
|
// via hierarchical reference (bypassing the RTL under test
|
|
// entirely) and compared against the exact bytes written to
|
|
// PSRAM -- the true §A.1 independent-oracle check for F3.
|
|
//
|
|
// TEST 2 (adversarial §A.3, page-boundary crossing): a SAVE whose
|
|
// length spans MULTIPLE 256B pages within one sector (e.g. 300
|
|
// bytes) must still produce byte-exact flash content across the
|
|
// page boundary -- proving the internal Page Program loop
|
|
// correctly issues a SEPARATE WREN+PP+poll per page rather than
|
|
// one PP call that would silently truncate/corrupt at the 256B
|
|
// boundary (Page Program cannot cross a page, §8 intro p.24).
|
|
//
|
|
// TEST 3 (adversarial §A.3, sector-unaligned SAVE): flash_addr not
|
|
// a multiple of 4096 must be rejected with `err`, no flash or
|
|
// PSRAM transaction attempted (checked via a flash-side
|
|
// sentinel: a byte just before the requested address, and the
|
|
// model's own $fatal-on-out-of-alignment-SE guard in
|
|
// flash_model.v, which would fire if the engine incorrectly
|
|
// proceeded anyway).
|
|
//
|
|
// TEST 4 (adversarial §A.3, erase-then-verify across sector):
|
|
// confirms the erase phase actually reaches all-0xFF everywhere
|
|
// in the target sector(s) BEFORE programming (checked by
|
|
// pre-poisoning the target sector with non-0xFF flash content,
|
|
// then SAVEing a pattern shorter than the sector and verifying
|
|
// the untouched TAIL of the sector reads back 0xFF -- proving a
|
|
// real erase happened, not just an in-place program over old
|
|
// data that would leave the tail with its original poison).
|
|
//
|
|
// TEST 5 (adversarial §A.3, prolonged WIP / stuck busy): flash_
|
|
// model's RDSR-1 response is hijacked (via a small wrapper wire)
|
|
// to report BUSY indefinitely for a bounded window, confirming
|
|
// the engine's RDSR poll loop keeps re-polling (not looping a
|
|
// fixed number of times and giving up silently) and completes
|
|
// correctly once BUSY finally clears -- flash_copy_engine itself
|
|
// has NO poll-count timeout by design (documented in
|
|
// WORKLOG.md/module header: an unbounded RDSR poll is correct
|
|
// per the datasheet, which gives no hard upper bound other than
|
|
// tSE/tPP MAX -- a host-side watchdog, not this engine, is where
|
|
// any hard timeout policy belongs, per the phase-plan's own
|
|
// §5 "STATUS" error-reporting design, deferred to F5).
|
|
// ================================================================
|
|
|
|
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
|
|
|
|
// ------------------------------------------------------------
|
|
// Flash side (with an RDSR-hijack wrapper for TEST 5)
|
|
// ------------------------------------------------------------
|
|
wire mosi, miso_flash, cs_n, sclk_w;
|
|
reg force_busy;
|
|
wire miso = force_busy ? 1'b1 : miso_flash; // see TEST 5
|
|
|
|
// ------------------------------------------------------------
|
|
// flash_copy_engine command interface
|
|
// ------------------------------------------------------------
|
|
reg op_start;
|
|
reg [1:0] op_dir;
|
|
reg [23:0] flash_addr;
|
|
reg [ADDR_WIDTH-1:0] psram_addr;
|
|
reg [23:0] len;
|
|
wire busy;
|
|
wire done;
|
|
wire err;
|
|
|
|
localparam DIR_LOAD = 2'd0;
|
|
localparam DIR_SAVE = 2'd1;
|
|
|
|
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_copy_engine #(
|
|
.PSRAM_ADDR_WIDTH(ADDR_WIDTH),
|
|
.CLK_FREQ_MHZ(80),
|
|
.SCLK_DIV(2)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
|
|
.mosi(mosi), .miso(miso), .cs_n(cs_n),
|
|
.sclk(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_flash), .cs_n(cs_n)
|
|
);
|
|
|
|
// ------------------------------------------------------------
|
|
// Real PSRAM stack (same as flash_copy_engine_load_tb.v)
|
|
// ------------------------------------------------------------
|
|
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_dir,
|
|
input [23:0] p_flash_addr,
|
|
input [ADDR_WIDTH-1:0] p_psram_addr,
|
|
input [23:0] p_len
|
|
);
|
|
integer wd;
|
|
begin
|
|
@(posedge clk);
|
|
op_start <= 1'b1;
|
|
op_dir <= p_dir;
|
|
flash_addr <= p_flash_addr;
|
|
psram_addr <= p_psram_addr;
|
|
len <= p_len;
|
|
@(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;
|
|
|
|
initial begin
|
|
errors = 0;
|
|
rst = 1'b1;
|
|
op_start = 1'b0;
|
|
op_dir = DIR_SAVE;
|
|
flash_addr = 24'h0;
|
|
psram_addr = {ADDR_WIDTH{1'b0}};
|
|
len = 24'h0;
|
|
a_req = 1'b0;
|
|
a_wr = 1'b0;
|
|
a_addr = {ADDR_WIDTH{1'b0}};
|
|
a_wdata = 8'sd0;
|
|
force_busy = 1'b0;
|
|
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
repeat (5) @(posedge clk);
|
|
|
|
// ========================================================
|
|
// TEST 1 / 1b: SAVE round-trip + independent oracle
|
|
// ========================================================
|
|
$display("--- TEST 1/1b starting ---");
|
|
for (i = 0; i < 40; i = i + 1)
|
|
psram_write_byte(23'h001000 + i, 8'h30 + i[7:0]);
|
|
|
|
do_op(DIR_SAVE, 24'h004000, 23'h001000, 24'd40); // sector-aligned flash_addr
|
|
if (err) begin $display("FAIL: TEST1 unexpected err"); errors = errors + 1; end
|
|
|
|
// TEST 1b: independent oracle, direct flash_model peek
|
|
for (i = 0; i < 40; i = i + 1)
|
|
check_byte(dut_flash.mem[24'h004000 + i], 8'h30 + i[7:0], "TEST1b independent flash_model peek");
|
|
|
|
// TEST 1: round-trip via the already-verified LOAD path
|
|
do_op(DIR_LOAD, 24'h004000, 23'h002000, 24'd40);
|
|
for (i = 0; i < 40; i = i + 1) begin
|
|
psram_read_byte(23'h002000 + i, rb);
|
|
check_byte(rb, 8'h30 + i[7:0], "TEST1 SAVE->LOAD round-trip");
|
|
end
|
|
|
|
// ========================================================
|
|
// TEST 2 (adversarial, page-boundary crossing): 300 bytes,
|
|
// spanning pages [0..255] and [256..299] within one sector.
|
|
// ========================================================
|
|
$display("--- TEST 2 starting ---");
|
|
for (i = 0; i < 300; i = i + 1)
|
|
psram_write_byte(23'h003000 + i, 8'h80 + i[7:0]);
|
|
|
|
do_op(DIR_SAVE, 24'h005000, 23'h003000, 24'd300);
|
|
if (err) begin $display("FAIL: TEST2 unexpected err"); errors = errors + 1; end
|
|
|
|
for (i = 0; i < 300; i = i + 1)
|
|
check_byte(dut_flash.mem[24'h005000 + i], 8'h80 + i[7:0], "TEST2 page-boundary-crossing SAVE");
|
|
|
|
// ========================================================
|
|
// TEST 3 (adversarial §A.3): non-sector-aligned flash_addr.
|
|
// ========================================================
|
|
$display("--- TEST 3 starting ---");
|
|
do_op(DIR_SAVE, 24'h005100, 23'h001000, 24'd16); // 0x5100 is NOT a multiple of 0x1000
|
|
if (!err) begin $display("FAIL: TEST3 expected err for unaligned SAVE, got none"); errors = errors + 1; end
|
|
// No $fatal from flash_model.v (its own SE-alignment guard)
|
|
// means the engine correctly never attempted the erase --
|
|
// reaching this line at all is part of the pass condition.
|
|
|
|
// ========================================================
|
|
// TEST 4 (adversarial §A.3): erase reaches the WHOLE sector,
|
|
// not just the programmed prefix. Poison the sector, SAVE a
|
|
// short pattern, confirm the untouched tail is 0xFF.
|
|
// ========================================================
|
|
$display("--- TEST 4 starting ---");
|
|
for (i = 0; i < 4096; i = i + 1)
|
|
dut_flash.mem[24'h006000 + i] = 8'h55; // poison, not 0xFF
|
|
|
|
for (i = 0; i < 20; i = i + 1)
|
|
psram_write_byte(23'h001100 + i, 8'hA0 + i[7:0]);
|
|
|
|
do_op(DIR_SAVE, 24'h006000, 23'h001100, 24'd20);
|
|
if (err) begin $display("FAIL: TEST4 unexpected err"); errors = errors + 1; end
|
|
|
|
for (i = 0; i < 20; i = i + 1)
|
|
check_byte(dut_flash.mem[24'h006000 + i], 8'hA0 + i[7:0], "TEST4 programmed prefix");
|
|
for (i = 20; i < 4096; i = i + 1) begin
|
|
if (dut_flash.mem[24'h006000 + i] !== 8'hFF) begin
|
|
$display("FAIL: TEST4 sector tail not erased at offset %0d, got=%02h", i, dut_flash.mem[24'h006000 + i]);
|
|
errors = errors + 1;
|
|
i = 4096; // stop spamming after the first mismatch
|
|
end
|
|
end
|
|
|
|
// ========================================================
|
|
// TEST 5 (adversarial §A.3): prolonged WIP. Force RDSR's
|
|
// MISO to report BUSY for a bounded window during the first
|
|
// poll after this SAVE's erase, then release it -- the
|
|
// engine must keep polling (not give up) and finish
|
|
// correctly once BUSY clears.
|
|
// ========================================================
|
|
$display("--- TEST 5 starting ---");
|
|
for (i = 0; i < 16; i = i + 1)
|
|
psram_write_byte(23'h001200 + i, 8'hC0 + i[7:0]);
|
|
|
|
fork
|
|
do_op(DIR_SAVE, 24'h007000, 23'h001200, 24'd16);
|
|
begin
|
|
// Hold MISO high (BUSY=1, since RDSR-1 bit0 is the
|
|
// first bit shifted out MSB-first... actually the
|
|
// model shifts BUSY at a specific bit position, see
|
|
// flash_model.v -- forcing the whole byte to 0xFF
|
|
// via MISO=1 throughout guarantees bit0=1 (BUSY)
|
|
// regardless of exact bit timing) for a fixed
|
|
// window, comfortably longer than one real poll
|
|
// transaction takes, then release control back to
|
|
// the real flash model.
|
|
#(50_000);
|
|
force_busy = 1'b1;
|
|
#(300_000);
|
|
force_busy = 1'b0;
|
|
end
|
|
join
|
|
if (err) begin $display("FAIL: TEST5 unexpected err"); errors = errors + 1; end
|
|
|
|
for (i = 0; i < 16; i = i + 1)
|
|
check_byte(dut_flash.mem[24'h007000 + i], 8'hC0 + i[7:0], "TEST5 SAVE completes correctly despite prolonged WIP");
|
|
|
|
// ========================================================
|
|
if (errors == 0)
|
|
$display("ALL TESTS PASSED");
|
|
else
|
|
$display("FAILED: %0d error(s)", errors);
|
|
|
|
$finish;
|
|
end
|
|
|
|
initial begin
|
|
#100_000_000;
|
|
$display("FATAL: global simulation timeout");
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|