`timescale 1ns/1ps // ============================================================ // Isolated correctness test for flash_spi_master.v's own bit-level // SPI master logic (mode 0, MSB-first), against a small behavioral // model of the REAL W25Q32JV command set (Write Enable=0x06, Read // Status Register-1=0x05 with BUSY=bit0/WEL=bit1, Page Program=0x02, // Read Data=0x03 -- all verified against the real Winbond datasheet, // see flash_spi_master.v's own header). // // STARTUPE2 (the real Xilinx primitive this module uses for CCLK) is // stood in here by a trivial simulation-only stub (`u_startupe2_stub`, // just passes CLK through) -- this test verifies the BIT-LEVEL SPI // protocol logic is correct, which is independent of STARTUPE2's own // real behavior. Full verification against the real Xilinx UNISIM // STARTUPE2 model (via xsim, same technique as EXP-0068's real DDR3 // verification) is a disclosed follow-up, not done here. // ============================================================ 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 module tb; reg clk, rst; initial begin clk = 0; forever #(1000.0/155.039/2) clk = ~clk; end // real ui_clk period, 155.039MHz reg xfer_active, byte_req; reg [7:0] byte_wdata; wire [7:0] byte_rdata; wire byte_done, busy; wire flash_cs_n, flash_mosi; reg flash_miso; flash_spi_master u_dut ( .clk(clk), .rst(rst), .xfer_active(xfer_active), .byte_req(byte_req), .byte_wdata(byte_wdata), .byte_rdata(byte_rdata), .byte_done(byte_done), .busy(busy), .flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso) ); // ---- behavioral W25Q32JV-like flash model: real command set, // simplified (single in-memory byte array, no real program/erase // timing, no protection checks -- enough to prove the physical // SPI relay is bit-exact end to end) ---- reg [7:0] flash_mem [0:255]; reg [7:0] flash_cmd; reg [7:0] flash_addr; reg [1:0] flash_phase; // 0=cmd, 1=addr(x3, only using 1 byte here), 2=data reg flash_wel; reg [7:0] flash_bit_shift_out; reg [2:0] flash_bit_idx; reg flash_prev_cs; reg flash_prev_cclk; // The model watches the SAME physical bus the DUT drives -- it // reconstructs bytes from raw SCLK/MOSI transitions, exactly as a // real chip would, using the DUT's own internal cclk_r (only // observable via hierarchical reference since flash_spi_master.v // doesn't expose CCLK as a port, it's internal post-STARTUPE2 // wiring in the real module -- acceptable for a testbench, not // for synthesis). wire flash_sclk = u_dut.cclk_r; reg [7:0] model_shift; reg [2:0] model_bitcnt; reg [7:0] model_out_byte; reg [2:0] model_bytecnt; 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; // full byte received case (model_bytecnt) 3'd0: begin // check the just-captured byte directly, not // flash_cmd (whose own NBA update from this // SAME line hasn't committed yet this cycle) 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) begin flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi}; end model_bytecnt <= model_bytecnt + 1'b1; end default: ; endcase end else begin model_bitcnt <= model_bitcnt + 1'b1; end end end // MISO driver: Read Status Register-1 (0x05) returns {6'b0, wel, 1'b0(BUSY=0)} // Read Data (0x03) returns flash_mem[flash_addr] starting at the byte after addr reg [7:0] model_rdata_byte; 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 integer errors, tests; task automatic 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 // Drives byte_req/byte_wdata with NONBLOCKING assignment, same // established fix as EXP-0073/0075 (tb_neural_director_packed.v / // tb_spi_host_bridge_v3.v): a blocking-assignment one-shot pulse // races the DUT's own posedge-triggered read under Icarus and can // be missed entirely, not just corrupted -- confirmed here via a // real hang (byte_req never observed by the DUT at all) before // this fix. task automatic send_byte(input [7:0] b, output [7:0] r); begin @(posedge clk); byte_wdata <= b; byte_req <= 1'b1; @(posedge clk); byte_req <= 1'b0; while (!byte_done) @(posedge clk); r = byte_rdata; @(posedge clk); end endtask reg [7:0] rb; initial begin errors = 0; tests = 0; rst = 1; xfer_active <= 0; byte_req = 0; byte_wdata = 0; flash_miso = 0; model_bytecnt = 0; model_bitcnt = 0; flash_wel = 0; repeat(5) @(posedge clk); rst = 0; @(posedge clk); $display("=== TEST 1: WRITE ENABLE (0x06), then READ STATUS REGISTER-1 (0x05), expect WEL=1 ==="); xfer_active <= 1'b1; send_byte(8'h06, rb); xfer_active <= 1'b0; @(posedge clk); @(posedge clk); xfer_active <= 1'b1; send_byte(8'h05, rb); // command byte, response don't-care send_byte(8'h00, rb); // dummy clock, get status back xfer_active <= 1'b0; check(rb[1] == 1'b1, "T1: WEL bit set after Write Enable"); @(posedge clk); @(posedge clk); $display("=== TEST 2: PAGE PROGRAM (0x02) @ addr 0x10 = 0xA5, then READ DATA (0x03) same addr ==="); xfer_active <= 1'b1; send_byte(8'h02, rb); send_byte(8'h10, rb); send_byte(8'hA5, rb); xfer_active <= 1'b0; @(posedge clk); @(posedge clk); xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h10, rb); send_byte(8'h00, rb); // dummy clock, get data back xfer_active <= 1'b0; check(rb == 8'hA5, "T2: Read Data returns the byte just programmed, bit-exact"); @(posedge clk); @(posedge clk); $display("=== TEST 3: byte relay bit-exactness across several values (0x00,0xFF,0x55,0xAA) ==="); xfer_active <= 1'b1; send_byte(8'h02, rb); send_byte(8'h20, rb); send_byte(8'h00, rb); xfer_active <= 1'b0; @(posedge clk); @(posedge clk); xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h20, rb); send_byte(8'h00, rb); xfer_active <= 1'b0; check(rb == 8'h00, "T3: 0x00 round-trip"); @(posedge clk); @(posedge clk); xfer_active <= 1'b1; send_byte(8'h02, rb); send_byte(8'h21, rb); send_byte(8'hFF, rb); xfer_active <= 1'b0; @(posedge clk); @(posedge clk); xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h21, rb); send_byte(8'h00, rb); xfer_active <= 1'b0; check(rb == 8'hFF, "T3: 0xFF round-trip (catches stuck-low relay bugs)"); @(posedge clk); @(posedge clk); $display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors); if (errors == 0) $display("ALL TESTS PASSED (tb_flash_spi_master)"); $finish; end endmodule