User-requested hardware notification so the ESP32 can be interrupt-driven instead of polling STATUS in a loop. spi_host_bridge_v3.v: new job_out_done input (wired from neural_director_packed.v, already available at the top level) and new data_ready_n output. A sticky irq_pending register sets on job_out_done (latched, survives the pulse itself deasserting) and clears when the host completes a real STATUS (0x20) or REG_READ(0x02) transaction - reusing cs_rose, the same real transaction-complete event the module already relies on elsewhere, not a new mechanism. dir_error is ORed in live/combinational, not latched. SET has priority over CLEAR on the rare cycle both coincide. Real pin: D14, bank 15 (already 3.3V, alongside the SPI bus and sys_rst) - tentative, not yet a final board decision. Deliberately added after EXP-0084's own P&R iterations settled, so it didn't complicate that already-tight I/O/VCCO budget mid-fix. Its own real P&R verification is deferred to the next real P&R run (already needed to close EXP-0084's clock-period timing gap), not run separately against a config already known to fail timing for unrelated reasons. Real verification: tb_spi_host_bridge_v3.v extended with 10 new checks (idle state, sticky set, mid-transaction hold, real-acknowledge clear, unrelated-register non-acknowledge, dir_error live assert/clear). 49/49 PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
518 lines
24 KiB
Verilog
518 lines
24 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;
|
|
reg init_calib_complete_model = 0;
|
|
reg dir_error_model = 0;
|
|
reg job_out_done_model = 0;
|
|
wire data_ready_n;
|
|
|
|
// ---- config-flash passthrough path: real flash_spi_master.v +
|
|
// the same behavioral W25Q32JV-like model used standalone in
|
|
// tb_flash_spi_master.v (EXP-0077), wired end to end through
|
|
// spi_host_bridge_v3.v's own new FLASH_XFER opcode ----
|
|
wire flash_xfer_active, flash_byte_req, flash_byte_done;
|
|
wire [7:0] flash_byte_wdata, flash_byte_rdata;
|
|
wire flash_cs_n, flash_mosi, flash_miso;
|
|
|
|
flash_spi_master u_flash (
|
|
.clk(clk), .rst(rst),
|
|
.xfer_active(flash_xfer_active), .byte_req(flash_byte_req),
|
|
.byte_wdata(flash_byte_wdata), .byte_rdata(flash_byte_rdata), .byte_done(flash_byte_done), .busy(),
|
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
|
|
);
|
|
flash_model_w25q32 u_flash_model (
|
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso),
|
|
.flash_sclk(u_flash.cclk_r)
|
|
);
|
|
|
|
spi_host_bridge_v3 #(
|
|
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
.flash_xfer_active(flash_xfer_active), .flash_byte_req(flash_byte_req),
|
|
.flash_byte_wdata(flash_byte_wdata), .flash_byte_rdata(flash_byte_rdata), .flash_byte_done(flash_byte_done),
|
|
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
|
.init_calib_complete(init_calib_complete_model), .dir_error(dir_error_model),
|
|
.job_out_done(job_out_done_model), .data_ready_n(data_ready_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 G: REG_READ, DEVICE_ID (0x00) ========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb); // opcode REG_READ
|
|
spi_byte(8'h00, rxb); // reg_addr=0x00 DEVICE_ID
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h4E, "G: DEVICE_ID byte0 == 'N'");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h50, "G: DEVICE_ID byte1 == 'P'");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h56, "G: DEVICE_ID byte2 == 'V'");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h01, "G: DEVICE_ID byte3 == version 1");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test H: REG_READ, N_SLOTS (0x03) ==========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb);
|
|
spi_byte(8'h03, rxb);
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "H: N_SLOTS byte0 == 0");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "H: N_SLOTS byte1 == 0");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "H: N_SLOTS byte2 == 0");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h02, "H: N_SLOTS byte3 == 2 (matches N_SLOTS param)");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test I: REG_READ, STATUS (0x02), with
|
|
// init_calib_complete and dir_error both driven high by the
|
|
// model, confirming they land in the right bits ============
|
|
init_calib_complete_model = 1'b1;
|
|
dir_error_model = 1'b1;
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb);
|
|
spi_byte(8'h02, rxb);
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "I: STATUS byte0 == 0 (bits[31:8] reserved)");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "I: STATUS byte1 == 0");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h00, "I: STATUS byte2 == 0");
|
|
spi_byte(8'h00, rxb); check(rxb[3] == 1'b1, "I: STATUS bit3 == init_calib_complete");
|
|
check(rxb[4] == 1'b1, "I: STATUS bit4 == dir_error");
|
|
cs_n = 1; #40;
|
|
init_calib_complete_model = 1'b0;
|
|
dir_error_model = 1'b0;
|
|
|
|
// ================= Test J: REG_READ, unknown address =========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb);
|
|
spi_byte(8'hEE, rxb); // unmapped register
|
|
spi_byte(8'h00, rxb); check(rxb == 8'hFF, "J: unmapped reg byte0 == 0xFF");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'hFF, "J: unmapped reg byte1 == 0xFF");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'hFF, "J: unmapped reg byte2 == 0xFF");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'hFF, "J: unmapped reg byte3 == 0xFF (distinct from a real 0)");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test K: REG_WRITE to CONTROL (0x01) bit0
|
|
// pulses soft_rst_pulse, same physical effect as RESET.
|
|
// UNLIKE the RESET opcode (which pulses only after CS rises),
|
|
// REG_WRITE applies immediately when its last data byte
|
|
// lands -- no backend handshake to wait on (see this module's
|
|
// own header). The watchdog must therefore run CONCURRENTLY
|
|
// with the last data byte's own spi_byte() call (a `fork`,
|
|
// same technique as tb_sdram_arbiter_n.v's own one-shot-pulse
|
|
// watchers), not after CS has already risen -- a first draft
|
|
// of this test watched only after CS rose and missed the
|
|
// pulse entirely (a testbench-timing bug, not an RTL one,
|
|
// confirmed via a DUT-internal trace before writing this). ==
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h30, rxb); // opcode REG_WRITE
|
|
spi_byte(8'h01, rxb); // reg_addr=0x01 CONTROL
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); // value bytes 31:8 = 0
|
|
begin : wait_reg_soft_rst
|
|
reg seen;
|
|
seen = 1'b0;
|
|
fork
|
|
spi_byte(8'h01, rxb); // value byte 7:0 = 1 (bit0 set) -- triggers the pulse
|
|
begin : watcher
|
|
integer wi;
|
|
for (wi = 0; wi < 410; wi = wi + 1) begin // covers spi_byte's own ~400-clk duration plus margin
|
|
@(posedge clk);
|
|
if (soft_rst_pulse) seen = 1'b1;
|
|
end
|
|
end
|
|
join
|
|
check(seen, "K: REG_WRITE CONTROL bit0 pulses soft_rst_pulse");
|
|
end
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test L: 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, "L: soft_rst_pulse asserted after CS rises (within CDC latency)");
|
|
end
|
|
|
|
// ================= Test M: READ_MEM regression for the ROUT-
|
|
// exit bit_count==0 corruption (found via REG_READ this
|
|
// session, see spi_host_bridge_v3.v's own header note) --
|
|
// Test C/D's word 0x1234 has LSB byte 0x34 (bit0=0), which
|
|
// coincidentally matched the corrupted substitute's bit7=0
|
|
// and masked the bug. Use 0x5679 instead: LSB byte 0x79 =
|
|
// 0111_1001, bit0=1, which the (now-fixed) bug would have
|
|
// flipped to 0 (reading back 0x78 instead of 0x79). =========
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h01, rxb);
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h60, rxb); // addr=0x60
|
|
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb); // len_words=1
|
|
spi_byte(8'h56, rxb); spi_byte(8'h79, rxb); // data=0x5679
|
|
#200;
|
|
cs_n = 1; #40;
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h02, rxb);
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h60, rxb);
|
|
spi_byte(8'h00, rxb); spi_byte(8'h01, rxb);
|
|
#200;
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h56, "M: READ_MEM MSB byte == 0x56");
|
|
spi_byte(8'h00, rxb); check(rxb == 8'h79, "M: READ_MEM LSB byte == 0x79 (bit0=1, catches the ROUT-exit bug)");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test N: FLASH_XFER passthrough end to end
|
|
// -- real flash_spi_master.v + a real Winbond-command-set
|
|
// behavioral flash model behind it. Write Enable + Page
|
|
// Program + Read Data, entirely through spi_host_bridge_v3.v's
|
|
// own opcode 0x40, proving the WHOLE relay chain (host SPI ->
|
|
// this bridge -> flash_spi_master.v -> physical flash bus) is
|
|
// bit-exact, not just each half in isolation. ===============
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h40, rxb); // opcode FLASH_XFER
|
|
spi_byte(8'h06, rxb); // relay: Write Enable
|
|
cs_n = 1; #40;
|
|
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h40, rxb);
|
|
spi_byte(8'h02, rxb); // relay: Page Program
|
|
spi_byte(8'h30, rxb); // relay: addr=0x30
|
|
spi_byte(8'h5A, rxb); // relay: data=0x5A
|
|
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2 -- see header's own real latency note
|
|
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2
|
|
cs_n = 1; #40;
|
|
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h40, rxb);
|
|
spi_byte(8'h03, rxb); // relay: Read Data
|
|
spi_byte(8'h30, rxb); // relay: addr=0x30
|
|
spi_byte(8'h00, rxb); // relay: dummy clock for the data byte
|
|
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2
|
|
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2 -- response is safely stable here
|
|
check(rxb == 8'h5A, "N: FLASH_XFER end-to-end round trip through the real flash model, bit-exact");
|
|
cs_n = 1; #40;
|
|
|
|
// ================= Test O: data_ready_n sticky IRQ (real
|
|
// feature, user-requested, EXP-0084) ==========================
|
|
check(data_ready_n === 1'b1, "O1: data_ready_n idle-high (no job done, no dir_error)");
|
|
|
|
// a real job/pair completion sets it, and it STAYS set (sticky)
|
|
// even after job_out_done itself drops back to 0.
|
|
job_out_done_model = 1'b1;
|
|
#10;
|
|
job_out_done_model = 1'b0;
|
|
#10;
|
|
check(data_ready_n === 1'b0, "O2: job_out_done sets data_ready_n low, and it's sticky (job_out_done already deasserted)");
|
|
|
|
// acknowledged by a real completed STATUS (0x20) transaction --
|
|
// clears the moment CS rises on that transaction, not before.
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h20, rxb); // opcode STATUS
|
|
check(data_ready_n === 1'b0, "O3: data_ready_n still low mid-transaction (not yet acknowledged)");
|
|
cs_n = 1; #40;
|
|
check(data_ready_n === 1'b1, "O4: STATUS transaction completing (CS rise) clears data_ready_n");
|
|
|
|
// REG_READ of STATUS (reg 0x02) is an equally valid acknowledge.
|
|
job_out_done_model = 1'b1;
|
|
#10;
|
|
job_out_done_model = 1'b0;
|
|
#10;
|
|
check(data_ready_n === 1'b0, "O5: job_out_done sets data_ready_n low again");
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb); // opcode REG_READ
|
|
spi_byte(8'h02, rxb); // reg_addr = 0x02 STATUS
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb);
|
|
cs_n = 1; #40;
|
|
check(data_ready_n === 1'b1, "O6: REG_READ(STATUS) completing also clears data_ready_n");
|
|
|
|
// REG_READ of an UNRELATED register must NOT acknowledge it.
|
|
job_out_done_model = 1'b1;
|
|
#10;
|
|
job_out_done_model = 1'b0;
|
|
#10;
|
|
cs_n = 0; #20;
|
|
spi_byte(8'h31, rxb);
|
|
spi_byte(8'h00, rxb); // reg_addr = 0x00 DEVICE_ID, not STATUS
|
|
spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb); spi_byte(8'h00, rxb);
|
|
cs_n = 1; #40;
|
|
check(data_ready_n === 1'b0, "O7: REG_READ of an unrelated register does NOT clear data_ready_n");
|
|
|
|
// acknowledge it for real now, then confirm dir_error alone
|
|
// (no job_out_done) also asserts it, combinationally, and that
|
|
// it clears again once dir_error itself deasserts.
|
|
cs_n = 0; #20; spi_byte(8'h20, rxb); cs_n = 1; #40;
|
|
check(data_ready_n === 1'b1, "O8: real acknowledge clears the pending job_out_done from O7");
|
|
dir_error_model = 1'b1;
|
|
#10;
|
|
check(data_ready_n === 1'b0, "O9: dir_error alone (no job_out_done) also asserts data_ready_n");
|
|
dir_error_model = 1'b0;
|
|
#10;
|
|
check(data_ready_n === 1'b1, "O10: data_ready_n clears once dir_error itself deasserts");
|
|
|
|
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
|
|
if (errors != 0) $display("*** %0d FAILURES ***", errors);
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|
|
|
|
// STARTUPE2 simulation-only stub -- see tb_flash_spi_master.v's own
|
|
// header for why real UNISIM verification is a disclosed follow-up,
|
|
// not done here (this test verifies the protocol/relay logic, which
|
|
// is independent of STARTUPE2's own real behavior).
|
|
module STARTUPE2 #(
|
|
parameter PROG_USR = "FALSE",
|
|
parameter real SIM_CCLK_FREQ = 0.0
|
|
)(
|
|
output wire CFGCLK, output wire CFGMCLK, output wire EOS, output wire PREQ,
|
|
input wire CLK, input wire GSR, input wire GTS, input wire KEYCLEARB, input wire PACK,
|
|
input wire USRCCLKO, input wire USRCCLKTS,
|
|
input wire USRDONEO, input wire USRDONETS
|
|
);
|
|
endmodule
|
|
|
|
// Same behavioral W25Q32JV-like flash model as tb_flash_spi_master.v
|
|
// (EXP-0077) -- kept independent (not shared via `include) since each
|
|
// testbench owns its own self-contained model, matching this
|
|
// project's existing convention (e.g. sdram_model.v is the one real
|
|
// exception, shared because it stands in for real vendor-supplied
|
|
// silicon behavior, not a test-specific convenience model).
|
|
module flash_model_w25q32 (
|
|
input wire flash_cs_n,
|
|
input wire flash_mosi,
|
|
output reg flash_miso,
|
|
input wire flash_sclk
|
|
);
|
|
reg [7:0] flash_mem [0:255];
|
|
reg [7:0] flash_cmd;
|
|
reg [7:0] flash_addr;
|
|
reg flash_wel;
|
|
reg [7:0] model_shift;
|
|
reg [2:0] model_bitcnt;
|
|
reg [2:0] model_bytecnt;
|
|
reg [7:0] model_rdata_byte;
|
|
|
|
initial begin flash_wel = 0; model_bytecnt = 0; model_bitcnt = 0; flash_miso = 0; end
|
|
|
|
always @(posedge flash_sclk) begin
|
|
if (!flash_cs_n) begin
|
|
model_shift <= {model_shift[6:0], flash_mosi};
|
|
if (model_bitcnt == 3'd7) begin
|
|
model_bitcnt <= 3'd0;
|
|
case (model_bytecnt)
|
|
3'd0: begin
|
|
flash_cmd <= {model_shift[6:0], flash_mosi};
|
|
if ({model_shift[6:0], flash_mosi} == 8'h06) flash_wel <= 1'b1;
|
|
model_bytecnt <= model_bytecnt + 1'b1;
|
|
end
|
|
3'd1: begin
|
|
if (flash_cmd == 8'h02 || flash_cmd == 8'h03) begin
|
|
flash_addr <= {model_shift[6:0], flash_mosi};
|
|
model_bytecnt <= model_bytecnt + 1'b1;
|
|
end
|
|
end
|
|
3'd2: begin
|
|
if (flash_cmd == 8'h02) flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi};
|
|
model_bytecnt <= model_bytecnt + 1'b1;
|
|
end
|
|
default: ;
|
|
endcase
|
|
end else begin
|
|
model_bitcnt <= model_bitcnt + 1'b1;
|
|
end
|
|
end
|
|
end
|
|
|
|
always @(*) begin
|
|
if (flash_cmd == 8'h05) model_rdata_byte = {6'b0, flash_wel, 1'b0};
|
|
else if (flash_cmd == 8'h03) model_rdata_byte = flash_mem[flash_addr];
|
|
else model_rdata_byte = 8'h00;
|
|
end
|
|
always @(negedge flash_sclk) begin
|
|
if (!flash_cs_n && model_bytecnt >= (flash_cmd==8'h05 ? 3'd1 : 3'd2))
|
|
flash_miso <= model_rdata_byte[3'd7 - model_bitcnt];
|
|
end
|
|
always @(posedge flash_cs_n) begin
|
|
model_bytecnt <= 3'd0;
|
|
model_bitcnt <= 3'd0;
|
|
end
|
|
endmodule
|