Files
FPGA-Neural/sim/flash_model.v
T
micheleandClaude Sonnet 5 97a21be240 feat: flash boot/persistence subsystem (SPI master, copy engine, CRC32 slot catalog)
Adds FPGA-exclusive access to the onboard W25Q128JV SPI NOR flash for
weights/bias/network persistence, layered as spi_flash_master (raw SPI,
USRMCLK-driven) -> flash_copy_engine (flash<->PSRAM streaming, erase-
before-write, Page Program loop) -> flash_slot_manager (16-slot catalog
with CRC32), exposed via 8 new SPI opcodes (0x40-0x47). Fixes two
pre-existing bugs found during bring-up: a psram_controller.v request
lost during power-up, and a one-cycle-pulse race in the PSRAM arbiter
request handshake. Full simulation + real Yosys/nextpnr-ecp5 synthesis
verification (0 errors, Fmax 66.68MHz) in WORKLOG.md and
docs/FPGA-Neural-Flash-Subsystem-Verification.md.

Also updates docs/pinout to reflect the 56-signal real .lpf (3 new
flash pins) and documents the WRITE_RAM/READ_RAM host backpressure
risk found while testing this subsystem.

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

327 lines
15 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_MODEL - behavioral model of the Winbond W25Q128JV(-DTR)
// SPI NOR flash, SIMULATION ONLY (not synthesizable, not intended
// to be instantiated anywhere but a testbench).
//
// This is an independent-oracle support model for F1-F6 of the
// flash subsystem (see WORKLOG.md): it exists so RTL testbenches
// have something to talk to that enforces the datasheet's write
// rules in hardware-like fashion (erase-to-FF, program-can-only-
// clear-bits, WIP timing, 256B page-program wraparound), rather
// than a trivial RAM that would silently accept violations the
// real chip would corrupt on. It is NOT the oracle for "is the
// design's behavior correct" (that oracle is the Python reference
// model + datasheet numbers, per WORKLOG.md §A.1) -- it is the
// thing-under-test's counterparty, playing the role of the flash.
//
// Unlike rtl/spi_slave.v (which crosses an async SPI clock into
// the FPGA's own `clk` domain via double-flop sync), this model has
// no domain of its own to defend -- it IS the SPI-clocked device,
// so sampling/driving directly on posedge/negedge of `sclk` (as the
// real chip's internal logic does) is the correct and simplest
// idiom here, not a shortcut.
//
// Datasheet: local copy at
// /Users/michelebigi/Development/HubAudio/datasheets/W25Q128JVS.pdf
// title page identifies it as "W25Q128JV-DTR" (Double Transfer Rate
// variant). All rules modeled here come from that document's
// STANDARD SPI instruction set (§8.1.2 Table 1, p.26) and AC
// Electrical Characteristics (§9.6, p.90), which are shared across
// the whole W25Q128JV family (DTR adds extra instructions/modes on
// top; it does not change the base standard-SPI behavior modeled
// here) -- see the JEDEC ID note below for the one confirmed
// discrepancy between this PDF and the actual populated part.
//
// Modeled (datasheet citations inline at point of use below):
// - RDID (9Fh) §8.2.30 p.70 -- manufacturer/type/capacity, 3 bytes
// - READ (03h) Table 1 p.26 -- sequential byte read, address wraps
// at the modeled DEPTH boundary
// - WREN (06h) §8.2.1 p.30 -- sets WEL
// - PP (02h) Table 1 p.26, -- program (AND-only, never sets bits),
// note 3 p.29 wraps to page start past byte 256
// - SE (20h) Table 1 p.26 -- 4KB sector erase -> all-FF
// - RDSR-1(05h) §7.1.1/7.1.2 -- bit0=BUSY(WIP), bit1=WEL (TOC order
// p.15 on p.2 lists BUSY before WEL; this
// bit assignment is also universal
// across the whole Winbond SPI-NOR
// family -- flagged as the one bit-
// layout fact taken from convention
// rather than a single explicit
// consolidated bit table in this
// particular local PDF, see WORKLOG.md
// F1 entry)
// - tPP/tSE timing: §9.6 p.90, MAX (worst-case) values used, scaled
// by TIME_SCALE (see parameter) so simulation doesn't burn real
// milliseconds of event-queue time. Using MAX rather than TYP is
// deliberate: a design that only polls WIP a couple of times
// (tuned to the typical-case latency) must still pass against the
// worst case, or it is not actually correct.
//
// NOT modeled (explicit limitation, see WORKLOG.md §A.6):
// - Fast Read / Dual / Quad / QPI / DTR instructions (out of scope
// for this design's F1, which only uses standard single-SPI).
// - Status Register-2/3, block/sector protect bits, individual
// block locks, /WP, /HOLD, power-down (B9h/ABh) -- the FPGA has
// exclusive, trusted control of this flash, so write protection
// is out of scope; this model always accepts writes once WEL is
// set, like a factory-default (WPS=0, all BP bits 0) part.
// - Analog/electrical timing (rise/fall times, setup/hold margins,
// signal integrity) -- not reachable in a digital behavioral model.
// - True mid-operation power-loss: real NOR flash can leave a page
// PARTIALLY programmed if power is cut mid-instruction. This
// model's commit is a single non-blocking assignment burst after
// the modeled tPP/tSE delay, so the power-loss hook (below) can
// only represent "operation aborted before it committed at all",
// not "committed halfway" -- still sufficient to exercise the
// catalog's CRC+valid_flag rejection path (F4), but coarser than
// real silicon.
// ================================================================
module flash_model #(
parameter DEPTH = 32'h0002_0000, // 128KB modeled (32x 4KB sectors).
// §A.6: NOT the real 16MB (2^24) --
// mirrors sim/psram_model.v's own
// precedent of a reduced DEPTH for
// simulation speed. Covers the
// catalog sector + several data
// sectors for F2-F5 without the
// multi-second iverilog elaboration
// cost a full 16MB reg array would add.
parameter integer TIME_SCALE = 100000 // divides real ms-scale datasheet
// timing (see tPP/tSE below) into
// simulation-friendly ns
)(
input wire sclk,
input wire mosi,
output reg miso,
input wire cs_n
);
localparam [31:0] SECTOR_BYTES = 32'd4096; // Sector Erase (4KB), Table 1 p.26 "20h"
// ------------------------------------------------------------
// JEDEC ID: EF4018h (plain, non-DTR W25Q128JV), NOT the 7018h
// this DTR-titled PDF's own §8.1.1 table (p.24) shows.
//
// ASSUMPTION FLAGGED (§A.1/§A.6): docs/FPGA-Neural-Hardware-Design.md
// and docs/FPGA-Neural-Datapatch-Benchmark.md both specify the
// populated part as "W25Q128JV" / "W25Q128JVS" -- WITHOUT the
// -DTR suffix -- and those hardware docs are the confirmed BOM
// source (per project memory). The plain (non-DTR) part's
// publicly-documented JEDEC ID is EF4018h (memory type 40h);
// this local PDF is for the DTR-capable die (memory type 70h),
// a different part number that happens to share the same base
// filename. Rather than silently trusting whichever byte the
// one local PDF prints, this model uses the value that matches
// the part actually specified in the project's own hardware
// docs, and flags the mismatch explicitly (see WORKLOG.md F1
// entry) -- real hardware bring-up MUST confirm the populated
// chip's actual RDID response against this constant.
// ------------------------------------------------------------
localparam [7:0] JEDEC_MFR = 8'hEF;
localparam [7:0] JEDEC_MEMTYPE = 8'h40;
localparam [7:0] JEDEC_CAPACITY = 8'h18; // 128Mbit -- §8.1.1 p.24, agrees between DTR/non-DTR
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_READ = 8'h03;
localparam [7:0] OP_PP = 8'h02;
localparam [7:0] OP_SE = 8'h20;
localparam [7:0] OP_RDSR1 = 8'h05;
localparam [7:0] OP_RDID = 8'h9F;
reg [7:0] mem [0:DEPTH-1];
integer i;
initial begin
// Erase state is 0xFF everywhere -- §8.2.18 p.56: Sector
// Erase sets every bit in the addressed region to 1. A
// freshly-elaborated model matches a freshly-blanked part.
for (i = 0; i < DEPTH; i = i + 1)
mem[i] = 8'hFF;
end
reg wel;
reg busy;
reg [7:0] opcode;
reg [23:0] addr;
reg [31:0] bit_count; // total bits received since CS fell
reg [7:0] shift_out;
reg pending_pp, pending_se;
reg [23:0] pending_addr;
reg [8:0] pp_nbytes;
reg [7:0] pp_bytes [0:255];
localparam realtime TPP_MAX_NS = 3_000_000.0 / TIME_SCALE; // 3ms MAX, §9.6 p.90
localparam realtime TSE_MAX_NS = 400_000_000.0 / TIME_SCALE; // 400ms MAX, §9.6 p.90
initial begin
wel = 1'b0; busy = 1'b0; miso = 1'b0;
pending_pp = 1'b0; pending_se = 1'b0; bit_count = 0;
end
// ============================================================
// Transaction start: reset framing state. Every instruction is
// exactly one CS-low period (§8 intro, p.24: "Instructions are
// initiated with the falling edge of Chip Select").
// ============================================================
always @(negedge cs_n) begin
bit_count <= 0;
opcode <= 8'h00;
end
// ============================================================
// MOSI sampled on the rising edge of CLK (mode 0), matching
// every timing diagram in the datasheet (Fig. 7/28/30/43a).
// ============================================================
always @(posedge sclk) begin
if (!cs_n) begin
if (bit_count < 8) begin
opcode <= {opcode[6:0], mosi};
end else if (bit_count < 32) begin
addr <= {addr[22:0], mosi};
end else begin
// Data phase: PP captures incoming bytes; READ's
// outgoing bytes are handled entirely in the
// falling-edge block below (this model does not
// echo MOSI during READ, matching "DI High
// Impedance" shown for read-type instructions).
if (opcode == OP_PP && wel) begin
pp_bytes[(bit_count - 32) >> 3][7 - ((bit_count - 32) & 3'h7)] <= mosi;
end
end
bit_count <= bit_count + 1;
end
end
// ============================================================
// MISO driven on the falling edge of CLK, one bit ahead of the
// next rising-edge sample (mode 0), exactly as every read-type
// timing diagram in the datasheet shows (e.g. Fig. 43a: DO
// changes shortly after each falling edge).
// ============================================================
always @(negedge sclk) begin
if (!cs_n) begin
case (opcode)
OP_RDID: begin
if (bit_count < 16) miso <= JEDEC_MFR[15 - bit_count];
else if (bit_count < 24) miso <= JEDEC_MEMTYPE[23 - bit_count];
else if (bit_count < 32) miso <= JEDEC_CAPACITY[31 - bit_count];
else miso <= 1'b0;
end
OP_RDSR1: begin
// bit0=BUSY, bit1=WEL -- see header note.
if (bit_count < 8) miso <= 1'b0;
else case ((bit_count - 8) & 3'h7)
3'd7: miso <= busy; // MSB-first shift-out of {6'b0,WEL,BUSY}
3'd6: miso <= wel;
default: miso <= 1'b0;
endcase
end
OP_READ: begin
if (bit_count < 32) begin
miso <= 1'b0;
end else if (bit_count == 32) begin
if (addr >= DEPTH) begin
$display("[flash_model] FATAL: READ out of modeled range 0x%06h (DEPTH=0x%06h) at t=%0t", addr, DEPTH, $time);
$fatal(1);
end
shift_out <= mem[addr];
miso <= mem[addr][7];
end else if (((bit_count - 32) & 3'h7) == 0) begin
shift_out <= mem[(addr + ((bit_count - 32) >> 3)) % DEPTH];
miso <= mem[(addr + ((bit_count - 32) >> 3)) % DEPTH][7];
end else begin
shift_out <= {shift_out[6:0], 1'b0};
miso <= shift_out[6];
end
end
default: miso <= 1'b0;
endcase
end
end
// ============================================================
// Transaction end: byte-boundary-gated side effects (§8 intro,
// p.24: writes/erases that don't end on a byte boundary are
// ignored entirely -- modeled by requiring bit_count to be an
// exact multiple of 8 with the right minimum length below).
// ============================================================
always @(posedge cs_n) begin
if (!busy && opcode == OP_WREN && bit_count == 8) begin
wel <= 1'b1;
end
if (!busy && wel && opcode == OP_PP
&& bit_count >= 40 && ((bit_count - 32) & 3'h7) == 0) begin
pp_nbytes <= (bit_count - 32) >> 3;
pending_addr <= addr;
pending_pp <= 1'b1;
busy <= 1'b1;
wel <= 1'b0;
end
if (!busy && wel && opcode == OP_SE && bit_count == 32) begin
if (addr >= DEPTH || addr[11:0] != 12'h000) begin
// §A.3 negative test: Sector Erase to an address not
// aligned to a 4KB sector boundary. Real Winbond
// parts silently erase the containing sector (low
// address bits don't-care); this model instead
// FATALs so a design bug that relies on that
// silent behavior (rather than aligning addresses
// itself, per WORKLOG.md's erase-before-write design
// decision) is caught, not masked.
$display("[flash_model] FATAL: Sector Erase to unaligned/out-of-range address 0x%06h at t=%0t", addr, $time);
$fatal(1);
end
pending_addr <= addr;
pending_se <= 1'b1;
busy <= 1'b1;
wel <= 1'b0;
end
end
// ============================================================
// WIP timers: commit the write/erase after the modeled MAX
// (worst-case) datasheet duration. Committing all at once here
// (rather than incrementally) is what makes the power-loss hook
// below meaningful: forcing `pending_pp`/`pending_se` and `busy`
// low from a testbench (via hierarchical reference) before this
// block's delay expires means the commit loop never runs at all
// -- see the NOT-modeled note in the header for the fidelity
// limit of that hook.
// ============================================================
always @(posedge pending_pp) begin
# (TPP_MAX_NS);
if (pending_pp) begin // still armed: not aborted by a power-loss hook
for (i = 0; i < pp_nbytes; i = i + 1)
mem[pending_addr + i] <= mem[pending_addr + i] & pp_bytes[i]; // AND-only, Table1/§8.2.16 p.53
busy <= 1'b0;
end
pending_pp <= 1'b0;
end
always @(posedge pending_se) begin
# (TSE_MAX_NS);
if (pending_se) begin
for (i = 0; i < SECTOR_BYTES; i = i + 1)
mem[pending_addr + i] <= 8'hFF; // erase -> all-ones, §8.2.18 p.56
busy <= 1'b0;
end
pending_se <= 1'b0;
end
endmodule