Files
FPGA-Neural/sim/flash_copy_engine_erase_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

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(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