`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