`timescale 1ns/1ps // ================================================================ // SPI_FLASH_MASTER // // SPI MASTER toward the boot/persistence NOR flash (Winbond // W25Q128JV, confirmed part per docs/FPGA-Neural-Hardware-Design.md // §6/§7 -- see sim/flash_model.v's header for the JEDEC-ID variant // caveat). This is the FPGA's *only* path to that flash: the host // never touches these pins directly (see the phase-plan's §0 // constraint) -- it issues opcodes through spi_engine, which this // module (and, in later phases, the copy engine built on top of it) // serves. // // Everything the existing design talks to (rtl/spi_slave.v) is an // SPI SLAVE toward the host. This module is the mirror image: an // SPI MASTER toward the flash, mode 0 (CPOL=0, CPHA=0), MSB-first, // matching every timing diagram in the W25Q128JV datasheet (Fig. // 7/28/30/43a): MOSI driven on the falling edge of SCLK (one edge // ahead of the flash's own rising-edge sample), MISO sampled on the // rising edge (the flash drove it on the previous falling edge). // // ---------------------------------------------------------------- // DEDICATED BUS, NO CCLK/USRMCLK SHARING (revised 2026-09-04) // ---------------------------------------------------------------- // This master's 4 pins (sclk/mosi/miso/cs_n) are ALL ordinary GPIO, // wired to a second, independent connection on the same flash chip // -- the runtime persistence path is fully separate from the boot // config-SPI path (which still uses the dedicated CCLK/DQ0/DQ1/CS // sysCONFIG pins on their own, untouched by this module). No pin is // shared between the two, and no ECP5 config-primitive (`USRMCLK`) // is involved: `sclk` is driven the same way `mosi`/`cs_n` already // are, a plain synchronous output, real from simulation straight // through to place&route -- one identical `.lpf` entry like every // other signal in this design, not a special MCLK-site placement. // // This design was originally built reusing the CCLK pad via // `USRMCLK` (see git history / WORKLOG.md's Phase F1 entry for that // version) to save one pin. That coupling was dropped: sharing the // boot clock pad made the "exclusive flash SPI bus" claim misleading // (electrically it wasn't independent of the config engine at all), // and it carried a real unresolved verification gap (`USRMCLKTS` // pad-enable timing was never checked against the primary Lattice // sysCONFIG Usage Guide, FPGA-TN-02039 -- not present in this // project's local document set). A 4th ordinary GPIO ball costs // nothing on this part (huge pin headroom, docs/FPGA-Neural- // Hardware-Design.md §2) and removes the coupling and the // verification gap entirely. // ---------------------------------------------------------------- // // Command interface (byte-oriented, req/valid handshakes matching // this codebase's existing conventions -- see rtl/spi_slave.v's // rx_valid/tx_byte_req and rtl/mem_arbiter.v's req/ready): // // start -- one-cycle pulse, transaction accepted iff !busy // opcode[7:0] -- flash instruction byte (RDID/READ/WREN/PP/SE/RDSR1) // has_addr -- 1: send 3 address bytes (A23-A0) after opcode // addr[23:0] -- address, sent MSB-first (matches every W25Q128JV // instruction diagram: A23-A16, A15-A8, A7-A0) // dir[1:0] -- DIR_NONE (opcode/addr only, e.g. WREN/SE), // DIR_WRITE (stream n_data bytes TO the flash, // e.g. PP), DIR_READ (stream n_data bytes FROM // the flash, e.g. READ/RDID/RDSR1) // n_data[15:0] -- byte count for the data phase (0 for DIR_NONE) // // wdata_req -- one-cycle pulse: master needs the next write // byte now; caller responds (same cycle or later, // this module simply waits, sclk idles low with // CS still held low -- a legal SPI technique, no // deselect-time constraint applies mid-transaction) // with wdata_valid+wdata. // wdata_valid -- one-cycle pulse, wdata is valid this cycle // wdata[7:0] // // rdata_valid -- one-cycle pulse: rdata holds a freshly-received // byte; master pauses (sclk idle, CS still low) // until the caller acks. // rdata[7:0] // rdata_ack -- one-cycle pulse from caller: byte consumed, // resume shifting. // // busy, done (one-cycle pulse on transaction completion) // // SCLK RATE -- §1 of the phase-plan prompt requires citing timing: // the W25Q128JV(-DTR) datasheet's §9.6 AC Electrical Characteristics // (p.90) caps the Read Data (03h) instruction specifically at // fR=50MHz (all OTHER standard-SPI instructions allow up to // 104-133MHz depending on VCC). Since this master uses one fixed // divider for every instruction, it must honor the TIGHTEST of // those limits. Default SCLK_DIV=2 at CLK_FREQ_MHZ=80 gives // sclk = 80/(2*2) = 20MHz, comfortably under the 50MHz Read Data cap // with margin for the rise/fall-time and setup/hold non-idealities // this digital model does not represent (§A.6) -- correctness over // speed, per the phase-plan's own §A.6/§8 guidance (this is an // init/persistence path, not the inference hot path). // ================================================================ module spi_flash_master #( parameter CLK_FREQ_MHZ = 80, parameter SCLK_DIV = 2 // sclk = CLK_FREQ_MHZ / (2*SCLK_DIV) MHz )( input wire clk, input wire rst, // ------------------------------------------------------------ // Physical pins toward the flash // ------------------------------------------------------------ output reg mosi, input wire miso, output reg cs_n, output wire sclk, // ordinary GPIO, real in both sim and synthesis -- see header // ------------------------------------------------------------ // Command interface // ------------------------------------------------------------ input wire start, input wire [7:0] opcode, input wire has_addr, input wire [23:0] addr, input wire [1:0] dir, input wire [15:0] n_data, output reg wdata_req, input wire [7:0] wdata, input wire wdata_valid, output reg rdata_valid, output reg [7:0] rdata, input wire rdata_ack, output wire busy, output reg done ); localparam DIR_NONE = 2'd0; localparam DIR_WRITE = 2'd1; localparam DIR_READ = 2'd2; // ============================================================ // SCLK generator: free-running divider, gated by `shifting` // (asserted only while actively clocking a bit; held with sclk // low and CS still low during the WAIT_W/EMIT_R handshake // pauses between data bytes). // ============================================================ reg [15:0] div_cnt; reg sclk_reg; reg shifting; wire sclk_half_reached = (div_cnt == SCLK_DIV - 1); always @(posedge clk) begin if (rst || !shifting) begin div_cnt <= 16'd0; sclk_reg <= 1'b0; end else if (sclk_half_reached) begin div_cnt <= 16'd0; sclk_reg <= ~sclk_reg; end else begin div_cnt <= div_cnt + 16'd1; end end wire sclk_will_rise = shifting & sclk_half_reached & ~sclk_reg; // about to go 0->1 wire sclk_will_fall = shifting & sclk_half_reached & sclk_reg; // about to go 1->0 assign sclk = sclk_reg; // ============================================================ // Main FSM // ============================================================ localparam ST_IDLE = 4'd0; localparam ST_CS_SETTLE = 4'd1; // one clk cycle: CS asserted, sclk still idle (setup margin) localparam ST_HDR = 4'd2; // shifting opcode (+ addr) out localparam ST_DATA_WAIT_W = 4'd3; // paused: need next write byte from caller localparam ST_DATA_SHIFT = 4'd4; // shifting one data byte (either direction) localparam ST_DATA_EMIT_R = 4'd5; // paused: present a received byte, wait ack localparam ST_CS_RELEASE = 4'd6; // one clk cycle: CS deasserted, settle localparam ST_DONE = 4'd7; reg [3:0] state; reg [31:0] hdr_shift; // up to 32 bits: 8 opcode + 24 addr reg [5:0] hdr_len; // total header bits for this transaction reg [5:0] bit_idx; // bit position within the current chunk (header or one data byte) reg [7:0] byte_shift; // current data byte, shifting reg [15:0] data_idx; // completed data bytes so far reg [15:0] data_total; reg [1:0] cur_dir; assign busy = (state != ST_IDLE); always @(posedge clk) begin if (rst) begin state <= ST_IDLE; cs_n <= 1'b1; mosi <= 1'b0; shifting <= 1'b0; wdata_req <= 1'b0; rdata_valid <= 1'b0; rdata <= 8'h00; done <= 1'b0; hdr_shift <= 32'h0; hdr_len <= 6'd0; bit_idx <= 6'd0; byte_shift <= 8'h00; data_idx <= 16'd0; data_total <= 16'd0; cur_dir <= DIR_NONE; end else begin wdata_req <= 1'b0; rdata_valid <= 1'b0; done <= 1'b0; case (state) // -------------------------------------------- ST_IDLE: begin shifting <= 1'b0; if (start) begin cs_n <= 1'b0; hdr_shift <= has_addr ? {opcode, addr} : {opcode, 24'h0}; hdr_len <= has_addr ? 6'd32 : 6'd8; bit_idx <= 6'd0; data_idx <= 16'd0; data_total <= n_data; cur_dir <= dir; mosi <= opcode[7]; // bit index 0, preloaded ahead of the first rising edge state <= ST_CS_SETTLE; end end // -------------------------------------------- ST_CS_SETTLE: begin shifting <= 1'b1; state <= ST_HDR; end // -------------------------------------------- // Generic bit shifter for the header (opcode+addr). // MOSI updated on the falling edge (one edge ahead // of the flash's rising-edge sample); bit_idx // advances on the rising edge (the edge on which // the flash actually captures the bit we set up on // the PRECEDING falling edge). // -------------------------------------------- ST_HDR: begin if (sclk_will_fall) begin // At this point bit_idx already equals the // number of bits sampled so far (updated by // the preceding rising edge, below), which // is exactly the index of the NEXT bit to // put on MOSI ahead of its own rising-edge // sample -- e.g. after the 1st rising edge // samples bit 0, bit_idx==1 and this falling // edge must prepare bit 1 = hdr_shift[31-1]. if (bit_idx < hdr_len) mosi <= hdr_shift[31 - bit_idx]; end if (sclk_will_rise) begin if (bit_idx == hdr_len - 1) begin // Header done. Move to data phase or // straight to CS release (DIR_NONE). bit_idx <= 6'd0; if (cur_dir == DIR_NONE || data_total == 16'd0) begin shifting <= 1'b0; state <= ST_CS_RELEASE; end else if (cur_dir == DIR_WRITE) begin shifting <= 1'b0; wdata_req <= 1'b1; state <= ST_DATA_WAIT_W; end else begin // DIR_READ state <= ST_DATA_SHIFT; end end else begin bit_idx <= bit_idx + 6'd1; end end end // -------------------------------------------- ST_DATA_WAIT_W: begin if (wdata_valid) begin byte_shift <= wdata; mosi <= wdata[7]; bit_idx <= 6'd0; shifting <= 1'b1; state <= ST_DATA_SHIFT; end end // -------------------------------------------- // One data byte, either direction. // -------------------------------------------- ST_DATA_SHIFT: begin if (sclk_will_rise) begin if (cur_dir == DIR_READ) byte_shift <= {byte_shift[6:0], miso}; if (bit_idx == 6'd7) begin data_idx <= data_idx + 16'd1; if (cur_dir == DIR_READ) begin shifting <= 1'b0; rdata <= {byte_shift[6:0], miso}; rdata_valid <= 1'b1; state <= ST_DATA_EMIT_R; end else begin if (data_idx + 16'd1 == data_total) begin shifting <= 1'b0; state <= ST_CS_RELEASE; end else begin shifting <= 1'b0; wdata_req <= 1'b1; state <= ST_DATA_WAIT_W; end end end else begin bit_idx <= bit_idx + 6'd1; end end if (sclk_will_fall && cur_dir == DIR_WRITE) begin // Same indexing rationale as ST_HDR above. if (bit_idx < 6'd8) mosi <= byte_shift[7 - bit_idx]; end end // -------------------------------------------- ST_DATA_EMIT_R: begin if (rdata_ack) begin if (data_idx == data_total) begin state <= ST_CS_RELEASE; end else begin bit_idx <= 6'd0; shifting <= 1'b1; state <= ST_DATA_SHIFT; end end end // -------------------------------------------- ST_CS_RELEASE: begin cs_n <= 1'b1; state <= ST_DONE; end // -------------------------------------------- ST_DONE: begin done <= 1'b1; state <= ST_IDLE; end default: state <= ST_IDLE; endcase end end endmodule