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
409 lines
13 KiB
Verilog
409 lines
13 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// SPI_NEURON_TOP END-TO-END TESTBENCH
|
|
//
|
|
// Drives the FULL real stack (spi_slave + spi_engine + mem_arbiter
|
|
// + int8_memory_access + memory_interface + psram_controller +
|
|
// psram_model) purely over simulated SPI -- no direct injection
|
|
// into neuron_memory's ports. This is the "verified with the real
|
|
// RAM" test: everything WRITE_RAM/READ_RAM/neuron_memory touches
|
|
// goes through the actual PSRAM timing model, not a synthetic mock.
|
|
//
|
|
// Session (matches docs §8.1 example + neuron_memory_tb.v scenarios,
|
|
// this time reached only through the SPI opcode set):
|
|
// RESET -> READ_CONFIG -> WRITE_RAM(X) -> WRITE_RAM(W) ->
|
|
// WRITE_RAM(bias) -> READ_RAM verify -> SET_BASE x3 -> START ->
|
|
// poll STATUS -> READ_OUTPUT
|
|
//
|
|
// TEST 1: X=1 (32x), W=1, bias=0 -> y = 32
|
|
// TEST 2: reload W=4 -> y saturates to 127
|
|
// TEST 3: reload W=-1 -> ReLU -> y = 0
|
|
// ================================================================
|
|
|
|
module tb;
|
|
|
|
localparam ADDR_WIDTH = 23;
|
|
localparam DATA_WIDTH = 8;
|
|
localparam N_INPUTS = 32;
|
|
localparam N_NEURONS = 1;
|
|
localparam PARALLEL = 8;
|
|
localparam ACC_WIDTH = 32;
|
|
localparam MEM_DATA_WIDTH = 16;
|
|
|
|
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 [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)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
|
|
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
|
|
|
.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)
|
|
);
|
|
|
|
// ============================================================
|
|
// SPI MASTER BFM
|
|
// ============================================================
|
|
|
|
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
|
|
|
|
// RAM-touching bytes need enough margin for the real PSRAM
|
|
// chain's latency (psram_controller ACCESS_CYCLES ~= ceil(70ns
|
|
// * 80MHz) = 6 cycles, plus memory_interface/int8_memory_access/
|
|
// arbiter overhead -- comfortably covered by 40 cycles/half-bit).
|
|
localparam HB_RAM = 40;
|
|
// Pure register opcodes (SET_BASE/START/STATUS/READ_OUTPUT/
|
|
// READ_CONFIG/RESET) only need CDC margin (~4 cycles).
|
|
localparam HB_REG = 8;
|
|
|
|
reg [7:0] rx_tmp;
|
|
integer errors;
|
|
integer i;
|
|
integer poll_count;
|
|
|
|
// ============================================================
|
|
// HELPER TASKS
|
|
// ============================================================
|
|
|
|
task do_reset;
|
|
begin
|
|
spi_begin(HB_REG);
|
|
spi_xfer_byte(8'h0F, HB_REG, rx_tmp); // RESET
|
|
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_const;
|
|
input [ADDR_WIDTH-1:0] addr;
|
|
input integer len;
|
|
input signed [7:0] value;
|
|
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(value, HB_RAM, rx_tmp);
|
|
spi_end(HB_RAM);
|
|
end
|
|
endtask
|
|
|
|
task read_ram_byte;
|
|
input [ADDR_WIDTH-1:0] addr;
|
|
output [7:0] data;
|
|
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(8'h00, HB_RAM, rx_tmp);
|
|
spi_xfer_byte(8'h01, HB_RAM, rx_tmp);
|
|
spi_xfer_byte(8'h00, HB_RAM, data);
|
|
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 do_start;
|
|
begin
|
|
spi_begin(HB_REG);
|
|
spi_xfer_byte(8'h20, HB_REG, rx_tmp);
|
|
spi_end(HB_REG);
|
|
end
|
|
endtask
|
|
|
|
task wait_done;
|
|
reg [7:0] status;
|
|
begin
|
|
poll_count = 0;
|
|
status = 8'h00;
|
|
while (!status[1] && poll_count < 1000) begin
|
|
clk_wait(20);
|
|
read_status(status);
|
|
poll_count = poll_count + 1;
|
|
end
|
|
if (!status[1]) begin
|
|
$display(" FAIL: done never asserted (poll_count=%0d)", poll_count);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
task read_output_byte0;
|
|
output signed [7:0] y0;
|
|
begin
|
|
spi_begin(HB_REG);
|
|
spi_xfer_byte(8'h22, HB_REG, rx_tmp);
|
|
spi_xfer_byte(8'h00, HB_REG, y0);
|
|
spi_end(HB_REG);
|
|
end
|
|
endtask
|
|
|
|
// ============================================================
|
|
// MAIN
|
|
// ============================================================
|
|
|
|
reg signed [7:0] y_result;
|
|
reg [7:0] cfg_byte;
|
|
reg [7:0] verify_byte;
|
|
|
|
initial begin
|
|
|
|
$dumpfile("sim/spi_neuron_top.vcd");
|
|
$dumpvars(0, tb);
|
|
|
|
rst = 1'b1;
|
|
cs_n = 1'b1;
|
|
sclk = 1'b0;
|
|
mosi = 1'b0;
|
|
errors = 0;
|
|
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
|
|
// Wait for PSRAM controller initialization (same as
|
|
// sim/neuron_memory_tb.v).
|
|
wait (dut.u_psram_ctrl.state == dut.u_psram_ctrl.STATE_IDLE);
|
|
|
|
$display("");
|
|
$display("========================================");
|
|
$display("SPI_NEURON_TOP END-TO-END TEST (real PSRAM)");
|
|
$display("========================================");
|
|
|
|
// --------------------------------------------------------
|
|
// RESET (opcode) + READ_CONFIG
|
|
// --------------------------------------------------------
|
|
|
|
do_reset;
|
|
|
|
spi_begin(HB_REG);
|
|
spi_xfer_byte(8'h30, HB_REG, rx_tmp);
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== ADDR_WIDTH[7:0]) begin $display(" FAIL: READ_CONFIG ADDR_WIDTH"); errors = errors + 1; end
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== N_INPUTS[15:8]) begin $display(" FAIL: READ_CONFIG N_INPUTS hi"); errors = errors + 1; end
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== N_INPUTS[7:0]) begin $display(" FAIL: READ_CONFIG N_INPUTS lo"); errors = errors + 1; end
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== N_NEURONS[7:0]) begin $display(" FAIL: READ_CONFIG N_NEURONS"); errors = errors + 1; end
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== PARALLEL[7:0]) begin $display(" FAIL: READ_CONFIG PARALLEL"); errors = errors + 1; end
|
|
spi_xfer_byte(8'h00, HB_REG, cfg_byte);
|
|
if (cfg_byte !== DATA_WIDTH[7:0]) begin $display(" FAIL: READ_CONFIG DATA_WIDTH"); errors = errors + 1; end
|
|
spi_end(HB_REG);
|
|
|
|
$display("READ_CONFIG: %0s", (errors == 0) ? "PASS" : "FAIL");
|
|
|
|
// --------------------------------------------------------
|
|
// Load X=1 (32x), W=1 (32x), bias=0 into real PSRAM over SPI
|
|
// --------------------------------------------------------
|
|
|
|
write_ram_const(22'h000000, N_INPUTS, 8'sd1); // X
|
|
write_ram_const(22'h000100, N_INPUTS, 8'sd1); // W
|
|
write_ram_const(22'h000200, 1, 8'sd0); // bias
|
|
|
|
// Verify one written byte reads back correctly through the
|
|
// real PSRAM (READ_RAM), confirming the write actually
|
|
// landed and isn't just accepted-but-dropped.
|
|
read_ram_byte(22'h000000, verify_byte);
|
|
if (verify_byte !== 8'sd1) begin
|
|
$display(" FAIL: READ_RAM verify X[0] = 0x%02x expected 0x01", verify_byte);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("READ_RAM verify: PASS (X[0]=0x%02x)", verify_byte);
|
|
end
|
|
|
|
set_base(8'h00, 22'h000000); // X_BASE
|
|
set_base(8'h01, 22'h000100); // W_BASE
|
|
set_base(8'h02, 22'h000200); // BIAS_ADDR
|
|
|
|
// --------------------------------------------------------
|
|
// TEST 1: 32 * 1 * 1 + 0 = 32
|
|
// --------------------------------------------------------
|
|
|
|
do_start;
|
|
wait_done;
|
|
read_output_byte0(y_result);
|
|
|
|
$display("");
|
|
$display("TEST 1 (SUM=32): y = %0d (expected 32)", y_result);
|
|
if (y_result !== 8'sd32) begin
|
|
$display(" FAIL");
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display(" PASS");
|
|
end
|
|
|
|
// --------------------------------------------------------
|
|
// TEST 2: reload W=4 -> 32*1*4 = 128 -> saturate to 127
|
|
// --------------------------------------------------------
|
|
|
|
write_ram_const(22'h000100, N_INPUTS, 8'sd4);
|
|
|
|
do_start;
|
|
wait_done;
|
|
read_output_byte0(y_result);
|
|
|
|
$display("");
|
|
$display("TEST 2 (SATURATION): y = %0d (expected 127)", y_result);
|
|
if (y_result !== 8'sd127) begin
|
|
$display(" FAIL");
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display(" PASS");
|
|
end
|
|
|
|
// --------------------------------------------------------
|
|
// TEST 3: reload W=-1 -> 32*1*(-1) = -32 -> ReLU -> 0
|
|
// --------------------------------------------------------
|
|
|
|
write_ram_const(22'h000100, N_INPUTS, -8'sd1);
|
|
|
|
do_start;
|
|
wait_done;
|
|
read_output_byte0(y_result);
|
|
|
|
$display("");
|
|
$display("TEST 3 (RELU): y = %0d (expected 0)", y_result);
|
|
if (y_result !== 8'sd0) begin
|
|
$display(" FAIL");
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display(" PASS");
|
|
end
|
|
|
|
// --------------------------------------------------------
|
|
// SUMMARY
|
|
// --------------------------------------------------------
|
|
|
|
$display("");
|
|
$display("========================================");
|
|
if (errors == 0)
|
|
$display("SPI_NEURON_TOP END-TO-END TEST PASSED");
|
|
else
|
|
$display("SPI_NEURON_TOP END-TO-END TEST FAILED: %0d errors", errors);
|
|
$display("========================================");
|
|
$display("");
|
|
|
|
$finish;
|
|
|
|
end
|
|
|
|
endmodule
|