Files
FPGA-Neural/rtl/spi_neuron_top.v
T
micheleandClaude Sonnet 5 a918c3f1e9 feat: configurable activation functions + runtime-configurable network topology
Two related Phase 5 additions, both threaded the same way (a new
runtime field defaulting to the pre-existing behavior, settable
per-layer via the descriptor table or per-run via SET_BASE):

Configurable activation functions:
- neuron_parallel.v gains a 2-bit `activation` port (ACT_NONE =
  linear + two-sided INT8 saturate, ACT_RELU = the original
  hardwired behavior, kept as the default so every pre-existing
  caller/testbench is unaffected), threaded through neuron_memory.v.
- spi_engine.v: SET_BASE sel=6 (single-layer path); the descriptor
  table gains a 7th byte (multi-layer path).
- Verified in neuron_parallel_tb.v (negative pass-through + negative
  saturation to -128) and end-to-end in
  spi_neuron_top_runnetwork_tb.v (a real negative accumulator that
  ACT_RELU would clamp to 0 comes through unclamped under ACT_NONE,
  over real SPI/RAM).

Runtime network width (one bitstream, any topology up to its
build-time max, entirely host-configured over SPI):
- neuron_parallel.v gains n_inputs_real, bounding its MAC group loop
  (n_inputs_real/PARALLEL groups instead of the fixed build-time
  count). neuron_memory.v gains n_inputs_real/n_neurons_real,
  bounding its X/W RAM-read loop and its neuron loop. All default to
  the build-time max, so unconnected callers are unaffected.
  n_inputs_real must stay a multiple of PARALLEL (same constraint
  N_INPUTS itself is held to at elaboration time, now the caller's
  runtime responsibility).
- spi_engine.v: SET_BASE sel=7/8 (single-layer path); the descriptor
  table grows to 11 bytes/layer (+n_inputs_real +n_neurons_real,
  multi-layer path) -- layer_sequencer.v also now copies only
  n_neurons_real bytes into the ping-pong buffer, not the full
  build width.
- This is real early termination, not bookkeeping: no RAM
  zero-padding needed for the unused tail, and it measurably
  completes faster. neuron_parallel_tb.v TEST 7: 3 cycles vs 6 for a
  reduced-vs-full run, with garbage loaded into the skipped lanes to
  prove they're never read. neuron_memory_tb.v TEST 5: through the
  real PSRAM stack, 209 cycles vs 788. layer_sequencer_tb.v proves a
  reduced n_neurons_real shortens the ping-pong copy-out itself
  (bytes beyond the real count stay untouched, not just differing).

docs/FPGA-NeuralNetwork-Engine.md: §8.1 opcode/SET_BASE table, new
"Runtime network width" subsection, Phase 5 checklist, Current
Status table, and the "Core architectural principle" statement
updated to reflect that topology (not just trained parameters) is
now host-configured at runtime up to a build-time ceiling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
2026-09-02 20:18:24 +02:00

