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:
@@ -137,6 +137,48 @@
|
||||
// below), so host software doesn't need
|
||||
// to hardcode it.
|
||||
//
|
||||
// 0x40 FLASH_XFER -- raw byte-for-byte SPI passthrough to the
|
||||
// FPGA's OWN configuration flash (see
|
||||
// flash_spi_master.v's own header for why
|
||||
// this exists: the board wires the config
|
||||
// flash EXCLUSIVELY to the FPGA, so the host
|
||||
// can only reach it by going through this
|
||||
// opcode). Every MOSI byte received while
|
||||
// this opcode is active is relayed, bit for
|
||||
// bit, onto the physical flash's own MOSI
|
||||
// line; whatever the flash returns is relayed
|
||||
// back on MISO. This module knows NOTHING
|
||||
// about SPI NOR command semantics (Write
|
||||
// Enable, Page Program, etc.) -- the host is
|
||||
// responsible for sending a real flash command
|
||||
// sequence, exactly as if it were wired to
|
||||
// the flash directly.
|
||||
// LATENCY (real, measured via simulation, not
|
||||
// guessed -- see EXP-0077): flash_spi_master.v's
|
||||
// own byte transfer takes real internal clock
|
||||
// cycles to complete (~640ns at this project's
|
||||
// real 155.039MHz ui_clk with the default
|
||||
// DIV=4 setting), and that transfer only
|
||||
// STARTS once byte N is fully received -- i.e.
|
||||
// right as byte N+1's OWN transmission begins,
|
||||
// not before. Byte N's response therefore only
|
||||
// becomes stable partway through byte N+1's
|
||||
// own window, NOT for its very first bit --
|
||||
// relying on "ready by the next byte" corrupts
|
||||
// exactly the byte N+1 response's own early
|
||||
// bits (confirmed: a real, reproduced bug
|
||||
// during this opcode's own development, not
|
||||
// hypothetical). The safe, real requirement is
|
||||
// TWO trailing dummy bytes, not one: byte N's
|
||||
// response is only guaranteed stable and
|
||||
// correct during host byte N+2's own window,
|
||||
// since a full extra host byte period is
|
||||
// always comfortably longer than one internal
|
||||
// flash transfer at any realistic host SPI
|
||||
// clock rate. The host must clock TWO extra
|
||||
// dummy bytes at the end of a transaction to
|
||||
// safely receive the final real response.
|
||||
//
|
||||
// Any opcode byte not listed above is treated as NOP (0 payload,
|
||||
// MISO drives 0x00) -- matches spi_host_bridge.v's own "unknown
|
||||
// opcode is inert, never wedges the bus" precedent.
|
||||
@@ -179,6 +221,13 @@ module spi_host_bridge_v3 #(
|
||||
input wire [15:0] mem_rdata,
|
||||
input wire mem_ready,
|
||||
|
||||
// ---- config-flash passthrough (-> flash_spi_master.v) ----
|
||||
output reg flash_xfer_active,
|
||||
output reg flash_byte_req,
|
||||
output reg [7:0] flash_byte_wdata,
|
||||
input wire [7:0] flash_byte_rdata,
|
||||
input wire flash_byte_done,
|
||||
|
||||
output reg soft_rst_pulse
|
||||
);
|
||||
|
||||
@@ -280,6 +329,7 @@ module spi_host_bridge_v3 #(
|
||||
localparam OP_STATUS = 8'h20;
|
||||
localparam OP_REG_WRITE = 8'h30;
|
||||
localparam OP_REG_READ = 8'h31;
|
||||
localparam OP_FLASH_XFER= 8'h40;
|
||||
|
||||
localparam ST_OPCODE = 4'd0;
|
||||
localparam ST_JOB = 4'd1; // collecting 16 WRITE_JOB payload bytes
|
||||
@@ -294,6 +344,8 @@ module spi_host_bridge_v3 #(
|
||||
localparam ST_REG_ADDR = 4'd10; // collecting 1 reg_addr byte
|
||||
localparam ST_REG_WDATA= 4'd11; // REG_WRITE: collecting 4 value bytes
|
||||
localparam ST_REG_ROUT = 4'd12; // REG_READ: shifting 4 value bytes out
|
||||
localparam ST_FLASH_XFER = 4'd13; // FLASH_XFER: ready for next host byte
|
||||
localparam ST_FLASH_WAIT = 4'd14; // FLASH_XFER: waiting for flash_byte_done
|
||||
|
||||
reg [3:0] state;
|
||||
reg [7:0] opcode;
|
||||
@@ -304,6 +356,7 @@ module spi_host_bridge_v3 #(
|
||||
reg job_busy_r, mem_busy_r, last_job_accepted_r;
|
||||
reg [7:0] reg_addr;
|
||||
reg [31:0] reg_wdata; // REG_WRITE: assembling the 4 value bytes
|
||||
reg [7:0] flash_rdata_r; // FLASH_XFER: previous byte's flash response (see header's own "off by one" note)
|
||||
|
||||
// ---- ROUT-exit deferral (real bug found and fixed this session,
|
||||
// see the header's own note near the physical layer): the
|
||||
@@ -355,6 +408,8 @@ module spi_host_bridge_v3 #(
|
||||
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
|
||||
else if (opcode == OP_REG_READ && state == ST_REG_ROUT)
|
||||
tx_mux = reg_rdata[8*(3-byte_idx) +: 8];
|
||||
else if (opcode == OP_FLASH_XFER)
|
||||
tx_mux = flash_rdata_r;
|
||||
end
|
||||
assign tx_byte = tx_mux;
|
||||
|
||||
@@ -372,9 +427,12 @@ module spi_host_bridge_v3 #(
|
||||
reg_addr <= 8'h00; reg_wdata <= 32'h0;
|
||||
mem_rout_pending_ignore <= 1'b0; mem_rout_pending_riss <= 1'b0;
|
||||
reg_rout_pending <= 1'b0;
|
||||
flash_xfer_active <= 1'b0; flash_byte_req <= 1'b0;
|
||||
flash_byte_wdata <= 8'h00; flash_rdata_r <= 8'h00;
|
||||
end else begin
|
||||
mem_req <= 1'b0;
|
||||
soft_rst_pulse <= 1'b0;
|
||||
flash_byte_req <= 1'b0;
|
||||
|
||||
// Same protection as spi_host_bridge.v: don't let a new CS
|
||||
// assertion reset state/byte_idx while a previous
|
||||
@@ -398,6 +456,11 @@ module spi_host_bridge_v3 #(
|
||||
OP_READ_MEM: state <= ST_MEM_ADDR;
|
||||
OP_REG_WRITE: state <= ST_REG_ADDR;
|
||||
OP_REG_READ: state <= ST_REG_ADDR;
|
||||
OP_FLASH_XFER: begin
|
||||
state <= ST_FLASH_XFER;
|
||||
flash_xfer_active <= 1'b1;
|
||||
flash_rdata_r <= 8'h00;
|
||||
end
|
||||
OP_RESET: state <= ST_IGNORE;
|
||||
default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload
|
||||
endcase
|
||||
@@ -499,7 +562,13 @@ module spi_host_bridge_v3 #(
|
||||
if (byte_idx != 5'd3) byte_idx <= byte_idx + 5'd1;
|
||||
end
|
||||
|
||||
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_REG_ROUT/ST_IGNORE: no MOSI payload expected
|
||||
ST_FLASH_XFER: begin
|
||||
flash_byte_wdata <= rx_byte;
|
||||
flash_byte_req <= 1'b1;
|
||||
state <= ST_FLASH_WAIT;
|
||||
end
|
||||
|
||||
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_REG_ROUT/ST_FLASH_WAIT/ST_IGNORE: no MOSI payload expected
|
||||
endcase
|
||||
end
|
||||
|
||||
@@ -575,12 +644,18 @@ module spi_host_bridge_v3 #(
|
||||
state <= ST_IGNORE;
|
||||
end
|
||||
|
||||
if (state == ST_FLASH_WAIT && flash_byte_done) begin
|
||||
flash_rdata_r <= flash_byte_rdata;
|
||||
state <= ST_FLASH_XFER;
|
||||
end
|
||||
|
||||
job_busy_r <= (state == ST_JOB_WAIT);
|
||||
|
||||
if (cs_rose) begin
|
||||
if (opcode == OP_RESET) soft_rst_pulse <= 1'b1;
|
||||
if (state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS)
|
||||
state <= ST_OPCODE;
|
||||
flash_xfer_active <= 1'b0;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user