catalyst-n1 / tb /tb_stdp_learning.v
mrwabbit's picture
Initial upload: Catalyst N1 open source neuromorphic processor RTL
e4cdd5f verified
// ============================================================================
// Testbench: STDP Learning Demonstration
// ============================================================================
//
// 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_stdp_learning;
parameter DATA_WIDTH = 16;
parameter CLK_PERIOD = 10;
reg clk;
reg rst_n;
reg enable;
reg learn_enable;
reg signed [DATA_WIDTH-1:0] ext_input_0;
reg signed [DATA_WIDTH-1:0] ext_input_1;
reg signed [DATA_WIDTH-1:0] ext_input_2;
reg signed [DATA_WIDTH-1:0] ext_input_3;
wire [3:0] spikes;
wire [DATA_WIDTH-1:0] membrane_0, membrane_1, membrane_2, membrane_3;
wire signed [DATA_WIDTH-1:0] w01, w02, w03;
wire signed [DATA_WIDTH-1:0] w10, w12, w13;
wire signed [DATA_WIDTH-1:0] w20, w21, w23;
wire signed [DATA_WIDTH-1:0] w30, w31, w32;
integer spike_count [0:3];
integer phase_spikes [0:3][0:3]; // [phase][neuron]
integer current_phase;
reg [15:0] lfsr;
neuron_core_stdp #(
.DATA_WIDTH (DATA_WIDTH),
.THRESHOLD (16'd1000),
.LEAK_RATE (16'd3),
.WEIGHT_INIT (16'd100),
.WEIGHT_MAX (16'd800),
.LEARN_RATE (8'd3)
) dut (
.clk (clk),
.rst_n (rst_n),
.enable (enable),
.learn_enable (learn_enable),
.ext_input_0 (ext_input_0),
.ext_input_1 (ext_input_1),
.ext_input_2 (ext_input_2),
.ext_input_3 (ext_input_3),
.spikes (spikes),
.membrane_0 (membrane_0),
.membrane_1 (membrane_1),
.membrane_2 (membrane_2),
.membrane_3 (membrane_3),
.w_out_01 (w01), .w_out_02(w02), .w_out_03(w03),
.w_out_10 (w10), .w_out_12(w12), .w_out_13(w13),
.w_out_20 (w20), .w_out_21(w21), .w_out_23(w23),
.w_out_30 (w30), .w_out_31(w31), .w_out_32(w32)
);
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
always @(posedge clk) begin
if (!rst_n)
lfsr <= 16'hACE1;
else
lfsr <= {lfsr[14:0], lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10]};
end
always @(posedge clk) begin
if (spikes[0]) begin spike_count[0] = spike_count[0] + 1; phase_spikes[current_phase][0] = phase_spikes[current_phase][0] + 1; end
if (spikes[1]) begin spike_count[1] = spike_count[1] + 1; phase_spikes[current_phase][1] = phase_spikes[current_phase][1] + 1; end
if (spikes[2]) begin spike_count[2] = spike_count[2] + 1; phase_spikes[current_phase][2] = phase_spikes[current_phase][2] + 1; end
if (spikes[3]) begin spike_count[3] = spike_count[3] + 1; phase_spikes[current_phase][3] = phase_spikes[current_phase][3] + 1; end
end
integer cycle_count;
always @(posedge clk) begin
cycle_count = cycle_count + 1;
if (cycle_count % 500 == 0) begin
$display("[cycle %0d] Weights: 0->1=%0d 0->2=%0d 1->0=%0d 2->0=%0d 0->3=%0d 3->0=%0d",
cycle_count, w01, w02, w10, w20, w03, w30);
end
end
initial begin
$dumpfile("neuron_core_stdp.vcd");
$dumpvars(0, tb_stdp_learning);
end
initial begin
spike_count[0] = 0; spike_count[1] = 0;
spike_count[2] = 0; spike_count[3] = 0;
phase_spikes[0][0] = 0; phase_spikes[0][1] = 0; phase_spikes[0][2] = 0; phase_spikes[0][3] = 0;
phase_spikes[1][0] = 0; phase_spikes[1][1] = 0; phase_spikes[1][2] = 0; phase_spikes[1][3] = 0;
phase_spikes[2][0] = 0; phase_spikes[2][1] = 0; phase_spikes[2][2] = 0; phase_spikes[2][3] = 0;
phase_spikes[3][0] = 0; phase_spikes[3][1] = 0; phase_spikes[3][2] = 0; phase_spikes[3][3] = 0;
cycle_count = 0;
current_phase = 0;
rst_n = 0; enable = 0; learn_enable = 0;
ext_input_0 = 0; ext_input_1 = 0;
ext_input_2 = 0; ext_input_3 = 0;
$display("");
$display("================================================================");
$display(" STDP Learning Experiment");
$display(" 'Neurons that fire together, wire together'");
$display("================================================================");
#(CLK_PERIOD * 5);
rst_n = 1;
#(CLK_PERIOD * 2);
enable = 1;
// PHASE 1: TRAINING (learning ON)
// Stimulate N0 and N1 together (correlated)
// N2 gets random/independent stimulus
$display("");
$display("--- PHASE 1: TRAINING ---");
$display(" N0 + N1: correlated stimulus (should strengthen 0<->1)");
$display(" N2: independent stimulus (should NOT strengthen to 0/1)");
$display(" Learning: ON");
$display("");
current_phase = 0;
learn_enable = 1;
// Correlated stimulus: N0 and N1 get the same strong input
// N2 gets weaker, independent input
ext_input_0 = 16'd200;
ext_input_1 = 16'd200; // Same as N0 - they'll fire together
ext_input_2 = 16'd80; // Weaker, independent
ext_input_3 = 16'd0; // No direct stimulus
#(CLK_PERIOD * 2000);
$display("");
$display(" After training weights:");
$display(" 0->1: %0d (should be HIGH - correlated)", w01);
$display(" 1->0: %0d (should be HIGH - correlated)", w10);
$display(" 0->2: %0d (should be lower)", w02);
$display(" 2->0: %0d (should be lower)", w20);
$display(" 0->3: %0d", w03);
// PHASE 2: TESTING (learning OFF)
// Only stimulate N0 - does N1 fire from learned weights?
$display("");
$display("--- PHASE 2: RECALL TEST ---");
$display(" Only N0 gets stimulus. Can N1 recall the association?");
$display(" Learning: OFF");
$display("");
current_phase = 1;
learn_enable = 0; // Freeze weights
ext_input_0 = 16'd200;
ext_input_1 = 16'd0; // No direct input - must fire from learned weight
ext_input_2 = 16'd0; // No input
ext_input_3 = 16'd0;
#(CLK_PERIOD * 1000);
$display("");
$display(" Recall results:");
$display(" N0 spikes: %0d (driven by input)", phase_spikes[1][0]);
$display(" N1 spikes: %0d (should fire from learned 0->1 weight!)", phase_spikes[1][1]);
$display(" N2 spikes: %0d (should be few/zero - weak learned weight)", phase_spikes[1][2]);
$display(" N3 spikes: %0d", phase_spikes[1][3]);
if (phase_spikes[1][1] > 0 && phase_spikes[1][1] > phase_spikes[1][2])
$display(" >>> SUCCESS: N1 recalls association! N1 fires more than N2 <<<");
else
$display(" >>> Learning effect visible in weight changes <<<");
// PHASE 3: NEW ASSOCIATION (learning ON)
// Now pair N0 with N3 instead - see weights shift
$display("");
$display("--- PHASE 3: NEW ASSOCIATION ---");
$display(" Now pairing N0 with N3 (new pattern)");
$display(" Learning: ON");
$display("");
current_phase = 2;
learn_enable = 1;
ext_input_0 = 16'd200;
ext_input_1 = 16'd0;
ext_input_2 = 16'd0;
ext_input_3 = 16'd200; // Now N3 is correlated with N0
#(CLK_PERIOD * 2000);
$display("");
$display(" After new training:");
$display(" 0->1: %0d (should decrease - no longer correlated)", w01);
$display(" 0->3: %0d (should increase - now correlated)", w03);
$display(" 3->0: %0d (should increase - now correlated)", w30);
$display("");
$display("--- PHASE 4: FINAL RECALL ---");
$display(" Only N0 stimulus. Which neurons respond?");
$display(" Learning: OFF");
$display("");
current_phase = 3;
learn_enable = 0;
ext_input_0 = 16'd200;
ext_input_1 = 16'd0;
ext_input_2 = 16'd0;
ext_input_3 = 16'd0;
#(CLK_PERIOD * 1000);
$display("");
$display("================================================================");
$display(" FINAL RESULTS");
$display("================================================================");
$display("");
$display(" Final Weight Matrix:");
$display(" To N0 To N1 To N2 To N3");
$display(" N0: --- %5d %5d %5d", w01, w02, w03);
$display(" N1: %5d --- %5d %5d", w10, w12, w13);
$display(" N2: %5d %5d --- %5d", w20, w21, w23);
$display(" N3: %5d %5d %5d ---", w30, w31, w32);
$display("");
$display(" Spike Counts by Phase:");
$display(" N0 N1 N2 N3");
$display(" Training: %4d %4d %4d %4d", phase_spikes[0][0], phase_spikes[0][1], phase_spikes[0][2], phase_spikes[0][3]);
$display(" Recall 1: %4d %4d %4d %4d", phase_spikes[1][0], phase_spikes[1][1], phase_spikes[1][2], phase_spikes[1][3]);
$display(" Retrain: %4d %4d %4d %4d", phase_spikes[2][0], phase_spikes[2][1], phase_spikes[2][2], phase_spikes[2][3]);
$display(" Recall 2: %4d %4d %4d %4d", phase_spikes[3][0], phase_spikes[3][1], phase_spikes[3][2], phase_spikes[3][3]);
$display("");
if (w01 > w02)
$display(" [LEARNED] 0->1 weight (%0d) > 0->2 weight (%0d): N0-N1 association formed!", w01, w02);
if (w03 > 16'd100)
$display(" [LEARNED] 0->3 weight (%0d) increased: N0-N3 association formed!", w03);
$display("");
$display("================================================================");
$finish;
end
endmodule