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
220 lines
8.8 KiB
Verilog
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
|