diff --git a/hardware/v2/logs/experiments.log b/hardware/v2/logs/experiments.log index 9b8cbef..a3bdb78 100644 --- a/hardware/v2/logs/experiments.log +++ b/hardware/v2/logs/experiments.log @@ -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 constraints (SPI/reset pin LOCs) once the board layout itself is 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. diff --git a/hardware/v3/constraints/n2_system_ddr3_top.xdc b/hardware/v3/constraints/n2_system_ddr3_top.xdc index 07801e7..17504f9 100644 --- a/hardware/v3/constraints/n2_system_ddr3_top.xdc +++ b/hardware/v3/constraints/n2_system_ddr3_top.xdc @@ -10,15 +10,36 @@ # 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}] +# ---- required for flash_spi_master.v to work at all: D00_MOSI/ +# D01_DIN/FCS_B only become ordinary fabric I/O post-configuration +# when PERSIST is FALSE (the Vivado default -- set explicitly here so +# this dependency is self-documenting in the constraints, not just a +# silent default someone could flip later without realizing why). +set_property BITSTREAM.CONFIG.PERSIST FALSE [current_design] + +# ---- config-flash passthrough (-> flash_spi_master.v, EXP-0077): +# 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): # job submission + register file. Bank 15, column A/B (package edge, diff --git a/hardware/v3/rtl/flash_spi_master.v b/hardware/v3/rtl/flash_spi_master.v new file mode 100644 index 0000000..f8db928 --- /dev/null +++ b/hardware/v3/rtl/flash_spi_master.v @@ -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 diff --git a/hardware/v3/rtl/n2_system_ddr3_top.v b/hardware/v3/rtl/n2_system_ddr3_top.v index fbc57c9..6368d1b 100644 --- a/hardware/v3/rtl/n2_system_ddr3_top.v +++ b/hardware/v3/rtl/n2_system_ddr3_top.v @@ -70,6 +70,17 @@ module n2_system_ddr3_top #( output wire miso, 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 // observation; NOT part of the activation-interface pin-count // problem described below) ---- @@ -201,6 +212,9 @@ module n2_system_ddr3_top #( wire [15:0] mem_wdata, mem_rdata; 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 #( .JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(N_SLOTS) ) u_spi ( @@ -214,9 +228,19 @@ module n2_system_ddr3_top #( .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_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) ); + 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 #( .BURST_LEN(BURST_LEN), .ADDR_WIDTH(MEM_ADDR_WIDTH) ) u_host_bridge ( diff --git a/hardware/v3/rtl/spi_host_bridge_v3.v b/hardware/v3/rtl/spi_host_bridge_v3.v index cc0613e..788e947 100644 --- a/hardware/v3/rtl/spi_host_bridge_v3.v +++ b/hardware/v3/rtl/spi_host_bridge_v3.v @@ -137,6 +137,48 @@ // below), so host software doesn't need // 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, // MISO drives 0x00) -- matches spi_host_bridge.v's own "unknown // 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 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 ); @@ -280,6 +329,7 @@ module spi_host_bridge_v3 #( localparam OP_STATUS = 8'h20; localparam OP_REG_WRITE = 8'h30; localparam OP_REG_READ = 8'h31; + localparam OP_FLASH_XFER= 8'h40; localparam ST_OPCODE = 4'd0; 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_WDATA= 4'd11; // REG_WRITE: collecting 4 value bytes 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 [7:0] opcode; @@ -304,6 +356,7 @@ module spi_host_bridge_v3 #( 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 + 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, // 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]; else if (opcode == OP_REG_READ && state == ST_REG_ROUT) tx_mux = reg_rdata[8*(3-byte_idx) +: 8]; + else if (opcode == OP_FLASH_XFER) + tx_mux = flash_rdata_r; end assign tx_byte = tx_mux; @@ -372,9 +427,12 @@ module spi_host_bridge_v3 #( 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; + flash_xfer_active <= 1'b0; flash_byte_req <= 1'b0; + flash_byte_wdata <= 8'h00; flash_rdata_r <= 8'h00; end else begin mem_req <= 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 // 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_REG_WRITE: 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; default: state <= ST_IGNORE; // NOP, STATUS: no MOSI payload endcase @@ -499,7 +562,13 @@ module spi_host_bridge_v3 #( 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 + 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 end @@ -575,12 +644,18 @@ module spi_host_bridge_v3 #( state <= ST_IGNORE; 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); if (cs_rose) begin if (opcode == OP_RESET) soft_rst_pulse <= 1'b1; if (state != ST_JOB_WAIT && state != ST_MEM_WISS && state != ST_MEM_RISS) state <= ST_OPCODE; + flash_xfer_active <= 1'b0; end end end diff --git a/hardware/v3/sim/tb_flash_spi_master.v b/hardware/v3/sim/tb_flash_spi_master.v new file mode 100644 index 0000000..69814bb --- /dev/null +++ b/hardware/v3/sim/tb_flash_spi_master.v @@ -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 diff --git a/hardware/v3/sim/tb_spi_host_bridge_v3.v b/hardware/v3/sim/tb_spi_host_bridge_v3.v index 2bdb941..24ba66c 100644 --- a/hardware/v3/sim/tb_spi_host_bridge_v3.v +++ b/hardware/v3/sim/tb_spi_host_bridge_v3.v @@ -44,10 +44,31 @@ module tb_spi_host_bridge_v3; reg init_calib_complete_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 #( .JOB_ADDR_WIDTH(JOB_ADDR_WIDTH), .MEM_ADDR_WIDTH(MEM_ADDR_WIDTH), .N_SLOTS(2) ) dut ( .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), .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), @@ -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)"); 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); if (errors != 0) $display("*** %0d FAILURES ***", errors); $finish; end 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