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..d3345361c5 --- /dev/null +++ b/tests/xilinx/gtx7/test_Gtx7RxFixedLatPhaseAligner.py @@ -0,0 +1,311 @@ +############################################################################## +## 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. +# 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 +# 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 + +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 +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. + + 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 sample_after_tpd(self.dut.rxUsrClk) + 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.""" + 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.""" + 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." + ) + + +# 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, + LANDING=str(landing)) + for mode in ("BITSLIP", "RESET") + for landing in range(WORD_SIZE) +] + + +@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": mode}, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter(MODE_ENTRYPOINT[mode]), + ), + ) 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/7Series/gtx7/rtl/Gtx7Core.vhd b/xilinx/7Series/gtx7/rtl/Gtx7Core.vhd index 4a0a48dc26..d964f006c5 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; @@ -80,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; @@ -157,8 +171,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 +291,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); @@ -338,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); ---------------------------- @@ -388,9 +405,27 @@ 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; + cPllLockOut <= cPllLock; + cPllRefClkLostOut <= cPllRefClkLost; -------------------------------------------------------------------------------------------------- -- PLL Resets. Driven from TX Rst if both use same PLL @@ -414,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 @@ -581,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 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. 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" }