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
336 lines
12 KiB
Verilog
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
|