Files
FPGA-Neural/hardware/v3/sim/tb_flash_spi_master.v
T
micheleandClaude Sonnet 5 a4c080da83 feat: config-flash passthrough bridge via STARTUPE2, real board-exclusive flash access (EXP-0077)
Implements the user's board architecture: config flash wired
exclusively to the FPGA, host (ESP32) reaches it only through the
FPGA. flash_spi_master.v is a plain byte-wide SPI master using
STARTUPE2 to reclaim CCLK after configuration (the real, Xilinx-
documented "indirect SPI flash programming" technique, UG470 p94-96).
New opcode 0x40 FLASH_XFER in spi_host_bridge_v3.v relays bytes
byte-for-byte between host and the physical flash bus -- the host
decides the exact SPI NOR command sequence (verified against the real
W25Q32JV datasheet), this RTL knows nothing about flash semantics.

Found and fixed two real bugs during verification: a byte-assembly
off-by-one in flash_spi_master.v, and a genuine protocol-latency bug
in the FLASH_XFER opcode's response timing (needed 2 trailing margin
bytes, not 1 -- the internal flash transfer doesn't start until the
triggering byte finishes, so 1 byte of margin isn't enough). 39/39
tests pass end to end (host SPI -> bridge -> flash_spi_master ->
behavioral flash model).

Wired into n2_system_ddr3_top.v with real pin constraints (flash_mosi
=K17/flash_miso=K18/flash_cs_n=L13, the same pins reserved-but-unused
in EXP-0075) and BITSTREAM.CONFIG.PERSIST=FALSE made explicit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-19 21:45:17 +02:00

