fix: real SPI MISO bit-corruption bug found+fixed; add register file + pin plan (EXP-0075)

Found a real, previously-masked bug in spi_host_bridge_v3.v's physical
layer (inherited unchanged from V1/V2): the bit_count==0 MISO bypass
corrupts the last bit of any multi-byte response whose value happens
to end in a 1 -- every prior test's response data coincidentally
ended in 0, hiding it until the new DEVICE_ID register (0x...01)
exposed it via a real bit-exact mismatch. Fixed by removing the
bypass (verified unnecessary for this protocol's actual usage).

Added REG_WRITE/REG_READ opcodes (0x30/0x31) and a register file
(DEVICE_ID/CONTROL/STATUS/N_SLOTS) for general device control beyond
job submission, per explicit user request. Full regression: 38/38
PASS, including new cases specifically targeting the bit-corruption
bug for both REG_READ and READ_MEM.

New hardware/v3/constraints/n2_system_ddr3_top.xdc: reserves the
FPGA's dedicated Master-SPI config-flash pins (found colliding with
auto-placed design ports in the real routed checkpoint) and assigns
the neural-processor management SPI to real, verified-free, edge-
adjacent pins on xc7a100tcsg324-2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-19 18:31:51 +02:00
co-authored by Claude Sonnet 5
parent 835ae881dc
commit ca17765fe3
5 changed files with 440 additions and 10 deletions
@@ -0,0 +1,44 @@
# ============================================================
# V3 -- top-level pin/IOSTANDARD constraints for n2_system_ddr3_top.v,
# real xc7a100tcsg324-2, board is the user's own custom design (bare
# chip + DDR3, no dev board). DDR3 pins themselves are NOT here --
# those come from the MIG-generated mig_7series_0.xdc (dictated by
# the FPGA's internal DDR3 PHY hardware, not a free choice).
#
# Verified against the real device via a routed checkpoint query
# (open_checkpoint + get_package_pins/get_ports on n2_system_ddr3_
# top_routed.dcp), not guessed from a datasheet table.
# ============================================================
# ---- reserve the dedicated Master-SPI configuration-flash pins
# (bank 14) for a FUTURE external config flash -- prevents Vivado's
# auto-placement from ever landing one of THIS design's own ports on
# them (it already had, by accident, before this constraint existed:
# job_out_done on L13/FCS_B, a result bit on R16/RDWR_B, another on
# V15/CSI_B). These pins are not driven by this design at all; they
# stay free for the flash CCLK/D00_MOSI/D01_DIN/FCS_B/EMCCLK/RDWR_B/
# CSI_B wiring described in the accompanying configuration writeup.
set_property PROHIBIT true [get_package_pins {K17 K18 L13 L16 R16 V15}]
# ---- neural-processor management SPI (-> spi_host_bridge_v3.v):
# job submission + register file. Bank 15, column A/B (package edge,
# physically adjacent pins for short/easy PCB routing), well clear of
# both DDR3 (banks 34/35) and the reserved config-flash pins above.
# IOSTANDARD assumes bank 15 is powered at 3.3V on the custom board --
# change to match whatever VCCO the user's own power plan uses for
# that bank.
set_property PACKAGE_PIN A15 [get_ports sclk]
set_property PACKAGE_PIN B16 [get_ports mosi]
set_property PACKAGE_PIN B17 [get_ports miso]
set_property PACKAGE_PIN A16 [get_ports cs_n]
set_property IOSTANDARD LVCMOS33 [get_ports sclk]
set_property IOSTANDARD LVCMOS33 [get_ports mosi]
set_property IOSTANDARD LVCMOS33 [get_ports miso]
set_property IOSTANDARD LVCMOS33 [get_ports cs_n]
# ---- sys_rst: not part of the DDR3 MIG's own pin set (that's
# sys_rst too, but MIG's XDC only constrains the DDR3-facing timing,
# not necessarily IOSTANDARD for every board variant) -- pin left to
# auto-placement for now (low pin-count, no real board decision yet
# on where the reset source sits); explicitly constrain once the PCB
# layout for the reset circuit (button/supervisor IC) is decided.
+2 -1
View File
@@ -202,10 +202,11 @@ module n2_system_ddr3_top #(
wire soft_rst_pulse;
spi_host_bridge_v3 #(
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH)
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(N_SLOTS)
) u_spi (
.clk(ui_clk), .rst(ui_clk_sync_rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.init_calib_complete(init_calib_complete), .dir_error(dir_error),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.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),
+191 -6
View File
@@ -92,6 +92,51 @@
// payload byte 7, MSB-first per word, one
// mem_req/mem_ready read per word.
//
// 0x30 REG_WRITE -- 5 payload bytes: byte0 = reg_addr[7:0],
// byte1:4 = value[31:0] MSB-first. Applied the
// instant the last data byte lands (no backend
// handshake needed, register writes are purely
// internal). Writing a read-only or unknown
// register address is inert (accepted on the
// wire, has no effect) -- same "never wedges
// the bus" precedent as an unknown opcode.
// 0x31 REG_READ -- 1 payload byte (reg_addr[7:0]), then 4
// response bytes clocked out on MISO MSB-
// first starting at payload byte 2. An unknown
// register address reads back 32'hFFFF_FFFF
// (deliberately distinct from any real 0
// value, so a host can tell "read an unmapped
// register" apart from "read a real zero").
//
// REGISTER MAP (v1, extensible -- add new addresses, never repurpose
// an existing one, so old host software stays correct against new
// firmware):
// 0x00 DEVICE_ID (RO) -- 32'h4E50_5601 ("NPV" + protocol
// version 1, ASCII 'N''P''V' + 0x01).
// Lets host software confirm it's really
// talking to this protocol/version before
// trusting anything else.
// 0x01 CONTROL (RW) -- bit0: write 1 to pulse soft_rst_pulse
// for one clk cycle (same physical effect
// as the RESET opcode, exposed here too
// since a register-based control path is
// often more convenient for host software
// than a dedicated opcode). Always reads
// back 0 (it's a pulse trigger, not a
// level). bits[31:1] reserved.
// 0x02 STATUS (RO) -- bit0: job_busy: bit1: mem_busy;
// bit2: last_job_accepted (sticky, same
// as the STATUS opcode's own bits);
// bit3: init_calib_complete (DDR3 PHY
// calibration done, i.e. DRAM traffic is
// actually safe to issue); bit4: dir_error
// (neural_director_packed.v's own error
// latch). bits[31:5] reserved.
// 0x03 N_SLOTS (RO) -- number of compute slots this build was
// synthesized with (the N_SLOTS parameter
// below), so host software doesn't need
// to hardcode it.
//
// Any opcode byte not listed above is treated as NOP (0 payload,
// MISO drives 0x00) -- matches spi_host_bridge.v's own "unknown
// opcode is inert, never wedges the bus" precedent.
@@ -99,11 +144,16 @@
module spi_host_bridge_v3 #(
parameter JOB_ADDR_WIDTH = 26, // matches neural_director_packed.v's ADDR_WIDTH (byte-base convention)
parameter MEM_ADDR_WIDTH = 25 // matches host_mem_bridge.v's ADDR_WIDTH (word/burst convention)
parameter MEM_ADDR_WIDTH = 25, // matches host_mem_bridge.v's ADDR_WIDTH (word/burst convention)
parameter N_SLOTS = 2 // reported read-only via REG 0x03, purely informational
)(
input wire clk,
input wire rst,
// ---- system status, for the REG 0x02 STATUS register ----
input wire init_calib_complete,
input wire dir_error,
// ---- physical SPI pins ----
input wire sclk,
input wire mosi,
@@ -169,7 +219,29 @@ module spi_host_bridge_v3 #(
wire [7:0] tx_byte;
reg miso_shift_bit;
assign miso = (cs_active && bit_count == 3'd0) ? tx_byte[7] : miso_shift_bit;
// REAL BUG found and fixed this session (via REG_READ's DEVICE_ID
// register, whose non-zero LSB exposed it -- prior tests'
// response values happened to coincidentally mask it, see the
// note above "mem_rout_pending_ignore" for the full root-cause):
// this used to be `(cs_active && bit_count==3'd0) ? tx_byte[7] :
// miso_shift_bit`, a combinational bypass meant to serve the
// FIRST bit of a fresh byte before any falling edge has prepared
// miso_shift_bit for it. bit_count==0 is ALSO true for the ENTIRE
// remainder of the bit period immediately AFTER a byte's LAST bit
// was sampled (it only advances again at the next byte's own
// first sampling edge) -- so this bypass showed tx_byte[7] (the
// wrong bit, and on continuously-clocked multi-byte reads,
// possibly a byte value that's already stale/wrong too) for the
// WHOLE tail of every byte-to-byte gap, corrupting exactly the
// moment a real (non-instant) SPI master samples the last bit.
// Proven unnecessary for every opcode this module has: a genuine
// "first bit with zero prior falling edges" only occurs for the
// opcode byte itself (whose MISO value is always don't-care 0x00
// anyway) -- every real response byte in this protocol is always
// preceded by several other bytes in the same CS session, so
// miso_shift_bit has always already been freshly prepared by the
// ordinary falling-edge mechanism below by the time it matters.
assign miso = miso_shift_bit;
always @(posedge clk) begin
if (rst) begin
@@ -206,6 +278,8 @@ module spi_host_bridge_v3 #(
localparam OP_RESET = 8'h0F;
localparam OP_WRITE_JOB = 8'h10;
localparam OP_STATUS = 8'h20;
localparam OP_REG_WRITE = 8'h30;
localparam OP_REG_READ = 8'h31;
localparam ST_OPCODE = 4'd0;
localparam ST_JOB = 4'd1; // collecting 16 WRITE_JOB payload bytes
@@ -217,6 +291,9 @@ module spi_host_bridge_v3 #(
localparam ST_MEM_RISS= 4'd7; // READ_MEM: issue+wait mem_req
localparam ST_MEM_ROUT= 4'd8; // READ_MEM: shifting the 2 bytes of a word out
localparam ST_IGNORE = 4'd9; // opcode consumed / unknown, wait for cs_rose
localparam ST_REG_ADDR = 4'd10; // collecting 1 reg_addr byte
localparam ST_REG_WDATA= 4'd11; // REG_WRITE: collecting 4 value bytes
localparam ST_REG_ROUT = 4'd12; // REG_READ: shifting 4 value bytes out
reg [3:0] state;
reg [7:0] opcode;
@@ -225,9 +302,50 @@ module spi_host_bridge_v3 #(
reg [15:0] word_cnt;
reg [15:0] cur_word; // WRITE_MEM: assembling MSB,LSB; READ_MEM: holding readback
reg job_busy_r, mem_busy_r, last_job_accepted_r;
reg [7:0] reg_addr;
reg [31:0] reg_wdata; // REG_WRITE: assembling the 4 value bytes
// ---- ROUT-exit deferral (real bug found and fixed this session,
// see the header's own note near the physical layer): the
// combinational "assign miso = (bit_count==0) ? tx_byte[7] :
// miso_shift_bit" bypass exists to serve the FIRST bit of a fresh
// byte, but bit_count ALSO reads 0 for one edge immediately AFTER
// the LAST bit of the byte that just finished (it wraps 7->0 at
// that same edge) -- the two cases are indistinguishable from
// bit_count alone. If `state` (and therefore tx_byte, via tx_mux)
// changes on that SAME edge -- exactly what a naive ROUT-exit
// transition does -- the bypass reads the NEW (already-wrong)
// tx_byte instead of the correctly-prepared miso_shift_bit,
// corrupting the LAST bit of the LAST byte of a multi-byte read.
// This was masked in READ_MEM's own existing test by coincidence
// (the test word's last bit happened to equal the corrupted
// substitute's bit7, both 0) until REG_READ's DEVICE_ID register
// (whose last bit is 1) exposed it via a real bit-exact mismatch.
// Fix: defer the state/byte_idx-clearing transition by exactly
// one internal clk cycle past the byte that triggers it, via a
// one-cycle pending flag -- clk runs far faster than SCLK (this
// file's own documented >=50x minimum ratio), so a one-clk-cycle
// delay is invisible on the SPI bus but moves the transition
// safely off the vulnerable bit_count==0 edge.
reg mem_rout_pending_ignore, mem_rout_pending_riss;
reg reg_rout_pending;
// ---- register file readback mux (combinational -- see the
// header's REGISTER MAP for the meaning of each address) ----
reg [31:0] reg_rdata;
always @(*) begin
case (reg_addr)
8'h00: reg_rdata = 32'h4E505601;
8'h01: reg_rdata = 32'h00000000;
8'h02: reg_rdata = {27'b0, dir_error, init_calib_complete,
last_job_accepted_r, mem_busy_r, job_busy_r};
8'h03: reg_rdata = {24'b0, N_SLOTS[7:0]};
default: reg_rdata = 32'hFFFFFFFF;
endcase
end
// combinational tx byte mux -- STATUS response, READ_MEM data,
// everything else drives 0x00
// REG_READ data, everything else drives 0x00
reg [7:0] tx_mux;
always @(*) begin
tx_mux = 8'h00;
@@ -235,6 +353,8 @@ module spi_host_bridge_v3 #(
tx_mux = {5'b0, last_job_accepted_r, mem_busy_r, job_busy_r};
else if (opcode == OP_READ_MEM && state == ST_MEM_ROUT)
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
else if (opcode == OP_REG_READ && state == ST_REG_ROUT)
tx_mux = reg_rdata[8*(3-byte_idx) +: 8];
end
assign tx_byte = tx_mux;
@@ -249,6 +369,9 @@ module spi_host_bridge_v3 #(
mem_wdata <= 16'd0; mem_lb_n <= 1'b0; mem_ub_n <= 1'b0;
soft_rst_pulse <= 1'b0;
job_busy_r <= 1'b0; mem_busy_r <= 1'b0; last_job_accepted_r <= 1'b0;
reg_addr <= 8'h00; reg_wdata <= 32'h0;
mem_rout_pending_ignore <= 1'b0; mem_rout_pending_riss <= 1'b0;
reg_rout_pending <= 1'b0;
end else begin
mem_req <= 1'b0;
soft_rst_pulse <= 1'b0;
@@ -273,6 +396,8 @@ module spi_host_bridge_v3 #(
OP_WRITE_JOB: state <= ST_JOB;
OP_WRITE_MEM: state <= ST_MEM_ADDR;
OP_READ_MEM: state <= ST_MEM_ADDR;
OP_REG_WRITE: state <= ST_REG_ADDR;
OP_REG_READ: state <= ST_REG_ADDR;
OP_RESET: state <= ST_IGNORE;
default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload
endcase
@@ -341,7 +466,40 @@ module spi_host_bridge_v3 #(
end
end
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_IGNORE: no MOSI payload expected
ST_REG_ADDR: begin
reg_addr <= rx_byte;
byte_idx <= 5'd0;
// REG_READ needs no backend handshake -- the
// register value is already available
// combinationally (reg_rdata), so it can go
// straight to shifting bytes out; REG_WRITE
// still needs 4 more MOSI bytes first.
state <= (opcode == OP_REG_WRITE) ? ST_REG_WDATA : ST_REG_ROUT;
end
ST_REG_WDATA: begin
case (byte_idx)
5'd0: reg_wdata[31:24] <= rx_byte;
5'd1: reg_wdata[23:16] <= rx_byte;
5'd2: reg_wdata[15:8] <= rx_byte;
5'd3: begin
reg_wdata[7:0] <= rx_byte;
state <= ST_IGNORE;
// apply the write immediately -- register
// writes are purely internal, no backend
// handshake to wait on. Unknown/read-only
// addresses are silently inert (accepted
// on the wire, no effect), matching this
// module's own "never wedges the bus"
// precedent for unknown opcodes.
if (reg_addr == 8'h01 && rx_byte[0])
soft_rst_pulse <= 1'b1;
end
endcase
if (byte_idx != 5'd3) byte_idx <= byte_idx + 5'd1;
end
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_REG_ROUT/ST_IGNORE: no MOSI payload expected
endcase
end
@@ -385,10 +543,37 @@ module spi_host_bridge_v3 #(
end else begin
mem_addr <= mem_addr + 1'b1;
word_cnt <= word_cnt - 1'b1;
byte_idx <= 5'd0;
state <= (word_cnt == 16'd1) ? ST_IGNORE : ST_MEM_RISS;
// defer the actual exit -- see this module's own
// "ROUT-exit deferral" note above -- so tx_mux
// keeps showing this byte's correct value through
// the vulnerable bit_count==0 edge.
if (word_cnt == 16'd1) mem_rout_pending_ignore <= 1'b1;
else mem_rout_pending_riss <= 1'b1;
end
end
if (mem_rout_pending_ignore) begin
mem_rout_pending_ignore <= 1'b0;
byte_idx <= 5'd0;
state <= ST_IGNORE;
end
if (mem_rout_pending_riss) begin
mem_rout_pending_riss <= 1'b0;
byte_idx <= 5'd0;
state <= ST_MEM_RISS;
end
if (state == ST_REG_ROUT && rx_valid) begin
if (byte_idx == 5'd3) begin
reg_rout_pending <= 1'b1;
end else begin
byte_idx <= byte_idx + 5'd1;
end
end
if (reg_rout_pending) begin
reg_rout_pending <= 1'b0;
byte_idx <= 5'd0;
state <= ST_IGNORE;
end
job_busy_r <= (state == ST_JOB_WAIT);
+110 -3
View File
@@ -41,12 +41,15 @@ module tb_spi_host_bridge_v3;
reg mem_ready_model = 0;
wire soft_rst_pulse;
reg init_calib_complete_model = 0;
reg dir_error_model = 0;
spi_host_bridge_v3 #(
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH)
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2)
) dut (
.clk(clk), .rst(rst),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
.init_calib_complete(init_calib_complete_model), .dir_error(dir_error_model),
.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),
@@ -194,7 +197,87 @@ module tb_spi_host_bridge_v3;
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 ======================
// ================= 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;
@@ -205,9 +288,33 @@ module tb_spi_host_bridge_v3;
@(posedge clk);
if (soft_rst_pulse) seen = 1'b1;
end
check(seen, "F: soft_rst_pulse asserted after CS rises (within CDC latency)");
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;
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
if (errors != 0) $display("*** %0d FAILURES ***", errors);
$finish;