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:
@@ -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