Files
FPGA-Neural/hardware/v1/sim/spi_flash_master_tb.v
micheleandClaude Sonnet 5 dc0b331d3e feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor
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
2026-09-05 14:06:53 +02:00

336 lines
12 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// SPI_FLASH_MASTER TESTBENCH (Phase F1)
//
// Drives rtl/spi_flash_master.v against sim/flash_model.v (an
// independent behavioral model of the datasheet's write rules, see
// its own header) and checks:
//
// TEST 1 (RDID, happy path): oracle is the datasheet's own
// manufacturer/type/capacity bytes (EF/40/18h -- see
// flash_model.v's header for the plain-vs-DTR JEDEC-ID note),
// hardcoded here independently of both the RTL and the model's
// internal constant (this testbench does not read
// flash_model.v's localparam, it states the expected value
// itself, from the same citation).
//
// TEST 2 (READ, known block, INDEPENDENT oracle): a pattern is
// planted directly into flash_model's memory array via
// hierarchical reference (dut_flash.mem[...]) -- NOT written
// through spi_flash_master -- then read back through the DUT
// and compared byte-exact. This is the true independent-oracle
// test per WORKLOG.md §A.1: the expected data was never
// produced by the RTL under test.
//
// TEST 3 (WREN+PP+RDSR-poll+READ round-trip): exercises the
// write-side primitives (not yet independent of the RTL's own
// write path -- a round-trip self-consistency check, distinct
// from TEST 2's independence, and explicitly noted as such).
// Confirms AND-only programming into an erased (0xFF) region
// reproduces the exact bytes written, and that RDSR's BUSY bit
// is observed high during the modeled tPP delay and clears
// after.
//
// TEST 4 (WREN+SE+RDSR-poll+READ): confirms sector erase drives
// the target sector back to all-0xFF, verified against a region
// TEST 3 deliberately left non-0xFF first (so this test cannot
// pass by coincidence on an already-blank region).
//
// TEST 5 (negative, §A.3): an opcode this design never uses
// (0xAB, Release Power-down / Device ID -- a real, harmless
// W25Q128JV instruction, but NOT one spi_flash_master or
// flash_model implement) is issued. Requirement: the
// transaction completes (`done` pulses within a bounded cycle
// count) rather than hanging -- a master that can wedge on an
// opcode it doesn't specifically recognize is broken by
// construction, since it always just shifts bits regardless of
// opcode semantics.
// ================================================================
module tb;
localparam CLK_PERIOD = 12.5; // 80 MHz, matches CLK_FREQ_MHZ default
reg clk;
reg rst;
initial begin
clk = 1'b0;
forever #(CLK_PERIOD / 2.0) clk = ~clk;
end
// ------------------------------------------------------------
// DUT <-> flash_model physical wiring
// ------------------------------------------------------------
wire mosi;
wire miso;
wire cs_n;
wire sclk_w;
reg start;
reg [7:0] opcode;
reg has_addr;
reg [23:0] addr;
reg [1:0] dir;
reg [15:0] n_data;
wire wdata_req;
reg [7:0] wdata;
reg wdata_valid;
wire rdata_valid;
wire [7:0] rdata;
reg rdata_ack;
wire busy;
wire done;
localparam DIR_NONE = 2'd0;
localparam DIR_WRITE = 2'd1;
localparam DIR_READ = 2'd2;
spi_flash_master #(
.CLK_FREQ_MHZ(80),
.SCLK_DIV(2)
) dut (
.clk(clk), .rst(rst),
.mosi(mosi), .miso(miso), .cs_n(cs_n),
.sclk(sclk_w),
.start(start), .opcode(opcode), .has_addr(has_addr), .addr(addr),
.dir(dir), .n_data(n_data),
.wdata_req(wdata_req), .wdata(wdata), .wdata_valid(wdata_valid),
.rdata_valid(rdata_valid), .rdata(rdata), .rdata_ack(rdata_ack),
.busy(busy), .done(done)
);
flash_model #(
.DEPTH(32'h0002_0000),
.TIME_SCALE(100000) // see flash_model.v header: MAX datasheet timing / 100000
) dut_flash (
.sclk(sclk_w), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
// ------------------------------------------------------------
// Bookkeeping
// ------------------------------------------------------------
integer errors;
reg [7:0] rbuf [0:511];
reg [7:0] wbuf [0:511];
// ============================================================
// do_cmd: drive one full command through the DUT's byte-level
// handshake, servicing wdata_req/rdata_valid as needed. Reads
// land in rbuf[0..n_data-1]; writes are sourced from wbuf.
// ============================================================
integer k;
integer wd_cyc;
task automatic do_cmd(
input [7:0] p_opcode,
input p_has_addr,
input [23:0] p_addr,
input [1:0] p_dir,
input [15:0] p_n
);
begin
@(posedge clk);
start <= 1'b1;
opcode <= p_opcode;
has_addr <= p_has_addr;
addr <= p_addr;
dir <= p_dir;
n_data <= p_n;
@(posedge clk);
start <= 1'b0;
k = 0;
wd_cyc = 0;
while (!done) begin
@(posedge clk);
if (wdata_req) begin
wdata_valid <= 1'b1;
wdata <= wbuf[k];
end else begin
wdata_valid <= 1'b0;
end
if (wdata_valid) begin
k = k + 1;
end
if (rdata_valid) begin
rbuf[k] = rdata;
k = k + 1;
rdata_ack <= 1'b1;
end else begin
rdata_ack <= 1'b0;
end
wd_cyc = wd_cyc + 1;
if (wd_cyc > 200000) begin
$display("FATAL: do_cmd watchdog timeout (opcode=%02h) at t=%0t", p_opcode, $time);
$finish;
end
end
@(posedge clk);
rdata_ack <= 1'b0;
wdata_valid <= 1'b0;
end
endtask
task automatic check_byte(input [7:0] got, input [7:0] exp, input [255:0] label);
begin
if (got !== exp) begin
$display("FAIL: %0s got=%02h exp=%02h", label, got, exp);
errors = errors + 1;
end
end
endtask
localparam [7:0] OP_WREN = 8'h06;
localparam [7:0] OP_READ = 8'h03;
localparam [7:0] OP_PP = 8'h02;
localparam [7:0] OP_SE = 8'h20;
localparam [7:0] OP_RDSR1 = 8'h05;
localparam [7:0] OP_RDID = 8'h9F;
task automatic wait_wip_clear;
begin
rbuf[0] = 8'hFF;
while (rbuf[0][0] == 1'b1) begin
do_cmd(OP_RDSR1, 1'b0, 24'h0, DIR_READ, 16'd1);
end
end
endtask
integer i;
initial begin
errors = 0;
rst = 1'b1;
start = 1'b0;
opcode = 8'h00;
has_addr = 1'b0;
addr = 24'h0;
dir = DIR_NONE;
n_data = 16'd0;
wdata_valid = 1'b0;
wdata = 8'h00;
rdata_ack = 1'b0;
repeat (5) @(posedge clk);
rst = 1'b0;
repeat (5) @(posedge clk);
// ========================================================
// TEST 1: RDID happy path
// ========================================================
do_cmd(OP_RDID, 1'b0, 24'h0, DIR_READ, 16'd3);
check_byte(rbuf[0], 8'hEF, "TEST1 RDID manufacturer");
check_byte(rbuf[1], 8'h40, "TEST1 RDID memtype");
check_byte(rbuf[2], 8'h18, "TEST1 RDID capacity");
// ========================================================
// TEST 2: READ, known block, planted independently of the
// RTL (hierarchical poke into the flash model's own array).
// ========================================================
for (i = 0; i < 16; i = i + 1)
dut_flash.mem[24'h001000 + i] = 8'hA0 + i[7:0];
do_cmd(OP_READ, 1'b1, 24'h001000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'hA0 + i[7:0], "TEST2 READ known block");
// ========================================================
// TEST 3: WREN + PP (16 bytes) + RDSR poll + READ round-trip.
// Target region 0x002000 starts erased (0xFF, from
// flash_model's own reset init) -- programming a pattern
// with some cleared bits must read back exactly that
// pattern (AND-with-0xFF is a no-op, so this alone doesn't
// yet prove AND-only; TEST 3b below does).
// ========================================================
for (i = 0; i < 16; i = i + 1)
wbuf[i] = 8'h10 + i[7:0];
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3 PP/READ round-trip");
// TEST 3b: program the SAME region again with a value that
// tries to SET a bit the first program cleared (0x10 has
// bit4 set high already at 0x10=00010000; try programming
// 0xFF over it, which per the AND-only rule must leave 0x10
// unchanged, NOT flip it to 0xFF). This is the datasheet
// rule TEST 3 alone cannot distinguish from a naive
// "programming just overwrites" implementation.
for (i = 0; i < 16; i = i + 1)
wbuf[i] = 8'hFF;
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_PP, 1'b1, 24'h002000, DIR_WRITE, 16'd16);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'h10 + i[7:0], "TEST3b PP AND-only (program cannot set bits)");
// ========================================================
// TEST 4: WREN + SE (sector containing 0x002000) + RDSR
// poll + READ, confirming erase -> all-FF. The region was
// deliberately left non-FF by TEST 3/3b above, so this
// cannot pass by coincidence.
// ========================================================
do_cmd(OP_WREN, 1'b0, 24'h0, DIR_NONE, 16'd0);
do_cmd(OP_SE, 1'b1, 24'h002000, DIR_NONE, 16'd0);
wait_wip_clear;
do_cmd(OP_READ, 1'b1, 24'h002000, DIR_READ, 16'd16);
for (i = 0; i < 16; i = i + 1)
check_byte(rbuf[i], 8'hFF, "TEST4 SE erase -> 0xFF");
// ========================================================
// TEST 5 (negative, §A.3): unsupported/unrecognized opcode
// (0xABh, a real W25Q128JV instruction -- Release
// Power-down/Device ID -- that neither spi_flash_master nor
// flash_model implement any special-case for). Requirement:
// do_cmd's watchdog must NOT fire -- the transaction has to
// complete (`done` pulses) purely from the master's own
// fixed bit-count/opcode-agnostic shifting, proving the
// master cannot wedge on an opcode it doesn't recognize.
// ========================================================
do_cmd(8'hAB, 1'b0, 24'h0, DIR_READ, 16'd1);
$display("TEST5 (illegal/unsupported opcode 0xAB): completed without watchdog timeout, got=%02h (undefined/don't-care by design)", rbuf[0]);
// ========================================================
if (errors == 0)
$display("ALL TESTS PASSED");
else
$display("FAILED: %0d error(s)", errors);
$finish;
end
// Safety net: absolute simulation timeout independent of the
// per-command watchdog above (catches a hang between commands,
// e.g. in the top-level initial block itself).
initial begin
#50_000_000;
$display("FATAL: global simulation timeout");
$finish;
end
endmodule