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
147 lines
6.5 KiB
Verilog
147 lines
6.5 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// V3 -- physical SPI master for the FPGA's OWN configuration flash,
|
|
// used AFTER normal configuration completes (indirect programming,
|
|
// the same real, Xilinx-documented technique used by Vivado's own
|
|
// Hardware Manager "Program Configuration Memory Device" feature --
|
|
// UG470 7 Series FPGAs Configuration User Guide, pages 94-96).
|
|
//
|
|
// WHY THIS EXISTS: the user's board design keeps the config flash
|
|
// wired EXCLUSIVELY to the FPGA (no external host has a direct SPI
|
|
// connection to it) -- the host (an ESP32) can only reach the flash
|
|
// BY GOING THROUGH the FPGA, over the already-existing neural-
|
|
// processor management SPI (spi_host_bridge_v3.v). This module is
|
|
// the physical side of that bridge: a plain byte-wide SPI master
|
|
// (mode 0, MSB-first) driving the flash's own MOSI/CS_B pins and
|
|
// reading its MISO, at a fixed internal clock divide, completely
|
|
// independent of the host's own (slow, externally-clocked) SPI
|
|
// timing.
|
|
//
|
|
// DESIGN CHOICE (passthrough, not a smart flash controller): this
|
|
// module does NOT know any Winbond-specific command opcodes (Write
|
|
// Enable 0x06, Page Program 0x02, Sector Erase 0x20, Read Data 0x03,
|
|
// Read Status Register-1 0x05, BUSY=status bit0 -- all verified
|
|
// against the real W25Q32JV datasheet for the bridge's own protocol
|
|
// documentation, see spi_host_bridge_v3.v's header) -- it just
|
|
// relays whatever bytes the host sends, byte for byte, onto the
|
|
// physical flash bus, and relays back whatever the flash returns.
|
|
// The HOST decides the exact command sequence. This keeps this
|
|
// module trivial and correct-by-construction, and means a future
|
|
// flash part swap needs zero RTL changes here.
|
|
//
|
|
// CCLK REQUIRES STARTUPE2 (a real, hard Xilinx-imposed requirement,
|
|
// not a design choice): the physical CCLK pin is never an ordinary
|
|
// fabric I/O, even after configuration completes -- it can only be
|
|
// driven by fabric logic through the STARTUPE2 primitive's
|
|
// USRCCLKO/USRCCLKTS ports (UG953). MOSI/MISO/CS_B (this project's
|
|
// own board pins D00_MOSI/D01_DIN/FCS_B) DO become ordinary fabric
|
|
// I/O once configuration completes, PROVIDED the bitstream's
|
|
// CONFIG.PERSIST option is FALSE (the Vivado default) -- if a future
|
|
// build ever needs to flip PERSIST on for some other reason, this
|
|
// module stops working and that's a real, disclosed dependency, not
|
|
// a hidden one.
|
|
//
|
|
// ONLY ONE STARTUPE2 PRIMITIVE IS ALLOWED PER DESIGN (a real Xilinx
|
|
// placement rule) -- if this module is ever instantiated alongside
|
|
// another STARTUPE2 use (e.g. a future ICAPE2-based warm-reboot
|
|
// module that also needs it), they must share ONE instance, not two.
|
|
// ============================================================
|
|
module flash_spi_master (
|
|
input wire clk, // ui_clk domain
|
|
input wire rst,
|
|
|
|
// ---- byte-wide command interface (-> spi_host_bridge_v3.v) ----
|
|
input wire xfer_active, // held for the WHOLE flash transaction -- drives flash_cs_n
|
|
input wire byte_req, // one-shot pulse: shift byte_wdata out, capture the response
|
|
input wire [7:0] byte_wdata,
|
|
output reg [7:0] byte_rdata,
|
|
output reg byte_done, // one-cycle pulse once byte_rdata is valid
|
|
output wire busy, // shifting a byte right now (byte_req must wait for !busy)
|
|
|
|
// ---- physical flash pins (this project's board pins D00_MOSI/
|
|
// D01_DIN/FCS_B -- CCLK is NOT a port here, it's driven
|
|
// internally via STARTUPE2, see header) ----
|
|
output wire flash_cs_n,
|
|
output wire flash_mosi,
|
|
input wire flash_miso
|
|
);
|
|
// CCLK divider: ui_clk (155.039MHz per EXP-0074/0076's real P&R)
|
|
// /8 -> ~19.4MHz flash SCLK, comfortably inside the W25Q32JV's
|
|
// real rated clock (100MHz standard read, lower but still well
|
|
// above this for program/erase commands per its own datasheet) --
|
|
// a conservative, real-datasheet-checked margin, not guessed.
|
|
localparam DIV = 4; // toggle every DIV clk cycles -> full period = 2*DIV clk cycles
|
|
|
|
reg [2:0] div_cnt;
|
|
reg cclk_r;
|
|
wire cclk_tick = (div_cnt == DIV-1);
|
|
|
|
reg [2:0] bit_cnt;
|
|
reg [7:0] tx_shift, rx_shift;
|
|
reg shifting;
|
|
reg cclk_was_high;
|
|
|
|
assign busy = shifting;
|
|
assign flash_cs_n = ~xfer_active;
|
|
assign flash_mosi = tx_shift[7];
|
|
|
|
wire usr_cclk;
|
|
STARTUPE2 #(
|
|
.PROG_USR("FALSE"),
|
|
.SIM_CCLK_FREQ(0.0)
|
|
) u_startupe2 (
|
|
.CFGCLK(), .CFGMCLK(), .EOS(), .PREQ(),
|
|
.CLK(1'b0), .GSR(1'b0), .GTS(1'b0), .KEYCLEARB(1'b0), .PACK(1'b0),
|
|
.USRCCLKO(usr_cclk), .USRCCLKTS(1'b0),
|
|
.USRDONEO(1'b1), .USRDONETS(1'b1)
|
|
);
|
|
assign usr_cclk = cclk_r;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
div_cnt <= 3'd0; cclk_r <= 1'b0; bit_cnt <= 3'd0;
|
|
tx_shift <= 8'h00; rx_shift <= 8'h00;
|
|
shifting <= 1'b0; byte_done <= 1'b0; byte_rdata <= 8'h00;
|
|
end else begin
|
|
byte_done <= 1'b0;
|
|
|
|
if (!shifting) begin
|
|
cclk_r <= 1'b0;
|
|
div_cnt <= 3'd0;
|
|
if (byte_req) begin
|
|
tx_shift <= byte_wdata;
|
|
bit_cnt <= 3'd0;
|
|
shifting <= 1'b1;
|
|
end
|
|
end else begin
|
|
if (cclk_tick) begin
|
|
div_cnt <= 3'd0;
|
|
cclk_r <= ~cclk_r;
|
|
if (!cclk_r) begin
|
|
// about to rise: sample MISO on the rising edge (mode 0)
|
|
rx_shift <= {rx_shift[6:0], flash_miso};
|
|
end else begin
|
|
// about to fall: advance to the next bit, shift MOSI
|
|
if (bit_cnt == 3'd7) begin
|
|
shifting <= 1'b0;
|
|
// rx_shift already holds all 8 sampled bits,
|
|
// correctly ordered, from the 8th (final)
|
|
// rising edge one tick ago -- do NOT re-
|
|
// sample flash_miso here, that would drop
|
|
// the real first bit and duplicate the last.
|
|
byte_rdata <= rx_shift;
|
|
byte_done <= 1'b1;
|
|
end else begin
|
|
bit_cnt <= bit_cnt + 3'd1;
|
|
tx_shift <= {tx_shift[6:0], 1'b0};
|
|
end
|
|
end
|
|
end else begin
|
|
div_cnt <= div_cnt + 1'b1;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
endmodule
|