Files
FPGA-Neural/rtl/spi_flash_master.v
T
micheleandClaude Sonnet 5 b029e3d95a fix: make flash SPI bus electrically independent, drop USRMCLK/CCLK reuse (Phase F7)
The flash subsystem's SCLK previously reused the boot config-SPI's CCLK
pad via the ECP5 USRMCLK primitive to save one pin. This made the
"exclusive flash bus" claim misleading (SCLK still depended on the
config engine's own pad electrically) and carried an unresolved
verification gap (USRMCLKTS pad-enable timing never checked against
the primary Lattice sysCONFIG Usage Guide).

flash_sclk is now a genuine 4th ordinary GPIO pin (E3, bank 7), added
purely additively to the real .lpf (git diff: one new line, no existing
ball moved). The flash bus is now 4 fully independent wires
(sclk/mosi/miso/cs_n), zero pins shared with any ECP5 config primitive
-- confirmed by the full-system synthesis reporting USRMCLK 0/1 (0%)
utilisation.

All 33 project testbenches re-run clean after the port rename (no
functional change, only sclk_sim -> sclk). Full-system real synthesis
re-verified: 0 constraint errors, Fmax 67.91MHz (up slightly from
66.68MHz, same critical path, not a regression).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-04 10:16:31 +02:00

372 lines
16 KiB
Verilog

`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