Files
FPGA-Neural/sim/neuron_parallel_bug003_n_inputs_real_zero_tb.v
T
micheleandClaude Sonnet 5 07a48e401f fix: close 7 zero-value/mid-run guard gaps found in re-certification campaign
Fixes all 7 bugs found in the FPGA-Neural re-certification campaign
(docs/validation/bugs.md, CERTIFICATION.md), per campaign policy that
fixes land as a commit separate from the analysis work (commits
313a199..77e74db):

- BUG-005 (CRITICAL): layer_sequencer.v -- RUN_NETWORK(num_layers=0)
  ran through 256 fabricated layers reading arbitrary PSRAM data as
  descriptors. Now an immediate no-op.
- BUG-007 (CRITICAL): spi_engine.v -- SET_NET_TYPE received mid-run
  remapped the arbiter mux and hung the in-progress engine. Now
  rejected while graph_busy/seq_busy, verified not to partially apply.
- BUG-002 (MEDIA): neuron_parallel.v -- N_INPUTS=0 bypassed the
  elaboration-time guard, leaving x_bus/w_bus undriven. Guard extended
  to reject N_INPUTS==0.
- BUG-003 (MEDIA): neuron_parallel.v -- n_inputs_real=0 at runtime had
  inconsistent behavior across repeated runs. Now an explicit early-out
  via the existing "finishing" completion path.
- BUG-004 (BASSA): neuron_memory.v -- n_neurons_real=0 silently ignored
  the limit. Fixed at all three entry points into the vulnerable
  termination checks (STATE_READ_X, STATE_READ_W, and the X->W
  dispatch).
- BUG-006 (BASSA): graph_engine.v -- num_neurons_graph=0 relied on an
  incidental guard rather than a real one. Now an explicit no-op.
- BUG-001 (INFO): removed sim/top.v, confirmed dead code from the
  pre-INT8 Q8.8 era.

