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

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_sim;
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_sim(flash_sclk_sim),
.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_sim), .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