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
775 lines
29 KiB
Verilog
775 lines
29 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// SPI_ENGINE TESTBENCH
|
|
//
|
|
// Drives spi_slave.v + spi_engine.v together through a simulated
|
|
// SPI master (same clk-cycle-counted BFM proven in spi_slave_tb.v),
|
|
// against a synthetic byte-RAM model (small fixed latency, isolates
|
|
// the opcode FSM from the full PSRAM stack) and a manually-driven
|
|
// neuron_memory mock (nm_busy/nm_done/y_bus driven by the test,
|
|
// x_base/w_base/bias_addr/nm_start/nm_soft_rst observed).
|
|
//
|
|
// Covers every opcode plus a few protocol edge cases:
|
|
// A: WRITE_RAM then READ_RAM back
|
|
// B: SET_BASE for X/W/BIAS
|
|
// C: START (accepted when idle, ignored when busy)
|
|
// D: STATUS (live busy bit, sticky done bit, clear-on-read)
|
|
// E: RESET (pulses nm_soft_rst, clears sticky done)
|
|
// F: READ_OUTPUT (N_NEURONS=3 bytes, neuron-major)
|
|
// G: READ_CONFIG (8-byte hardware record)
|
|
// H: NOP (no side effects)
|
|
// I: WRITE_RAM with more MOSI bytes than len (extra bytes ignored)
|
|
// J: back-to-back transactions (state resets cleanly via cs_end)
|
|
// K: SET_BASE for TABLE/BUF_A/BUF_B (Phase 5)
|
|
// L: RUN_NETWORK (Phase 5: accepted/ignored gating, STATUS.busy
|
|
// following seq_busy, STATUS.done latching on seq_done only)
|
|
// M: SET_BASE for ACTIVATION (configurable activation functions)
|
|
// N: SET_BASE for N_INPUTS/N_NEURONS (runtime topology width)
|
|
// ================================================================
|
|
|
|
module tb;
|
|
|
|
localparam ADDR_WIDTH = 23;
|
|
localparam DATA_WIDTH = 8;
|
|
localparam N_INPUTS = 32;
|
|
localparam N_NEURONS = 3;
|
|
localparam PARALLEL = 8;
|
|
|
|
localparam CLK_PERIOD = 12.5; // 80 MHz
|
|
|
|
reg clk;
|
|
reg rst;
|
|
|
|
initial begin
|
|
clk = 1'b0;
|
|
forever #(CLK_PERIOD / 2.0) clk = ~clk;
|
|
end
|
|
|
|
// ============================================================
|
|
// SPI PINS
|
|
// ============================================================
|
|
|
|
reg sclk;
|
|
reg mosi;
|
|
wire miso;
|
|
reg cs_n;
|
|
|
|
// ============================================================
|
|
// spi_slave <-> spi_engine byte-level bus
|
|
// ============================================================
|
|
|
|
wire [7:0] rx_byte;
|
|
wire rx_valid;
|
|
wire cs_start;
|
|
wire cs_end_w;
|
|
wire [7:0] tx_byte;
|
|
wire tx_byte_req;
|
|
|
|
spi_slave u_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_w)
|
|
);
|
|
|
|
// ============================================================
|
|
// spi_engine <-> synthetic RAM
|
|
// ============================================================
|
|
|
|
wire ram_req;
|
|
wire ram_wr;
|
|
wire [ADDR_WIDTH-1:0] ram_addr;
|
|
wire signed [7:0] ram_wdata;
|
|
|
|
reg signed [7:0] ram_rdata;
|
|
reg ram_ready;
|
|
|
|
wire [ADDR_WIDTH-1:0] x_base;
|
|
wire [ADDR_WIDTH-1:0] w_base;
|
|
wire [ADDR_WIDTH-1:0] bias_addr;
|
|
|
|
wire nm_start;
|
|
reg nm_busy;
|
|
reg nm_done;
|
|
|
|
reg signed [DATA_WIDTH*N_NEURONS-1:0] y_bus;
|
|
|
|
wire nm_soft_rst;
|
|
|
|
// Phase 5 layer_sequencer ports: this testbench only exercises
|
|
// spi_engine's legacy single-layer path, so seq_busy/seq_done
|
|
// are tied off (must be driven, not left floating -- a floating
|
|
// input here would make busy_all/done_event unknown ('x') and
|
|
// silently break the existing OP_START/STATUS tests below).
|
|
// table_base/buf_a_base/buf_b_base/run_start/run_num_layers are
|
|
// outputs and are left unconnected on purpose.
|
|
reg seq_busy = 1'b0;
|
|
reg seq_done = 1'b0;
|
|
|
|
// Phase G5 graph_engine ports: same reasoning as seq_busy/
|
|
// seq_done above -- this testbench never exercises net_type=
|
|
// graph, but graph_busy/graph_done/graph_err feed busy_all/
|
|
// done_event/status_snapshot directly, so they must be tied to
|
|
// known-idle values rather than left floating. net_type/
|
|
// num_neurons_graph/n_out are outputs, left unconnected like the
|
|
// other SET_BASE-driven outputs above.
|
|
reg graph_busy = 1'b0;
|
|
reg graph_done = 1'b0;
|
|
reg graph_err = 1'b0;
|
|
|
|
wire [ADDR_WIDTH-1:0] table_base;
|
|
wire [ADDR_WIDTH-1:0] buf_a_base;
|
|
wire [ADDR_WIDTH-1:0] buf_b_base;
|
|
wire [1:0] activation;
|
|
wire [15:0] n_inputs_real;
|
|
wire [15:0] n_neurons_real;
|
|
wire run_start;
|
|
wire [7:0] run_num_layers;
|
|
|
|
spi_engine #(
|
|
.ADDR_WIDTH(ADDR_WIDTH),
|
|
.DATA_WIDTH(DATA_WIDTH),
|
|
.N_INPUTS(N_INPUTS),
|
|
.N_NEURONS(N_NEURONS),
|
|
.PARALLEL(PARALLEL)
|
|
) u_engine (
|
|
.clk(clk), .rst(rst),
|
|
|
|
.rx_byte(rx_byte), .rx_valid(rx_valid),
|
|
.cs_start(cs_start), .cs_end(cs_end_w),
|
|
.tx_byte(tx_byte), .tx_byte_req(tx_byte_req),
|
|
|
|
.ram_req(ram_req), .ram_wr(ram_wr), .ram_addr(ram_addr), .ram_wdata(ram_wdata),
|
|
.ram_rdata(ram_rdata), .ram_ready(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),
|
|
|
|
.net_type(), .num_neurons_graph(), .n_out(),
|
|
.graph_busy(graph_busy), .graph_done(graph_done), .graph_err(graph_err)
|
|
);
|
|
|
|
// ============================================================
|
|
// SYNTHETIC BYTE-RAM MODEL
|
|
//
|
|
// Fixed 2-cycle latency (req seen -> 1 extra cycle -> ready
|
|
// pulse), independent of the real PSRAM stack, to isolate
|
|
// spi_engine's own request/ready handshake correctness.
|
|
// ============================================================
|
|
|
|
reg [7:0] ram_mem [0:1023];
|
|
|
|
localparam RAM_IDLE = 1'b0;
|
|
localparam RAM_WAIT = 1'b1;
|
|
reg ram_state;
|
|
reg [ADDR_WIDTH-1:0] ram_addr_latched;
|
|
reg ram_wr_latched;
|
|
reg signed [7:0] ram_wdata_latched;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
ram_state <= RAM_IDLE;
|
|
ram_ready <= 1'b0;
|
|
ram_rdata <= 8'sd0;
|
|
end else begin
|
|
|
|
ram_ready <= 1'b0;
|
|
|
|
case (ram_state)
|
|
|
|
RAM_IDLE: begin
|
|
if (ram_req) begin
|
|
ram_addr_latched <= ram_addr;
|
|
ram_wr_latched <= ram_wr;
|
|
ram_wdata_latched <= ram_wdata;
|
|
ram_state <= RAM_WAIT;
|
|
end
|
|
end
|
|
|
|
RAM_WAIT: begin
|
|
if (ram_wr_latched)
|
|
ram_mem[ram_addr_latched] <= ram_wdata_latched;
|
|
else
|
|
ram_rdata <= $signed(ram_mem[ram_addr_latched]);
|
|
|
|
ram_ready <= 1'b1;
|
|
ram_state <= RAM_IDLE;
|
|
end
|
|
|
|
endcase
|
|
|
|
end
|
|
end
|
|
|
|
// ============================================================
|
|
// SPI MASTER BFM (same pattern as sim/spi_slave_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 = 6; // half-bit cycles for all transfers in this bench
|
|
|
|
// ============================================================
|
|
// nm_start / nm_soft_rst pulse latches (declared here, ahead of
|
|
// the main initial block below, since Icarus in -g2012 mode
|
|
// still requires declaration-before-use for plain Verilog regs)
|
|
// ============================================================
|
|
|
|
reg nm_start_seen;
|
|
reg nm_soft_rst_seen;
|
|
reg run_start_seen;
|
|
|
|
always @(posedge clk) begin
|
|
if (nm_start) nm_start_seen <= 1'b1;
|
|
if (nm_soft_rst) nm_soft_rst_seen <= 1'b1;
|
|
if (run_start) run_start_seen <= 1'b1;
|
|
end
|
|
|
|
reg [7:0] rx_tmp;
|
|
integer errors;
|
|
integer errors_before;
|
|
integer i;
|
|
|
|
// ============================================================
|
|
// MAIN
|
|
// ============================================================
|
|
|
|
initial begin
|
|
|
|
$dumpfile("sim/spi_engine.vcd");
|
|
$dumpvars(0, tb);
|
|
|
|
rst = 1'b1;
|
|
cs_n = 1'b1;
|
|
sclk = 1'b0;
|
|
mosi = 1'b0;
|
|
nm_busy = 1'b0;
|
|
nm_done = 1'b0;
|
|
y_bus = 0;
|
|
errors = 0;
|
|
|
|
for (i = 0; i < 1024; i = i + 1)
|
|
ram_mem[i] = 8'h00;
|
|
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
repeat (5) @(posedge clk);
|
|
|
|
$display("");
|
|
$display("========================================");
|
|
$display("SPI_ENGINE TEST");
|
|
$display("========================================");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST A: WRITE_RAM then READ_RAM back
|
|
// WRITE_RAM opcode=0x01, addr=0x000010 (3B), len=0x0004 (2B),
|
|
// data = 11 22 33 44.
|
|
// READ_RAM opcode=0x02, addr=0x000010 (3B), len=0x0004 (2B).
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h01, HB, rx_tmp); // WRITE_RAM
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[23:16]
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[15:8]
|
|
spi_xfer_byte(8'h10, HB, rx_tmp); // addr[7:0]
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // len[15:8]
|
|
spi_xfer_byte(8'h04, HB, rx_tmp); // len[7:0]
|
|
spi_xfer_byte(8'h11, HB, rx_tmp);
|
|
spi_xfer_byte(8'h22, HB, rx_tmp);
|
|
spi_xfer_byte(8'h33, HB, rx_tmp);
|
|
spi_xfer_byte(8'h44, HB, rx_tmp);
|
|
spi_end(HB);
|
|
|
|
clk_wait(4);
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h02, HB, rx_tmp); // READ_RAM
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[23:16]
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // addr[15:8]
|
|
spi_xfer_byte(8'h10, HB, rx_tmp); // addr[7:0]
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // len[15:8]
|
|
spi_xfer_byte(8'h04, HB, rx_tmp); // len[7:0]
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h11, "A: byte0");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h22, "A: byte1");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h33, "A: byte2");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h44, "A: byte3");
|
|
spi_end(HB);
|
|
|
|
report("TEST A: WRITE_RAM / READ_RAM");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST B: SET_BASE for X, W, BIAS
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
set_base(8'h00, 22'h000001); // X_BASE
|
|
set_base(8'h01, 22'h000101); // W_BASE
|
|
set_base(8'h02, 22'h000201); // BIAS_ADDR
|
|
|
|
clk_wait(2);
|
|
|
|
if (x_base !== 22'h000001) begin $display(" FAIL: x_base = 0x%06x", x_base); errors = errors + 1; end
|
|
if (w_base !== 22'h000101) begin $display(" FAIL: w_base = 0x%06x", w_base); errors = errors + 1; end
|
|
if (bias_addr !== 22'h000201) begin $display(" FAIL: bias_addr = 0x%06x", bias_addr); errors = errors + 1; end
|
|
|
|
report("TEST B: SET_BASE (X/W/BIAS)");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST C: START -- accepted when idle, ignored when busy
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
nm_busy = 1'b0;
|
|
nm_start_seen = 1'b0;
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h20, HB, rx_tmp); // START
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (!nm_start_seen) begin
|
|
$display(" FAIL: nm_start not pulsed while idle");
|
|
errors = errors + 1;
|
|
end
|
|
|
|
// now busy: START must be ignored
|
|
nm_busy = 1'b1;
|
|
nm_start_seen = 1'b0;
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h20, HB, rx_tmp); // START
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (nm_start_seen) begin
|
|
$display(" FAIL: nm_start pulsed while busy (should be ignored)");
|
|
errors = errors + 1;
|
|
end
|
|
nm_busy = 1'b0;
|
|
|
|
report("TEST C: START (idle vs busy)");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST D: STATUS -- live busy, sticky/clear-on-read done
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
nm_busy = 1'b1;
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[0] !== 1'b1) begin $display(" FAIL: busy bit not set while nm_busy=1"); errors = errors + 1; end
|
|
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit set before any nm_done pulse"); errors = errors + 1; end
|
|
nm_busy = 1'b0;
|
|
|
|
// pulse nm_done, then read STATUS well after the pulse: must still be set (sticky)
|
|
@(negedge clk); nm_done = 1'b1; @(negedge clk); nm_done = 1'b0;
|
|
clk_wait(10);
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[1] !== 1'b1) begin $display(" FAIL: done bit not sticky after nm_done pulse"); errors = errors + 1; end
|
|
|
|
// reading STATUS must clear done
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit not cleared after STATUS read"); errors = errors + 1; end
|
|
|
|
report("TEST D: STATUS (busy live, done sticky/clear-on-read)");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST E: RESET -- pulses nm_soft_rst, clears sticky done
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
@(negedge clk); nm_done = 1'b1; @(negedge clk); nm_done = 1'b0;
|
|
clk_wait(4);
|
|
|
|
nm_soft_rst_seen = 1'b0;
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h0F, HB, rx_tmp); // RESET
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (!nm_soft_rst_seen) begin
|
|
$display(" FAIL: nm_soft_rst not pulsed by RESET opcode");
|
|
errors = errors + 1;
|
|
end
|
|
|
|
clk_wait(4);
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit still set after RESET"); errors = errors + 1; end
|
|
|
|
report("TEST E: RESET");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST F: READ_OUTPUT (N_NEURONS=3, neuron-major)
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
y_bus[0*DATA_WIDTH +: DATA_WIDTH] = 8'sd10;
|
|
y_bus[1*DATA_WIDTH +: DATA_WIDTH] = -8'sd20;
|
|
y_bus[2*DATA_WIDTH +: DATA_WIDTH] = 8'sd127;
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h22, HB, rx_tmp); // READ_OUTPUT
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'sd10, "F: neuron0");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, -8'sd20, "F: neuron1");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'sd127, "F: neuron2");
|
|
spi_end(HB);
|
|
|
|
report("TEST F: READ_OUTPUT");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST G: READ_CONFIG
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h30, HB, rx_tmp); // READ_CONFIG
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, ADDR_WIDTH[7:0], "G: ADDR_WIDTH");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_INPUTS[15:8], "G: N_INPUTS hi");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_INPUTS[7:0], "G: N_INPUTS lo");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, N_NEURONS[7:0], "G: N_NEURONS");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, PARALLEL[7:0], "G: PARALLEL");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, DATA_WIDTH[7:0], "G: DATA_WIDTH");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h00, "G: version hi");
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); miso_check(rx_tmp, 8'h01, "G: version lo");
|
|
spi_end(HB);
|
|
|
|
report("TEST G: READ_CONFIG");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST H: NOP -- no side effects
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // NOP
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (x_base !== 22'h000001 || w_base !== 22'h000101 || bias_addr !== 22'h000201) begin
|
|
$display(" FAIL: NOP changed base registers");
|
|
errors = errors + 1;
|
|
end
|
|
|
|
report("TEST H: NOP");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST I: WRITE_RAM with more MOSI bytes than len
|
|
// len=2 but 4 data bytes sent; only the first 2 must land.
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
ram_mem[16'h0300] = 8'hFF; // sentinel: must NOT be overwritten
|
|
ram_mem[16'h0301] = 8'hFF;
|
|
ram_mem[16'h0302] = 8'hFF; // sentinel: extra byte must not land here
|
|
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h01, HB, rx_tmp); // WRITE_RAM
|
|
spi_xfer_byte(8'h00, HB, rx_tmp);
|
|
spi_xfer_byte(8'h03, HB, rx_tmp);
|
|
spi_xfer_byte(8'h00, HB, rx_tmp); // addr = 0x000300
|
|
spi_xfer_byte(8'h00, HB, rx_tmp);
|
|
spi_xfer_byte(8'h02, HB, rx_tmp); // len = 2
|
|
spi_xfer_byte(8'hAA, HB, rx_tmp); // byte 0 (written)
|
|
spi_xfer_byte(8'hBB, HB, rx_tmp); // byte 1 (written)
|
|
spi_xfer_byte(8'hCC, HB, rx_tmp); // byte 2 (must be ignored)
|
|
spi_xfer_byte(8'hDD, HB, rx_tmp); // byte 3 (must be ignored)
|
|
spi_end(HB);
|
|
|
|
clk_wait(4);
|
|
|
|
if (ram_mem[16'h0300] !== 8'hAA) begin $display(" FAIL: ram[0x300]=0x%02x expected 0xAA", ram_mem[16'h0300]); errors = errors + 1; end
|
|
if (ram_mem[16'h0301] !== 8'hBB) begin $display(" FAIL: ram[0x301]=0x%02x expected 0xBB", ram_mem[16'h0301]); errors = errors + 1; end
|
|
if (ram_mem[16'h0302] !== 8'hFF) begin $display(" FAIL: ram[0x302] was overwritten (extra byte not ignored)"); errors = errors + 1; end
|
|
|
|
report("TEST I: WRITE_RAM extra MOSI bytes ignored");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST J: back-to-back transactions
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
set_base(8'h00, 22'h000005);
|
|
set_base(8'h01, 22'h000006);
|
|
|
|
if (x_base !== 22'h000005) begin $display(" FAIL: x_base after back-to-back = 0x%06x", x_base); errors = errors + 1; end
|
|
if (w_base !== 22'h000006) begin $display(" FAIL: w_base after back-to-back = 0x%06x", w_base); errors = errors + 1; end
|
|
|
|
report("TEST J: back-to-back transactions");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST K: SET_BASE for TABLE / BUF_A / BUF_B (Phase 5)
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
set_base(8'h03, 22'h000301); // TABLE_BASE
|
|
set_base(8'h04, 22'h000401); // BUF_A_BASE
|
|
set_base(8'h05, 22'h000501); // BUF_B_BASE
|
|
|
|
clk_wait(2);
|
|
|
|
if (table_base !== 22'h000301) begin $display(" FAIL: table_base = 0x%06x", table_base); errors = errors + 1; end
|
|
if (buf_a_base !== 22'h000401) begin $display(" FAIL: buf_a_base = 0x%06x", buf_a_base); errors = errors + 1; end
|
|
if (buf_b_base !== 22'h000501) begin $display(" FAIL: buf_b_base = 0x%06x", buf_b_base); errors = errors + 1; end
|
|
|
|
report("TEST K: SET_BASE (TABLE/BUF_A/BUF_B)");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST L: RUN_NETWORK (Phase 5)
|
|
// - accepted when idle: pulses run_start, captures
|
|
// run_num_layers, and STATUS.busy tracks seq_busy
|
|
// (independent of nm_busy) until seq_done.
|
|
// - ignored (no run_start) if the engine is already busy
|
|
// in any form (nm_busy or seq_busy).
|
|
// - STATUS.done latches only on seq_done while a
|
|
// RUN_NETWORK job is in flight, not on an intermediate
|
|
// nm_done pulse (the sequencer's own per-layer nm_done).
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
// -- accepted when idle --
|
|
run_start_seen = 1'b0;
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h23, HB, rx_tmp); // RUN_NETWORK
|
|
spi_xfer_byte(8'h02, HB, rx_tmp); // num_layers = 2
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (!run_start_seen) begin $display(" FAIL: run_start not pulsed while idle"); errors = errors + 1; end
|
|
if (run_num_layers !== 8'h02) begin $display(" FAIL: run_num_layers = %0d, expected 2", run_num_layers); errors = errors + 1; end
|
|
|
|
// -- STATUS.busy follows seq_busy even while nm_busy=0 (the
|
|
// gaps between layers, e.g. descriptor read/output copy) --
|
|
seq_busy = 1'b1;
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[0] !== 1'b1) begin $display(" FAIL: busy bit not set from seq_busy alone"); errors = errors + 1; end
|
|
|
|
// -- ignored while seq_busy (a RUN_NETWORK job in flight) --
|
|
run_start_seen = 1'b0;
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h23, HB, rx_tmp); // RUN_NETWORK
|
|
spi_xfer_byte(8'h03, HB, rx_tmp); // num_layers = 3 (must be ignored)
|
|
spi_end(HB);
|
|
clk_wait(4);
|
|
|
|
if (run_start_seen) begin $display(" FAIL: run_start pulsed while seq_busy (should be ignored)"); errors = errors + 1; end
|
|
if (run_num_layers !== 8'h02) begin $display(" FAIL: run_num_layers changed to %0d while busy (should stay 2)", run_num_layers); errors = errors + 1; end
|
|
|
|
// -- an intermediate nm_done pulse (sequencer's per-layer
|
|
// completion) must NOT set STATUS.done while net_mode
|
|
// (i.e. seq_busy) is active --
|
|
@(negedge clk); nm_done = 1'b1; @(negedge clk); nm_done = 1'b0;
|
|
clk_wait(4);
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[1] !== 1'b0) begin $display(" FAIL: done bit set by an intermediate nm_done during RUN_NETWORK"); errors = errors + 1; end
|
|
|
|
// -- the sequencer's own seq_done (final layer) DOES latch
|
|
// STATUS.done, and busy drops once seq_busy deasserts --
|
|
@(negedge clk); seq_busy = 1'b0; seq_done = 1'b1;
|
|
@(negedge clk); seq_done = 1'b0;
|
|
clk_wait(4);
|
|
|
|
read_status(rx_tmp);
|
|
if (rx_tmp[0] !== 1'b0) begin $display(" FAIL: busy bit still set after seq_busy/seq_done"); errors = errors + 1; end
|
|
if (rx_tmp[1] !== 1'b1) begin $display(" FAIL: done bit not set by seq_done"); errors = errors + 1; end
|
|
|
|
report("TEST L: RUN_NETWORK");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST M: SET_BASE selector 0x06 (ACTIVATION)
|
|
//
|
|
// Only the low 2 bits of the low address byte are used
|
|
// (see rtl/neuron_parallel.v's ACT_* localparams); reset
|
|
// default is ACT_RELU (2'd1).
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
if (activation !== 2'd1) begin $display(" FAIL: activation reset default = %0d, expected 1 (ACT_RELU)", activation); errors = errors + 1; end
|
|
|
|
set_base(8'h06, 22'h000000); // ACT_NONE
|
|
clk_wait(2);
|
|
if (activation !== 2'd0) begin $display(" FAIL: activation = %0d after SET_BASE(ACT_NONE)", activation); errors = errors + 1; end
|
|
|
|
set_base(8'h06, 22'h0000FF); // only low 2 bits (=3) matter, high bits ignored
|
|
clk_wait(2);
|
|
if (activation !== 2'd3) begin $display(" FAIL: activation = %0d after SET_BASE(0xFF), expected low 2 bits = 3", activation); errors = errors + 1; end
|
|
|
|
set_base(8'h06, 22'h000001); // back to ACT_RELU for any test relying on the default below
|
|
clk_wait(2);
|
|
if (activation !== 2'd1) begin $display(" FAIL: activation = %0d after SET_BASE(ACT_RELU)", activation); errors = errors + 1; end
|
|
|
|
report("TEST M: SET_BASE (ACTIVATION)");
|
|
|
|
// --------------------------------------------------------
|
|
// TEST N: SET_BASE selectors 0x07/0x08 (N_INPUTS/N_NEURONS)
|
|
//
|
|
// Reset default must equal the module's own N_INPUTS/
|
|
// N_NEURONS parameters (32/3 in this bench); SET_BASE loads
|
|
// the low 2 of the 3 transmitted address bytes, BE.
|
|
// --------------------------------------------------------
|
|
|
|
errors_before = errors;
|
|
|
|
if (n_inputs_real !== 16'd32) begin $display(" FAIL: n_inputs_real reset default = %0d, expected 32", n_inputs_real); errors = errors + 1; end
|
|
if (n_neurons_real !== 16'd3) begin $display(" FAIL: n_neurons_real reset default = %0d, expected 3", n_neurons_real); errors = errors + 1; end
|
|
|
|
set_base(8'h07, 22'h000008); // N_INPUTS = 8
|
|
clk_wait(2);
|
|
if (n_inputs_real !== 16'd8) begin $display(" FAIL: n_inputs_real = %0d after SET_BASE(8)", n_inputs_real); errors = errors + 1; end
|
|
|
|
set_base(8'h08, 22'h000002); // N_NEURONS = 2
|
|
clk_wait(2);
|
|
if (n_neurons_real !== 16'd2) begin $display(" FAIL: n_neurons_real = %0d after SET_BASE(2)", n_neurons_real); errors = errors + 1; end
|
|
|
|
set_base(8'h07, 22'h010102); // 3 bytes 01/01/02: N_INPUTS must be low 2 bytes = 0x0102, high byte 0x01 ignored
|
|
clk_wait(2);
|
|
if (n_inputs_real !== 16'h0102) begin $display(" FAIL: n_inputs_real = %0d after SET_BASE(0x010102), expected 0x0102", n_inputs_real); errors = errors + 1; end
|
|
|
|
report("TEST N: SET_BASE (N_INPUTS/N_NEURONS)");
|
|
|
|
// --------------------------------------------------------
|
|
// SUMMARY
|
|
// --------------------------------------------------------
|
|
|
|
$display("");
|
|
$display("========================================");
|
|
if (errors == 0)
|
|
$display("SPI_ENGINE TEST PASSED");
|
|
else
|
|
$display("SPI_ENGINE TEST FAILED: %0d errors", errors);
|
|
$display("========================================");
|
|
$display("");
|
|
|
|
$finish;
|
|
|
|
end
|
|
|
|
// ============================================================
|
|
// HELPER TASKS
|
|
// ============================================================
|
|
|
|
task set_base;
|
|
input [7:0] sel;
|
|
input [ADDR_WIDTH-1:0] addr;
|
|
begin
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h10, HB, rx_tmp); // SET_BASE
|
|
spi_xfer_byte(sel, HB, rx_tmp); // selector
|
|
spi_xfer_byte(addr[23:16], HB, rx_tmp);
|
|
spi_xfer_byte(addr[15:8], HB, rx_tmp);
|
|
spi_xfer_byte(addr[7:0], HB, rx_tmp);
|
|
spi_end(HB);
|
|
end
|
|
endtask
|
|
|
|
task read_status;
|
|
output [7:0] status;
|
|
begin
|
|
spi_begin(HB);
|
|
spi_xfer_byte(8'h21, HB, rx_tmp); // STATUS
|
|
spi_xfer_byte(8'h00, HB, status);
|
|
spi_end(HB);
|
|
end
|
|
endtask
|
|
|
|
task miso_check;
|
|
input [7:0] got;
|
|
input [7:0] expected;
|
|
input [511:0] label;
|
|
begin
|
|
if (got !== expected) begin
|
|
$display(" FAIL %0s: got 0x%02x expected 0x%02x", label, got, expected);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
task report;
|
|
input [511:0] label;
|
|
begin
|
|
$display("");
|
|
if (errors == errors_before)
|
|
$display("%0s: PASS", label);
|
|
else
|
|
$display("%0s: FAIL", label);
|
|
end
|
|
endtask
|
|
|
|
initial begin
|
|
nm_start_seen = 1'b0;
|
|
nm_soft_rst_seen = 1'b0;
|
|
end
|
|
|
|
endmodule
|