diff --git a/hdl/ip/vhd/spi_nor_controller/BUCK b/hdl/ip/vhd/spi_nor_controller/BUCK
index 388feaca..c8b457bd 100644
--- a/hdl/ip/vhd/spi_nor_controller/BUCK
+++ b/hdl/ip/vhd/spi_nor_controller/BUCK
@@ -37,6 +37,7 @@ vunit_sim(
":spi_nor_top",
"//hdl/ip/vhd/axi_blocks:axil_interconnect",
"//hdl/ip/vhd/axi_blocks:axil8_resizer",
+ "//hdl/ip/vhd/vunit_components:spi_nor_target_vc",
],
visibility = ["PUBLIC"],
)
\ No newline at end of file
diff --git a/hdl/ip/vhd/spi_nor_controller/docs/README.md b/hdl/ip/vhd/spi_nor_controller/docs/README.md
index d851550c..29e7cc82 100644
--- a/hdl/ip/vhd/spi_nor_controller/docs/README.md
+++ b/hdl/ip/vhd/spi_nor_controller/docs/README.md
@@ -1,25 +1,115 @@
-This controller aims to provide an interface from software and the espi blocks
-
-From the software side, we support read and writes.
-
-The sw interface is intended to be rather simple:
-There are registers for a 256byte tx/rx fifo, and various fifo flags in the status register.
-There are fifo reset signals in the control register.
-
-The software interface to issue commands is as follows:
-If doing a data write:
-- (Optional: clean out any data in FIFOs using control register to reset them)
-- Write up to 256 data bytes into TX FIFO, each write is 4 bytes due to 32bit access.
-- Set data size register to the number of data bytes to send. This does not have to be 4 byte multiple
-- Write number of dummy *clocks* into the dummy register as required for the instruction according to flash datasheeet
-- Write instruction into the instruction register. Write-side effect will begin the transaction.
-- Wait until status shows not busy
-
-If doing a data read
-- (Optional: clean out any data in FIFOs using control register to reset them)
-- Set data size register to the number of data bytes to send. This does not have to be 4 byte multiple
-- Write number of dummy *clocks* into the dummy register as required for the instruction according to flash datasheeet
-- Write instruction into the instruction register. Write-side effect will begin the transaction.
-- You can either wait until status shows not busy, or poll on the rx fifo used wds and start consuming data
-- as it becomes available
-- Wait until status shows not busy
\ No newline at end of file
+# SPI NOR controller
+
+Drives a Winbond W25Q01JV QSPI NOR flash on behalf of two masters: the SP over
+AXI-Lite, and the eSPI block fetching the host image. `SPICR.sp5_owns_flash`
+picks which one owns the part.
+
+
+
+## Software interface
+
+Registers are generated from `spi_nor_regs.rdl`. Writing `Instr` starts a
+transaction; reading `rx_fifo_rdata` pops the RX FIFO.
+
+Write:
+
+- (optional) reset the FIFOs via the control register
+- write up to 256 data bytes into the TX FIFO, 4 bytes per access
+- set the data size register to the byte count (need not be a multiple of 4)
+- set the dummy register to the clock count the instruction needs
+- write the instruction; the write side effect starts the transaction
+- poll `SPISR.busy`
+
+Read: as above without the FIFO fill. Either wait on `busy`, or poll
+`rx_used_wds` and drain as data arrives.
+
+The opcode alone determines the phase sequence and bus width, via
+`get_txn_info` in `spi_nor_pkg`. Software supplies only the dummy count.
+
+## Structure
+
+| unit | role |
+| --- | --- |
+| `spi_nor_regs` | AXI-Lite target, command and FIFO registers |
+| `espi_flash_txn_mgr` | turns eSPI read requests into commands, remaps host and APOB addresses |
+| `spi_txn_mgr` | phase FSM, chip select, byte and io-mode prefetch |
+| `spi_link` | serializer, deserializer, rx capture and sample-point mux |
+| `spi_clk_gen` | sclk divider |
+| `mixed_width_adaptor` | 32-bit FIFO side to 8-bit link side |
+
+## Clocking and edges
+
+`sclk = clk / (2 * (sclk_divisor + 1))`. Both projects run `sclk_divisor => 0`
+off `clk_125m`, so **sclk is 62.5MHz**, a 16ns period.
+
+
+
+Three things follow from a half period being one clk cycle, and all three are
+load-bearing:
+
+**Launch on the falling edge itself.** The shifter, `io_o`, `io_oe` and the io
+mode all move on the clk edge that drives sclk low, so mosi and sclk leave the
+FPGA together. Reacting to an edge detector instead spends a whole clk of the
+half-period budget and caps sclk at clk/4.
+
+**The byte and its io mode are prefetched together.** The serializer reloads on
+the same edge the FSM advances a phase, so both must already be sitting there.
+Taking the mode from the registered state instead means the first data byte of a
+dual or quad write is loaded while the state still reads `addr`: it goes out
+with single-bit lane assignment and gets shifted by one instead of four, which
+leaves the shifter's sentinel where the byte-complete compare can never match
+and the transaction never ends.
+
+**Read data is sampled at a placed point, not on an edge.** The round trip out
+to the part and back does not shrink with sclk, so `rx_sample_taps` selects the
+sample point in half-clk steps after the sclk rising edge (default 2 = 8ns),
+sourced from a rising- and a falling-edge capture flop. The point must satisfy
+
+```
+round_trip_valid - half_period <= S <= half_period + round_trip_hold
+```
+
+Note the upper limit comes from the part's tCLQX, not tCLQV: sampling too late
+catches the next bit. The project XDC carries the arithmetic for the delays it
+bounds.
+
+`sclk_running` is deliberately narrower than `in_tx_phases`: `cs_deassert` still
+drives the bus, so mosi is not torn down on the edge the part samples it, but it
+must not clock. The part counts edges to find the end of an instruction, and a
+single trailing edge makes an erase or a write enable be discarded silently.
+
+## Physical
+
+Every pin-facing flop is a dedicated duplicate with no internal fanout
+(`sclk_pin`, `cs_n_pin`, `io_o`, `io_oe`, the rx capture) so it can be packed
+into its IOB. That is what makes the round trip bounded and repeatable: left in
+the fabric the placer put these 12-13ns of routing from their pins and varied by
+several ns between builds, which both blew the clock-to-data skew budget and
+pushed the round trip outside every available sample point.
+
+62.5MHz is the ceiling for this structure. The sample point is placed, not
+trained, so the round trip has to fit within half a period of it; above this
+rate that window closes and it would take per-lane `IDELAY` read training to go
+further.
+
+## Simulation
+
+`spi_nor_target_vc` models the part with datasheet AC timing (tCLQV, tCLQX,
+tDVCH, tCHDX, tSLCH, tSHSL) and discards malformed commands the way silicon
+does, so a trailing clock or a bad sample point fails a test rather than
+producing a plausible waveform. The harness also models the FPGA's own
+flop-to-pin and pin-to-flop delays; without them simulation validates a regime
+that does not exist on hardware.
+
+| testbench | configuration |
+| --- | --- |
+| `spi_nor_tb` | clk/6, the historical setting |
+| `spi_nor_fast_tb` | clk/2, slow IO corner |
+| `spi_nor_fast_quick_io_tb` | clk/2, fast IO corner |
+
+The fast benches also sweep the part's output delay across its datasheet range
+to show the sample point has margin at both ends.
+
+```
+buck2 run //hdl/ip/vhd/spi_nor_controller:spi_nor_top_sim
+```
diff --git a/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_block.drawio.svg b/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_block.drawio.svg
index 16ddae42..686d63fa 100644
--- a/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_block.drawio.svg
+++ b/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_block.drawio.svg
@@ -1,4 +1,97 @@
-
\ No newline at end of file
+
diff --git a/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_edges.drawio.svg b/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_edges.drawio.svg
new file mode 100644
index 00000000..6d33befc
--- /dev/null
+++ b/hdl/ip/vhd/spi_nor_controller/docs/spi_nor_edges.drawio.svg
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd
index 3c262712..e11fa293 100644
--- a/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/link/spi_clk_gen.vhd
@@ -13,7 +13,21 @@ entity spi_clk_gen is
reset : in std_logic;
divisor : in unsigned(15 downto 0);
enable : in boolean;
- sclk : out std_logic
+ -- For internal consumers: edge detection, phase counting, debug
+ sclk : out std_logic;
+ -- A second copy of the same flop, for the pin and nothing else. Both
+ -- change on the same clk edge with the same value, so this costs no
+ -- latency, but having no internal fanout is what lets it be packed into
+ -- the IOB. Without that the launch flop lands wherever the placer likes,
+ -- which measured 12ns of routing to the pin and put the read round trip
+ -- outside every available sample point.
+ sclk_pin : out std_logic;
+ -- True during the cycle whose clk edge will drive sclk low. Consumers
+ -- use this to move data on the *same* edge sclk moves, rather than a
+ -- cycle later after an edge detector has seen it. That distinction is
+ -- what sets the maximum sclk rate: a cycle-late launch has to fit inside
+ -- a half period, so it caps sclk at clk/4 rather than clk/2.
+ sclk_fall_now : out boolean
);
end entity;
@@ -23,9 +37,25 @@ architecture rtl of spi_clk_gen is
signal strobe : boolean := false;
signal internal_enable : boolean := false;
signal enable_last : boolean := false;
+ signal sclk_int : std_logic := '0';
+ signal running : boolean;
begin
+ sclk <= sclk_int;
+
+ -- internal_enable is registered, so on its own it lingers for a cycle after
+ -- enable drops -- long enough to emit one more toggle. For a command that
+ -- ends on an exact bit count, an erase or a program, that stray edge is a
+ -- whole extra bit and the part throws the instruction away. Qualifying with
+ -- the live enable stops the clock on the same edge the caller asked it to.
+ running <= internal_enable and enable;
+
+ -- strobe is registered, so it is already asserted during the cycle that
+ -- precedes the toggling edge. That makes this safe to use as a synchronous
+ -- enable by anything that needs to change state exactly when sclk does.
+ sclk_fall_now <= running and strobe and sclk_int = '1';
+
-- Pretty simple spi generator.
-- start with a rising edge
-- generate requested clock
@@ -53,7 +83,8 @@ begin
variable nxt_sclk : std_logic;
begin
if reset then
- sclk <= '0';
+ sclk_int <= '0';
+ sclk_pin <= '0';
internal_enable <= false;
enable_last <= false;
elsif rising_edge(clk) then
@@ -64,14 +95,16 @@ begin
internal_enable <= false;
end if;
- if internal_enable then
- nxt_sclk := sclk;
+ if running then
+ nxt_sclk := sclk_int;
if strobe then
- nxt_sclk := not sclk;
+ nxt_sclk := not sclk_int;
end if;
- sclk <= nxt_sclk; -- assign value to output
+ sclk_int <= nxt_sclk; -- assign value to output
+ sclk_pin <= nxt_sclk; -- IOB-resident duplicate, same edge
else
- sclk <= '0';
+ sclk_int <= '0';
+ sclk_pin <= '0';
end if;
end if;
end process;
diff --git a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd
index aa7d0479..c03edd0a 100644
--- a/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/link/spi_link.vhd
@@ -9,6 +9,15 @@ use ieee.numeric_std_unsigned.all;
use work.spi_nor_pkg.all;
entity spi_link is
+ generic (
+ -- Where to sample read data, in half-clk steps after the sclk rising
+ -- edge. The round trip (clk to sclk pin, flash tCLQV, data back to the
+ -- pin, pin to flop) does not scale with sclk, so at higher rates the
+ -- sample point has to be placed deliberately rather than left at
+ -- whatever the edge detector happens to produce. 2 reproduces the
+ -- original one-clk-after-the-edge behaviour.
+ rx_sample_taps : natural range 0 to 4 := 2
+ );
port (
clk : in std_logic;
reset : in std_logic;
@@ -17,16 +26,36 @@ entity spi_link is
divisor : in unsigned(15 downto 0);
in_tx_phases : in boolean;
in_rx_phases : in boolean;
+ -- Runs the sclk generator. Not the same as in_tx_phases: the bus is
+ -- still driven while cs_n is being torn down, but the part must not see
+ -- any clock edges there.
+ sclk_running : in boolean;
+ -- Lanes to stop driving early, ahead of a controller-to-flash
+ -- turnaround, so the two ends are never enabled at once
+ release_lanes : in std_logic_vector(3 downto 0);
rx_byte : out std_logic_vector(7 downto 0);
rx_byte_done : out boolean;
+ -- The next byte to shift out. Must be held ready ahead of the reload
+ -- edge rather than produced in response to tx_byte_req: it lands
+ -- directly on the io_o launch flops, so anything combinational behind it
+ -- is in series with the shifter's own empty detect.
tx_byte : in std_logic_vector(7 downto 0);
- tx_byte_done : out boolean;
- sclk_redge : out boolean;
- sclk_fedge : out boolean;
+ -- The io mode tx_byte belongs to, adopted at the reload edge
+ tx_byte_mode : in io_mode;
+ -- Asserted during the cycle whose clk edge consumes a tx byte. The
+ -- transaction manager advances its phase on this, so the byte and the
+ -- sclk edge that launches it move together.
+ tx_byte_req : out boolean;
+ sclk_redge : out boolean;
+ sclk_fedge : out boolean;
-- qspi interface
cs_n : in std_logic;
- sclk : out std_logic;
+ -- sclk as the internal logic sees it, for the transaction manager's
+ -- phase counting
+ sclk : out std_logic;
+ -- sclk for the pin only, so it can live in the IOB. See spi_clk_gen.
+ sclk_pin : out std_logic;
io : in std_logic_vector(3 downto 0);
io_o : out std_logic_vector(3 downto 0);
io_oe : out std_logic_vector(3 downto 0)
@@ -37,26 +66,77 @@ architecture rtl of spi_link is
attribute mark_debug : string;
+ constant SENTINEL_AT_TOP : std_logic_vector(8 downto 0) := "100000000";
+
+ -- rx_sample_taps counts half-clks, but a capture flop only lets us pick a
+ -- source (rising or falling phase) and how many whole clks later to use it.
+ -- Odd taps come from the falling-edge capture, and the whole-clk part of
+ -- the delay is the number of pipeline stages the sample pulse walks.
+ constant SAMPLE_DELAY : natural := (rx_sample_taps + 1) / 2;
+ constant SAMPLE_ON_NEG : boolean := (rx_sample_taps mod 2) = 1;
+
signal tx_reg : std_logic_vector(8 downto 0);
signal rx_reg : std_logic_vector(8 downto 0);
attribute mark_debug of tx_reg : signal is "TRUE";
attribute mark_debug of rx_reg : signal is "TRUE";
- signal tx_byte_ack : boolean;
signal sclk_last : std_logic;
+ signal sclk_int : std_logic;
+ signal sclk_fall_now : boolean;
signal shift_amt : integer range 1 to 4;
signal csn_last : std_logic;
- signal is_last_bit : boolean;
signal cur_io_mode : io_mode;
signal dbg_sclk_cnts : unsigned(31 downto 0);
attribute mark_debug of dbg_sclk_cnts : signal is "TRUE";
-begin
+ -- Dedicated input capture. These are the only loads on the io port pins,
+ -- which is what makes the pin-to-flop delay bound in the XDC meaningful.
+ signal io_cap_p : std_logic_vector(3 downto 0);
+ signal io_cap_n : std_logic_vector(3 downto 0);
+ signal io_n_q : std_logic_vector(3 downto 0);
+ signal io_tap : std_logic_vector(3 downto 0);
+
+ -- Sample pulse pipeline. Stage 0 is combinational and fires on the clk edge
+ -- immediately after the sclk rising edge; each further stage is one clk
+ -- later.
+ signal sample_pipe : std_logic_vector(2 downto 0);
+ signal sample_now : boolean;
+
+ -- Resolve the shifter into the four io lanes for the current mode. Lane 3
+ -- carries the most significant bit in quad, lane 1 in dual, and lanes 1/3
+ -- have to sit high otherwise so the part doesn't see a HOLD request.
+ function io_out_bits (
+ reg : std_logic_vector(8 downto 0);
+ mode : io_mode
+ ) return std_logic_vector is
+ begin
+ case mode is
+ when QUAD =>
+ return reg(8 downto 5);
+ when DUAL =>
+ return '1' & reg(7) & reg(8) & reg(7);
+ when SINGLE =>
+ return '1' & reg(7) & '1' & reg(8);
+ end case;
+ end function;
- shift_amt <= 1 when cur_io_mode = SINGLE else
- 2 when cur_io_mode = DUAL else
- 4 when cur_io_mode = QUAD else
- 1;
+ function shift_amt_of (
+ mode : io_mode
+ ) return integer is
+ begin
+ case mode is
+ when QUAD =>
+ return 4;
+ when DUAL =>
+ return 2;
+ when SINGLE =>
+ return 1;
+ end case;
+ end function;
+
+begin
+ sclk <= sclk_int;
+ shift_amt <= shift_amt_of(cur_io_mode);
rx_byte_done <= rx_reg(rx_reg'high) = '1';
rx_byte <= rx_reg(7 downto 0);
@@ -64,10 +144,9 @@ begin
begin
if reset then
sclk_last <= '0';
- cur_io_mode <= single;
dbg_sclk_cnts <= (others => '0');
elsif rising_edge(clk) then
- sclk_last <= sclk;
+ sclk_last <= sclk_int;
-- This is a simple sclk counter that is used for debugging
-- purposes. I can be tricky to figure out where in the
-- transaction you are, when using the ila, so this counter
@@ -78,28 +157,22 @@ begin
elsif cs_n = '1' then
dbg_sclk_cnts <= (others => '0');
end if;
-
- if cs_n = '0' and sclk_fedge then
- -- only update the io mode on the falling edge
- -- so that we're done with the current bit
- cur_io_mode <= req_io_mode;
- elsif cs_n = '1' then
- cur_io_mode <= req_io_mode;
- end if;
end if;
end process;
- sclk_redge <= sclk = '1' and sclk_last = '0';
- sclk_fedge <= sclk = '0' and sclk_last = '1';
+ sclk_redge <= sclk_int = '1' and sclk_last = '0';
+ sclk_fedge <= sclk_int = '0' and sclk_last = '1';
-- spi clock gen block
clk_gen: entity work.spi_clk_gen
port map (
- clk => clk,
- reset => reset,
- divisor => divisor,
- enable => in_tx_phases or in_rx_phases,
- sclk => sclk
+ clk => clk,
+ reset => reset,
+ divisor => divisor,
+ enable => sclk_running,
+ sclk => sclk_int,
+ sclk_pin => sclk_pin,
+ sclk_fall_now => sclk_fall_now
);
-- This is the main "output" serializer. The internal
@@ -108,115 +181,177 @@ begin
-- We know we're done with a byte when the MSB is '1'
-- and all the other bits are '0' b/c we've shifted the
-- sentinel up 8x
- -- There are two key indications that we need and we need them
- -- at different times:
- -- 1) We need to ack bytes from fifos/other data inputs. This happens
- -- *before* the byte-shifting is done.
- -- 2) We need to know when the last bit of the byte has been shifted
+ --
+ -- Both the shifter and the io_o output register move on the same clk edge
+ -- that drives the sclk falling edge, so mosi and sclk leave the FPGA
+ -- together and the only skew left for the flash's setup time to absorb is
+ -- the IOB and routing difference between the pins. Doing this a cycle later
+ -- off an edge detector, as this used to, spends a whole clk period of the
+ -- half-period budget and caps sclk at clk/4.
serializer: process(clk, reset)
variable cs_n_assert_edge : boolean := false;
+ variable nxt_tx_reg : std_logic_vector(8 downto 0);
+ variable nxt_mode : io_mode;
+ variable oe_mode : io_mode;
+ variable oe : std_logic_vector(3 downto 0);
begin
if reset then
tx_reg <= (others => '0');
- tx_byte_ack <= false;
csn_last <= '1';
- is_last_bit <= false;
+ cur_io_mode <= single;
+ io_o <= (others => '1');
+ io_oe <= (others => '0');
elsif rising_edge(clk) then
csn_last <= cs_n;
cs_n_assert_edge := cs_n = '0' and csn_last = '1';
- -- clear single-cycle flags
- tx_byte_ack <= false;
- is_last_bit <= false;
+
+ -- The io mode only changes on a byte boundary, which is also when
+ -- the shifter reloads, so the mode that applies to the bits going
+ -- out at this edge is the one selected here.
+ --
+ -- req_io_mode comes off the registered transaction state, which has
+ -- not advanced yet on the edge that reloads the shifter. That is
+ -- fine while we are mid-phase, and it is what keeps reads right
+ -- (their phase change lands on a rising edge, away from any reload),
+ -- but the byte being loaded here may belong to the *next* phase. So
+ -- a reload takes the mode that travelled with the byte instead.
+ nxt_mode := cur_io_mode;
+ if (cs_n = '0' and sclk_fall_now) or cs_n = '1' then
+ nxt_mode := req_io_mode;
+ end if;
+
+ nxt_tx_reg := tx_reg;
if cs_n_assert_edge then
-- as the controller here, we need to pre-load data before the first
-- clock
- tx_reg(8 downto 1) <= tx_byte;
- tx_reg(tx_reg'low) <= '1';
- tx_byte_ack <= true;
- is_last_bit <= false;
-
- -- Main serializer logic, shift out on sclk_fedge
- -- when we're chip-selected and not doing turnaround
- elsif in_tx_phases and sclk_redge then
- if shift_left(tx_reg, shift_amt) = "100000000" then
- is_last_bit <= true;
- end if;
- elsif in_tx_phases and sclk_fedge then
- -- if next-shift would be our sentinal value, load new data
- if shift_left(tx_reg, shift_amt) = "100000000" then
+ nxt_tx_reg := tx_byte & '1';
+ nxt_mode := tx_byte_mode;
+ elsif in_tx_phases and sclk_fall_now then
+ if shift_left(tx_reg, shift_amt) = SENTINEL_AT_TOP then
-- tx_register is "empty" load a new one
- -- and the sentinal value
- tx_reg(8 downto 1) <= tx_byte;
- tx_reg(tx_reg'low) <= '1';
- tx_byte_ack <= true;
- -- mid-byte, shift
+ -- and the sentinel value
+ nxt_tx_reg := tx_byte & '1';
+ nxt_mode := tx_byte_mode;
else
- tx_reg <= shift_left(tx_reg, shift_amt);
+ nxt_tx_reg := shift_left(tx_reg, shift_amt);
end if;
elsif not in_tx_phases then
- tx_reg <= (others => '0');
- is_last_bit <= false;
+ nxt_tx_reg := (others => '0');
end if;
- end if;
- end process;
- tx_byte_done <= is_last_bit;
- -- Based on state and qspi mode, deal with the tri-state controls
- -- of the spi pins
- oe_control: process(clk, reset)
- begin
- if reset then
- io_oe <= (others => '0');
- elsif rising_edge(clk) then
+ cur_io_mode <= nxt_mode;
+
+ -- Direction is decided here, alongside the data, so the two can
+ -- never disagree about which mode is in force. Computing it in its
+ -- own process off req_io_mode put the quad lanes' output enable one
+ -- clk behind the first quad data nibble, which at clk/2 is the edge
+ -- the part samples on.
+ --
+ -- Turning *on* follows nxt_mode so it lands with the data; turning
+ -- *off* follows req_io_mode, which moves as soon as the phase does,
+ -- because releasing early is what keeps us clear of the part on a
+ -- read turnaround.
if in_tx_phases then
- case cur_io_mode is
+ oe_mode := nxt_mode;
+ else
+ oe_mode := req_io_mode;
+ end if;
+
+ if in_tx_phases then
+ case oe_mode is
when single =>
-- data going out 0 port, but need 3 port to be high so
-- chip doesn't see a HOLD operation
- io_oe <= (0 => '1', 3 => '1', others => '0');
+ oe := (0 => '1', 3 => '1', others => '0');
when dual =>
- -- data going out 0 port, but need 3 port to be high so
- -- chip doesn't see a HOLD operation
- io_oe <= (1 downto 0 => '1', 3 => '1', others => '0');
+ -- data going out 0 and 1 ports, 3 port high so the chip
+ -- doesn't see a HOLD operation
+ oe := (1 downto 0 => '1', 3 => '1', others => '0');
when quad =>
- io_oe <= (others => '1');
+ oe := (others => '1');
end case;
else -- rx only in all rx phases
- case cur_io_mode is
+ case oe_mode is
when single =>
-- data coming in 1 port, but need 3 port to be high so
-- chip doesn't see a HOLD operation
- io_oe <= (3 => '1', others => '0');
+ oe := (3 => '1', others => '0');
when dual =>
-- data coming in 0, 1 ports, but need 3 port to be high so
-- chip doesn't see a HOLD operation
- io_oe <= ( 3 => '1', others => '0');
+ oe := (3 => '1', others => '0');
when quad =>
-- data coming in all ports, no outputs
- io_oe <= (others => '0');
+ oe := (others => '0');
end case;
end if;
+
+ tx_reg <= nxt_tx_reg;
+ io_o <= io_out_bits(nxt_tx_reg, nxt_mode);
+ io_oe <= oe and not release_lanes;
end if;
end process;
- -- Deal with output logic for the different modes
- io_o(0) <= tx_reg(tx_reg'high-3) when cur_io_mode = QUAD else
- tx_reg(tx_reg'high-1) when cur_io_mode = DUAL else
- tx_reg(tx_reg'high);
+ -- Flag the reload a cycle ahead of the edge that consumes the byte so the
+ -- transaction manager can present the right one. This is the same condition
+ -- the serializer uses above, just not yet registered.
+ tx_byte_req <= in_tx_phases and sclk_fall_now and
+ shift_left(tx_reg, shift_amt) = SENTINEL_AT_TOP;
- io_o(1) <= tx_reg(tx_reg'high) when cur_io_mode = DUAL else
- tx_reg(tx_reg'high-2) when cur_io_mode = QUAD else
- '1'; -- not used due to oe-gate
- io_o(2) <= tx_reg(tx_reg'high - 1); -- only used in quad mode
- io_o(3) <= tx_reg(tx_reg'high) when cur_io_mode = QUAD else
- '1'; -- only used in quad mode, but need to not be 0 in
- -- single or dual modes to not act as a HOLD
+ -- Based on state and qspi mode, deal with the tri-state controls
+ -- of the spi pins
+ -- Note this tracks req_io_mode, not the byte-aligned cur_io_mode. The
+ -- latched mode is deliberately held until a falling edge so a byte is never
+ -- split across two widths, but that leaves it reading `single` for the first
+ -- cycles of a dual/quad read -- long enough to re-enable io3 for HOLD
+ -- avoidance just as the part starts driving it. Direction has no reason to
+ -- wait for a byte boundary, so it follows the transaction directly.
+ -- Input capture. Two flops, one per clk phase, so the sample point can be
+ -- placed on a half-clk grid without needing a faster clock.
+ capture_p: process(clk)
+ begin
+ if rising_edge(clk) then
+ io_cap_p <= io;
+ io_n_q <= io_cap_n;
+ end if;
+ end process;
+
+ capture_n: process(clk)
+ begin
+ if falling_edge(clk) then
+ io_cap_n <= io;
+ end if;
+ end process;
+
+ io_tap <= io_n_q when SAMPLE_ON_NEG else io_cap_p;
+
+ -- Walk the sample pulse out to the requested whole-clk delay.
+ --
+ -- in_rx_phases is sampled here, when the pulse is injected, rather than
+ -- where it is consumed. The phase advances on the clk edge right after the
+ -- sclk edge, so a pulse still in the pipeline would otherwise be validated
+ -- against a phase that has already moved on: the last dummy clock's pulse
+ -- lands once rdata is active and steals a sample, shifting every subsequent
+ -- byte by one group.
+ sample_pipe(0) <= '1' when sclk_redge and in_rx_phases else '0';
+
+ sample_pipeline: process(clk, reset)
+ begin
+ if reset then
+ sample_pipe(2 downto 1) <= (others => '0');
+ elsif rising_edge(clk) then
+ sample_pipe(1) <= sample_pipe(0);
+ sample_pipe(2) <= sample_pipe(1);
+ end if;
+ end process;
+
+ sample_now <= sample_pipe(SAMPLE_DELAY) = '1';
-- This is the main "input" deserializer. The internal
-- register is 9 bits wide using a sentinel value in the
-- LSB so that we don't need bit counters here.
- -- We know we're done with a byte when the MSB is '1'
+ -- We know we're done shifting when the MSB is '1'
-- This bit can also function as the valid flag
deserializer: process(clk, reset)
begin
@@ -228,21 +363,22 @@ begin
rx_reg <= (rx_reg'low => '1', others => '0');
elsif rising_edge(clk) then
-- Do the sample/shift when requested and flag the
- -- valid bytes once we have them
- if in_rx_phases and sclk_redge then
+ -- valid bytes once we have them. sample_now is already qualified
+ -- by in_rx_phases at the point the pulse was generated.
+ if sample_now then
-- Shift data by amount depending on mode
rx_reg <= shift_left(rx_reg, shift_amt);
-- Sample new data into vacated locations
if cur_io_mode = SINGLE then
- rx_reg(0) <= io(1);
+ rx_reg(0) <= io_tap(1);
elsif cur_io_mode = DUAL then
- rx_reg(0) <= io(0);
- rx_reg(1) <= io(1);
+ rx_reg(0) <= io_tap(0);
+ rx_reg(1) <= io_tap(1);
elsif cur_io_mode = QUAD then
- rx_reg(0) <= io(0);
- rx_reg(1) <= io(1);
- rx_reg(2) <= io(2);
- rx_reg(3) <= io(3);
+ rx_reg(0) <= io_tap(0);
+ rx_reg(1) <= io_tap(1);
+ rx_reg(2) <= io_tap(2);
+ rx_reg(3) <= io_tap(3);
end if;
elsif not in_rx_phases or rx_reg(rx_reg'high) = '1' then
-- Reset shifter to sentinel value when we become
diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_quick_io_tb.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_quick_io_tb.vhd
new file mode 100644
index 00000000..da48a5c9
--- /dev/null
+++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_quick_io_tb.vhd
@@ -0,0 +1,95 @@
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this
+-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
+--
+-- The shipped fast configuration at the *quick* IO corner.
+--
+-- spi_nor_fast_tb runs the same RTL configuration with the slow-corner FPGA IO
+-- delays, which is the corner that decides whether the sample point is late
+-- enough. This one runs the fast corner, which decides whether it is early
+-- enough: the sample point must land no later than half an sclk period past the
+-- point the part stops holding the previous bit (tCLQX), and at the fast corner
+-- everything arrives sooner, so that limit tightens.
+--
+-- Both corners have to pass for rx_sample_taps to be the right choice, and at
+-- 62.5MHz the margin at this corner is well under a nanosecond. That is the
+-- headline number for anyone considering pushing sclk higher, or deciding
+-- whether to fall back to sclk_divisor = 1.
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+library vunit_lib;
+ context vunit_lib.com_context;
+ context vunit_lib.vunit_context;
+use work.spi_nor_tb_pkg.all;
+use work.spi_nor_pkg.all;
+use work.spi_nor_target_vc_pkg.all;
+
+entity spi_nor_fast_quick_io_tb is
+ generic (
+ runner_cfg : string
+ );
+end entity;
+
+architecture tb of spi_nor_fast_quick_io_tb is
+
+begin
+
+ th: entity work.spi_nor_th
+ generic map (
+ sclk_divisor => 0,
+ rx_sample_taps => 2,
+ -- Fast corner of the delays the XDC bounds
+ out_delay => 1.6 ns,
+ in_delay => 0.5 ns
+ );
+
+ bench: process
+ alias reset is <>;
+ alias cs_n is <>;
+ constant flash : actor_t := find("spi_nor_target");
+ begin
+ test_runner_setup(runner, runner_cfg);
+
+ wait until reset = '0';
+ wait for 500 ns;
+
+ fill_pattern(net, flash);
+
+ while test_suite loop
+ if run("read_quad_32addr_dummy") then
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, 16#1200#, 64);
+ elsif run("read_dual_32addr_dummy") then
+ check_flash_read(net, FAST_READ_4BYTE_DUAL_OP, 8, 16#80#, 16);
+ elsif run("write_then_read_back") then
+ check_program_readback(net, flash, 16#2000#);
+ elsif run("back_to_back_reads") then
+ check_back_to_back_reads(net, 16#400#, 16#500#, 32);
+ elsif run("block_erase") then
+ check_block_erase(net, flash, 16#30000#);
+ elsif run("quad_page_program") then
+ check_quad_page_program(net, flash, 16#5000#);
+ elsif run("output_delay_margin") then
+ for clqv_ns in 2 to 7 loop
+ info("checking with tCLQV = " & to_string(clqv_ns) & " ns");
+ set_output_valid_delay(net, flash, clqv_ns * 1 ns);
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, 16#1200#, 32);
+ end loop;
+ end if;
+ end loop;
+
+ wait for 1 us;
+ if cs_n = '0' then
+ wait until cs_n = '1' for 1 ms;
+ end if;
+
+ wait for 1 us;
+ test_runner_cleanup(runner);
+ wait;
+ end process;
+
+ test_runner_watchdog(runner, 10 ms);
+
+end tb;
diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_tb.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_tb.vhd
new file mode 100644
index 00000000..200dfa9e
--- /dev/null
+++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_fast_tb.vhd
@@ -0,0 +1,113 @@
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this
+-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
+--
+-- The same checked scenarios as spi_nor_tb, run at the fast configuration the
+-- projects actually ship: sclk_divisor 0, so sclk is clk/2 (62.5MHz off the
+-- 125MHz system clock) instead of clk/6.
+--
+-- This is the configuration where every clk edge is also an sclk edge, so there
+-- are no spare cycles between sclk edges for phase transitions to settle in.
+-- It is a genuinely different timing regime from clk/6, not just a faster one,
+-- which is why it gets its own testbench rather than a comment.
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+library vunit_lib;
+ context vunit_lib.com_context;
+ context vunit_lib.vunit_context;
+use work.spi_nor_tb_pkg.all;
+use work.spi_nor_pkg.all;
+use work.spi_nor_target_vc_pkg.all;
+
+entity spi_nor_fast_tb is
+ generic (
+ runner_cfg : string
+ );
+end entity;
+
+architecture tb of spi_nor_fast_tb is
+
+begin
+
+ th: entity work.spi_nor_th
+ generic map (
+ sclk_divisor => 0,
+ rx_sample_taps => 2
+ );
+
+ bench: process
+ alias reset is <>;
+ alias cs_n is <>;
+ constant flash : actor_t := find("spi_nor_target");
+ begin
+ test_runner_setup(runner, runner_cfg);
+
+ wait until reset = '0';
+ wait for 500 ns;
+
+ fill_pattern(net, flash);
+
+ while test_suite loop
+ if run("jedec_id") then
+ check_jedec_id(net);
+ elsif run("read_quad_32addr_dummy") then
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, 16#1200#, 64);
+ elsif run("read_single_32addr") then
+ -- Note: on a real W25Q01JV plain READ_DATA is limited to 50MHz,
+ -- so this opcode is out of spec at this rate. It is exercised
+ -- here for the datapath only; hubris uses the fast reads.
+ check_flash_read(net, READ_DATA_4BYTE_OP, 0, 16#40#, 16);
+ elsif run("read_dual_32addr_dummy") then
+ check_flash_read(net, FAST_READ_4BYTE_DUAL_OP, 8, 16#80#, 16);
+ elsif run("write_then_read_back") then
+ check_program_readback(net, flash, 16#2000#);
+ elsif run("back_to_back_reads") then
+ check_back_to_back_reads(net, 16#400#, 16#500#, 32);
+ elsif run("block_erase") then
+ -- Commands with an address but no data phase leave the sclk
+ -- generator enabled a moment longer than the part allows if
+ -- the drive and clock-enable conditions are conflated.
+ check_block_erase(net, flash, 16#30000#);
+ elsif run("quad_page_program") then
+ check_quad_page_program(net, flash, 16#5000#);
+ elsif run("output_delay_margin") then
+ -- Sweep the part's clock-low-to-output-valid delay across the
+ -- datasheet range and well beyond it, and require correct data
+ -- throughout.
+ --
+ -- The harness already models the FPGA's clock-to-pin and
+ -- pin-to-flop delays, so this sweeps only the part's own tCLQV,
+ -- across and a little beyond its datasheet range. Combined with
+ -- the harness delays this covers the whole read round trip that
+ -- the XDC allows at the slow corner.
+ --
+ -- The sample point has to satisfy
+ -- round_trip_valid - half_period <= S <= half_period + round_trip_hold
+ -- and note the upper limit comes from tCLQX (output hold), not
+ -- tCLQV: sampling too late catches the *next* bit. That upper
+ -- limit is the binding one at the fast corner, which is what
+ -- spi_nor_fast_quick_io_tb covers.
+ for clqv_ns in 2 to 7 loop
+ info("checking with tCLQV = " & to_string(clqv_ns) & " ns");
+ set_output_valid_delay(net, flash, clqv_ns * 1 ns);
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, 16#1200#, 32);
+ end loop;
+ end if;
+ end loop;
+
+ wait for 1 us;
+ if cs_n = '0' then
+ wait until cs_n = '1' for 1 ms;
+ end if;
+
+ wait for 1 us;
+ test_runner_cleanup(runner);
+ wait;
+ end process;
+
+ test_runner_watchdog(runner, 10 ms);
+
+end tb;
diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb.vhd
index e28becf6..c4a05aee 100644
--- a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb.vhd
@@ -11,6 +11,7 @@ library vunit_lib;
context vunit_lib.vunit_context;
use work.spi_nor_tb_pkg.all;
use work.spi_nor_pkg.all;
+use work.spi_nor_target_vc_pkg.all;
entity spi_nor_tb is
generic (
@@ -22,7 +23,13 @@ architecture tb of spi_nor_tb is
begin
- th: entity work.spi_nor_th;
+ th: entity work.spi_nor_th
+ generic map(
+ sclk_divisor => 2,
+ rx_sample_taps => 2,
+ out_delay => 3.7 ns,
+ in_delay => 1.5 ns
+ );
bench: process
-- Note: External names are broken in GHDL llvm backends https://github.com/ghdl/ghdl/issues/2610
@@ -31,6 +38,7 @@ begin
-- reset_b uses the relative path form of external naming for example purposes.
alias reset is <>;
alias cs_n is <>;
+ constant flash : actor_t := find("spi_nor_target");
begin
-- Always the first thing in the process, set up things for the VUnit test runner
test_runner_setup(runner, runner_cfg);
@@ -40,12 +48,29 @@ begin
wait until reset = '0';
wait for 500 ns;
- -- I haven't built a flash model yet, so I'm just doing some basic
- -- waveform inspection here.
+ -- Give the flash model known contents so read paths can be checked
+ -- rather than just watched.
+ fill_pattern(net, flash);
+
while test_suite loop
if run("instr_only") then
- write_data_size(net, 3); -- read out 3 bytes
- write_instr(net, READ_JEDEC_ID_OP);
+ check_jedec_id(net);
+ elsif run("read_quad_32addr_dummy") then
+ -- The read path that actually matters: hubris and the eSPI flash
+ -- reader both use 4-byte-address quad fast read with 8 dummies.
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, 16#1200#, 64);
+ elsif run("read_single_32addr") then
+ check_flash_read(net, READ_DATA_4BYTE_OP, 0, 16#40#, 16);
+ elsif run("read_dual_32addr_dummy") then
+ check_flash_read(net, FAST_READ_4BYTE_DUAL_OP, 8, 16#80#, 16);
+ elsif run("write_then_read_back") then
+ check_program_readback(net, flash, 16#2000#);
+ elsif run("back_to_back_reads") then
+ check_back_to_back_reads(net, 16#400#, 16#500#, 32);
+ elsif run("block_erase") then
+ check_block_erase(net, flash, 16#30000#);
+ elsif run("quad_page_program") then
+ check_quad_page_program(net, flash, 16#5000#);
elsif run("write_24addr_no_dummy") then
write_data(net, x"03020100"); -- do do words
write_data(net, x"07060504"); -- do do words
diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb_pkg.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb_pkg.vhd
index 6403b635..c444a273 100644
--- a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb_pkg.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_tb_pkg.vhd
@@ -12,6 +12,8 @@ library vunit_lib;
context vunit_lib.com_context;
context vunit_lib.vc_context;
use work.spi_nor_regs_pkg.all;
+use work.spi_nor_pkg.all;
+use work.spi_nor_target_vc_pkg.all;
package spi_nor_tb_pkg is
@@ -43,6 +45,86 @@ package spi_nor_tb_pkg is
data : integer
);
+ procedure clear_fifos (
+ signal net : inout network_t
+ );
+
+ -- Wait for a transaction to start and then finish. Polling only for "not
+ -- busy" races the start of the transaction: writing Instr queues the go
+ -- strobe, and a register read issued straight afterwards can easily land
+ -- before cs_n has fallen.
+ procedure wait_txn_done (
+ signal net : inout network_t
+ );
+
+ procedure read_rx_word (
+ signal net : inout network_t;
+ variable data : out std_logic_vector(31 downto 0)
+ );
+
+ -- Pop count bytes out of the RX FIFO and check them against the pattern the
+ -- flash model was preloaded with. This is what makes the rx sample point
+ -- observable: a sample taken outside the part's valid window shifts in 'X'
+ -- or a neighbouring bit, and lands here as a mismatch.
+ procedure check_read_pattern (
+ signal net : inout network_t;
+ constant addr : natural;
+ constant count : natural
+ );
+
+ -- Checked scenarios, shared by the testbenches so the same coverage can be
+ -- run at more than one sclk rate without duplicating it.
+ procedure check_jedec_id (
+ signal net : inout network_t
+ );
+
+ -- opcode picks single / dual / quad and 3 or 4 byte addressing; dummies must
+ -- match what the part expects for that opcode.
+ procedure check_flash_read (
+ signal net : inout network_t;
+ constant opcode : std_logic_vector(7 downto 0);
+ constant dummies : natural;
+ constant addr : natural;
+ constant count : natural
+ );
+
+ procedure check_program_readback (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ );
+
+ -- Two reads issued with no software delay between them, which is what the
+ -- eSPI reader does when it chains page reads.
+ procedure check_back_to_back_reads (
+ signal net : inout network_t;
+ constant addr_a : natural;
+ constant addr_b : natural;
+ constant count : natural
+ );
+
+ -- Erase a block and prove the part actually did it. Commands with an address
+ -- but no data phase are the ones that expose trailing-clock bugs: the part
+ -- discards the instruction rather than reporting anything, so nothing but a
+ -- readback notices.
+ procedure check_block_erase (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ );
+
+ -- A full 256 byte page programmed in quad mode, which is exactly what the
+ -- hubris driver does. This is the hardest thing the tx path has to do: quad
+ -- data is two sclk cycles per byte, so at clk/2 a byte leaves every four clk
+ -- cycles, and every fourth byte crosses a 32 bit boundary in the tx FIFO.
+ -- A short single-mode write has eight times the slack and proves nothing
+ -- about it.
+ procedure check_quad_page_program (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ );
+
end package;
package body spi_nor_tb_pkg is
@@ -87,4 +169,238 @@ package body spi_nor_tb_pkg is
write_bus(net, bus_handle, To_StdLogicVector(TX_FIFO_WDATA_OFFSET + 16#100#, bus_handle.p_address_length), resize(data, 32));
end;
+ procedure clear_fifos (
+ signal net : inout network_t
+ ) is
+ begin
+ write_bus(net, bus_handle, To_StdLogicVector(SPICR_OFFSET + 16#100#, bus_handle.p_address_length),
+ SPICR_RX_FIFO_RESET_MASK or SPICR_TX_FIFO_RESET_MASK);
+
+ -- The self-clearing reset bits produce a single-cycle pulse into the XPM
+ -- dual-clock FIFOs, which want their reset held longer than that and are
+ -- unusable for a number of cycles afterwards. Software gets away with it
+ -- because consecutive register accesses are far apart; a testbench
+ -- issuing back-to-back writes does not, and the FIFO data is dropped.
+ wait for 1 us;
+ end;
+
+ procedure wait_txn_done (
+ signal net : inout network_t
+ ) is
+
+ variable status : std_logic_vector(31 downto 0);
+ variable started : boolean := false;
+
+ begin
+ -- Wait for cs_n to fall. A register read takes several clocks and cs_n
+ -- asserts a handful of clocks after the go strobe, so this always
+ -- catches even the shortest transaction.
+ for i in 0 to 63 loop
+ read_bus(net, bus_handle, To_StdLogicVector(SPISR_OFFSET + 16#100#, bus_handle.p_address_length), status);
+ if (status and SPISR_BUSY_MASK) /= (status'range => '0') then
+ started := true;
+ exit;
+ end if;
+ end loop;
+
+ check_true(started, "transaction never became busy");
+
+ loop
+ read_bus(net, bus_handle, To_StdLogicVector(SPISR_OFFSET + 16#100#, bus_handle.p_address_length), status);
+ exit when (status and SPISR_BUSY_MASK) = (status'range => '0');
+ end loop;
+ end;
+
+ procedure read_rx_word (
+ signal net : inout network_t;
+ variable data : out std_logic_vector(31 downto 0)
+ ) is
+
+ variable status : std_logic_vector(31 downto 0);
+
+ begin
+ -- The RX FIFO is a dual-clock FIFO and the last partial word is only
+ -- pushed when cs_n rises, so a word can still be in flight when busy
+ -- clears. Wait for it to show up rather than reading a stale register.
+ loop
+ read_bus(net, bus_handle, To_StdLogicVector(SPISR_OFFSET + 16#100#, bus_handle.p_address_length), status);
+ exit when (status and SPISR_RX_EMPTY_MASK) = (status'range => '0');
+ end loop;
+
+ read_bus(net, bus_handle, To_StdLogicVector(RX_FIFO_RDATA_OFFSET + 16#100#, bus_handle.p_address_length), data);
+ end;
+
+ procedure check_read_pattern (
+ signal net : inout network_t;
+ constant addr : natural;
+ constant count : natural
+ ) is
+
+ variable word : std_logic_vector(31 downto 0);
+ variable got : std_logic_vector(7 downto 0);
+
+ begin
+ for i in 0 to count - 1 loop
+ -- The width adaptor packs bytes little-endian into each word, so a
+ -- fresh word is popped every fourth byte.
+ if i mod 4 = 0 then
+ read_rx_word(net, word);
+ end if;
+
+ got := word(8 * (i mod 4) + 7 downto 8 * (i mod 4));
+ check_equal(got, pattern_byte(addr + i),
+ "flash read data mismatch at offset " & to_string(i) &
+ " (flash address " & to_string(addr + i) & ")");
+ end loop;
+ end;
+
+ procedure check_jedec_id (
+ signal net : inout network_t
+ ) is
+
+ variable word : std_logic_vector(31 downto 0);
+
+ begin
+ clear_fifos(net);
+ write_data_size(net, 3);
+ write_dummy(net, 0);
+ write_instr(net, READ_JEDEC_ID_OP);
+ wait_txn_done(net);
+ read_rx_word(net, word);
+ -- Winbond mfr id, W25Q01JV device id, lsb-packed by the width adaptor
+ check_equal(word(23 downto 0), std_logic_vector'(x"2140EF"),
+ "unexpected JEDEC id");
+ end;
+
+ procedure check_flash_read (
+ signal net : inout network_t;
+ constant opcode : std_logic_vector(7 downto 0);
+ constant dummies : natural;
+ constant addr : natural;
+ constant count : natural
+ ) is
+ begin
+ clear_fifos(net);
+ write_dummy(net, dummies);
+ write_data_size(net, count);
+ write_addr(net, To_StdLogicVector(addr, 32));
+ write_instr(net, opcode);
+ wait_txn_done(net);
+ check_read_pattern(net, addr, count);
+ end;
+
+ procedure check_program_readback (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ ) is
+
+ variable got : std_logic_vector(7 downto 0);
+
+ begin
+ clear_fifos(net);
+ -- NOR can only clear bits, so start from erased
+ erase_flash(net, flash, addr, 16#1000#);
+ write_data(net, x"03020100");
+ write_data(net, x"07060504");
+ write_dummy(net, 0);
+ write_data_size(net, 8);
+ write_addr(net, To_StdLogicVector(addr, 32));
+ write_instr(net, PAGE_PROGRAM_4BYTE_OP);
+ wait_txn_done(net);
+
+ for i in 0 to 7 loop
+ read_flash_byte(net, flash, addr + i, got);
+ check_equal(got, std_logic_vector(to_unsigned(i, 8)),
+ "programmed byte mismatch at offset " & to_string(i));
+ end loop;
+ end;
+
+ procedure check_block_erase (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ ) is
+
+ variable got : std_logic_vector(7 downto 0);
+
+ begin
+ -- Put something other than 0xFF in the way so a no-op erase cannot pass
+ for i in 0 to 7 loop
+ write_flash_byte(net, flash, addr + i, pattern_byte(addr + i));
+ end loop;
+
+ clear_fifos(net);
+ write_dummy(net, 0);
+ write_data_size(net, 0);
+
+ -- Issue the write enable the way the driver does. It carries neither an
+ -- address nor data, so it is the shortest command the block emits and
+ -- the least forgiving about trailing clocks -- and every erase and
+ -- program on real hardware is preceded by one, so a discarded write
+ -- enable looks exactly like a discarded erase.
+ write_instr(net, WRITE_ENABLE_OP);
+ wait_txn_done(net);
+
+ write_addr(net, To_StdLogicVector(addr, 32));
+ write_instr(net, BLOCK_ERASE_64K_4BYTE_OP);
+ wait_txn_done(net);
+
+ -- The model only applies the erase if the instruction was well formed,
+ -- so this catches a discarded command as well as a wrong address.
+ for i in 0 to 7 loop
+ read_flash_byte(net, flash, addr + i, got);
+ check_equal(got, std_logic_vector'(x"FF"),
+ "block erase left byte " & to_string(i) & " unerased");
+ end loop;
+ end;
+
+ procedure check_quad_page_program (
+ signal net : inout network_t;
+ constant flash : actor_t;
+ constant addr : natural
+ ) is
+
+ variable word : std_logic_vector(31 downto 0);
+ variable got : std_logic_vector(7 downto 0);
+
+ begin
+ clear_fifos(net);
+ erase_flash(net, flash, addr, 256);
+
+ write_dummy(net, 0);
+ write_data_size(net, 256);
+ write_addr(net, To_StdLogicVector(addr, 32));
+
+ -- 64 words fill the tx FIFO exactly, the same way the driver does it
+ for w in 0 to 63 loop
+ for b in 0 to 3 loop
+ word(8 * b + 7 downto 8 * b) := pattern_byte(addr + 4 * w + b);
+ end loop;
+ write_data(net, word);
+ end loop;
+
+ write_instr(net, QUAD_INPUT_PAGE_PROGRAM_4BYTE_OP);
+ wait_txn_done(net);
+
+ for i in 0 to 255 loop
+ read_flash_byte(net, flash, addr + i, got);
+ check_equal(got, pattern_byte(addr + i),
+ "quad page program mismatch at offset " & to_string(i));
+ end loop;
+ end;
+
+ procedure check_back_to_back_reads (
+ signal net : inout network_t;
+ constant addr_a : natural;
+ constant addr_b : natural;
+ constant count : natural
+ ) is
+ begin
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, addr_a, count);
+ -- No delay here on purpose: the flash needs a minimum cs_n high time
+ -- and the model enforces it.
+ check_flash_read(net, FAST_READ_4BYTE_QUAD_OP, 8, addr_b, count);
+ end;
+
end package body;
diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd
index 4e275c1f..46750368 100644
--- a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd
@@ -18,6 +18,20 @@ use work.axil26x32_pkg;
use work.axilite_if_2k19_helper_pkg.all;
entity spi_nor_th is
+ generic (
+ -- Defaults match the shipped-for-years configuration: 125MHz / 6 =
+ -- 20.83MHz sclk with the sample point one clk after the rising edge.
+ sclk_divisor : natural := 2;
+ rx_sample_taps : natural := 2;
+ -- Delay from a launch flop to the pin at the flash, and from the pin back
+ -- to the capture flop, board trace included. RTL simulation has no notion
+ -- of either, but together they are a bigger share of a 16ns sclk period
+ -- than the flash's own tCLQV, so the sample point cannot be validated
+ -- without them. The XDC bounds both (via the output delay window and the
+ -- IOB packing); these defaults are the slow corner it allows.
+ out_delay : time := 3.7 ns;
+ in_delay : time := 1.5 ns
+ );
end entity;
architecture th of spi_nor_th is
@@ -30,6 +44,12 @@ architecture th of spi_nor_th is
signal io : std_logic_vector(3 downto 0);
signal io_o : std_logic_vector(3 downto 0);
signal io_oe : std_logic_vector(3 downto 0);
+ signal flash_o : std_logic_vector(3 downto 0);
+ signal flash_oe : std_logic_vector(3 downto 0);
+ -- The bus and clock as seen at the part, i.e. after the outbound delay
+ signal io_flash : std_logic_vector(3 downto 0);
+ signal sclk_flash : std_logic;
+ signal csn_flash : std_logic;
constant config_array : axil_responder_cfg_array_t(0 downto 0) :=
(
0 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8)
@@ -80,6 +100,10 @@ begin
resiser: entity work.axil8_resizer port map(fabric => responders(0), responder =>responders_8b(0));
spi_nor_top_inst: entity work.spi_nor_top
+ generic map (
+ sclk_divisor => sclk_divisor,
+ rx_sample_taps => rx_sample_taps
+ )
port map (
clk => clk,
reset => reset,
@@ -89,6 +113,7 @@ begin
io => io,
io_o => io_o,
io_oe => io_oe,
+ sp5_owns_flash => open,
espi_cmd_fifo_rdata => (others => '0'),
espi_cmd_fifo_rdack => open,
espi_cmd_fifo_rempty => '1',
@@ -96,10 +121,52 @@ begin
espi_data_fifo_write => open
);
- io_tris: process(all)
+ -- Everything the part sees is delayed by out_delay; everything the DUT
+ -- captures is delayed again by in_delay on the way back.
+ sclk_flash <= sclk after out_delay;
+ csn_flash <= cs_n after out_delay;
+
+ flash: entity work.spi_nor_target_vc
+ generic map (
+ actor_name => "spi_nor_target"
+ )
+ port map (
+ cs_n => csn_flash,
+ sclk => sclk_flash,
+ io => io_flash,
+ io_o => flash_o,
+ io_oe => flash_oe
+ );
+
+ -- Both ends contribute to the resolved bus at the part, plus a weak pull-up
+ -- standing in for the board's. If both ends drive the same lane the
+ -- resolution goes to 'X', which the DUT then shifts in and the data checks
+ -- catch.
+ bus_gen: for i in io_flash'range generate
+ io_flash(i) <= io_o(i) after out_delay when io_oe(i) = '1' else 'Z' after out_delay;
+ io_flash(i) <= flash_o(i) when flash_oe(i) = '1' else 'Z';
+ io_flash(i) <= 'H';
+ end generate;
+
+ io <= io_flash after in_delay;
+
+ -- Contention is a real bug that shows up as marginal reads on hardware, so
+ -- name it explicitly rather than leaving it to be inferred from an 'X'.
+ contention_check: process(all)
+ alias rel is << signal spi_nor_top_inst.release_lanes : std_logic_vector(3 downto 0) >>;
+ alias in_tx is << signal spi_nor_top_inst.in_tx_phases : boolean >>;
+ alias in_rx is << signal spi_nor_top_inst.in_rx_phases : boolean >>;
begin
for i in io'range loop
- io(i) <= io_o(i) when io_oe(i) = '1' else 'H';
+ assert not (io_oe(i) = '1' and flash_oe(i) = '1')
+ report "Bus contention: controller and flash both driving io(" &
+ to_string(i) & "), ctrl_oe=" & to_string(io_oe) &
+ " flash_oe=" & to_string(flash_oe) &
+ " cs_n=" & to_string(cs_n) &
+ " release=" & to_string(rel) &
+ " in_tx=" & to_string(in_tx) &
+ " in_rx=" & to_string(in_rx)
+ severity error;
end loop;
end process;
diff --git a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd
index 0dc27f17..cf5dd8df 100644
--- a/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/spi_nor_top.vhd
@@ -11,6 +11,18 @@ use work.spi_nor_regs_pkg.all;
use work.axil8x32_pkg.all;
entity spi_nor_top is
+ generic (
+ -- sclk = clk / (2 * (sclk_divisor + 1)). 0 gives clk/2, which is the
+ -- fastest this structure supports.
+ sclk_divisor : natural := 2;
+ -- Read data sample point, in half-clk steps after the sclk rising edge.
+ -- 2 is the historical behaviour (one whole clk after the edge). See
+ -- spi_link for how the taps map onto capture flops.
+ rx_sample_taps : natural range 0 to 4 := 2;
+ -- Chip select timing, in clk cycles. Independent of sclk.
+ cs_setup_cnts : natural := 4;
+ cs_high_cnts : natural := 7
+ );
port (
clk : in std_logic;
reset : in std_logic;
@@ -40,13 +52,21 @@ end entity;
architecture rtl of spi_nor_top is
- constant div_val : unsigned(15 downto 0) := to_unsigned(2, 16);
+ constant div_val : unsigned(15 downto 0) := to_unsigned(sclk_divisor, 16);
signal rx_byte_done : boolean;
- signal tx_byte_done : boolean;
+ signal tx_byte_req : boolean;
signal in_rx_phases : boolean;
signal in_tx_phases : boolean;
+ signal release_lanes : std_logic_vector(3 downto 0);
+ signal sclk_running : boolean;
+ -- Internal copies of sclk and cs_n. The versions that leave the block are
+ -- duplicate flops with no internal fanout so they can be packed into the
+ -- IOBs; these are what the phase logic looks at.
+ signal sclk_internal : std_logic;
+ signal cs_n_internal : std_logic;
signal link_rx_byte : std_logic_vector(7 downto 0);
signal link_tx_byte : std_logic_vector(7 downto 0);
+ signal link_tx_mode : io_mode;
signal cur_io_mode : io_mode;
signal rx_fifo_write8 : std_logic;
signal tx_fifo_read8 : std_logic;
@@ -87,6 +107,9 @@ begin
-- this does clock-gen, and has the serializer and
-- deserializer that operate during tx and rx phases
link: entity work.spi_link
+ generic map (
+ rx_sample_taps => rx_sample_taps
+ )
port map (
clk => clk,
reset => reset,
@@ -95,15 +118,19 @@ begin
divisor => div_val,
in_tx_phases => in_tx_phases,
in_rx_phases => in_rx_phases,
+ sclk_running => sclk_running,
+ release_lanes => release_lanes,
rx_byte => link_rx_byte,
rx_byte_done => rx_byte_done,
tx_byte => link_tx_byte,
- tx_byte_done => tx_byte_done,
+ tx_byte_mode => link_tx_mode,
+ tx_byte_req => tx_byte_req,
sclk_redge => open,
sclk_fedge => open,
-- spi link signals
- sclk => sclk,
- cs_n => cs_n,
+ sclk => sclk_internal,
+ sclk_pin => sclk,
+ cs_n => cs_n_internal,
io => io,
io_o => io_o,
io_oe => io_oe
@@ -113,20 +140,28 @@ begin
-- feeds data to/from the serializer/deserializer
-- to/from FIFOs
spi_txn_mgr_inst: entity work.spi_txn_mgr
+ generic map (
+ cs_setup_cnts => cs_setup_cnts,
+ cs_high_cnts => cs_high_cnts
+ )
port map (
clk => clk,
reset => reset,
-- registers i/f
spi_cmd => spi_cmd_if,
-- link i/f
- cs_n => cs_n,
- sclk => sclk,
+ cs_n => cs_n_internal,
+ cs_n_pin => cs_n,
+ sclk => sclk_internal,
rx_byte_done => rx_byte_done,
rx_link_byte => link_rx_byte,
- tx_byte_done => tx_byte_done,
+ tx_byte_req => tx_byte_req,
tx_link_byte => link_tx_byte,
+ tx_link_mode => link_tx_mode,
in_rx_phases => in_rx_phases,
in_tx_phases => in_tx_phases,
+ sclk_running => sclk_running,
+ release_lanes => release_lanes,
cur_io_mode => cur_io_mode,
tx_fifo_ack => tx_fifo_read8,
tx_fifo_data => tx_fifo_data8,
@@ -155,7 +190,7 @@ begin
port map (
clk => clk,
reset => reset,
- txn_complete => cs_n,
+ txn_complete => cs_n_internal,
-- TX FIFO Interface
read_data => tx_fifo_data8,
read_ack => tx_fifo_read8,
@@ -237,7 +272,7 @@ begin
spisr_reg.rx_full <= '1' when spisr_reg.rx_used_wds = 64 else '0';
- spisr_reg.busy <= '1' when cs_n = '0' else '0';
+ spisr_reg.busy <= '1' when cs_n_internal = '0' else '0';
-- Hubris-interactable registers for control and status
spi_nor_regs_inst: entity work.spi_nor_regs
diff --git a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd
index 5936f356..aff99f20 100644
--- a/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd
+++ b/hdl/ip/vhd/spi_nor_controller/spi_txn/spi_txn_mgr.vhd
@@ -9,6 +9,15 @@ use ieee.numeric_std_unsigned.all;
use work.spi_nor_pkg.all;
entity spi_txn_mgr is
+ generic (
+ -- clk cycles from cs_n asserting to the first sclk edge (tSLCH)
+ cs_setup_cnts : natural := 4;
+ -- Minimum clk cycles cs_n must stay high between transactions (tSHSL).
+ -- The eSPI read path chains page reads back to back and will otherwise
+ -- re-assert cs_n the cycle after it goes high, which the flash does not
+ -- allow. This is in clk cycles, so it does not scale with sclk.
+ cs_high_cnts : natural := 7
+ );
port (
clk : in std_logic;
reset : in std_logic;
@@ -20,15 +29,26 @@ entity spi_txn_mgr is
-- instr: in std_logic_vector(7 downto 0);
-- go_flag: in std_logic;
-- link interface
- cs_n : out std_logic;
+ cs_n : out std_logic;
+ -- Second copy of the cs_n flop, for the pin only, so it can be packed
+ -- into the IOB. Same reasoning as spi_clk_gen's sclk_pin.
+ cs_n_pin : out std_logic;
sclk : in std_logic;
rx_byte_done : in boolean;
rx_link_byte : in std_logic_vector(7 downto 0);
- tx_byte_done : in boolean;
+ tx_byte_req : in boolean;
tx_link_byte : out std_logic_vector(7 downto 0);
+ -- The io mode the byte above belongs to. Travels with it because the
+ -- serializer needs both on the same edge; see next_tx.
+ tx_link_mode : out io_mode;
in_rx_phases : out boolean;
in_tx_phases : out boolean;
- cur_io_mode : out io_mode;
+ -- Gates the sclk generator. Narrower than in_tx_phases on purpose, see
+ -- is_sclk_running below.
+ sclk_running : out boolean;
+ -- Lanes to stop driving ahead of a turnaround, see below
+ release_lanes : out std_logic_vector(3 downto 0);
+ cur_io_mode : out io_mode;
-- fifo interface
rx_fifo_data : out std_logic_vector(7 downto 0);
rx_fifo_write : out std_logic;
@@ -42,7 +62,6 @@ architecture rtl of spi_txn_mgr is
attribute mark_debug : string;
constant BYTES_24BIT_ADDR : integer := 3;
constant BYTES_32BIT_ADDR : integer := 4;
- constant CS_CLK_DELAY_CNTS : integer := 4;
type state_t is (idle, cs_assert, instruction, addr, dummy, wdata, rdata, cs_deassert);
@@ -51,13 +70,25 @@ architecture rtl of spi_txn_mgr is
txn : txn_info_t;
csn : std_logic;
counter : integer range 0 to 512;
+ -- Counts down the enforced cs_n high time. Separate from `counter`
+ -- because it has to keep running while we sit in idle.
+ cs_high : integer range 0 to 63;
end record;
- constant r_reset : reg_type := (idle, txn_info_t_reset, '1', 0);
+ constant r_reset : reg_type := (idle, txn_info_t_reset, '1', 0, 0);
signal r, rin : reg_type;
attribute mark_debug of r : signal is "TRUE";
signal sclk_last : std_logic;
+ -- The byte the serializer will load at its next reload, and the io mode it
+ -- belongs to, held ready ahead of the edge that consumes them. See next_tx.
+ type tx_prefetch_t is record
+ byte : std_logic_vector(7 downto 0);
+ mode : io_mode;
+ end record;
+
+ signal tx_pre : tx_prefetch_t;
+
-- Some helper functions
-- This block gets the expected IO mode given the state and the transaction we're running
function get_cur_io_mode (
@@ -66,11 +97,17 @@ architecture rtl of spi_txn_mgr is
) return io_mode is
begin
if state = idle or
+ state = cs_assert or
state = instruction or
state = addr or
state = dummy then
-- no matter what the transaction moves to, we're in single mode
- -- for these phases
+ -- for these phases. cs_assert belongs here because the serializer is
+ -- already shifting the opcode out during it: sclk is enabled as soon
+ -- as we assert cs_n, and at clk/2 the first sclk edge lands inside
+ -- cs_assert rather than after it. Reporting the data mode here made
+ -- the opcode go out 4 bits at a time, so the part saw a different
+ -- instruction entirely.
return single;
else
-- otherwise use the mode specified by the opcode
@@ -78,13 +115,37 @@ architecture rtl of spi_txn_mgr is
end if;
end;
+ -- The phases sclk is allowed to run in. This is deliberately *not* the same
+ -- question as "are we driving the bus": cs_deassert drives but must not
+ -- clock. The part counts sclk edges to decide where an instruction ends, and
+ -- an erase or a write enable that gets even one trailing edge before cs_n
+ -- rises is discarded outright, with no error anywhere -- it just silently
+ -- does not happen. Conflating the two conditions is what broke erase.
+ function is_sclk_running (
+ state: state_t
+ ) return boolean is
+ begin
+ return state = cs_assert or state = instruction or state = addr or
+ state = dummy or state = wdata or state = rdata;
+ end;
+
-- This function takes the state and returns if we're driving data lines
- -- currently
+ -- currently.
+ --
+ -- cs_deassert counts as driving for anything that is not a read: the part
+ -- samples our last bit on the sclk edge after we leave wdata, and io_oe is
+ -- registered, so releasing there would change mosi on the very edge being
+ -- sampled once a half period is a single clk cycle. Holding until cs_n rises
+ -- is what a controller should do anyway. Reads must not drive here, since
+ -- the part keeps driving until it is deselected.
function is_in_tx_phases (
+ txn: txn_info_t;
state: state_t
) return boolean is
begin
- return state = cs_assert or state = wdata or state = dummy or state = addr or state = instruction;
+ return state = cs_assert or state = wdata or state = dummy or
+ state = addr or state = instruction or
+ (state = cs_deassert and txn.data_kind /= read);
end;
-- This function takes the state and returns if we're sampling data lines
@@ -96,15 +157,91 @@ architecture rtl of spi_txn_mgr is
return state = rdata;
end;
+ -- The byte the serializer will load at its *next* reload.
+ --
+ -- The serializer reloads on the same clk edge this state machine advances a
+ -- phase, so the byte has to already be sitting there when that edge arrives
+ -- rather than be selected by it. Driving the mux from `v` satisfied that by
+ -- hanging the entire selection off tx_reg in one combinational path:
+ -- shifter-empty detect, then the next-state decode, then a byte select off
+ -- the decremented counter, then the lane mux into io_o. At clk/6 there was
+ -- room for it; at clk/2 it was the design's critical path, eight levels of
+ -- LUT spending 6.9ns of an 8ns period to reach io_o.
+ --
+ -- *Which* byte comes next does not depend on *when* the request arrives,
+ -- though -- it is a function of the registered state alone. So compute it
+ -- from `r` and register it, leaving the reload edge nothing to do but copy a
+ -- flop. The prefetch settles two clks after each phase advance and byte
+ -- slots are at least four clks apart (quad data at clk/2, the fastest this
+ -- block runs), so it is always in place in time.
+ -- The prefetch carries the io mode with the byte, and it has to: the mode is
+ -- a property of the phase the byte belongs to, and the serializer needs both
+ -- at the same instant. Taking the mode from the registered state instead
+ -- means the first data byte of a dual or quad write is loaded while the
+ -- state still reads `addr`, so it goes out with single-bit lane assignment
+ -- and, worse, gets shifted by one instead of four. That leaves the shifter's
+ -- sentinel on an odd bit where the byte-complete compare can never match it,
+ -- so the byte counter stops advancing and the transaction never ends.
+ function next_tx (
+ r : reg_type;
+ cmd : spi_nor_cmd_t;
+ fifo : std_logic_vector(7 downto 0)
+ ) return tx_prefetch_t is
+ variable idx : integer range 0 to 3;
+ begin
+ case r.state is
+ when idle =>
+ -- The opcode is the first byte out. The serializer preloads it
+ -- on the cs_n assert edge, before the first sclk edge.
+ return (byte => cmd.instr, mode => single);
+ when cs_assert | instruction =>
+ -- Whatever follows the opcode: the top address byte, or the
+ -- first data byte for an opcode that writes without an address.
+ case r.txn.addr_kind is
+ when bit32 =>
+ return (byte => cmd.addr(31 downto 24), mode => single);
+ when bit24 =>
+ return (byte => cmd.addr(23 downto 16), mode => single);
+ when none =>
+ if not r.txn.uses_dummys and r.txn.data_kind = write then
+ return (byte => fifo, mode => r.txn.data_mode);
+ end if;
+ end case;
+ when addr =>
+ if r.counter > 0 then
+ idx := r.counter - 1;
+ return (byte => cmd.addr(8 * idx + 7 downto 8 * idx),
+ mode => single);
+ elsif not r.txn.uses_dummys and r.txn.data_kind = write then
+ return (byte => fifo, mode => r.txn.data_mode);
+ end if;
+ when wdata =>
+ -- The fifo was acked on the edge that consumed the previous
+ -- byte, so its head is already the one after it. The tail of the
+ -- phase keeps reporting the data mode so the lane assignment
+ -- does not twitch on the last edge.
+ return (byte => fifo, mode => r.txn.data_mode);
+ when others =>
+ null;
+ end case;
+
+ -- Nothing of ours to send: dummy cycles or a read, both of which shift
+ -- ones out a bit at a time.
+ return (byte => (others => '1'), mode => single);
+ end;
+
begin
-- "Simple outputs"
cs_n <= r.csn;
+ tx_link_byte <= tx_pre.byte;
+ tx_link_mode <= tx_pre.mode;
-- rx fifo is just a pass through here, no need for any muxing
rx_fifo_data <= rx_link_byte;
rx_fifo_write <= '1' when rx_byte_done else '0';
in_rx_phases <= is_in_rx_phases(r.state);
- in_tx_phases <= is_in_tx_phases(r.state);
+ in_tx_phases <= is_in_tx_phases(r.txn, r.state);
+ sclk_running <= is_sclk_running(r.state);
-- more complicated outputs
@@ -114,14 +251,28 @@ begin
cur_io_mode <= get_cur_io_mode(r.txn, r.state);
end process;
- -- Set up a bunch of muxes, and other signals that are used in the modules below
- -- mux into the serializer: we are sending static data sometimes, and fifo data sometimes
- tx_link_byte <= spi_cmd.instr when (r.state = instruction or r.state = cs_assert) else
- spi_cmd.addr(8 * r.counter + 7 downto 8 * r.counter) when r.state = addr else
- tx_fifo_data when r.state = wdata else
- (others => '1');
- -- we only want to FIFO ack when we were reading from the fifo, not the static data
- tx_fifo_ack <= '1' when tx_byte_done and r.state = wdata else '0';
+ -- Stop driving the lanes the flash is about to drive, an sclk cycle before
+ -- it starts. Only multi-bit reads with dummy cycles need this: during the
+ -- dummy phase cur_io_mode is still single, so io0/io3 are being driven for
+ -- HOLD avoidance right up to the point a dual or quad read takes them over.
+ -- At 20MHz the registered io_oe happened to drop in time; at clk/2 it does
+ -- not, and the overlap shows up as marginal first bytes rather than an
+ -- obvious failure. Lanes the part never drives in this mode are left alone.
+ release_gen: process(all)
+ begin
+ release_lanes <= "0000";
+ if r.state = dummy and r.txn.data_kind = read and r.counter <= 1 then
+ case r.txn.data_mode is
+ when quad =>
+ release_lanes <= "1111";
+ when dual =>
+ release_lanes <= "0011";
+ when single =>
+ -- the part only drives io1, which we are not driving
+ release_lanes <= "0000";
+ end case;
+ end if;
+ end process;
-- main controller state machine
controller: process(all)
@@ -130,13 +281,20 @@ begin
begin
v := r;
slk_redge := sclk = '1' and sclk_last = '0';
+
+ if r.cs_high > 0 then
+ v.cs_high := r.cs_high - 1;
+ end if;
case r.state is
when idle =>
- if spi_cmd.go_flag then
+ -- Hold off until the part's minimum cs_n high time has elapsed.
+ -- The eSPI manager re-asserts go_flag as soon as it sees us go
+ -- un-busy, which is the same cycle cs_n rises.
+ if spi_cmd.go_flag = '1' and r.cs_high = 0 then
v.state := cs_assert;
-- build up transaction info based on opcode
v.txn := get_txn_info(spi_cmd.instr);
- v.counter := CS_CLK_DELAY_CNTS;
+ v.counter := cs_setup_cnts;
end if;
when cs_assert =>
if r.counter = 0 then
@@ -149,7 +307,7 @@ begin
-- address phase, or issue dummy clocks,
-- or go directly to a read/write phase
-- so we check all the options here
- if tx_byte_done then
+ if tx_byte_req then
case r.txn.addr_kind is
when bit24 =>
v.counter := BYTES_24BIT_ADDR - 1; -- zero indexed
@@ -170,7 +328,7 @@ begin
v.state := wdata;
when none =>
v.state := cs_deassert;
- v.counter := CS_CLK_DELAY_CNTS;
+ v.counter := cs_setup_cnts;
end case;
end if;
end case;
@@ -181,7 +339,7 @@ begin
-- dummy clocks. I don't think there are commands
-- that issue an address and then do nothing but
-- we added the de-assert state for completeness
- if tx_byte_done and r.counter = 0 then
+ if tx_byte_req and r.counter = 0 then
if r.txn.uses_dummys then
v.state := dummy;
v.counter := to_integer(spi_cmd.dummy_cycles);
@@ -193,9 +351,9 @@ begin
v.counter := to_integer(spi_cmd.data_bytes);
else
v.state := cs_deassert;
- v.counter := CS_CLK_DELAY_CNTS;
+ v.counter := cs_setup_cnts;
end if;
- elsif tx_byte_done then
+ elsif tx_byte_req then
v.counter := r.counter - 1;
end if;
when dummy =>
@@ -208,7 +366,7 @@ begin
v.counter := to_integer(spi_cmd.data_bytes);
else
v.state := cs_deassert;
- v.counter := CS_CLK_DELAY_CNTS;
+ v.counter := cs_setup_cnts;
end if;
elsif slk_redge then
v.counter := r.counter - 1;
@@ -217,11 +375,11 @@ begin
-- Data counter is 1 indexded to better align
-- with sw expectations so we're done when
-- r.counter = 1
- if tx_byte_done and r.counter = 1 then
+ if tx_byte_req and r.counter = 1 then
-- We're done with the transaction
v.state := cs_deassert;
- v.counter := CS_CLK_DELAY_CNTS;
- elsif tx_byte_done then
+ v.counter := cs_setup_cnts;
+ elsif tx_byte_req then
v.counter := r.counter - 1;
end if;
when rdata =>
@@ -231,7 +389,7 @@ begin
if rx_byte_done and r.counter = 1 then
-- We're done with the transaction
v.state := cs_deassert;
- v.counter := CS_CLK_DELAY_CNTS;
+ v.counter := cs_setup_cnts;
elsif rx_byte_done then
v.counter := r.counter - 1;
end if;
@@ -242,14 +400,33 @@ begin
v.counter := r.counter - 1;
end if;
end case;
- -- Deal with the chip selects once we've figured
- -- out what state we're going to be in next
- if v.state = cs_assert then
+ -- cs_n is derived from `r` rather than from the v.state we just decoded,
+ -- even though the two agree. cs_n_pin is an IOB flop, so whatever feeds
+ -- its D lands on the same kind of long launch path io_o does, and going
+ -- through v.state put the shifter's empty detect and the entire phase
+ -- decode on it for no reason: cs_n only moves on the idle -> cs_assert
+ -- and cs_deassert -> idle edges, and neither involves tx_byte_req.
+ -- Staying in cs_assert or idle leaves it where it already is.
+ if r.state = idle and spi_cmd.go_flag = '1' and r.cs_high = 0 then
v.csn := '0';
- elsif v.state = idle then
+ elsif r.state = cs_deassert and r.counter = 0 then
v.csn := '1';
end if;
+ -- Arm the high-time counter on the edge that raises cs_n so the next
+ -- transaction cannot start too soon. Off v.state, since cs_high feeds
+ -- nothing but internal logic.
+ if v.state = idle and r.csn = '0' then
+ v.cs_high := cs_high_cnts;
+ end if;
+
+ -- Only ack the FIFO for bytes that actually came from it
+ if tx_byte_req and v.state = wdata then
+ tx_fifo_ack <= '1';
+ else
+ tx_fifo_ack <= '0';
+ end if;
+
rin <= v;
end process;
@@ -258,9 +435,17 @@ begin
if reset then
r <= r_reset;
sclk_last <= '0';
+ cs_n_pin <= '1';
+ tx_pre <= (byte => (others => '1'), mode => single);
elsif rising_edge(clk) then
sclk_last <= sclk;
r <= rin;
+ -- Off `r`, not `rin`: the point of the prefetch is to give this
+ -- selection a full clk period of its own, clear of the reload edge.
+ tx_pre <= next_tx(r, spi_cmd, tx_fifo_data);
+ -- Duplicate of r.csn, driven from the same next-state value so the
+ -- two flops always agree and change on the same edge.
+ cs_n_pin <= rin.csn;
end if;
end process;
diff --git a/hdl/ip/vhd/vunit_components/BUCK b/hdl/ip/vhd/vunit_components/BUCK
index 4560f006..17103cfd 100644
--- a/hdl/ip/vhd/vunit_components/BUCK
+++ b/hdl/ip/vhd/vunit_components/BUCK
@@ -20,6 +20,13 @@ vhdl_unit(
visibility = ['PUBLIC'],
)
+vhdl_unit(
+ name = "spi_nor_target_vc",
+ srcs = glob(["spi_nor_target/*.vhd"]),
+ standard = "2019",
+ visibility = ['PUBLIC'],
+)
+
vhdl_unit(
name = "i2c_cmd_vc",
srcs = glob(["i2c_cmd/*.vhd"]),
diff --git a/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc.vhd b/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc.vhd
new file mode 100644
index 00000000..662b56d6
--- /dev/null
+++ b/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc.vhd
@@ -0,0 +1,611 @@
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this
+-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
+--
+-- Winbond W25Q-family QSPI NOR flash target model.
+--
+-- This exists to answer two questions the previous "pull the bus to 'H' and
+-- eyeball the waveform" harness could not:
+--
+-- 1) does the controller sample read data at the right point, and
+-- 2) does it meet the flash's AC timing at the sclk rate we want to run.
+--
+-- So read data is driven with a real tCLQV output delay and is only held until
+-- tCLQX after the falling edge, going to 'X' in between. A controller that
+-- samples outside the valid window therefore shifts in 'X' and fails the
+-- testbench's data check rather than quietly producing a plausible waveform.
+-- MOSI setup/hold and the chip-select timing are checked directly.
+--
+-- Bit ordering follows the datasheet: on multi-bit transfers the highest
+-- numbered IO carries the most significant bit of each group.
+
+library ieee;
+ use ieee.std_logic_1164.all;
+ use ieee.numeric_std.all;
+
+library vunit_lib;
+ context vunit_lib.vunit_context;
+ context vunit_lib.com_context;
+
+use work.spi_nor_target_vc_pkg.all;
+
+entity spi_nor_target_vc is
+ generic (
+ actor_name : string := "spi_nor_target";
+ -- W25Q01JV AC characteristics. The read data path delays are the
+ -- interesting ones; they are what bounds the controller's usable
+ -- sample window.
+ t_clqv : time := 6 ns; -- clock low to output valid (max)
+ t_clqx : time := 1.5 ns; -- output hold after clock low (min)
+ t_dvch : time := 2 ns; -- data in setup to clock high
+ t_chdx : time := 3 ns; -- data in hold after clock high
+ t_slch : time := 5 ns; -- cs low to first clock high
+ t_shsl : time := 30 ns; -- cs high time between transactions
+ -- Winbond manufacturer id, W25Q01JV device id
+ jedec_id : std_logic_vector(23 downto 0) := x"EF4021";
+ unique_id : std_logic_vector(63 downto 0) := x"0123456789ABCDEF"
+ );
+ port (
+ cs_n : in std_logic;
+ sclk : in std_logic;
+ -- Resolved bus as seen by the part
+ io : in std_logic_vector(3 downto 0);
+ -- Model's contribution to the bus, resolved externally so that
+ -- contention with the controller is visible
+ io_o : out std_logic_vector(3 downto 0) := (others => '0');
+ io_oe : out std_logic_vector(3 downto 0) := (others => '0')
+ );
+end entity;
+
+architecture model of spi_nor_target_vc is
+
+ -- Opcodes this model understands. Anything else is logged and treated as a
+ -- no-operand command.
+ constant OP_WRITE_ENABLE : std_logic_vector(7 downto 0) := x"06";
+ constant OP_WRITE_DISABLE : std_logic_vector(7 downto 0) := x"04";
+ constant OP_READ_STATUS_1 : std_logic_vector(7 downto 0) := x"05";
+ constant OP_READ_STATUS_2 : std_logic_vector(7 downto 0) := x"35";
+ constant OP_READ_STATUS_3 : std_logic_vector(7 downto 0) := x"15";
+ constant OP_WRITE_STATUS_1 : std_logic_vector(7 downto 0) := x"01";
+ constant OP_WRITE_STATUS_2 : std_logic_vector(7 downto 0) := x"31";
+ constant OP_WRITE_STATUS_3 : std_logic_vector(7 downto 0) := x"11";
+ constant OP_READ_DATA : std_logic_vector(7 downto 0) := x"03";
+ constant OP_READ_DATA_4B : std_logic_vector(7 downto 0) := x"13";
+ constant OP_FAST_READ : std_logic_vector(7 downto 0) := x"0B";
+ constant OP_FAST_READ_4B : std_logic_vector(7 downto 0) := x"0C";
+ constant OP_FAST_READ_DUAL : std_logic_vector(7 downto 0) := x"3B";
+ constant OP_FAST_READ_DUAL_4B : std_logic_vector(7 downto 0) := x"3C";
+ constant OP_FAST_READ_QUAD : std_logic_vector(7 downto 0) := x"6B";
+ constant OP_FAST_READ_QUAD_4B : std_logic_vector(7 downto 0) := x"6C";
+ constant OP_PAGE_PROGRAM : std_logic_vector(7 downto 0) := x"02";
+ constant OP_PAGE_PROGRAM_4B : std_logic_vector(7 downto 0) := x"12";
+ constant OP_QUAD_PAGE_PROGRAM : std_logic_vector(7 downto 0) := x"32";
+ constant OP_QUAD_PAGE_PROGRAM_4B : std_logic_vector(7 downto 0) := x"34";
+ constant OP_SECTOR_ERASE : std_logic_vector(7 downto 0) := x"20";
+ constant OP_SECTOR_ERASE_4B : std_logic_vector(7 downto 0) := x"21";
+ constant OP_BLOCK_ERASE_32K : std_logic_vector(7 downto 0) := x"52";
+ constant OP_BLOCK_ERASE_64K : std_logic_vector(7 downto 0) := x"D8";
+ constant OP_BLOCK_ERASE_64K_4B : std_logic_vector(7 downto 0) := x"DC";
+ constant OP_READ_JEDEC_ID : std_logic_vector(7 downto 0) := x"9F";
+ constant OP_READ_UNIQUE_ID : std_logic_vector(7 downto 0) := x"4B";
+ constant OP_DIE_SELECT : std_logic_vector(7 downto 0) := x"C2";
+
+ constant SECTOR_BYTES : natural := 16#1000#;
+ constant BLOCK32_BYTES : natural := 16#8000#;
+ constant BLOCK64_BYTES : natural := 16#10000#;
+ constant PAGE_BYTES : natural := 256;
+
+ type phase_t is (ph_cmd, ph_addr, ph_dummy, ph_rd, ph_wr, ph_done);
+
+ -- Where read data comes from for the current opcode
+ type src_t is (src_mem, src_jedec, src_status, src_uid);
+
+ shared variable mem : flash_mem_t;
+
+ signal clqv : time := t_clqv;
+
+ -- Snapshot of the bits sampled at the last input clock edge, published so
+ -- the hold checker can look at them tCHDX later.
+ signal sample_evt : std_logic := '0';
+ signal sampled_data : std_logic_vector(3 downto 0) := (others => '0');
+ signal sampled_lane : std_logic_vector(3 downto 0) := (others => '0');
+
+ signal dbg_phase : phase_t := ph_cmd;
+ signal dbg_opcode : std_logic_vector(7 downto 0) := (others => '0');
+
+ constant vc_logger : logger_t := get_logger("work:spi_nor_target_vc");
+
+begin
+
+ -- MOSI hold check. The main process publishes what it sampled and on which
+ -- lanes; tCHDX later those lanes must still be holding the same value.
+ hold_check : process is
+
+ variable lanes : std_logic_vector(3 downto 0);
+ variable data : std_logic_vector(3 downto 0);
+
+ begin
+ wait on sample_evt;
+ lanes := sampled_lane;
+ data := sampled_data;
+ wait for t_chdx;
+
+ for i in lanes'range loop
+ if lanes(i) = '1' and cs_n = '0' then
+ if io(i) /= data(i) then
+ error(vc_logger, "Input hold (tCHDX) violated on io(" & to_string(i) &
+ "): sampled " & to_string(data(i)) &
+ " but bus is now " & to_string(io(i)));
+ end if;
+ end if;
+ end loop;
+ end process;
+
+ -- Main protocol engine. Everything lives in one process so that the
+ -- rising-edge sampling and the falling-edge output presentation share
+ -- state with no inter-process delta delay.
+ proto : process (sclk, cs_n) is
+
+ variable phase : phase_t := ph_cmd;
+ variable bit_cnt : natural := 0;
+ variable in_shifter : std_logic_vector(7 downto 0) := (others => '0');
+ variable opcode : std_logic_vector(7 downto 0) := (others => '0');
+ variable addr : unsigned(31 downto 0) := (others => '0');
+ variable addr_left : natural := 0;
+ variable dummy_left : natural := 0;
+ variable in_width : natural := 1;
+ variable out_width : natural := 1;
+ variable out_src : src_t := src_mem;
+ variable rd_idx : natural := 0;
+ variable wr_idx : natural := 0;
+ variable out_shift : std_logic_vector(7 downto 0) := (others => '0');
+ variable out_left : natural := 0;
+ variable status : std_logic_vector(7 downto 0) := x"00";
+ variable erase_len : natural := 0;
+ variable do_erase : boolean := false;
+ -- Set when the host clocks the part in a way that makes the instruction
+ -- invalid. The real part discards such a command silently, so anything
+ -- it would have done has to be suppressed too or the model is more
+ -- forgiving than the hardware and the testbench proves nothing.
+ variable cmd_invalid : boolean := false;
+ variable modelled : boolean := true;
+ variable cs_fall_at : time := 0 ps;
+ variable cs_rise_at : time := 0 ps;
+ variable first_clk : boolean := true;
+ variable t_invalid : time := t_clqx;
+
+ -- The window is far smaller than the real part, so translate a flash
+ -- address into an index the backing store understands. Anything past
+ -- the window reads as erased.
+ impure function mem_addr (a : unsigned) return natural is
+ begin
+ if a < flash_window_bytes then
+ return to_integer(a);
+ else
+ return flash_window_bytes;
+ end if;
+ end function;
+
+ -- Lanes the controller is expected to be driving in the current phase
+ impure function in_lanes return std_logic_vector is
+ begin
+ case in_width is
+ when 4 =>
+ return "1111";
+ when 2 =>
+ return "0011";
+ when others =>
+ return "0001";
+ end case;
+ end function;
+
+ -- Setup check, split out so every call site uses a literal index and
+ -- the attribute prefix stays a static signal name.
+ procedure check_setup (constant idx : natural; constant since : time) is
+ begin
+ if since < t_dvch then
+ error(vc_logger, "Input setup (tDVCH) violated on io(" & to_string(idx) &
+ "): last changed " & to_string(since) & " before sclk");
+ end if;
+ end procedure;
+
+ -- Next byte the part would present, from whichever source the current
+ -- opcode selected
+ impure function next_out_byte return std_logic_vector is
+
+ variable b : std_logic_vector(7 downto 0);
+ variable k : natural;
+
+ begin
+ case out_src is
+ when src_jedec =>
+ -- 3 id bytes, repeating for as long as the host clocks
+ k := rd_idx mod 3;
+ b := jedec_id(23 - 8 * k downto 16 - 8 * k);
+ when src_status =>
+ b := status;
+ when src_uid =>
+ if rd_idx < 8 then
+ b := unique_id(63 - 8 * rd_idx downto 56 - 8 * rd_idx);
+ else
+ b := x"FF";
+ end if;
+ when src_mem =>
+ b := mem.get(mem_addr(addr + rd_idx));
+ end case;
+
+ rd_idx := rd_idx + 1;
+ return b;
+ end function;
+
+ -- Decode the opcode into the phase sequence and bus widths
+ procedure decode is
+ begin
+ in_width := 1;
+ out_width := 1;
+ out_src := src_mem;
+ rd_idx := 0;
+ wr_idx := 0;
+ addr := (others => '0');
+ addr_left := 0;
+ dummy_left := 0;
+ do_erase := false;
+ erase_len := 0;
+
+ case opcode is
+ when OP_READ_JEDEC_ID =>
+ out_src := src_jedec;
+ phase := ph_rd;
+ when OP_READ_UNIQUE_ID =>
+ out_src := src_uid;
+ dummy_left := 32;
+ phase := ph_dummy;
+ when OP_READ_STATUS_1 | OP_READ_STATUS_2 | OP_READ_STATUS_3 =>
+ out_src := src_status;
+ phase := ph_rd;
+ when OP_WRITE_STATUS_1 | OP_WRITE_STATUS_2 | OP_WRITE_STATUS_3 =>
+ phase := ph_wr;
+ when OP_WRITE_ENABLE =>
+ status := status or x"02";
+ phase := ph_done;
+ when OP_WRITE_DISABLE =>
+ status := status and x"FD";
+ phase := ph_done;
+ when OP_DIE_SELECT =>
+ phase := ph_wr;
+ when OP_READ_DATA =>
+ addr_left := 3;
+ phase := ph_addr;
+ when OP_READ_DATA_4B =>
+ addr_left := 4;
+ phase := ph_addr;
+ when OP_FAST_READ =>
+ addr_left := 3;
+ dummy_left := 8;
+ phase := ph_addr;
+ when OP_FAST_READ_4B =>
+ addr_left := 4;
+ dummy_left := 8;
+ phase := ph_addr;
+ when OP_FAST_READ_DUAL =>
+ addr_left := 3;
+ dummy_left := 8;
+ out_width := 2;
+ phase := ph_addr;
+ when OP_FAST_READ_DUAL_4B =>
+ addr_left := 4;
+ dummy_left := 8;
+ out_width := 2;
+ phase := ph_addr;
+ when OP_FAST_READ_QUAD =>
+ addr_left := 3;
+ dummy_left := 8;
+ out_width := 4;
+ phase := ph_addr;
+ when OP_FAST_READ_QUAD_4B =>
+ addr_left := 4;
+ dummy_left := 8;
+ out_width := 4;
+ phase := ph_addr;
+ when OP_PAGE_PROGRAM =>
+ addr_left := 3;
+ phase := ph_addr;
+ when OP_PAGE_PROGRAM_4B =>
+ addr_left := 4;
+ phase := ph_addr;
+ when OP_QUAD_PAGE_PROGRAM =>
+ addr_left := 3;
+ phase := ph_addr;
+ when OP_QUAD_PAGE_PROGRAM_4B =>
+ addr_left := 4;
+ phase := ph_addr;
+ when OP_SECTOR_ERASE =>
+ addr_left := 3;
+ erase_len := SECTOR_BYTES;
+ phase := ph_addr;
+ when OP_SECTOR_ERASE_4B =>
+ addr_left := 4;
+ erase_len := SECTOR_BYTES;
+ phase := ph_addr;
+ when OP_BLOCK_ERASE_32K =>
+ addr_left := 3;
+ erase_len := BLOCK32_BYTES;
+ phase := ph_addr;
+ when OP_BLOCK_ERASE_64K =>
+ addr_left := 3;
+ erase_len := BLOCK64_BYTES;
+ phase := ph_addr;
+ when OP_BLOCK_ERASE_64K_4B =>
+ addr_left := 4;
+ erase_len := BLOCK64_BYTES;
+ phase := ph_addr;
+ when others =>
+ info(vc_logger, "Unmodelled opcode 0x" & to_hstring(opcode) &
+ ", treating as no-operand");
+ -- Don't police the clock count for something we can't decode
+ modelled := false;
+ phase := ph_done;
+ end case;
+ end procedure;
+
+ -- What follows the address phase for this opcode
+ procedure after_addr is
+ begin
+ if dummy_left > 0 then
+ phase := ph_dummy;
+ elsif erase_len > 0 then
+ do_erase := true;
+ phase := ph_done;
+ elsif opcode = OP_READ_DATA or opcode = OP_READ_DATA_4B then
+ phase := ph_rd;
+ elsif opcode = OP_PAGE_PROGRAM or opcode = OP_PAGE_PROGRAM_4B then
+ phase := ph_wr;
+ elsif opcode = OP_QUAD_PAGE_PROGRAM or opcode = OP_QUAD_PAGE_PROGRAM_4B then
+ in_width := 4;
+ phase := ph_wr;
+ else
+ phase := ph_done;
+ end if;
+ end procedure;
+
+ -- Programs wrap within the 256 byte page rather than running on
+ procedure program_byte (constant data : std_logic_vector(7 downto 0)) is
+
+ variable page_base : unsigned(31 downto 0);
+ variable offset : natural;
+
+ begin
+ page_base := addr(31 downto 8) & x"00";
+ offset := (to_integer(addr(7 downto 0)) + wr_idx) mod PAGE_BYTES;
+ mem.set(mem_addr(page_base + offset), data);
+ wr_idx := wr_idx + 1;
+ end procedure;
+
+ begin
+ if cs_n /= '0' then
+ if cs_n = '1' and cs_n'event then
+ cs_rise_at := now;
+
+ -- The part latches an instruction only if cs_n rises on a byte
+ -- boundary. Trailing clocks that leave a partial byte make the
+ -- whole command invalid, and it is discarded without any
+ -- indication -- an erase simply does not happen.
+ if bit_cnt /= 0 then
+ cmd_invalid := true;
+ error(vc_logger, "cs_n rose " & to_string(bit_cnt) &
+ " bits into a byte, instruction 0x" &
+ to_hstring(opcode) & " is discarded");
+ end if;
+ end if;
+
+ -- Deselected: finish any pending erase, then reset per-transaction
+ -- state. Erases are modelled as instantaneous on deselect.
+ if do_erase then
+ if not cmd_invalid then
+ mem.erase((mem_addr(addr) / erase_len) * erase_len, erase_len);
+ end if;
+ do_erase := false;
+ end if;
+
+ cmd_invalid := false;
+ modelled := true;
+ phase := ph_cmd;
+ bit_cnt := 0;
+ in_width := 1;
+ out_width := 1;
+ out_left := 0;
+ first_clk := true;
+ in_shifter := (others => '0');
+ io_oe <= (others => '0');
+ io_o <= (others => '0');
+ else
+ if cs_n = '0' and cs_n'event then
+ cs_fall_at := now;
+
+ -- tSHSL: the part needs the select to stay high for a while
+ -- between transactions. Back to back page reads are the case
+ -- that trips this.
+ if cs_rise_at > 0 ps and now - cs_rise_at < t_shsl then
+ error(vc_logger, "cs_n high time (tSHSL) violated: " &
+ to_string(now - cs_rise_at) & " < " & to_string(t_shsl));
+ end if;
+ end if;
+
+ if rising_edge(sclk) then
+ if first_clk then
+ first_clk := false;
+ if now - cs_fall_at < t_slch then
+ error(vc_logger, "cs_n to first sclk (tSLCH) violated: " &
+ to_string(now - cs_fall_at) & " < " & to_string(t_slch));
+ end if;
+ end if;
+
+ case phase is
+ when ph_cmd | ph_addr | ph_wr =>
+ -- Setup check on the lanes the controller owns, then
+ -- shift the group in. Highest lane is the msb.
+ check_setup(0, io(0)'last_event);
+ if in_width >= 2 then
+ check_setup(1, io(1)'last_event);
+ end if;
+ if in_width = 4 then
+ check_setup(2, io(2)'last_event);
+ check_setup(3, io(3)'last_event);
+ end if;
+
+ sampled_data <= io;
+ sampled_lane <= in_lanes;
+ sample_evt <= not sample_evt;
+
+ if in_width = 4 then
+ in_shifter := in_shifter(3 downto 0) & io(3) & io(2) & io(1) & io(0);
+ elsif in_width = 2 then
+ in_shifter := in_shifter(5 downto 0) & io(1) & io(0);
+ else
+ in_shifter := in_shifter(6 downto 0) & io(0);
+ end if;
+ bit_cnt := bit_cnt + in_width;
+
+ if bit_cnt = 8 then
+ bit_cnt := 0;
+
+ case phase is
+ when ph_cmd =>
+ opcode := in_shifter;
+ decode;
+ when ph_addr =>
+ addr := addr(23 downto 0) & unsigned(in_shifter);
+ addr_left := addr_left - 1;
+ if addr_left = 0 then
+ after_addr;
+ end if;
+ when ph_wr =>
+ if opcode = OP_DIE_SELECT or
+ opcode = OP_WRITE_STATUS_1 or
+ opcode = OP_WRITE_STATUS_2 or
+ opcode = OP_WRITE_STATUS_3 then
+ status := in_shifter;
+ else
+ program_byte(in_shifter);
+ end if;
+ when others =>
+ null;
+ end case;
+ end if;
+
+ when ph_dummy =>
+ dummy_left := dummy_left - 1;
+ if dummy_left = 0 then
+ -- Data starts on the next falling edge
+ phase := ph_rd;
+ out_left := 0;
+ end if;
+
+ when ph_rd =>
+ null;
+
+ when ph_done =>
+ -- The instruction and its operands are complete, so the
+ -- host should have raised cs_n by now. Clocking on makes
+ -- the command invalid: an erase or a write enable that
+ -- gets trailing clocks is thrown away, which shows up
+ -- much later as "the sector did not erase".
+ if modelled and not cmd_invalid then
+ cmd_invalid := true;
+ error(vc_logger, "sclk continued after instruction 0x" &
+ to_hstring(opcode) &
+ " completed, command is discarded");
+ end if;
+ end case;
+ elsif falling_edge(sclk) then
+ if phase = ph_rd then
+ if out_left = 0 then
+ out_shift := next_out_byte;
+ out_left := 8;
+ end if;
+
+ -- Hold the previous value until tCLQX, go invalid, then
+ -- present the new group at tCLQV. The invalid gap is what
+ -- makes the controller's sample point matter.
+ t_invalid := t_clqx;
+ if t_invalid >= clqv then
+ t_invalid := clqv / 2;
+ end if;
+
+ case out_width is
+ when 4 =>
+ io_o <= "XXXX" after t_invalid,
+ out_shift(7 downto 4) after clqv;
+ io_oe <= "1111" after t_invalid;
+ out_shift := out_shift(3 downto 0) & x"F";
+ out_left := out_left - 4;
+ when 2 =>
+ io_o(1 downto 0) <= "XX" after t_invalid,
+ out_shift(7 downto 6) after clqv;
+ io_oe <= "0011" after t_invalid;
+ out_shift := out_shift(5 downto 0) & "11";
+ out_left := out_left - 2;
+ when others =>
+ io_o(1) <= 'X' after t_invalid,
+ out_shift(7) after clqv;
+ io_oe <= "0010" after t_invalid;
+ out_shift := out_shift(6 downto 0) & '1';
+ out_left := out_left - 1;
+ end case;
+ else
+ io_oe <= (others => '0');
+ end if;
+ end if;
+ end if;
+
+ dbg_phase <= phase;
+ dbg_opcode <= opcode;
+ end process;
+
+ -- Backdoor access to the memory array for preload and readback
+ msg_handler : process is
+
+ variable self : actor_t;
+ variable msg_type : msg_type_t;
+ variable request_msg : msg_t;
+ variable reply_msg : msg_t;
+ variable addr : natural;
+ variable len : natural;
+ variable data : std_logic_vector(7 downto 0);
+
+ begin
+ self := new_actor(actor_name);
+
+ loop
+ receive(net, self, request_msg);
+ msg_type := message_type(request_msg);
+ reply_msg := new_msg;
+
+ if msg_type = flash_fill_pattern_msg then
+ mem.fill_pattern;
+ elsif msg_type = flash_write_byte_msg then
+ addr := pop(request_msg);
+ data := pop(request_msg);
+ -- Backdoor writes are absolute, not and-ed like a real program
+ mem.erase(addr, 1);
+ mem.set(addr, data);
+ elsif msg_type = flash_read_byte_msg then
+ addr := pop(request_msg);
+ push(reply_msg, mem.get(addr));
+ elsif msg_type = flash_erase_msg then
+ addr := pop(request_msg);
+ len := pop(request_msg);
+ mem.erase(addr, len);
+ elsif msg_type = flash_set_clqv_msg then
+ len := pop(request_msg);
+ clqv <= len * 1 ps;
+ info(vc_logger, "tCLQV set to " & to_string(len * 1 ps));
+ else
+ unexpected_msg_type(msg_type);
+ end if;
+
+ reply(net, request_msg, reply_msg);
+ end loop;
+ end process;
+
+end model;
diff --git a/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc_pkg.vhd b/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc_pkg.vhd
new file mode 100644
index 00000000..5372e866
--- /dev/null
+++ b/hdl/ip/vhd/vunit_components/spi_nor_target/spi_nor_target_vc_pkg.vhd
@@ -0,0 +1,227 @@
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this
+-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
+--
+-- Message definitions and backing store for the SPI NOR flash target VC.
+--
+-- The VC models a Winbond W25Q-family QSPI NOR flash closely enough to check
+-- both data integrity and the AC timing relationships that bound how fast the
+-- controller's sclk can run. The interesting knob for margin testing is
+-- set_output_valid_delay(), which walks the modelled tCLQV so a testbench can
+-- prove the controller samples correctly across the whole datasheet range
+-- rather than just at one nominal value.
+
+library ieee;
+ use ieee.std_logic_1164.all;
+ use ieee.numeric_std.all;
+
+library vunit_lib;
+ context vunit_lib.vunit_context;
+ context vunit_lib.com_context;
+
+package spi_nor_target_vc_pkg is
+
+ -- Size of the modelled memory window. The real part is 1Gbit, which we
+ -- have no interest in allocating, so reads outside the window return
+ -- erased (0xFF) data and writes outside it are dropped.
+ constant flash_window_bytes : natural := 16#10000#;
+
+ -- Deterministic address -> data mapping used by fill_pattern. Multiplying
+ -- the low address byte by an odd constant makes this a bijection over any
+ -- aligned 256 byte run, so an off-by-one address or a dropped bit always
+ -- shows up as a different byte rather than aliasing to the same value.
+ function pattern_byte (addr : natural) return std_logic_vector;
+
+ type flash_mem_t is protected
+
+ procedure set (addr : natural; data : std_logic_vector(7 downto 0));
+ impure function get (addr : natural) return std_logic_vector;
+ procedure erase (addr : natural; len : natural);
+ procedure fill_pattern;
+
+ end protected;
+
+ -- Message types
+ constant flash_fill_pattern_msg : msg_type_t := new_msg_type("flash_fill_pattern");
+ constant flash_write_byte_msg : msg_type_t := new_msg_type("flash_write_byte");
+ constant flash_read_byte_msg : msg_type_t := new_msg_type("flash_read_byte");
+ constant flash_erase_msg : msg_type_t := new_msg_type("flash_erase");
+ constant flash_set_clqv_msg : msg_type_t := new_msg_type("flash_set_clqv");
+
+ -- Fill the whole modelled window with pattern_byte(addr).
+ procedure fill_pattern (
+ signal net : inout network_t;
+ constant actor : actor_t
+ );
+
+ procedure write_flash_byte (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ constant data : std_logic_vector(7 downto 0)
+ );
+
+ procedure read_flash_byte (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ variable data : out std_logic_vector(7 downto 0)
+ );
+
+ procedure erase_flash (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ constant len : natural
+ );
+
+ -- Override the modelled clock-low-to-output-valid delay (tCLQV). Used to
+ -- sweep the controller's rx sample point margin at a fixed sclk rate.
+ procedure set_output_valid_delay (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant delay : time
+ );
+
+end package;
+
+package body spi_nor_target_vc_pkg is
+
+ function pattern_byte (addr : natural) return std_logic_vector is
+
+ variable a : unsigned(31 downto 0);
+ variable v : unsigned(7 downto 0);
+
+ begin
+ a := to_unsigned(addr, 32);
+ v := resize(a(7 downto 0) * 7, 8) xor
+ a(15 downto 8) xor
+ resize(a(23 downto 16) * 3, 8) xor
+ x"A5";
+ return std_logic_vector(v);
+ end function;
+
+ type flash_mem_t is protected body
+
+ type mem_arr_t is array (0 to flash_window_bytes - 1) of std_logic_vector(7 downto 0);
+
+ variable mem : mem_arr_t := (others => x"FF");
+
+ procedure set (addr : natural; data : std_logic_vector(7 downto 0)) is
+ begin
+ if addr < flash_window_bytes then
+ -- Real NOR can only clear bits on a program, it cannot set
+ -- them. Modelling that catches a missing erase.
+ mem(addr) := mem(addr) and data;
+ end if;
+ end procedure;
+
+ impure function get (addr : natural) return std_logic_vector is
+ begin
+ if addr < flash_window_bytes then
+ return mem(addr);
+ else
+ return x"FF";
+ end if;
+ end function;
+
+ procedure erase (addr : natural; len : natural) is
+ begin
+ for i in addr to addr + len - 1 loop
+ if i < flash_window_bytes then
+ mem(i) := x"FF";
+ end if;
+ end loop;
+ end procedure;
+
+ procedure fill_pattern is
+ begin
+ for i in mem'range loop
+ mem(i) := pattern_byte(i);
+ end loop;
+ end procedure;
+
+ end protected body;
+
+ procedure fill_pattern (
+ signal net : inout network_t;
+ constant actor : actor_t
+ ) is
+
+ variable request_msg : msg_t := new_msg(flash_fill_pattern_msg);
+ variable reply_msg : msg_t;
+
+ begin
+ request(net, actor, request_msg, reply_msg);
+ delete(reply_msg);
+ end procedure;
+
+ procedure write_flash_byte (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ constant data : std_logic_vector(7 downto 0)
+ ) is
+
+ variable request_msg : msg_t := new_msg(flash_write_byte_msg);
+ variable reply_msg : msg_t;
+
+ begin
+ push(request_msg, addr);
+ push(request_msg, data);
+ request(net, actor, request_msg, reply_msg);
+ delete(reply_msg);
+ end procedure;
+
+ procedure read_flash_byte (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ variable data : out std_logic_vector(7 downto 0)
+ ) is
+
+ variable request_msg : msg_t := new_msg(flash_read_byte_msg);
+ variable reply_msg : msg_t;
+
+ begin
+ push(request_msg, addr);
+ request(net, actor, request_msg, reply_msg);
+ data := pop(reply_msg);
+ delete(reply_msg);
+ end procedure;
+
+ procedure erase_flash (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant addr : natural;
+ constant len : natural
+ ) is
+
+ variable request_msg : msg_t := new_msg(flash_erase_msg);
+ variable reply_msg : msg_t;
+
+ begin
+ push(request_msg, addr);
+ push(request_msg, len);
+ request(net, actor, request_msg, reply_msg);
+ delete(reply_msg);
+ end procedure;
+
+ procedure set_output_valid_delay (
+ signal net : inout network_t;
+ constant actor : actor_t;
+ constant delay : time
+ ) is
+
+ variable request_msg : msg_t := new_msg(flash_set_clqv_msg);
+ variable reply_msg : msg_t;
+
+ begin
+ -- Passed as an integer count of ps rather than a time so we don't
+ -- depend on queue_pkg's time overloads being present.
+ push(request_msg, delay / 1 ps);
+ request(net, actor, request_msg, reply_msg);
+ delete(reply_msg);
+ end procedure;
+
+end package body;
diff --git a/hdl/projects/cosmo_seq/cosmo_seq_pins.xdc b/hdl/projects/cosmo_seq/cosmo_seq_pins.xdc
index c34a107f..56d12080 100644
--- a/hdl/projects/cosmo_seq/cosmo_seq_pins.xdc
+++ b/hdl/projects/cosmo_seq/cosmo_seq_pins.xdc
@@ -226,7 +226,7 @@ set_property -dict { PACKAGE_PIN N1 IOSTANDARD LVCMOS18 } [get_ports { spi0_sp5_
set_property -dict { PACKAGE_PIN P3 IOSTANDARD LVCMOS18 } [get_ports { spi1_sp5_to_fpga1_cs_l }];
set_property -dict { PACKAGE_PIN P2 IOSTANDARD LVCMOS18 } [get_ports { spi2_sp5_to_fpga1_cs_l }];
set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga1_to_flash_clk }];
-set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga1_to_flash_cs_l }];
+set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga1_to_flash_cs_l }];
set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga1_to_flash_dat[0] }];
set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga1_to_flash_dat[1] }];
set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga1_to_flash_dat[2] }];
diff --git a/hdl/projects/cosmo_seq/cosmo_timing.xdc b/hdl/projects/cosmo_seq/cosmo_timing.xdc
index 92a42b73..8dae48f1 100644
--- a/hdl/projects/cosmo_seq/cosmo_timing.xdc
+++ b/hdl/projects/cosmo_seq/cosmo_timing.xdc
@@ -171,4 +171,119 @@ set_false_path -from [get_ports {*}] -to [get_ports {fpga1_spare_v1p8[*]}]
# This is a stop-gap to provide some kind of output timing constraints per the eSPI base spec
set_max_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 6
-set_min_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 0
\ No newline at end of file
+set_min_delay -to [get_ports espi0_sp5_to_fpga1_dat[*]] 0
+
+# #######################
+# SPI NOR flash interface (Winbond W25Q01JV)
+# #######################
+# sclk is toggled by fabric logic off clk_125m at clk/2, so 62.5MHz, a 16ns
+# period with an 8ns half period. Nothing inside the FPGA is clocked by it, so
+# there is deliberately no create_generated_clock here: what actually has to be
+# bounded is the clock-to-data skew leaving the FPGA and the pin-to-flop delay
+# coming back, and both are directly constrainable.
+#
+# Trace delays are short and local; using the same 6.8ns/m as the FMC block
+# above.
+# On cosmo flash trace min is 31.982mm (Dat0), and max is 33.436 (Dat3)
+# so min = 0.031982m * 6.8ns/m = 0.217
+# max = 0.033436 * 6.8ns/m = 0.227
+set flash_trace_max 0.227
+set flash_trace_min 0.217
+
+# Pull the launch flops into the IOBs. Every one of these is a dedicated
+# duplicate whose only load is its pin (see spi_clk_gen's sclk_pin and
+# spi_txn_mgr's cs_n_pin), which is what makes packing legal. It matters a lot:
+# left in the fabric the placer put them wherever it liked and measured 12 to 13
+# ns of routing to the pin, which both blew the clock-to-data skew budget and
+# pushed the read round trip past every available sample point. In the IOB the
+# delay is small, deterministic, and the same for all four.
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/clk_gen/sclk_pin_reg}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/spi_txn_mgr_inst/cs_n_pin_reg}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_o_reg[*]}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_cap_*_reg[*]}]
+
+# #################
+# Outputs: sclk, and dat[] during the instruction, address and write phases.
+# cs_n is handled separately below.
+#
+# The part samples mosi on the sclk rising edge, and the FPGA launches both mosi
+# and the sclk falling edge from the same clk edge. So the flash sees a full half
+# period of setup, less whatever skew the IOBs and routing add between the clock
+# pin and the data pins:
+#
+# skew_budget = half_period - tDVCH = 8.0 - 2.0 = 6.0 ns
+#
+# Constraining all of these pins into one delay window makes the worst-case skew
+# the difference between the two bounds, which is the quantity that matters here:
+# the same clock insertion delay applies to every one of these launch flops, so it
+# cancels out of the skew and only the window width has to fit the budget.
+#
+# Note the window looks wide (4.5ns against a 6ns budget) for four pins that are
+# all IOB-packed and launched off the same clk edge. That is because max and min
+# delay checks compare the slow corner of one path against the fast corner of
+# another, so most of the width is process/voltage/temperature spread rather than
+# pin-to-pin skew, which is well under a nanosecond here. The bound is a tripwire
+# against a pin losing its IOB or picking up extra logic, not a skew estimate.
+#
+# Hold is not a concern for the part: mosi is held until the following falling
+# edge, 8ns after the sampling edge, against a tCHDX of 3ns.
+#
+# These numbers assume the IOB packing above. Packed, the flop-to-pin delay is
+# about 3.3ns and essentially all of it is logic -- 0.001ns of routing -- so a
+# tight window is both meetable and meaningful. Left in the fabric the same paths
+# measured 12 to 13ns of routing and varied by several ns between builds.
+set_max_delay 5.0 -to [get_ports {spi_fpga1_to_flash_clk \
+ spi_fpga1_to_flash_dat[*]}]
+set_min_delay 0.5 -to [get_ports {spi_fpga1_to_flash_clk \
+ spi_fpga1_to_flash_dat[*]}]
+
+# Two things are deliberately outside that window, because pulling them into it
+# would make the placer work hard on paths that have an order of magnitude more
+# real slack than the data pins do:
+#
+# cs_n only has to be low before the first sclk edge and stay low after
+# the last. spi_txn_mgr spends cs_setup_cnts = 4 clk cycles, 32ns,
+# on each, against tSLCH/tCHSH of 5ns.
+# the tristate carries no data and only has to have settled before the part
+# enable starts driving, which release_lanes gives it a full sclk cycle
+# to do.
+set_max_delay 16.0 -to [get_ports spi_fpga1_to_flash_cs_l]
+set_max_delay 16.0 -from [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] \
+ -to [get_ports spi_fpga1_to_flash_dat[*]]
+
+# #################
+# Inputs: dat[] during read phases.
+#
+# The controller samples read data at a fixed point S after the sclk rising edge,
+# set by the rx_sample_taps generic on spi_nor_top. S has to satisfy
+#
+# round_trip_valid - half_period <= S <= half_period + round_trip_hold
+#
+# where round_trip_valid is built from the part's tCLQV and round_trip_hold from
+# its tCLQX. Note the upper limit comes from tCLQX, not tCLQV: sampling too late
+# catches the next bit rather than the current one.
+#
+# With the IOB packing and the output bounds above:
+# flop to sclk pin 3.30 max 1.50 min
+# sclk trace 0.40 0.10
+# flash tCLQV / tCLQX 6.00 1.50
+# data trace back 0.40 0.10
+# pin to capture flop 1.50 0.50
+# ----- -----
+# round_trip_valid max 11.60 round_trip_hold min 3.70
+#
+# so at an 8ns half period S has to land in 3.6 .. 11.7ns. rx_sample_taps = 2 puts
+# it at 8ns, about 4ns clear of either limit. spi_nor_fast_tb and
+# spi_nor_fast_quick_io_tb model these delays and check both corners.
+#
+# If reads are marginal on hardware, sweep rx_sample_taps before assuming anything
+# else is wrong; taps are 4ns apart so 1 and 3 bracket the shipped value.
+#
+# -datapath_only because this is a pin to flop propagation bound, not a
+# synchronous transfer: without it Vivado charges the MMCM's clock insertion
+# delay against the budget and the check becomes meaningless.
+#
+# The dedicated capture flops in spi_link are the only loads on these pins, so
+# these paths are exactly the pin-to-flop delay.
+set_max_delay 3.0 -datapath_only -from [get_ports spi_fpga1_to_flash_dat[*]]
\ No newline at end of file
diff --git a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd
index 8154fdb3..47c167a4 100644
--- a/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd
+++ b/hdl/projects/cosmo_seq/sp5_espi_flash_subsystem/sp5_espi_flash_subsystem.vhd
@@ -152,6 +152,23 @@ begin
spi_nor_top_inst: entity work.spi_nor_top
+ generic map(
+ -- 125MHz / 2 = 62.5MHz sclk. This is as fast as the block goes off
+ -- clk_125m, and it is the ceiling for a fixed sample point: the
+ -- round trip out to the flash and back has to land within half an
+ -- sclk period of rx_sample_taps, and above this rate that window
+ -- closes. Faster would need per-lane IDELAY read training.
+ sclk_divisor => 0,
+ -- Sample 8ns after the sclk rising edge. Taps are in half-clk (4ns)
+ -- steps. With the flash IO flops packed into the IOBs the round trip
+ -- out and back is bounded to roughly 3.7..11.6ns, which puts the
+ -- usable sample window at 3.6..11.7ns; 8ns sits about 4ns clear of
+ -- either end. cosmo_timing.xdc carries the arithmetic. Sweep this on
+ -- hardware if reads come back corrupted.
+ rx_sample_taps => 2,
+ cs_setup_cnts => 4,
+ cs_high_cnts => 7
+ )
port map(
clk => clk_125m,
reset => reset_125m,
diff --git a/hdl/projects/grapefruit/grapefruit_pins.xdc b/hdl/projects/grapefruit/grapefruit_pins.xdc
index ecee16c9..730a1831 100644
--- a/hdl/projects/grapefruit/grapefruit_pins.xdc
+++ b/hdl/projects/grapefruit/grapefruit_pins.xdc
@@ -88,12 +88,12 @@ set_property -dict { PACKAGE_PIN C15 IOSTANDARD LVCMOS33 } [get_ports { sp_to_se
set_property -dict { PACKAGE_PIN B16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[2] }];
set_property -dict { PACKAGE_PIN B15 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[1] }];
set_property -dict { PACKAGE_PIN C16 IOSTANDARD LVCMOS33 } [get_ports { seq_rev_id[0] }];
-set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_cs_l }];
-set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_clk }];
-set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_dat[3] }];
-set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_dat[2] }];
-set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_dat[1] }];
-set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash_dat[0] }];
+set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_cs_l }];
+set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_clk }];
+set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_dat[3] }];
+set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_dat[2] }];
+set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_dat[1] }];
+set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 SLEW FAST} [get_ports { spi_fpga_to_flash_dat[0] }];
set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash2_cs_l }];
set_property -dict { PACKAGE_PIN A19 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash2_clk }];
set_property -dict { PACKAGE_PIN A20 IOSTANDARD LVCMOS33 } [get_ports { spi_fpga_to_flash2_dat[3] }];
diff --git a/hdl/projects/grapefruit/grapefruit_timing.xdc b/hdl/projects/grapefruit/grapefruit_timing.xdc
index f6c31359..395863df 100644
--- a/hdl/projects/grapefruit/grapefruit_timing.xdc
+++ b/hdl/projects/grapefruit/grapefruit_timing.xdc
@@ -69,4 +69,36 @@ set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out*/C}] -to [
set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -setup 2
set_multicycle_path -from [get_pins {stm32h7_fmc_target_inst/data_out_en_reg*/C}] -to [get_ports {fmc_sp_to_fpga_da[*]}] -hold 1
+# #######################
+# SPI NOR flash interface (Winbond W25Q01JV)
+# #######################
+# sclk is fabric-toggled off clk_125m at clk/2, so 62.5MHz with an 8ns half
+# period. See cosmo_timing.xdc for the full derivation; the short version is that
+# the outputs are bound to a common delay window so the clock-to-data skew at the
+# part stays inside half_period - tDVCH = 6ns, and the return path is bound so
+# the round trip stays inside the sample window that rx_sample_taps picks.
+#
+# TODO: replace the trace numbers with measured lengths for this board.
+
+# Launch and capture flops into the IOBs; see cosmo_timing.xdc for why this is
+# load-bearing rather than a nicety.
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/clk_gen/sclk_pin_reg}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/spi_txn_mgr_inst/cs_n_pin_reg}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_o_reg[*]}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}]
+set_property IOB TRUE [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_cap_*_reg[*]}]
+
+set_max_delay 5.0 -to [get_ports {spi_fpga_to_flash_clk \
+ spi_fpga_to_flash_dat[*]}]
+set_min_delay 0.5 -to [get_ports {spi_fpga_to_flash_clk \
+ spi_fpga_to_flash_dat[*]}]
+
+# cs_n and the tristate enable are exempted from the data window; both have an
+# order of magnitude more real slack than the data pins. See cosmo_timing.xdc.
+set_max_delay 16.0 -to [get_ports spi_fpga_to_flash_cs_l]
+set_max_delay 16.0 -from [get_cells -hier -filter {NAME =~ *spi_nor_top_inst/link/io_oe_reg[*]}] \
+ -to [get_ports spi_fpga_to_flash_dat[*]]
+
+set_max_delay 3.0 -datapath_only -from [get_ports spi_fpga_to_flash_dat[*]]
+
diff --git a/hdl/projects/grapefruit/grapefruit_top.vhd b/hdl/projects/grapefruit/grapefruit_top.vhd
index 5d70aad9..c0eaf1a1 100644
--- a/hdl/projects/grapefruit/grapefruit_top.vhd
+++ b/hdl/projects/grapefruit/grapefruit_top.vhd
@@ -338,6 +338,12 @@ begin
resize_axil(fabric_responders(1), responders_8b(1));
spi_nor_top_inst: entity work.spi_nor_top
+ generic map(
+ -- 125MHz / 2 = 62.5MHz sclk; see sp5_espi_flash_subsystem for why this
+ -- is the ceiling and how rx_sample_taps relates to the board round trip.
+ sclk_divisor => 0,
+ rx_sample_taps => 2
+ )
port map(
clk => clk_125m,
reset => reset_125m,