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

681 lines
27 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP FLASH-SUBSYSTEM END-TO-END TESTBENCH (Phase F5)
//
// Same real-stack BFM style as sim/spi_neuron_top_graph_tb.v (SPI
// master bit-banging tasks copied verbatim from there), driving the
// FULL integrated top level -- spi_slave + spi_engine + graph_engine
// + flash_slot_manager (-> flash_copy_engine -> spi_flash_master) +
// mem_arbiter + int8_memory_access + memory_interface +
// psram_controller + psram_model + flash_model -- purely over
// simulated host SPI and simulated flash SPI.
//
// TEST 1: FLASH_ERASE + FLASH_WRITE_BLOCK + FLASH_READ_BLOCK
// opcode-level smoke test over real SPI framing (deep
// correctness of these primitives already covered by
// sim/flash_copy_engine_erase_tb.v and
// sim/flash_slot_manager_raw_tb.v -- this proves the SPI byte
// framing in spi_engine.v decodes them correctly, which those
// module-level tests cannot).
// TEST 2: CAT_WRITE_SLOT + CAT_READ + CAT_INSPECT over real SPI.
// TEST 3 (adversarial §A.3): LOAD_SLOT on a never-saved slot ->
// STATUS.bit3 (flash_err) observed over real SPI.
// TEST 4 (the phase-plan's explicit §6 end-to-end requirement):
// netasm -> WRITE_RAM -> SAVE_SLOT -> (PSRAM region overwritten
// with garbage, proving the reload is real) -> LOAD_SLOT ->
// RUN_NETWORK -> READ_RAM matches the hand-computed expected
// output (126), the SAME independently-derived value already
// used in sim/graph_engine_tb.v / sim/spi_neuron_top_graph_tb.v
// (spec §3's worked example, x=[10,1,4,0] -> n4=49, n5=126) --
// not re-derived here, reused as the existing independent
// oracle. The exact WRITE_RAM/SET_BASE/RUN_NETWORK byte sequence
// below was generated by netasm itself (not hand-typed):
// python3 tools/netasm/cli.py tools/netasm/examples/graph_example.netasm \
// -o /tmp/netasm_out/graph --table-base 0x000000 \
// --edges-base 0x000100 --x-base 0x000400 --out-base 0x000500
// (see that command's own .debug.txt output, quoted inline below
// at each step for traceability).
// ================================================================
module tb;
localparam ADDR_WIDTH = 23;
localparam DATA_WIDTH = 8;
localparam N_INPUTS = 4;
localparam N_NEURONS = 4;
localparam PARALLEL = 2;
localparam ACC_WIDTH = 32;
localparam MEM_DATA_WIDTH = 16;
localparam N_LAYERS = 4;
localparam GRAPH_MAX_CONN = 4;
localparam GRAPH_N_TOTAL = 4096;
localparam CLK_PERIOD = 12.5; // 80 MHz
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
reg sclk;
reg mosi;
wire miso;
reg cs_n;
wire flash_mosi, flash_miso, flash_cs_n, flash_sclk;
wire [ADDR_WIDTH-1:0] psram_a;
wire [MEM_DATA_WIDTH-1:0] psram_dq;
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
spi_neuron_top #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH),
.MEM_DATA_WIDTH(MEM_DATA_WIDTH),
.CLK_FREQ_MHZ(80),
.N_LAYERS(N_LAYERS),
.GRAPH_MAX_CONN(GRAPH_MAX_CONN),
.GRAPH_N_TOTAL(GRAPH_N_TOTAL)
) dut (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.flash_mosi(flash_mosi), .flash_miso(flash_miso), .flash_cs_n(flash_cs_n),
.flash_sclk(flash_sclk),
.psram_a(psram_a), .psram_dq(psram_dq),
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
);
psram_model #(
.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(MEM_DATA_WIDTH), .DEPTH(16384)
) u_psram (
.clk(clk), .a(psram_a), .dq(psram_dq),
.ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n),
.lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n)
);
flash_model #(
.DEPTH(32'h0002_0000), .TIME_SCALE(100000)
) u_flash (
.sclk(flash_sclk), .mosi(flash_mosi), .miso(flash_miso), .cs_n(flash_cs_n)
);
// ============================================================
// SPI MASTER BFM (identical to sim/spi_neuron_top_graph_tb.v)
// ============================================================
task clk_wait;
input integer n;
integer k;
begin
for (k = 0; k < n; k = k + 1)
@(posedge clk);
end
endtask
task spi_begin;
input integer half_bit_cycles;
begin
cs_n = 1'b1;
sclk = 1'b0;
mosi = 1'b0;
clk_wait(half_bit_cycles * 2);
cs_n = 1'b0;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_end;
input integer half_bit_cycles;
begin
clk_wait(half_bit_cycles * 2);
cs_n = 1'b1;
clk_wait(half_bit_cycles * 2);
end
endtask
task spi_xfer_byte;
input [7:0] tx;
input integer half_bit_cycles;
output [7:0] rx;
integer i;
reg [7:0] rx_acc;
begin
rx_acc = 8'h00;
for (i = 7; i >= 0; i = i - 1) begin
mosi = tx[i];
clk_wait(half_bit_cycles);
sclk = 1'b1;
rx_acc[i] = miso;
clk_wait(half_bit_cycles);
sclk = 1'b0;
clk_wait(half_bit_cycles);
end
rx = rx_acc;
end
endtask
localparam HB_RAM = 40;
localparam HB_REG = 8;
reg [7:0] rx_tmp;
integer errors;
integer poll_count;
reg signed [7:0] payload [0:63];
reg signed [7:0] readback [0:63];
// ============================================================
// HELPER TASKS (existing opcodes, same convention as
// sim/spi_neuron_top_graph_tb.v)
// ============================================================
task do_reset;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h0F, HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task set_net_type;
input [7:0] t;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h11, HB_REG, rx_tmp);
spi_xfer_byte(t, HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task set_base;
input [7:0] sel;
input [ADDR_WIDTH-1:0] addr;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h10, HB_REG, rx_tmp);
spi_xfer_byte(sel, HB_REG, rx_tmp);
spi_xfer_byte(addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(addr[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task write_ram_bytes;
input [ADDR_WIDTH-1:0] addr;
input integer len;
integer k;
begin
spi_begin(HB_RAM);
spi_xfer_byte(8'h01, HB_RAM, rx_tmp);
spi_xfer_byte(addr[23:16], HB_RAM, rx_tmp);
spi_xfer_byte(addr[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(addr[7:0], HB_RAM, rx_tmp);
spi_xfer_byte(len[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(len[7:0], HB_RAM, rx_tmp);
for (k = 0; k < len; k = k + 1)
spi_xfer_byte(payload[k], HB_RAM, rx_tmp);
spi_end(HB_RAM);
end
endtask
task read_ram_bytes;
input [ADDR_WIDTH-1:0] addr;
input integer len;
integer k;
begin
spi_begin(HB_RAM);
spi_xfer_byte(8'h02, HB_RAM, rx_tmp);
spi_xfer_byte(addr[23:16], HB_RAM, rx_tmp);
spi_xfer_byte(addr[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(addr[7:0], HB_RAM, rx_tmp);
spi_xfer_byte(len[15:8], HB_RAM, rx_tmp);
spi_xfer_byte(len[7:0], HB_RAM, rx_tmp);
for (k = 0; k < len; k = k + 1)
spi_xfer_byte(8'h00, HB_RAM, readback[k]);
spi_end(HB_RAM);
end
endtask
task read_status;
output [7:0] status;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h21, HB_REG, rx_tmp);
spi_xfer_byte(8'h00, HB_REG, status);
spi_end(HB_REG);
end
endtask
task run_network;
input [7:0] payload_byte;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h23, HB_REG, rx_tmp);
spi_xfer_byte(payload_byte, HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
reg [7:0] last_status;
// Polls until STATUS.bit1(done) OR bit2(graph_err) OR bit3(flash_err)
// latches, or timeout -- covers both inference and flash completions.
task wait_done_or_err;
begin
poll_count = 0;
last_status = 8'h00;
while (!last_status[1] && !last_status[2] && !last_status[3] && poll_count < 200000) begin
clk_wait(20);
read_status(last_status);
poll_count = poll_count + 1;
end
end
endtask
task read_output_bytes;
input integer n;
integer k;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h22, HB_REG, rx_tmp);
for (k = 0; k < n; k = k + 1)
spi_xfer_byte(8'h00, HB_REG, readback[k]);
spi_end(HB_REG);
end
endtask
// ============================================================
// HELPER TASKS (new F5 flash opcodes)
// ============================================================
task flash_erase;
input [23:0] sector_addr;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h42, HB_REG, rx_tmp); // FLASH_ERASE
spi_xfer_byte(sector_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(sector_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(sector_addr[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task flash_write_block;
input [ADDR_WIDTH-1:0] psram_addr;
input [23:0] flash_addr;
input [23:0] len;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h41, HB_REG, rx_tmp); // FLASH_WRITE_BLOCK
spi_xfer_byte(psram_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[7:0], HB_REG, rx_tmp);
spi_xfer_byte(flash_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(flash_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(flash_addr[7:0], HB_REG, rx_tmp);
spi_xfer_byte(len[23:16], HB_REG, rx_tmp);
spi_xfer_byte(len[15:8], HB_REG, rx_tmp);
spi_xfer_byte(len[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task flash_read_block;
input [23:0] flash_addr;
input [ADDR_WIDTH-1:0] psram_addr;
input [23:0] len;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h40, HB_REG, rx_tmp); // FLASH_READ_BLOCK
spi_xfer_byte(flash_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(flash_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(flash_addr[7:0], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[7:0], HB_REG, rx_tmp);
spi_xfer_byte(len[23:16], HB_REG, rx_tmp);
spi_xfer_byte(len[15:8], HB_REG, rx_tmp);
spi_xfer_byte(len[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task cat_write_slot;
input [3:0] slot;
input [23:0] offset;
input [23:0] length;
input [7:0] slot_type;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h44, HB_REG, rx_tmp); // CAT_WRITE_SLOT
spi_xfer_byte({4'h0, slot}, HB_REG, rx_tmp);
spi_xfer_byte(offset[23:16], HB_REG, rx_tmp);
spi_xfer_byte(offset[15:8], HB_REG, rx_tmp);
spi_xfer_byte(offset[7:0], HB_REG, rx_tmp);
spi_xfer_byte(length[23:16], HB_REG, rx_tmp);
spi_xfer_byte(length[15:8], HB_REG, rx_tmp);
spi_xfer_byte(length[7:0], HB_REG, rx_tmp);
spi_xfer_byte(slot_type, HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task load_slot;
input [3:0] slot;
input [ADDR_WIDTH-1:0] psram_addr;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h45, HB_REG, rx_tmp); // LOAD_SLOT
spi_xfer_byte({4'h0, slot}, HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task save_slot;
input [3:0] slot;
input [ADDR_WIDTH-1:0] psram_addr;
input [23:0] length;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h46, HB_REG, rx_tmp); // SAVE_SLOT
spi_xfer_byte({4'h0, slot}, HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[23:16], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[15:8], HB_REG, rx_tmp);
spi_xfer_byte(psram_addr[7:0], HB_REG, rx_tmp);
spi_xfer_byte(length[23:16], HB_REG, rx_tmp);
spi_xfer_byte(length[15:8], HB_REG, rx_tmp);
spi_xfer_byte(length[7:0], HB_REG, rx_tmp);
spi_end(HB_REG);
end
endtask
task cat_read;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h43, HB_REG, rx_tmp); // CAT_READ
spi_end(HB_REG);
end
endtask
task cat_inspect;
input [3:0] slot;
begin
spi_begin(HB_REG);
spi_xfer_byte(8'h47, HB_REG, rx_tmp); // CAT_INSPECT
spi_xfer_byte({4'h0, slot}, HB_REG, rx_tmp);
spi_xfer_byte(8'h00, HB_REG, readback[0]); // offset[23:16]
spi_xfer_byte(8'h00, HB_REG, readback[1]); // offset[15:8]
spi_xfer_byte(8'h00, HB_REG, readback[2]); // offset[7:0]
spi_xfer_byte(8'h00, HB_REG, readback[3]); // length[23:16]
spi_xfer_byte(8'h00, HB_REG, readback[4]); // length[15:8]
spi_xfer_byte(8'h00, HB_REG, readback[5]); // length[7:0]
spi_xfer_byte(8'h00, HB_REG, readback[6]); // type
spi_xfer_byte(8'h00, HB_REG, readback[7]); // valid
spi_xfer_byte(8'h00, HB_REG, readback[8]); // crc[31:24]
spi_xfer_byte(8'h00, HB_REG, readback[9]); // crc[23:16]
spi_xfer_byte(8'h00, HB_REG, readback[10]); // crc[15:8]
spi_xfer_byte(8'h00, HB_REG, readback[11]); // crc[7:0]
spi_end(HB_REG);
end
endtask
task check_byte;
input [255:0] label;
input signed [7:0] got, exp;
begin
if (got !== exp) begin
$display("FAIL: %0s got=%0d exp=%0d", label, got, exp);
errors = errors + 1;
end
end
endtask
integer i;
initial begin
errors = 0;
rst = 1'b1;
sclk = 1'b0; mosi = 1'b0; cs_n = 1'b1;
repeat (5) @(posedge clk);
rst = 1'b0;
// Wait for psram_controller's own power-up (STATE_INIT +
// STATE_CR_INIT, ~150us+ @ 80MHz) to fully complete before
// the FIRST WRITE_RAM/READ_RAM -- same established pattern
// sim/spi_neuron_top_graph_tb.v and friends already use
// (`wait (dut.u_psram_ctrl.state == ...STATE_IDLE)`), just
// not one this file had copied initially.
//
// REAL FINDING (see WORKLOG.md's F5 entry): WRITE_RAM/
// READ_RAM have NO backpressure to the SPI master (documented
// as a known "v1 limitation" in spi_engine.v's own header --
// predates this session). Skipping this wait does not hang or
// error -- it SILENTLY CORRUPTS DATA: the host's un-
// backpressured SPI clocking drifts ahead of spi_engine while
// spi_engine is stuck waiting the FULL ~150us for the very
// first PSRAM access to complete, and bytes received during
// that wait are dropped without any error signaled. Verified
// directly with a minimal WRITE_RAM-only reproduction with NO
// flash opcodes involved at all -- this is a general PSRAM
// access hazard, not specific to the flash subsystem, but
// real host software (and every testbench touching PSRAM)
// MUST account for it explicitly.
wait (dut.u_psram_ctrl.state == dut.u_psram_ctrl.STATE_IDLE);
do_reset;
// ========================================================
// TEST 1: FLASH_ERASE + FLASH_WRITE_BLOCK + FLASH_READ_BLOCK
// ========================================================
$display("--- TEST 1 starting ---");
flash_erase(24'h010000);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST1 erase unexpected flash_err"); errors = errors + 1; end
for (i = 0; i < 16; i = i + 1)
payload[i] = 8'h40 + i;
write_ram_bytes(23'h000800, 16);
flash_write_block(23'h000800, 24'h010000, 24'd16);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST1 write_block unexpected flash_err"); errors = errors + 1; end
flash_read_block(24'h010000, 23'h000900, 24'd16);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST1 read_block unexpected flash_err"); errors = errors + 1; end
read_ram_bytes(23'h000900, 16);
for (i = 0; i < 16; i = i + 1)
check_byte("TEST1 FLASH_WRITE_BLOCK/READ_BLOCK round-trip", readback[i], 8'h40 + i);
// ========================================================
// TEST 2: CAT_WRITE_SLOT + CAT_READ + CAT_INSPECT
// ========================================================
$display("--- TEST 2 starting ---");
cat_write_slot(4'd2, 24'h011000, 24'd0, 8'h05);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST2 cat_write_slot unexpected flash_err"); errors = errors + 1; end
cat_read; // force a fresh reload from flash
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST2 cat_read unexpected flash_err"); errors = errors + 1; end
cat_inspect(4'd2);
check_byte("TEST2 CAT_INSPECT offset[23:16]", readback[0], 8'h01);
check_byte("TEST2 CAT_INSPECT offset[15:8]", readback[1], 8'h10);
check_byte("TEST2 CAT_INSPECT offset[7:0]", readback[2], 8'h00);
check_byte("TEST2 CAT_INSPECT type", readback[6], 8'h05);
check_byte("TEST2 CAT_INSPECT valid (never saved)", readback[7], 8'h00);
// ========================================================
// TEST 3 (adversarial §A.3): LOAD_SLOT on a never-saved slot
// ========================================================
$display("--- TEST 3 starting ---");
load_slot(4'd2, 23'h000A00); // slot 2 registered but never SAVE_SLOT'd
wait_done_or_err;
if (!last_status[3]) begin $display("FAIL: TEST3 expected flash_err for never-saved slot, got none"); errors = errors + 1; end
// ========================================================
// TEST 4: netasm -> WRITE_RAM -> SAVE_SLOT -> LOAD_SLOT ->
// RUN_NETWORK, independent oracle = 126 (spec §3 worked
// example, already used in sim/graph_engine_tb.v /
// sim/spi_neuron_top_graph_tb.v).
// ========================================================
$display("--- TEST 4 starting ---");
do_reset;
// From netasm's own debug dump, regenerated TWICE during
// bring-up (see WORKLOG.md's F5 entry for both findings):
// 1) --parallel 2 --max-conn 4, to MATCH this testbench's
// own RTL parameters (PARALLEL=2 above -- same as
// sim/spi_neuron_top_graph_tb.v, whose header explains
// why: n_conn=2 for both neurons exactly equals
// PARALLEL=2, so n_conn_padded=2, no padding needed).
// The default --parallel (8) instead produces padded=8
// edge blocks this GRAPH_MAX_CONN=4 build cannot
// correctly consume.
// 2) --table-base/--edges-base matching where the blob is
// ACTUALLY placed in PSRAM below (0x002000/0x002100).
// netasm bakes each neuron's edge-block address into the
// descriptor table as an ABSOLUTE PSRAM address AT
// COMPILE TIME (not an offset relative to table_base) --
// placing the compiled blob at a different PSRAM address
// than the one netasm was told about leaves those
// embedded pointers stale, silently pointing at
// whatever (unrelated, here all-zero) bytes happen to
// sit at the ORIGINAL address instead of the real edges.
// This does not error or hang -- graph_engine reads
// zeroed/garbage edges and computes a wrong (here, 0)
// result with STATUS reporting a completely normal,
// error-free completion. First discovered by getting
// output=0 instead of 126 with table/edges compiled for
// table_base=0x000000 but placed at 0x002000 (needed
// anyway to dodge the CATALOG_PSRAM_ADDR collision noted
// below) -- every other signal (STATUS, CRC, byte-exact
// PSRAM/flash content checks) looked perfectly correct,
// which is exactly why this is worth calling out: wrong
// base-address arguments to a code generator can produce
// a fully "successful", fully wrong result with no
// hardware-visible symptom at all.
// Final, correct invocation:
// python3 tools/netasm/cli.py tools/netasm/examples/graph_example.netasm \
// -o /tmp/netasm_out3/graph --parallel 2 --max-conn 4 \
// --table-base 0x002000 --edges-base 0x002100 --x-base 0x000400 --out-base 0x000500
//
// Addresses AS GENERATED (table_base=0x002000):
// descriptor table (22 bytes) @ 0x002000
// edges for n4 (8 bytes) @ 0x002100
// edges for n5 (8 bytes) @ 0x002108
// -> spans 0x002000-0x00210F (0x110 = 272 bytes), with an
// unused gap between the table and the edges (harmless,
// included verbatim in the SAVE_SLOT/CRC below -- a slot's
// saved range need not be semantically packed, only byte-
// exact on round-trip).
//
// Placed at PSRAM 0x002000 specifically (not netasm's own
// default 0x000000) because 0x000000 collides with
// flash_slot_manager's own CATALOG_PSRAM_ADDR staging region
// (default, also 0x000000) -- confirmed by direct trace
// during bring-up (see WORKLOG.md's F5 entry): the catalog's
// own serialization step during SAVE_SLOT's persist phase
// overwrote that exact PSRAM range out from under the network
// blob. Not an RTL bug -- exactly the limitation
// rtl/flash_slot_manager.v's own header already documents
// ("nothing else in this design may use that PSRAM range").
payload[0]=8'h00; payload[1]=8'h21; payload[2]=8'h00; payload[3]=8'h00;
payload[4]=8'h02; payload[5]=8'h00; payload[6]=8'h04; payload[7]=8'h01;
payload[8]=8'h02; payload[9]=8'h00; payload[10]=8'h00; payload[11]=8'h00;
payload[12]=8'h21; payload[13]=8'h08; payload[14]=8'h00; payload[15]=8'h02;
payload[16]=8'h00; payload[17]=8'h05; payload[18]=8'h00; payload[19]=8'h00;
payload[20]=8'h00; payload[21]=8'h00;
write_ram_bytes(23'h002000, 22); // descriptor table
payload[0]=8'h00; payload[1]=8'h00; payload[2]=8'h05; payload[3]=8'h00;
payload[4]=8'h00; payload[5]=8'h01; payload[6]=8'hfd; payload[7]=8'h00;
write_ram_bytes(23'h002100, 8); // edges for n4
payload[0]=8'h00; payload[1]=8'h04; payload[2]=8'h02; payload[3]=8'h00;
payload[4]=8'h00; payload[5]=8'h02; payload[6]=8'h07; payload[7]=8'h00;
write_ram_bytes(23'h002108, 8); // edges for n5
// Persist that 0x110-byte (272B) blob to flash as slot 4,
// sector-aligned target offset 0x012000 (arbitrary, unrelated
// to the catalog's own reserved sector 0).
cat_write_slot(4'd4, 24'h012000, 24'd0, 8'h02);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST4 cat_write_slot unexpected flash_err"); errors = errors + 1; end
save_slot(4'd4, 23'h002000, 24'd272);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST4 save_slot unexpected flash_err"); errors = errors + 1; end
// Overwrite the PSRAM region with garbage -- proves the
// network the RUN_NETWORK below actually computes with comes
// from the flash reload, not leftover PSRAM content.
for (i = 0; i < 64; i = i + 1) payload[i] = 8'h7E;
write_ram_bytes(23'h002000, 32); // covers the 22-byte descriptor table
write_ram_bytes(23'h002100, 16); // covers both 8-byte edge blocks exactly
load_slot(4'd4, 23'h002000);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST4 load_slot unexpected flash_err (CRC mismatch?)"); errors = errors + 1; end
// Rest of netasm's own load/run sequence (debug dump, see
// file header), unchanged -- inputs x=[10,1,4,0] at x_base,
// Type#2 dispatch, RUN_NETWORK.
set_net_type(8'h02);
set_base(8'h00, 24'h000400); // x_base
set_base(8'h03, 24'h002000); // table_base
set_base(8'h04, 24'h000500); // buf_a_base (== out_base for graph)
set_base(8'h07, 24'h000004); // n_inputs_real = 4
set_base(8'h09, 24'h000002); // num_neurons_graph = 2
set_base(8'h0a, 24'h000001); // n_out = 1
payload[0] = 8'sd10; payload[1] = 8'sd1; payload[2] = 8'sd4; payload[3] = 8'sd0;
write_ram_bytes(23'h000400, 4);
run_network(8'h00);
wait_done_or_err;
if (last_status[3]) begin $display("FAIL: TEST4 RUN_NETWORK unexpected graph err"); errors = errors + 1; end
if (!last_status[1]) begin $display("FAIL: TEST4 RUN_NETWORK never completed (done)"); errors = errors + 1; end
read_ram_bytes(23'h000500, 1);
check_byte("TEST4 end-to-end netasm->SAVE_SLOT->LOAD_SLOT->RUN_NETWORK output", readback[0], 8'sd126);
// ========================================================
if (errors == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d error(s)", errors);
$finish;
end
initial begin
#300_000_000;
$display("FATAL: global simulation timeout");
$finish;
end
endmodule