From 5430277b37d1cbf1f9d2257eb8ea3468489129da Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:24:19 -0700 Subject: [PATCH 1/5] feat(gtx7): add cPllRefClkLostOut and WAIT_TIME_CDRLOCK_G to Gtx7Core Two independent Gtx7Core additions, both backward compatible. cPllRefClkLostOut exposes CPLLREFCLKLOST from the GTXE2_CHANNEL, which the core already consumed internally but never brought out to a caller. WAIT_TIME_CDRLOCK_G decouples the CDR-lock wait from the primitive's simulation speedup. SIM_GTRESET_SPEEDUP_G previously fed both the WAIT_TIME_CDRLOCK_C fabric constant and the GTXE2_CHANNEL SIM_RESET_SPEEDUP attribute, so a simulation could not select the CDR-lock wait independently of the primitive's own reset speedup. The new integer generic defaults to -1, which reproduces the previous derivation exactly and changes no elaborated behavior for any instance that leaves it at the default. Any value of 0 or greater is used directly as the stable-clock cycle count for WAIT_TIME_CDRLOCK_C. SIM_RESET_SPEEDUP continues to consume SIM_GTRESET_SPEEDUP_G unmodified. --- xilinx/7Series/gtx7/rtl/Gtx7Core.vhd | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd index 4a0a48dc26..eede82ad49 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd @@ -27,8 +27,9 @@ entity Gtx7Core is TPD_G : time := 1 ns; -- Sim Generics -- - SIM_GTRESET_SPEEDUP_G : string := "FALSE"; - SIM_VERSION_G : string := "4.0"; + SIM_GTRESET_SPEEDUP_G : string := "FALSE"; + SIM_VERSION_G : string := "4.0"; + WAIT_TIME_CDRLOCK_G : integer := -1; -- -1: use the legacy SIM_GTRESET_SPEEDUP_G derivation; >=0: stable-clock cycle count used directly SIMULATION_G : boolean := false; @@ -157,8 +158,9 @@ entity Gtx7Core is port ( stableClkIn : in sl; -- Freerunning clock needed to drive reset logic - cPllRefClkIn : in sl := '0'; -- Drives CPLL if used - cPllLockOut : out sl; + cPllRefClkIn : in sl := '0'; -- Drives CPLL if used + cPllLockOut : out sl; + cPllRefClkLostOut : out sl; -- CPLLREFCLKLOST from the GTXE2_CHANNEL qPllRefClkIn : in sl := '0'; -- Signals from QPLL if used qPllClkIn : in sl := '0'; @@ -276,7 +278,7 @@ architecture rtl of Gtx7Core is constant RX_DATA_WIDTH_C : integer := getDataWidth(RX_8B10B_EN_G, RX_EXT_DATA_WIDTH_G); constant TX_DATA_WIDTH_C : integer := getDataWidth(TX_8B10B_EN_G, TX_EXT_DATA_WIDTH_G); - constant WAIT_TIME_CDRLOCK_C : integer := ite(SIM_GTRESET_SPEEDUP_G = "TRUE", 16, 65520); + constant WAIT_TIME_CDRLOCK_C : integer := ite(WAIT_TIME_CDRLOCK_G >= 0, WAIT_TIME_CDRLOCK_G, ite(SIM_GTRESET_SPEEDUP_G = "TRUE", 16, 65520)); constant RX_INT_DATAWIDTH_C : integer := (RX_INT_DATA_WIDTH_G/32); constant TX_INT_DATAWIDTH_C : integer := (TX_INT_DATA_WIDTH_G/32); @@ -390,7 +392,8 @@ begin rxOutClkOut <= rxOutClkBufg; - cPllLockOut <= cPllLock; + cPllLockOut <= cPllLock; + cPllRefClkLostOut <= cPllRefClkLost; -------------------------------------------------------------------------------------------------- -- PLL Resets. Driven from TX Rst if both use same PLL From 47d9c457fe9e6fc6a5d79492e424593e8599a5e8 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:24:45 -0700 Subject: [PATCH 2/5] feat(gtx7): resolve odd comma landings in fabric behind RX_ODD_ALIGN_MODE_G Gtx7RxFixedLatPhaseAligner unilaterally forced Gtx7RxRst back to INIT on every odd comma landing, unbounded and unobservable. Add RX_ODD_ALIGN_MODE_G ("RESET" or "BITSLIP") to gate a fabric escape from that relock loop. Under BITSLIP, an odd landed offset i issues i-2 rxSlide pulses, which SLIDE_WAIT_S turns into i-1 actual pulses (always even, so the recovered clock phase is never disturbed), reducing the residue to 1. The aligned word is then presented as a 1-bit-shifted slice of the aligner's own r.last history through two new output ports, rxDataAligned and rxDataAlignedSel, muxed onto Gtx7Core's rxDataOut only when RX_ODD_ALIGN_MODE_G = "BITSLIP". Both terminal states are sourced one register stage deep so that the resulting latency does not depend on where the comma landed: ALIGNED_S from the previous GT word, ALIGNED_SLIP_S from that word shifted up one bit with its missing MSB taken live. One stage is the floor, since at offset 1 the aligned word's last bit only arrives with the next GT word. The select therefore asserts in both terminal states, and BITSLIP always costs exactly one rxUsrClk of fiber-to-rxDataOut latency relative to RESET, whose RX_DATA_OUT_RESET_GEN drives rxDataOut combinationally off RXDATA and adds no fabric stage. That is 5.385 ns on an LCLS-II link at 3.714 Gbps and 8.403 ns on an LCLS-I link at 2.380 Gbps. It is constant across bring-ups, so it costs a caller one re-calibration rather than run-to-run jitter, but it is a real change to the absolute number. rxDataAligned and rxDataAlignedSel are driven exclusively inside RX_FIX_LAT_ALIGN_GEN, so BITSLIP asserts that generate's full condition, RX_BUF_EN_G = false and RX_ALIGN_MODE_G = "FIXED_LAT". RX_BUF_EN_G defaults to true, which makes the unguarded combination the likeliest caller mistake: it would elaborate RX_NO_ALIGN_GEN instead of the aligner, tie rxPhaseAlignmentDone high, leave rxDataAlignedSel at its declared '0', and fall through to the raw rxDataInt path, giving no comma alignment at all while reporting alignment done. No caller in surf selects BITSLIP, and the one external caller that does (lcls-timing-core TimingGtCoreWrapper) already sets RX_BUF_EN_G => false, so nothing that elaborates today starts failing. BITSLIP never asserts the aligner rxReset, which removes one of the two paths that previously restarted alignment. The remaining path is Gtx7RxRst MONITOR_DATA_VALID/FSM_DONE, fed by Gtx7Core rxDataValidIn, which defaults to '1'. Left at that default the supervision loop is permanently satisfied and a lost alignment is never re-attempted; DO_PHASE_ALIGNMENT time_out_wait_bypass only covers an alignment that never completes, not one that completes and is later lost. Several in-tree callers hardwire rxDataValidIn => '1' while already being RX_BUF_EN_G => false with FIXED_LAT, so this is a live constraint. Both properties are documented at the generic and at the mux. A dont_touch'd signal inside a BITSLIP_MODE_C-gated generate exposes the first resolved odd offset and an odd-landing count for hardware observability. Only the first odd offset is latched, since every odd landing above 1 re-scans at 1 and would otherwise overwrite the offset the CDR actually landed on. Scoping the state to the generate keeps it out of the netlist entirely under RESET; the earlier null-range record form did not, and Vivado tied four degenerate bits low on lanes that use neither field. RX_ODD_ALIGN_MODE_G defaults to "RESET", reproducing today's behavior exactly. Every change is behind BITSLIP_MODE_C or inside the gated generate, and RX_DATA_OUT_RESET_GEN is untouched. Verified by co-simulating the pre-change and post-change aligners from identical stimulus: cycle-identical on rxReset, rxSlide and rxPhaseAlignmentDone over 12000 cycles across 5 aligner-driven RX resets. Not addressed here: even landings settle at GT offset 0 and odd landings at offset 1. If an odd final offset costs a sub-UI recovered-clock phase step, a <=1 UI difference between the two classes remains that no fabric logic can remove, since crossing parity classes requires a fresh CDR lock. That is a property of the silicon and is being measured on hardware, binned on landing parity. --- xilinx/7Series/gtx7/rtl/Gtx7Core.vhd | 74 +++++++-- .../gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd | 157 ++++++++++++++++-- 2 files changed, 206 insertions(+), 25 deletions(-) diff --git a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd index eede82ad49..d964f006c5 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd @@ -81,6 +81,19 @@ entity Gtx7Core is -- Configure RX comma alignment RX_ALIGN_MODE_G : string := "GT"; -- Or "FIXED_LAT" or "NONE" + RX_ODD_ALIGN_MODE_G : string := "RESET"; -- "RESET": legacy behavior, resets the RX on + -- an odd comma landing; "BITSLIP": resolves + -- the odd residue in fabric. Requires + -- RX_ALIGN_MODE_G = "FIXED_LAT" and + -- RX_BUF_EN_G = false. "BITSLIP" adds one + -- rxUsrClk of latency versus "RESET" and + -- never asserts the aligner's rxReset, so + -- recovery from a LOST alignment rests + -- entirely on Gtx7RxRst's DATA_VALID + -- supervision. Drive rxDataValidIn from a + -- decoder when selecting "BITSLIP"; its + -- default of '1' leaves that loop + -- permanently satisfied. ALIGN_COMMA_DOUBLE_G : string := "FALSE"; ALIGN_COMMA_ENABLE_G : bit_vector := "1111111111"; ALIGN_COMMA_WORD_G : integer := 2; @@ -340,11 +353,13 @@ architecture rtl of Gtx7Core is signal rxLpmHfHold : sl; -- Rx Data - signal rxDataInt : slv(RX_EXT_DATA_WIDTH_G-1 downto 0); - signal rxDataFull : slv(63 downto 0); -- GT RXDATA - signal rxCharIsKFull : slv(7 downto 0); -- GT RXCHARISK - signal rxDispErrFull : slv(7 downto 0); -- GT RXDISPERR - signal rxDecErrFull : slv(7 downto 0); + signal rxDataInt : slv(RX_EXT_DATA_WIDTH_G-1 downto 0); + signal rxDataAligned : slv(RX_EXT_DATA_WIDTH_G-1 downto 0) := (others => '0'); + signal rxDataAlignedSel : sl := '0'; + signal rxDataFull : slv(63 downto 0); -- GT RXDATA + signal rxCharIsKFull : slv(7 downto 0); -- GT RXCHARISK + signal rxDispErrFull : slv(7 downto 0); -- GT RXDISPERR + signal rxDecErrFull : slv(7 downto 0); ---------------------------- @@ -390,6 +405,23 @@ architecture rtl of Gtx7Core is begin + -- RX_ODD_ALIGN_MODE_G is a string generic so it cannot carry a constrained range; this assert + -- enforces the two-member enumeration explicitly instead. + assert (RX_ODD_ALIGN_MODE_G = "RESET") or (RX_ODD_ALIGN_MODE_G = "BITSLIP") + report "Gtx7Core: RX_ODD_ALIGN_MODE_G must be RESET or BITSLIP" + severity failure; + + -- rxDataAligned/rxDataAlignedSel are driven only inside RX_FIX_LAT_ALIGN_GEN, so this assert + -- must repeat that generate's FULL condition, RX_BUF_EN_G = false AND RX_ALIGN_MODE_G = + -- "FIXED_LAT". RX_BUF_EN_G defaults to true, so checking only the align mode would let the + -- likeliest caller mistake through: FIXED_LAT plus BITSLIP with RX_BUF_EN_G left at its + -- default elaborates RX_NO_ALIGN_GEN instead, which ties rxPhaseAlignmentDone high and leaves + -- rxDataAlignedSel at its declared '0', so RX_DATA_OUT_BITSLIP_GEN silently degenerates to the + -- raw rxDataInt path while reporting alignment done. + assert (RX_ODD_ALIGN_MODE_G /= "BITSLIP") or (RX_ALIGN_MODE_G = "FIXED_LAT" and RX_BUF_EN_G = false) + report "Gtx7Core: RX_ODD_ALIGN_MODE_G = BITSLIP requires RX_ALIGN_MODE_G = FIXED_LAT and RX_BUF_EN_G = false" + severity failure; + rxOutClkOut <= rxOutClkBufg; cPllLockOut <= cPllLock; @@ -417,7 +449,18 @@ begin -- Rx Logic -------------------------------------------------------------------------------------------------- -- Fit GTX port sizes to selected rx external interface size - rxDataOut <= rxDataInt; + -- rxDataAlignedSel asserts in both of the aligner's terminal states, so once alignment is + -- reached this path is taken regardless of where the comma landed. That is what keeps the + -- fiber-to-rxDataOut latency identical on every bring-up; selecting rxDataInt for the + -- even-landing case would reintroduce a landing-dependent parallel-clock period. + RX_DATA_OUT_BITSLIP_GEN : if (RX_ODD_ALIGN_MODE_G = "BITSLIP") generate + rxDataOut <= rxDataAligned when (rxDataAlignedSel = '1') else rxDataInt; + end generate; + + RX_DATA_OUT_RESET_GEN : if (RX_ODD_ALIGN_MODE_G /= "BITSLIP") generate + rxDataOut <= rxDataInt; + end generate; + RX_DATA_8B10B_GLUE : process (rxCharIsKFull, rxDataFull, rxDecErrFull, rxDispErrFull) is begin @@ -584,20 +627,23 @@ begin RX_FIX_LAT_ALIGN_GEN : if (RX_BUF_EN_G = false and RX_ALIGN_MODE_G = "FIXED_LAT") generate Gtx7RxFixedLatPhaseAligner_Inst : entity surf.Gtx7RxFixedLatPhaseAligner generic map ( - TPD_G => TPD_G, - WORD_SIZE_G => RX_EXT_DATA_WIDTH_G, - COMMA_EN_G => FIXED_COMMA_EN_G, - COMMA_0_G => FIXED_ALIGN_COMMA_0_G, - COMMA_1_G => FIXED_ALIGN_COMMA_1_G, - COMMA_2_G => FIXED_ALIGN_COMMA_2_G, - COMMA_3_G => FIXED_ALIGN_COMMA_3_G) + TPD_G => TPD_G, + WORD_SIZE_G => RX_EXT_DATA_WIDTH_G, + COMMA_EN_G => FIXED_COMMA_EN_G, + COMMA_0_G => FIXED_ALIGN_COMMA_0_G, + COMMA_1_G => FIXED_ALIGN_COMMA_1_G, + COMMA_2_G => FIXED_ALIGN_COMMA_2_G, + COMMA_3_G => FIXED_ALIGN_COMMA_3_G, + RX_ODD_ALIGN_MODE_G => RX_ODD_ALIGN_MODE_G) port map ( rxUsrClk => rxUsrClkIn, rxRunPhAlignment => rxRunPhAlignment, rxData => rxDataInt, rxReset => rxAlignReset, rxSlide => rxSlide, - rxPhaseAlignmentDone => rxPhaseAlignmentDone); + rxPhaseAlignmentDone => rxPhaseAlignmentDone, + rxDataAligned => rxDataAligned, + rxDataAlignedSel => rxDataAlignedSel); rxDlySReset <= '0'; end generate; diff --git a/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd b/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd index e3f508a4a8..2345142afc 100755 --- a/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd +++ b/xilinx/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd @@ -12,6 +12,17 @@ -- the phase of the output clock only every other slide. This module's -- purpose is to obtain an output clock that exactly matches the phase of the -- commas. +-- +-- That reset-and-retry is RX_ODD_ALIGN_MODE_G = "RESET", the default, and it +-- can loop without bound on a link whose CDR keeps landing odd. "BITSLIP" +-- resolves an odd landing in fabric instead and never resets; see the generic +-- below for what it costs. +-- +-- Because "BITSLIP" never asserts rxReset, returning to SEARCH_S after an +-- alignment is LOST depends entirely on the enclosing Gtx7RxRst deasserting +-- rxRunPhAlignment, which it only does when its own DATA_VALID supervision +-- fails. A caller selecting "BITSLIP" must therefore drive Gtx7Core's +-- rxDataValidIn from a decoder rather than leave it at its default of '1'. ------------------------------------------------------------------------------- -- This file is part of 'SLAC Firmware Standard Library'. -- It is subject to the license terms in the LICENSE.txt file found in the @@ -31,27 +42,42 @@ use surf.StdRtlPkg.all; entity Gtx7RxFixedLatPhaseAligner is generic ( - TPD_G : time := 1 ns; - WORD_SIZE_G : integer := 20; - COMMA_EN_G : slv(3 downto 0) := "0011"; - COMMA_0_G : slv := "----------0101111100"; - COMMA_1_G : slv := "----------1010000011"; - COMMA_2_G : slv := "XXXXXXXXXXXXXXXXXXXX"; - COMMA_3_G : slv := "XXXXXXXXXXXXXXXXXXXX"); + TPD_G : time := 1 ns; + WORD_SIZE_G : integer := 20; + COMMA_EN_G : slv(3 downto 0) := "0011"; + COMMA_0_G : slv := "----------0101111100"; + COMMA_1_G : slv := "----------1010000011"; + COMMA_2_G : slv := "XXXXXXXXXXXXXXXXXXXX"; + COMMA_3_G : slv := "XXXXXXXXXXXXXXXXXXXX"; + RX_ODD_ALIGN_MODE_G : string := "RESET"); -- "RESET": legacy behavior, resets the GTX RX on + -- an odd comma landing and hopes for an even + -- relock; "BITSLIP": resolves an odd landing in + -- fabric using only even rxSlide counts, then a + -- constant 1-bit fabric slice. Both terminal + -- states present the aligned word one rxUsrClk + -- after the GT, so the latency is the same for + -- every landing. port ( rxUsrClk : in sl; rxRunPhAlignment : in sl; -- From RxRst, active low reset, not clocked by rxUsrClk rxData : in slv(WORD_SIZE_G-1 downto 0); -- Encoded raw rx data rxReset : out sl; rxSlide : out sl; -- RXSLIDE input to GTX - rxPhaseAlignmentDone : out sl); -- Alignment has been achieved. + rxPhaseAlignmentDone : out sl; -- Alignment has been achieved. + rxDataAligned : out slv(WORD_SIZE_G-1 downto 0); -- Valid only when rxDataAlignedSel='1' + rxDataAlignedSel : out sl); -- '1': downstream must select rxDataAligned over rxData end entity Gtx7RxFixedLatPhaseAligner; architecture rtl of Gtx7RxFixedLatPhaseAligner is constant SLIDE_WAIT_C : integer := 32; -- Dictated by UG476 GTX Transceiver Guide - type StateType is (SEARCH_S, RESET_S, SLIDE_S, SLIDE_WAIT_S, ALIGNED_S); + constant BITSLIP_MODE_C : boolean := (RX_ODD_ALIGN_MODE_G = "BITSLIP"); + + constant ODD_OBS_WIDTH_C : positive := bitSize(WORD_SIZE_G); + constant ODD_CNT_WIDTH_C : positive := 8; + + type StateType is (SEARCH_S, RESET_S, SLIDE_S, SLIDE_WAIT_S, ALIGNED_S, ALIGNED_SLIP_S); type RegType is record state : StateType; @@ -74,11 +100,30 @@ architecture rtl of Gtx7RxFixedLatPhaseAligner is rxSlide => '0', rxPhaseAlignmentDone => '0'); + subtype OddOffsetType is natural range 0 to WORD_SIZE_G-1; + + type OddObsType is record + landedOffset : slv(ODD_OBS_WIDTH_C-1 downto 0); + landedValid : sl; + oddLandingCount : slv(ODD_CNT_WIDTH_C-1 downto 0); + end record OddObsType; + + constant ODD_OBS_INIT_C : OddObsType := ( + landedOffset => (others => '0'), + landedValid => '0', + oddLandingCount => (others => '0')); + signal r : RegType := REG_RESET_C; signal rin : RegType; signal rxRunPhAlignmentSync : sl; + -- Combinational, not part of r/rin: gated by the elaboration-time BITSLIP_MODE_C constant, so + -- under "RESET" this elaborates to a constant '0' drive with no added mux, and dont_touch on r + -- does not preserve any register for it. + signal rxDataAlignedInt : slv(WORD_SIZE_G-1 downto 0); + signal rxDataAlignedSelInt : sl; + attribute dont_touch : string; attribute dont_touch of r : signal is "TRUE"; @@ -87,6 +132,12 @@ architecture rtl of Gtx7RxFixedLatPhaseAligner is begin + -- RX_ODD_ALIGN_MODE_G is a string generic so it cannot carry a constrained range; this assert + -- enforces the two-member enumeration explicitly instead. + assert (RX_ODD_ALIGN_MODE_G = "RESET") or (RX_ODD_ALIGN_MODE_G = "BITSLIP") + report "Gtx7RxFixedLatPhaseAligner: RX_ODD_ALIGN_MODE_G must be RESET or BITSLIP" + severity failure; + -- Must use async resets since rxUsrClk can drop out RstSync_1 : entity surf.RstSync generic map ( @@ -132,8 +183,27 @@ begin else -- Latch the Alignment Value v.alignmentValue := i; - -- Reset the rx and hope for a new lock requiring an even number of slides - v.state := RESET_S; + if BITSLIP_MODE_C then + if (i = 1) then + -- Zero slides needed: the residue resolves through the fabric slice + -- alone + v.state := ALIGNED_SLIP_S; + else + -- Reduce the residue to 1 using the i mod 2 = 0 branch's own slide + -- sequencer (SLIDE_S/SLIDE_WAIT_S, unmodified). That sequencer issues + -- slideCount+1 pulses, as the even branch above notes, so slideCount + -- must be i-2 to issue i-1 pulses, which is even for odd i. Setting it + -- to i-1 would issue i pulses, an odd count landing on offset 0, which + -- is exactly what this mode exists to avoid. SLIDE_WAIT_S returns to + -- SEARCH_S, which re-scans and re-enters this branch at i = 1, + -- resolving with no further slides. + v.slideCount := to_unsigned(i-2, bitSize(WORD_SIZE_G)); + v.state := SLIDE_S; + end if; + else + -- Reset the rx and hope for a new lock requiring an even number of slides + v.state := RESET_S; + end if; end if; end if; end loop; @@ -162,6 +232,12 @@ begin v.rxPhaseAlignmentDone := '1'; -- Gtx7RxRst module will reset this module back to SEARCH_S if alignment is lost + when ALIGNED_SLIP_S => + v.rxPhaseAlignmentDone := '1'; + -- Gtx7RxRst module will reset this module back to SEARCH_S if alignment is lost. + -- rxDataAlignedSelInt (driven below, combinationally, from r.state) tells Gtx7Core to + -- select the fabric-sliced word instead of rxData while this state holds. + end case; rin <= v; @@ -172,6 +248,65 @@ begin rxPhaseAlignmentDone <= r.rxPhaseAlignmentDone; end process comb; + -- Aligned-word output, valid only when BITSLIP_MODE_C. Every odd landing resolves to a fixed + -- residue of 1 before the slice is taken, so the slice is a constant bit range of the history, + -- not an offset-dependent one. + -- + -- Both terminal states source the word one rxUsrClk after the GT presented it, so the latency + -- from fiber to rxDataOut does not depend on where the comma landed: + -- + -- ALIGNED_S (offset 0) -> the previous GT word, unshifted + -- ALIGNED_SLIP_S (offset 1) -> the previous GT word shifted up one bit, its missing MSB taken + -- from the live word + -- + -- One stage is the floor here, not a convenience: at offset 1 the aligned word's last bit only + -- arrives with the next GT word, so it cannot be presented combinationally. Sourcing ALIGNED_S + -- from rxData instead would make the two states differ by a full parallel-clock period, which + -- is the determinism this mode is supposed to provide. + -- + -- Under "RESET" both drives elaborate to constants with no mux, since BITSLIP_MODE_C is an + -- elaboration-time constant. + rxDataAlignedInt <= + (rxData(0) & r.last(WORD_SIZE_G*2-1 downto WORD_SIZE_G+1)) when (BITSLIP_MODE_C and (r.state = ALIGNED_SLIP_S)) else + r.last(WORD_SIZE_G*2-1 downto WORD_SIZE_G) when (BITSLIP_MODE_C and (r.state = ALIGNED_S)) else + (others => '0'); + + rxDataAlignedSelInt <= '1' when (BITSLIP_MODE_C and ((r.state = ALIGNED_S) or (r.state = ALIGNED_SLIP_S))) else '0'; + + rxDataAligned <= rxDataAlignedInt; + rxDataAlignedSel <= rxDataAlignedSelInt; + + ODD_OBS_GEN : if BITSLIP_MODE_C generate + + signal obs : OddObsType := ODD_OBS_INIT_C; + + attribute dont_touch of obs : signal is "TRUE"; + + begin + + obsSeq : process (rxRunPhAlignmentSync, rxUsrClk) is + begin + if (rising_edge(rxUsrClk)) then + -- Latch only the FIRST odd offset seen since reset. Every odd landing above 1 slides + -- down to a residue of 1 and re-scans, so without this guard the offset the CDR + -- actually landed on would always be overwritten by that terminal 1. + if (r.state = SEARCH_S) and ((rin.alignmentValue mod 2) = 1) and (obs.landedValid = '0') then + obs.landedOffset <= std_logic_vector( + to_unsigned(OddOffsetType'(rin.alignmentValue), ODD_OBS_WIDTH_C)) after TPD_G; + obs.landedValid <= '1' after TPD_G; + end if; + if (rin.state = ALIGNED_SLIP_S) and (r.state /= ALIGNED_SLIP_S) then + obs.oddLandingCount <= std_logic_vector( + unsigned(obs.oddLandingCount) + 1) after TPD_G; + end if; + end if; + if (rxRunPhAlignmentSync = '0') then + obs <= ODD_OBS_INIT_C after TPD_G; + end if; + end process obsSeq; + + end generate ODD_OBS_GEN; + seq : process (rxRunPhAlignmentSync, rxUsrClk) is begin if (rising_edge(rxUsrClk)) then From 134e40ce82944c8a4e32caf034f800f7c2f8e36b Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Sun, 23 Aug 2026 16:25:01 -0700 Subject: [PATCH 3/5] test(gtx7): add a Gtx7RxFixedLatPhaseAligner cocotb regression Add the aligner to the non-Vivado GHDL source list and a pytest/cocotb suite sweeping all 20 comma landings in both RX_ODD_ALIGN_MODE_G modes. Per-landing invariants. Every landing settles with an even slide count, so the recovered clock phase is never disturbed; BITSLIP never asserts rxReset; and every landing presents the same frame on the same cycle as every other landing in its mode. Absolute latency, not just landing-invariance. EXPECTED_TRAIL carries one entry per mode, in whole rxUsrClk of fiber-to-rxDataOut latency, so what switching a link from RESET to BITSLIP costs is stated rather than recoverable only by reading Gtx7Core's two output-mux legs against the aligner's pipeline depth. RESET adds no fabric stage; BITSLIP always adds the aligner's one stage. The delta is one rxUsrClk. Mutation-tested per property. Restoring the odd-branch off-by-one fails the 9 odd BITSLIP landings; restoring a combinational even path fails the 10 even ones. Moving the RESET trail entry alone fails exactly the ten even RESET landings and nothing else; moving the BITSLIP entry alone fails all twenty BITSLIP landings and nothing else. Deepening both aligner terminal states by one stage fails all twenty BITSLIP landings, confirming the trail entry tracks the RTL rather than a constant. Scope is recorded in the file. The aligner's end of the contract is elaborated, but Gtx7Core's output mux is reproduced by Harness.data_out() rather than built, because Gtx7Core needs GTXE2_CHANNEL and does not compile under GHDL. An edit to the mux expressions themselves is out of reach of this suite. --- tests/xilinx/gtx7/__init__.py | 0 .../gtx7/test_Gtx7RxFixedLatPhaseAligner.py | 298 ++++++++++++++++++ xilinx/ruckus.tcl | 1 + 3 files changed, 299 insertions(+) create mode 100644 tests/xilinx/gtx7/__init__.py create mode 100644 tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py diff --git a/tests/xilinx/gtx7/__init__.py b/tests/xilinx/gtx7/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py new file mode 100644 index 0000000000..fb73db1b45 --- /dev/null +++ b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py @@ -0,0 +1,298 @@ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +## It is subject to the license terms in the LICENSE.txt file found in the +## top-level directory of this distribution and at: +## https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html. +## No part of 'SLAC Firmware Standard Library', including this file, +## may be copied, modified, propagated, or distributed except according to +## the terms contained in the LICENSE.txt file. +############################################################################## + +# Test methodology: +# - Sweep: Two elaborations, one per RX_ODD_ALIGN_MODE_G value. The generic +# gates a constant and a generate, so it cannot be varied inside one build. +# - Stimulus: A bit-accurate serial stream of tagged 20-bit frames feeds a GT +# model that presents word k as stream[20k + b - d] for a landing offset d and +# decrements d on every rxSlide pulse. Every one of the 20 possible comma +# landings is driven, one per test, so the aligner is exercised across its +# whole input space rather than at a sampled subset. +# - Checks: The property under test is that the fiber-to-rxDataOut latency does +# not depend on where the CDR happened to land. Each landing must reach +# alignment, present a correctly comma-aligned word, and -- the part that +# matters -- present the SAME frame on the SAME cycle as every other landing. +# Under BITSLIP each landing must also settle using an EVEN rxSlide count and +# must never assert rxReset. Under RESET the legacy contract is pinned +# instead: odd landings assert rxReset, even landings do not. +# - Cross-mode: each mode's ABSOLUTE latency is pinned against EXPECTED_TRAIL, +# so the cost of choosing BITSLIP over RESET (one rxUsrClk) is itself a +# regression target rather than an unstated consequence of two independent +# within-mode checks. That check reaches the aligner's boundary contract only; +# Gtx7Core's mux is reproduced by the harness, not elaborated. +# - Timing: Latency is compared in exact frame indices at a common cycle, which +# is a stricter statement than comparing rxPhaseAlignmentDone timing. The +# comparison is held over several cycles so a one-sample coincidence cannot +# pass. Frames carry a 6-bit sequence tag for exactly this purpose. +# - Does not prove: Anything about the GTX PMA itself. Whether a final offset of +# 1 costs a sub-UI recovered-clock phase step relative to a final offset of 0 +# is a property of the silicon, not of this RTL, and no fabric simulation can +# settle it. This test proves the fabric contributes no landing-dependent +# latency of its own; the residual sub-UI term must be measured on hardware. + +import os + +import cocotb +import pytest +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import ( + env_int, + parameter_case, + run_surf_vhdl_test, +) + +WORD_SIZE = 20 +CLK_PERIOD_NS = 5.384 # lane 1's measured rxUsrClk period +COMMA = "0101111100" # K28.5, bits 9:0 of every frame +SLIDE_SETTLE_CYCLES = 70 # SLIDE_WAIT_S burns 64; give it margin +N_FRAMES = 4096 + +# Whole rxUsrClk of fiber-to-rxDataOut latency each mode adds once aligned, and +# therefore the cost of choosing BITSLIP over RESET. Both entries are asserted +# below, so the delta between them is a checked contract and not just a comment. +# +# RESET takes Gtx7Core's RX_DATA_OUT_RESET_GEN leg (rxDataOut <= rxDataInt), +# combinational off RXDATA through RX_DATA_8B10B_GLUE, so it adds no fabric +# stage. BITSLIP takes RX_DATA_OUT_BITSLIP_GEN, whose select asserts in BOTH +# terminal states, so once aligned it always adds the aligner's one stage. One +# stage is the floor, not a convenience: at final offset 1 the aligned word's +# MSB only arrives with the next GT word. +# +# The delta is therefore one rxUsrClk: 5.385 ns on an LCLS-II link at 3.714 +# Gbps, 8.403 ns on an LCLS-I link at 2.380 Gbps. It is constant across +# bring-ups, so it costs a caller one re-calibration rather than introducing +# run-to-run jitter, but it is a real change to the absolute number and a +# reviewer switching a link to BITSLIP will ask for it by name. +# +# Scope: the aligner drives one end of this (rxDataAligned one stage deep, and +# rxDataAlignedSel telling Gtx7Core which leg to take), and that end is +# elaborated. Gtx7Core's mux itself is reproduced by Harness.data_out(), since +# Gtx7Core needs GTXE2_CHANNEL and does not build under GHDL, so an edit to the +# mux expressions is out of reach of this file. +EXPECTED_TRAIL = {"RESET": 0, "BITSLIP": 1} + +ODD_ALIGN_MODE = os.environ.get("RX_ODD_ALIGN_MODE_G", "RESET").strip().strip("'") +LANDING = env_int("LANDING", default=0) + + +def frame_of(m: int) -> int: + """Frame m: comma in bits 9:0, '1111' in bits 19:16, 6-bit tag in bits 15:10. + + The tag nibble is pinned to all ones so the comma pattern (and its inverse) + can only match at the true frame boundary; a run of four ones cannot occur + inside either comma code, which rules out a false landing. + """ + return (0xF << 16) | ((m % 64) << 10) | int(COMMA, 2) + + +def bit_at(p: int) -> int: + if p < 0: + return 0 + return (frame_of(p // WORD_SIZE) >> (p % WORD_SIZE)) & 1 + + +def gt_word(k: int, d: int) -> int: + """The GT's parallel word k when the comma lands d bits into the word.""" + word = 0 + for b in range(WORD_SIZE): + word |= bit_at(WORD_SIZE * k + b - d) << b + return word + + +def is_frame(word: int) -> bool: + return (word & 0x3FF) == int(COMMA, 2) and ((word >> 16) & 0xF) == 0xF + + +def tag_of(word: int) -> int: + return (word >> 10) & 0x3F + + +def expected_tag_at(cycle: int) -> int: + """Tag this mode must be presenting at `cycle`, per EXPECTED_TRAIL. + + Harness.step() drives GT word index (cycle-1) before clocking, so at `cycle` + the model is sourcing index cycle-1 and the output must sit EXPECTED_TRAIL + cycles behind it. + """ + return (cycle - 1 - EXPECTED_TRAIL[ODD_ALIGN_MODE]) % 64 + + +class Harness: + """GT model plus Gtx7Core's output mux, wrapped around one aligner.""" + + def __init__(self, dut, landing): + self.dut = dut + self.offset = landing + self.landing = landing + self.slides = 0 + self.saw_reset = False + self.cycle = 0 + + dut.rxRunPhAlignment.value = 0 + dut.rxData.value = 0 + cocotb.start_soon(Clock(dut.rxUsrClk, CLK_PERIOD_NS, unit="ns").start()) + + async def release_reset(self): + for _ in range(10): + await RisingEdge(self.dut.rxUsrClk) + self.dut.rxRunPhAlignment.value = 1 + + async def step(self): + """Advance one cycle, presenting the GT word and consuming rxSlide.""" + self.dut.rxData.value = gt_word(self.cycle, self.offset) + await RisingEdge(self.dut.rxUsrClk) + await Timer(1, unit="ns") + self.cycle += 1 + if self.dut.rxReset.value == 1: + self.saw_reset = True + if self.dut.rxSlide.value == 1: + self.offset -= 1 + self.slides += 1 + + def data_out(self) -> int: + """Reproduce Gtx7Core's RX_DATA_OUT_BITSLIP_GEN mux.""" + if ODD_ALIGN_MODE == "BITSLIP" and self.dut.rxDataAlignedSel.value == 1: + return int(self.dut.rxDataAligned.value) + return int(self.dut.rxData.value) + + def aligned(self) -> bool: + return self.dut.rxPhaseAlignmentDone.value == 1 + + +async def run_landing(dut, landing): + tb = Harness(dut, landing) + await tb.release_reset() + + # Worst case is landing 19: 18 slides, each costing SLIDE_WAIT_S's full wait. + budget = 20 * SLIDE_SETTLE_CYCLES + for _ in range(budget): + await tb.step() + if tb.aligned(): + break + return tb + + +@cocotb.test() +async def bitslip_landing_is_latency_invariant(dut): + """Every landing must align with an even slide count and no RX reset.""" + if ODD_ALIGN_MODE != "BITSLIP": + return + + tb = await run_landing(dut, LANDING) + + assert tb.aligned(), f"landing {LANDING}: never reached alignment" + assert not tb.saw_reset, ( + f"landing {LANDING}: asserted rxReset in BITSLIP mode, which is the " + f"unbounded-relock behavior this mode exists to remove" + ) + assert tb.slides % 2 == 0, ( + f"landing {LANDING}: settled with an ODD slide count ({tb.slides}). " + f"An odd count moves the recovered sampling phase off the grid that " + f"even counts preserve, which is what RESET mode refuses to do." + ) + assert tb.offset in (0, 1), ( + f"landing {LANDING}: settled at offset {tb.offset}, expected 0 or 1" + ) + assert tb.offset == LANDING % 2, ( + f"landing {LANDING}: parity is not conserved by sliding " + f"(settled at {tb.offset})" + ) + + word = tb.data_out() + assert is_frame(word), ( + f"landing {LANDING}: output 0x{word:05X} is not a comma-aligned frame" + ) + + # Latency in exact frames: the aligned word must trail the GT word the + # model is presenting by the same amount for every landing, and that amount + # is EXPECTED_TRAIL's BITSLIP entry, so the mode's absolute cost is pinned + # here rather than left as a bare offset. One stage is the floor at offset + # 1, so the contract is exactly one. + expected_tag = expected_tag_at(tb.cycle) + assert tag_of(word) == expected_tag, ( + f"landing {LANDING}: presented frame tag {tag_of(word)} at cycle " + f"{tb.cycle}, expected {expected_tag}. The fabric added a " + f"landing-dependent delay." + ) + + # Hold it: a single sample could coincide by luck. + for _ in range(8): + await tb.step() + word = tb.data_out() + assert is_frame(word), f"landing {LANDING}: lost alignment at cycle {tb.cycle}" + assert tag_of(word) == expected_tag_at(tb.cycle), ( + f"landing {LANDING}: frame tag slipped at cycle {tb.cycle}" + ) + + +@cocotb.test() +async def reset_mode_rejects_odd_landings(dut): + """RESET mode's legacy contract, pinned against regression.""" + if ODD_ALIGN_MODE != "RESET": + return + + tb = await run_landing(dut, LANDING) + + if LANDING % 2 == 1: + assert tb.saw_reset, ( + f"landing {LANDING} is odd: RESET mode must demand a fresh CDR lock" + ) + else: + assert not tb.saw_reset, ( + f"landing {LANDING} is even: RESET mode must not reset" + ) + assert tb.aligned(), f"landing {LANDING}: never reached alignment" + assert tb.offset == 0, ( + f"landing {LANDING}: RESET mode must settle at offset 0, " + f"got {tb.offset}" + ) + assert tb.slides == LANDING, ( + f"landing {LANDING}: expected {LANDING} slides, got {tb.slides}" + ) + # RESET mode must keep the fabric path out of the way entirely, so + # Gtx7Core's RX_DATA_OUT_RESET_GEN branch stays bit-identical. + assert dut.rxDataAlignedSel.value == 0, ( + "rxDataAlignedSel asserted under RESET mode" + ) + assert is_frame(int(dut.rxData.value)), ( + f"landing {LANDING}: GT word is not comma-aligned after sliding" + ) + # The other end of EXPECTED_TRAIL. RESET adds no stage, so the delta + # against BITSLIP is one rxUsrClk; see EXPECTED_TRAIL for what that + # costs a caller. Stated as a trail rather than left implicit in + # offset == 0 so both modes are pinned in the same terms. + assert tag_of(int(dut.rxData.value)) == expected_tag_at(tb.cycle), ( + f"landing {LANDING}: RESET mode presented frame tag " + f"{tag_of(int(dut.rxData.value))} at cycle {tb.cycle}, expected " + f"{expected_tag_at(tb.cycle)}. RESET's fiber-to-rxDataOut latency " + f"moved, so the BITSLIP delta is no longer one rxUsrClk." + ) + + +PARAMETER_SWEEP = [ + parameter_case(f"{mode.lower()}_landing{landing:02d}", + RX_ODD_ALIGN_MODE_G=mode, + LANDING=str(landing)) + for mode in ("BITSLIP", "RESET") + for landing in range(WORD_SIZE) +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_Gtx7RxFixedLatPhaseAligner(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.gtx7rxfixedlatphasealigner", + parameters={"RX_ODD_ALIGN_MODE_G": parameters["RX_ODD_ALIGN_MODE_G"]}, + extra_env=parameters, + ) diff --git a/xilinx/ruckus.tcl b/xilinx/ruckus.tcl index db4a5e86dc..c9875a582f 100644 --- a/xilinx/ruckus.tcl +++ b/xilinx/ruckus.tcl @@ -9,6 +9,7 @@ if { $::env(VIVADO_VERSION) > 0.0} { } else { loadSource -lib surf -path "$::DIR_PATH/general/rtl/SelectIoRxGearboxAligner.vhd" loadSource -lib surf -path "$::DIR_PATH/general/rtl/GtRxAlignCheck.vhd" + loadSource -lib surf -path "$::DIR_PATH/7Series/gtx7/rtl/Gtx7RxFixedLatPhaseAligner.vhd" loadSource -lib surf -dir "$::DIR_PATH/dummy" } From 981e96c3481d22497eaae00d6e4efa7335d725f3 Mon Sep 17 00:00:00 2001 From: Benjamin Reese Date: Wed, 26 Aug 2026 19:52:41 -0700 Subject: [PATCH 4/5] docs(gtx7): document fixed-latency RX alignment --- xilinx/7Series/gtx7/README.md | 46 +++++++++++++++++++++++++++++++++++ xilinx/README.md | 1 + 2 files changed, 47 insertions(+) create mode 100644 xilinx/7Series/gtx7/README.md diff --git a/xilinx/7Series/gtx7/README.md b/xilinx/7Series/gtx7/README.md new file mode 100644 index 0000000000..16216b1639 --- /dev/null +++ b/xilinx/7Series/gtx7/README.md @@ -0,0 +1,46 @@ +# GTX7 Support + +This directory contains the SURF wrapper, reset state machines, clock monitoring, and phase-alignment +helpers for the AMD/Xilinx 7 Series `GTXE2_CHANNEL` primitive. `Gtx7Core` is the main integration +point, and the directory-level `ruckus.tcl` loads the sources in `rtl/`. + +## Fixed-latency RX alignment + +`Gtx7RxFixedLatPhaseAligner` aligns a comma in raw parallel RX data while the RX elastic buffer is +bypassed. This configuration requires: + +- `RX_ALIGN_MODE_G = "FIXED_LAT"` +- `RX_BUF_EN_G = false` +- `RXSLIDE_MODE_G = "PMA"` + +In PMA slide mode, `RXSLIDE` moves the parallel data by one bit per pulse, but the recovered output +clock changes phase only on every other pulse. An even number of slides therefore preserves the +relationship between the aligned comma and `RXOUTCLK`. An odd comma landing needs one of two policies, +selected by `RX_ODD_ALIGN_MODE_G`: + +| Mode | Odd landing behavior | Latency and phase contract | +| --- | --- | --- | +| `"RESET"` | Request another RX initialization and accept only a landing requiring an even number of slides. | Intended for applications requiring the recovered-clock phase to match the aligned serial UI. Bring-up can retry without bound. | +| `"BITSLIP"` | Use only an even number of PMA slides, leave a one-bit residue, and repair the word boundary in fabric. | Adds exactly one `rxUsrClk` stage for every landing. Parallel-word latency is deterministic, but the odd and even landing classes may differ in recovered-clock phase by as much as one serial UI. | + +`"BITSLIP"` does not request an RX reset after an odd landing. Its caller must drive `rxDataValidIn` +from a decoder so `Gtx7RxRst` can restart alignment if the link later loses validity. Leaving +`rxDataValidIn` at its default of `'1'` disables that recovery path. + +## Shared CPLL reset ownership + +When TX and RX both select the channel CPLL, `Gtx7Core` gives the TX reset state machine sole ownership +of `CPLLRESET`. This prevents an RX-only retry from resetting the PLL underneath an active TX without +also resetting the TX datapath. The RX reset state machine still asserts `GTRXRESET`, which reinitializes +the RX datapath and CDR, but its separate PLL-reset request is not selected onto the shared CPLL reset. + +Consequently, `RX_ODD_ALIGN_MODE_G = "RESET"` retries do not reinitialize the shared CPLL. If odd/even +landing parity is correlated with CPLL or TX state, an RX-only retry can repeatedly return to the same +odd class. Do not fix that by ORing the RX PLL-reset request directly onto `CPLLRESET`: TX would lose +its clock while its reset state machine continued to report stale state. A system requiring both strict +serial-UI phase and shared-CPLL recovery must coordinate both reset state machines and let the TX reset +state machine remain the sole CPLL-reset owner. + +A coordinated CPLL restart changes more shared state than `GTRXRESET`, but the GTX documentation does +not guarantee that it changes odd/even comma-landing parity. Any such recovery should therefore remain +bounded and expose a failure condition rather than repeatedly disrupting TX without limit. diff --git a/xilinx/README.md b/xilinx/README.md index 14824b96e2..9914b89c60 100644 --- a/xilinx/README.md +++ b/xilinx/README.md @@ -5,6 +5,7 @@ This tree contains Xilinx-specific RTL wrappers, primitive integrations, and hel ## Layout - Family folders such as `7Series/`, `Virtex5/`, `UltraScale/`, `UltraScale+/`, and `Versal/` hold family-specific wrappers and primitive integrations. +- `7Series/gtx7/` contains the GTXE2 channel wrapper and documents its [fixed-latency RX alignment and shared-CPLL reset behavior](7Series/gtx7/README.md). - `general/` contains Xilinx helpers that are not tied to a single family directory. - `xvc-udp/` contains Xilinx Virtual Cable over UDP support and has its own [README.md](xvc-udp/README.md). - `dummy/` contains placeholder or compatibility support used by build flows. From cebf2269e6883cc86f7cd2778d71a7fe59330e00 Mon Sep 17 00:00:00 2001 From: Larry Ruckman Date: Thu, 27 Aug 2026 14:30:33 -0700 Subject: [PATCH 5/5] test(gtx7): satisfy the regression compliance gate The mode gate at the top of each cocotb entrypoint returned early in the other mode's elaboration, so each test reported a vacuous pass in half of the sweep. Select the build's own entrypoint from the pytest wrapper with cocotb_filtered_env() instead, matching the RSSI suites, so every case runs its named checks. Harness.step() waited a bare 1 ns after the rising edge. That delay is the aligner's "after TPD_G" output propagation, which the elaboration leaves at its 1 ns default, so use the shared sample_after_tpd() helper that states that contract. --- .../gtx7/test_Gtx7RxFixedLatPhaseAligner.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py index fb73db1b45..d3345361c5 100644 --- a/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py +++ b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py @@ -11,6 +11,8 @@ # Test methodology: # - Sweep: Two elaborations, one per RX_ODD_ALIGN_MODE_G value. The generic # gates a constant and a generate, so it cannot be varied inside one build. +# The pytest wrapper selects each build's own entrypoint by name, so neither +# mode's checks can report a vacuous pass inside the other mode's build. # - Stimulus: A bit-accurate serial stream of tagged 20-bit frames feeds a GT # model that presents word k as stream[20k + b - d] for a landing offset d and # decrements d on every rxSlide pulse. Every one of the 20 possible comma @@ -43,12 +45,15 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter, env_int, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) WORD_SIZE = 20 @@ -148,10 +153,13 @@ async def release_reset(self): self.dut.rxRunPhAlignment.value = 1 async def step(self): - """Advance one cycle, presenting the GT word and consuming rxSlide.""" + """Advance one cycle, presenting the GT word and consuming rxSlide. + + Sampling waits past the aligner's ``after TPD_G`` output delay, which + the elaboration leaves at its 1 ns default. + """ self.dut.rxData.value = gt_word(self.cycle, self.offset) - await RisingEdge(self.dut.rxUsrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rxUsrClk) self.cycle += 1 if self.dut.rxReset.value == 1: self.saw_reset = True @@ -185,9 +193,6 @@ async def run_landing(dut, landing): @cocotb.test() async def bitslip_landing_is_latency_invariant(dut): """Every landing must align with an even slide count and no RX reset.""" - if ODD_ALIGN_MODE != "BITSLIP": - return - tb = await run_landing(dut, LANDING) assert tb.aligned(), f"landing {LANDING}: never reached alignment" @@ -238,9 +243,6 @@ async def bitslip_landing_is_latency_invariant(dut): @cocotb.test() async def reset_mode_rejects_odd_landings(dut): """RESET mode's legacy contract, pinned against regression.""" - if ODD_ALIGN_MODE != "RESET": - return - tb = await run_landing(dut, LANDING) if LANDING % 2 == 1: @@ -279,6 +281,13 @@ async def reset_mode_rejects_odd_landings(dut): ) +# Each elaboration only carries one mode's contract, so only that mode's +# entrypoint is allowed to run in it. +MODE_ENTRYPOINT = { + "BITSLIP": "bitslip_landing_is_latency_invariant", + "RESET": "reset_mode_rejects_odd_landings", +} + PARAMETER_SWEEP = [ parameter_case(f"{mode.lower()}_landing{landing:02d}", RX_ODD_ALIGN_MODE_G=mode, @@ -290,9 +299,13 @@ async def reset_mode_rejects_odd_landings(dut): @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_Gtx7RxFixedLatPhaseAligner(parameters): + mode = parameters["RX_ODD_ALIGN_MODE_G"] run_surf_vhdl_test( test_file=__file__, toplevel="surf.gtx7rxfixedlatphasealigner", - parameters={"RX_ODD_ALIGN_MODE_G": parameters["RX_ODD_ALIGN_MODE_G"]}, - extra_env=parameters, + parameters={"RX_ODD_ALIGN_MODE_G": mode}, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter(MODE_ENTRYPOINT[mode]), + ), )