Files
FPGA-Neural/hardware/v1/rtl/flash_slot_manager.v
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

652 lines
29 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_SLOT_MANAGER
//
// Fixed-slot catalog layer on top of rtl/flash_copy_engine.v (F2/F3,
// reused UNCHANGED -- zero modifications, zero regression risk to
// its already-verified LOAD/SAVE paths). This is the module the
// flash-subsystem phase-plan's §4 describes: NOT a filesystem --
// a small fixed-size table mapping slot_id -> (offset, length,
// type, valid, CRC), read at boot and resolved before every
// LOAD_SLOT/SAVE_SLOT, no dynamic allocation, no GC.
//
// ----------------------------------------------------------------
// Catalog layout (matches tools/flash_catalog/oracle.py exactly --
// that script is the independent §A.1 oracle for both the byte
// layout and the CRC32 value; kept in sync by hand, cross-checked
// by the testbenches comparing actual bytes, not by either side
// trusting the other's prose)
// ----------------------------------------------------------------
// - N_SLOTS = 16 entries x ENTRY_BYTES = 16 bytes = 256 bytes
// total, comfortably inside one 4KB sector (CATALOG_SECTOR_ADDR,
// flash sector 0 -- RESERVED, never used for slot data).
// - Per-entry layout, MSB-first: offset[24b] | length[24b] |
// type[8b] | valid[8b] (0x01=valid, else invalid) | crc32[32b]
// | reserved[32b, always 0].
// - A freshly-erased catalog sector is all-0xFF, so every
// unwritten slot decodes as valid=0xFF != 0x01 => invalid,
// without needing any format/init step -- matches flash_model.v
// and the real Winbond erase behavior (§8.2.18 p.56).
//
// ----------------------------------------------------------------
// How the catalog gets to/from flash without touching PSRAM as user
// data (design decision, documented per the phase-plan's own
// "traccia ogni scelta" requirement):
// ----------------------------------------------------------------
// flash_copy_engine already knows how to move bytes flash<->PSRAM
// (F2/F3). Rather than teach it a THIRD destination (on-chip
// registers), this module stages the 256-byte catalog through a
// small reserved PSRAM region (CATALOG_PSRAM_ADDR) using
// flash_copy_engine's EXISTING, unmodified DIR_LOAD/DIR_SAVE, then
// itself reads/writes that staging region byte-by-byte via the
// SAME Port D connection flash_copy_engine uses -- safe because
// the two uses are TEMPORALLY DISJOINT by construction (this
// module's own sequential FSM never drives Port D itself while an
// internal flash_copy_engine operation is in flight, and vice
// versa): a static mux on `fce_busy` selects the owner, no new
// arbiter port needed.
// ASSUMPTION flagged (§A.5): CATALOG_PSRAM_ADDR is a convention,
// not enforced anywhere else -- nothing else in this design may
// use that PSRAM range while a catalog operation is in flight.
//
// ----------------------------------------------------------------
// CRC32 (rtl/crc32.v, IEEE 802.3/zlib -- verified independently
// against tools/flash_catalog/oracle.py + a textbook check-value,
// see sim/crc32_tb.v / WORKLOG.md's F4 entry) is computed by TAPPING
// flash_copy_engine's own Port D traffic while it is the active
// owner during a LOAD_SLOT or SAVE_SLOT (fce_d_wdata for a LOAD's
// PSRAM writes, fce_d_rdata for a SAVE's PSRAM reads) -- again zero
// modification to flash_copy_engine.v itself.
// ----------------------------------------------------------------
//
// Command interface:
// op_start, op_code (OP_CAT_READ / OP_CAT_WRITE_SLOT /
// OP_LOAD_SLOT / OP_SAVE_SLOT), slot_id, and per-opcode fields
// below. busy/done/err mirror flash_copy_engine's own convention.
//
// OP_CAT_READ: reloads the on-chip catalog register file from the
// flash catalog sector (e.g. at boot, or to force a refresh).
// OP_CAT_WRITE_SLOT: registers/updates slot_id's (offset, length,
// type) in the on-chip table, marks it INVALID (no verified
// data behind it yet -- SAVE_SLOT is what marks it valid), and
// persists the WHOLE catalog table to flash.
// OP_LOAD_SLOT(slot_id, psram_addr): resolves slot_id's offset+
// length from the on-chip catalog; if invalid, `err`+`done`
// immediately, no flash/PSRAM touched. Otherwise streams the
// slot's data flash->PSRAM (reusing flash_copy_engine's DIR_LOAD
// verbatim) while recomputing the CRC32 live; if it does not
// match the catalog's stored CRC, `err` is asserted (the data
// still lands in PSRAM -- the host is being told "unreliable",
// not given a transactional rollback, matching this project's
// existing STATUS-flag error convention rather than introducing
// a new one).
// OP_SAVE_SLOT(slot_id, psram_addr, length): resolves slot_id's
// already-registered offset (from a prior CAT_WRITE_SLOT);
// streams `length` bytes PSRAM->flash at that offset (reusing
// flash_copy_engine's DIR_SAVE verbatim) while computing the
// CRC32 live; on completion updates the on-chip entry (length,
// crc, valid=1) and persists the whole catalog to flash.
// ================================================================
module flash_slot_manager #(
parameter PSRAM_ADDR_WIDTH = 23,
parameter CLK_FREQ_MHZ = 80,
parameter SCLK_DIV = 2,
parameter [PSRAM_ADDR_WIDTH-1:0] CATALOG_PSRAM_ADDR = {PSRAM_ADDR_WIDTH{1'b0}}
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// Physical flash pins (owns flash_copy_engine, which owns
// spi_flash_master -- same ownership chain as F2/F3)
// ------------------------------------------------------------
output wire mosi,
input wire miso,
output wire cs_n,
output wire sclk, // ordinary GPIO, real in both sim and synthesis
// ------------------------------------------------------------
// Command interface. op_code 0-3 are the F4 catalog/slot ops;
// 4-6 (added in F5) are raw, non-slot, non-CRC block ops that
// just forward straight to the internal flash_copy_engine --
// the phase-plan's §5 FLASH_READ_BLOCK/FLASH_WRITE_BLOCK/
// FLASH_ERASE, which take an explicit flash address rather than
// resolving one from the catalog.
// ------------------------------------------------------------
input wire op_start,
input wire [2:0] op_code,
input wire [3:0] slot_id,
input wire [23:0] new_offset, // CAT_WRITE_SLOT
input wire [23:0] new_length, // CAT_WRITE_SLOT
input wire [7:0] new_type, // CAT_WRITE_SLOT
input wire [PSRAM_ADDR_WIDTH-1:0] ext_psram_addr, // LOAD_SLOT / SAVE_SLOT / raw ops
input wire [23:0] ext_length, // SAVE_SLOT / raw block ops
input wire [23:0] raw_flash_addr, // FLASH_READ_BLOCK / FLASH_WRITE_BLOCK / FLASH_ERASE
output wire busy,
output reg done, // one-cycle pulse
output reg err, // held until next op_start
// ------------------------------------------------------------
// Catalog inspection (combinational read port, e.g. for a
// future CAT_READ SPI response -- F5 -- or this module's own
// testbench)
// ------------------------------------------------------------
input wire [3:0] cat_read_sel,
output wire [23:0] cat_out_offset,
output wire [23:0] cat_out_length,
output wire [7:0] cat_out_type,
output wire cat_out_valid,
output wire [31:0] cat_out_crc,
// ------------------------------------------------------------
// PSRAM arbiter master port (mem_arbiter.v Port D) -- muxed
// between the internal flash_copy_engine's own use and this
// module's own catalog-staging-buffer access, see header.
// ------------------------------------------------------------
output wire d_req,
output wire d_wr,
output wire [PSRAM_ADDR_WIDTH-1:0] d_addr,
output wire signed [7:0] d_wdata,
input wire signed [7:0] d_rdata,
input wire d_ready
);
localparam OP_CAT_READ = 3'd0;
localparam OP_CAT_WRITE_SLOT = 3'd1;
localparam OP_LOAD_SLOT = 3'd2;
localparam OP_SAVE_SLOT = 3'd3;
localparam OP_FLASH_READ_BLOCK = 3'd4; // F5, raw: flash -> PSRAM, explicit flash_addr
localparam OP_FLASH_WRITE_BLOCK = 3'd5; // F5, raw: PSRAM -> flash, explicit flash_addr
localparam OP_FLASH_ERASE = 3'd6; // F5, raw: standalone sector erase
localparam N_SLOTS = 16;
localparam ENTRY_BYTES = 16;
localparam CATALOG_BYTES = N_SLOTS * ENTRY_BYTES; // 256
localparam [23:0] CATALOG_SECTOR_ADDR = 24'h000000; // reserved, see header
localparam [7:0] VALID_MARK = 8'h01;
// ============================================================
// On-chip catalog register file
// ============================================================
reg [23:0] cat_offset [0:N_SLOTS-1];
reg [23:0] cat_length [0:N_SLOTS-1];
reg [7:0] cat_type [0:N_SLOTS-1];
reg cat_valid [0:N_SLOTS-1];
reg [31:0] cat_crc [0:N_SLOTS-1];
assign cat_out_offset = cat_offset[cat_read_sel];
assign cat_out_length = cat_length[cat_read_sel];
assign cat_out_type = cat_type[cat_read_sel];
assign cat_out_valid = cat_valid[cat_read_sel];
assign cat_out_crc = cat_crc[cat_read_sel];
// ============================================================
// flash_copy_engine instance (F2/F3 primitive, unmodified)
// ============================================================
reg fce_start;
reg [1:0] fce_dir;
reg [23:0] fce_flash_addr;
reg [PSRAM_ADDR_WIDTH-1:0] fce_psram_addr;
reg [23:0] fce_len;
wire fce_busy, fce_done, fce_err;
wire fce_d_req, fce_d_wr;
wire [PSRAM_ADDR_WIDTH-1:0] fce_d_addr;
wire signed [7:0] fce_d_wdata;
localparam FCE_DIR_LOAD = 2'd0;
localparam FCE_DIR_SAVE = 2'd1;
localparam FCE_DIR_ERASE = 2'd2;
flash_copy_engine #(
.PSRAM_ADDR_WIDTH(PSRAM_ADDR_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ),
.SCLK_DIV(SCLK_DIV)
) u_fce (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk(sclk),
.op_start(fce_start), .op_dir(fce_dir),
.flash_addr(fce_flash_addr), .psram_addr(fce_psram_addr), .len(fce_len),
.busy(fce_busy), .done(fce_done), .err(fce_err),
.d_req(fce_d_req), .d_wr(fce_d_wr), .d_addr(fce_d_addr), .d_wdata(fce_d_wdata),
.d_rdata(d_rdata), .d_ready(d_ready)
);
// ============================================================
// Port D mux: fce owns it whenever busy; otherwise this module
// drives it directly for catalog-staging-buffer access. See
// header for why this is safe without a new arbiter port.
// ============================================================
reg fsm_d_req;
reg fsm_d_wr;
reg [PSRAM_ADDR_WIDTH-1:0] fsm_d_addr;
reg signed [7:0] fsm_d_wdata;
assign d_req = fce_busy ? fce_d_req : fsm_d_req;
assign d_wr = fce_busy ? fce_d_wr : fsm_d_wr;
assign d_addr = fce_busy ? fce_d_addr : fsm_d_addr;
assign d_wdata = fce_busy ? fce_d_wdata : fsm_d_wdata;
// ============================================================
// CRC32 accumulator, tapped from fce's own Port D traffic while
// it is the active owner (see header). Active only during
// LOAD_SLOT/SAVE_SLOT's data-transfer phase (`crc_active`).
// ============================================================
reg crc_active;
reg [31:0] crc_acc;
wire [31:0] crc_next;
wire [7:0] crc_tap_byte = fce_d_wr ? fce_d_wdata : d_rdata; // LOAD writes vs SAVE reads
crc32_byte u_crc32 (
.crc_in(crc_acc),
.data(crc_tap_byte),
.crc_out(crc_next)
);
// NOTE: fce's own `d_req` is combinationally defined (in
// flash_copy_engine.v) as `(waiting_state) && !d_ready` -- so
// `fce_d_req && d_ready` is a contradiction, always false, and
// would never fire. The correct "a byte transfer through fce
// just completed" condition is simply `fce_busy && d_ready`: at
// that exact cycle fce's own state hasn't advanced out of its
// waiting state yet (same one-cycle-late FSM update reasoning
// documented in flash_copy_engine.v), so fce_busy is still 1,
// and this is the only reason d_ready would be high while fce
// owns Port D (see the mux above).
always @(posedge clk) begin
if (rst) begin
crc_acc <= 32'hFFFFFFFF;
end else if (crc_active && fce_busy && d_ready) begin
crc_acc <= crc_next;
end else if (!crc_active) begin
crc_acc <= 32'hFFFFFFFF; // re-armed for the next operation
end
end
// ============================================================
// Main FSM
// ============================================================
localparam ST_IDLE = 4'd0;
localparam ST_CATRD_FCE_GO = 4'd1;
localparam ST_CATRD_FCE_WAIT = 4'd2;
localparam ST_CATRD_RD_ISSUE = 4'd3;
localparam ST_CATRD_RD_WAIT = 4'd4;
localparam ST_CATWR_SER_ISSUE = 4'd5;
localparam ST_CATWR_SER_WAIT = 4'd6;
localparam ST_CATWR_FCE_GO = 4'd7;
localparam ST_CATWR_FCE_WAIT = 4'd8;
localparam ST_SLOT_FCE_GO = 4'd9;
localparam ST_SLOT_FCE_WAIT = 4'd10;
localparam ST_SLOT_CATWR_KICK = 4'd11; // SAVE_SLOT only: fall into the CAT_WRITE_SLOT persist sequence
localparam ST_DONE = 4'd15;
reg [3:0] state;
reg [8:0] byte_idx; // 0..255, position within the 256B catalog
reg [3:0] cur_slot; // slot_id of the entry currently being (de)serialized
reg [127:0] entry_shift; // 16-byte (de)serialization shift register, see header
reg [2:0] active_op;
reg [3:0] active_slot;
integer rst_i;
assign busy = (state != ST_IDLE);
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
done <= 1'b0;
err <= 1'b0;
fce_start <= 1'b0;
fce_dir <= 2'd0;
fce_flash_addr <= 24'h0;
fce_psram_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fce_len <= 24'h0;
fsm_d_req <= 1'b0;
fsm_d_wr <= 1'b0;
fsm_d_addr <= {PSRAM_ADDR_WIDTH{1'b0}};
fsm_d_wdata <= 8'sd0;
crc_active <= 1'b0;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
entry_shift <= 128'h0;
active_op <= 2'd0;
active_slot <= 4'h0;
for (rst_i = 0; rst_i < N_SLOTS; rst_i = rst_i + 1) begin
cat_offset[rst_i] <= 24'h0;
cat_length[rst_i] <= 24'h0;
cat_type[rst_i] <= 8'h0;
cat_valid[rst_i] <= 1'b0;
cat_crc[rst_i] <= 32'h0;
end
end else begin
fce_start <= 1'b0;
fsm_d_req <= 1'b0;
done <= 1'b0;
case (state)
// ========================================
ST_IDLE: begin
if (op_start) begin
active_op <= op_code;
active_slot <= slot_id;
case (op_code)
OP_CAT_READ: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= CATALOG_SECTOR_ADDR;
fce_psram_addr <= CATALOG_PSRAM_ADDR;
fce_len <= CATALOG_BYTES[23:0];
state <= ST_CATRD_FCE_WAIT;
end
OP_CAT_WRITE_SLOT: begin
err <= 1'b0;
cat_offset[slot_id] <= new_offset;
cat_length[slot_id] <= new_length;
cat_type[slot_id] <= new_type;
cat_valid[slot_id] <= 1'b0; // no verified data yet, see header
cat_crc[slot_id] <= 32'h0;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
state <= ST_CATWR_SER_ISSUE;
end
OP_LOAD_SLOT: begin
if (!cat_valid[slot_id]) begin
err <= 1'b1;
done <= 1'b1;
end else begin
err <= 1'b0;
crc_active <= 1'b1;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= cat_offset[slot_id];
fce_psram_addr <= ext_psram_addr;
fce_len <= cat_length[slot_id];
state <= ST_SLOT_FCE_WAIT;
end
end
OP_SAVE_SLOT: begin
err <= 1'b0;
crc_active <= 1'b1;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= cat_offset[slot_id]; // registered by a prior CAT_WRITE_SLOT
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
// F5: raw block ops, no catalog/CRC
// involvement at all -- straight pass-
// through to flash_copy_engine with an
// explicit flash address from the host.
OP_FLASH_READ_BLOCK: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_LOAD;
fce_flash_addr <= raw_flash_addr;
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
OP_FLASH_WRITE_BLOCK: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= raw_flash_addr;
fce_psram_addr <= ext_psram_addr;
fce_len <= ext_length;
state <= ST_SLOT_FCE_WAIT;
end
OP_FLASH_ERASE: begin
err <= 1'b0;
fce_start <= 1'b1;
fce_dir <= FCE_DIR_ERASE;
fce_flash_addr <= raw_flash_addr;
state <= ST_SLOT_FCE_WAIT;
end
default: begin
err <= 1'b1;
done <= 1'b1;
end
endcase
end
end
// ========================================
// CAT_READ: load the catalog sector into the
// PSRAM staging buffer (fce, unmodified), then
// parse it 16 bytes (one entry) at a time.
// ========================================
ST_CATRD_FCE_WAIT: begin
if (fce_done) begin
byte_idx <= 9'h0;
state <= ST_CATRD_RD_ISSUE;
end
end
ST_CATRD_RD_ISSUE: begin
fsm_d_req <= 1'b1;
fsm_d_wr <= 1'b0;
fsm_d_addr <= CATALOG_PSRAM_ADDR + byte_idx;
state <= ST_CATRD_RD_WAIT;
end
ST_CATRD_RD_WAIT: begin
if (d_ready) begin
if (byte_idx[3:0] == 4'hF) begin
// 16th byte of this entry (bytes 12-15
// are reserved/ignored, so `d_rdata`
// itself -- this 16th byte -- never
// actually feeds any decoded field, only
// the PRE-update `entry_shift`, which at
// this point holds bytes 0-14 from the
// 15 shifts so far: entry_shift[119:112]
// = byte0 ... entry_shift[7:0] = byte14.
// See the module header for the full
// derivation.
cat_offset[byte_idx[8:4]] <= entry_shift[119:96]; // bytes 0-2
cat_length[byte_idx[8:4]] <= entry_shift[95:72]; // bytes 3-5
cat_type[byte_idx[8:4]] <= entry_shift[71:64]; // byte 6
cat_valid[byte_idx[8:4]] <= (entry_shift[63:56] == VALID_MARK); // byte 7
cat_crc[byte_idx[8:4]] <= entry_shift[55:24]; // bytes 8-11
end
entry_shift <= {entry_shift[119:0], d_rdata};
if (byte_idx == 9'd255) begin
state <= ST_DONE;
end else begin
byte_idx <= byte_idx + 9'd1;
state <= ST_CATRD_RD_ISSUE;
end
end
end
// ========================================
// CAT_WRITE_SLOT: serialize all N_SLOTS entries
// (from the just-updated on-chip table) into the
// PSRAM staging buffer, then persist via fce's
// unmodified DIR_SAVE (erase + page-program loop
// + WIP poll, all F3).
// ========================================
ST_CATWR_SER_ISSUE: begin
fsm_d_req <= 1'b1;
fsm_d_wr <= 1'b1;
fsm_d_addr <= CATALOG_PSRAM_ADDR + byte_idx;
// Byte to send: byte_idx[3:0] selects which of the
// 16 bytes of cur_slot's entry, MSB-first, same
// layout as the CAT_READ decode.
case (byte_idx[3:0])
4'h0: fsm_d_wdata <= cat_offset[cur_slot][23:16];
4'h1: fsm_d_wdata <= cat_offset[cur_slot][15:8];
4'h2: fsm_d_wdata <= cat_offset[cur_slot][7:0];
4'h3: fsm_d_wdata <= cat_length[cur_slot][23:16];
4'h4: fsm_d_wdata <= cat_length[cur_slot][15:8];
4'h5: fsm_d_wdata <= cat_length[cur_slot][7:0];
4'h6: fsm_d_wdata <= cat_type[cur_slot];
4'h7: fsm_d_wdata <= cat_valid[cur_slot] ? VALID_MARK : 8'h00;
4'h8: fsm_d_wdata <= cat_crc[cur_slot][31:24];
4'h9: fsm_d_wdata <= cat_crc[cur_slot][23:16];
4'hA: fsm_d_wdata <= cat_crc[cur_slot][15:8];
4'hB: fsm_d_wdata <= cat_crc[cur_slot][7:0];
default: fsm_d_wdata <= 8'h00; // reserved, bytes C-F
endcase
state <= ST_CATWR_SER_WAIT;
end
ST_CATWR_SER_WAIT: begin
if (d_ready) begin
if (byte_idx == 9'd255) begin
fce_start <= 1'b1;
fce_dir <= FCE_DIR_SAVE;
fce_flash_addr <= CATALOG_SECTOR_ADDR;
fce_psram_addr <= CATALOG_PSRAM_ADDR;
fce_len <= CATALOG_BYTES[23:0];
state <= ST_CATWR_FCE_WAIT;
end else begin
byte_idx <= byte_idx + 9'd1;
cur_slot <= (byte_idx[3:0] == 4'hF) ? (byte_idx[8:4] + 4'd1) : byte_idx[8:4];
state <= ST_CATWR_SER_ISSUE;
end
end
end
ST_CATWR_FCE_WAIT: begin
// Reached either directly from a CAT_WRITE_SLOT
// op, or via ST_SLOT_CATWR_KICK after a
// SAVE_SLOT's own data transfer already updated
// the catalog entry -- either way, this state is
// just "persist the catalog sector, then done".
if (fce_done)
state <= ST_DONE;
end
// ========================================
// LOAD_SLOT / SAVE_SLOT: run fce's existing
// DIR_LOAD/DIR_SAVE verbatim, CRC accumulated via
// the tap above.
// ========================================
ST_SLOT_FCE_WAIT: begin
if (fce_done) begin
crc_active <= 1'b0;
case (active_op)
OP_LOAD_SLOT: begin
// fce_err here would mean the
// catalog's own (offset,length) is
// somehow out of range -- shouldn't
// happen for a slot that passed the
// CAT_WRITE_SLOT/SAVE_SLOT bounds
// checks, but checked anyway rather
// than assumed.
if (fce_err || ((crc_acc ^ 32'hFFFFFFFF) != cat_crc[active_slot]))
err <= 1'b1;
state <= ST_DONE;
end
OP_SAVE_SLOT: begin
// BUG FIXED HERE (found during F5
// integration review, before ever
// running the F5 testbench): this
// branch previously updated+persisted
// the catalog as "valid" unconditionally
// on fce_done, without checking
// fce_err first -- a SAVE_SLOT whose
// underlying flash_copy_engine call
// itself failed (e.g. an out-of-range
// length) would still have been
// marked valid in the catalog. Now
// skips the catalog update entirely
// on a failed transfer.
if (fce_err) begin
err <= 1'b1;
state <= ST_DONE;
end else begin
cat_length[active_slot] <= fce_len;
cat_crc[active_slot] <= crc_acc ^ 32'hFFFFFFFF;
cat_valid[active_slot] <= 1'b1;
byte_idx <= 9'h0;
cur_slot <= 4'h0;
state <= ST_SLOT_CATWR_KICK;
end
end
default: begin
// F5 raw block ops (FLASH_READ_BLOCK/
// FLASH_WRITE_BLOCK/FLASH_ERASE): no
// catalog/CRC involvement, just
// forward fce's own error status.
err <= fce_err;
state <= ST_DONE;
end
endcase
end
end
// One-cycle bridge: the cat_* writes above need a
// cycle to land before ST_CATWR_SER_ISSUE reads them
// back out for serialization.
ST_SLOT_CATWR_KICK: begin
state <= ST_CATWR_SER_ISSUE;
end
// ========================================
ST_DONE: begin
done <= 1'b1;
state <= ST_IDLE;
end
default: state <= ST_IDLE;
endcase
end
end
endmodule