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
217 lines
9.2 KiB
Verilog
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
|