220 lines
8.8 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// Isolated correctness test for flash_spi_master.v's own bit-level
// SPI master logic (mode 0, MSB-first), against a small behavioral
// model of the REAL W25Q32JV command set (Write Enable=0x06, Read
// Status Register-1=0x05 with BUSY=bit0/WEL=bit1, Page Program=0x02,
// Read Data=0x03 -- all verified against the real Winbond datasheet,
// see flash_spi_master.v's own header).
//
// STARTUPE2 (the real Xilinx primitive this module uses for CCLK) is
// stood in here by a trivial simulation-only stub (`u_startupe2_stub`,
// just passes CLK through) -- this test verifies the BIT-LEVEL SPI
// protocol logic is correct, which is independent of STARTUPE2's own
// real behavior. Full verification against the real Xilinx UNISIM
// STARTUPE2 model (via xsim, same technique as EXP-0068's real DDR3
// verification) is a disclosed follow-up, not done here.
// ============================================================
module STARTUPE2 #(
parameter PROG_USR = "FALSE",
parameter real SIM_CCLK_FREQ = 0.0
)(
output wire CFGCLK, output wire CFGMCLK, output wire EOS, output wire PREQ,
input wire CLK, input wire GSR, input wire GTS, input wire KEYCLEARB, input wire PACK,
input wire USRCCLKO, input wire USRCCLKTS,
input wire USRDONEO, input wire USRDONETS
);
endmodule
module tb;
reg clk, rst;
initial begin clk = 0; forever #(1000.0/155.039/2) clk = ~clk; end // real ui_clk period, 155.039MHz
reg xfer_active, byte_req;
reg [7:0] byte_wdata;
wire [7:0] byte_rdata;
wire byte_done, busy;
wire flash_cs_n, flash_mosi;
reg flash_miso;
flash_spi_master u_dut (
.clk(clk), .rst(rst),
.xfer_active(xfer_active), .byte_req(byte_req),
.byte_wdata(byte_wdata), .byte_rdata(byte_rdata), .byte_done(byte_done), .busy(busy),
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
);
// ---- behavioral W25Q32JV-like flash model: real command set,
// simplified (single in-memory byte array, no real program/erase
// timing, no protection checks -- enough to prove the physical
// SPI relay is bit-exact end to end) ----
reg [7:0] flash_mem [0:255];
reg [7:0] flash_cmd;
reg [7:0] flash_addr;
reg [1:0] flash_phase; // 0=cmd, 1=addr(x3, only using 1 byte here), 2=data
reg flash_wel;
reg [7:0] flash_bit_shift_out;
reg [2:0] flash_bit_idx;
reg flash_prev_cs;
reg flash_prev_cclk;
// The model watches the SAME physical bus the DUT drives -- it
// reconstructs bytes from raw SCLK/MOSI transitions, exactly as a
// real chip would, using the DUT's own internal cclk_r (only
// observable via hierarchical reference since flash_spi_master.v
// doesn't expose CCLK as a port, it's internal post-STARTUPE2
// wiring in the real module -- acceptable for a testbench, not
// for synthesis).
wire flash_sclk = u_dut.cclk_r;
reg [7:0] model_shift;
reg [2:0] model_bitcnt;
reg [7:0] model_out_byte;
reg [2:0] model_bytecnt;
always @(posedge flash_sclk) begin
if (!flash_cs_n) begin
model_shift <= {model_shift[6:0], flash_mosi};
if (model_bitcnt == 3'd7) begin
model_bitcnt <= 3'd0;
// full byte received
case (model_bytecnt)
3'd0: begin
// check the just-captured byte directly, not
// flash_cmd (whose own NBA update from this
// SAME line hasn't committed yet this cycle)
flash_cmd <= {model_shift[6:0], flash_mosi};
if ({model_shift[6:0], flash_mosi} == 8'h06)
flash_wel <= 1'b1;
model_bytecnt <= model_bytecnt + 1'b1;
end
3'd1: begin
if (flash_cmd == 8'h02 || flash_cmd == 8'h03) begin
flash_addr <= {model_shift[6:0], flash_mosi};
model_bytecnt <= model_bytecnt + 1'b1;
end
end
3'd2: begin
if (flash_cmd == 8'h02) begin
flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi};
end
model_bytecnt <= model_bytecnt + 1'b1;
end
default: ;
endcase
end else begin
model_bitcnt <= model_bitcnt + 1'b1;
end
end
end
// MISO driver: Read Status Register-1 (0x05) returns {6'b0, wel, 1'b0(BUSY=0)}
// Read Data (0x03) returns flash_mem[flash_addr] starting at the byte after addr
reg [7:0] model_rdata_byte;
always @(*) begin
if (flash_cmd == 8'h05) model_rdata_byte = {6'b0, flash_wel, 1'b0};
else if (flash_cmd == 8'h03) model_rdata_byte = flash_mem[flash_addr];
else model_rdata_byte = 8'h00;
end
always @(negedge flash_sclk) begin
if (!flash_cs_n && model_bytecnt >= (flash_cmd==8'h05 ? 3'd1 : 3'd2))
flash_miso <= model_rdata_byte[3'd7 - model_bitcnt];
end
always @(posedge flash_cs_n) begin
model_bytecnt <= 3'd0;
model_bitcnt <= 3'd0;
end
integer errors, tests;
task automatic check(input cond, input [255:0] name);
begin
tests = tests + 1;
if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end
else $display("PASS: %0s", name);
end
endtask
// Drives byte_req/byte_wdata with NONBLOCKING assignment, same
// established fix as EXP-0073/0075 (tb_neural_director_packed.v /
// tb_spi_host_bridge_v3.v): a blocking-assignment one-shot pulse
// races the DUT's own posedge-triggered read under Icarus and can
// be missed entirely, not just corrupted -- confirmed here via a
// real hang (byte_req never observed by the DUT at all) before
// this fix.
task automatic send_byte(input [7:0] b, output [7:0] r);
begin
@(posedge clk);
byte_wdata <= b;
byte_req <= 1'b1;
@(posedge clk);
byte_req <= 1'b0;
while (!byte_done) @(posedge clk);
r = byte_rdata;
@(posedge clk);
end
endtask
reg [7:0] rb;
initial begin
errors = 0; tests = 0;
rst = 1; xfer_active <= 0; byte_req = 0; byte_wdata = 0; flash_miso = 0;
model_bytecnt = 0; model_bitcnt = 0; flash_wel = 0;
repeat(5) @(posedge clk);
rst = 0;
@(posedge clk);
$display("=== TEST 1: WRITE ENABLE (0x06), then READ STATUS REGISTER-1 (0x05), expect WEL=1 ===");
xfer_active <= 1'b1;
send_byte(8'h06, rb);
xfer_active <= 1'b0;
@(posedge clk); @(posedge clk);
xfer_active <= 1'b1;
send_byte(8'h05, rb); // command byte, response don't-care
send_byte(8'h00, rb); // dummy clock, get status back
xfer_active <= 1'b0;
check(rb[1] == 1'b1, "T1: WEL bit set after Write Enable");
@(posedge clk); @(posedge clk);
$display("=== TEST 2: PAGE PROGRAM (0x02) @ addr 0x10 = 0xA5, then READ DATA (0x03) same addr ===");
xfer_active <= 1'b1;
send_byte(8'h02, rb);
send_byte(8'h10, rb);
send_byte(8'hA5, rb);
xfer_active <= 1'b0;
@(posedge clk); @(posedge clk);
xfer_active <= 1'b1;
send_byte(8'h03, rb);
send_byte(8'h10, rb);
send_byte(8'h00, rb); // dummy clock, get data back
xfer_active <= 1'b0;
check(rb == 8'hA5, "T2: Read Data returns the byte just programmed, bit-exact");
@(posedge clk); @(posedge clk);
$display("=== TEST 3: byte relay bit-exactness across several values (0x00,0xFF,0x55,0xAA) ===");
xfer_active <= 1'b1;
send_byte(8'h02, rb); send_byte(8'h20, rb);
send_byte(8'h00, rb);
xfer_active <= 1'b0; @(posedge clk); @(posedge clk);
xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h20, rb); send_byte(8'h00, rb); xfer_active <= 1'b0;
check(rb == 8'h00, "T3: 0x00 round-trip");
@(posedge clk); @(posedge clk);
xfer_active <= 1'b1;
send_byte(8'h02, rb); send_byte(8'h21, rb);
send_byte(8'hFF, rb);
xfer_active <= 1'b0; @(posedge clk); @(posedge clk);
xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h21, rb); send_byte(8'h00, rb); xfer_active <= 1'b0;
check(rb == 8'hFF, "T3: 0xFF round-trip (catches stuck-low relay bugs)");
@(posedge clk); @(posedge clk);
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
if (errors == 0) $display("ALL TESTS PASSED (tb_flash_spi_master)");
$finish;
end
endmodule