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

424 lines
14 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP IRQ_N / DATA_READY_N PIN TESTBENCH
//
// New physical attention pins (active-low): irq_n mirrors
// graph_engine's `err` (STATUS.bit2), data_ready_n mirrors
// spi_engine's status_done_sticky (STATUS.bit1). Verifies:
//
// TEST 1 - idle: both pins HIGH (inactive) after reset.
// TEST 2 - successful graph run: data_ready_n goes LOW exactly
// when done latches, irq_n stays HIGH throughout, and
// data_ready_n goes back HIGH the moment STATUS is read
// (same flip-flop as STATUS.bit1, clear-on-read).
// TEST 3 - guard-violation graph run: irq_n goes LOW, data_ready_n
// stays HIGH (no result is actually ready), irq_n stays
// LOW even across an unrelated STATUS read (it is NOT
// clear-on-read like data_ready_n -- only RESET or a
// fresh run_start clears it, matching graph_engine.err).
// TEST 4 - RESET clears irq_n back to HIGH.
//
// Same SPI BFM style as sim/spi_neuron_top_graph_tb.v.
// ================================================================
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 irq_n;
wire data_ready_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), .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),
.irq_n(irq_n), .data_ready_n(data_ready_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 (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 errors_before;
integer poll_count;
reg signed [7:0] payload [0:31];
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_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
task write_graph_desc;
input [ADDR_WIDTH-1:0] base;
input [23:0] conn_ptr;
input [15:0] n_conn;
input [15:0] out_id;
input [7:0] activation;
input [7:0] bias;
begin
payload[0] = conn_ptr[23:16]; payload[1] = conn_ptr[15:8]; payload[2] = conn_ptr[7:0];
payload[3] = n_conn[15:8]; payload[4] = n_conn[7:0];
payload[5] = out_id[15:8]; payload[6] = out_id[7:0];
payload[7] = activation; payload[8] = bias;
payload[9] = 8'h00; payload[10] = 8'h00;
write_ram_bytes(base, 11);
end
endtask
task write_edge;
input [ADDR_WIDTH-1:0] base;
input [15:0] src_id;
input [7:0] weight;
begin
payload[0] = src_id[15:8]; payload[1] = src_id[7:0];
payload[2] = weight; payload[3] = 8'h00;
write_ram_bytes(base, 4);
end
endtask
task wait_pin_low;
input pin;
integer cyc;
begin
cyc = 0;
while (pin !== 1'b0 && cyc < 3000) begin
@(posedge clk);
cyc = cyc + 1;
end
end
endtask
localparam [ADDR_WIDTH-1:0] X_BASE = 22'h000000;
localparam [ADDR_WIDTH-1:0] TABLE_BASE = 22'h000100;
localparam [ADDR_WIDTH-1:0] N4_EDGES = 22'h000200;
localparam [ADDR_WIDTH-1:0] N5_EDGES = 22'h000210;
localparam [ADDR_WIDTH-1:0] OUT_BASE = 22'h000300;
localparam ACT_NONE = 8'h00;
localparam ACT_RELU = 8'h01;
initial begin
rst = 1'b1; cs_n = 1'b1; sclk = 1'b0; mosi = 1'b0;
errors = 0;
repeat (5) @(posedge clk);
rst = 1'b0;
wait (dut.u_psram_ctrl.state == dut.u_psram_ctrl.STATE_IDLE);
$display("");
$display("========================================");
$display("SPI_NEURON_TOP IRQ_N / DATA_READY_N PIN TEST");
$display("========================================");
// ---- TEST 1: idle, both pins high after reset ----
errors_before = errors;
@(posedge clk);
if (irq_n !== 1'b1) begin $display(" FAIL: irq_n not high after reset"); errors = errors + 1; end
if (data_ready_n !== 1'b1) begin $display(" FAIL: data_ready_n not high after reset"); errors = errors + 1; end
if (errors == errors_before) $display("TEST 1 (idle after reset, both pins high): PASS");
else $display("TEST 1: FAIL");
// ---- TEST 2: valid graph run -> data_ready_n pulses low, irq_n stays high ----
errors_before = errors;
do_reset;
payload[0] = 8'sd10; payload[1] = 8'sd1; payload[2] = 8'sd4; payload[3] = 8'sd0;
write_ram_bytes(X_BASE, 4);
write_graph_desc(TABLE_BASE + 0*11, N4_EDGES, 16'd2, 16'd4, ACT_RELU, 8'sd2);
write_graph_desc(TABLE_BASE + 1*11, N5_EDGES, 16'd2, 16'd5, ACT_NONE, 8'sd0);
write_edge(N4_EDGES + 0*4, 16'd0, 8'sd5);
write_edge(N4_EDGES + 1*4, 16'd1, -8'sd3);
write_edge(N5_EDGES + 0*4, 16'd4, 8'sd2);
write_edge(N5_EDGES + 1*4, 16'd2, 8'sd7);
set_net_type(8'h02);
set_base(8'h00, X_BASE);
set_base(8'h03, TABLE_BASE);
set_base(8'h04, OUT_BASE);
set_base(8'h07, 24'h000004);
set_base(8'h09, 24'h000002);
set_base(8'h0A, 24'h000001);
if (irq_n !== 1'b1) begin $display(" FAIL: irq_n dropped before run_start"); errors = errors + 1; end
run_network(8'h00);
wait_pin_low(data_ready_n);
if (data_ready_n !== 1'b0) begin
$display(" FAIL: data_ready_n never went low after a valid run");
errors = errors + 1;
end else begin
$display(" data_ready_n went low as expected");
end
if (irq_n !== 1'b1) begin
$display(" FAIL: irq_n dropped on a VALID graph run (should stay high)");
errors = errors + 1;
end
// Reading STATUS clears the sticky bit -> data_ready_n back high
read_status(rx_tmp);
@(posedge clk);
if (rx_tmp[1] !== 1'b1) begin
$display(" FAIL: STATUS.done bit not set alongside data_ready_n");
errors = errors + 1;
end
if (data_ready_n !== 1'b1) begin
$display(" FAIL: data_ready_n did not return high after STATUS read");
errors = errors + 1;
end else begin
$display(" data_ready_n returned high after STATUS read (clear-on-read, same flip-flop)");
end
if (errors == errors_before) $display("TEST 2 (valid run: data_ready_n low->high, irq_n stays high): PASS");
else $display("TEST 2: FAIL");
// ---- TEST 3: guard-violation graph run -> irq_n low, data_ready_n stays high ----
errors_before = errors;
do_reset;
set_net_type(8'h02);
payload[0] = 8'sd0;
write_ram_bytes(X_BASE, 1);
write_graph_desc(TABLE_BASE, N4_EDGES, 16'd1, 16'd4, ACT_RELU, 8'sd0);
write_edge(N4_EDGES + 0*4, 16'd4, 8'sd1); // src_id == out_id: invalid
write_edge(N4_EDGES + 1*4, 16'd0, 8'sd0);
set_base(8'h00, X_BASE);
set_base(8'h03, TABLE_BASE);
set_base(8'h04, OUT_BASE);
set_base(8'h07, 24'h000001);
set_base(8'h09, 24'h000001);
set_base(8'h0A, 24'h000001);
run_network(8'h00);
wait_pin_low(irq_n);
if (irq_n !== 1'b0) begin
$display(" FAIL: irq_n never went low on an invalid graph");
errors = errors + 1;
end else begin
$display(" irq_n went low as expected");
end
if (data_ready_n !== 1'b1) begin
$display(" FAIL: data_ready_n dropped on a run that errored (no result is ready)");
errors = errors + 1;
end
// An unrelated STATUS read must NOT clear irq_n (only RESET / fresh run_start do)
read_status(rx_tmp);
@(posedge clk);
if (rx_tmp[2] !== 1'b1) begin
$display(" FAIL: STATUS.err bit not set alongside irq_n");
errors = errors + 1;
end
if (irq_n !== 1'b0) begin
$display(" FAIL: irq_n cleared by a plain STATUS read (should only clear on RESET/fresh run_start)");
errors = errors + 1;
end else begin
$display(" irq_n correctly stayed low across a STATUS read (not clear-on-read)");
end
if (errors == errors_before) $display("TEST 3 (invalid run: irq_n low, data_ready_n stays high, not clear-on-read): PASS");
else $display("TEST 3: FAIL");
// ---- TEST 4: RESET clears irq_n back to high ----
errors_before = errors;
do_reset;
@(posedge clk);
if (irq_n !== 1'b1) begin
$display(" FAIL: irq_n not cleared by RESET");
errors = errors + 1;
end else begin
$display(" irq_n cleared by RESET as expected");
end
if (errors == errors_before) $display("TEST 4 (RESET clears irq_n): PASS");
else $display("TEST 4: FAIL");
$display("");
$display("========================================");
if (errors == 0)
$display("SPI_NEURON_TOP IRQ/DATA_READY PIN TEST PASSED");
else
$display("SPI_NEURON_TOP IRQ/DATA_READY PIN TEST FAILED: %0d errors", errors);
$display("========================================");
$display("");
$finish;
end
initial begin
#50000000;
$display("TIMEOUT: simulation did not finish in time");
$finish;
end
endmodule