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
This commit is contained in:
2026-09-19 21:45:17 +02:00
co-authored by Claude Sonnet 5
parent fd6cc7a2fa
commit a4c080da83
7 changed files with 727 additions and 10 deletions
+134
View File
@@ -44,10 +44,31 @@ module tb_spi_host_bridge_v3;
reg init_calib_complete_model = 0;
reg dir_error_model = 0;
// ---- config-flash passthrough path: real flash_spi_master.v +
// the same behavioral W25Q32JV-like model used standalone in
// tb_flash_spi_master.v (EXP-0077), wired end to end through
// spi_host_bridge_v3.v's own new FLASH_XFER opcode ----
wire flash_xfer_active, flash_byte_req, flash_byte_done;
wire [7:0] flash_byte_wdata, flash_byte_rdata;
wire flash_cs_n, flash_mosi, flash_miso;
flash_spi_master u_flash (
.clk(clk), .rst(rst),
.xfer_active(flash_xfer_active), .byte_req(flash_byte_req),
.byte_wdata(flash_byte_wdata), .byte_rdata(flash_byte_rdata), .byte_done(flash_byte_done), .busy(),
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
);
flash_model_w25q32 u_flash_model (
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso),
.flash_sclk(u_flash.cclk_r)
);
spi_host_bridge_v3 #(
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2)
) dut (
.clk(clk), .rst(rst),
.flash_xfer_active(flash_xfer_active), .flash_byte_req(flash_byte_req),
.flash_byte_wdata(flash_byte_wdata), .flash_byte_rdata(flash_byte_rdata), .flash_byte_done(flash_byte_done),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.init_calib_complete(init_calib_complete_model), .dir_error(dir_error_model),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready_model),
@@ -315,9 +336,122 @@ module tb_spi_host_bridge_v3;
spi_byte(8'h00, rxb); check(rxb == 8'h79, "M: READ_MEM LSB byte == 0x79 (bit0=1, catches the ROUT-exit bug)");
cs_n = 1; #40;
// ================= Test N: FLASH_XFER passthrough end to end
// -- real flash_spi_master.v + a real Winbond-command-set
// behavioral flash model behind it. Write Enable + Page
// Program + Read Data, entirely through spi_host_bridge_v3.v's
// own opcode 0x40, proving the WHOLE relay chain (host SPI ->
// this bridge -> flash_spi_master.v -> physical flash bus) is
// bit-exact, not just each half in isolation. ===============
cs_n = 0; #20;
spi_byte(8'h40, rxb); // opcode FLASH_XFER
spi_byte(8'h06, rxb); // relay: Write Enable
cs_n = 1; #40;
cs_n = 0; #20;
spi_byte(8'h40, rxb);
spi_byte(8'h02, rxb); // relay: Page Program
spi_byte(8'h30, rxb); // relay: addr=0x30
spi_byte(8'h5A, rxb); // relay: data=0x5A
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2 -- see header's own real latency note
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2
cs_n = 1; #40;
cs_n = 0; #20;
spi_byte(8'h40, rxb);
spi_byte(8'h03, rxb); // relay: Read Data
spi_byte(8'h30, rxb); // relay: addr=0x30
spi_byte(8'h00, rxb); // relay: dummy clock for the data byte
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2 -- response is safely stable here
check(rxb == 8'h5A, "N: FLASH_XFER end-to-end round trip through the real flash model, bit-exact");
cs_n = 1; #40;
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
if (errors != 0) $display("*** %0d FAILURES ***", errors);
$finish;
end
endmodule
// STARTUPE2 simulation-only stub -- see tb_flash_spi_master.v's own
// header for why real UNISIM verification is a disclosed follow-up,
// not done here (this test verifies the protocol/relay logic, which
// is independent of STARTUPE2's own real behavior).
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
// Same behavioral W25Q32JV-like flash model as tb_flash_spi_master.v
// (EXP-0077) -- kept independent (not shared via `include) since each
// testbench owns its own self-contained model, matching this
// project's existing convention (e.g. sdram_model.v is the one real
// exception, shared because it stands in for real vendor-supplied
// silicon behavior, not a test-specific convenience model).
module flash_model_w25q32 (
input wire flash_cs_n,
input wire flash_mosi,
output reg flash_miso,
input wire flash_sclk
);
reg [7:0] flash_mem [0:255];
reg [7:0] flash_cmd;
reg [7:0] flash_addr;
reg flash_wel;
reg [7:0] model_shift;
reg [2:0] model_bitcnt;
reg [2:0] model_bytecnt;
reg [7:0] model_rdata_byte;
initial begin flash_wel = 0; model_bytecnt = 0; model_bitcnt = 0; flash_miso = 0; end
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;
case (model_bytecnt)
3'd0: begin
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) flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi};
model_bytecnt <= model_bytecnt + 1'b1;
end
default: ;
endcase
end else begin
model_bitcnt <= model_bitcnt + 1'b1;
end
end
end
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
endmodule