357 lines
12 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_NEURON_TOP
//
// Full Phase 3 + Phase 4 integration: SPI host interface (spi_slave
// + spi_engine, docs §8.1) driving neuron_memory.v (Phase 3,
// N_NEURONS>=1) through a shared PSRAM (memory_interface +
// psram_controller), arbitrated between spi_engine's own RAM access
// (WRITE_RAM/READ_RAM opcodes) and neuron_memory's own X/W/bias
// reads during a run.
//
// neuron_memory's own `rst` is the global reset OR'd with the
// RESET opcode's soft-reset pulse from spi_engine, so a host can
// recover the compute engine over SPI without a physical reset
// (RAM contents are untouched either way).
// ================================================================
module spi_neuron_top #(
parameter ADDR_WIDTH = 22,
parameter DATA_WIDTH = 8,
parameter N_INPUTS = 32,
parameter N_NEURONS = 1,
parameter PARALLEL = 8,
parameter ACC_WIDTH = 32,
parameter MEM_DATA_WIDTH = 16,
parameter CLK_FREQ_MHZ = 80,
parameter N_LAYERS = 4 // Phase 5: RUN_NETWORK, requires N_INPUTS==N_NEURONS
)(
input wire clk,
input wire rst,
// ------------------------------------------------------------
// SPI host interface
// ------------------------------------------------------------
input wire sclk,
input wire mosi,
output wire miso,
input wire cs_n,
// ------------------------------------------------------------
// PSRAM physical interface
// ------------------------------------------------------------
output wire [ADDR_WIDTH-1:0] psram_a,
inout wire [MEM_DATA_WIDTH-1:0] psram_dq,
output wire psram_ce_n,
output wire psram_oe_n,
output wire psram_we_n,
output wire psram_lb_n,
output wire psram_ub_n,
output wire psram_zz_n
);
// ============================================================
// SPI PHYSICAL LAYER
// ============================================================
wire [7:0] rx_byte;
wire rx_valid;
wire cs_start;
wire cs_end;
wire [7:0] tx_byte;
wire tx_byte_req;
spi_slave u_spi_slave (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.cs_active(), .cs_start(cs_start), .cs_end(cs_end)
);
// ============================================================
// SPI PROTOCOL ENGINE
// ============================================================
wire spi_ram_req;
wire spi_ram_wr;
wire [ADDR_WIDTH-1:0] spi_ram_addr;
wire signed [7:0] spi_ram_wdata;
wire signed [7:0] spi_ram_rdata;
wire spi_ram_ready;
wire [ADDR_WIDTH-1:0] x_base;
wire [ADDR_WIDTH-1:0] w_base;
wire [ADDR_WIDTH-1:0] bias_addr;
wire [1:0] activation;
wire [15:0] n_inputs_real;
wire [15:0] n_neurons_real;
wire nm_start;
wire nm_busy;
wire nm_done;
wire signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
wire nm_soft_rst;
// Phase 5: layer_sequencer control/status, driven by spi_engine's
// RUN_NETWORK opcode.
wire [ADDR_WIDTH-1:0] table_base;
wire [ADDR_WIDTH-1:0] buf_a_base;
wire [ADDR_WIDTH-1:0] buf_b_base;
wire run_start;
wire [7:0] run_num_layers;
wire seq_busy;
wire seq_done;
spi_engine #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL)
) u_spi_engine (
.clk(clk), .rst(rst),
.rx_byte(rx_byte), .rx_valid(rx_valid),
.cs_start(cs_start), .cs_end(cs_end),
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
.ram_req(spi_ram_req), .ram_wr(spi_ram_wr),
.ram_addr(spi_ram_addr), .ram_wdata(spi_ram_wdata),
.ram_rdata(spi_ram_rdata), .ram_ready(spi_ram_ready),
.x_base(x_base), .w_base(w_base), .bias_addr(bias_addr),
.activation(activation),
.n_inputs_real(n_inputs_real), .n_neurons_real(n_neurons_real),
.nm_start(nm_start), .nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.nm_soft_rst(nm_soft_rst),
.table_base(table_base), .buf_a_base(buf_a_base), .buf_b_base(buf_b_base),
.run_start(run_start), .run_num_layers(run_num_layers),
.seq_busy(seq_busy), .seq_done(seq_done)
);
// ============================================================
// LAYER SEQUENCER (Phase 5: RUN_NETWORK)
//
// Requires N_INPUTS == N_NEURONS (both equal N_WIDTH below) --
// see rtl/layer_sequencer.v header for why. neuron_memory is
// shared with the legacy single-layer path: the two mux_nm_*
// wires below select which master drives it, based on seq_busy.
// ============================================================
wire [ADDR_WIDTH-1:0] seq_nm_x_base;
wire [ADDR_WIDTH-1:0] seq_nm_w_base;
wire [ADDR_WIDTH-1:0] seq_nm_bias_addr;
wire [1:0] seq_nm_activation;
wire [15:0] seq_nm_n_inputs;
wire [15:0] seq_nm_n_neurons;
wire seq_nm_start;
wire seq_ram_req;
wire seq_ram_wr;
wire [ADDR_WIDTH-1:0] seq_ram_addr;
wire signed [7:0] seq_ram_wdata;
wire signed [7:0] seq_ram_rdata;
wire seq_ram_ready;
layer_sequencer #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_WIDTH(N_NEURONS),
.N_LAYERS(N_LAYERS)
) u_layer_sequencer (
.clk(clk), .rst(rst),
.run_start(run_start), .run_num_layers(run_num_layers),
.seq_busy(seq_busy), .seq_done(seq_done),
.x_base(x_base), .table_base(table_base),
.buf_a_base(buf_a_base), .buf_b_base(buf_b_base),
.nm_x_base(seq_nm_x_base), .nm_w_base(seq_nm_w_base),
.nm_bias_addr(seq_nm_bias_addr), .nm_activation(seq_nm_activation),
.nm_n_inputs(seq_nm_n_inputs), .nm_n_neurons(seq_nm_n_neurons),
.nm_start(seq_nm_start),
.nm_busy(nm_busy), .nm_done(nm_done),
.y_bus(y_bus),
.ram_req(seq_ram_req), .ram_wr(seq_ram_wr),
.ram_addr(seq_ram_addr), .ram_wdata(seq_ram_wdata),
.ram_rdata(seq_ram_rdata), .ram_ready(seq_ram_ready)
);
// neuron_memory master mux: the sequencer owns it for the whole
// duration of a RUN_NETWORK job (seq_busy), otherwise spi_engine
// drives it directly (legacy single-layer SET_BASE/START path).
wire [ADDR_WIDTH-1:0] mux_nm_x_base = seq_busy ? seq_nm_x_base : x_base;
wire [ADDR_WIDTH-1:0] mux_nm_w_base = seq_busy ? seq_nm_w_base : w_base;
wire [ADDR_WIDTH-1:0] mux_nm_bias_addr = seq_busy ? seq_nm_bias_addr : bias_addr;
wire [1:0] mux_nm_activation = seq_busy ? seq_nm_activation : activation;
wire [15:0] mux_nm_n_inputs = seq_busy ? seq_nm_n_inputs : n_inputs_real;
wire [15:0] mux_nm_n_neurons = seq_busy ? seq_nm_n_neurons : n_neurons_real;
wire mux_nm_start = seq_busy ? seq_nm_start : nm_start;
// ============================================================
// NEURON MEMORY
//
// rst is the global reset OR'd with the SPI RESET opcode pulse.
// ============================================================
wire nm_rst = rst | nm_soft_rst;
wire nm_ram_req;
wire nm_ram_wr;
wire [ADDR_WIDTH-1:0] nm_ram_addr;
wire signed [7:0] nm_ram_wdata;
wire signed [7:0] nm_ram_rdata;
wire nm_ram_ready;
neuron_memory #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.N_INPUTS(N_INPUTS),
.N_NEURONS(N_NEURONS),
.PARALLEL(PARALLEL),
.ACC_WIDTH(ACC_WIDTH)
) u_neuron_memory (
.clk(clk), .rst(nm_rst),
.start(mux_nm_start),
.mem_req(nm_ram_req), .mem_wr(nm_ram_wr),
.mem_addr(nm_ram_addr), .mem_wdata(nm_ram_wdata),
.mem_rdata(nm_ram_rdata), .mem_ready(nm_ram_ready),
.x_base(mux_nm_x_base), .w_base(mux_nm_w_base), .bias_addr(mux_nm_bias_addr),
.activation(mux_nm_activation),
.n_inputs_real(mux_nm_n_inputs), .n_neurons_real(mux_nm_n_neurons),
.y_bus(y_bus), .busy(nm_busy), .done(nm_done)
);
// ============================================================
// SHARED MEMORY ARBITER
// ============================================================
wire arb_req;
wire arb_wr;
wire [ADDR_WIDTH-1:0] arb_addr;
wire signed [7:0] arb_wdata;
wire signed [7:0] arb_rdata;
wire arb_ready;
mem_arbiter #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_arbiter (
.clk(clk), .rst(rst),
.a_req(spi_ram_req), .a_wr(spi_ram_wr),
.a_addr(spi_ram_addr), .a_wdata(spi_ram_wdata),
.a_rdata(spi_ram_rdata), .a_ready(spi_ram_ready),
.b_req(nm_ram_req), .b_wr(nm_ram_wr),
.b_addr(nm_ram_addr), .b_wdata(nm_ram_wdata),
.b_rdata(nm_ram_rdata), .b_ready(nm_ram_ready),
.c_req(seq_ram_req), .c_wr(seq_ram_wr),
.c_addr(seq_ram_addr), .c_wdata(seq_ram_wdata),
.c_rdata(seq_ram_rdata), .c_ready(seq_ram_ready),
.m_req(arb_req), .m_wr(arb_wr),
.m_addr(arb_addr), .m_wdata(arb_wdata),
.m_rdata(arb_rdata), .m_ready(arb_ready)
);
// ============================================================
// BYTE <-> WORD BRIDGE (shared, single instance)
// ============================================================
wire i8_mem_req;
wire i8_mem_wr;
wire [ADDR_WIDTH-1:0] i8_mem_addr;
wire [MEM_DATA_WIDTH-1:0] i8_mem_wdata;
wire i8_mem_lb_n;
wire i8_mem_ub_n;
wire [MEM_DATA_WIDTH-1:0] i8_mem_rdata;
wire i8_mem_ready;
int8_memory_access #(
.ADDR_WIDTH(ADDR_WIDTH)
) u_int8_access (
.clk(clk), .rst(rst),
.req(arb_req), .wr(arb_wr), .addr(arb_addr), .wdata(arb_wdata),
.rdata(arb_rdata), .ready(arb_ready),
.mem_req(i8_mem_req), .mem_wr(i8_mem_wr),
.mem_addr(i8_mem_addr), .mem_wdata(i8_mem_wdata),
.mem_lb_n(i8_mem_lb_n), .mem_ub_n(i8_mem_ub_n),
.mem_rdata(i8_mem_rdata), .mem_ready(i8_mem_ready)
);
// ============================================================
// MEMORY INTERFACE / PSRAM CONTROLLER
// ============================================================
wire [MEM_DATA_WIDTH-1:0] psram_mem_rdata;
wire psram_mem_ready;
wire psram_mem_req;
wire psram_mem_wr;
wire [ADDR_WIDTH-1:0] psram_mem_addr;
wire [MEM_DATA_WIDTH-1:0] psram_mem_wdata;
wire psram_mem_lb_n;
wire psram_mem_ub_n;
memory_interface #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH)
) u_memory_if (
.clk(clk), .rst(rst),
.req(i8_mem_req), .wr(i8_mem_wr), .addr(i8_mem_addr), .wdata(i8_mem_wdata),
.lb_n(i8_mem_lb_n), .ub_n(i8_mem_ub_n),
.rdata(i8_mem_rdata), .ready(i8_mem_ready),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready)
);
psram_controller #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(MEM_DATA_WIDTH),
.CLK_FREQ_MHZ(CLK_FREQ_MHZ)
) u_psram_ctrl (
.clk(clk), .rst(rst),
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr),
.mem_addr(psram_mem_addr), .mem_wdata(psram_mem_wdata),
.mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready),
.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)
);
endmodule