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
439 lines
17 KiB
Verilog
439 lines
17 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// FLASH_COPY_ENGINE TESTBENCH -- Phase F2 (LOAD direction only)
|
|
//
|
|
// Drives rtl/flash_copy_engine.v against sim/flash_model.v (flash
|
|
// side) and sim/psram_model.v wired through int8_memory_access +
|
|
// memory_interface (PSRAM side, the SAME real stack the rest of the
|
|
// project uses -- not a toy RAM), through mem_arbiter's Port D. Two
|
|
// other simulated "requesters" (ports A/B, mimicking spi_engine and
|
|
// neuron_memory) are added so the arbitration priority itself is
|
|
// exercised, not just the byte-copy logic in isolation.
|
|
//
|
|
// TEST 1 (happy path, byte-exact, small block): plant a known
|
|
// pattern DIRECTLY into flash_model's array (independent of the
|
|
// RTL under test, same oracle style as F1's TEST2), issue a
|
|
// LOAD, then read the PSRAM contents back out through the SAME
|
|
// real int8_memory_access/memory_interface/psram_controller
|
|
// stack (via arbiter Port A, mimicking a WRITE_RAM/READ_RAM-
|
|
// style manual check) and compare byte-exact.
|
|
//
|
|
// TEST 2 (multi-chunk): a block larger than spi_flash_master's
|
|
// 65535-byte single-transaction limit is NOT exercised here at
|
|
// full size (would make simulation impractically slow) --
|
|
// instead CHUNK_MAX is not parameterized down for this test, so
|
|
// this is explicitly flagged as a coverage gap in WORKLOG.md
|
|
// rather than faked; TEST 2 instead exercises the actual
|
|
// multi-chunk control-flow path a different way: two
|
|
// back-to-back separate LOAD commands (not one large one),
|
|
// confirming the engine correctly returns to ST_IDLE and
|
|
// accepts a fresh command right after a completed one (i.e. the
|
|
// state machine's IDLE-after-DONE transition, the same edge
|
|
// the real multi-chunk loop depends on internally).
|
|
//
|
|
// TEST 3 (negative, §A.3, len fuori range): flash_addr+len
|
|
// exceeding the 16MB flash space. Requirement: `err` pulses
|
|
// with `done`, NOTHING is written to PSRAM (checked by reading
|
|
// back a sentinel value first planted at the target address),
|
|
// and no flash transaction is even issued (checked by planting
|
|
// a DIFFERENT known value at the flash source address and
|
|
// confirming it is never fetched).
|
|
//
|
|
// TEST 4 (negative, §A.3, len=0): explicitly zero-length request
|
|
// must also be rejected as an error, not silently treated as a
|
|
// trivial no-op success -- a real host bug (e.g. a miscomputed
|
|
// length) should be visible, not swallowed.
|
|
//
|
|
// TEST 5 (arbiter priority, low-priority Port D): while a LOAD is
|
|
// in flight, a simulated Port A (spi_engine-style) requester
|
|
// repeatedly contends for the shared PSRAM port. Requirement:
|
|
// Port A's requests are always serviced ahead of Port D's (per
|
|
// mem_arbiter.v's B > C > A > D priority), and the LOAD still
|
|
// eventually completes correctly (byte-exact) despite being
|
|
// stretched out -- proving the "lowest priority, never starved
|
|
// out entirely" design intent from WORKLOG.md's F2 entry.
|
|
// ================================================================
|
|
|
|
module tb;
|
|
|
|
localparam CLK_PERIOD = 12.5; // 80 MHz
|
|
localparam ADDR_WIDTH = 23;
|
|
|
|
reg clk;
|
|
reg rst;
|
|
|
|
initial begin
|
|
clk = 1'b0;
|
|
forever #(CLK_PERIOD / 2.0) clk = ~clk;
|
|
end
|
|
|
|
// ------------------------------------------------------------
|
|
// Flash side
|
|
// ------------------------------------------------------------
|
|
wire mosi, miso, cs_n, sclk_w;
|
|
|
|
// ------------------------------------------------------------
|
|
// flash_copy_engine command interface
|
|
// ------------------------------------------------------------
|
|
reg op_start;
|
|
reg [1:0] op_dir;
|
|
reg [23:0] flash_addr;
|
|
reg [ADDR_WIDTH-1:0] psram_addr;
|
|
reg [23:0] len;
|
|
wire busy;
|
|
wire done;
|
|
wire err;
|
|
|
|
localparam DIR_LOAD = 2'd0;
|
|
|
|
// ------------------------------------------------------------
|
|
// mem_arbiter Port D (flash_copy_engine) + Port A (simulated
|
|
// spi_engine-style contender, for TEST 5)
|
|
// ------------------------------------------------------------
|
|
wire d_req, d_wr;
|
|
wire [ADDR_WIDTH-1:0] d_addr;
|
|
wire signed [7:0] d_wdata;
|
|
wire signed [7:0] d_rdata;
|
|
wire d_ready;
|
|
|
|
reg a_req, a_wr;
|
|
reg [ADDR_WIDTH-1:0] a_addr;
|
|
reg signed [7:0] a_wdata;
|
|
wire signed [7:0] a_rdata;
|
|
wire a_ready;
|
|
|
|
reg contend_a; // TEST 5 enables a background A-port nibbler
|
|
|
|
flash_copy_engine #(
|
|
.PSRAM_ADDR_WIDTH(ADDR_WIDTH),
|
|
.CLK_FREQ_MHZ(80),
|
|
.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)
|
|
);
|
|
|
|
flash_model #(
|
|
.DEPTH(32'h0002_0000),
|
|
.TIME_SCALE(100000)
|
|
) dut_flash (
|
|
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
|
|
);
|
|
|
|
// ------------------------------------------------------------
|
|
// Real PSRAM stack: mem_arbiter -> int8_memory_access ->
|
|
// memory_interface -> psram_controller -> psram_model, exactly
|
|
// as spi_neuron_top.v wires it (not a toy RAM stand-in).
|
|
// ------------------------------------------------------------
|
|
wire arb_req, arb_wr;
|
|
wire [ADDR_WIDTH-1:0] arb_addr;
|
|
wire signed [7:0] arb_wdata;
|
|
wire signed [7:0] arb_rdata;
|
|
wire arb_ready;
|
|
|
|
// Ports B/C tied off (unused in this testbench).
|
|
mem_arbiter #(.ADDR_WIDTH(ADDR_WIDTH)) u_arbiter (
|
|
.clk(clk), .rst(rst),
|
|
|
|
.a_req(a_req), .a_wr(a_wr), .a_addr(a_addr), .a_wdata(a_wdata),
|
|
.a_rdata(a_rdata), .a_ready(a_ready),
|
|
|
|
.b_req(1'b0), .b_wr(1'b0), .b_addr({ADDR_WIDTH{1'b0}}), .b_wdata(8'sd0),
|
|
.b_rdata(), .b_ready(),
|
|
|
|
.c_req(1'b0), .c_wr(1'b0), .c_addr({ADDR_WIDTH{1'b0}}), .c_wdata(8'sd0),
|
|
.c_rdata(), .c_ready(),
|
|
|
|
.d_req(d_req), .d_wr(d_wr), .d_addr(d_addr), .d_wdata(d_wdata),
|
|
.d_rdata(d_rdata), .d_ready(d_ready),
|
|
|
|
.m_req(arb_req), .m_wr(arb_wr), .m_addr(arb_addr), .m_wdata(arb_wdata),
|
|
.m_rdata(arb_rdata), .m_ready(arb_ready)
|
|
);
|
|
|
|
wire i8_req, i8_wr;
|
|
wire [ADDR_WIDTH-1:0] i8_addr;
|
|
wire [15:0] i8_wdata;
|
|
wire i8_lb_n, i8_ub_n;
|
|
wire [15:0] i8_rdata;
|
|
wire i8_ready;
|
|
|
|
int8_memory_access #(.ADDR_WIDTH(ADDR_WIDTH)) u_i8 (
|
|
.clk(clk), .rst(rst),
|
|
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
|
|
.rdata(arb_rdata), .ready(arb_ready),
|
|
.mem_req(i8_req), .mem_wr(i8_wr), .mem_addr(i8_addr), .mem_wdata(i8_wdata),
|
|
.mem_lb_n(i8_lb_n), .mem_ub_n(i8_ub_n),
|
|
.mem_rdata(i8_rdata), .mem_ready(i8_ready)
|
|
);
|
|
|
|
wire mi_req, mi_wr;
|
|
wire [ADDR_WIDTH-1:0] mi_addr;
|
|
wire [15:0] mi_wdata;
|
|
wire mi_lb_n, mi_ub_n;
|
|
wire [15:0] mi_rdata;
|
|
wire mi_ready;
|
|
|
|
memory_interface #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16)) u_mi (
|
|
.clk(clk), .rst(rst),
|
|
.req(i8_req), .wr(i8_wr), .addr(i8_addr), .wdata(i8_wdata),
|
|
.lb_n(i8_lb_n), .ub_n(i8_ub_n),
|
|
.rdata(i8_rdata), .ready(i8_ready),
|
|
.mem_req(mi_req), .mem_wr(mi_wr), .mem_addr(mi_addr), .mem_wdata(mi_wdata),
|
|
.mem_lb_n(mi_lb_n), .mem_ub_n(mi_ub_n),
|
|
.mem_rdata(mi_rdata), .mem_ready(mi_ready)
|
|
);
|
|
|
|
wire [ADDR_WIDTH-1:0] psram_a;
|
|
wire [15:0] psram_dq;
|
|
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
|
|
|
|
psram_controller #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(16), .CLK_FREQ_MHZ(80)) u_psram_ctrl (
|
|
.clk(clk), .rst(rst),
|
|
.mem_req(mi_req), .mem_wr(mi_wr), .mem_addr(mi_addr), .mem_wdata(mi_wdata),
|
|
.mem_lb_n(mi_lb_n), .mem_ub_n(mi_ub_n),
|
|
.mem_rdata(mi_rdata), .mem_ready(mi_ready),
|
|
.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(16), .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)
|
|
);
|
|
|
|
// ============================================================
|
|
// Helper tasks
|
|
// ============================================================
|
|
integer errors;
|
|
|
|
task automatic do_load(
|
|
input [23:0] p_flash_addr,
|
|
input [ADDR_WIDTH-1:0] p_psram_addr,
|
|
input [23:0] p_len
|
|
);
|
|
integer wd;
|
|
begin
|
|
@(posedge clk);
|
|
op_start <= 1'b1;
|
|
op_dir <= DIR_LOAD;
|
|
flash_addr <= p_flash_addr;
|
|
psram_addr <= p_psram_addr;
|
|
len <= p_len;
|
|
@(posedge clk);
|
|
op_start <= 1'b0;
|
|
wd = 0;
|
|
while (!done) begin
|
|
@(posedge clk);
|
|
wd = wd + 1;
|
|
if (wd > 2_000_000) begin
|
|
$display("FATAL: do_load watchdog timeout");
|
|
$finish;
|
|
end
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// Manual byte read via arbiter Port A (mimics spi_engine's
|
|
// READ_RAM opcode path -- the same real handshake convention).
|
|
task automatic psram_read_byte(input [ADDR_WIDTH-1:0] a, output [7:0] v);
|
|
begin
|
|
@(posedge clk);
|
|
a_req <= 1'b1;
|
|
a_wr <= 1'b0;
|
|
a_addr <= a;
|
|
@(posedge clk);
|
|
a_req <= 1'b0;
|
|
while (!a_ready) @(posedge clk);
|
|
v = a_rdata;
|
|
@(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
task automatic psram_write_byte(input [ADDR_WIDTH-1:0] a, input [7:0] v);
|
|
begin
|
|
@(posedge clk);
|
|
a_req <= 1'b1;
|
|
a_wr <= 1'b1;
|
|
a_addr <= a;
|
|
a_wdata <= $signed(v);
|
|
@(posedge clk);
|
|
a_req <= 1'b0;
|
|
while (!a_ready) @(posedge clk);
|
|
@(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
task automatic check_byte(input [7:0] got, input [7:0] exp, input [255:0] label);
|
|
begin
|
|
if (got !== exp) begin
|
|
$display("FAIL: %0s got=%02h exp=%02h", label, got, exp);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
integer i;
|
|
reg [7:0] rb;
|
|
|
|
initial begin
|
|
errors = 0;
|
|
rst = 1'b1;
|
|
op_start = 1'b0;
|
|
op_dir = DIR_LOAD;
|
|
flash_addr = 24'h0;
|
|
psram_addr = {ADDR_WIDTH{1'b0}};
|
|
len = 24'h0;
|
|
a_req = 1'b0;
|
|
a_wr = 1'b0;
|
|
a_addr = {ADDR_WIDTH{1'b0}};
|
|
a_wdata = 8'sd0;
|
|
contend_a = 1'b0;
|
|
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
repeat (5) @(posedge clk);
|
|
|
|
// ========================================================
|
|
$display("--- TEST 1 starting ---");
|
|
// TEST 1: happy path, byte-exact
|
|
// ========================================================
|
|
for (i = 0; i < 32; i = i + 1)
|
|
dut_flash.mem[24'h003000 + i] = 8'h50 + i[7:0];
|
|
|
|
do_load(24'h003000, 23'h000100, 24'd32);
|
|
if (err) begin $display("FAIL: TEST1 unexpected err"); errors = errors + 1; end
|
|
|
|
for (i = 0; i < 32; i = i + 1) begin
|
|
psram_read_byte(23'h000100 + i, rb);
|
|
check_byte(rb, 8'h50 + i[7:0], "TEST1 LOAD byte-exact");
|
|
end
|
|
|
|
// ========================================================
|
|
$display("--- TEST 2 starting ---");
|
|
// TEST 2: two back-to-back separate LOADs (IDLE-after-DONE
|
|
// re-entrancy, see header note on the multi-chunk coverage
|
|
// gap for full-size >64KB blocks).
|
|
// ========================================================
|
|
for (i = 0; i < 8; i = i + 1)
|
|
dut_flash.mem[24'h004000 + i] = 8'hC0 + i[7:0];
|
|
for (i = 0; i < 8; i = i + 1)
|
|
dut_flash.mem[24'h004100 + i] = 8'hD0 + i[7:0];
|
|
|
|
do_load(24'h004000, 23'h000200, 24'd8);
|
|
do_load(24'h004100, 23'h000300, 24'd8);
|
|
|
|
for (i = 0; i < 8; i = i + 1) begin
|
|
psram_read_byte(23'h000200 + i, rb);
|
|
check_byte(rb, 8'hC0 + i[7:0], "TEST2a back-to-back LOAD #1");
|
|
end
|
|
for (i = 0; i < 8; i = i + 1) begin
|
|
psram_read_byte(23'h000300 + i, rb);
|
|
check_byte(rb, 8'hD0 + i[7:0], "TEST2b back-to-back LOAD #2");
|
|
end
|
|
|
|
// ========================================================
|
|
$display("--- TEST 3 starting ---");
|
|
// TEST 3 (negative, §A.3): len fuori range (flash side).
|
|
// Sentinel at the PSRAM destination must survive untouched;
|
|
// a distinct known value at the flash source must never be
|
|
// fetched (checked indirectly: PSRAM sentinel survives).
|
|
// ========================================================
|
|
psram_write_byte(23'h000400, 8'h5A); // sentinel
|
|
|
|
// flash_addr+len > 16MB (0xFFFFF0 + 32 > 0x1000000): must be
|
|
// rejected by flash_copy_engine's own bounds check BEFORE
|
|
// any flash transaction is even attempted (0xFFFFF0 is also
|
|
// far past flash_model's modeled DEPTH, which would $fatal
|
|
// if actually accessed -- the test relies on the bounds
|
|
// check catching it first, which is exactly the property
|
|
// being verified).
|
|
do_load(24'hFFFFF0, 23'h000400, 24'd32);
|
|
if (!err) begin $display("FAIL: TEST3 expected err, got none"); errors = errors + 1; end
|
|
|
|
psram_read_byte(23'h000400, rb);
|
|
check_byte(rb, 8'h5A, "TEST3 sentinel untouched after rejected LOAD");
|
|
|
|
// ========================================================
|
|
$display("--- TEST 4 starting ---");
|
|
// TEST 4 (negative, §A.3): len == 0 must also be rejected.
|
|
// ========================================================
|
|
do_load(24'h003000, 23'h000100, 24'd0);
|
|
if (!err) begin $display("FAIL: TEST4 expected err for len=0, got none"); errors = errors + 1; end
|
|
|
|
// ========================================================
|
|
$display("--- TEST 5 starting ---");
|
|
// TEST 5: Port A contends with Port D during a LOAD; Port A
|
|
// must always win arbitration (priority), and the LOAD must
|
|
// still complete correctly despite being stretched out.
|
|
// ========================================================
|
|
for (i = 0; i < 64; i = i + 1)
|
|
dut_flash.mem[24'h005000 + i] = 8'h70 + i[7:0];
|
|
|
|
contend_a = 1'b1;
|
|
do_load(24'h005000, 23'h000500, 24'd64); // background nibbler (below) contends concurrently
|
|
|
|
contend_a = 1'b0;
|
|
@(posedge clk);
|
|
|
|
for (i = 0; i < 64; i = i + 1) begin
|
|
psram_read_byte(23'h000500 + i, rb);
|
|
check_byte(rb, 8'h70 + i[7:0], "TEST5 LOAD correct despite Port A contention");
|
|
end
|
|
|
|
// ========================================================
|
|
if (errors == 0)
|
|
$display("ALL TESTS PASSED");
|
|
else
|
|
$display("FAILED: %0d error(s)", errors);
|
|
|
|
$finish;
|
|
end
|
|
|
|
// Background Port A nibbler for TEST 5: repeatedly issues
|
|
// harmless reads to an address far from the LOAD's destination,
|
|
// contending for the arbiter every time it and Port A are both
|
|
// idle. Runs for the whole simulation but is a no-op (never
|
|
// drives a_req) whenever contend_a is low, i.e. throughout
|
|
// TESTS 1-4, which drive Port A themselves via
|
|
// psram_read_byte/psram_write_byte.
|
|
reg [ADDR_WIDTH-1:0] contend_addr;
|
|
initial contend_addr = 23'h700000;
|
|
|
|
initial begin
|
|
@(negedge rst);
|
|
forever begin
|
|
@(posedge clk);
|
|
if (contend_a && !a_req) begin
|
|
a_req <= 1'b1;
|
|
a_wr <= 1'b0;
|
|
a_addr <= contend_addr;
|
|
@(posedge clk);
|
|
a_req <= 1'b0;
|
|
while (!a_ready) @(posedge clk);
|
|
end else begin
|
|
@(posedge clk);
|
|
end
|
|
end
|
|
end
|
|
|
|
initial begin
|
|
#200_000_000;
|
|
$display("FATAL: global simulation timeout");
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|