Files
FPGA-Neural/hardware/v3/sim/tb_spi_host_bridge_v3.v
T
micheleandClaude Sonnet 5 562cf91f1e feat: spi_host_bridge_v3.v, SPI opcode re-audit against real V3 RTL (EXP-0072)
Forked from V2's spi_host_bridge.v after finding two real protocol
mismatches: WRITE_JOB carried dependency-manager fields (required/
producer_ids) that neural_director_packed.v's job_in_* port doesn't
have (no dependency manager exists in V3 -- dropped, disclosed, not
silently ignored), and WRITE_MEM/READ_MEM assumed a word-granularity
host-arb port V3 never had (now wired through host_mem_bridge.v,
EXP-0071). Physical SPI layer carried over unchanged.

Verified standalone: 18/18 tests, 0 errors, including a case
exercising the narrower 25-bit MEM_ADDR_WIDTH's own top bit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-17 08:56:15 +02:00

217 lines
9.2 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// Isolated unit regression for spi_host_bridge_v3.v (V3 SPI opcode
// re-audit, this session). Mirrors hardware/v2/sim/tb_spi_host_
// bridge.v's own proven BFM/latency-model structure exactly, adapted
// for the new job_in_*/mem_* port shapes (16-byte WRITE_JOB, no
// required/producer_ids fields; 4-byte WRITE_MEM/READ_MEM address).
//
// Emulates: (1) neural_director_packed.v's job_in_ready contract (a
// level, deliberately delayed for a few cycles on the first job to
// prove job_in_valid is HELD, not pulsed blind); (2) host_mem_
// bridge.v's mem_ready contract (one clean req/ready handshake, fixed
// latency, backed by a simple model array standing in for real DDR3
// content -- host_mem_bridge.v itself is already independently
// verified in EXP-0071, so this test only needs to prove
// spi_host_bridge_v3.v drives ITS OWN side of that same word-
// granularity contract correctly).
// ================================================================
module tb_spi_host_bridge_v3;
localparam JOB_ADDR_WIDTH = 26;
localparam MEM_ADDR_WIDTH = 25;
reg clk = 0, rst = 1;
always #5 clk = ~clk; // 100MHz sim clock
reg sclk = 0, mosi = 0, cs_n = 1;
wire miso;
reg job_in_ready_model = 0;
wire job_in_valid;
wire [JOB_ADDR_WIDTH-1:0] job_in_x_base, job_in_w_base, job_in_result_addr;
wire [15:0] job_in_n_tiles, job_in_node_id;
wire mem_req, mem_wr, mem_lb_n, mem_ub_n;
wire [MEM_ADDR_WIDTH-1:0] mem_addr;
wire [15:0] mem_wdata;
reg [15:0] mem_rdata_model;
reg mem_ready_model = 0;
wire soft_rst_pulse;
spi_host_bridge_v3 #(
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH)
) dut (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready_model),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr),
.mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
.mem_rdata(mem_rdata_model), .mem_ready(mem_ready_model),
.soft_rst_pulse(soft_rst_pulse)
);
// ---- simple backing memory model: fixed 6-cycle mem_ready latency ----
reg [15:0] mem_model [0:1023];
integer mem_latency_cnt;
reg mem_pending;
always @(posedge clk) begin
if (rst) begin
mem_ready_model <= 1'b0; mem_pending <= 1'b0; mem_latency_cnt <= 0;
end else begin
mem_ready_model <= 1'b0;
if (mem_req && !mem_pending) begin
mem_pending <= 1'b1;
mem_latency_cnt <= 6;
end else if (mem_pending) begin
if (mem_latency_cnt == 0) begin
mem_pending <= 1'b0;
mem_ready_model <= 1'b1;
if (mem_wr) mem_model[mem_addr[9:0]] <= mem_wdata;
else mem_rdata_model <= mem_model[mem_addr[9:0]];
end else begin
mem_latency_cnt <= mem_latency_cnt - 1;
end
end
end
end
// ---- SPI master BFM: mode 0, MSB-first (same timing as tb_spi_host_bridge.v) ----
task spi_byte(input [7:0] tx, output [7:0] rx);
integer i;
begin
rx = 8'h00;
for (i = 7; i >= 0; i = i - 1) begin
mosi = tx[i];
#200; sclk = 1; #50; rx = {rx[6:0], miso}; #50; sclk = 0; #200;
end
end
endtask
integer errors = 0, tests = 0;
task check(input cond, input [255:0] name);
begin
tests = tests + 1;
if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end
else $display("PASS: %0s", name);
end
endtask
reg [7:0] rxb;
initial begin
rst = 1; cs_n = 1; sclk = 0; mosi = 0;
repeat (10) @(posedge clk);
rst = 0;
repeat (5) @(posedge clk);
// ================= Test A: WRITE_JOB (16 bytes), delayed job_in_ready =====
job_in_ready_model = 0;
cs_n = 0; #20;
spi_byte(8'h10, rxb); // opcode WRITE_JOB
spi_byte(8'h00, rxb); // node_id[15:8]
spi_byte(8'h05, rxb); // node_id[7:0] -> node_id=5
spi_byte(8'h00, rxb); // x_base[25:24]
spi_byte(8'h00, rxb); // x_base[23:16]
spi_byte(8'h10, rxb); // x_base[15:8]
spi_byte(8'h00, rxb); // x_base[7:0] -> x_base=0x001000
spi_byte(8'h00, rxb); // w_base[25:24]
spi_byte(8'h00, rxb); // w_base[23:16]
spi_byte(8'h20, rxb); // w_base[15:8]
spi_byte(8'h00, rxb); // w_base[7:0] -> w_base=0x002000
spi_byte(8'h00, rxb); // n_tiles[15:8]
spi_byte(8'h04, rxb); // n_tiles[7:0] -> n_tiles=4
spi_byte(8'h00, rxb); // result_addr[25:24]
spi_byte(8'h00, rxb); // result_addr[23:16]
spi_byte(8'h30, rxb); // result_addr[15:8]
spi_byte(8'h00, rxb); // result_addr[7:0] -> result_addr=0x003000
repeat (8) @(posedge clk);
check(job_in_valid == 1'b1, "A: job_in_valid asserted after 16th payload byte");
check(job_in_node_id == 16'h0005, "A: job_in_node_id");
check(job_in_x_base == 26'h001000, "A: job_in_x_base");
check(job_in_w_base == 26'h002000, "A: job_in_w_base");
check(job_in_n_tiles == 16'h0004, "A: job_in_n_tiles");
check(job_in_result_addr == 26'h003000, "A: job_in_result_addr");
repeat (3) begin
@(posedge clk);
check(job_in_valid == 1'b1, "A: job_in_valid still held while job_in_ready=0");
end
job_in_ready_model = 1;
@(posedge clk);
#1;
check(job_in_valid == 1'b0, "A: job_in_valid drops the cycle after job_in_ready seen");
job_in_ready_model = 0;
cs_n = 1; #40;
// ================= Test B: STATUS after accepted job ========
cs_n = 0; #20;
spi_byte(8'h20, rxb); // opcode STATUS
spi_byte(8'h00, rxb); // clocks out status byte
check(rxb[2] == 1'b1, "B: STATUS last_job_accepted=1");
check(rxb[0] == 1'b0, "B: STATUS job_busy=0 (already accepted)");
cs_n = 1; #40;
// ================= Test C: WRITE_MEM, single word (4-byte addr) =====
cs_n = 0; #20;
spi_byte(8'h01, rxb); // opcode WRITE_MEM
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1
spi_byte(8'h12, rxb); spi_byte(8'h34, rxb); // data=0x1234
#200;
cs_n = 1; #40;
check(mem_model[16'h0055] == 16'h1234, "C: WRITE_MEM wrote 0x1234 @ 0x000055");
// ================= Test D: READ_MEM, single word =============
cs_n = 0; #20;
spi_byte(8'h02, rxb); // opcode READ_MEM
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h55, rxb); // addr=0x000055
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1
#200;
spi_byte(8'h00, rxb);
check(rxb == 8'h12, "D: READ_MEM MSB byte == 0x12");
spi_byte(8'h00, rxb);
check(rxb == 8'h34, "D: READ_MEM LSB byte == 0x34");
cs_n = 1; #40;
// ================= Test E: multi-word WRITE_MEM/READ_MEM, exercising
// the 25-bit MEM_ADDR_WIDTH's own top bit (addr near 2^24) =========
cs_n = 0; #20;
spi_byte(8'h01, rxb); // opcode WRITE_MEM
spi_byte(8'h01, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); // addr=0x1000000 (bit24=1)
spi_byte(8'h00, rxb); spi_byte(8'h02, rxb); // len_words=2
spi_byte(8'hAA, rxb); spi_byte(8'hBB, rxb); // word0=0xAABB
spi_byte(8'hCC, rxb); spi_byte(8'hDD, rxb); // word1=0xCCDD
#400;
cs_n = 1; #40;
check(mem_model[(25'h1000000) & 10'h3FF] == 16'hAABB, "E: WRITE_MEM word0 @ addr bit24 set");
check(mem_model[((25'h1000000)+1) & 10'h3FF] == 16'hCCDD, "E: WRITE_MEM word1 @ addr bit24 set");
// ================= Test F: RESET opcode ======================
cs_n = 0; #20;
spi_byte(8'h0F, rxb); // opcode RESET
cs_n = 1;
begin : wait_soft_rst
integer wi; reg seen;
seen = 1'b0;
for (wi = 0; wi < 10; wi = wi + 1) begin
@(posedge clk);
if (soft_rst_pulse) seen = 1'b1;
end
check(seen, "F: soft_rst_pulse asserted after CS rises (within CDC latency)");
end
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
if (errors != 0) $display("*** %0d FAILURES ***", errors);
$finish;
end
endmodule