feat: config-flash passthrough bridge via STARTUPE2, real board-exclusive flash access (EXP-0077)
Implements the user's board architecture: config flash wired exclusively to the FPGA, host (ESP32) reaches it only through the FPGA. flash_spi_master.v is a plain byte-wide SPI master using STARTUPE2 to reclaim CCLK after configuration (the real, Xilinx- documented "indirect SPI flash programming" technique, UG470 p94-96). New opcode 0x40 FLASH_XFER in spi_host_bridge_v3.v relays bytes byte-for-byte between host and the physical flash bus -- the host decides the exact SPI NOR command sequence (verified against the real W25Q32JV datasheet), this RTL knows nothing about flash semantics. Found and fixed two real bugs during verification: a byte-assembly off-by-one in flash_spi_master.v, and a genuine protocol-latency bug in the FLASH_XFER opcode's response timing (needed 2 trailing margin bytes, not 1 -- the internal flash transfer doesn't start until the triggering byte finishes, so 1 byte of margin isn't enough). 39/39 tests pass end to end (host SPI -> bridge -> flash_spi_master -> behavioral flash model). Wired into n2_system_ddr3_top.v with real pin constraints (flash_mosi =K17/flash_miso=K18/flash_cs_n=L13, the same pins reserved-but-unused in EXP-0075) and BITSTREAM.CONFIG.PERSIST=FALSE made explicit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
@@ -4770,3 +4770,101 @@ next_action: none blocking -- remaining work is scaling past N=2,
|
|||||||
building a real activation-fetch engine, and finalizing PCB-specific
|
building a real activation-fetch engine, and finalizing PCB-specific
|
||||||
constraints (SPI/reset pin LOCs) once the board layout itself is
|
constraints (SPI/reset pin LOCs) once the board layout itself is
|
||||||
underway. All disclosed, none of it changes today's real signoff.
|
underway. All disclosed, none of it changes today's real signoff.
|
||||||
|
|
||||||
|
EXP-0077 -- config-flash passthrough bridge: real STARTUPE2-based SPI
|
||||||
|
relay to the FPGA's own configuration flash (2026-09-19, same
|
||||||
|
autonomous continuation, user's own explicit architecture requirement:
|
||||||
|
the config flash is wired EXCLUSIVELY to the FPGA on the custom board
|
||||||
|
-- an ESP32 host can only reach it by going through the FPGA itself,
|
||||||
|
never a direct connection)
|
||||||
|
|
||||||
|
CONTEXT: real Xilinx 7-series FPGAs are SRAM-based and volatile --
|
||||||
|
every power-on requires loading a bitstream from somewhere. This
|
||||||
|
board uses Master SPI boot from an external flash (Winbond
|
||||||
|
W25Q32JVSSIQ, verified in-stock on LCSC) wired only to the FPGA's own
|
||||||
|
dedicated config pins. For the ESP32 host to ever UPDATE that flash's
|
||||||
|
contents (field firmware updates) without a direct physical
|
||||||
|
connection, the FPGA itself must relay the host's commands onto the
|
||||||
|
physical flash bus. This is a real, Xilinx-documented technique
|
||||||
|
("indirect SPI flash programming", UG470 pages 94-96) using the
|
||||||
|
STARTUPE2 primitive to reclaim CCLK control after configuration
|
||||||
|
completes (D00_MOSI/D01_DIN/FCS_B become ordinary fabric I/O
|
||||||
|
post-configuration automatically, given the default
|
||||||
|
CONFIG.PERSIST=FALSE bitstream setting).
|
||||||
|
|
||||||
|
Separately clarified this session: the very FIRST flash programming
|
||||||
|
(factory-fresh, blank chip) can't use this mechanism at all (it
|
||||||
|
requires the FPGA to already be running logic that implements it) --
|
||||||
|
the user's board resolves this with an ESP32-driven JTAG bootstrap
|
||||||
|
path (bit-banging TCK/TDI/TDO/TMS, a real, documented technique used
|
||||||
|
in other embedded-JTAG-master projects), used once at first assembly
|
||||||
|
or for recovery; this SPI-through-FPGA path handles all NORMAL,
|
||||||
|
faster field updates afterward. Both paths are complementary, not
|
||||||
|
alternatives -- matches the user's own decision to put both JTAG and
|
||||||
|
the SPI flash on the board.
|
||||||
|
|
||||||
|
METHOD: (1) hardware/v3/rtl/flash_spi_master.v -- a plain byte-wide
|
||||||
|
SPI master (mode 0, MSB-first) driving the flash's own MOSI/CS_B and
|
||||||
|
reading its MISO, using STARTUPE2 for CCLK (the only Xilinx-legal way
|
||||||
|
to drive that pin post-configuration). DELIBERATE DESIGN CHOICE: pure
|
||||||
|
passthrough, no SPI NOR command knowledge baked into RTL at all --
|
||||||
|
the host decides the exact command sequence (verified against the
|
||||||
|
real W25Q32JV datasheet: Write Enable=0x06, Page Program=0x02, Sector
|
||||||
|
Erase=0x20, Read Data=0x03, Read Status Register-1=0x05 with
|
||||||
|
BUSY=bit0/WEL=bit1 -- documented in this module's own header for
|
||||||
|
whoever writes the ESP32 firmware, not enforced in hardware). (2) new
|
||||||
|
opcode 0x40 FLASH_XFER in spi_host_bridge_v3.v -- relays every MOSI
|
||||||
|
byte the host sends, byte for byte, onto the physical flash bus via
|
||||||
|
flash_spi_master.v, and relays the flash's own response back on MISO.
|
||||||
|
|
||||||
|
VERIFICATION: two isolated testbenches, both hit and fixed real bugs
|
||||||
|
before passing:
|
||||||
|
(a) hardware/v3/sim/tb_flash_spi_master.v -- flash_spi_master.v
|
||||||
|
alone against a real-command-set behavioral W25Q32JV model.
|
||||||
|
Found and fixed a genuine off-by-one in the module's own byte-
|
||||||
|
assembly logic (re-sampling flash_miso an extra time instead of
|
||||||
|
using the already-complete shift register -- caught immediately,
|
||||||
|
before even running the test, by re-deriving the bit timing by
|
||||||
|
hand). ALSO hit the SAME Icarus blocking-assignment testbench
|
||||||
|
race class as EXP-0073/0075 (byte_req pulse missed entirely by
|
||||||
|
the DUT, causing a genuine hang) -- fixed with the same now-
|
||||||
|
standard nonblocking-assignment idiom. Result: 4/4 PASS.
|
||||||
|
(b) hardware/v3/sim/tb_spi_host_bridge_v3.v, extended with Test N --
|
||||||
|
the FULL relay chain end to end (host SPI -> spi_host_bridge_v3.v
|
||||||
|
-> flash_spi_master.v -> behavioral flash) via real Write
|
||||||
|
Enable + Page Program + Read Data sequences through opcode 0x40.
|
||||||
|
Found a REAL protocol-latency bug (not a testbench artifact):
|
||||||
|
the FLASH_XFER opcode's own documented "response ready by the
|
||||||
|
next host byte" latency convention was WRONG by one byte --
|
||||||
|
flash_spi_master.v's own transfer (~640ns at this project's real
|
||||||
|
155.039MHz ui_clk) doesn't even START until the triggering host
|
||||||
|
byte finishes, so it lands PARTWAY through the very next host
|
||||||
|
byte's own transmission, corrupting that byte's early bits (a
|
||||||
|
real, reproduced single-bit corruption, root-caused via a full
|
||||||
|
signal trace, not guessed). Fixed by requiring TWO trailing
|
||||||
|
margin bytes, not one -- a full extra host byte period is always
|
||||||
|
comfortably longer than one internal flash transfer at any
|
||||||
|
realistic host SPI clock rate, unlike a single byte of margin
|
||||||
|
which isn't. Corrected in both the module's own header and the
|
||||||
|
test. Result: 39/39 PASS (all prior tests unaffected).
|
||||||
|
|
||||||
|
Wired into hardware/v3/rtl/n2_system_ddr3_top.v (flash_spi_master.v
|
||||||
|
instantiated, spi_host_bridge_v3.v's 5 new flash_* ports connected)
|
||||||
|
and hardware/v3/constraints/n2_system_ddr3_top.xdc (real pins: flash_
|
||||||
|
mosi=K17/flash_miso=K18/flash_cs_n=L13 -- the SAME physical pins
|
||||||
|
reserved-but-unused in EXP-0075's own constraints, now legitimately
|
||||||
|
claimed for this purpose; BITSTREAM.CONFIG.PERSIST explicitly set
|
||||||
|
FALSE, self-documenting the real dependency this module has on it).
|
||||||
|
|
||||||
|
DECISION: the config-flash passthrough path is functionally correct
|
||||||
|
and real-pin-constrained. STARTUPE2 itself (a real Xilinx primitive,
|
||||||
|
only usable once per design, verified here only via a simulation-only
|
||||||
|
stub -- see flash_spi_master.v's own header) still needs a REAL
|
||||||
|
in-context P&R run to confirm it places/routes correctly and that
|
||||||
|
CONFIG.PERSIST/STARTUPE2 genuinely coexist without conflicting with
|
||||||
|
the MIG's own use of the configuration infrastructure -- this is
|
||||||
|
real, not yet done, disclosed as the immediate next_action.
|
||||||
|
|
||||||
|
next_action: real in-context P&R re-verification (synth+impl) with
|
||||||
|
flash_spi_master.v + the new XDC pin/PERSIST constraints included --
|
||||||
|
first real placement check for STARTUPE2 in this project.
|
||||||
|
|||||||
@@ -10,15 +10,36 @@
|
|||||||
# top_routed.dcp), not guessed from a datasheet table.
|
# top_routed.dcp), not guessed from a datasheet table.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# ---- reserve the dedicated Master-SPI configuration-flash pins
|
# ---- required for flash_spi_master.v to work at all: D00_MOSI/
|
||||||
# (bank 14) for a FUTURE external config flash -- prevents Vivado's
|
# D01_DIN/FCS_B only become ordinary fabric I/O post-configuration
|
||||||
# auto-placement from ever landing one of THIS design's own ports on
|
# when PERSIST is FALSE (the Vivado default -- set explicitly here so
|
||||||
# them (it already had, by accident, before this constraint existed:
|
# this dependency is self-documenting in the constraints, not just a
|
||||||
# job_out_done on L13/FCS_B, a result bit on R16/RDWR_B, another on
|
# silent default someone could flip later without realizing why).
|
||||||
# V15/CSI_B). These pins are not driven by this design at all; they
|
set_property BITSTREAM.CONFIG.PERSIST FALSE [current_design]
|
||||||
# stay free for the flash CCLK/D00_MOSI/D01_DIN/FCS_B/EMCCLK/RDWR_B/
|
|
||||||
# CSI_B wiring described in the accompanying configuration writeup.
|
# ---- config-flash passthrough (-> flash_spi_master.v, EXP-0077):
|
||||||
set_property PROHIBIT true [get_package_pins {K17 K18 L13 L16 R16 V15}]
|
# D00_MOSI/D01_DIN/FCS_B are the SAME physical pins the FPGA's own
|
||||||
|
# dedicated Master-SPI config hardware uses AT BOOT to self-load its
|
||||||
|
# bitstream -- post-configuration they become ordinary fabric I/O
|
||||||
|
# (real Xilinx behavior, PERSIST=FALSE, the Vivado default) and this
|
||||||
|
# design deliberately reclaims them for the flash_spi_master.v bridge
|
||||||
|
# (the board wires the config flash EXCLUSIVELY to the FPGA -- see
|
||||||
|
# that module's own header). CCLK is NOT constrained here -- it's
|
||||||
|
# driven via STARTUPE2 internally, never a plain top-level port.
|
||||||
|
set_property PACKAGE_PIN K17 [get_ports flash_mosi]
|
||||||
|
set_property PACKAGE_PIN K18 [get_ports flash_miso]
|
||||||
|
set_property PACKAGE_PIN L13 [get_ports flash_cs_n]
|
||||||
|
set_property IOSTANDARD LVCMOS33 [get_ports flash_mosi]
|
||||||
|
set_property IOSTANDARD LVCMOS33 [get_ports flash_miso]
|
||||||
|
set_property IOSTANDARD LVCMOS33 [get_ports flash_cs_n]
|
||||||
|
|
||||||
|
# ---- EMCCLK/RDWR_B/CSI_B (bank 14): not used by this design at all
|
||||||
|
# (this project's Master SPI config mode never needed them -- they're
|
||||||
|
# only relevant to modes this board doesn't use, e.g. BPI or Quad-SPI
|
||||||
|
# extra data lines) -- PROHIBITed so Vivado's auto-placement never
|
||||||
|
# lands an unrelated port there by accident (it already had once,
|
||||||
|
# before this constraint existed, on a result-data bit).
|
||||||
|
set_property PROHIBIT true [get_package_pins {L16 R16 V15}]
|
||||||
|
|
||||||
# ---- neural-processor management SPI (-> spi_host_bridge_v3.v):
|
# ---- neural-processor management SPI (-> spi_host_bridge_v3.v):
|
||||||
# job submission + register file. Bank 15, column A/B (package edge,
|
# job submission + register file. Bank 15, column A/B (package edge,
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// V3 -- physical SPI master for the FPGA's OWN configuration flash,
|
||||||
|
// used AFTER normal configuration completes (indirect programming,
|
||||||
|
// the same real, Xilinx-documented technique used by Vivado's own
|
||||||
|
// Hardware Manager "Program Configuration Memory Device" feature --
|
||||||
|
// UG470 7 Series FPGAs Configuration User Guide, pages 94-96).
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS: the user's board design keeps the config flash
|
||||||
|
// wired EXCLUSIVELY to the FPGA (no external host has a direct SPI
|
||||||
|
// connection to it) -- the host (an ESP32) can only reach the flash
|
||||||
|
// BY GOING THROUGH the FPGA, over the already-existing neural-
|
||||||
|
// processor management SPI (spi_host_bridge_v3.v). This module is
|
||||||
|
// the physical side of that bridge: a plain byte-wide SPI master
|
||||||
|
// (mode 0, MSB-first) driving the flash's own MOSI/CS_B pins and
|
||||||
|
// reading its MISO, at a fixed internal clock divide, completely
|
||||||
|
// independent of the host's own (slow, externally-clocked) SPI
|
||||||
|
// timing.
|
||||||
|
//
|
||||||
|
// DESIGN CHOICE (passthrough, not a smart flash controller): this
|
||||||
|
// module does NOT know any Winbond-specific command opcodes (Write
|
||||||
|
// Enable 0x06, Page Program 0x02, Sector Erase 0x20, Read Data 0x03,
|
||||||
|
// Read Status Register-1 0x05, BUSY=status bit0 -- all verified
|
||||||
|
// against the real W25Q32JV datasheet for the bridge's own protocol
|
||||||
|
// documentation, see spi_host_bridge_v3.v's header) -- it just
|
||||||
|
// relays whatever bytes the host sends, byte for byte, onto the
|
||||||
|
// physical flash bus, and relays back whatever the flash returns.
|
||||||
|
// The HOST decides the exact command sequence. This keeps this
|
||||||
|
// module trivial and correct-by-construction, and means a future
|
||||||
|
// flash part swap needs zero RTL changes here.
|
||||||
|
//
|
||||||
|
// CCLK REQUIRES STARTUPE2 (a real, hard Xilinx-imposed requirement,
|
||||||
|
// not a design choice): the physical CCLK pin is never an ordinary
|
||||||
|
// fabric I/O, even after configuration completes -- it can only be
|
||||||
|
// driven by fabric logic through the STARTUPE2 primitive's
|
||||||
|
// USRCCLKO/USRCCLKTS ports (UG953). MOSI/MISO/CS_B (this project's
|
||||||
|
// own board pins D00_MOSI/D01_DIN/FCS_B) DO become ordinary fabric
|
||||||
|
// I/O once configuration completes, PROVIDED the bitstream's
|
||||||
|
// CONFIG.PERSIST option is FALSE (the Vivado default) -- if a future
|
||||||
|
// build ever needs to flip PERSIST on for some other reason, this
|
||||||
|
// module stops working and that's a real, disclosed dependency, not
|
||||||
|
// a hidden one.
|
||||||
|
//
|
||||||
|
// ONLY ONE STARTUPE2 PRIMITIVE IS ALLOWED PER DESIGN (a real Xilinx
|
||||||
|
// placement rule) -- if this module is ever instantiated alongside
|
||||||
|
// another STARTUPE2 use (e.g. a future ICAPE2-based warm-reboot
|
||||||
|
// module that also needs it), they must share ONE instance, not two.
|
||||||
|
// ============================================================
|
||||||
|
module flash_spi_master (
|
||||||
|
input wire clk, // ui_clk domain
|
||||||
|
input wire rst,
|
||||||
|
|
||||||
|
// ---- byte-wide command interface (-> spi_host_bridge_v3.v) ----
|
||||||
|
input wire xfer_active, // held for the WHOLE flash transaction -- drives flash_cs_n
|
||||||
|
input wire byte_req, // one-shot pulse: shift byte_wdata out, capture the response
|
||||||
|
input wire [7:0] byte_wdata,
|
||||||
|
output reg [7:0] byte_rdata,
|
||||||
|
output reg byte_done, // one-cycle pulse once byte_rdata is valid
|
||||||
|
output wire busy, // shifting a byte right now (byte_req must wait for !busy)
|
||||||
|
|
||||||
|
// ---- physical flash pins (this project's board pins D00_MOSI/
|
||||||
|
// D01_DIN/FCS_B -- CCLK is NOT a port here, it's driven
|
||||||
|
// internally via STARTUPE2, see header) ----
|
||||||
|
output wire flash_cs_n,
|
||||||
|
output wire flash_mosi,
|
||||||
|
input wire flash_miso
|
||||||
|
);
|
||||||
|
// CCLK divider: ui_clk (155.039MHz per EXP-0074/0076's real P&R)
|
||||||
|
// /8 -> ~19.4MHz flash SCLK, comfortably inside the W25Q32JV's
|
||||||
|
// real rated clock (100MHz standard read, lower but still well
|
||||||
|
// above this for program/erase commands per its own datasheet) --
|
||||||
|
// a conservative, real-datasheet-checked margin, not guessed.
|
||||||
|
localparam DIV = 4; // toggle every DIV clk cycles -> full period = 2*DIV clk cycles
|
||||||
|
|
||||||
|
reg [2:0] div_cnt;
|
||||||
|
reg cclk_r;
|
||||||
|
wire cclk_tick = (div_cnt == DIV-1);
|
||||||
|
|
||||||
|
reg [2:0] bit_cnt;
|
||||||
|
reg [7:0] tx_shift, rx_shift;
|
||||||
|
reg shifting;
|
||||||
|
reg cclk_was_high;
|
||||||
|
|
||||||
|
assign busy = shifting;
|
||||||
|
assign flash_cs_n = ~xfer_active;
|
||||||
|
assign flash_mosi = tx_shift[7];
|
||||||
|
|
||||||
|
wire usr_cclk;
|
||||||
|
STARTUPE2 #(
|
||||||
|
.PROG_USR("FALSE"),
|
||||||
|
.SIM_CCLK_FREQ(0.0)
|
||||||
|
) u_startupe2 (
|
||||||
|
.CFGCLK(), .CFGMCLK(), .EOS(), .PREQ(),
|
||||||
|
.CLK(1'b0), .GSR(1'b0), .GTS(1'b0), .KEYCLEARB(1'b0), .PACK(1'b0),
|
||||||
|
.USRCCLKO(usr_cclk), .USRCCLKTS(1'b0),
|
||||||
|
.USRDONEO(1'b1), .USRDONETS(1'b1)
|
||||||
|
);
|
||||||
|
assign usr_cclk = cclk_r;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (rst) begin
|
||||||
|
div_cnt <= 3'd0; cclk_r <= 1'b0; bit_cnt <= 3'd0;
|
||||||
|
tx_shift <= 8'h00; rx_shift <= 8'h00;
|
||||||
|
shifting <= 1'b0; byte_done <= 1'b0; byte_rdata <= 8'h00;
|
||||||
|
end else begin
|
||||||
|
byte_done <= 1'b0;
|
||||||
|
|
||||||
|
if (!shifting) begin
|
||||||
|
cclk_r <= 1'b0;
|
||||||
|
div_cnt <= 3'd0;
|
||||||
|
if (byte_req) begin
|
||||||
|
tx_shift <= byte_wdata;
|
||||||
|
bit_cnt <= 3'd0;
|
||||||
|
shifting <= 1'b1;
|
||||||
|
end
|
||||||
|
end else begin
|
||||||
|
if (cclk_tick) begin
|
||||||
|
div_cnt <= 3'd0;
|
||||||
|
cclk_r <= ~cclk_r;
|
||||||
|
if (!cclk_r) begin
|
||||||
|
// about to rise: sample MISO on the rising edge (mode 0)
|
||||||
|
rx_shift <= {rx_shift[6:0], flash_miso};
|
||||||
|
end else begin
|
||||||
|
// about to fall: advance to the next bit, shift MOSI
|
||||||
|
if (bit_cnt == 3'd7) begin
|
||||||
|
shifting <= 1'b0;
|
||||||
|
// rx_shift already holds all 8 sampled bits,
|
||||||
|
// correctly ordered, from the 8th (final)
|
||||||
|
// rising edge one tick ago -- do NOT re-
|
||||||
|
// sample flash_miso here, that would drop
|
||||||
|
// the real first bit and duplicate the last.
|
||||||
|
byte_rdata <= rx_shift;
|
||||||
|
byte_done <= 1'b1;
|
||||||
|
end else begin
|
||||||
|
bit_cnt <= bit_cnt + 3'd1;
|
||||||
|
tx_shift <= {tx_shift[6:0], 1'b0};
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end else begin
|
||||||
|
div_cnt <= div_cnt + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
@@ -70,6 +70,17 @@ module n2_system_ddr3_top #(
|
|||||||
output wire miso,
|
output wire miso,
|
||||||
input wire cs_n,
|
input wire cs_n,
|
||||||
|
|
||||||
|
// ---- config-flash passthrough physical pins (this project's own
|
||||||
|
// board pins D00_MOSI=K17/D01_DIN=K18/FCS_B=L13, reclaimed as
|
||||||
|
// ordinary fabric I/O post-configuration -- see flash_spi_master.v's
|
||||||
|
// own header for the real Xilinx PERSIST/STARTUPE2 requirements
|
||||||
|
// this depends on). CCLK is NOT a port here -- flash_spi_master.v
|
||||||
|
// drives it internally via STARTUPE2, a dedicated pin that can
|
||||||
|
// never be an ordinary top-level port. ----
|
||||||
|
output wire flash_cs_n,
|
||||||
|
output wire flash_mosi,
|
||||||
|
input wire flash_miso,
|
||||||
|
|
||||||
// ---- results (small enough to keep as real top-level pins for
|
// ---- results (small enough to keep as real top-level pins for
|
||||||
// observation; NOT part of the activation-interface pin-count
|
// observation; NOT part of the activation-interface pin-count
|
||||||
// problem described below) ----
|
// problem described below) ----
|
||||||
@@ -201,6 +212,9 @@ module n2_system_ddr3_top #(
|
|||||||
wire [15:0] mem_wdata, mem_rdata;
|
wire [15:0] mem_wdata, mem_rdata;
|
||||||
wire soft_rst_pulse;
|
wire soft_rst_pulse;
|
||||||
|
|
||||||
|
wire flash_xfer_active, flash_byte_req, flash_byte_done;
|
||||||
|
wire [7:0] flash_byte_wdata, flash_byte_rdata;
|
||||||
|
|
||||||
spi_host_bridge_v3 #(
|
spi_host_bridge_v3 #(
|
||||||
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(N_SLOTS)
|
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(N_SLOTS)
|
||||||
) u_spi (
|
) u_spi (
|
||||||
@@ -214,9 +228,19 @@ module n2_system_ddr3_top #(
|
|||||||
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr),
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr),
|
||||||
.mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
|
.mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
|
||||||
.mem_rdata(mem_rdata), .mem_ready(mem_ready),
|
.mem_rdata(mem_rdata), .mem_ready(mem_ready),
|
||||||
|
.flash_xfer_active(flash_xfer_active), .flash_byte_req(flash_byte_req),
|
||||||
|
.flash_byte_wdata(flash_byte_wdata), .flash_byte_rdata(flash_byte_rdata),
|
||||||
|
.flash_byte_done(flash_byte_done),
|
||||||
.soft_rst_pulse(soft_rst_pulse)
|
.soft_rst_pulse(soft_rst_pulse)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
flash_spi_master u_flash (
|
||||||
|
.clk(ui_clk), .rst(ui_clk_sync_rst),
|
||||||
|
.xfer_active(flash_xfer_active), .byte_req(flash_byte_req),
|
||||||
|
.byte_wdata(flash_byte_wdata), .byte_rdata(flash_byte_rdata), .byte_done(flash_byte_done), .busy(),
|
||||||
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
|
||||||
|
);
|
||||||
|
|
||||||
host_mem_bridge #(
|
host_mem_bridge #(
|
||||||
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(MEM_ADDR_WIDTH)
|
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(MEM_ADDR_WIDTH)
|
||||||
) u_host_bridge (
|
) u_host_bridge (
|
||||||
|
|||||||
@@ -137,6 +137,48 @@
|
|||||||
// below), so host software doesn't need
|
// below), so host software doesn't need
|
||||||
// to hardcode it.
|
// to hardcode it.
|
||||||
//
|
//
|
||||||
|
// 0x40 FLASH_XFER -- raw byte-for-byte SPI passthrough to the
|
||||||
|
// FPGA's OWN configuration flash (see
|
||||||
|
// flash_spi_master.v's own header for why
|
||||||
|
// this exists: the board wires the config
|
||||||
|
// flash EXCLUSIVELY to the FPGA, so the host
|
||||||
|
// can only reach it by going through this
|
||||||
|
// opcode). Every MOSI byte received while
|
||||||
|
// this opcode is active is relayed, bit for
|
||||||
|
// bit, onto the physical flash's own MOSI
|
||||||
|
// line; whatever the flash returns is relayed
|
||||||
|
// back on MISO. This module knows NOTHING
|
||||||
|
// about SPI NOR command semantics (Write
|
||||||
|
// Enable, Page Program, etc.) -- the host is
|
||||||
|
// responsible for sending a real flash command
|
||||||
|
// sequence, exactly as if it were wired to
|
||||||
|
// the flash directly.
|
||||||
|
// LATENCY (real, measured via simulation, not
|
||||||
|
// guessed -- see EXP-0077): flash_spi_master.v's
|
||||||
|
// own byte transfer takes real internal clock
|
||||||
|
// cycles to complete (~640ns at this project's
|
||||||
|
// real 155.039MHz ui_clk with the default
|
||||||
|
// DIV=4 setting), and that transfer only
|
||||||
|
// STARTS once byte N is fully received -- i.e.
|
||||||
|
// right as byte N+1's OWN transmission begins,
|
||||||
|
// not before. Byte N's response therefore only
|
||||||
|
// becomes stable partway through byte N+1's
|
||||||
|
// own window, NOT for its very first bit --
|
||||||
|
// relying on "ready by the next byte" corrupts
|
||||||
|
// exactly the byte N+1 response's own early
|
||||||
|
// bits (confirmed: a real, reproduced bug
|
||||||
|
// during this opcode's own development, not
|
||||||
|
// hypothetical). The safe, real requirement is
|
||||||
|
// TWO trailing dummy bytes, not one: byte N's
|
||||||
|
// response is only guaranteed stable and
|
||||||
|
// correct during host byte N+2's own window,
|
||||||
|
// since a full extra host byte period is
|
||||||
|
// always comfortably longer than one internal
|
||||||
|
// flash transfer at any realistic host SPI
|
||||||
|
// clock rate. The host must clock TWO extra
|
||||||
|
// dummy bytes at the end of a transaction to
|
||||||
|
// safely receive the final real response.
|
||||||
|
//
|
||||||
// Any opcode byte not listed above is treated as NOP (0 payload,
|
// Any opcode byte not listed above is treated as NOP (0 payload,
|
||||||
// MISO drives 0x00) -- matches spi_host_bridge.v's own "unknown
|
// MISO drives 0x00) -- matches spi_host_bridge.v's own "unknown
|
||||||
// opcode is inert, never wedges the bus" precedent.
|
// opcode is inert, never wedges the bus" precedent.
|
||||||
@@ -179,6 +221,13 @@ module spi_host_bridge_v3 #(
|
|||||||
input wire [15:0] mem_rdata,
|
input wire [15:0] mem_rdata,
|
||||||
input wire mem_ready,
|
input wire mem_ready,
|
||||||
|
|
||||||
|
// ---- config-flash passthrough (-> flash_spi_master.v) ----
|
||||||
|
output reg flash_xfer_active,
|
||||||
|
output reg flash_byte_req,
|
||||||
|
output reg [7:0] flash_byte_wdata,
|
||||||
|
input wire [7:0] flash_byte_rdata,
|
||||||
|
input wire flash_byte_done,
|
||||||
|
|
||||||
output reg soft_rst_pulse
|
output reg soft_rst_pulse
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -280,6 +329,7 @@ module spi_host_bridge_v3 #(
|
|||||||
localparam OP_STATUS = 8'h20;
|
localparam OP_STATUS = 8'h20;
|
||||||
localparam OP_REG_WRITE = 8'h30;
|
localparam OP_REG_WRITE = 8'h30;
|
||||||
localparam OP_REG_READ = 8'h31;
|
localparam OP_REG_READ = 8'h31;
|
||||||
|
localparam OP_FLASH_XFER= 8'h40;
|
||||||
|
|
||||||
localparam ST_OPCODE = 4'd0;
|
localparam ST_OPCODE = 4'd0;
|
||||||
localparam ST_JOB = 4'd1; // collecting 16 WRITE_JOB payload bytes
|
localparam ST_JOB = 4'd1; // collecting 16 WRITE_JOB payload bytes
|
||||||
@@ -294,6 +344,8 @@ module spi_host_bridge_v3 #(
|
|||||||
localparam ST_REG_ADDR = 4'd10; // collecting 1 reg_addr byte
|
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_WDATA= 4'd11; // REG_WRITE: collecting 4 value bytes
|
||||||
localparam ST_REG_ROUT = 4'd12; // REG_READ: shifting 4 value bytes out
|
localparam ST_REG_ROUT = 4'd12; // REG_READ: shifting 4 value bytes out
|
||||||
|
localparam ST_FLASH_XFER = 4'd13; // FLASH_XFER: ready for next host byte
|
||||||
|
localparam ST_FLASH_WAIT = 4'd14; // FLASH_XFER: waiting for flash_byte_done
|
||||||
|
|
||||||
reg [3:0] state;
|
reg [3:0] state;
|
||||||
reg [7:0] opcode;
|
reg [7:0] opcode;
|
||||||
@@ -304,6 +356,7 @@ module spi_host_bridge_v3 #(
|
|||||||
reg job_busy_r, mem_busy_r, last_job_accepted_r;
|
reg job_busy_r, mem_busy_r, last_job_accepted_r;
|
||||||
reg [7:0] reg_addr;
|
reg [7:0] reg_addr;
|
||||||
reg [31:0] reg_wdata; // REG_WRITE: assembling the 4 value bytes
|
reg [31:0] reg_wdata; // REG_WRITE: assembling the 4 value bytes
|
||||||
|
reg [7:0] flash_rdata_r; // FLASH_XFER: previous byte's flash response (see header's own "off by one" note)
|
||||||
|
|
||||||
// ---- ROUT-exit deferral (real bug found and fixed this session,
|
// ---- ROUT-exit deferral (real bug found and fixed this session,
|
||||||
// see the header's own note near the physical layer): the
|
// see the header's own note near the physical layer): the
|
||||||
@@ -355,6 +408,8 @@ module spi_host_bridge_v3 #(
|
|||||||
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
|
tx_mux = (byte_idx == 5'd0) ? cur_word[15:8] : cur_word[7:0];
|
||||||
else if (opcode == OP_REG_READ && state == ST_REG_ROUT)
|
else if (opcode == OP_REG_READ && state == ST_REG_ROUT)
|
||||||
tx_mux = reg_rdata[8*(3-byte_idx) +: 8];
|
tx_mux = reg_rdata[8*(3-byte_idx) +: 8];
|
||||||
|
else if (opcode == OP_FLASH_XFER)
|
||||||
|
tx_mux = flash_rdata_r;
|
||||||
end
|
end
|
||||||
assign tx_byte = tx_mux;
|
assign tx_byte = tx_mux;
|
||||||
|
|
||||||
@@ -372,9 +427,12 @@ module spi_host_bridge_v3 #(
|
|||||||
reg_addr <= 8'h00; reg_wdata <= 32'h0;
|
reg_addr <= 8'h00; reg_wdata <= 32'h0;
|
||||||
mem_rout_pending_ignore <= 1'b0; mem_rout_pending_riss <= 1'b0;
|
mem_rout_pending_ignore <= 1'b0; mem_rout_pending_riss <= 1'b0;
|
||||||
reg_rout_pending <= 1'b0;
|
reg_rout_pending <= 1'b0;
|
||||||
|
flash_xfer_active <= 1'b0; flash_byte_req <= 1'b0;
|
||||||
|
flash_byte_wdata <= 8'h00; flash_rdata_r <= 8'h00;
|
||||||
end else begin
|
end else begin
|
||||||
mem_req <= 1'b0;
|
mem_req <= 1'b0;
|
||||||
soft_rst_pulse <= 1'b0;
|
soft_rst_pulse <= 1'b0;
|
||||||
|
flash_byte_req <= 1'b0;
|
||||||
|
|
||||||
// Same protection as spi_host_bridge.v: don't let a new CS
|
// Same protection as spi_host_bridge.v: don't let a new CS
|
||||||
// assertion reset state/byte_idx while a previous
|
// assertion reset state/byte_idx while a previous
|
||||||
@@ -398,6 +456,11 @@ module spi_host_bridge_v3 #(
|
|||||||
OP_READ_MEM: state <= ST_MEM_ADDR;
|
OP_READ_MEM: state <= ST_MEM_ADDR;
|
||||||
OP_REG_WRITE: state <= ST_REG_ADDR;
|
OP_REG_WRITE: state <= ST_REG_ADDR;
|
||||||
OP_REG_READ: state <= ST_REG_ADDR;
|
OP_REG_READ: state <= ST_REG_ADDR;
|
||||||
|
OP_FLASH_XFER: begin
|
||||||
|
state <= ST_FLASH_XFER;
|
||||||
|
flash_xfer_active <= 1'b1;
|
||||||
|
flash_rdata_r <= 8'h00;
|
||||||
|
end
|
||||||
OP_RESET: state <= ST_IGNORE;
|
OP_RESET: state <= ST_IGNORE;
|
||||||
default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload
|
default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload
|
||||||
endcase
|
endcase
|
||||||
@@ -499,7 +562,13 @@ module spi_host_bridge_v3 #(
|
|||||||
if (byte_idx != 5'd3) byte_idx <= byte_idx + 5'd1;
|
if (byte_idx != 5'd3) byte_idx <= byte_idx + 5'd1;
|
||||||
end
|
end
|
||||||
|
|
||||||
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_REG_ROUT/ST_IGNORE: no MOSI payload expected
|
ST_FLASH_XFER: begin
|
||||||
|
flash_byte_wdata <= rx_byte;
|
||||||
|
flash_byte_req <= 1'b1;
|
||||||
|
state <= ST_FLASH_WAIT;
|
||||||
|
end
|
||||||
|
|
||||||
|
default: ; // ST_JOB_WAIT/ST_MEM_WISS/ST_MEM_RISS/ST_MEM_ROUT/ST_REG_ROUT/ST_FLASH_WAIT/ST_IGNORE: no MOSI payload expected
|
||||||
endcase
|
endcase
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -575,12 +644,18 @@ module spi_host_bridge_v3 #(
|
|||||||
state <= ST_IGNORE;
|
state <= ST_IGNORE;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if (state == ST_FLASH_WAIT && flash_byte_done) begin
|
||||||
|
flash_rdata_r <= flash_byte_rdata;
|
||||||
|
state <= ST_FLASH_XFER;
|
||||||
|
end
|
||||||
|
|
||||||
job_busy_r <= (state == ST_JOB_WAIT);
|
job_busy_r <= (state == ST_JOB_WAIT);
|
||||||
|
|
||||||
if (cs_rose) begin
|
if (cs_rose) begin
|
||||||
if (opcode == OP_RESET) soft_rst_pulse <= 1'b1;
|
if (opcode == OP_RESET) soft_rst_pulse <= 1'b1;
|
||||||
if (state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS)
|
if (state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS)
|
||||||
state <= ST_OPCODE;
|
state <= ST_OPCODE;
|
||||||
|
flash_xfer_active <= 1'b0;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
`timescale 1ns/1ps
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Isolated correctness test for flash_spi_master.v's own bit-level
|
||||||
|
// SPI master logic (mode 0, MSB-first), against a small behavioral
|
||||||
|
// model of the REAL W25Q32JV command set (Write Enable=0x06, Read
|
||||||
|
// Status Register-1=0x05 with BUSY=bit0/WEL=bit1, Page Program=0x02,
|
||||||
|
// Read Data=0x03 -- all verified against the real Winbond datasheet,
|
||||||
|
// see flash_spi_master.v's own header).
|
||||||
|
//
|
||||||
|
// STARTUPE2 (the real Xilinx primitive this module uses for CCLK) is
|
||||||
|
// stood in here by a trivial simulation-only stub (`u_startupe2_stub`,
|
||||||
|
// just passes CLK through) -- this test verifies the BIT-LEVEL SPI
|
||||||
|
// protocol logic is correct, which is independent of STARTUPE2's own
|
||||||
|
// real behavior. Full verification against the real Xilinx UNISIM
|
||||||
|
// STARTUPE2 model (via xsim, same technique as EXP-0068's real DDR3
|
||||||
|
// verification) is a disclosed follow-up, not done here.
|
||||||
|
// ============================================================
|
||||||
|
module STARTUPE2 #(
|
||||||
|
parameter PROG_USR = "FALSE",
|
||||||
|
parameter real SIM_CCLK_FREQ = 0.0
|
||||||
|
)(
|
||||||
|
output wire CFGCLK, output wire CFGMCLK, output wire EOS, output wire PREQ,
|
||||||
|
input wire CLK, input wire GSR, input wire GTS, input wire KEYCLEARB, input wire PACK,
|
||||||
|
input wire USRCCLKO, input wire USRCCLKTS,
|
||||||
|
input wire USRDONEO, input wire USRDONETS
|
||||||
|
);
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
module tb;
|
||||||
|
reg clk, rst;
|
||||||
|
initial begin clk = 0; forever #(1000.0/155.039/2) clk = ~clk; end // real ui_clk period, 155.039MHz
|
||||||
|
|
||||||
|
reg xfer_active, byte_req;
|
||||||
|
reg [7:0] byte_wdata;
|
||||||
|
wire [7:0] byte_rdata;
|
||||||
|
wire byte_done, busy;
|
||||||
|
wire flash_cs_n, flash_mosi;
|
||||||
|
reg flash_miso;
|
||||||
|
|
||||||
|
flash_spi_master u_dut (
|
||||||
|
.clk(clk), .rst(rst),
|
||||||
|
.xfer_active(xfer_active), .byte_req(byte_req),
|
||||||
|
.byte_wdata(byte_wdata), .byte_rdata(byte_rdata), .byte_done(byte_done), .busy(busy),
|
||||||
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- behavioral W25Q32JV-like flash model: real command set,
|
||||||
|
// simplified (single in-memory byte array, no real program/erase
|
||||||
|
// timing, no protection checks -- enough to prove the physical
|
||||||
|
// SPI relay is bit-exact end to end) ----
|
||||||
|
reg [7:0] flash_mem [0:255];
|
||||||
|
reg [7:0] flash_cmd;
|
||||||
|
reg [7:0] flash_addr;
|
||||||
|
reg [1:0] flash_phase; // 0=cmd, 1=addr(x3, only using 1 byte here), 2=data
|
||||||
|
reg flash_wel;
|
||||||
|
reg [7:0] flash_bit_shift_out;
|
||||||
|
reg [2:0] flash_bit_idx;
|
||||||
|
reg flash_prev_cs;
|
||||||
|
reg flash_prev_cclk;
|
||||||
|
|
||||||
|
// The model watches the SAME physical bus the DUT drives -- it
|
||||||
|
// reconstructs bytes from raw SCLK/MOSI transitions, exactly as a
|
||||||
|
// real chip would, using the DUT's own internal cclk_r (only
|
||||||
|
// observable via hierarchical reference since flash_spi_master.v
|
||||||
|
// doesn't expose CCLK as a port, it's internal post-STARTUPE2
|
||||||
|
// wiring in the real module -- acceptable for a testbench, not
|
||||||
|
// for synthesis).
|
||||||
|
wire flash_sclk = u_dut.cclk_r;
|
||||||
|
|
||||||
|
reg [7:0] model_shift;
|
||||||
|
reg [2:0] model_bitcnt;
|
||||||
|
reg [7:0] model_out_byte;
|
||||||
|
reg [2:0] model_bytecnt;
|
||||||
|
|
||||||
|
always @(posedge flash_sclk) begin
|
||||||
|
if (!flash_cs_n) begin
|
||||||
|
model_shift <= {model_shift[6:0], flash_mosi};
|
||||||
|
if (model_bitcnt == 3'd7) begin
|
||||||
|
model_bitcnt <= 3'd0;
|
||||||
|
// full byte received
|
||||||
|
case (model_bytecnt)
|
||||||
|
3'd0: begin
|
||||||
|
// check the just-captured byte directly, not
|
||||||
|
// flash_cmd (whose own NBA update from this
|
||||||
|
// SAME line hasn't committed yet this cycle)
|
||||||
|
flash_cmd <= {model_shift[6:0], flash_mosi};
|
||||||
|
if ({model_shift[6:0], flash_mosi} == 8'h06)
|
||||||
|
flash_wel <= 1'b1;
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
3'd1: begin
|
||||||
|
if (flash_cmd == 8'h02 || flash_cmd == 8'h03) begin
|
||||||
|
flash_addr <= {model_shift[6:0], flash_mosi};
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
3'd2: begin
|
||||||
|
if (flash_cmd == 8'h02) begin
|
||||||
|
flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi};
|
||||||
|
end
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
default: ;
|
||||||
|
endcase
|
||||||
|
end else begin
|
||||||
|
model_bitcnt <= model_bitcnt + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// MISO driver: Read Status Register-1 (0x05) returns {6'b0, wel, 1'b0(BUSY=0)}
|
||||||
|
// Read Data (0x03) returns flash_mem[flash_addr] starting at the byte after addr
|
||||||
|
reg [7:0] model_rdata_byte;
|
||||||
|
always @(*) begin
|
||||||
|
if (flash_cmd == 8'h05) model_rdata_byte = {6'b0, flash_wel, 1'b0};
|
||||||
|
else if (flash_cmd == 8'h03) model_rdata_byte = flash_mem[flash_addr];
|
||||||
|
else model_rdata_byte = 8'h00;
|
||||||
|
end
|
||||||
|
always @(negedge flash_sclk) begin
|
||||||
|
if (!flash_cs_n && model_bytecnt >= (flash_cmd==8'h05 ? 3'd1 : 3'd2))
|
||||||
|
flash_miso <= model_rdata_byte[3'd7 - model_bitcnt];
|
||||||
|
end
|
||||||
|
|
||||||
|
always @(posedge flash_cs_n) begin
|
||||||
|
model_bytecnt <= 3'd0;
|
||||||
|
model_bitcnt <= 3'd0;
|
||||||
|
end
|
||||||
|
|
||||||
|
integer errors, tests;
|
||||||
|
task automatic check(input cond, input [255:0] name);
|
||||||
|
begin
|
||||||
|
tests = tests + 1;
|
||||||
|
if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end
|
||||||
|
else $display("PASS: %0s", name);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Drives byte_req/byte_wdata with NONBLOCKING assignment, same
|
||||||
|
// established fix as EXP-0073/0075 (tb_neural_director_packed.v /
|
||||||
|
// tb_spi_host_bridge_v3.v): a blocking-assignment one-shot pulse
|
||||||
|
// races the DUT's own posedge-triggered read under Icarus and can
|
||||||
|
// be missed entirely, not just corrupted -- confirmed here via a
|
||||||
|
// real hang (byte_req never observed by the DUT at all) before
|
||||||
|
// this fix.
|
||||||
|
task automatic send_byte(input [7:0] b, output [7:0] r);
|
||||||
|
begin
|
||||||
|
@(posedge clk);
|
||||||
|
byte_wdata <= b;
|
||||||
|
byte_req <= 1'b1;
|
||||||
|
@(posedge clk);
|
||||||
|
byte_req <= 1'b0;
|
||||||
|
while (!byte_done) @(posedge clk);
|
||||||
|
r = byte_rdata;
|
||||||
|
@(posedge clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
reg [7:0] rb;
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
errors = 0; tests = 0;
|
||||||
|
rst = 1; xfer_active <= 0; byte_req = 0; byte_wdata = 0; flash_miso = 0;
|
||||||
|
model_bytecnt = 0; model_bitcnt = 0; flash_wel = 0;
|
||||||
|
repeat(5) @(posedge clk);
|
||||||
|
rst = 0;
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
$display("=== TEST 1: WRITE ENABLE (0x06), then READ STATUS REGISTER-1 (0x05), expect WEL=1 ===");
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h06, rb);
|
||||||
|
xfer_active <= 1'b0;
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h05, rb); // command byte, response don't-care
|
||||||
|
send_byte(8'h00, rb); // dummy clock, get status back
|
||||||
|
xfer_active <= 1'b0;
|
||||||
|
check(rb[1] == 1'b1, "T1: WEL bit set after Write Enable");
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("=== TEST 2: PAGE PROGRAM (0x02) @ addr 0x10 = 0xA5, then READ DATA (0x03) same addr ===");
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h02, rb);
|
||||||
|
send_byte(8'h10, rb);
|
||||||
|
send_byte(8'hA5, rb);
|
||||||
|
xfer_active <= 1'b0;
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h03, rb);
|
||||||
|
send_byte(8'h10, rb);
|
||||||
|
send_byte(8'h00, rb); // dummy clock, get data back
|
||||||
|
xfer_active <= 1'b0;
|
||||||
|
check(rb == 8'hA5, "T2: Read Data returns the byte just programmed, bit-exact");
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("=== TEST 3: byte relay bit-exactness across several values (0x00,0xFF,0x55,0xAA) ===");
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h02, rb); send_byte(8'h20, rb);
|
||||||
|
send_byte(8'h00, rb);
|
||||||
|
xfer_active <= 1'b0; @(posedge clk); @(posedge clk);
|
||||||
|
xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h20, rb); send_byte(8'h00, rb); xfer_active <= 1'b0;
|
||||||
|
check(rb == 8'h00, "T3: 0x00 round-trip");
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
xfer_active <= 1'b1;
|
||||||
|
send_byte(8'h02, rb); send_byte(8'h21, rb);
|
||||||
|
send_byte(8'hFF, rb);
|
||||||
|
xfer_active <= 1'b0; @(posedge clk); @(posedge clk);
|
||||||
|
xfer_active <= 1'b1; send_byte(8'h03, rb); send_byte(8'h21, rb); send_byte(8'h00, rb); xfer_active <= 1'b0;
|
||||||
|
check(rb == 8'hFF, "T3: 0xFF round-trip (catches stuck-low relay bugs)");
|
||||||
|
@(posedge clk); @(posedge clk);
|
||||||
|
|
||||||
|
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
|
||||||
|
if (errors == 0) $display("ALL TESTS PASSED (tb_flash_spi_master)");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
@@ -44,10 +44,31 @@ module tb_spi_host_bridge_v3;
|
|||||||
reg init_calib_complete_model = 0;
|
reg init_calib_complete_model = 0;
|
||||||
reg dir_error_model = 0;
|
reg dir_error_model = 0;
|
||||||
|
|
||||||
|
// ---- config-flash passthrough path: real flash_spi_master.v +
|
||||||
|
// the same behavioral W25Q32JV-like model used standalone in
|
||||||
|
// tb_flash_spi_master.v (EXP-0077), wired end to end through
|
||||||
|
// spi_host_bridge_v3.v's own new FLASH_XFER opcode ----
|
||||||
|
wire flash_xfer_active, flash_byte_req, flash_byte_done;
|
||||||
|
wire [7:0] flash_byte_wdata, flash_byte_rdata;
|
||||||
|
wire flash_cs_n, flash_mosi, flash_miso;
|
||||||
|
|
||||||
|
flash_spi_master u_flash (
|
||||||
|
.clk(clk), .rst(rst),
|
||||||
|
.xfer_active(flash_xfer_active), .byte_req(flash_byte_req),
|
||||||
|
.byte_wdata(flash_byte_wdata), .byte_rdata(flash_byte_rdata), .byte_done(flash_byte_done), .busy(),
|
||||||
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso)
|
||||||
|
);
|
||||||
|
flash_model_w25q32 u_flash_model (
|
||||||
|
.flash_cs_n(flash_cs_n), .flash_mosi(flash_mosi), .flash_miso(flash_miso),
|
||||||
|
.flash_sclk(u_flash.cclk_r)
|
||||||
|
);
|
||||||
|
|
||||||
spi_host_bridge_v3 #(
|
spi_host_bridge_v3 #(
|
||||||
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2)
|
.JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2)
|
||||||
) dut (
|
) dut (
|
||||||
.clk(clk), .rst(rst),
|
.clk(clk), .rst(rst),
|
||||||
|
.flash_xfer_active(flash_xfer_active), .flash_byte_req(flash_byte_req),
|
||||||
|
.flash_byte_wdata(flash_byte_wdata), .flash_byte_rdata(flash_byte_rdata), .flash_byte_done(flash_byte_done),
|
||||||
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n),
|
||||||
.init_calib_complete(init_calib_complete_model), .dir_error(dir_error_model),
|
.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_valid(job_in_valid), .job_in_ready(job_in_ready_model),
|
||||||
@@ -315,9 +336,122 @@ module tb_spi_host_bridge_v3;
|
|||||||
spi_byte(8'h00, rxb); check(rxb == 8'h79, "M: READ_MEM LSB byte == 0x79 (bit0=1, catches the ROUT-exit bug)");
|
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;
|
cs_n = 1; #40;
|
||||||
|
|
||||||
|
// ================= Test N: FLASH_XFER passthrough end to end
|
||||||
|
// -- real flash_spi_master.v + a real Winbond-command-set
|
||||||
|
// behavioral flash model behind it. Write Enable + Page
|
||||||
|
// Program + Read Data, entirely through spi_host_bridge_v3.v's
|
||||||
|
// own opcode 0x40, proving the WHOLE relay chain (host SPI ->
|
||||||
|
// this bridge -> flash_spi_master.v -> physical flash bus) is
|
||||||
|
// bit-exact, not just each half in isolation. ===============
|
||||||
|
cs_n = 0; #20;
|
||||||
|
spi_byte(8'h40, rxb); // opcode FLASH_XFER
|
||||||
|
spi_byte(8'h06, rxb); // relay: Write Enable
|
||||||
|
cs_n = 1; #40;
|
||||||
|
|
||||||
|
cs_n = 0; #20;
|
||||||
|
spi_byte(8'h40, rxb);
|
||||||
|
spi_byte(8'h02, rxb); // relay: Page Program
|
||||||
|
spi_byte(8'h30, rxb); // relay: addr=0x30
|
||||||
|
spi_byte(8'h5A, rxb); // relay: data=0x5A
|
||||||
|
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2 -- see header's own real latency note
|
||||||
|
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2
|
||||||
|
cs_n = 1; #40;
|
||||||
|
|
||||||
|
cs_n = 0; #20;
|
||||||
|
spi_byte(8'h40, rxb);
|
||||||
|
spi_byte(8'h03, rxb); // relay: Read Data
|
||||||
|
spi_byte(8'h30, rxb); // relay: addr=0x30
|
||||||
|
spi_byte(8'h00, rxb); // relay: dummy clock for the data byte
|
||||||
|
spi_byte(8'h00, rxb); // trailing margin byte 1 of 2
|
||||||
|
spi_byte(8'h00, rxb); // trailing margin byte 2 of 2 -- response is safely stable here
|
||||||
|
check(rxb == 8'h5A, "N: FLASH_XFER end-to-end round trip through the real flash model, bit-exact");
|
||||||
|
cs_n = 1; #40;
|
||||||
|
|
||||||
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
|
$display("=== tb_spi_host_bridge_v3: %0d/%0d PASS ===", tests-errors, tests);
|
||||||
if (errors != 0) $display("*** %0d FAILURES ***", errors);
|
if (errors != 0) $display("*** %0d FAILURES ***", errors);
|
||||||
$finish;
|
$finish;
|
||||||
end
|
end
|
||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
|
|
||||||
|
// STARTUPE2 simulation-only stub -- see tb_flash_spi_master.v's own
|
||||||
|
// header for why real UNISIM verification is a disclosed follow-up,
|
||||||
|
// not done here (this test verifies the protocol/relay logic, which
|
||||||
|
// is independent of STARTUPE2's own real behavior).
|
||||||
|
module STARTUPE2 #(
|
||||||
|
parameter PROG_USR = "FALSE",
|
||||||
|
parameter real SIM_CCLK_FREQ = 0.0
|
||||||
|
)(
|
||||||
|
output wire CFGCLK, output wire CFGMCLK, output wire EOS, output wire PREQ,
|
||||||
|
input wire CLK, input wire GSR, input wire GTS, input wire KEYCLEARB, input wire PACK,
|
||||||
|
input wire USRCCLKO, input wire USRCCLKTS,
|
||||||
|
input wire USRDONEO, input wire USRDONETS
|
||||||
|
);
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
// Same behavioral W25Q32JV-like flash model as tb_flash_spi_master.v
|
||||||
|
// (EXP-0077) -- kept independent (not shared via `include) since each
|
||||||
|
// testbench owns its own self-contained model, matching this
|
||||||
|
// project's existing convention (e.g. sdram_model.v is the one real
|
||||||
|
// exception, shared because it stands in for real vendor-supplied
|
||||||
|
// silicon behavior, not a test-specific convenience model).
|
||||||
|
module flash_model_w25q32 (
|
||||||
|
input wire flash_cs_n,
|
||||||
|
input wire flash_mosi,
|
||||||
|
output reg flash_miso,
|
||||||
|
input wire flash_sclk
|
||||||
|
);
|
||||||
|
reg [7:0] flash_mem [0:255];
|
||||||
|
reg [7:0] flash_cmd;
|
||||||
|
reg [7:0] flash_addr;
|
||||||
|
reg flash_wel;
|
||||||
|
reg [7:0] model_shift;
|
||||||
|
reg [2:0] model_bitcnt;
|
||||||
|
reg [2:0] model_bytecnt;
|
||||||
|
reg [7:0] model_rdata_byte;
|
||||||
|
|
||||||
|
initial begin flash_wel = 0; model_bytecnt = 0; model_bitcnt = 0; flash_miso = 0; end
|
||||||
|
|
||||||
|
always @(posedge flash_sclk) begin
|
||||||
|
if (!flash_cs_n) begin
|
||||||
|
model_shift <= {model_shift[6:0], flash_mosi};
|
||||||
|
if (model_bitcnt == 3'd7) begin
|
||||||
|
model_bitcnt <= 3'd0;
|
||||||
|
case (model_bytecnt)
|
||||||
|
3'd0: begin
|
||||||
|
flash_cmd <= {model_shift[6:0], flash_mosi};
|
||||||
|
if ({model_shift[6:0], flash_mosi} == 8'h06) flash_wel <= 1'b1;
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
3'd1: begin
|
||||||
|
if (flash_cmd == 8'h02 || flash_cmd == 8'h03) begin
|
||||||
|
flash_addr <= {model_shift[6:0], flash_mosi};
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
3'd2: begin
|
||||||
|
if (flash_cmd == 8'h02) flash_mem[flash_addr] <= {model_shift[6:0], flash_mosi};
|
||||||
|
model_bytecnt <= model_bytecnt + 1'b1;
|
||||||
|
end
|
||||||
|
default: ;
|
||||||
|
endcase
|
||||||
|
end else begin
|
||||||
|
model_bitcnt <= model_bitcnt + 1'b1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
always @(*) begin
|
||||||
|
if (flash_cmd == 8'h05) model_rdata_byte = {6'b0, flash_wel, 1'b0};
|
||||||
|
else if (flash_cmd == 8'h03) model_rdata_byte = flash_mem[flash_addr];
|
||||||
|
else model_rdata_byte = 8'h00;
|
||||||
|
end
|
||||||
|
always @(negedge flash_sclk) begin
|
||||||
|
if (!flash_cs_n && model_bytecnt >= (flash_cmd==8'h05 ? 3'd1 : 3'd2))
|
||||||
|
flash_miso <= model_rdata_byte[3'd7 - model_bitcnt];
|
||||||
|
end
|
||||||
|
always @(posedge flash_cs_n) begin
|
||||||
|
model_bytecnt <= 3'd0;
|
||||||
|
model_bitcnt <= 3'd0;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
|||||||
Reference in New Issue
Block a user