Files
FPGA-Neural/hardware/v1/rtl/flash_copy_engine.v
T
micheleandClaude Sonnet 5 dc0b331d3e feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor
Begins the V2 Neural Multiprocessor / Dataflow architecture per
docs/v2-description.md, per explicit user request to freeze V1 and
start V2 development, copying from V1 what's needed.

Scaffold:
- hardware/v1/: byte-exact, read-only copy of the current V1 codebase
  (rtl, testbenches, tools, constraints, a representative subset of
  synthesis results, and reference docs) -- verified identical via
  diff/cmp against the live top-level tree before being made
  filesystem-read-only. The live top-level tree is untouched and
  remains the project's "production" V1 (see hardware/v1/README.md
  and hardware/v2/logs/decisions.log DEC-0001 for why copy-not-move).
- hardware/v2/: mandatory structure (rtl/sim/constraints/synthesis/
  reports/scripts/logs/docs) plus the full logging system required by
  the spec (development/architecture/simulation/synthesis/timing/
  benchmark/decisions/experiments/errors.log).

M1 -- Neural Processor (hardware/v2/rtl/neural_processor.v):
- 8-stage pipelined perceptron unit (P_IN=8): input align, 8
  multipliers, 3-level adder tree, accumulator, bias+activation, INT8
  saturation. Genuine 1-tile/cycle throughput, not just a wider
  combinational datapath.
- 7-state FSM (NP_IDLE..NP_ERROR per docs/v2-description.md §6, with
  4 baseline states merged into NP_WAIT_OPERANDS -- see
  decisions.log DEC-0002); valid/ready/data/last stream interfaces
  per §7.
- Bit-exact vs the frozen hardware/v1/rtl/neuron_parallel.v + mac8.v
  + mac_unit.v: 7/7 tests pass (hardware/v2/sim/tb_neural_processor.v),
  covering regular/mixed-sign/extreme-INT8 vectors, both activations,
  a zero-idle-gap back-to-back-tiles throughput check, and an 8-tile
  job -- verified with Verilator (see below for why).
- Real synthesis + place&route (Yosys + nextpnr-ecp5): 0 CHECK
  problems, Fmax 183.12 MHz at ACC_WIDTH=32 (PASS at 80MHz, ~3x V1's
  isolated PARALLEL=8 Fmax of 61.71 MHz) and 176.21 MHz at ACC_WIDTH=24
  (a user-requested comparison experiment, also bit-exact-verified;
  see experiments.log EXP-0001/EXP-0002 and benchmark.log).

Three real bugs found and resolved during M1 development (full
diagnostic record in errors.log):
- Two independent, reproducible Icarus Verilog v13.0 scheduling
  defects (ERR-0001, ERR-0002) that silently produced wrong simulation
  results for standard sequential Verilog -- confirmed via Verilator
  5.050 giving correct results on the same minimal repros. Verilator
  is now the trusted simulator for hardware/v2/ (decisions.log
  DEC-0004); Icarus's affected protocol-violation check was removed
  from the RTL and deferred architecturally to the Neural Director
  (DEC-0003) rather than chased further.
- One real RTL bug (ERR-0003): last0 wasn't gated like valid0,
  letting a "last tile" tag leak into the pipeline ahead of its
  actual valid tile on back-to-back jobs. Fixed and verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-05 14:06:53 +02:00

