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
208 lines
8.5 KiB
Verilog
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
|