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:
@@ -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.
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user