609 lines
28 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_COPY_ENGINE
//
// DMA-style block-streaming engine between the boot/persistence
// flash (via rtl/spi_flash_master.v, which it owns internally) and
// the shared PSRAM (via a new low-priority Port D on
// rtl/mem_arbiter.v -- see that file's updated header). This is the
// module the flash-subsystem phase-plan's §3 describes: the host
// issues a high-level command and walks away; there is no
// byte-at-a-time host involvement, only the FPGA streaming at
// whatever rate the flash/PSRAM actually allow.
//
// F2 SCOPE: LOAD direction, flash -> PSRAM. `op_start` with
// `op_dir = DIR_LOAD` copies `len` bytes from `flash_addr` to
// `psram_addr`. The Read Data (03h) instruction has no page-boundary
// restriction (unlike Page Program, §8 intro p.24: only
// WRITE/PROGRAM/ERASE instructions must land on a byte/page boundary
// or get ignored -- READ just streams and auto-increments, wrapping
// at the top of the array), so a LOAD needs no internal
// erase/program looping.
//
// F3 SCOPE: SAVE direction, PSRAM -> flash. `op_start` with
// `op_dir = DIR_SAVE` copies `len` bytes from `psram_addr` to
// `flash_addr`, doing erase-before-write internally:
//
// - Design decision (phase-plan §2.1 explicitly asks for one,
// with a stated reason): `flash_addr` MUST be 4KB-sector-aligned
// (low 12 bits zero) -- rejected as a bounds/alignment error
// otherwise (§A.3 "blocco non allineato al settore"), rather
// than silently doing a read-modify-erase-write of a partial
// sector. Reason: this project has NO scratch buffer large
// enough to hold a whole 4KB sector's unrelated surrounding data
// while erasing/reprogramming it, and the flash-subsystem's own
// design (fixed-size catalog slots, §4 of the phase-plan) means
// every real SAVE_SLOT call (F5) already writes whole,
// sector-aligned slots -- so alignment is not a real-world
// restriction here, only a rejected pathological case.
// - Erase phase: every 4KB sector overlapping [flash_addr,
// flash_addr+len) is erased (WREN + SE + poll RDSR-1 bit0/WIP
// until clear) before any programming starts. A `len` that
// isn't itself a sector multiple still erases the WHOLE last
// (partial) sector -- erase has no finer granularity (Table 1
// p.26) -- leaving the unwritten tail of that sector at 0xFF
// (erased state), which is correct/expected, not a bug: the
// catalog's own valid_flag+CRC (F4) is what marks the meaningful
// length of a slot, not "everything in the sector is meaningful".
// F5 SCOPE: DIR_ERASE, a standalone sector erase (the phase-plan's
// §5 FLASH_ERASE opcode, exposed once F5 wires this engine to
// spi_engine). `op_start` with `op_dir = DIR_ERASE` erases exactly
// the one 4KB sector at `flash_addr` (which must be sector-aligned,
// same check/reason as DIR_SAVE) -- `psram_addr` and `len` are
// ignored. Reuses DIR_SAVE's own erase-phase states verbatim (WREN +
// SE + poll RDSR-1/WIP), just stopping after that one sector instead
// of falling through to the program phase -- see `erase_only` below.
//
// - Program phase: looped Page Program (02h) calls of up to 256B
// each (never crossing a page boundary -- guaranteed by
// `flash_addr` being sector-, hence page-, aligned, and by
// capping every chunk at 256B), each individually WREN'd and
// RDSR-polled to completion before the next, exactly as the
// phase-plan's §2.2 requires ("Il loop lo fa la FPGA"). Each
// page's bytes are sourced live from PSRAM one at a time via
// Port D reads, driven by spi_flash_master's own `wdata_req`
// handshake (F1) -- no local buffer needed, matching this
// engine's LOAD-side "stream through" style.
//
// spi_flash_master's `n_data` is 16 bits (max 65535 bytes per SPI
// transaction); this engine's own `len` is 24 bits (matching the
// flash-subsystem opcode draft's 3-byte length field, §5 of the
// phase-plan), so a LOAD larger than 65535 bytes is split into
// multiple back-to-back spi_flash_master READ transactions
// (CHUNK_MAX bytes each) rather than assumed to fit in one -- closes
// a latent bug rather than leaving an untested edge case.
//
// Bounds checking (§A.3 "len fuori range" negative case, enforced
// here rather than deferred entirely to the F5 opcode layer, in
// case a future caller other than spi_engine ever drives this
// module directly): a request whose flash_addr+len would exceed the
// modeled 16MB (2^24) flash address space, or whose psram_addr+len
// would exceed the 8MB (2^23, ADDR_WIDTH) PSRAM address space,
// completes immediately with `err` asserted and does not touch the
// flash or PSRAM at all.
// ================================================================
module flash_copy_engine #(
parameter PSRAM_ADDR_WIDTH = 23,
parameter CLK_FREQ_MHZ = 80,
parameter SCLK_DIV = 2
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Physical flash pins (this module owns spi_flash_master)
// ------------------------------------------------------------
output wire mosi,
input wire miso,
output wire cs_n,
output wire sclk, // ordinary GPIO, real in both sim and synthesis
// ------------------------------------------------------------
// Command interface
// ------------------------------------------------------------
input wire op_start,
input wire [1:0] op_dir, // DIR_LOAD (F2, flash->PSRAM) or DIR_SAVE (F3, PSRAM->flash)
input wire [23:0] flash_addr,
input wire [PSRAM_ADDR_WIDTH-1:0] psram_addr,
input wire [23:0] len,
output wire busy,
output reg done, // one-cycle pulse
output reg err, // held until next op_start; see bounds check above
// ------------------------------------------------------------
// PSRAM arbiter master port (mem_arbiter.v Port D)
//
// d_req is a LEVEL signal (held for the whole ST_PSRAM_WAIT
// state, see below), not a one-cycle pulse like the other
// ports' requesters (spi_engine.v etc.) use. Found necessary
// during F2 bring-up (see WORKLOG.md): mem_arbiter.v only
// samples a requester's `req` while `owner==SEL_NONE`, so a
// ONE-CYCLE pulse that happens to land on the exact same cycle
// a higher-priority port (A/B/C) also requests is granted to
// that other port and simply never retried -- Port D is lowest
// priority by design (see mem_arbiter.v's header), so under any
// real, sustained contention from Port A a one-shot pulse would
// eventually get "unlucky" and hang this engine forever waiting
// for a d_ready that will never come. Holding d_req at the
// arbiter continuously (not just once) makes the wait exactly
// what the design intends -- "gets stretched out", never lost --
// without needing to change mem_arbiter.v itself (which serves
// the three already-validated masters too).
// ------------------------------------------------------------
output wire d_req,
output reg d_wr,
output reg [PSRAM_ADDR_WIDTH-1:0] d_addr,
output reg signed [7:0] d_wdata,
input wire signed [7:0] d_rdata,
input wire d_ready
);
localparam DIR_LOAD = 2'd0;
localparam DIR_SAVE = 2'd1;
localparam DIR_ERASE = 2'd2;
// FLASH_SPACE_BYTES = 16MB = 2^24 does NOT fit in 24 bits (24 bits
// only reaches 2^24-1) -- needs 25. Caught by iverilog's own
// "numeric constant truncated" warning on the first compile
// attempt (it silently became 0, which would have broken every
// bounds check below into "always in range"); widened here.
localparam [24:0] FLASH_SPACE_BYTES = 25'h100_0000; // 16MB, 2^24
localparam [23:0] CHUNK_MAX = 24'h00_FFFF; // spi_flash_master's n_data is 16b
localparam [23:0] SECTOR_BYTES = 24'd4096; // Sector Erase (4KB), Table 1 p.26 "20h"
localparam [23:0] PAGE_BYTES = 24'd256; // Page Program max, Table 1 p.26/note 3 p.29
// ============================================================
// spi_flash_master instance (F1 primitive)
// ============================================================
reg fm_start;
reg [7:0] fm_opcode;
reg fm_has_addr;
reg [23:0] fm_addr;
reg [1:0] fm_dir;
reg [15:0] fm_n_data;
wire fm_wdata_req;
reg [7:0] fm_wdata; // F3: driven from a live PSRAM read during Page Program
reg fm_wdata_valid;
wire fm_rdata_valid;
wire [7:0] fm_rdata;
reg fm_rdata_ack;
wire fm_busy;
wire fm_done;
localparam [1:0] FM_DIR_NONE = 2'd0;
localparam [1:0] FM_DIR_WRITE = 2'd1;
localparam [1:0] FM_DIR_READ = 2'd2;
spi_flash_master #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ),
.SCLK_DIV(SCLK_DIV)
) u_spi_flash_master (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk(sclk),
.start(fm_start), .opcode(fm_opcode), .has_addr(fm_has_addr),
.addr(fm_addr), .dir(fm_dir), .n_data(fm_n_data),
.wdata_req(fm_wdata_req), .wdata(fm_wdata), .wdata_valid(fm_wdata_valid),
.rdata_valid(fm_rdata_valid), .rdata(fm_rdata), .rdata_ack(fm_rdata_ack),
.busy(fm_busy), .done(fm_done)
);
localparam [7:0] OP_READ = 8'h03;
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_PP = 8'h02;
localparam [7:0] OP_SE = 8'h20;
localparam [7:0] OP_RDSR1 = 8'h05;
// ============================================================
// Main FSM
// ============================================================
localparam ST_IDLE = 5'd0;
localparam ST_CHUNK_ISSUE = 5'd1; // LOAD: starts one spi_flash_master READ for up to CHUNK_MAX bytes
localparam ST_CHUNK_WAIT = 5'd2; // LOAD: services rdata_valid -> PSRAM write, one byte at a time
localparam ST_PSRAM_WAIT = 5'd3; // LOAD: waiting for d_ready after issuing a PSRAM write
localparam ST_DONE = 5'd4;
// F3 (SAVE) states
localparam ST_SAVE_ERASE_WREN = 5'd5; // WREN before this sector's erase
localparam ST_SAVE_ERASE_WWAIT = 5'd6; // wait for WREN's own fm_done
localparam ST_SAVE_ERASE_ISSUE = 5'd7; // issue SE for the current sector
localparam ST_SAVE_ERASE_EWAIT = 5'd8; // wait for SE's own fm_done (command accepted, not WIP clear)
localparam ST_SAVE_PROG_WREN = 5'd9; // WREN before this page's Page Program
localparam ST_SAVE_PROG_WWAIT = 5'd10; // wait for WREN's own fm_done
localparam ST_SAVE_PROG_ISSUE = 5'd11; // issue PP header for the current page
localparam ST_SAVE_PROG_BYTE = 5'd12; // waiting for fm_wdata_req or fm_done (page byte loop)
localparam ST_SAVE_PROG_PWAIT = 5'd13; // waiting for d_ready on the PSRAM source read (presents wdata_valid the same cycle d_ready arrives)
// Shared RDSR (WIP) poll, used after both SE and PP -- which one
// is in progress, and what to do once WIP clears, is tracked by
// `save_phase` (below) rather than duplicated poll logic.
localparam ST_RDSR_ISSUE = 5'd15;
localparam ST_RDSR_BYTE = 5'd16; // waiting for the single response byte
localparam ST_RDSR_DWAIT = 5'd17; // wait for RDSR's own fm_done
reg [4:0] state;
reg [23:0] remaining;
reg [23:0] cur_flash_addr;
reg [PSRAM_ADDR_WIDTH-1:0] cur_psram_addr;
// F3 (SAVE) bookkeeping
localparam SAVE_PHASE_ERASE = 1'b0;
localparam SAVE_PHASE_PROG = 1'b1;
reg save_phase;
reg [24:0] save_end; // flash_addr + len, one extra bit of headroom
reg [23:0] erase_addr; // current sector cursor during the erase phase
reg [23:0] prog_addr; // current flash address cursor during the program phase
reg [PSRAM_ADDR_WIDTH-1:0] prog_psram_addr; // current PSRAM source cursor
reg [23:0] prog_remaining; // bytes left to program overall
reg wip_busy; // RDSR-1 bit0, captured by the poll
reg erase_only; // F5 (DIR_ERASE): stop after the erase phase, no program phase
assign busy = (state != ST_IDLE);
// d_req must drop the SAME cycle d_ready is observed, not wait
// for the state transition out of ST_PSRAM_WAIT (which only
// takes effect the following cycle): mem_arbiter's grant of Port
// D also completes (owner -> SEL_NONE) that same cycle, and its
// SEL_NONE case is combinational logic re-evaluated that very
// cycle -- if d_req were still 1 (as it would be with a plain
// `state == ST_PSRAM_WAIT` here, since `state` itself hasn't
// updated yet), the arbiter would immediately re-grant Port D a
// SECOND time using stale d_addr/d_wdata, which this engine is
// no longer driving meaningfully. Found during F2 bring-up (see
// WORKLOG.md) as a hang after the last byte of a LOAD. Same
// reasoning applies to F3's SAVE-side PSRAM source reads
// (ST_SAVE_PROG_PWAIT) -- same Port D, same arbiter, same race.
assign d_req = ((state == ST_PSRAM_WAIT) || (state == ST_SAVE_PROG_PWAIT)) && !d_ready;
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
done <= 1'b0;
err <= 1'b0;
fm_start <= 1'b0;
fm_opcode <= 8'h00;
fm_has_addr <= 1'b0;
fm_addr <= 24'h0;
fm_dir <= 2'd0;
fm_n_data <= 16'h0;
fm_rdata_ack <= 1'b0;
d_wr <= 1'b0;
d_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
d_wdata <= 8'sd0;
remaining <= 24'h0;
cur_flash_addr <= 24'h0;
cur_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fm_wdata <= 8'h00;
fm_wdata_valid <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
save_end <= 25'h0;
erase_addr <= 24'h0;
prog_addr <= 24'h0;
prog_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
prog_remaining <= 24'h0;
wip_busy <= 1'b0;
erase_only <= 1'b0;
end else begin
fm_start <= 1'b0;
fm_rdata_ack <= 1'b0;
fm_wdata_valid <= 1'b0;
done <= 1'b0;
case (state)
// --------------------------------------------
ST_IDLE: begin
if (op_start) begin
if (op_dir == DIR_ERASE) begin
// F5: standalone sector erase. `len` and
// `psram_addr` are not meaningful here
// (ignored) -- only flash_addr's own
// sector-alignment and range matter.
if ( ({8'h0, flash_addr} + {8'h0, SECTOR_BYTES}) > {7'h0, FLASH_SPACE_BYTES} ||
flash_addr[11:0] != 12'h000
) begin
err <= 1'b1;
done <= 1'b1;
end else begin
err <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
erase_addr <= flash_addr;
save_end <= {1'b0, flash_addr} + {1'b0, SECTOR_BYTES}; // exactly one sector
erase_only <= 1'b1;
state <= ST_SAVE_ERASE_WREN;
end
// §A.3 negative cases (LOAD/SAVE): length
// pushes either address space past its own
// size (compared in generously-wide 32b
// temporaries so no same-width overflow can
// hide the very condition being checked
// for); an unknown direction; and, for SAVE
// only, a non-sector-aligned flash_addr (the
// design decision documented in the module
// header).
end else if ( ({8'h0, flash_addr} + {8'h0, len}) > {7'h0, FLASH_SPACE_BYTES} ||
({{(32-PSRAM_ADDR_WIDTH){1'b0}}, psram_addr} + {8'h0, len})
> (32'h1 << PSRAM_ADDR_WIDTH) ||
len == 24'h0 ||
(op_dir != DIR_LOAD && op_dir != DIR_SAVE) ||
(op_dir == DIR_SAVE && flash_addr[11:0] != 12'h000)
) begin
err <= 1'b1;
done <= 1'b1;
end else if (op_dir == DIR_LOAD) begin
err <= 1'b0;
remaining <= len;
cur_flash_addr <= flash_addr;
cur_psram_addr <= psram_addr;
state <= ST_CHUNK_ISSUE;
end else begin // DIR_SAVE
err <= 1'b0;
erase_only <= 1'b0;
save_phase <= SAVE_PHASE_ERASE;
save_end <= {1'b0, flash_addr} + {1'b0, len};
erase_addr <= flash_addr;
prog_addr <= flash_addr;
prog_psram_addr <= psram_addr;
prog_remaining <= len;
state <= ST_SAVE_ERASE_WREN;
end
end
end
// --------------------------------------------
ST_CHUNK_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_READ;
fm_has_addr <= 1'b1;
fm_addr <= cur_flash_addr;
fm_dir <= FM_DIR_READ;
fm_n_data <= (remaining > {8'h0, CHUNK_MAX}) ? CHUNK_MAX[15:0] : remaining[15:0];
state <= ST_CHUNK_WAIT;
end
// --------------------------------------------
// Services one byte at a time: spi_flash_master
// pauses (holds CS low, sclk idle) with rdata_valid
// asserted until fm_rdata_ack; meanwhile this state
// issues the matching PSRAM write and waits for
// d_ready before acking, giving natural backpressure
// -- the flash is never read faster than PSRAM can
// absorb.
// --------------------------------------------
ST_CHUNK_WAIT: begin
if (fm_rdata_valid) begin
d_wr <= 1'b1;
d_addr <= cur_psram_addr;
d_wdata <= $signed(fm_rdata);
state <= ST_PSRAM_WAIT; // d_req becomes 1 combinationally, see assign above
end else if (fm_done) begin
// Chunk's spi_flash_master transaction fully
// complete (`remaining` already decremented,
// one per byte, in ST_PSRAM_WAIT below --
// for each byte of THIS chunk). Zero means
// the whole request is done; nonzero means
// another chunk is still needed.
if (remaining == 24'h0) begin
state <= ST_DONE;
end else begin
state <= ST_CHUNK_ISSUE;
end
end
end
// --------------------------------------------
ST_PSRAM_WAIT: begin
if (d_ready) begin
fm_rdata_ack <= 1'b1;
cur_flash_addr <= cur_flash_addr + 24'd1;
cur_psram_addr <= cur_psram_addr + 1'b1;
remaining <= remaining - 24'd1;
state <= ST_CHUNK_WAIT;
end
end
// --------------------------------------------
ST_DONE: begin
done <= 1'b1;
state <= ST_IDLE;
end
// ============================================
// F3 (SAVE): erase phase
// ============================================
ST_SAVE_ERASE_WREN: begin
fm_start <= 1'b1;
fm_opcode <= OP_WREN;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_ERASE_WWAIT;
end
ST_SAVE_ERASE_WWAIT: begin
if (fm_done)
state <= ST_SAVE_ERASE_ISSUE;
end
ST_SAVE_ERASE_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_SE;
fm_has_addr <= 1'b1;
fm_addr <= erase_addr;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_ERASE_EWAIT;
end
ST_SAVE_ERASE_EWAIT: begin
// SE's own fm_done only means the command was
// clocked in, NOT that the erase has physically
// completed -- §2.1/§2.4 of the phase-plan: must
// poll RDSR-1's WIP bit next.
if (fm_done) begin
save_phase <= SAVE_PHASE_ERASE;
state <= ST_RDSR_ISSUE;
end
end
// ============================================
// F3 (SAVE): program phase, one page (<=256B) at a
// time, each individually WREN'd and WIP-polled.
// ============================================
ST_SAVE_PROG_WREN: begin
fm_start <= 1'b1;
fm_opcode <= OP_WREN;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_NONE;
fm_n_data <= 16'd0;
state <= ST_SAVE_PROG_WWAIT;
end
ST_SAVE_PROG_WWAIT: begin
if (fm_done)
state <= ST_SAVE_PROG_ISSUE;
end
ST_SAVE_PROG_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_PP;
fm_has_addr <= 1'b1;
fm_addr <= prog_addr;
fm_dir <= FM_DIR_WRITE;
// Never crosses a 256B page boundary: prog_addr
// is always page-aligned when a page starts
// (flash_addr was required sector-, hence page-,
// aligned at DIR_SAVE dispatch, and every page
// before this one consumed exactly PAGE_BYTES
// bytes -- only the LAST page of the whole
// request can be shorter).
fm_n_data <= (prog_remaining > PAGE_BYTES) ? PAGE_BYTES[15:0] : prog_remaining[15:0];
state <= ST_SAVE_PROG_BYTE;
end
// Services spi_flash_master's wdata_req one byte at
// a time by reading the next PSRAM source byte
// (Port D) and handing it straight through -- no
// local page buffer, same "stream through" style as
// the LOAD path. prog_addr/prog_psram_addr/
// prog_remaining are advanced per-byte in
// ST_SAVE_PROG_PWAIT below, so by the time fm_done
// fires here they already reflect the post-page
// state (mirrors ST_PSRAM_WAIT's own bookkeeping on
// the LOAD side).
ST_SAVE_PROG_BYTE: begin
if (fm_wdata_req) begin
d_wr <= 1'b0;
d_addr <= prog_psram_addr;
state <= ST_SAVE_PROG_PWAIT;
end else if (fm_done) begin
save_phase <= SAVE_PHASE_PROG;
state <= ST_RDSR_ISSUE;
end
end
ST_SAVE_PROG_PWAIT: begin
if (d_ready) begin
fm_wdata <= d_rdata;
fm_wdata_valid <= 1'b1;
prog_psram_addr <= prog_psram_addr + 1'b1;
prog_addr <= prog_addr + 24'd1;
prog_remaining <= prog_remaining - 24'd1;
state <= ST_SAVE_PROG_BYTE;
end
end
// ============================================
// Shared RDSR-1 (WIP) poll, used after both SE and
// PP. `save_phase` (latched by the caller just
// before entering here) decides what "WIP cleared"
// means next: advance to the next sector / finish
// the erase phase, or advance to the next page /
// finish the whole SAVE.
// ============================================
ST_RDSR_ISSUE: begin
fm_start <= 1'b1;
fm_opcode <= OP_RDSR1;
fm_has_addr <= 1'b0;
fm_dir <= FM_DIR_READ;
fm_n_data <= 16'd1;
state <= ST_RDSR_BYTE;
end
ST_RDSR_BYTE: begin
if (fm_rdata_valid) begin
wip_busy <= fm_rdata[0]; // RDSR-1 bit0 = BUSY/WIP, sim/flash_model.v header
fm_rdata_ack <= 1'b1;
state <= ST_RDSR_DWAIT;
end
end
ST_RDSR_DWAIT: begin
if (fm_done) begin
if (wip_busy) begin
state <= ST_RDSR_ISSUE; // still busy: poll again
end else if (save_phase == SAVE_PHASE_ERASE) begin
erase_addr <= erase_addr + SECTOR_BYTES;
if (({1'b0, erase_addr} + {1'b0, SECTOR_BYTES}) >= save_end) begin
// Erase phase covers the whole requested
// range: F5's standalone DIR_ERASE stops
// here (erase_only); DIR_SAVE falls
// through to programming.
state <= erase_only ? ST_DONE : ST_SAVE_PROG_WREN;
end else begin
state <= ST_SAVE_ERASE_WREN; // next sector
end
end else begin // SAVE_PHASE_PROG
if (prog_remaining == 24'h0) begin
state <= ST_DONE;
end else begin
state <= ST_SAVE_PROG_WREN; // next page
end
end
end
end
default: state <= ST_IDLE;
endcase
end
end
endmodule