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
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
`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
|
||||
Reference in New Issue
Block a user