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

336 lines
12 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_FLASH_MASTER TESTBENCH (Phase F1)
//
// Drives rtl/spi_flash_master.v against sim/flash_model.v (an
// independent behavioral model of the datasheet's write rules, see
// its own header) and checks:
//
// TEST 1 (RDID, happy path): oracle is the datasheet's own
// manufacturer/type/capacity bytes (EF/40/18h -- see
// flash_model.v's header for the plain-vs-DTR JEDEC-ID note),
// hardcoded here independently of both the RTL and the model's
// internal constant (this testbench does not read
// flash_model.v's localparam, it states the expected value
// itself, from the same citation).
//
// TEST 2 (READ, known block, INDEPENDENT oracle): a pattern is
// planted directly into flash_model's memory array via
// hierarchical reference (dut_flash.mem[...]) -- NOT written
// through spi_flash_master -- then read back through the DUT
// and compared byte-exact. This is the true independent-oracle
// test per WORKLOG.md §A.1: the expected data was never
// produced by the RTL under test.
//
// TEST 3 (WREN+PP+RDSR-poll+READ round-trip): exercises the
// write-side primitives (not yet independent of the RTL's own
// write path -- a round-trip self-consistency check, distinct
// from TEST 2's independence, and explicitly noted as such).
// Confirms AND-only programming into an erased (0xFF) region
// reproduces the exact bytes written, and that RDSR's BUSY bit
// is observed high during the modeled tPP delay and clears
// after.
//
// TEST 4 (WREN+SE+RDSR-poll+READ): confirms sector erase drives
// the target sector back to all-0xFF, verified against a region
// TEST 3 deliberately left non-0xFF first (so this test cannot
// pass by coincidence on an already-blank region).
//
// TEST 5 (negative, §A.3): an opcode this design never uses
// (0xAB, Release Power-down / Device ID -- a real, harmless
// W25Q128JV instruction, but NOT one spi_flash_master or
// flash_model implement) is issued. Requirement: the
// transaction completes (`done` pulses within a bounded cycle
// count) rather than hanging -- a master that can wedge on an
// opcode it doesn't specifically recognize is broken by
// construction, since it always just shifts bits regardless of
// opcode semantics.
// ================================================================
module tb;
localparam CLK_PERIOD = 12.5; // 80 MHz, matches CLK_FREQ_MHZ default
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
// ------------------------------------------------------------
// DUT <-> flash_model physical wiring
// ------------------------------------------------------------
wire mosi;
wire miso;
wire cs_n;
wire sclk_w;
reg start;
reg [7:0] opcode;
reg has_addr;
reg [23:0] addr;
reg [1:0] dir;
reg [15:0] n_data;
wire wdata_req;
reg [7:0] wdata;
reg wdata_valid;
wire rdata_valid;
wire [7:0] rdata;
reg rdata_ack;
wire busy;
wire done;
localparam DIR_NONE = 2'd0;
localparam DIR_WRITE = 2'd1;
localparam DIR_READ = 2'd2;
spi_flash_master #(
.CLK_FREQ_MHZ(80),
.SCLK_DIV(2)
) dut (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n),
.sclk(sclk_w),
.start(start), .opcode(opcode), .has_addr(has_addr), .addr(addr),
.dir(dir), .n_data(n_data),
.wdata_req(wdata_req), .wdata(wdata), .wdata_valid(wdata_valid),
.rdata_valid(rdata_valid), .rdata(rdata), .rdata_ack(rdata_ack),
.busy(busy), .done(done)
);
flash_model #(
.DEPTH(32'h0002_0000),
.TIME_SCALE(100000) // see flash_model.v header: MAX datasheet timing / 100000
) dut_flash (
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
// ------------------------------------------------------------
// Bookkeeping
// ------------------------------------------------------------
integer errors;
reg [7:0] rbuf [0:511];
reg [7:0] wbuf [0:511];
// ============================================================
// do_cmd: drive one full command through the DUT's byte-level
// handshake, servicing wdata_req/rdata_valid as needed. Reads
// land in rbuf[0..n_data-1]; writes are sourced from wbuf.
// ============================================================
integer k;
integer wd_cyc;
task automatic do_cmd(
input [7:0] p_opcode,
input p_has_addr,
input [23:0] p_addr,
input [1:0] p_dir,
input [15:0] p_n
);
begin
@(posedge clk);
start <= 1'b1;
opcode <= p_opcode;
has_addr <= p_has_addr;
addr <= p_addr;
dir <= p_dir;
n_data <= p_n;
@(posedge clk);
start <= 1'b0;
k = 0;
wd_cyc = 0;
while (!done) begin
@(posedge clk);
if (wdata_req) begin
wdata_valid <= 1'b1;
wdata <= wbuf[k];
end else begin
wdata_valid <= 1'b0;
end
if (wdata_valid) begin
k = k + 1;
end
if (rdata_valid) begin
rbuf[k] = rdata;
k = k + 1;
rdata_ack <= 1'b1;
end else begin
rdata_ack <= 1'b0;
end
wd_cyc = wd_cyc + 1;
if (wd_cyc > 200000) begin
$display("FATAL: do_cmd watchdog timeout (opcode=%02h) at t=%0t", p_opcode, $time);
$finish;
end
end
@(posedge clk);
rdata_ack <= 1'b0;
wdata_valid <= 1'b0;
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
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_READ = 8'h03;
localparam [7:0] OP_PP = 8'h02;
localparam [7:0] OP_SE = 8'h20;
localparam [7:0] OP_RDSR1 = 8'h05;
localparam [7:0] OP_RDID = 8'h9F;
task automatic wait_wip_clear;
begin
rbuf[0] = 8'hFF;
while (rbuf[0][0] == 1'b1) begin
do_cmd(OP_RDSR1, 1'b0, 24'h0, DIR_READ, 16'd1);
end
end
endtask
integer i;
initial begin
errors = 0;
rst = 1'b1;
start = 1'b0;
opcode = 8'h00;
has_addr = 1'b0;
addr = 24'h0;
dir = DIR_NONE;
n_data = 16'd0;
wdata_valid = 1'b0;
wdata = 8'h00;
rdata_ack = 1'b0;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
// ========================================================
// TEST 1: RDID happy path
// ========================================================
do_cmd(OP_RDID, 1'b0, 24'h0, DIR_READ, 16'd3);
check_byte(rbuf[0], 8'hEF, "TEST1 RDID manufacturer");
check_byte(rbuf[1], 8'h40, "TEST1 RDID memtype");
check_byte(rbuf[2], 8'h18, "TEST1 RDID capacity");
// ========================================================
// TEST 2: READ, known block, planted independently of the
// RTL (hierarchical poke into the flash model's own array).
// ========================================================
for (i = 0; i < 16; i = i + 1)
dut_flash.mem[24'h001000 + i] = 8'hA0 + i[7:0];
do_cmd(OP_READ, 1'b1, 24'h001000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'hA0 + i[7:0], "TEST2 READ known block");
// ========================================================
// TEST 3: WREN + PP (16 bytes) + RDSR poll + READ round-trip.
// Target region 0x002000 starts erased (0xFF, from
// flash_model's own reset init) -- programming a pattern
// with some cleared bits must read back exactly that
// pattern (AND-with-0xFF is a no-op, so this alone doesn't
// yet prove AND-only; TEST 3b below does).
// ========================================================
for (i = 0; i < 16; i = i + 1)
wbuf[i] = 8'h10 + i[7:0];
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3 PP/READ round-trip");
// TEST 3b: program the SAME region again with a value that
// tries to SET a bit the first program cleared (0x10 has
// bit4 set high already at 0x10=00010000; try programming
// 0xFF over it, which per the AND-only rule must leave 0x10
// unchanged, NOT flip it to 0xFF). This is the datasheet
// rule TEST 3 alone cannot distinguish from a naive
// "programming just overwrites" implementation.
for (i = 0; i < 16; i = i + 1)
wbuf[i] = 8'hFF;
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3b PP AND-only (program cannot set bits)");
// ========================================================
// TEST 4: WREN + SE (sector containing 0x002000) + RDSR
// poll + READ, confirming erase -> all-FF. The region was
// deliberately left non-FF by TEST 3/3b above, so this
// cannot pass by coincidence.
// ========================================================
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_SE, 1'b1, 24'h002000, DIR_NONE, 16'd0);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'hFF, "TEST4 SE erase -> 0xFF");
// ========================================================
// TEST 5 (negative, §A.3): unsupported/unrecognized opcode
// (0xABh, a real W25Q128JV instruction -- Release
// Power-down/Device ID -- that neither spi_flash_master nor
// flash_model implement any special-case for). Requirement:
// do_cmd's watchdog must NOT fire -- the transaction has to
// complete (`done` pulses) purely from the master's own
// fixed bit-count/opcode-agnostic shifting, proving the
// master cannot wedge on an opcode it doesn't recognize.
// ========================================================
do_cmd(8'hAB, 1'b0, 24'h0, DIR_READ, 16'd1);
$display("TEST5 (illegal/unsupported opcode 0xAB): completed without watchdog timeout, got=%02h (undefined/don't-care by design)", rbuf[0]);
// ========================================================
if (errors == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d error(s)", errors);
$finish;
end
// Safety net: absolute simulation timeout independent of the
// per-command watchdog above (catches a hang between commands,
// e.g. in the top-level initial block itself).
initial begin
#50_000_000;
$display("FATAL: global simulation timeout");
$finish;
end
endmodule