Files
FPGA-Neural/hardware/v1/sim/flash_latency_bench.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

208 lines
8.5 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// FLASH_LATENCY_BENCH -- Phase F6 real-timing measurement
//
// NOT a pass/fail testbench -- a benchmark, matching the style of
// sim/graph_engine_bandwidth_tb.v (numbers reported, checked only
// against the datasheet's own cited values, not a design oracle).
//
// METHODOLOGY (declared per §A.5 -- "ogni numero misurato: con come
// e' stato misurato e contro cosa"):
//
// Simulating the FULL real-time WIP wait (tSE=400ms / tPP=3ms MAX,
// §9.6 p.90) with flash_copy_engine's actual RDSR poll loop running
// at real SPI clock rates was ATTEMPTED FIRST and abandoned: at
// real timing, one RDSR poll transaction takes on the order of a
// microsecond, so covering a 400ms wait means on the order of
// 100,000+ discrete poll transactions, each many simulated clock
// edges -- tens of millions of Icarus events, which did not finish
// in reasonable wall-clock time (killed after >60s with no result).
// This is a SIMULATOR PERFORMANCE limit, not a hardware one (real
// silicon polling costs no wall-clock time at all) -- worth stating
// explicitly rather than silently switching approach.
//
// Approach actually used: measure the SPI-clock-bound "issue"
// phase directly in simulation (accurate regardless of
// flash_model.v's TIME_SCALE, since that parameter only scales the
// POST-issue WIP-wait delay, not the bit-shift timing itself) --
// stopping the clock the instant the flash model latches
// pending_pp/pending_se (i.e. the command has been fully clocked in
// and the flash has started its own internal write/erase) -- then
// ADD the datasheet's own cited MAX duration for the wait itself.
// This is the same total latency a real polling host would see,
// decomposed into a directly-measured part (SPI overhead) and a
// directly-cited part (flash-internal timing), rather than forcing
// a single simulated number that costs more than it's worth to
// obtain honestly.
//
// One representative RDSR poll transaction's own SPI time (needed
// once, to detect the eventual WIP=0) is computed the same way as
// every other transaction below: opcode(8b)+data(8b) = 16 bits.
//
// Representative operations (matching what the catalog design
// actually produces, phase-plan §4/§9):
// ERASE: one 4KB sector.
// SAVE: 256 bytes (one Page Program, sector-aligned -- includes
// its own internal erase, per F3's design).
// LOAD: 4096 bytes (one sector's worth -- READ has NO WIP wait
// at all, §8 intro p.24, so this number is purely SPI-
// clock-rate-bound and needs no analytical addition).
// ================================================================
module tb;
parameter CLK_FREQ_MHZ = 80; // overridden via -P from the command line
localparam CLK_PERIOD_NS = 1000.0 / CLK_FREQ_MHZ;
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD_NS / 2.0) clk = ~clk;
end
wire mosi, miso, cs_n, sclk_w;
reg op_start;
reg [1:0] op_dir;
reg [23:0] flash_addr;
reg [22:0] psram_addr;
reg [23:0] len;
wire busy, done, err;
wire d_req, d_wr;
wire [22:0] d_addr;
wire signed [7:0] d_wdata;
reg signed [7:0] d_rdata;
reg d_ready;
localparam DIR_LOAD = 2'd0;
localparam DIR_SAVE = 2'd1;
localparam DIR_ERASE = 2'd2;
flash_copy_engine #(
.PSRAM_ADDR_WIDTH(23), .CLK_FREQ_MHZ(CLK_FREQ_MHZ), .SCLK_DIV(2)
) dut (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n), .sclk(sclk_w),
.op_start(op_start), .op_dir(op_dir),
.flash_addr(flash_addr), .psram_addr(psram_addr), .len(len),
.busy(busy), .done(done), .err(err),
.d_req(d_req), .d_wr(d_wr), .d_addr(d_addr), .d_wdata(d_wdata),
.d_rdata(d_rdata), .d_ready(d_ready)
);
// Compressed TIME_SCALE (same 100000x as every other testbench in
// this project) -- safe here because we stop measuring BEFORE the
// scaled WIP-wait delay even starts (see do_op_until_issued below);
// the datasheet's real MAX duration is added back analytically.
flash_model #(.DEPTH(32'h0002_0000), .TIME_SCALE(100000)) dut_flash (
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
reg [7:0] fake_ram [0:8191];
always @(posedge clk) begin
d_ready <= d_req;
if (d_req && d_wr) fake_ram[d_addr[12:0]] <= d_wdata;
d_rdata <= fake_ram[d_addr[12:0]];
end
// Measures from op_start to the moment the flash model's own
// pending_pp/pending_se latches (command fully issued, WIP just
// started) -- NOT to `done` (which would include the compressed,
// not-representative-of-real-hardware TIME_SCALE'd wait).
task automatic do_op_until_issued(
input [1:0] p_dir, input [23:0] p_flash_addr, input [23:0] p_len, output real ns_elapsed
);
real t0;
begin
t0 = $realtime;
@(posedge clk);
op_start <= 1'b1;
op_dir <= p_dir;
flash_addr <= p_flash_addr;
psram_addr <= 23'h0;
len <= p_len;
@(posedge clk);
op_start <= 1'b0;
@(posedge dut_flash.pending_se or posedge dut_flash.pending_pp);
ns_elapsed = $realtime - t0;
// Let this op actually finish (compressed timing, fast)
// before starting the next one.
while (!done) @(posedge clk);
end
endtask
// LOAD has no WIP wait at all -- measure straight to `done`.
task automatic do_op_full(
input [1:0] p_dir, input [23:0] p_flash_addr, input [23:0] p_len, output real ns_elapsed
);
real t0;
begin
t0 = $realtime;
@(posedge clk);
op_start <= 1'b1;
op_dir <= p_dir;
flash_addr <= p_flash_addr;
psram_addr <= 23'h0;
len <= p_len;
@(posedge clk);
op_start <= 1'b0;
while (!done) @(posedge clk);
ns_elapsed = $realtime - t0;
end
endtask
real t_erase_issue, t_save_issue, t_load_full;
real rdsr_poll_ns;
real erase_total, save_total;
// One representative RDSR poll transaction: opcode(8b)+data(8b)
// = 16 bits, same bit-clock rate as every other transaction here
// -- computed from the SAME CLK_PERIOD_NS/SCLK_DIV=2 this bench
// itself uses (2*SCLK_DIV=4 clk cycles/bit, spi_flash_master.v's
// own divider), not a separate assumption.
initial rdsr_poll_ns = 16.0 * 4.0 * CLK_PERIOD_NS;
initial begin
rst = 1'b1;
op_start = 1'b0; op_dir = DIR_LOAD; flash_addr = 24'h0; psram_addr = 23'h0; len = 24'h0;
d_ready = 1'b0; d_rdata = 8'sd0;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
do_op_until_issued(DIR_ERASE, 24'h010000, 24'd0, t_erase_issue);
do_op_until_issued(DIR_SAVE, 24'h011000, 24'd256, t_save_issue);
do_op_full(DIR_LOAD, 24'h010000, 24'd4096, t_load_full);
// Datasheet MAX (worst-case) values, §9.6 p.90, already cited
// in sim/flash_model.v: tSE_MAX=400ms, tPP_MAX=3ms.
erase_total = t_erase_issue + 400_000_000.0 + rdsr_poll_ns;
save_total = t_save_issue + 400_000_000.0 + rdsr_poll_ns // SAVE's own internal erase
+ 3_000_000.0 + rdsr_poll_ns; // then its Page Program
$display("CLK_FREQ_MHZ=%0d SCLK_DIV=2:", CLK_FREQ_MHZ);
$display(" RDSR poll transaction = %0.3f us (measured bit-clock rate)", rdsr_poll_ns/1000.0);
$display(" ERASE issue (WREN+SE, measured) = %0.3f us", t_erase_issue/1000.0);
$display(" ERASE total (issue + tSE_MAX + 1 poll, tSE_MAX cited \302\247A.5) = %0.3f ms", erase_total/1_000_000.0);
$display(" SAVE issue (WREN+SE, measured, save's own erase phase) = %0.3f us", t_save_issue/1000.0);
$display(" SAVE total (256B page, incl. its own erase; issue + tSE_MAX + tPP_MAX + 2 polls) = %0.3f ms", save_total/1_000_000.0);
$display(" LOAD total (4096B, measured directly -- READ has no WIP wait) = %0.3f ms, effective bandwidth = %0.3f MB/s",
t_load_full/1_000_000.0, 4096.0 / (t_load_full/1000.0));
$finish;
end
initial begin
#100_000_000; // compressed-scale ops finish in well under this
$display("FATAL: unexpected timeout");
$finish;
end
endmodule