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
+93
View File
@@ -4644,3 +4644,96 @@ disclosed architectural gaps (real activation-fetch engine, N-slot
scaling beyond 2, board LOC constraints for SPI once the PCB pinout
is fixed) are the natural next steps once the user is back and can
weigh in on priority.
EXP-0075 -- real SPI physical-layer bug found+fixed (masked since V1);
register-file control interface added to spi_host_bridge_v3.v
(2026-09-19, same autonomous continuation, user's own explicit
request: "Creiamo un sistema configurabile con dei registri")
CONTEXT: extending spi_host_bridge_v3.v with REG_WRITE/REG_READ
opcodes (0x30/0x31) and a small register file (0x00 DEVICE_ID,
0x01 CONTROL, 0x02 STATUS, 0x03 N_SLOTS) for general device control/
status beyond job submission and raw memory access. Wrote hardware/
v3/sim/tb_spi_host_bridge_v3.v tests for the new opcodes; the
DEVICE_ID register (0x4E505601, whose LSB=0x01 has bit0 SET) was the
first response value in this module's entire test history whose last
transmitted bit is a real 1 -- every prior multi-byte MISO response
this project has ever tested (WRITE_JOB/READ_MEM's 0x1234, STATUS's
various bit patterns) coincidentally had a LAST bit of 0.
ROOT CAUSE (found via a DUT-internal $display, not guessing): the
physical layer's `assign miso = (cs_active && bit_count==3'd0) ?
tx_byte[7] : miso_shift_bit` -- present unchanged since hardware/v1/
rtl/spi_slave.v, carried into every SPI bridge this project has ever
built -- has a real bug. `bit_count` reads 0 not only right before a
fresh byte's first bit, but also for the ENTIRE remainder of the bit
period immediately AFTER a byte's LAST bit was sampled (it doesn't
advance again until the next byte's own first sampling edge). During
that whole tail window, the bypass shows tx_byte[7] (context for a
hypothetical NEW byte) instead of the correctly-prepared
miso_shift_bit (the OLD byte's real last bit) -- corrupting the last
bit of every response byte, WHENEVER a real (non-instantaneous) SPI
master's sample point falls inside that tail window, which is the
normal case for any real host. This was invisible in every previous
verified opcode purely because every test's own response data
happened to have a last bit of 0, matching the substituted tx_byte[7]
by coincidence, not because the value was actually correct.
FIX: removed the bypass entirely -- `assign miso = miso_shift_bit`.
Verified this doesn't need it: the special case only matters for a
genuinely fresh byte with ZERO prior falling edges in the current CS
session, which never happens for any of this protocol's real response
bytes (always preceded by the opcode byte and other payload bytes,
so miso_shift_bit is always already primed by the ordinary falling-
edge mechanism). Full regression re-run: 38/38 PASS, including a new
READ_MEM case (0x5679, LSB bit0=1) added specifically to catch this
class of bug for the memory-read path too, and the original WRITE_
JOB/STATUS/READ_MEM tests all still pass unchanged.
SCOPE (disclosed, not fixed today): hardware/v1/rtl/spi_slave.v and
hardware/v2/rtl/spi_host_bridge.v carry the SAME bypass, unchanged --
V2 is the frozen/archived ECP5 baseline (fork-before-promote
discipline, not touched this session) and V1 is even older, so
neither was modified, but BOTH almost certainly have the identical
real bug, silently corrupting the last bit of any MISO response whose
value happens to end in a 1. This does not affect any of this
session's DDR3/P&R work (independent module, no interaction with
memory timing), but is a real, disclosed correctness gap in the
V1/V2 SPI bridges for anyone revisiting them.
Also (separate, smaller): a `fork`/`join`-based watchdog was needed
for the new REG_WRITE test (Test K) -- REG_WRITE applies its effect
immediately on the last data byte, not after CS rises like RESET
does, so a watchdog that only starts watching after CS rises misses
the pulse entirely; fixed by running the watchdog concurrently with
the triggering spi_byte() call (same fork/join technique as
tb_sdram_arbiter_n.v's own one-shot-pulse watchers) -- another real,
now-documented testbench-timing lesson, not an RTL defect.
DECISION: spi_host_bridge_v3.v's physical layer is now genuinely
correct (not just "correct on the specific bit patterns tested so
far"). The register file (DEVICE_ID/CONTROL/STATUS/N_SLOTS, opcodes
0x30/0x31) gives host software a general-purpose control/status path
beyond job submission, per the user's own request.
Real board-level finding, same session: querying the routed n2_
system_ddr3_top's real device checkpoint (get_package_pins/get_ports
on xc7a100tcsg324-2) showed Vivado had auto-placed several of this
design's own unconstrained ports (job_out_done, two result-data bits)
directly onto the FPGA's DEDICATED Master-SPI configuration-flash
pins (FCS_B=L13, RDWR_B=R16, CSI_B=V15) -- a real conflict with any
future config-flash wiring. New hardware/v3/constraints/n2_system_
ddr3_top.xdc: (1) PROHIBITs those pins (plus D00_MOSI/D01_DIN/EMCCLK)
from ever being used by this design's own ports; (2) assigns the
neural-processor management SPI (sclk/mosi/miso/cs_n) to real,
verified-free pins A15/B16/B17/A16 (bank 15, package edge column,
physically adjacent for short PCB traces), chosen per the user's own
request ("pin più esterni possibile e vicini").
next_action: re-run the real in-context P&R (n2_system_ddr3_top.v +
new XDC + the register-file/physical-layer fixes) for a final,
up-to-date timing/resource signoff; write up the FPGA configuration
(boot) options for the user -- JTAG vs Master SPI with an external
config flash, using the real dedicated pins found this session
(PROGRAM_B=P9, INIT_B=P7, DONE=P10, M0=P12/M1=P13/M2=P11, CCLK=E9,
D00_MOSI=K17, D01_DIN=K18, FCS_B=L13).
@@ -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;