catalyst-n1 / tb /tb_programmable_neuron.v
mrwabbit's picture
Initial upload: Catalyst N1 open source neuromorphic processor RTL
e4cdd5f verified
// ============================================================================
// Testbench: Programmable Neuron Parameters (Phase 9)
// ============================================================================
//
// Copyright 2026 Henry Arthur Shulayev Barnes / Catalyst Neuromorphic Ltd
// Company No. 17054540 — UK Patent Application No. 2602902.6
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ============================================================================
`timescale 1ns / 1ps
module tb_programmable_neuron;
parameter NUM_NEURONS = 256;
parameter NEURON_BITS = 8;
parameter DATA_WIDTH = 16;
parameter MAX_FANOUT = 32;
parameter FANOUT_BITS = 5;
parameter CONN_ADDR_BITS = 13;
parameter CLK_PERIOD = 10;
reg clk;
reg rst_n;
reg start;
reg learn_enable;
reg graded_enable;
reg ext_valid;
reg [NEURON_BITS-1:0] ext_neuron_id;
reg signed [DATA_WIDTH-1:0] ext_current;
reg conn_we;
reg [NEURON_BITS-1:0] conn_src;
reg [FANOUT_BITS-1:0] conn_slot;
reg [NEURON_BITS-1:0] conn_target;
reg signed [DATA_WIDTH-1:0] conn_weight;
reg prog_param_we;
reg [NEURON_BITS-1:0] prog_param_neuron;
reg [2:0] prog_param_id;
reg signed [DATA_WIDTH-1:0] prog_param_value;
wire timestep_done;
wire spike_out_valid;
wire [NEURON_BITS-1:0] spike_out_id;
wire [7:0] spike_out_payload;
wire [4:0] state_out;
wire [31:0] total_spikes;
wire [31:0] timestep_count;
scalable_core_v2 #(
.NUM_NEURONS (NUM_NEURONS),
.NEURON_BITS (NEURON_BITS),
.DATA_WIDTH (DATA_WIDTH),
.MAX_FANOUT (MAX_FANOUT),
.FANOUT_BITS (FANOUT_BITS),
.CONN_ADDR_BITS(CONN_ADDR_BITS),
.THRESHOLD (16'sd1000),
.LEAK_RATE (16'sd3),
.RESTING_POT (16'sd0),
.REFRAC_CYCLES (2),
.TRACE_MAX (8'd100),
.TRACE_DECAY (8'd10),
.LEARN_SHIFT (3),
.WEIGHT_MAX (16'sd2000),
.WEIGHT_MIN (16'sd0)
) dut (
.clk (clk),
.rst_n (rst_n),
.start (start),
.learn_enable (learn_enable),
.graded_enable (graded_enable),
.dendritic_enable(1'b0),
.ext_valid (ext_valid),
.ext_neuron_id (ext_neuron_id),
.ext_current (ext_current),
.conn_we (conn_we),
.conn_src (conn_src),
.conn_slot (conn_slot),
.conn_target (conn_target),
.conn_weight (conn_weight),
.conn_comp (2'd0),
.prog_param_we (prog_param_we),
.prog_param_neuron(prog_param_neuron),
.prog_param_id (prog_param_id),
.prog_param_value (prog_param_value),
.timestep_done (timestep_done),
.spike_out_valid(spike_out_valid),
.spike_out_id (spike_out_id),
.spike_out_payload(spike_out_payload),
.state_out (state_out),
.total_spikes (total_spikes),
.timestep_count (timestep_count)
);
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
task program_conn;
input [NEURON_BITS-1:0] src;
input [FANOUT_BITS-1:0] slot;
input [NEURON_BITS-1:0] target;
input signed [DATA_WIDTH-1:0] weight;
begin
@(posedge clk);
conn_we <= 1;
conn_src <= src;
conn_slot <= slot;
conn_target <= target;
conn_weight <= weight;
@(posedge clk);
conn_we <= 0;
@(posedge clk);
end
endtask
task set_param;
input [NEURON_BITS-1:0] neuron;
input [2:0] param_id;
input signed [DATA_WIDTH-1:0] value;
begin
@(posedge clk);
prog_param_we <= 1;
prog_param_neuron <= neuron;
prog_param_id <= param_id;
prog_param_value <= value;
@(posedge clk);
prog_param_we <= 0;
@(posedge clk);
end
endtask
task stimulate;
input [NEURON_BITS-1:0] neuron;
input signed [DATA_WIDTH-1:0] current;
begin
@(posedge clk);
ext_valid <= 1;
ext_neuron_id <= neuron;
ext_current <= current;
@(posedge clk);
ext_valid <= 0;
end
endtask
task run_timestep;
begin
@(posedge clk);
start <= 1;
@(posedge clk);
start <= 0;
wait(timestep_done);
@(posedge clk);
end
endtask
// Read membrane potential
function signed [DATA_WIDTH-1:0] read_potential;
input [NEURON_BITS-1:0] neuron;
begin
read_potential = dut.neuron_mem.mem[neuron];
end
endfunction
// Read threshold parameter
function signed [DATA_WIDTH-1:0] read_threshold;
input [NEURON_BITS-1:0] neuron;
begin
read_threshold = dut.threshold_mem.mem[neuron];
end
endfunction
integer spike_count_per_neuron [0:NUM_NEURONS-1];
integer first_spike_ts [0:NUM_NEURONS-1];
integer total_spike_count;
integer i;
always @(posedge clk) begin
if (spike_out_valid) begin
spike_count_per_neuron[spike_out_id] =
spike_count_per_neuron[spike_out_id] + 1;
if (first_spike_ts[spike_out_id] == -1)
first_spike_ts[spike_out_id] = timestep_count;
total_spike_count = total_spike_count + 1;
end
end
task reset_spike_tracking;
begin
for (i = 0; i < NUM_NEURONS; i = i + 1) begin
spike_count_per_neuron[i] = 0;
first_spike_ts[i] = -1;
end
total_spike_count = 0;
end
endtask
integer pass_count, fail_count;
integer t;
initial begin
rst_n = 0;
start = 0;
learn_enable = 0;
graded_enable = 0;
ext_valid = 0;
conn_we = 0;
conn_src = 0;
conn_slot = 0;
conn_target = 0;
conn_weight = 0;
prog_param_we = 0;
prog_param_neuron = 0;
prog_param_id = 0;
prog_param_value = 0;
ext_neuron_id = 0;
ext_current = 0;
pass_count = 0;
fail_count = 0;
reset_spike_tracking();
#(CLK_PERIOD * 5);
rst_n = 1;
#(CLK_PERIOD * 3);
$display("");
$display("================================================================");
$display(" Programmable Neuron Parameters Test (Phase 9)");
$display("================================================================");
// TEST 1: Default Values (no programming)
// N0 with default threshold=1000, leak=3
// Stimulus=200/ts -> need ~6 timesteps to reach 1000
// (200-3)*5 = 985 < 1000, (200-3)*6 = 1182 >= 1000 -> spike at ts ~5-6
$display("");
$display("--- TEST 1: Default Values (backward compatibility) ---");
reset_spike_tracking();
for (t = 0; t < 10; t = t + 1) begin
stimulate(8'd0, 16'sd200);
run_timestep;
end
$display(" N0 spikes (default threshold=1000): %0d", spike_count_per_neuron[0]);
$display(" N0 first spike at timestep: %0d", first_spike_ts[0]);
// With stim=200, leak=3: net=197/ts. Threshold=1000.
// Accumulation: 197, 394, 591, 788, 985, 1182 -> spike at ts 5 (0-indexed)
if (spike_count_per_neuron[0] > 0 && first_spike_ts[0] >= 4 && first_spike_ts[0] <= 6) begin
$display(" PASS: Default parameters work correctly");
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected spike around ts 5, got first=%0d count=%0d",
first_spike_ts[0], spike_count_per_neuron[0]);
fail_count = fail_count + 1;
end
// Verify threshold SRAM was initialized to default
begin : test1_verify
reg signed [DATA_WIDTH-1:0] thr_val;
thr_val = read_threshold(8'd0);
$display(" Threshold SRAM N0 = %0d (expected 1000)", thr_val);
if (thr_val == 16'sd1000) begin
$display(" PASS: Threshold SRAM initialized correctly");
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected 1000, got %0d", thr_val);
fail_count = fail_count + 1;
end
end
// TEST 2: Per-Neuron Threshold Variation
// N10: threshold=500 (low), N11: threshold=1500 (high), N12: default=1000
// Same stimulus -> N10 fires first, N12 second, N11 last
$display("");
$display("--- TEST 2: Per-Neuron Threshold Variation ---");
reset_spike_tracking();
set_param(8'd10, 3'd0, 16'sd500); // N10: low threshold
set_param(8'd11, 3'd0, 16'sd1500); // N11: high threshold
// N12: keep default=1000
// Verify SRAM write
$display(" N10 threshold = %0d (programmed 500)", read_threshold(8'd10));
$display(" N11 threshold = %0d (programmed 1500)", read_threshold(8'd11));
$display(" N12 threshold = %0d (default 1000)", read_threshold(8'd12));
// Stimulate all three with same current
for (t = 0; t < 15; t = t + 1) begin
stimulate(8'd10, 16'sd200);
run_timestep;
stimulate(8'd11, 16'sd200);
run_timestep;
stimulate(8'd12, 16'sd200);
run_timestep;
end
$display(" N10 spikes: %0d (first at ts %0d) - threshold=500",
spike_count_per_neuron[10], first_spike_ts[10]);
$display(" N11 spikes: %0d (first at ts %0d) - threshold=1500",
spike_count_per_neuron[11], first_spike_ts[11]);
$display(" N12 spikes: %0d (first at ts %0d) - threshold=1000",
spike_count_per_neuron[12], first_spike_ts[12]);
// N10 (thr=500): 197, 394, 591 -> spikes at ts ~2
// N12 (thr=1000): needs ~6 stimulations
// N11 (thr=1500): needs ~8 stimulations
// Since we stimulate each neuron every 3 timesteps:
// N10 first spike should be earliest, N11 last
if (first_spike_ts[10] < first_spike_ts[12] &&
first_spike_ts[12] < first_spike_ts[11]) begin
$display(" PASS: N10 < N12 < N11 (low thr fires first)");
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected N10 < N12 < N11 ordering");
fail_count = fail_count + 1;
end
if (spike_count_per_neuron[10] > spike_count_per_neuron[11]) begin
$display(" PASS: Low threshold neuron fires more often (%0d > %0d)",
spike_count_per_neuron[10], spike_count_per_neuron[11]);
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected N10 > N11 spike count");
fail_count = fail_count + 1;
end
// TEST 3: Per-Neuron Leak Rate Variation
// N20: leak=1 (slow decay), N21: leak=50 (fast decay)
// Give sub-threshold stimulus then check potential retention
$display("");
$display("--- TEST 3: Per-Neuron Leak Rate Variation ---");
reset_spike_tracking();
set_param(8'd20, 3'd1, 16'sd1); // N20: very slow leak
set_param(8'd21, 3'd1, 16'sd50); // N21: very fast leak
// Give both 3 stimulations of 200 each
for (t = 0; t < 3; t = t + 1) begin
stimulate(8'd20, 16'sd200);
run_timestep;
stimulate(8'd21, 16'sd200);
run_timestep;
end
// Now run 5 empty timesteps (no stimulus) - let them leak
for (t = 0; t < 5; t = t + 1) begin
run_timestep;
end
begin : test3_block
reg signed [DATA_WIDTH-1:0] pot20, pot21;
pot20 = read_potential(8'd20);
pot21 = read_potential(8'd21);
$display(" N20 potential (leak=1): %0d", pot20);
$display(" N21 potential (leak=50): %0d", pot21);
// N20 should retain much more potential than N21
if (pot20 > pot21) begin
$display(" PASS: Slow-leak neuron retains more potential (%0d > %0d)", pot20, pot21);
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected N20 > N21 (%0d vs %0d)", pot20, pot21);
fail_count = fail_count + 1;
end
end
// TEST 4: Per-Neuron Refractory Period Variation
// N30: refrac=1 (fast recovery), N31: refrac=10 (slow recovery)
// Strong continuous stimulus -> N30 fires more often
$display("");
$display("--- TEST 4: Per-Neuron Refractory Period Variation ---");
reset_spike_tracking();
set_param(8'd30, 3'd3, 16'sd1); // N30: refrac=1 (fast)
set_param(8'd31, 3'd3, 16'sd10); // N31: refrac=10 (slow)
// Strong stimulus to both (above threshold in one shot)
for (t = 0; t < 30; t = t + 1) begin
stimulate(8'd30, 16'sd1200);
run_timestep;
stimulate(8'd31, 16'sd1200);
run_timestep;
end
$display(" N30 spikes (refrac=1): %0d", spike_count_per_neuron[30]);
$display(" N31 spikes (refrac=10): %0d", spike_count_per_neuron[31]);
// N30 should fire much more often (recovers in 1 cycle vs 10)
if (spike_count_per_neuron[30] > spike_count_per_neuron[31]) begin
$display(" PASS: Fast-recovery neuron fires more (%0d > %0d)",
spike_count_per_neuron[30], spike_count_per_neuron[31]);
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected N30 > N31 spike count");
fail_count = fail_count + 1;
end
// TEST 5: Mixed Population Chain
// N40->N41: N40 threshold=500, N41 threshold=1500
// N50->N51: N50 threshold=1500, N51 threshold=500
// Same stimulus -> first chain propagates, second doesn't
$display("");
$display("--- TEST 5: Mixed Population Chain ---");
reset_spike_tracking();
// Chain 1: easy source -> hard target
set_param(8'd40, 3'd0, 16'sd500); // N40: low threshold
set_param(8'd41, 3'd0, 16'sd1500); // N41: high threshold
program_conn(8'd40, 5'd0, 8'd41, 16'sd600);
// Chain 2: hard source -> easy target
set_param(8'd50, 3'd0, 16'sd1500); // N50: high threshold
set_param(8'd51, 3'd0, 16'sd500); // N51: low threshold
program_conn(8'd50, 5'd0, 8'd51, 16'sd600);
// Moderate stimulus to both sources
for (t = 0; t < 20; t = t + 1) begin
stimulate(8'd40, 16'sd200);
run_timestep;
stimulate(8'd50, 16'sd200);
run_timestep;
end
$display(" Chain 1: N40(thr=500) spikes=%0d, N41(thr=1500) spikes=%0d",
spike_count_per_neuron[40], spike_count_per_neuron[41]);
$display(" Chain 2: N50(thr=1500) spikes=%0d, N51(thr=500) spikes=%0d",
spike_count_per_neuron[50], spike_count_per_neuron[51]);
// N40 fires easily (low threshold), but N41 is hard to trigger
// N50 fires rarely (high threshold), but when it does N51 triggers easily
if (spike_count_per_neuron[40] > spike_count_per_neuron[50]) begin
$display(" PASS: Low-threshold source fires more (%0d > %0d)",
spike_count_per_neuron[40], spike_count_per_neuron[50]);
pass_count = pass_count + 1;
end else begin
$display(" FAIL: Expected N40 > N50");
fail_count = fail_count + 1;
end
$display("");
$display("================================================================");
$display(" PROGRAMMABLE NEURON TEST RESULTS: %0d PASS, %0d FAIL",
pass_count, fail_count);
$display("================================================================");
if (fail_count == 0)
$display(" ALL TESTS PASSED");
else
$display(" SOME TESTS FAILED");
$display("================================================================");
#(CLK_PERIOD * 10);
$finish;
end
initial begin
#(CLK_PERIOD * 5_000_000);
$display("TIMEOUT");
$finish;
end
endmodule