Every bug-reproduction testbench is rewritten from observe-only to
hard-assert the fixed behavior (sim/*_bug00[2-7]*_tb.v), verified
individually and via a full regression (44 testbenches, 43 PASS, 0
FAIL/ERROR, 1 benchmark by design). Re-verified on the real toolchain
(Yosys synth_ecp5 + nextpnr-ecp5): 0 constraint errors, Fmax 68.65 MHz
(was 67.91 MHz, within known placement noise), critical path
structurally unchanged (neuron_parallel/mac8 accumulator carry chain).

Updates docs/validation/bugs.md and CERTIFICATION.md to reflect the
resolved state, and docs/FPGA-NeuralNetwork-Engine.md + the LaTeX
datasheet (IT/EN) with inline notes on each fixed edge case, closing
the datasheet/RTL gap flagged in C.13 of the original certification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-04 20:29:05 +02:00

134 lines
5.1 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// C.2 CERTIFICATION + BUG-003 REGRESSION TEST (certification
// campaign, docs/validation/bugs.md / docs/validation/02-runtime-width.md).
//
// TESTS 1-3 (CERTIFIED): early termination for valid n_inputs_real
// values is real -- a "poison" region at indices 16-31 with
// saturating x=w=100 would corrupt the result if the RTL ever read
// past the real limit. It doesn't. These three checks fail the run
// (errors counted) if early termination breaks.
//
// TEST 4 (n_inputs_real=0), now FIXED. Unlike BUG-002 (N_INPUTS=0, a
// compile-time parameter), this runtime-reachable twin
// (n_inputs_real=0, exactly the port an SPI host drives via SET_BASE
// sel=7, docs/FPGA-NeuralNetwork-Engine.md §8.1) previously produced
// inconsistent, unisolated symptoms across nearly-identical repeated
// runs -- sometimes a clean hang, sometimes a silent full-width
// computation instead of zero elements. See
// docs/validation/02-runtime-width.md §2.3 for the full investigation
// record.
//
// Fix (rtl/neuron_parallel.v, inside `if (start && !busy)`):
// finishing <= (n_inputs_real == 16'h0);
// reuses the existing correct "finishing" completion path instead of
// entering the MAC loop at all. For zero real inputs the mathematical
// result is y = activation(bias); with bias=0 and ACT_RELU here,
// expect_y=0. This test now hard-asserts that TEST 4 completes
// quickly with the correct value, same scoring as TESTS 1-3.
// ================================================================
module tb;
localparam DATA_WIDTH = 8;
localparam N_INPUTS = 32;
localparam PARALLEL = 8;
localparam ACC_WIDTH = 32;
reg clk;
initial begin
clk = 1'b0;
forever #5 clk = ~clk;
end
reg rst, start;
reg signed [DATA_WIDTH*N_INPUTS-1:0] x_bus, w_bus;
reg [1:0] activation;
reg [15:0] n_inputs_real;
wire busy, done;
wire signed [DATA_WIDTH-1:0] y;
integer cyc, i;
integer errors;
neuron_parallel #(
.DATA_WIDTH(DATA_WIDTH), .N_INPUTS(N_INPUTS), .PARALLEL(PARALLEL), .ACC_WIDTH(ACC_WIDTH)
) dut (
.clk(clk), .rst(rst), .start(start),
.x_bus(x_bus), .w_bus(w_bus), .bias(8'sd0),
.activation(activation), .n_inputs_real(n_inputs_real),
.y(y), .busy(busy), .done(done)
);
task automatic run_case(
input [15:0] nreal,
input signed [7:0] expect_y,
input integer max_cycles
);
begin
n_inputs_real = nreal;
rst = 1; start = 0;
@(posedge clk); @(posedge clk);
rst = 0;
@(posedge clk);
start = 1;
@(posedge clk);
start = 0;
cyc = 0;
while (!done && cyc < 200) begin
@(posedge clk);
cyc = cyc + 1;
end
if (!done) begin
$display("n_inputs_real=%0d: FAIL -- expected done, got none in 200 cycles", nreal);
errors = errors + 1;
end else if (y !== expect_y) begin
$display("n_inputs_real=%0d: FAIL -- y=%0d expected=%0d", nreal, y, expect_y);
errors = errors + 1;
end else if (cyc > max_cycles) begin
$display("n_inputs_real=%0d: FAIL -- y=%0d correct but took %0d cycles (expected <= %0d -- suspiciously slow for this case)", nreal, y, cyc, max_cycles);
errors = errors + 1;
end else begin
$display("n_inputs_real=%0d: PASS -- y=%0d cycles=%0d (no over-read into poison region)", nreal, y, cyc);
end
end
endtask
initial begin
errors = 0;
activation = 2'd1; // ACT_RELU
// real region (0-15): x=w=1 -> contributes 1 each if read
for (i = 0; i < 16; i = i + 1) begin
x_bus[i*8 +: 8] = 8'sd1;
w_bus[i*8 +: 8] = 8'sd1;
end
// "poison" region (16-31): x=w=100 -> would saturate to 127 if
// ever read past the real limit
for (i = 16; i < 32; i = i + 1) begin
x_bus[i*8 +: 8] = 8'sd100;
w_bus[i*8 +: 8] = 8'sd100;
end
$display("--- TEST 1: n_inputs_real=16 -- early termination must NOT read the poison region ---");
run_case(16'd16, 8'sd16, 200);
$display("--- TEST 2: n_inputs_real=8 -- smaller early termination ---");
run_case(16'd8, 8'sd8, 200);
$display("--- TEST 3: n_inputs_real=32 (full) -- sanity: DOES read the poison region, saturates ---");
run_case(16'd32, 8'sd127, 200);
$display("--- TEST 4 (BUG-003 fix): n_inputs_real=0 -- must complete quickly via the 'finishing' fast path, y=activation(bias)=0 ---");
run_case(16'd0, 8'sd0, 8);
if (errors == 0)
$display("ALL TESTS PASSED (early termination certified correct for valid n_inputs_real values, TESTS 1-3; TEST 4 confirms BUG-003 fix -- n_inputs_real=0 completes quickly with y=activation(bias))");
else
$display("FAILED: %0d unexpected result(s) -- see messages above", errors);
$finish;
end
endmodule