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
609 lines
28 KiB
Verilog
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
|