diff --git a/.github/workflows/surf_ci.yml b/.github/workflows/surf_ci.yml index 82218e613e..ac4d723958 100644 --- a/.github/workflows/surf_ci.yml +++ b/.github/workflows/surf_ci.yml @@ -165,10 +165,15 @@ jobs: --ignore=tests/simlink ) + # Import once before either run mode. The compliance check consumes + # the same ruckus source inventory as the cocotb runner and fails + # fast on new structural violations before expensive simulations. + make MODULES=$PWD import + python -m tests.common.compliance_audit check tests + # Full integration/release runs build and simulate on apt mcode GHDL # and collect the coverage consumed by Codecov. if [[ "${{ steps.mode.outputs.value }}" == "full" ]]; then - make MODULES=$PWD import python -m pytest --cov -v -n auto --dist=worksteal "${full_ignores[@]}" "${full_targets[@]}" exit 0 fi @@ -192,7 +197,6 @@ jobs: fi echo "${selector_output}" - make MODULES=$PWD import if [[ "${selector_rc}" -ne 0 ]] || grep -qx "FORCE_FULL" <<< "${selector_output}"; then echo "Directory selector forced a full run (rc=${selector_rc})" @@ -289,6 +293,41 @@ jobs: SIMLINK_ROGUE_PYTHON="$(command -v python)" \ python -m pytest -q -n 0 tests/simlink/rogue/test_RogueTcpMemoryRogue.py +# ---------------------------------------------------------------------------- + + adc_ddr_rogue: + name: ADC DDR Rogue Tests + runs-on: ubuntu-24.04 + timeout-minutes: 15 + defaults: + run: + shell: bash -el {0} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Setup Miniforge + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1 + with: + miniforge-variant: Miniforge3 + miniforge-version: latest + activate-environment: surf-rogue-test + environment-file: conda-rogue.yml + auto-activate: false + conda-remove-defaults: true + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r pip_requirements.txt + + - name: Run ADC DDR Rogue tests + run: | + python -c "import rogue, pyrogue; print(rogue.Version.current())" + PYTHONPATH="$PWD/python" python -m pytest -q -n 0 \ + tests/devices/analog_devices/test_AdcDdrCalibration.py \ + tests/devices/analog_devices/test_AdcDdrModel.py + # ---------------------------------------------------------------------------- docs: @@ -317,7 +356,7 @@ jobs: # ---------------------------------------------------------------------------- gen_release: - needs: [lint, test, simlink_rogue, docs] + needs: [lint, test, simlink_rogue, adc_ddr_rogue, docs] if: startsWith(github.ref, 'refs/tags/') uses: slaclab/ruckus/.github/workflows/gen_release.yml@main with: @@ -328,7 +367,7 @@ jobs: # ---------------------------------------------------------------------------- conda_build_lib: - needs: [lint, test, simlink_rogue, docs] + needs: [lint, test, simlink_rogue, adc_ddr_rogue, docs] if: startsWith(github.ref, 'refs/tags/') uses: slaclab/ruckus/.github/workflows/conda_build_lib.yml@main with: diff --git a/AGENTS.md b/AGENTS.md index 14b4b6d882..09be9b1aa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,18 @@ Start with [README.md](README.md) for user-facing links and the source tree inde - [protocols/README.md](protocols/README.md) for PGP, SSI, SRP, RSSI, CoaXPress, JESD204B, I2C/SPI/UART, and related protocol cores. - [xilinx/README.md](xilinx/README.md) for Xilinx-family primitives, wrappers, and XVC UDP support. - [python/README.md](python/README.md) for the PyRogue package under `python/surf`. -- [tests/README.md](tests/README.md) for cocotb regression layout, methodology comments, helper reuse, and simulator conventions. +- [tests/README.md](tests/README.md) for the authoritative cocotb regression + methodology, coding style, coverage expectations, layout, and simulator + conventions. +- [tests/common/README.md](tests/common/README.md) for the shared pytest/GHDL + runner, parameter and environment handling, build isolation, and reusable + regression helpers. +- [tests/protocols/README.md](tests/protocols/README.md) for protocol-oracle, + layering, malformed-traffic, ready/valid, and integration-test guidance; then + read the nearest subsystem README, such as + [tests/protocols/batcher/README.md](tests/protocols/batcher/README.md) or + [tests/protocols/rssi/README.md](tests/protocols/rssi/README.md), when working + in that area. - [docs/plans/README.md](docs/plans/README.md) for substantial task planning, progress notes, and handoff conventions. Top-level `ruckus.tcl` loads `axi`, `base`, `dsp`, `devices`, `ethernet`, `protocols`, and `xilinx`. Module-level `ruckus.tcl` files should continue to be the source of truth for which HDL files and submodules are part of a build. @@ -41,6 +52,8 @@ SURF RTL generally follows the two-process style popularized by Gaisler: one com - Put registered state in a `RegType` record. Use a `REG_INIT_C` constant for reset/default state, and declare `r` and `rin` signals for current and next state. - Name the combinational process `comb` and the sequential process `seq` unless the surrounding file has a stronger local convention. - At the top of `comb`, declare `variable v : RegType;` and immediately assign `v := r;`. Make all next-state updates to `v`. +- Minimize additional process variables. Prefer adding intermediate state or diagnostic values to `RegType` and operating on `v.` directly, even when the registered value is not currently consumed. This keeps the next-state path uniform and makes a useful value straightforward to expose through AXI-Lite later. +- Use a process-local variable only when it is genuinely clearer or required by a helper, such as `AxiLiteEndpointType`. Give every such scratch variable an unconditional default immediately after `v := r;` before any conditional logic, unless the called helper initializes the complete object before its first use. Never depend on mutually exclusive branches to imply a combinational default; incomplete assignment can infer a latch in synthesis even when simulation and lint pass. - Assign `rin <= v;` near the end of `comb`. Drive module outputs from `r` for registered outputs and from `v` only when the local design intentionally exposes next-cycle/combinational behavior. - Include all combinational inputs read by the process in the sensitivity list. Existing files often use explicit lists rather than `process(all)`; match nearby style. - Apply synchronous reset in `comb` by assigning `v := REG_INIT_C` when `RST_ASYNC_G = false` and reset is asserted. @@ -169,7 +182,9 @@ C, C++, and C header files should use the same license text with `//` comment de Tcl, shell, YAML, and other hash-comment files should use the Python-style `#-----------------------------------------------------------------------------` license block when they are maintained SURF source. For executable scripts with a shebang, keep the shebang first and place the license block immediately after it. -Checked-in cocotb regression files must also include the `Test methodology` block described in [tests/README.md](tests/README.md), immediately after the license header. +New or substantially edited cocotb regression files must also include the +module-specific `Test methodology` block described in +[tests/README.md](tests/README.md), immediately after the license header. ## Python Conventions @@ -200,12 +215,19 @@ Checked-in cocotb regression files must also include the `Test methodology` bloc ## Tests And Verification -- For RTL regressions, use the guidance in [tests/README.md](tests/README.md). The expected stack is `pytest + cocotb + GHDL + ruckus`. +- For RTL regressions, start with [tests/README.md](tests/README.md). Use + [tests/common/README.md](tests/common/README.md) for runner/build mechanics, + [tests/protocols/README.md](tests/protocols/README.md) for protocol tests, and + the nearest test-subsystem README for local commands or exceptions. The + expected default stack is `pytest + cocotb + GHDL + ruckus`. - For docs-only changes, no RTL or Python tests are required, but check links and headings if the edit adds navigation. - For ruckus or source-list changes, run `make MODULES="$PWD" import` when practical. - For edited VHDL, run `./.venv/bin/vsg -c vsg-linter.yml path/to/file.vhd` and the most focused relevant cocotb/pytest target when practical. - For Python/PyRogue changes, run a focused import or pytest that exercises the changed module. Avoid packaging commands unless the task specifically requires packaging validation. - For cocotb tests, prefer `./.venv/bin/python -m pytest -q tests/`. Use `-n 0` when serial simulator logs are needed. +- Select or explicitly skip cocotb scenarios that do not apply to a parameter case; do not return early and record an unexercised scenario as a pass. +- Use `extra_vhdl_sources` only for design units absent from the ruckus import, and keep finite cocotb tasks awaited or lifetime agents explicitly owned by the bench. +- For bug regressions, demonstrate failure on the known-bad RTL when practical, or document the defect-catching assertion and why the comparison could not be run. - For protocol or bus behavior changes, include tests or a clear verification note covering sidebands, backpressure, reset behavior, and boundary/error cases relevant to the change. - Avoid hand-editing generated or cache directories such as `build/`, `tests/sim_build/`, `.pytest_cache/`, `docs/_build/`, and `docs/_generated/`. @@ -225,7 +247,14 @@ Before considering an RTL change done, check: ## Documentation Updates -When adding a new subsystem, add or update the closest `README.md` if the layout or usage is not obvious. Keep README files short and navigational: describe what belongs in the folder, important subdirectories, and any local build/test conventions, then link upward through the parent README chain. +When adding a new subsystem, add or update the closest `README.md` if the layout +or usage is not obvious. Keep README files short and navigational: describe what +belongs in the folder, important subdirectories, and any local build/test +conventions, then link upward through the parent README chain. Test-subsystem +READMEs should link to [tests/README.md](tests/README.md), and protocol-test +READMEs should also link to +[tests/protocols/README.md](tests/protocols/README.md), so local instructions +extend rather than duplicate the shared methodology. Add deeper README files as substantial areas are touched, especially in high-traffic module families such as `axi/axi-stream`, `axi/axi-lite`, `protocols/pgp`, `protocols/coaxpress`, `protocols/ssi`, `protocols/srp`, `ethernet/IpV4Engine`, `ethernet/UdpEngine`, and `ethernet/EthMacCore`. Prefer adding the README in the same change that introduces new layout or conventions for that area. diff --git a/axi/README.md b/axi/README.md index d5c2c23d23..3629633638 100644 --- a/axi/README.md +++ b/axi/README.md @@ -4,7 +4,7 @@ This tree contains reusable AXI-family RTL and wrappers. Top-level `axi/ruckus.t ## Layout -- `axi-lite/`: AXI-Lite records, crossbars, endpoints, masters, slaves, monitors, and IP-integrator adapters. +- [`axi-lite/`](axi-lite/README.md): AXI-Lite records, crossbars, endpoints, masters, slaves, monitors, and IP-integrator adapters. - `axi-stream/`: AXI Stream records, FIFOs, muxes, monitors, protocol adapters, and stream wrappers. - `axi4/`: full AXI4 support blocks and adapters. - `bridge/`: bridges between AXI-family buses and SURF protocol records. diff --git a/axi/axi-lite/README.md b/axi/axi-lite/README.md new file mode 100644 index 0000000000..670a8386d0 --- /dev/null +++ b/axi/axi-lite/README.md @@ -0,0 +1,52 @@ +# AXI-Lite + +This directory contains reusable AXI-Lite records, interconnects, endpoints, +clock-domain bridges, and IP-integrator adapters. + +## Layout + +- `rtl/`: AXI-Lite packages and synthesizable cores. +- `ip_integrator/`: flattened wrappers for block-design and simulator-facing + integration. +- `tb/`: legacy VHDL testbenches. + +Executable cocotb regressions live under +[`tests/axi/axi_lite/`](../../tests/axi/axi_lite/README.md). + +## `AxiLiteAsync` Contract + +`AxiLiteAsync` directly connects the slave and master interfaces when +`COMMON_CLK_G = true`. Otherwise, five asynchronous FIFOs carry the read +request, read response, write address, write data, and write response channels. + +The asynchronous bridge permits one read and one write in flight. The write +address and data channels remain independent, so either may arrive first, but +each channel accepts only one pending beat until the write response completes. +The bridge enforces this limit with its READY outputs. External masters that +pipeline requests are therefore backpressured rather than buffered to the FIFO +depth. + +All five FIFOs share a registered reset request and are flushed when either AXI +domain resets. Reset handling follows these rules: + +- A slave/source-domain reset abandons outstanding source transactions; no + response is owed after that reset. +- A master/destination-domain reset while the slave domain remains active + completes each accepted read locally with `AXI_ERROR_RESP_G`. +- A locally completed write returns `AXI_ERROR_RESP_G` only after both its AW + and W beats have been accepted. +- Transactions discarded by reset are not replayed when the master domain + recovers, and stale responses do not survive a slave-domain reset. + +A slave/source-domain reset can flush only transactions still held by the +bridge. It cannot retract a write-address or write-data beat that the +downstream slave has already accepted. If a system must prevent a partially +accepted downstream write from surviving reset, it must quiesce AXI-Lite +traffic before resetting or coordinate reset of the destination domain. + +If a clock is unavailable, its corresponding reset must remain asserted. The +clock must be stable before reset is released, and traffic must remain inactive +until synchronized reset release completes. + +`AxiLiteAsyncIpIntegrator.vhd` exposes the same clocks, resets, and response +codes through flattened AXI-Lite ports. diff --git a/axi/axi-lite/rtl/AxiLiteAsync.vhd b/axi/axi-lite/rtl/AxiLiteAsync.vhd index 4fbf5edb8c..507ae675fa 100755 --- a/axi/axi-lite/rtl/AxiLiteAsync.vhd +++ b/axi/axi-lite/rtl/AxiLiteAsync.vhd @@ -51,7 +51,7 @@ end AxiLiteAsync; architecture STRUCTURE of AxiLiteAsync is - signal s2mRst : sl; -- Slave rst sync'd to master clk + signal sRst : sl; -- Slave rst sync'd to slave clk signal m2sRst : sl; -- Master rst sync'd to slave clk signal readSlaveToMastDin : slv(NUM_ADDR_BITS_G+2 downto 0); @@ -89,6 +89,40 @@ architecture STRUCTURE of AxiLiteAsync is signal writeMastToSlaveRead : sl; signal writeMastToSlaveWrite : sl; + -- Depth of every channel FIFO instantiated below + constant FIFO_ADDR_WIDTH_C : positive := 4; + + -- Reset terms normalized to active HIGH, independent of RST_POLARITY_G + signal m2sRstActive : sl; + signal mAxiRstActive : sl; + + -- Registered active-HIGH reset request for every FIFO in the bridge + signal fifoRstReq : sl; + signal fifoRst : sl; + + -- Slave side handshakes, kept local so the error responder can observe them + signal sArReady : sl; + signal sRValid : sl; + signal sAwReady : sl; + signal sWReady : sl; + signal sBValid : sl; + + type RegType is record + errMode : sl; -- Answering locally with AXI_ERROR_RESP_G + rPend : sl; -- Read accepted, not yet answered + awPend : sl; -- Write address accepted, not yet answered + wPend : sl; -- Write data accepted, not yet answered + end record RegType; + + constant REG_INIT_C : RegType := ( + errMode => '0', + rPend => '0', + awPend => '0', + wPend => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + begin GEN_SYNC : if (COMMON_CLK_G = true) generate @@ -102,18 +136,18 @@ begin GEN_ASYNC : if (COMMON_CLK_G = false) generate - -- Synchronize each reset across to the other clock domain - LOC_S2M_RstSync : entity surf.RstSync + -- Synchronize the local reset release before it controls fifoRst + LOC_S_RstSync : entity surf.RstSync generic map ( TPD_G => TPD_G, IN_POLARITY_G => RST_POLARITY_G, - OUT_POLARITY_G => RST_POLARITY_G, - OUT_REG_RST_G => false) + OUT_POLARITY_G => RST_POLARITY_G) port map ( - clk => mAxiClk, + clk => sAxiClk, asyncRst => sAxiClkRst, - syncRst => s2mRst); + syncRst => sRst); + -- Synchronize the remote reset into the slave/control clock domain LOC_M2S_RstSync : entity surf.RstSync generic map ( TPD_G => TPD_G, @@ -125,6 +159,112 @@ begin asyncRst => mAxiClkRst, syncRst => m2sRst); + -- Normalize reset indications to active HIGH + m2sRstActive <= '1' when (m2sRst = RST_POLARITY_G) else '0'; + mAxiRstActive <= '1' when (mAxiClkRst = RST_POLARITY_G) else '0'; + + -- Build one glitch-free FIFO reset request in the slave/control domain. + -- The local reset asserts it asynchronously, so the FIFOs are reset even + -- if sAxiClk is stopped. The remote reset is synchronized above before it + -- sets this register. Deassertion is synchronous and delayed until error + -- mode has drained every abandoned transaction. FifoAsync then + -- resynchronizes this single registered request into both FIFO domains. + fifoRstReq <= m2sRstActive or r.errMode; + + U_FifoRstReg : entity surf.RegisterVector + generic map ( + TPD_G => TPD_G, + RST_POLARITY_G => RST_POLARITY_G, + RST_ASYNC_G => true, + WIDTH_G => 1, + INIT_G => "1") + port map ( + clk => sAxiClk, -- [in] + rst => sRst, -- [in] + sig_i(0) => fifoRstReq, -- [in] + reg_o(0) => fifoRst); -- [out] + + -- Transaction tracking and local error responder. + -- + -- One transaction per channel is in flight at a time, matching + -- AxiLiteCrossbar, whose per-slot state machine does not release a slot + -- until the response completes. The ready outputs below enforce that bound + -- rather than assuming the master honours it. + -- + -- The same state decides what the bridge owes the slave side while the + -- remote domain is in reset, when each access is answered locally instead + -- of being forwarded. That keeps AXI-Lite ordering intact, because a read + -- response only follows an accepted AR and a write response only follows + -- both an accepted AW and W, and it covers the transaction discarded by + -- fifoRst above, which still owes the slave side a response. + comb : process (m2sRstActive, r, sArReady, sAwReady, sAxiClkRst, + sAxiReadMaster, sAxiWriteMaster, sBValid, sRValid, sWReady) is + variable v : RegType; + variable arTxn : sl; + variable rTxn : sl; + variable awTxn : sl; + variable wTxn : sl; + variable bTxn : sl; + begin + -- Latch the current value + v := r; + + -- Slave side handshakes + arTxn := sAxiReadMaster.arvalid and sArReady; + rTxn := sRValid and sAxiReadMaster.rready; + awTxn := sAxiWriteMaster.awvalid and sAwReady; + wTxn := sAxiWriteMaster.wvalid and sWReady; + bTxn := sBValid and sAxiWriteMaster.bready; + + -- Read accepted but not yet answered. Set and clear are mutually + -- exclusive because ARREADY is held low while the read is pending. + if (arTxn = '1') then + v.rPend := '1'; + elsif (rTxn = '1') then + v.rPend := '0'; + end if; + + -- Write address accepted but not yet answered + if (awTxn = '1') then + v.awPend := '1'; + elsif (bTxn = '1') then + v.awPend := '0'; + end if; + + -- Write data accepted but not yet answered + if (wTxn = '1') then + v.wPend := '1'; + elsif (bTxn = '1') then + v.wPend := '0'; + end if; + + -- Enter error mode when the remote domain resets and stay there until + -- the abandoned transaction has been answered + if (m2sRstActive = '1') then + v.errMode := '1'; + elsif (v.rPend = '0') and (v.awPend = '0') and (v.wPend = '0') then + v.errMode := '0'; + end if; + + -- Synchronous Reset + if (RST_ASYNC_G = false) and (sAxiClkRst = RST_POLARITY_G) then + v := REG_INIT_C; + end if; + + -- Register the variable for the next clock cycle + rin <= v; + + end process comb; + + seq : process (sAxiClk, sAxiClkRst) is + begin + if (RST_ASYNC_G) and (sAxiClkRst = RST_POLARITY_G) then + r <= REG_INIT_C after TPD_G; + elsif rising_edge(sAxiClk) then + r <= rin after TPD_G; + end if; + end process seq; + ------------------------------------ -- Read: Slave to Master ------------------------------------ @@ -133,19 +273,19 @@ begin U_ReadSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => NUM_ADDR_BITS_G+3, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => readSlaveToMastWrite, din => readSlaveToMastDin, @@ -171,9 +311,12 @@ begin readSlaveToMastDin(2 downto 0) <= sAxiReadMaster.arprot; readSlaveToMastDin(NUM_ADDR_BITS_G+2 downto 3) <= sAxiReadMaster.araddr(NUM_ADDR_BITS_G-1 downto 0); - -- Write control and ready generation - sAxiReadSlave.arready <= ite(m2sRst = '0', not readSlaveToMastFull, '1'); - readSlaveToMastWrite <= sAxiReadMaster.arvalid and (not readSlaveToMastFull); + -- Write control and ready generation. The request is never queued while the + -- bridge is answering locally, otherwise an access already reported as + -- failed would still reach the master side. + sArReady <= (not r.rPend) when (r.errMode = '1') else ((not readSlaveToMastFull) and (not r.rPend)); + sAxiReadSlave.arready <= sArReady; + readSlaveToMastWrite <= sAxiReadMaster.arvalid and sArReady and (not r.errMode); -- Data Out mAxiReadMaster.arprot <= readSlaveToMastDout(2 downto 0); @@ -196,19 +339,19 @@ begin U_ReadMastToSlaveFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 34, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => m2sRst, + rst => fifoRst, wr_clk => mAxiClk, wr_en => readMastToSlaveWrite, din => readMastToSlaveDin, @@ -235,15 +378,17 @@ begin readMastToSlaveDin(33 downto 2) <= mAxiReadSlave.rdata; -- Write control and ready generation - mAxiReadMaster.rready <= ite(mAxiClkRst = '0', not readMastToSlaveFull, '1'); + mAxiReadMaster.rready <= '1' when (mAxiRstActive = '1') else (not readMastToSlaveFull); readMastToSlaveWrite <= mAxiReadSlave.rvalid and (not readMastToSlaveFull); -- Data Out - sAxiReadSlave.rresp <= ite(m2sRst = '0', readMastToSlaveDout(1 downto 0), AXI_ERROR_RESP_G); + sAxiReadSlave.rresp <= AXI_ERROR_RESP_G when (r.errMode = '1') else readMastToSlaveDout(1 downto 0); sAxiReadSlave.rdata <= readMastToSlaveDout(33 downto 2); - -- Read control and valid - sAxiReadSlave.rvalid <= ite(m2sRst = '0', readMastToSlaveValid, '1'); + -- Read control and valid. Answering locally requires an accepted AR, so the + -- response can never arrive ahead of its request. + sRValid <= r.rPend when (r.errMode = '1') else readMastToSlaveValid; + sAxiReadSlave.rvalid <= sRValid; readMastToSlaveRead <= sAxiReadMaster.rready; ------------------------------------ @@ -254,19 +399,19 @@ begin U_WriteAddrSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => NUM_ADDR_BITS_G+3, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => writeAddrSlaveToMastWrite, din => writeAddrSlaveToMastDin, @@ -293,8 +438,9 @@ begin writeAddrSlaveToMastDin(NUM_ADDR_BITS_G+2 downto 3) <= sAxiWriteMaster.awaddr(NUM_ADDR_BITS_G-1 downto 0); -- Write control and ready generation - sAxiWriteSlave.awready <= ite(m2sRst = '0', not writeAddrSlaveToMastFull, '1'); - writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and (not writeAddrSlaveToMastFull); + sAwReady <= (not r.awPend) when (r.errMode = '1') else ((not writeAddrSlaveToMastFull) and (not r.awPend)); + sAxiWriteSlave.awready <= sAwReady; + writeAddrSlaveToMastWrite <= sAxiWriteMaster.awvalid and sAwReady and (not r.errMode); -- Data Out mAxiWriteMaster.awprot <= writeAddrSlaveToMastDout(2 downto 0); @@ -317,19 +463,19 @@ begin U_WriteDataSlaveToMastFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 36, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => s2mRst, + rst => fifoRst, wr_clk => sAxiClk, wr_en => writeDataSlaveToMastWrite, din => writeDataSlaveToMastDin, @@ -356,8 +502,9 @@ begin writeDataSlaveToMastDin(35 downto 4) <= sAxiWriteMaster.wdata; -- Write control and ready generation - sAxiWriteSlave.wready <= ite(m2sRst = '0', not writeDataSlaveToMastFull, '1'); - writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and (not writeDataSlaveToMastFull); + sWReady <= (not r.wPend) when (r.errMode = '1') else ((not writeDataSlaveToMastFull) and (not r.wPend)); + sAxiWriteSlave.wready <= sWReady; + writeDataSlaveToMastWrite <= sAxiWriteMaster.wvalid and sWReady and (not r.errMode); -- Data Out mAxiWriteMaster.wstrb <= writeDataSlaveToMastDout(3 downto 0); @@ -375,19 +522,19 @@ begin U_WriteMastToSlaveFifo : entity surf.FifoASync generic map ( TPD_G => TPD_G, - RST_POLARITY_G => RST_POLARITY_G, + RST_POLARITY_G => '1', RST_ASYNC_G => RST_ASYNC_G, MEMORY_TYPE_G => "distributed", -- Use Dist Ram FWFT_EN_G => true, SYNC_STAGES_G => 3, PIPE_STAGES_G => PIPE_STAGES_G, DATA_WIDTH_G => 2, - ADDR_WIDTH_G => 4, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_C, INIT_G => "0", FULL_THRES_G => 15, EMPTY_THRES_G => 1) port map ( - rst => m2sRst, + rst => fifoRst, wr_clk => mAxiClk, wr_en => writeMastToSlaveWrite, din => writeMastToSlaveDin, @@ -417,10 +564,12 @@ begin writeMastToSlaveWrite <= mAxiWriteSlave.bvalid and (not writeMastToSlaveFull); -- Data Out - sAxiWriteSlave.bresp <= ite(m2sRst = '0', writeMastToSlaveDout, AXI_ERROR_RESP_G); + sAxiWriteSlave.bresp <= AXI_ERROR_RESP_G when (r.errMode = '1') else writeMastToSlaveDout; - -- Read control and valid - sAxiWriteSlave.bvalid <= ite(m2sRst = '0', writeMastToSlaveValid, '1'); + -- Read control and valid. Answering locally requires both an accepted AW and + -- an accepted W, so the two channels can still arrive in either order. + sBValid <= (r.awPend and r.wPend) when (r.errMode = '1') else writeMastToSlaveValid; + sAxiWriteSlave.bvalid <= sBValid; writeMastToSlaveRead <= sAxiWriteMaster.bready; end generate; diff --git a/conda-rogue.yml b/conda-rogue.yml new file mode 100644 index 0000000000..292d0156ef --- /dev/null +++ b/conda-rogue.yml @@ -0,0 +1,18 @@ +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +name: surf-rogue-test +channels: + - tidair-tag + - conda-forge +dependencies: + - python=3.12 + - pip + - rogue=v6.15.0 diff --git a/devices/AnalogDevices/README.md b/devices/AnalogDevices/README.md new file mode 100644 index 0000000000..ae9b81e9f8 --- /dev/null +++ b/devices/AnalogDevices/README.md @@ -0,0 +1,93 @@ +# Analog Devices + +This tree contains register interfaces, readout RTL, simulation models, and +FPGA-family wrappers for supported Analog Devices parts. + +## Serialized DDR ADCs + +AD9249, AD9252, and AD9681 use the shared [`adcDdr`](adcDdr/) infrastructure +for their normalized readout implementations. The device directories retain +the part-specific pin topology, sample assembly, configuration interface, and +simulation model: + +- [`ad9249/`](ad9249/) provides two independent eight-channel readout banks + and a detailed guide for migrating the retained legacy + `Ad9249ReadoutGroup` firmware and software. +- [`ad9252/`](ad9252/) provides one eight-channel readout. +- [`ad9681/`](ad9681/) assembles two physical serialized lanes per logical + channel and captures both halves with one selected DCLK. + +The shared [`adcDdr` README](adcDdr/README.md) describes the RTL boundary, +software calibration algorithm, measurement backends, and target-owned timing +responsibilities. +The matching PyRogue models are under +[`python/surf/devices/analog_devices`](../../python/surf/devices/analog_devices/). +AD9249 configuration/readout classes live in `_Ad9249.py`; the +unchanged `Ad9249ReadoutGroup` map and related legacy helper are isolated in +`_Ad9249Legacy.py` while remaining available through the package namespace. + +### PyRogue Readout Classes + +Application code should instantiate the device-specific readout class instead +of repeating normalized `AdcDdr` geometry. For example, the AD9681 +map is: + +```python +self.add(surf.devices.analog_devices.Ad9681Readout( + enabled = True, + name = 'Ad9681Readout', + offset = 0x00000000, + deviceFamily = '7SERIES')) +``` + +The device classes supply these mappings: + +| Class | Data lanes | FCO lanes | Channels | Sample bits | Serialization | +|---|---:|---:|---:|---:|---:| +| `Ad9249ReadoutBank` | 8 | 1 | 8 | 14 | 14 | +| `Ad9252Readout` | 8 by default | 1 | 8 by default | 14 | 14 | +| `Ad9681Readout` | 16 | 2 | 8 | 14 | 8 | + +`Ad9249Readout` represents the full converter and contains two independent +`Ad9249ReadoutBank` children at offsets `0x0000` and `0x1000`. AD9252 exposes a +`channels` argument because its RTL supports `NUM_CHANNELS_G` from +one through eight. All three classes expose `deviceFamily`, matching RTL +`DEVICE_FAMILY_G`; the models derive five delay bits for 7-Series and nine for +UltraScale/UltraScale+. Migration from the +removed AD9681 and AD9249 Group2 interfaces is documented in the +[serialized DDR ADC migration guide](adcDdr/migration-guide.md). +Migration from the retained legacy `Ad9249ReadoutGroup` interface is documented +in the [AD9249 readout migration guide](ad9249/README.md). + +### Device Simulation Timing + +The three serialized-DDR ADC directories provide primitive-free `*Sim` models +with the device pin topology, a binary ideal DCO, coherent frame serialization, +and datasheet conversion latency. Normal conversion data is delayed by 16 +sample clocks for AD9249 and AD9681 and by eight sample clocks for AD9252. +Digitally generated test patterns are selected after that conversion pipeline. + +Their serializer timing controls have the same meaning: + +- `DATA_PHASE_G` and `FCO_PHASE_G` apply a common static displacement relative + to DCO to all data or FCO transitions. +- `DATA_SKEW_G` applies independent physical-data-lane displacement. + `FCO_SKEW_G` is per FCO for AD9249 and AD9681 and scalar for AD9252. +- `JITTER_G` alternates each actual data/FCO transition between `-JITTER_G` + and `+JITTER_G`. It does not introduce `X` values or jitter DCO. +- `TIMING_BIAS_G` delays data, FCO, and DCO together. Set it to at least + `JITTER_G` when no positive phase/skew already makes the early transition + schedulable. Because it is common-mode, it does not change setup time. + +All timing controls default to zero, preserving an ideal centered eye. A +useful nonzero regression starting point is 50 ps of jitter with 50 ps of +timing bias, then measured or deliberately stressed per-lane skew. The models +assert if an early edge would fall before simulation time or a late data/FCO +edge would cross its following DCO sampling edge. The flattened cocotb wrappers +express timing controls in integer picoseconds and expose representative lane +zero/FCO-zero skew; instantiate the `*Sim` entity directly to provide complete +`TimeArray` lane maps. + +Other part directories contain independent control or datapath cores and do +not use the serialized-DDR calibration flow unless their readout is explicitly +built on the normalized `AdcDdr` map. diff --git a/devices/AnalogDevices/ad9249/README.md b/devices/AnalogDevices/ad9249/README.md new file mode 100644 index 0000000000..b38aaab2ce --- /dev/null +++ b/devices/AnalogDevices/ad9249/README.md @@ -0,0 +1,507 @@ +# AD9249 Readout Migration Guide + +## Purpose + +This guide explains how to move a downstream design from the retained legacy +`Ad9249ReadoutGroup` RTL/PyRogue interface to the AdcDdr-based serialized-DDR ADC +readout. It is written for firmware and software repositories that consume SURF +as a submodule and may need to schedule the migration independently. + +The legacy implementation is not removed by this migration effort. Both +family-specific legacy RTL files and the matching PyRogue class remain available +for existing projects: + +- [7-Series legacy RTL](7Series/rtl/Ad9249ReadoutGroup.vhd) +- [UltraScale legacy RTL](UltraScale/rtl/Ad9249ReadoutGroup.vhd) +- [legacy PyRogue map](../../../python/surf/devices/analog_devices/_Ad9249Legacy.py) + +The replacements are: + +- [Ad9249ReadoutBank](core/Ad9249ReadoutBank.vhd) for one independently clocked + eight-channel DCO/FCO bank; +- [Ad9249Readout](core/Ad9249Readout.vhd) for a complete 16-channel ADC with two + independent banks; +- `Ad9249ReadoutBank` and `Ad9249Readout` in the + [PyRogue module](../../../python/surf/devices/analog_devices/_Ad9249.py); +- `Ad9249ReadoutBankCalibration` and `Ad9249ReadoutCalibration` for software + eye calibration and verification. + +This is not a drop-in entity rename. The register map, address-window size, +clock/reset boundary, stream behavior, and software operations change. + +## Choose the Replacement + +Use `Ad9249ReadoutBank` when the existing `Ad9249ReadoutGroup` represents one +physical DCO/FCO bank. This is the normal migration and preserves the existing +one-bank integration boundary. + +Use full `Ad9249Readout` only when one module owns both physical banks and can +provide: + +- both `Ad9249SerialGroupType` records; +- one `0x2000`-byte AXI-Lite device window; +- separate initial data/FCO delays and `IODELAY_GROUP` values for each bank; +- one common stream clock/reset for all 16 logical channel streams. + +The full wrapper places bank 0 at offset `0x0000`, bank 1 at `0x1000`, and maps +stream destinations to `0..15`. Two separate `Ad9249ReadoutBank` instances are +equally valid when banks belong to different modules, AXI crossbars, resets, or +software devices. + +## Migration Gates + +Review these conditions before editing the instantiation. A project that hits a +gate should retain the legacy wrapper until the required target or SURF support +is designed and validated. + +| Legacy behavior | AdcDdr behavior | Required decision | +|---|---|---| +| `NUM_CHANNELS_G` may be `1..8`. | RTL bank supports `1..8`, but the typed `Ad9249ReadoutBank` PyRogue model and calibration adapter currently describe eight channels. | Eight-channel banks migrate directly. For reduced-channel software, retain legacy support or add and test a matching typed model before migration. | +| UltraScale `USE_MMCME_G=false` accepts shared external bit/divided clocks and resets. | The AdcDdr PHY derives its capture clocks internally from the bank DCO and has no external-clock ports. | Confirm per-bank clock-resource use is acceptable. If shared external clocking is required, retain legacy support or extend the AdcDdr PHY boundary first. | +| UltraScale `adcReady[ch]` can stall each channel FIFO independently. | AdcDdr output has no ready input and crosses all channels coherently through one wide FIFO. | Remove `adcReady` only when the consumer can accept every asserted `tValid`. Otherwise add an explicit downstream buffering/flow-control adapter. | +| Runtime `Invert` register complements every sample. | AdcDdr inversion/coding choices are compile-time generics or ADC configuration settings. | Replace runtime changes with a fixed `ADC_INVERT_CH_G`, `OFFSET_BINARY_G`, `NEGATE_G`, or ADC output-format policy. | +| Legacy AXI-Lite remains responsive from `axilClk` without DCO. | The normalized endpoint is crossed into the capture domain. | Do not access the readout while its DCO is absent. Sequence ADC clock startup before readout software access. | +| Legacy unlocked behavior differs by family. | AdcDdr output preserves cadence and marks unlocked samples with `tUser(0)`. | Consumers must inspect or intentionally ignore `tUser(0)`; do not assume the old all-ones or suppressed-valid behavior. | + +## Firmware Migration + +### Entity replacement + +Replace: + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutGroup +``` + +with: + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutBank +``` + +`Ad9249ReadoutBank` is family-neutral. `DEVICE_FAMILY_G` selects exactly one +PHY implementation and its native delay width: + +| Target | `DEVICE_FAMILY_G` | Delay bits | +|---|---|---:| +| 7-Series | `"7SERIES"` | `5` | +| UltraScale | `"ULTRASCALE"` | `9` | +| UltraScale+ | `"ULTRASCALE_PLUS"` | `9` | + +The legacy Python `fpga` choice exposed six or ten bits because its register +definition included the adjacent legacy load-control bit. AdcDdr delay +values contain only the physical tap count. RTL and software derive five bits +for 7-Series and nine bits for UltraScale/UltraScale+ from the family. + +### Generic mapping + +| Legacy generic | AdcDdr bank generic | Migration | +|---|---|---| +| `TPD_G` | `TPD_G` | Copy unchanged. | +| `NUM_CHANNELS_G` | `NUM_CHANNELS_G` | Copy for RTL; review the reduced-channel software gate above. | +| none | `AXIL_BASE_ADDR_G` | Set to the absolute base address visible on the incoming AXI-Lite master. | +| none | `DEVICE_FAMILY_G` | Select `7SERIES`, `ULTRASCALE`, or `ULTRASCALE_PLUS`. | +| `IODELAY_GROUP_G` | `IODELAY_GROUP_G` | Copy, then verify the target constraints and controller ownership. | +| `IDELAYCTRL_FREQ_G` | `IDELAYCTRL_FREQ_G` | Copy for 7-Series; it describes the target-owned `IDELAYCTRL` reference clock. | +| `DEFAULT_DELAY_G` | `DATA_DELAY_INIT_G`, `FCO_DELAY_INIT_G` | Expand the common legacy default into independent numeric tap values. | +| `ADC_INVERT_CH_G` | `ADC_INVERT_CH_G` | Copy the active channel bits. | +| none | `PATTERN_CHECK_G` | Enable to include hardware-assisted bounded pattern measurements. | +| legacy runtime `Invert` | `OFFSET_BINARY_G`, `NEGATE_G`, or `ADC_INVERT_CH_G` | Choose fixed numeric/polarity behavior deliberately; there is no runtime register equivalent. | + +UltraScale-only legacy generics do not have direct AdcDdr equivalents: + +| UltraScale legacy generic | AdcDdr handling | +|---|---| +| `SIM_DEVICE_G` | Replace with `DEVICE_FAMILY_G`. | +| `D_DELAY_CASCADE_G`, `F_DELAY_CASCADE_G` | No wrapper-level equivalent; the AdcDdr PHY delay topology is fixed. Revalidate timing/range. | +| `USE_MMCME_G` | Removed; the AdcDdr PHY owns DCO-derived capture clocking. | +| `SIM_SPEEDUP_G` | Removed; simulation selection belongs to the selected PHY/build environment. | + +### Initial delay conversion + +The legacy wrapper already has independent runtime data and frame delays, but +only one scalar compile-time default. `Ad9249ReadoutBank` has an independent +compile-time value for every data lane and FCO lane. + +For an initial, behavior-preserving seed: + +```vhdl +constant LEGACY_DELAY_TAPS_C : natural := 12; + +constant ADC_DATA_DELAY_INIT_C : NaturalArray(NUM_CHANNELS_C-1 downto 0) := + (others => LEGACY_DELAY_TAPS_C); + +constant ADC_FCO_DELAY_INIT_C : NaturalArray(0 downto 0) := + (0 => LEGACY_DELAY_TAPS_C); +``` + +Replace the replicated seed with characterized per-lane values after running +AdcDdr calibration. Each array value must fit the width selected by +`DEVICE_FAMILY_G`; elaboration asserts otherwise. + +If the old software saved `ChannelDelay[ch]` and `FrameDelay`, copy those actual +tap values—not the legacy Python field width—into `DATA_DELAY_INIT_G(ch)` and +`FCO_DELAY_INIT_G(0)`. + +### Port mapping + +The following ports retain their roles: + +- `axilClk`, `axilRst`, and all four AXI-Lite records; +- `adcClkRst`; +- `adcSerial : Ad9249SerialGroupType`; +- `adcStreamClk`; +- `adcStreams` with the active logical channel range. + +AdcDdr bank integration adds: + +```vhdl +idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] +adcStreamRst => adcStreamRst, -- [in] +``` + +For 7-Series, instantiate or reuse a target-owned `IDELAYCTRL`, match its +`IODELAY_GROUP` to `IODELAY_GROUP_G`, constrain its reference clock at +`IDELAYCTRL_FREQ_G`, and connect `RDY` to `idelayCtrlRdy`. Do not tie readiness +high merely to bypass startup sequencing in hardware. The input defaults low, +and any later loss of `RDY` holds the readout PHY in reset until readiness +returns. The target must reset `IDELAYCTRL` after its reference clock is stable; +the readout does not generate that target-owned reset. + +UltraScale/UltraScale+ uses `IDELAYE3` count mode and does not use +`IDELAYCTRL`; the selected PHY ignores the low `idelayCtrlRdy` default. + +`adcStreamRst` resets the coherent stream-domain FIFO/output state and must be +valid in the `adcStreamClk` domain. + +Remove the UltraScale legacy ports `adcBitClkIn`, `adcBitClkDiv4In`, +`adcBitRstIn`, `adcBitRstDiv4In`, and `adcReady`. Before doing so, resolve the +shared-clock and backpressure migration gates described above. + +### One-bank example + +```vhdl +U_AdcReadout : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => ADC_READOUT_BASE_ADDR_C, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => ADC_DEVICE_FAMILY_C, + IODELAY_GROUP_G => "ADC_BANK_0", + IDELAYCTRL_FREQ_G => 200.0, + DATA_DELAY_INIT_G => ADC_DATA_DELAY_INIT_C, + FCO_DELAY_INIT_G => ADC_FCO_DELAY_INIT_C, + ADC_INVERT_CH_G => ADC_INVERT_CH_C, + PATTERN_CHECK_G => true, + OFFSET_BINARY_G => false, + NEGATE_G => false) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => adcWriteMaster, -- [in] + axilWriteSlave => adcWriteSlave, -- [out] + axilReadMaster => adcReadMaster, -- [in] + axilReadSlave => adcReadSlave, -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] + adcSerial => adcSerial, -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => adcStreams); -- [out] +``` + +Set `ADC_DELAY_BITS_C` and `ADC_DEVICE_FAMILY_C` from the target family table; +do not infer them from the old PyRogue `fpga` field width. + +### Full-device example boundary + +`Ad9249Readout` has two fixed eight-channel banks. Its important differences +from one bank are: + +- `adcSerial` is `Ad9249SerialGroupArray(1 downto 0)`; +- `idelayCtrlRdy` is a two-bit vector; +- `IODELAY_GROUP_0_G` and `IODELAY_GROUP_1_G` are independent; +- `DATA_DELAY_INIT_G(7 downto 0)` belongs to bank 0 and + `(15 downto 8)` belongs to bank 1; +- `FCO_DELAY_INIT_G(0)` and `(1)` belong to banks 0 and 1; +- AXI offsets are `0x0000` and `0x1000` relative to `AXIL_BASE_ADDR_G`; +- output streams/destinations are channels `0..7` for bank 0 and `8..15` for + bank 1. + +Do not wrap two existing one-bank AXI regions with full `Ad9249Readout` unless +the surrounding crossbar and Python hierarchy are changed to the single +`0x2000`-byte device layout at the same time. + +## AXI-Lite Register Migration + +One legacy bank typically fits inside a small `0x100`-byte region. One +AdcDdr bank requires a non-overlapping `0x1000`-byte region because the +normalized map includes delay, counter, debug snapshot, and pattern-test +windows. Expand both the firmware crossbar allocation and the PyRogue stride. + +| Legacy offset/path | AdcDdr offset/path | Notes | +|---|---|---| +| `0x00+4*ch`, `ChannelDelay[ch]` | `0x100+4*ch`, `DataDelay[ch]` | Same physical data-lane tap concept; AdcDdr readback is the retained programmed setting. | +| `0x20`, `FrameDelay` | `0x200`, `FcoDelay[0]` | Same physical FCO tap concept. | +| `0x30[15:0]`, `LostLockCount` | `0x340`, `LostLockCount[0]` | The AdcDdr counter is 32-bit saturating. | +| `0x30[16]`, `Locked` | `0x020[0]`, `LockedMask`; `0x01C[2]`, `AllLocked` | Use the mask for per-FCO state or aggregate status for startup. | +| `0x34`, `AdcFrame` | `0x300`, `FcoWord[0]` | Most recent deserialized FCO word. | +| `0x38`, `LostLockCountReset()` | `0x018`, `ClearCounters()` | Also clears overflow count/sticky overflow. | +| `0x40`, `Invert` | no runtime register | Select compile-time wrapper/ADC coding behavior. | +| `0x80+4*ch`, `AdcChannel[ch]` | `0x600+0x10*ch`, four `DebugSample` words | Legacy packs two rolling samples; AdcDdr publishes four coherent samples after `Snapshot()`. | +| `0xA0`, `FreezeDebug` | `0x014`, `Snapshot()` | Snapshot is explicit, atomic across every channel, and blocking. | + +AdcDdr-specific controls/status include: + +- `Version`, geometry, delay-width, and pattern-check capabilities; +- `CaptureReset` and explicit `Relock`; +- `DelayReady`, `AllLocked`, and `AnyOverflow`; +- `SnapshotSequence`; +- `OverflowCount`; +- optional `PatternTester` configuration and result windows. + +Unmapped AdcDdr accesses return `DECERR`. Do not retain hard-coded legacy +offsets in C++, Python, YAML-generated maps, notebooks, or command scripts. + +## Stream Behavior Migration + +Payload placement remains compatible: the 14-bit ADC code is right-justified +in `tData(13 downto 0)` and bits `15:14` are zero during locked, normal +operation. `tDest` remains the logical channel number. + +The following timing-visible behavior changes: + +- AdcDdr capture moves all active channels through one wide asynchronous + FIFO, preserving channel coherence for each sample epoch. +- AdcDdr output has no ready/backpressure input. A full internal FIFO drops + the newest complete channel group, sets `AnyOverflow`, and increments + `OverflowCount`; it does not stall the ADC. +- AdcDdr output does not suppress cadence when FCO alignment is lost. + `tUser(0)` marks affected samples. +- `tLast` remains deasserted; this is not SSI framing. +- `adcStreamRst` explicitly resets the stream crossing. + +Legacy 7-Series emits all-ones data while unlocked. Legacy UltraScale +suppresses channel `tValid` while unlocked and can independently stall channels +with `adcReady`. Any consumer depending on either behavior must be updated to +use `tValid`, `tUser(0)`, and overflow status deliberately. + +## PyRogue Migration + +### One bank + +Replace: + +```python +self.add(surf.devices.analog_devices.Ad9249ReadoutGroup( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + fpga = '7series', + channels = 8)) +``` + +with: + +```python +self.add(surf.devices.analog_devices.Ad9249ReadoutBank( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = '7SERIES')) +``` + +Use `deviceFamily='ULTRASCALE'` or `'ULTRASCALE_PLUS'` for those FPGA families. +The Python and RTL models derive the same native delay width from this value. + +Ensure the next device offset is at least `ADC_READOUT_OFFSET + 0x1000`. If +several banks are created in a loop, change any legacy `0x100` stride to +`0x1000` or larger. + +### Full device + +For the full RTL wrapper: + +```python +self.add(surf.devices.analog_devices.Ad9249Readout( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = ADC_DEVICE_FAMILY)) +``` + +Software then accesses `Ad9249Readout.Bank[0]` and `.Bank[1]`, matching RTL +offsets `0x0000` and `0x1000` inside the device. + +### Operational path mapping + +| Legacy software | AdcDdr software | +|---|---| +| `ChannelDelay[ch]` | `DataDelay[ch]` | +| `FrameDelay` | `FcoDelay[0]` | +| `Locked` | `AllLocked` or `LockedMask & 0x1` | +| `LostLockCount` | `LostLockCount[0]` | +| `LostLockCountReset()` | `ClearCounters()` | +| `AdcFrame` | `FcoWord[0]` | +| background/explicit `AdcChannel[ch]` read | call `Snapshot()`, then read `DebugSample[ch]` or the raw `DebugSampleRaw` array | +| `FreezeDebug` | remove; `Snapshot()` owns atomic publication | +| `Invert` | no runtime path; use the selected RTL/ADC coding policy | + +`Snapshot()` holds its AXI-Lite write response until four valid coherent sample +groups are captured. It can return `SLVERR` during reset/startup and cannot +complete without a running DCO and valid samples. Do not call it from a generic +startup `ReadAll` path. + +## Calibration Integration + +AdcDdr calibration uses the ADC checkerboard pattern to measure FCO and +every data-lane eye independently, performs full-channel qualification, and can +verify the selected settings at startup. + +For one bank: + +```python +readout = surf.devices.analog_devices.Ad9249ReadoutBank( + name = 'Ad9249Readout', + offset = ADC_READOUT_OFFSET, + deviceFamily = ADC_DEVICE_FAMILY) +self.add(readout) + +calibration = surf.devices.analog_devices.Ad9249ReadoutBankCalibration( + name = 'Ad9249Calibration', + config = self.Ad9249Config.BankConfig[0], + readout = readout) +self.add(calibration) +``` + +For full-device RTL/Python, use `Ad9249ReadoutCalibration` with the complete +`Ad9249Config` and `Ad9249Readout`; it creates `Bank[0]` and `Bank[1]` +calibration processes. + +Recommended bring-up flow: + +1. Configure the ADC output format and enable its DCO/FCO/data outputs. +2. Wait for target resets and 7-Series `IDELAYCTRL.RDY` where applicable. +3. Read `Version`, geometry, `DelayBits`, and `DelayReady`; verify they match + the software/target configuration. +4. Run full calibration per bank with the complete delay range. +5. Review every FCO/data eye, selected tap, and left/right margin. +6. Copy stable characterized centers into `DATA_DELAY_INIT_G` and + `FCO_DELAY_INIT_G` for that board/target. +7. On later startups, run verify-current or guard-band verification rather than + assuming a successful lock bit proves adequate margin. + +Calibration is disruptive: it changes ADC test mode, delay taps, and alignment +while running. Schedule it before normal data taking and prevent concurrent +readout-control access. + +The shared [adcDdr README](../adcDdr/README.md) documents calibration controls, +measurement backends, results, cleanup behavior, and limitations. + +## Constraints and Timing + +Keep the AD9249 constraints in the board target's top-level XDC. Update its pin +queries and timing values using the selected ADC mode, datasheet `tCO`, PCB +flight times, board skew, DCO frequency, and actual target hierarchy. + +For a complete ADC, apply the bank constraint independently to both DCO/FCO/data +groups. Calibration does not replace `create_clock`, DDR rise/fall input delays, +clock-placement review, or min/max timing analysis. + +At minimum, inspect: + +```tcl +report_clocks +check_timing -verbose +report_timing -from [get_ports ] -max_paths 50 +report_timing -from [get_ports ] -delay_type min -max_paths 50 +report_methodology +report_cdc -details +``` + +Correlate implemented timing margin with measured eyes. Do not treat a wide +simulation or hardware eye as evidence that an unconstrained path is safe. + +## Suggested Repository Migration Sequence + +For firmware maintained outside the SURF repository: + +1. **Pin the current working revision.** Record the existing SURF commit, + target, FPGA part, ADC mode, delay settings, and known-good bitstream. +2. **Inventory dependencies.** Search RTL, Python, generated maps, scripts, + YAML, notebooks, and constraints for legacy entity names and register paths. +3. **Evaluate migration gates.** Resolve reduced channels, external UltraScale + clocks, `adcReady`, runtime inversion, and DCO startup ordering first. +4. **Expand address windows.** Reserve `0x1000` per bank or `0x2000` per full + ADC in RTL and software before replacing register paths. +5. **Replace RTL and constraints.** Add stream reset and delay readiness, + select the PHY family, and seed initial delays. +6. **Replace PyRogue and scripts.** Update constructor arguments, paths, + snapshot use, counter clear, status, and calibration. +7. **Compile and simulate.** Check source selection, entity uniqueness, reset, + lock/relock, channel order, `tUser(0)`, overflow, and stopped-clock behavior. +8. **Implement and review timing.** Check clocks, placement, min/max input + timing, CDC, and constraints for every bank. +9. **Validate on hardware.** Compare samples with the known-good revision, run + calibration, power-cycle, and verify repeatability before deployment. +10. **Keep rollback available.** Do not remove the known-good SURF pin or saved + delay configuration until production validation is complete. + +Useful initial searches include: + +```bash +rg -n 'Ad9249ReadoutGroup|ChannelDelay|FrameDelay|LostLockCountReset|FreezeDebug' +rg -n 'USE_MMCME_G|adcBitClkIn|adcBitClkDiv4In|adcReady' +rg -n '0x20|0x30|0x34|0x38|0x40|0x80|0xA0' +``` + +## Validation Checklist + +Do not consider the migration complete until the applicable checks pass. + +### Build and static checks + +- Exactly one family implementation of `AdcDdrPhy` is loaded. +- `DEVICE_FAMILY_G` names a supported FPGA family and selects the expected delay width. +- Every bank has a non-overlapping `0x1000` AXI-Lite window. +- RTL `AXIL_BASE_ADDR_G` matches the address observed by the endpoint. +- PyRogue `deviceFamily` matches RTL `DEVICE_FAMILY_G`. +- All DCO, FCO, and data input paths are constrained for rising and falling + capture edges. +- 7-Series `IODELAY_GROUP` and `IDELAYCTRL.RDY` are correctly connected. +- Clock, methodology, CDC, and min/max timing reports have no unexplained ADC + paths or broad false-path exceptions. + +### Functional simulation + +- Reset and startup work with unrelated AXI, capture, and stream clocks. +- FCO locks from every bitslip phase and relocks after injected errors. +- Data/FCO delay writes affect exactly the requested lane and read back the + programmed setting. +- Channel numbering, `tDest`, right-justified sample bits, and physical lane + inversion match the old design. +- Unlocked samples assert `tUser(0)` and do not masquerade as valid aligned + data. +- Stream reset and overflow behavior are checked with the stream clock stopped + or slowed. +- Four-sample snapshots are coherent across every channel. + +### Hardware + +- Device identity/configuration and output coding are verified before capture. +- All FCO and data lanes produce bounded eyes with acceptable guard bands. +- Selected taps remain valid after relock and power cycle. +- Stream samples match an independent stimulus/reference for every channel. +- Lost-lock recovery, counter clearing, stopped-DCO behavior, and overflow + diagnostics work as documented. +- Representative voltage/temperature conditions are checked when required by + the application. + +## Compatibility Summary + +The public legacy class remains available as +`surf.devices.analog_devices.Ad9249ReadoutGroup`, and the legacy RTL remains in +its family directories. Projects do not need to migrate merely because they +update other SURF modules. + +Projects that do migrate should switch firmware, address allocation, PyRogue, +scripts, constraints, and characterized delay settings as one reviewed change. +Mixing the legacy software map with AdcDdr RTL, or AdcDdr software with +legacy RTL, is unsupported and will access the wrong registers. diff --git a/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd b/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd deleted file mode 100644 index e9daaf4ddf..0000000000 --- a/devices/AnalogDevices/ad9249/UltraScale/rtl/Ad9249ReadoutGroup2.vhd +++ /dev/null @@ -1,580 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC Readout Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9249Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9249ReadoutGroup2 is - generic ( - TPD_G : time := 1 ns; - SIM_DEVICE_G : string := "ULTRASCALE"; - NUM_CHANNELS_G : natural := 8; - SIMULATION_G : boolean := false; - DEFAULT_DELAY_G : slv(8 downto 0) := "000000000"; - ADC_INVERT_CH_G : slv(7 downto 0) := "00000000"); - port ( - -- AXI-Lite clock - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Asynchronous reset for adc deserializer - adcClkRst : in sl; - - -- DDR Serial Data from ADC - adcSerial : in Ad9249SerialGroupType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0) := (others => axiStreamMasterInit(AD9249_AXIS_CFG_G))); -end Ad9249ReadoutGroup2; - -architecture rtl of Ad9249ReadoutGroup2 is - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - delay : slv(8 downto 0); - delaySet : sl; - freezeDebug : sl; - readoutDebug0 : slv16Array(7 downto 0); - readoutDebug1 : slv16Array(7 downto 0); - lockedCountRst : sl; - invert : sl; - realign : sl; - minEyeWidth : slv(7 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - delay => DEFAULT_DELAY_G, - delaySet => '0', - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => '0', - realign => '1', - minEyeWidth => X"50"); - - signal lockedSync : sl; - signal lockedFallCount : slv(15 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type AdcRegType is record - errorDet : sl; - end record; - - constant ADC_REG_INIT_C : AdcRegType := ( - errorDet => '1'); - - - signal adcR : AdcRegType := ADC_REG_INIT_C; - signal adcRin : AdcRegType; - - - -- Local Signals - signal adcBitClk : sl; - signal adcBitRst : sl; - signal adcBitClkDiv4 : sl; - signal adcBitRstDiv4 : sl; - - signal adcFrame : slv(13 downto 0); - signal adcFrameValid : sl; - signal adcFrameSync : slv(13 downto 0); - signal adcFrameSyncValid : sl; - signal adcData : slv14Array(NUM_CHANNELS_G-1 downto 0); - signal adcDataValid : slv(NUM_CHANNELS_G-1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_G-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_G*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_G*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_G-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_G*16-1 downto 0); - signal debugDataTmp : slv16Array(7 downto 0) := (others => (others => '0')); - - signal invertSync : sl; - signal bitSlip : sl; - signal dlyLoad : sl; - signal dlyCfg : slv(8 downto 0); - signal enUsrDlyCfg : sl; - signal usrDlyCfg : slv(8 downto 0) := (others => '0'); - signal minEyeWidthSync : slv(7 downto 0); - signal lockingCntCfg : slv(23 downto 0) := ite(SIMULATION_G, X"000008", X"00FFFF"); - signal locked : sl; - signal realignSync : sl; - signal curDelay : slv(8 downto 0); - signal errorDetCount : slv(15 downto 0); - signal errorDet : sl; - - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - Synchronizer_locked : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => locked, - dataOut => lockedSync); - - SynchronizerOneShotCnt_locked_fall : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => locked, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount, - wrClk => adcBitClkDiv4, - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - SynchronizerOneShotCnt_2 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '1', - OUT_POLARITY_G => '1', - CNT_RST_EDGE_G => false, - CNT_WIDTH_G => 16) - port map ( - dataIn => errorDet, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => errorDetCount, - wrClk => adcBitClkDiv4, - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - - SynchronizerVector_FRAME : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => 14, - ADDR_WIDTH_G => 4) - port map ( - rst => axilRst, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, - din => adcFrame, - rd_clk => axilClk, - rd_en => adcFrameSyncValid, - valid => adcFrameSyncValid, - dout => adcFrameSync); - - U_SynchronizerVector_CUR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 9) - port map ( - clk => axilClk, -- [in] - rst => axilRst, -- [in] - dataIn => dlyCfg, -- [in] - dataOut => curDelay); -- [out] - - - -- AXIL to ADC clock - Synchronizer_INVERT : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkDiv4, - dataIn => axilR.invert, - dataOut => invertSync); - - Synchronizer_REALIGN : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 3) - port map ( - clk => adcBitClkDiv4, - asyncRst => axilR.realign, - syncRst => realignSync); - - Synchronizer_USR_DELAY_SET : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkDiv4, - rst => adcBitRstDiv4, - dataIn => axilR.delaySet, - dataOut => enUsrDlyCfg); - - U_SynchronizerVector_USR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 9) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - dataIn => axilR.delay, -- [in] - dataOut => usrDlyCfg); -- [out] - - U_SynchronizerVector_EYE_WIDTH : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - dataIn => axilR.minEyeWidth, -- [in] - dataOut => minEyeWidthSync); -- [out] - - - -------------------------------------------------------------------------------------------------- --- AXIL Interface -------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelay, debugDataTmp, debugDataValid, - errorDetCount, lockedFallCount, lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - v.delaySet := '0'; - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Write delay values to IDELAY primitives - -- Overriding gearbox aligner - -- All writes go to same r.delay register, - axiSlaveRegister(axilEp, X"00", 0, v.delay); - axiWrDetect(axilEp, X"00", v.delaySet); - axiSlaveRegisterR(axilEp, X"00", 0, curDelay); - - v.realign := '0'; - axiSlaveRegister(axilEp, X"20", 0, v.realign); - axiSlaveRegisterR(axilEp, X"30", 0, errorDetCount); - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - - -- Debug registers. Output the last 2 words received - for ch in 0 to 7 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - -------------------------------------------------------------------------------------------------- --- Create Clocks -------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFGDS - port map ( - I => adcSerial.dClkP, - IB => adcSerial.dClkN, - O => adcBitClk); - - - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClk, - asyncRst => adcClkRst, - syncRst => adcBitRst); - - - U_AdcBitClkRD4 : BUFGCE_DIV - generic map ( - BUFGCE_DIVIDE => 4, -- 1-8 - -- Programmable Inversion Attributes: Specifies built-in programmable inversion on specific pins - IS_CE_INVERTED => '0', -- Optional inversion for CE - IS_CLR_INVERTED => '0', -- Optional inversion for CLR - IS_I_INVERTED => '0') -- Optional inversion for I - port map ( - I => adcBitClk, - O => adcBitClkDiv4, - CE => '1', - CLR => '0'); - - - ADC_BITCLK_DIV4_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkDiv4, - asyncRst => adcClkRst, - syncRst => adcBitRstDiv4); - - -------------------------------------------------------------------------------------------------- --- Deserializers -------------------------------------------------------------------------------------------------- - - U_FRAME_DESERIALIZER : entity surf.Ad9249Deserializer - generic map ( - TPD_G => TPD_G, - SIM_DEVICE_G => SIM_DEVICE_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IDELAYCTRL_FREQ_G => 350.0, -- Check this - ADC_INVERT_CH_G => '0', - BIT_REV_G => '1') - port map ( - dClk => adcBitClk, - dRst => adcBitRst, - dClkDiv4 => adcBitClkDiv4, - dRstDiv4 => realignSync, - sDataP => adcSerial.fClkP, - sDataN => adcSerial.fClkN, - loadDelay => dlyLoad, - delay => dlyCfg, - bitSlip => bitSlip, - delayValueOut => open, - adcData => adcFrame, - adcValid => adcFrameValid); - - --------------------------------- --- Data Input, 8 channels --------------------------------- - GenData : for ch in NUM_CHANNELS_G-1 downto 0 generate - U_DATA_DESERIALIZER : entity surf.Ad9249Deserializer - generic map ( - TPD_G => TPD_G, - SIM_DEVICE_G => SIM_DEVICE_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IDELAYCTRL_FREQ_G => 350.0, -- Check this - ADC_INVERT_CH_G => ADC_INVERT_CH_G(ch), - BIT_REV_G => '1') -- Should maybe be '1' - port map ( - dClk => adcBitClk, - dRst => adcBitRst, - dClkDiv4 => adcBitClkDiv4, - dRstDiv4 => realignSync, - sDataP => adcSerial.chP(ch), - sDataN => adcSerial.chN(ch), - loadDelay => dlyLoad, - delay => dlyCfg, - bitSlip => bitSlip, - delayValueOut => open, - adcData => adcData(ch), - adcValid => adcDataValid(ch)); - end generate; - - - ---------------------------------------------------------------------------------------------- - -- Aligner - ---------------------------------------------------------------------------------------------- - U_SelectIoRxGearboxAligner_1 : entity surf.SelectIoRxGearboxAligner - generic map ( - TPD_G => TPD_G, - SIMULATION_G => SIMULATION_G, - CODE_TYPE_G => "LINE_CODE", - DLY_STEP_SIZE_G => ite(SIMULATION_G, 16, 1)) - port map ( - clk => adcBitClkDiv4, -- [in] - rst => adcBitRstDiv4, -- [in] - lineCodeValid => '1', -- [in] - lineCodeErr => adcR.errorDet, -- [in] - lineCodeDispErr => realignSync, -- [in] - linkOutOfSync => '0', -- [in] - rxHeaderValid => '0', -- [in] - rxHeader => (others => '0'), -- [in] - bitSlip => bitSlip, -- [out] - dlyLoad => dlyLoad, -- [out] - dlyCfg => dlyCfg, -- [out] - enUsrDlyCfg => enUsrDlyCfg, -- [in] - usrDlyCfg => usrDlyCfg, -- [in] - bypFirstBerDet => '1', -- [in] - minEyeWidth => minEyeWidthSync, -- [in] - lockingCntCfg => lockingCntCfg, -- [in] - errorDet => errorDet, -- [out] - locked => locked); -- [out] - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcFrameValid, adcR) is - variable v : AdcRegType; - begin - v := adcR; - - if (adcFrameValid = '1') then - v.errorDet := toSl(adcFrame /= "11111110000000"); - end if; - - adcRin <= v; - - end process adcComb; - - adcSeq : process (adcBitClkDiv4, adcBitRstDiv4) is - begin - if (adcBitRstDiv4 = '1') then - adcR <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkDiv4)) then - adcR <= adcRin after TPD_G; - end if; - end process adcSeq; - - - GLUE_COMB : process (adcData, invertSync, locked) is - begin - for ch in NUM_CHANNELS_G-1 downto 0 loop - if (locked = '1') then - -- Locked, output adc data - if invertSync = '1' then - -- Invert all bits but keep 2 LSBs clear - fifoWrData(ch) <= "00" & ("11111111111111" - adcData(ch)); - else - fifoWrData(ch) <= "00" & adcData(ch); - end if; - else - -- Not locked - fifoWrData(ch) <= (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_G-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_G*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRstDiv4, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_G*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRstDiv4, - wr_clk => adcBitClkDiv4, - wr_en => adcFrameValid, --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd new file mode 100644 index 0000000000..5ecdcdf37b --- /dev/null +++ b/devices/AnalogDevices/ad9249/core/Ad9249Readout.vhd @@ -0,0 +1,190 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Full-device AD9249 serialized readout +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9249Pkg.all; + +entity Ad9249Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + IODELAY_GROUP_0_G : string := "DEFAULT_GROUP_0"; + IODELAY_GROUP_1_G : string := "DEFAULT_GROUP_1"; + IDELAYCTRL_FREQ_G : real := 200.0; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_DELAY_INIT_G : NaturalArray(15 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(1 downto 0) := (others => 0); + ADC_INVERT_CH_G : slv(15 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in slv(1 downto 0) := (others => '0'); + adcSerial : in Ad9249SerialGroupArray(1 downto 0); + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(15 downto 0)); +end entity Ad9249Readout; + +architecture rtl of Ad9249Readout is + + constant AXIL_CONFIG_C : AxiLiteCrossbarMasterConfigArray(1 downto 0) := + genAxiLiteConfig(2, AXIL_BASE_ADDR_G, 13, 12); + + function bankDataDelay ( + values : NaturalArray; + bank : natural) + return NaturalArray is + variable result : NaturalArray(7 downto 0); + begin + for ch in 7 downto 0 loop + result(ch) := values((8*bank)+ch); + end loop; + return result; + end function bankDataDelay; + + function bankFcoDelay ( + values : NaturalArray; + bank : natural) + return NaturalArray is + variable result : NaturalArray(0 downto 0); + begin + result(0) := values(bank); + return result; + end function bankFcoDelay; + + function bankInvert (values : slv; bank : natural) return slv is + variable result : slv(7 downto 0); + begin + for ch in 7 downto 0 loop + result(ch) := values((8*bank)+ch); + end loop; + return result; + end function bankInvert; + + signal bankWriteMasters : AxiLiteWriteMasterArray(1 downto 0); + signal bankWriteSlaves : AxiLiteWriteSlaveArray(1 downto 0); + signal bankReadMasters : AxiLiteReadMasterArray(1 downto 0); + signal bankReadSlaves : AxiLiteReadSlaveArray(1 downto 0); + signal bank0Streams : AxiStreamMasterArray(7 downto 0); + signal bank1Streams : AxiStreamMasterArray(7 downto 0); + +begin + + ------------------------------------------------------------------------------------------------- + -- Decode one 8-KiB device window into independent 4-KiB bank register + -- regions. Each bank keeps its own DCO/FCO capture domain and AdcDdrCore. + ------------------------------------------------------------------------------------------------- + U_AxiLiteCrossbar : entity surf.AxiLiteCrossbar + generic map ( + TPD_G => TPD_G, + NUM_SLAVE_SLOTS_G => 1, + NUM_MASTER_SLOTS_G => 2, + MASTERS_CONFIG_G => AXIL_CONFIG_C) + port map ( + axiClk => axilClk, -- [in] + axiClkRst => axilRst, -- [in] + sAxiWriteMasters(0) => axilWriteMaster, -- [in] + sAxiWriteSlaves(0) => axilWriteSlave, -- [out] + sAxiReadMasters(0) => axilReadMaster, -- [in] + sAxiReadSlaves(0) => axilReadSlave, -- [out] + mAxiWriteMasters => bankWriteMasters, -- [out] + mAxiWriteSlaves => bankWriteSlaves, -- [in] + mAxiReadMasters => bankReadMasters, -- [out] + mAxiReadSlaves => bankReadSlaves); -- [in] + + U_Bank0 : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_CONFIG_C(0).baseAddr, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_0_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_DELAY_INIT_G => bankDataDelay(DATA_DELAY_INIT_G, 0), + FCO_DELAY_INIT_G => bankFcoDelay(FCO_DELAY_INIT_G, 0), + ADC_INVERT_CH_G => bankInvert(ADC_INVERT_CH_G, 0), + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => bankWriteMasters(0), -- [in] + axilWriteSlave => bankWriteSlaves(0), -- [out] + axilReadMaster => bankReadMasters(0), -- [in] + axilReadSlave => bankReadSlaves(0), -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy(0), -- [in] + adcSerial => adcSerial(0), -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => bank0Streams); -- [out] + + U_Bank1 : entity surf.Ad9249ReadoutBank + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_CONFIG_C(1).baseAddr, + NUM_CHANNELS_G => 8, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_1_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_DELAY_INIT_G => bankDataDelay(DATA_DELAY_INIT_G, 1), + FCO_DELAY_INIT_G => bankFcoDelay(FCO_DELAY_INIT_G, 1), + ADC_INVERT_CH_G => bankInvert(ADC_INVERT_CH_G, 1), + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilWriteMaster => bankWriteMasters(1), -- [in] + axilWriteSlave => bankWriteSlaves(1), -- [out] + axilReadMaster => bankReadMasters(1), -- [in] + axilReadSlave => bankReadSlaves(1), -- [out] + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy(1), -- [in] + adcSerial => adcSerial(1), -- [in] + adcStreamClk => adcStreamClk, -- [in] + adcStreamRst => adcStreamRst, -- [in] + adcStreams => bank1Streams); -- [out] + + mapStreams : process (bank0Streams, bank1Streams) is + variable v : AxiStreamMasterArray(15 downto 0); + begin + for ch in 7 downto 0 loop + v(ch) := bank0Streams(ch); + v(ch).tDest := toSlv(ch, 8); + v(8+ch) := bank1Streams(ch); + v(8+ch).tDest := toSlv(8+ch, 8); + end loop; + adcStreams <= v; + end process mapStreams; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd new file mode 100644 index 0000000000..d53ddff0a7 --- /dev/null +++ b/devices/AnalogDevices/ad9249/core/Ad9249ReadoutBank.vhd @@ -0,0 +1,154 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Readout for one eight-channel AD9249 output bank +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9249Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9249ReadoutBank is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + NUM_CHANNELS_G : natural range 1 to 8 := 8; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_DELAY_INIT_G : NaturalArray(NUM_CHANNELS_G-1 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(0 downto 0) := (others => 0); + ADC_INVERT_CH_G : slv(7 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '0'; + adcSerial : in Ad9249SerialGroupType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0)); +end entity Ad9249ReadoutBank; + +architecture rtl of Ad9249ReadoutBank is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataWord : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal dataValid : slv(NUM_CHANNELS_G-1 downto 0); + signal sampleIn : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal fcoWord : Slv16Array(0 downto 0); + signal fcoValid : slv(0 downto 0); + + signal bitSlip : slv(0 downto 0); + signal dataDelay : AdcDdrDelayArray(NUM_CHANNELS_G-1 downto 0); + signal frameDelay : AdcDdrDelayArray(0 downto 0); + +begin + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + SERIALIZATION_FACTOR_G => 14, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + -- Ranged-choice (not "others =>") avoids a VCS elaborator segfault when + -- driving a generic-sized NaturalArray generic (DATA_LANES_G-1 downto 0). + DATA_FCO_MAP_G => (NUM_CHANNELS_G-1 downto 0 => 0)) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adcSerial.dClkP, -- [in] + dClkN => adcSerial.dClkN, -- [in] + fcoP => (0 => adcSerial.fClkP), -- [in] + fcoN => (0 => adcSerial.fClkN), -- [in] + dataP => adcSerial.chP(NUM_CHANNELS_G-1 downto 0), -- [in] + dataN => adcSerial.chN(NUM_CHANNELS_G-1 downto 0), -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + GEN_CHANNEL : for i in NUM_CHANNELS_G-1 downto 0 generate + sampleIn(i) <= "00" & ite(ADC_INVERT_CH_G(i) = '1', + not dataWord(i)(13 downto 0), dataWord(i)(13 downto 0)); + end generate GEN_CHANNEL; + + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + CHANNELS_G => NUM_CHANNELS_G, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => adcStreams); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/ruckus.tcl b/devices/AnalogDevices/ad9249/ruckus.tcl index 35ed2898d2..3b236cbf5a 100644 --- a/devices/AnalogDevices/ad9249/ruckus.tcl +++ b/devices/AnalogDevices/ad9249/ruckus.tcl @@ -2,8 +2,9 @@ source $::env(RUCKUS_PROC_TCL) # Load Source Code -loadSource -lib surf -dir "$::DIR_PATH/core" +loadSource -lib surf -dir "$::DIR_PATH/core" -fileType "VHDL 2008" loadSource -lib surf -sim_only -dir "$::DIR_PATH/tb" +loadRuckusTcl "$::DIR_PATH/sim" # Get the family type set family [getFpgaArch] @@ -23,4 +24,4 @@ if { ${family} eq {kintexu} || ${family} eq {zynquplus} || ${family} eq {zynquplusRFSOC} } { loadRuckusTcl "$::DIR_PATH/UltraScale" -} \ No newline at end of file +} diff --git a/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd b/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd new file mode 100644 index 0000000000..45df47bef2 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd @@ -0,0 +1,259 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9249 device simulation +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 24 ns; + DIVCLK_DIVIDE_G : integer := 1; + CLKFBOUT_MULT_G : integer := 49; + CLK_DCO_DIVIDE_G : integer := 49; + CLK_FCO_DIVIDE_G : integer := 7; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(15 downto 0) := (others => 0 ns); + FCO_SKEW_G : TimeArray(1 downto 0) := (others => 0 ns); + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + vin : in RealArray(15 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdio : inout sl; + csb : in slv(1 downto 0)); +end entity Ad9249Sim; + +architecture behavioral of Ad9249Sim is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/28; + constant CONVERSION_LATENCY_C : positive := 16; + + type Slv13Array is array (natural range <>) of slv(12 downto 0); + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of + Slv16Array(15 downto 0); + + signal cfgWrEn : slv(1 downto 0); + signal cfgAddr : Slv13Array(1 downto 0); + signal cfgWrData : Slv32Array(1 downto 0); + signal cfgByteValid : Slv4Array(1 downto 0); + signal cfgRdByte : Slv8Array(1 downto 0); + signal cfgRdData : Slv32Array(1 downto 0); + signal normalData : Slv16Array(15 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(15 downto 0); + signal sampleData : Slv16Array(15 downto 0); + signal sampleValid : slv(1 downto 0); + signal serialData : Slv8Array(1 downto 0); + signal serialDco : slv(1 downto 0); + signal serialFco : slv(1 downto 0); + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9249Sim requires CLK_PERIOD_G >= 28 simulator time units" + severity failure; + + -- These clock-manager generics are retained for source compatibility with + -- the legacy model. Device timing is derived directly from CLK_PERIOD_G. + assert DIVCLK_DIVIDE_G > 0 and CLKFBOUT_MULT_G > 0 and + CLK_DCO_DIVIDE_G > 0 and CLK_FCO_DIVIDE_G > 0 + report "Ad9249Sim clock compatibility generics must be positive" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9249Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9249Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 15 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9249Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + GEN_FCO_TIMING_CHECK : for i in 1 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := FCO_PHASE_G+FCO_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9249Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_FCO_TIMING_CHECK; + + GEN_NORMAL_DATA : for i in 15 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can briefly produce NaN at time zero. + -- Substitute low scale because adcConversion() cannot clamp NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9249 specifies 16 sample clocks of conversion latency. Fifteen + -- stages are explicit here; the coherent serializer-frame capture below + -- contributes the final sample clock at the output pins. Test patterns are + -- generated after this normal-conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + GEN_GROUP : for g in 1 downto 0 generate + constant LOW_CH_C : natural := 8*g; + constant HIGH_CH_C : natural := LOW_CH_C+7; + begin + + cfgRdData(g) <= x"000000" & cfgRdByte(g); + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb(g), -- [in] + wrEn => cfgWrEn(g), -- [out] + rdEn => open, -- [out] + addr => cfgAddr(g), -- [out] + wrData => cfgWrData(g), -- [out] + byteValid => cfgByteValid(g), -- [out] + rdData => cfgRdData(g)); -- [in] + + U_Core : entity surf.Ad9249SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData(HIGH_CH_C downto LOW_CH_C), -- [in] + cfgWrEn => cfgWrEn(g), -- [in] + cfgAddr => cfgAddr(g)(8 downto 0), -- [in] + cfgWrData => cfgWrData(g)(7 downto 0), -- [in] + cfgRdData => cfgRdByte(g), -- [out] + sampleData => sampleData(HIGH_CH_C downto LOW_CH_C), -- [out] + sampleValid => sampleValid(g)); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Latch one coherent bank word per frame before applying transition-only + -- static timing and bounded alternating jitter. DCO remains binary and + -- jitter-free; the common bias makes negative jitter schedulable. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : slv(7 downto 0) := (others => '0'); + variable fcoCurrent : sl := '0'; + variable dataJitterPositive : BooleanArray(7 downto 0) := (others => false); + variable fcoJitterPositive : boolean := false; + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData(g) <= (others => '0'); + serialDco(g) <= '0'; + serialFco(g) <= '0'; + wait until rising_edge(clkP); + loop + -- sampleData updates after the encode edge. Capturing it here both + -- prevents checkerboard tearing and supplies the last latency cycle. + for ch in 7 downto 0 loop + frameData(ch) := sampleData(LOW_CH_C+ch); + end loop; + for bitindex in 13 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex); + if (nextData /= dataCurrent(ch)) then + if (dataJitterPositive(ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive(ch) := not dataJitterPositive(ch); + serialData(g)(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+ + DATA_SKEW_G(LOW_CH_C+ch)+edgeJitter; + dataCurrent(ch) := nextData; + end if; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + if (nextFco /= fcoCurrent) then + if (fcoJitterPositive) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive := not fcoJitterPositive; + serialFco(g) <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(g)+edgeJitter; + fcoCurrent := nextFco; + end if; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco(g) <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + end generate GEN_GROUP; + + dP <= serialData(1) & serialData(0); + dN <= not (serialData(1) & serialData(0)); + dcoP <= serialDco; + dcoN <= not serialDco; + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd b/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd new file mode 100644 index 0000000000..1e2bf21f47 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd @@ -0,0 +1,323 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9249 output-bank simulation core +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9249SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9249SimCore; + +architecture rtl of Ad9249SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + constant SPI_CONFIG_ADDR_C : slv(8 downto 0) := '0' & X"00"; + constant CHIP_ID_ADDR_C : slv(8 downto 0) := '0' & X"01"; + constant CHIP_GRADE_ADDR_C : slv(8 downto 0) := '0' & X"02"; + constant DEVICE_INDEX2_ADDR_C : slv(8 downto 0) := '0' & X"04"; + constant DEVICE_INDEX1_ADDR_C : slv(8 downto 0) := '0' & X"05"; + constant POWER_MODE_ADDR_C : slv(8 downto 0) := '0' & X"08"; + constant TEST_MODE_ADDR_C : slv(8 downto 0) := '0' & X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(8 downto 0) := '0' & X"14"; + constant OUTPUT_PHASE_ADDR_C : slv(8 downto 0) := '0' & X"16"; + constant USER_PATTERN1_LSB_C : slv(8 downto 0) := '0' & X"19"; + constant USER_PATTERN1_MSB_C : slv(8 downto 0) := '0' & X"1A"; + constant USER_PATTERN2_LSB_C : slv(8 downto 0) := '0' & X"1B"; + constant USER_PATTERN2_MSB_C : slv(8 downto 0) := '0' & X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(8 downto 0) := '0' & X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(8 downto 0) := '0' & X"22"; + constant TRANSFER_ADDR_C : slv(8 downto 0) := '0' & X"FF"; + constant RESOLUTION_RATE_ADDR_C : slv(8 downto 0) := '1' & X"00"; + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(13 downto 0); + userPatternB : slv(13 downto 0); + outputPhase : slv(3 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputPhase => "0011", + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + + type RegType is record + rdData : slv(7 downto 0); + selectMask : slv(9 downto 0); + channel : ChannelArray(7 downto 0); + outputInvert : sl; + outputFormat : sl; + lsbFirst : sl; + powerMode : slv(2 downto 0); + resolution : slv(6 downto 0); + tmpResolution : slv(6 downto 0); + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => '1'), + channel => (others => CHANNEL_INIT_C), + outputInvert => '0', + outputFormat => '0', + lsbFirst => '0', + powerMode => "000", + resolution => (others => '0'), + tmpResolution => (others => '0'), + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- AD9249 writes take effect immediately except for the resolution/sample-rate + -- override at 0x100. The model represents one independently selected output + -- bank; a full device uses two instances. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + + if (sampleRst = '1') then + v := REG_INIT_C; + else + if (cfgWrEn = '1') then + case cfgAddr is + when SPI_CONFIG_ADDR_C => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when DEVICE_INDEX2_ADDR_C => + v.selectMask(7 downto 4) := cfgWrData(3 downto 0); + when DEVICE_INDEX1_ADDR_C => + v.selectMask(3 downto 0) := cfgWrData(3 downto 0); + v.selectMask(9 downto 8) := cfgWrData(5 downto 4); + when POWER_MODE_ADDR_C => + v.powerMode := cfgWrData(2 downto 0); + when TEST_MODE_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userMode := cfgWrData(7 downto 6); + v.channel(i).resetPn23 := cfgWrData(5); + v.channel(i).resetPn9 := cfgWrData(4); + v.channel(i).testMode := cfgWrData(3 downto 0); + if (cfgWrData(4) = '1') then + v.channel(i).pn9 := PN9_SEED_C; + end if; + if (cfgWrData(5) = '1') then + v.channel(i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + when OUTPUT_MODE_ADDR_C => + v.outputInvert := cfgWrData(2); + v.outputFormat := cfgWrData(0); + when OUTPUT_PHASE_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).outputPhase := cfgWrData(3 downto 0); + end if; + end loop; + when USER_PATTERN1_LSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternA(7 downto 0) := cfgWrData; + end if; + end loop; + when USER_PATTERN1_MSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternA(13 downto 8) := cfgWrData(5 downto 0); + end if; + end loop; + when USER_PATTERN2_LSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternB(7 downto 0) := cfgWrData; + end if; + end loop; + when USER_PATTERN2_MSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).userPatternB(13 downto 8) := cfgWrData(5 downto 0); + end if; + end loop; + when SERIAL_OUTPUT_ADDR_C => + assert cfgWrData(2 downto 0) = "000" + report "Ad9249SimCore supports only 14-bit serial output" + severity failure; + v.lsbFirst := cfgWrData(7); + when CHANNEL_STATUS_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i).outputReset := cfgWrData(1); + v.channel(i).powerDown := cfgWrData(0); + end if; + end loop; + when TRANSFER_ADDR_C => + if (cfgWrData(0) = '1') then + v.resolution := r.tmpResolution; + end if; + when RESOLUTION_RATE_ADDR_C => + v.tmpResolution := cfgWrData(6 downto 0); + when others => null; + end case; + end if; + + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => + word := normalData(i)(13 downto 0); + if (r.outputFormat = '1') then + word(13) := not word(13); + end if; + when "0001" => word := "10000000000000"; + when "0010" => word := (others => '1'); + when "0011" => word := (others => '0'); + when "0100" => + for j in 13 downto 0 loop + word(j) := ite((j mod 2) = 0, r.toggle, not r.toggle); + end loop; + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14); + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14); + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := (others => r.toggle); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "10101010101010"; + when "1010" => word := "00000001111111"; + when "1011" => word := "10000000000000"; + when "1100" => word := "10100001100111"; + when others => word := (others => '0'); + end case; + if (r.outputInvert = '1') then + word := not word; + end if; + if (r.lsbFirst = '1') then + word := bitReverse(word); + end if; + if (r.powerMode /= "000" or r.channel(i).powerDown = '1' or + r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := "00" & word; + end loop; + end if; + end if; + + -- Local-register reads return the lowest-numbered selected channel. This + -- is Channel A when the default mask selects all channels. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + active := r.channel(i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"92"; + when CHIP_GRADE_ADDR_C => v.rdData := X"30"; + when DEVICE_INDEX2_ADDR_C => v.rdData(3 downto 0) := r.selectMask(7 downto 4); + when DEVICE_INDEX1_ADDR_C => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); + when POWER_MODE_ADDR_C => v.rdData(2 downto 0) := r.powerMode; + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => + v.rdData(2) := r.outputInvert; + v.rdData(0) := r.outputFormat; + when OUTPUT_PHASE_ADDR_C => v.rdData(3 downto 0) := active.outputPhase; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); + when SERIAL_OUTPUT_ADDR_C => v.rdData(7) := r.lsbFirst; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when TRANSFER_ADDR_C => v.rdData := (others => '0'); + when RESOLUTION_RATE_ADDR_C => v.rdData(6 downto 0) := r.resolution; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/sim/ruckus.tcl b/devices/AnalogDevices/ad9249/sim/ruckus.tcl new file mode 100644 index 0000000000..876384e050 --- /dev/null +++ b/devices/AnalogDevices/ad9249/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9249SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9249Sim.vhd" diff --git a/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd b/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd index 3883ba05af..f4e4809fb9 100644 --- a/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd +++ b/devices/AnalogDevices/ad9249/tb/Ad9249Group.vhd @@ -93,7 +93,7 @@ architecture behavioral of Ad9249Group is clockDivRatio => "000", outputLvds => '0', outputInvert => '0', - binFormat => "00", + binFormat => "01", termination => "00", driveStrength => '0', lsbFirst => '0', diff --git a/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd new file mode 100644 index 0000000000..9b3a169469 --- /dev/null +++ b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd @@ -0,0 +1,60 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9249SimCore +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9249SimCoreWrapper; + +architecture rtl of Ad9249SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate GEN_FLATTEN; + + U_DUT : entity surf.Ad9249SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd new file mode 100644 index 0000000000..d1f9972edf --- /dev/null +++ b/devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd @@ -0,0 +1,85 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9249Sim +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9249SimWrapper is + generic ( + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_LANE0_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(255 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in slv(1 downto 0)); +end entity Ad9249SimWrapper; + +architecture rtl of Ad9249SimWrapper is + + signal vin : RealArray(15 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 15 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= to_x01z(sdio); + + U_DUT : entity surf.Ad9249Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => (0 => FCO_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dP, -- [out] + dN => dN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd new file mode 100644 index 0000000000..d049d2c9d4 --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Config.vhd @@ -0,0 +1,206 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Imported AD9252 support. +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; +use ieee.std_logic_arith.all; +use ieee.std_logic_unsigned.all; + +library unisim; +use unisim.vcomponents.all; + + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; + +entity Ad9252Config is + + generic ( + TPD_G : time := 1 ns; + SIMULATION_G : boolean := false); + + port ( + axiClk : in sl; + axiRst : in sl; + + axiReadMaster : in AxiLiteReadMasterType; + axiReadSlave : out AxiLiteReadSlaveType; + axiWriteMaster : in AxiLiteWriteMasterType; + axiWriteSlave : out AxiLiteWriteSlaveType; + + adcSclk : out sl; + adcSdio : inout sl; + adcCsb : out sl + ); + +end entity Ad9252Config; + +architecture rtl of Ad9252Config is + + -- AdcCore Outputs + signal rdData : slv(23 downto 0); + signal rdEn : sl; + + -- Adc Core Chip IO + signal coreSclk : sl; + signal coreSDin : sl; + signal coreSDout : sl; + signal coreCsb : sl; + + type StateType is (WAIT_AXI_TXN_S, WAIT_CYCLE_S, WAIT_SPI_TXN_DONE_S); + + -- Registers + type RegType is record + state : StateType; + axiReadSlave : AxiLiteReadSlaveType; + axiWriteSlave : AxiLiteWriteSlaveType; + -- Adc Core Inputs + wrData : slv(23 downto 0); + wrEn : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + state => WAIT_AXI_TXN_S, + axiReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axiWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, + wrData => (others => '0'), + wrEn => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + comb : process (axiRst, axiReadMaster, axiWriteMaster, r, rdData, rdEn) is + variable v : RegType; + variable axiStatus : AxiLiteStatusType; + begin + v := r; + + axiSlaveWaitTxn(axiWriteMaster, axiReadMaster, v.axiWriteSlave, v.axiReadSlave, axiStatus); + + case (r.state) is + when WAIT_AXI_TXN_S => + + if (axiStatus.writeEnable = '1') then + v.wrData(23) := '0'; -- Write bit + v.wrData(22 downto 21) := "00"; -- Number of bytes (1) + v.wrData(20 downto 16) := "00000"; -- Unused address bits + v.wrData(15 downto 8) := axiWriteMaster.awaddr(9 downto 2); -- Address + v.wrData(7 downto 0) := axiWriteMaster.wdata(7 downto 0); -- Data + v.wrEn := '1'; + v.state := WAIT_CYCLE_S; + end if; + + if (axiStatus.readEnable = '1') then + v.wrData(23) := '1'; -- read bit + v.wrData(22 downto 21) := "00"; -- Number of bytes (1) + v.wrData(20 downto 16) := "00000"; -- Unused address bits + v.wrData(15 downto 8) := axiReadMaster.araddr(9 downto 2); -- Address + v.wrData(7 downto 0) := (others => '1'); -- Make bus float to Z so slave can + -- drive during data segment + v.wrEn := '1'; + v.state := WAIT_CYCLE_S; + end if; + + when WAIT_CYCLE_S => + -- Wait 1 cycle for rdEn to drop + v.wrEn := '0'; + v.state := WAIT_SPI_TXN_DONE_S; + + when WAIT_SPI_TXN_DONE_S => + + if (rdEn = '1') then + v.state := WAIT_AXI_TXN_S; + if (r.wrData(23) = '0') then + -- Finish write + axiSlaveWriteResponse(v.axiWriteSlave); + else + -- Finish read + v.axiReadSlave.rdata := (others => '0'); + v.axiReadSlave.rdata(7 downto 0) := rdData(7 downto 0); + axiSlaveReadResponse(v.axiReadSlave); + end if; + end if; + + when others => null; + end case; + + if (axiRst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + axiWriteSlave <= r.axiWriteSlave; + axiReadSlave <= r.axiReadSlave; + + end process comb; + + seq : process (axiClk) is + begin + if (rising_edge(axiClk)) then + r <= rin after TPD_G; + end if; + end process seq; + + SpiMaster_1 : entity surf.SpiMaster + generic map ( + TPD_G => TPD_G, + NUM_CHIPS_G => 1, + DATA_SIZE_G => 24, + CPHA_G => '0', -- Sample on leading edge + CPOL_G => '0', -- Sample on rising edge + CLK_PERIOD_G => 8.0E-9, + SPI_SCLK_PERIOD_G => ite(SIMULATION_G, 100.0E-9, 100.0E-6)) + port map ( + clk => axiClk, + sRst => axiRst, + chipSel => "0", + wrEn => r.wrEn, + wrData => r.wrData, + rdEn => rdEn, + rdData => rdData, + spiCsL(0) => coreCsb, + spiSclk => coreSclk, + spiSdi => coreSDout, + spiSdo => coreSDin); + + -- Bus lines float to Z when not being driven to '0'. + -- Lines should all have resistor pullups off chip + SCLK_OBUFT : OBUFT + port map ( + I => '0', + O => adcSclk, + T => coreSclk); + + SDIO_IOBUFT : IOBUF + port map ( + I => '0', + O => coreSDin, + IO => adcSdio, + T => coreSDout); + + CSB_OBUFT : OBUFT + port map ( + I => '0', + O => adcCsb, + T => coreCsb); + +-- adcSclk <= '0' when coreSclk = '0' else 'Z'; +-- adcSdio <= '0' when coreSDout = '0' else 'Z'; +-- coreSDin <= to_x01z(adcSdio); +-- adcCsb <= '0' when coreCsb = '0' else 'Z'; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd new file mode 100644 index 0000000000..756ee3579c --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Pkg.vhd @@ -0,0 +1,35 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9252 serialized pin-interface types +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package Ad9252Pkg is + + -- One AD9252 bank has a shared differential DCO/FCO pair and eight + -- differential serialized data lanes. Array index zero corresponds to ADC channel zero. + type Ad9252SerialType is record + fClkP : sl; -- Frame clock, positive input + fClkN : sl; + dClkP : sl; -- Data clock, positive input + dClkN : sl; + chP : slv(7 downto 0); -- Serialized data, positive inputs + chN : slv(7 downto 0); + end record Ad9252SerialType; + + type Ad9252SerialArray is array (natural range <>) of Ad9252SerialType; + +end package Ad9252Pkg; diff --git a/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd new file mode 100644 index 0000000000..638e7b7ed9 --- /dev/null +++ b/devices/AnalogDevices/ad9252/core/Ad9252Readout.vhd @@ -0,0 +1,156 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9252 serialized readout +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9252Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9252Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + NUM_CHANNELS_G : natural range 1 to 8 := 8; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_DELAY_INIT_G : NaturalArray(NUM_CHANNELS_G-1 downto 0) := (others => 5); + FCO_DELAY_INIT_G : NaturalArray(0 downto 0) := (others => 5); + ADC_INVERT_CH_G : slv(7 downto 0) := (others => '0'); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '0'; + adc : in Ad9252SerialType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(NUM_CHANNELS_G-1 downto 0)); +end entity Ad9252Readout; + +architecture rtl of Ad9252Readout is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataWord : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal dataValid : slv(NUM_CHANNELS_G-1 downto 0); + signal sampleIn : Slv16Array(NUM_CHANNELS_G-1 downto 0); + signal fcoWord : Slv16Array(0 downto 0); + signal fcoValid : slv(0 downto 0); + + signal bitSlip : slv(0 downto 0); + signal dataDelay : AdcDdrDelayArray(NUM_CHANNELS_G-1 downto 0); + signal frameDelay : AdcDdrDelayArray(0 downto 0); + +begin + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + SERIALIZATION_FACTOR_G => 14, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => (others => 0)) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adc.dClkP, -- [in] + dClkN => adc.dClkN, -- [in] + fcoP => (0 => adc.fClkP), -- [in] + fcoN => (0 => adc.fClkN), -- [in] + dataP => adc.chP(NUM_CHANNELS_G-1 downto 0), -- [in] + dataN => adc.chN(NUM_CHANNELS_G-1 downto 0), -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + GEN_CHANNEL : for i in NUM_CHANNELS_G-1 downto 0 generate + sampleIn(i) <= "00" & ite(ADC_INVERT_CH_G(i) = '1', + not dataWord(i)(13 downto 0), dataWord(i)(13 downto 0)); + end generate GEN_CHANNEL; + + ------------------------------------------------------------------------------------------------- + -- Common alignment, register map, monitoring, and stream clock crossing + ------------------------------------------------------------------------------------------------- + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => NUM_CHANNELS_G, + FCO_LANES_G => 1, + CHANNELS_G => NUM_CHANNELS_G, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => adcStreams); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/ruckus.tcl b/devices/AnalogDevices/ad9252/ruckus.tcl new file mode 100644 index 0000000000..d9faa7daf1 --- /dev/null +++ b/devices/AnalogDevices/ad9252/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/core" -fileType "VHDL 2008" +loadRuckusTcl "$::DIR_PATH/sim" diff --git a/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd b/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd new file mode 100644 index 0000000000..98ce78b155 --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd @@ -0,0 +1,234 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9252 device simulation +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(7 downto 0) := (others => 0 ns); + FCO_SKEW_G : time := 0 ns; + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + + vin : in RealArray(7 downto 0); + + dP : out slv(7 downto 0); + dN : out slv(7 downto 0); + dcoP : out sl; + dcoN : out sl; + fcoP : out sl; + fcoN : out sl; + + sclk : in sl; + sdio : inout sl; + csb : in sl); +end entity Ad9252Sim; + +architecture behavioral of Ad9252Sim is + + constant FRAME_PATTERN_C : slv(13 downto 0) := "11111110000000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/28; + constant CONVERSION_LATENCY_C : positive := 8; + + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of + Slv16Array(7 downto 0); + + signal cfgWrEn : sl; + signal cfgAddr : slv(12 downto 0); + signal cfgWrData : slv(31 downto 0); + signal cfgByteValid : slv(3 downto 0); + signal cfgRdByte : slv(7 downto 0); + signal cfgRdData : slv(31 downto 0); + signal normalData : Slv16Array(7 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(7 downto 0); + signal sampleData : Slv16Array(7 downto 0); + signal serialData : slv(7 downto 0); + signal serialDco : sl; + signal serialFco : sl; + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9252Sim requires CLK_PERIOD_G >= 28 simulator time units" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9252Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9252Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 7 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9252Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + assert TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G-JITTER_G >= 0 ns and + FCO_PHASE_G+FCO_SKEW_G+JITTER_G < HALF_BIT_TIME_C + report "Ad9252Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + + GEN_NORMAL_DATA : for i in 7 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can briefly produce NaN at time zero. + -- Substitute low scale because adcConversion() cannot clamp NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9252 specifies eight sample clocks of conversion latency. Seven + -- stages are explicit here; the coherent serializer-frame capture below + -- contributes the final sample clock at the output pins. Test patterns are + -- generated after this normal-conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + cfgRdData <= x"000000" & cfgRdByte; + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb, -- [in] + wrEn => cfgWrEn, -- [out] + rdEn => open, -- [out] + addr => cfgAddr, -- [out] + wrData => cfgWrData, -- [out] + byteValid => cfgByteValid, -- [out] + rdData => cfgRdData); -- [in] + + U_Core : entity surf.Ad9252SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr(7 downto 0), -- [in] + cfgWrData => cfgWrData(7 downto 0), -- [in] + cfgRdData => cfgRdByte, -- [out] + sampleData => sampleData, -- [out] + sampleValid => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Latch one coherent word per frame before applying transition-only static + -- timing and bounded alternating jitter. DCO remains binary and jitter-free; + -- the common bias makes negative jitter schedulable. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : slv(7 downto 0) := (others => '0'); + variable fcoCurrent : sl := '0'; + variable dataJitterPositive : BooleanArray(7 downto 0) := (others => false); + variable fcoJitterPositive : boolean := false; + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData <= (others => '0'); + serialDco <= '0'; + serialFco <= '0'; + wait until rising_edge(clkP); + loop + -- sampleData updates after the encode edge. Capturing it here both + -- prevents checkerboard tearing and supplies the last latency cycle. + frameData := sampleData; + for bitindex in 13 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex); + if (nextData /= dataCurrent(ch)) then + if (dataJitterPositive(ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive(ch) := not dataJitterPositive(ch); + serialData(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(ch)+edgeJitter; + dataCurrent(ch) := nextData; + end if; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + if (nextFco /= fcoCurrent) then + if (fcoJitterPositive) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive := not fcoJitterPositive; + serialFco <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G+edgeJitter; + fcoCurrent := nextFco; + end if; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + dP <= serialData; + dN <= not serialData; + dcoP <= serialDco; + dcoN <= not serialDco; + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd new file mode 100644 index 0000000000..5121eef57a --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd @@ -0,0 +1,299 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9252 register and digital output model +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9252SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(7 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9252SimCore; + +architecture rtl of Ad9252SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + constant SPI_CONFIG_ADDR_C : slv(7 downto 0) := X"00"; + constant CHIP_ID_ADDR_C : slv(7 downto 0) := X"01"; + constant CHIP_GRADE_ADDR_C : slv(7 downto 0) := X"02"; + constant DEVICE_INDEX2_ADDR_C : slv(7 downto 0) := X"04"; + constant DEVICE_INDEX1_ADDR_C : slv(7 downto 0) := X"05"; + constant TEST_MODE_ADDR_C : slv(7 downto 0) := X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(7 downto 0) := X"14"; + constant OUTPUT_PHASE_ADDR_C : slv(7 downto 0) := X"16"; + constant USER_PATTERN1_LSB_C : slv(7 downto 0) := X"19"; + constant USER_PATTERN1_MSB_C : slv(7 downto 0) := X"1A"; + constant USER_PATTERN2_LSB_C : slv(7 downto 0) := X"1B"; + constant USER_PATTERN2_MSB_C : slv(7 downto 0) := X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(7 downto 0) := X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(7 downto 0) := X"22"; + constant TRANSFER_ADDR_C : slv(7 downto 0) := X"FF"; + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(13 downto 0); + userPatternB : slv(13 downto 0); + outputPhase : slv(3 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputPhase => "0011", + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + + type RegType is record + rdData : slv(7 downto 0); + selectMask : slv(9 downto 0); + staged : ChannelType; + channel : ChannelArray(7 downto 0); + outputInvert : sl; + stagedInvert : sl; + lsbFirst : sl; + stagedLsb : sl; + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => '0'), + staged => CHANNEL_INIT_C, + channel => (others => CHANNEL_INIT_C), + outputInvert => '0', + stagedInvert => '0', + lsbFirst => '0', + stagedLsb => '0', + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- Configuration writes and sample generation share sampleClk in this + -- logical model. Register writes update staging state; only device-update + -- register 0xFF publishes buffered settings to selected channels. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + + if (sampleRst = '1') then + v := REG_INIT_C; + else + ------------------------------------------------------------------------------------------- + -- AD9252 SPI-register behavior represented as a byte write port. + -- Device-index writes take effect immediately. Functional settings + -- are staged, matching the device's transfer/update requirement. + ------------------------------------------------------------------------------------------- + if (cfgWrEn = '1') then + case cfgAddr is + when SPI_CONFIG_ADDR_C => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when DEVICE_INDEX2_ADDR_C => + v.selectMask(7 downto 4) := cfgWrData(3 downto 0); + when DEVICE_INDEX1_ADDR_C => + v.selectMask(3 downto 0) := cfgWrData(3 downto 0); + v.selectMask(9 downto 8) := cfgWrData(5 downto 4); + when TEST_MODE_ADDR_C => + v.staged.userMode := cfgWrData(7 downto 6); + v.staged.resetPn23 := cfgWrData(5); + v.staged.resetPn9 := cfgWrData(4); + v.staged.testMode := cfgWrData(3 downto 0); + when OUTPUT_MODE_ADDR_C => + v.stagedInvert := cfgWrData(2); + when OUTPUT_PHASE_ADDR_C => + v.staged.outputPhase := cfgWrData(3 downto 0); + when USER_PATTERN1_LSB_C => + v.staged.userPatternA(7 downto 0) := cfgWrData; + when USER_PATTERN1_MSB_C => + v.staged.userPatternA(13 downto 8) := cfgWrData(5 downto 0); + when USER_PATTERN2_LSB_C => + v.staged.userPatternB(7 downto 0) := cfgWrData; + when USER_PATTERN2_MSB_C => + v.staged.userPatternB(13 downto 8) := cfgWrData(5 downto 0); + when SERIAL_OUTPUT_ADDR_C => + assert cfgWrData(2 downto 0) = "000" + report "Ad9252SimCore supports only 14-bit serial output" + severity failure; + v.stagedLsb := cfgWrData(7); + when CHANNEL_STATUS_ADDR_C => + v.staged.outputReset := cfgWrData(1); + v.staged.powerDown := cfgWrData(0); + when TRANSFER_ADDR_C => + -- Atomically publish global settings and the staged + -- channel image to every channel selected by 0x04/0x05. + if (cfgWrData(0) = '1') then + v.outputInvert := r.stagedInvert; + v.lsbFirst := r.stagedLsb; + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + v.channel(i) := r.staged; + -- Preserve live PN state across ordinary + -- transfers. An asserted reset instead installs + -- and holds the documented seed value. + v.channel(i).pn9 := r.channel(i).pn9; + v.channel(i).pn23 := r.channel(i).pn23; + if (r.staged.resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + end if; + if (r.staged.resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + end if; + when others => null; + end case; + end if; + + ------------------------------------------------------------------------------------------- + -- Generate one parallel ADC word per enabled sample. Pattern state + -- advances independently per channel, then global inversion/bit + -- order and per-channel suppression are applied in pin-data order. + ------------------------------------------------------------------------------------------- + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => word := normalData(i)(13 downto 0); + when "0001" => word := "10000000000000"; + when "0010" => word := (others => '1'); + when "0011" => word := (others => '0'); + when "0100" => + for j in 13 downto 0 loop + word(j) := ite((j mod 2) = 0, r.toggle, not r.toggle); + end loop; + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14); + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14); + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := (others => r.toggle); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "10101010101010"; + when "1010" => word := "00000001111111"; + when "1011" => word := "10000000000000"; + when "1100" => word := "10100001100111"; + when others => word := (others => '0'); + end case; + if (r.outputInvert = '1') then + word := not word; + end if; + if (r.lsbFirst = '1') then + word := bitReverse(word); + end if; + if (r.channel(i).powerDown = '1' or r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := "00" & word; + end loop; + end if; + end if; + + -- Local-register reads return the lowest-numbered selected channel. This + -- is Channel A when all channels are selected. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i) = '1') then + active := r.channel(i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"09"; + when CHIP_GRADE_ADDR_C => v.rdData := X"30"; + when DEVICE_INDEX2_ADDR_C => v.rdData(3 downto 0) := r.selectMask(7 downto 4); + when DEVICE_INDEX1_ADDR_C => v.rdData(5 downto 0) := r.selectMask(9 downto 8) & r.selectMask(3 downto 0); + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => v.rdData(2) := r.outputInvert; + when OUTPUT_PHASE_ADDR_C => v.rdData(3 downto 0) := active.outputPhase; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData(5 downto 0) := active.userPatternA(13 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData(5 downto 0) := active.userPatternB(13 downto 8); + when SERIAL_OUTPUT_ADDR_C => v.rdData(7) := r.lsbFirst; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/sim/ruckus.tcl b/devices/AnalogDevices/ad9252/sim/ruckus.tcl new file mode 100644 index 0000000000..3079db2061 --- /dev/null +++ b/devices/AnalogDevices/ad9252/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9252SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9252Sim.vhd" diff --git a/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd new file mode 100644 index 0000000000..b5d259bd1f --- /dev/null +++ b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd @@ -0,0 +1,60 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9252SimCore +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(7 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9252SimCoreWrapper; + +architecture rtl of Ad9252SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate; + + U_DUT : entity surf.Ad9252SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd new file mode 100644 index 0000000000..ba6b15094a --- /dev/null +++ b/devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd @@ -0,0 +1,85 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9252Sim +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9252SimWrapper is + generic ( + CLK_PERIOD_G : time := 24 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(127 downto 0); + dP : out slv(7 downto 0); + dN : out slv(7 downto 0); + dcoP : out sl; + dcoN : out sl; + fcoP : out sl; + fcoN : out sl; + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in sl); +end entity Ad9252SimWrapper; + +architecture rtl of Ad9252SimWrapper is + + signal vin : RealArray(7 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 7 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= sdio; + + U_DUT : entity surf.Ad9252Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => FCO_SKEW_PS_G*1 ps, + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dP, -- [out] + dN => dN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd deleted file mode 100755 index 9094af1b53..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Deserializer.vhd +++ /dev/null @@ -1,121 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: 14 bit DDR deserializer using 7 series IDELAYE2 and ISERDESE2. -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; - -library surf; -use surf.StdRtlPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Deserializer is - generic ( - TPD_G : time := 1 ns; - DEFAULT_DELAY_G : natural := 0; - IODELAY_GROUP_G : string; - IDELAYCTRL_FREQ_G : real := 200.0); - port ( - clkIo : in sl; - clkIoInv : in sl; - clkR : in sl; - rst : in sl; - slip : in sl; - - sysClk : in sl; - curDelay : out slv(4 downto 0); - setDelay : in slv(4 downto 0); - setValid : in sl; - - iData : in sl; - oData : out slv(7 downto 0)); -end entity Ad9681Deserializer; - -architecture rtl of Ad9681Deserializer is - - signal dlyData : sl; - - attribute IODELAY_GROUP : string; - attribute IODELAY_GROUP of U_DELAY : label is IODELAY_GROUP_G; - -begin - - -- ADC frame delay - U_DELAY : IDELAYE2 - generic map ( - DELAY_SRC => "IDATAIN", - HIGH_PERFORMANCE_MODE => "TRUE", - IDELAY_TYPE => "VAR_LOAD", - IDELAY_VALUE => DEFAULT_DELAY_G, -- Here - REFCLK_FREQUENCY => IDELAYCTRL_FREQ_G, - SIGNAL_PATTERN => "DATA") - port map ( - C => sysClk, - REGRST => '0', - LD => setValid, - CE => '0', - INC => '1', - CINVCTRL => '0', - CNTVALUEIN => setDelay, - IDATAIN => iData, - DATAIN => '0', - LDPIPEEN => '0', - DATAOUT => dlyData, - CNTVALUEOUT => curDelay); - - U_ISERDES_MASTER : ISERDESE2 - generic map ( - DATA_RATE => "DDR", - DATA_WIDTH => 8, - INTERFACE_TYPE => "NETWORKING", - DYN_CLKDIV_INV_EN => "FALSE", - DYN_CLK_INV_EN => "FALSE", - NUM_CE => 1, - OFB_USED => "FALSE", - IOBDELAY => "IFD", -- Use input at DDLY to output the data on Q1-Q6 - SERDES_MODE => "MASTER") - port map ( - Q1 => oData(0), - Q2 => oData(1), - Q3 => oData(2), - Q4 => oData(3), - Q5 => oData(4), - Q6 => oData(5), - Q7 => oData(6), - Q8 => oData(7), - SHIFTOUT1 => open, -- Cascade connection to Slave ISERDES - SHIFTOUT2 => open, -- Cascade connection to Slave ISERDES - BITSLIP => slip, -- 1-bit Invoke Bitslip. This can be used with any - -- DATA_WIDTH, cascaded or not. - CE1 => '1', -- 1-bit Clock enable input - CE2 => '1', -- 1-bit Clock enable input - CLK => clkIo, -- Fast Source Synchronous SERDES clock from BUFIO - CLKB => clkIoInv, -- Locally inverted clock - CLKDIV => clkR, -- Slow clock driven by BUFR - CLKDIVP => '0', - D => '0', - DDLY => dlyData, -- 1-bit Input signal from IODELAYE1. - RST => rst, -- 1-bit Asynchronous reset only. - SHIFTIN1 => '0', - SHIFTIN2 => '0', - -- unused connections - DYNCLKDIVSEL => '0', - DYNCLKSEL => '0', - OFB => '0', - OCLK => '0', - OCLKB => '0', - O => open); -- unregistered output of ISERDESE1 - -end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd deleted file mode 100644 index f408b53626..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681Readout.vhd +++ /dev/null @@ -1,639 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC Readout Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9681Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Readout is - generic ( - TPD_G : time := 1 ns; - SIMULATION_G : boolean := false; - IODELAY_GROUP_G : string := "DEFAULT_GROUP"; - IDELAYCTRL_FREQ_G : real := 200.0; - DEFAULT_DELAY_G : integer range 0 to 2**5-1 := 0; - INVERT_G : boolean := false; - NEGATE_G : boolean := false); - port ( - -- Master system clock, 125Mhz - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Reset for adc deserializer - adcClkRst : in sl; - - -- Serial Data from ADC - adcSerial : in Ad9681SerialType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(7 downto 0) := (others => axiStreamMasterInit(AD9681_AXIS_CFG_G))); -end Ad9681Readout; - -architecture rtl of Ad9681Readout is - - constant NUM_CHANNELS_C : natural := 8; - - type AdcDataArray is array (natural range <>) of slv8Array(7 downto 0); - type DelayDataArray is array (natural range <>) of slv5Array(7 downto 0); - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - usrDly : slv5Array(1 downto 0); - enUsrDly : sl; - freezeDebug : sl; - readoutDebug0 : slv16Array(NUM_CHANNELS_C-1 downto 0); - readoutDebug1 : slv16Array(NUM_CHANNELS_C-1 downto 0); - lockedCountRst : sl; - invert : sl; - negate : sl; - realign : sl; - minEyeWidth : slv(7 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - usrDly => (others => toSlv(DEFAULT_DELAY_G, 5)), - enUsrDly => '0', - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => toSl(INVERT_G), - negate => toSl(NEGATE_G), - realign => '1', - minEyeWidth => X"50"); - - signal lockedSync : slv(1 downto 0); - signal lockedFallCount : slv16Array(1 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type AdcRegType is record - errorDet : sl; - end record; - - type AdcRegArray is array (natural range <>) of AdcRegType; - - constant ADC_REG_INIT_C : AdcRegType := ( - errorDet => '0'); - - signal adcR : AdcRegArray(1 downto 0) := (others => ADC_REG_INIT_C); - signal adcRin : AdcRegArray(1 downto 0); - signal adcValid : slv(1 downto 0); - - - -- Local Signals - signal tmpAdcClk : slv(1 downto 0); - signal adcBitClkIo : slv(1 downto 0); - signal adcBitClkIoInv : slv(1 downto 0); - signal adcBitClkR : slv(1 downto 0); - signal adcBitRst : slv(1 downto 0); - - signal adcFramePad : slv(1 downto 0); - signal adcFrame : slv8Array(1 downto 0); - signal adcFrameSync : slv8Array(1 downto 0); - signal adcDataPad : slv8Array(1 downto 0); - signal adcData : AdcDataArray(1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_C-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal debugDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal invertSync : slv(1 downto 0); - signal negateSync : slv(1 downto 0); - signal bitSlip : slv(1 downto 0); - signal dlyLoad : slv(1 downto 0); - signal dlyCfg : Slv9Array(1 downto 0); - signal enUsrDlyCfg : slv(1 downto 0); - signal usrDlyCfg : slv9Array(1 downto 0) := (others => (others => '0')); - signal minEyeWidthSync : slv8Array(1 downto 0); - signal lockingCntCfg : slv(23 downto 0) := ite(SIMULATION_G, X"000008", X"00FFFF"); - signal locked : slv(1 downto 0); - signal realignSync : slv(1 downto 0); - signal curDelay : slv5Array(1 downto 0); - signal errorDetCount : slv16Array(1 downto 0); - signal errorDet : slv(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - SYNC_GEN : for i in 1 downto 0 generate - - Synchronizer_locked : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => locked(i), - dataOut => lockedSync(i)); - - - SynchronizerOneShotCnt_locked_fall : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => locked(i), - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - SynchronizerOneShotCnt_2 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '1', - OUT_POLARITY_G => '1', - CNT_RST_EDGE_G => false, - CNT_WIDTH_G => 16) - port map ( - dataIn => errorDet(i), - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => errorDetCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - - - SynchronizerVector_FRAME : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcFrame(i), - dataOut => adcFrameSync(i)); - - U_SynchronizerVector_CUR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 5) - port map ( - clk => axilClk, -- [in] - rst => axilRst, -- [in] - dataIn => dlyCfg(i)(8 downto 4), -- [in] - dataOut => curDelay(i)); -- [out] - - - - -- AXIL to ADC clock - Synchronizer_INVERT : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.invert, - dataOut => invertSync(i)); - - Synchronizer_NEGATE : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.negate, - dataOut => negateSync(i)); - - - Synchronizer_REALIGN : entity surf.SynchronizerEdge - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkR(i), - rst => adcBitRst(i), - dataIn => axilR.realign, - risingEdge => realignSync(i)); - - Synchronizer_USR_DELAY_SET : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 3) - port map ( - clk => adcBitClkR(i), - rst => adcBitRst(i), - dataIn => axilR.enUsrDly, - dataOut => enUsrDlyCfg(i)); - - U_SynchronizerVector_USR_DELAY : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 5) - port map ( - clk => adcBitClkR(i), -- [in] - rst => adcBitRst(i), -- [in] - dataIn => axilR.usrDly(i), -- [in] - dataOut => usrDlyCfg(i)(8 downto 4)); -- [out] - - U_SynchronizerVector_EYE_WIDTH : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => adcBitClkR(i), -- [in] - rst => adcBitRst(i), -- [in] - dataIn => axilR.minEyeWidth, -- [in] - dataOut => minEyeWidthSync(i)); -- [out] - - - - end generate SYNC_GEN; - - ------------------------------------------------------------------------------------------------- - -- AXIL Interface - ------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelay, debugDataTmp, debugDataValid, - errorDetCount, lockedFallCount, lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Overriding gearbox aligner - if (axilR.enUsrDly = '0') then - v.usrDly := curDelay; - end if; - - axiSlaveRegister(axilEp, X"00", 0, v.usrDly(0)); - axiSlaveRegisterR(axilEp, X"00", 0, curDelay(0)); - - axiSlaveRegister(axilEp, X"04", 0, v.usrDly(1)); - axiSlaveRegisterR(axilEp, X"04", 0, curDelay(1)); - - axiSlaveRegister(axilEp, X"20", 0, v.enUsrDly); - - axiSlaveRegister(axilEp, X"70", 0, v.realign); - axiSlaveRegisterR(axilEp, X"30", 0, errorDetCount(0)); - axiSlaveRegisterR(axilEp, X"34", 0, errorDetCount(1)); - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount(0)); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync(0)); - axiSlaveRegisterR(axilEp, X"54", 0, lockedFallCount(1)); - axiSlaveRegisterR(axilEp, X"54", 16, lockedSync(1)); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync(0)); - axiSlaveRegisterR(axilEp, X"58", 8, adcFrameSync(1)); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - axiSlaveRegister(axilEp, X"60", 1, v.negate); - - -- Debug registers. Output the last 2 words received - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - - GEN_PARTS : for i in 1 downto 0 generate - - ------------------------------------------------------------------------------------------------- - -- Create Clocks - ------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFDS - generic map ( - DIFF_TERM => true, - IOSTANDARD => "LVDS_25") - port map ( - I => adcSerial.dClkP(i), - IB => adcSerial.dClkN(i), - O => tmpAdcClk(i)); - - -- IO Clock - U_BUFIO : BUFIO - port map ( - I => tmpAdcClk(i), - O => adcBitClkIo(i)); - - adcBitClkIoInv(i) <= not adcBitClkIo(i); - - -- Regional clock - U_AdcBitClkR : BUFR - generic map ( - SIM_DEVICE => "7SERIES", - BUFR_DIVIDE => "4") - port map ( - I => tmpAdcClk(i), - O => adcBitClkR(i), - CE => '1', - CLR => '0'); - - -- Regional clock reset - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkR(i), - asyncRst => adcClkRst, - syncRst => adcBitRst(i)); - - - ------------------------------------------------------------------------------------------------- - -- Deserializers - ------------------------------------------------------------------------------------------------- - - -- Frame signal input - U_FrameIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.fClkP(i), - IB => adcSerial.fClkN(i), - O => adcFramePad(i)); - - U_FRAME_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => realignSync(0), - slip => bitSlip(i), - sysClk => adcBitClkR(0), - curDelay => open, --curDelayFrame(i), - setDelay => dlyCfg(i)(8 downto 4), - setValid => dlyLoad(i), --axilR.frameDelaySet(i), - iData => adcFramePad(i), - oData => adcFrame(i)); - - - - -------------------------------- - -- Data Input, 8 channels - -------------------------------- - GenData : for ch in NUM_CHANNELS_C-1 downto 0 generate - - -- Frame signal input - U_DataIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.chP(i)(ch), - IB => adcSerial.chN(i)(ch), - O => adcDataPad(i)(ch)); - - -- Optionally invert the pad input --- adcDataPad(i)(ch) <= adcDataPadOut(i)(ch) when ADC_INVERT_CH_G(i)(ch) = '0' else (not adcDataPadOut(i)(ch)); - - U_DATA_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => realignSync(0), - slip => bitSlip(i), - sysClk => adcBitClkR(0), - curDelay => open, --curDelayData(i)(ch), - setDelay => dlyCfg(i)(8 downto 4), - setValid => dlyLoad(i), --axilR.dataDelaySet(i)(ch), - iData => adcDataPad(i)(ch), - oData => adcData(i)(ch)); - end generate; - - - ---------------------------------------------------------------------------------------------- - -- Aligner - ---------------------------------------------------------------------------------------------- - U_SelectIoRxGearboxAligner_1 : entity surf.SelectIoRxGearboxAligner - generic map ( - TPD_G => TPD_G, - SIMULATION_G => SIMULATION_G, - CODE_TYPE_G => "LINE_CODE", - DLY_STEP_SIZE_G => 16) - port map ( - clk => adcBitClkR(0), -- [in] - rst => adcBitRst(0), -- [in] - lineCodeValid => '1', -- [in] - lineCodeErr => adcR(i).errorDet, -- [in] - lineCodeDispErr => realignSync(0), -- [in] - linkOutOfSync => '0', -- [in] - rxHeaderValid => '0', -- [in] - rxHeader => (others => '0'), -- [in] - bitSlip => bitSlip(i), -- [out] - dlyLoad => dlyLoad(i), -- [out] - dlyCfg => dlyCfg(i), -- [out] - enUsrDlyCfg => enUsrDlyCfg(i), -- [in] - usrDlyCfg => usrDlyCfg(i), -- [in] - bypFirstBerDet => '1', -- [in] - minEyeWidth => minEyeWidthSync(i), -- [in] - lockingCntCfg => lockingCntCfg, -- [in] - errorDet => errorDet(i), -- [out] - locked => locked(i)); -- [out] - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcR) is - variable v : AdcRegType; - begin - v := adcR(i); --- v.adcValid := '0'; - ---------------------------------------------------------------------------------------------- - -- Slip bits until correct alignment seen - ---------------------------------------------------------------------------------------------- - v.errorDet := toSl(adcFrame(i) /= "11110000"); - - adcRin(i) <= v; - - end process adcComb; - - adcSeq : process (adcBitClkR, adcBitRst) is - begin - if (adcBitRst(0) = '1') then - adcR(i) <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkR(0))) then - adcR(i) <= adcRin(i) after TPD_G; - end if; - end process adcSeq; - - end generate; - - GLUE_COMB : process (adcData, invertSync, locked, negateSync) is - variable tmp : slv16Array(7 downto 0); - begin - for ch in NUM_CHANNELS_C-1 downto 0 loop - if (locked = "11") then - tmp(ch) := adcData(1)(ch) & adcData(0)(ch); - -- Locked, output adc data - if invertSync(0) = '1' then - -- Invert all bits but keep 2 LSBs clear - tmp(ch) := (X"FFFF" - tmp(ch)) and X"FFFC"; - elsif (negateSync(0) = '1') then - if (tmp(ch) = X"8000") then - -- Negative 1 case - tmp(ch) := X"7FFC"; - else - tmp(ch) := (not(tmp(ch)(15 downto 2)) + 1) & "00"; - end if; - end if; - else - -- Not locked - tmp(ch) := (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - fifoWrData <= tmp; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_C-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd b/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd deleted file mode 100644 index b55e527d19..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/rtl/Ad9681ReadoutManual.vhd +++ /dev/null @@ -1,600 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: --- ADC ReadoutManual Controller --- Receives ADC Data from an AD9592 chip. --- Designed specifically for Xilinx 7 series FPGAs -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_arith.all; -use ieee.std_logic_unsigned.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.AxiLitePkg.all; -use surf.AxiStreamPkg.all; -use surf.Ad9681Pkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681ReadoutManual is - generic ( - TPD_G : time := 1 ns; - - IODELAY_GROUP_G : string := "DEFAULT_GROUP"; - IDELAYCTRL_FREQ_G : real := 200.0; - DEFAULT_DELAY_G : integer range 0 to 2**5-1 := 0; - INVERT_G : boolean := false; - NEGATE_G : boolean := false); - port ( - -- Master system clock, 125Mhz - axilClk : in sl; - axilRst : in sl; - - -- Axi Interface - axilWriteMaster : in AxiLiteWriteMasterType; - axilWriteSlave : out AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; - axilReadMaster : in AxiLiteReadMasterType; - axilReadSlave : out AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; - - -- Reset for adc deserializer - adcClkRst : in sl; - - -- Serial Data from ADC - adcSerial : in Ad9681SerialType; - - -- Deserialized ADC Data - adcStreamClk : in sl; - adcStreams : out AxiStreamMasterArray(7 downto 0) := (others => axiStreamMasterInit(AD9681_AXIS_CFG_G))); -end Ad9681ReadoutManual; - -architecture rtl of Ad9681ReadoutManual is - - constant NUM_CHANNELS_C : natural := 8; - - type AdcDataArray is array (natural range <>) of slv8Array(7 downto 0); - type DelayDataArray is array (natural range <>) of slv5Array(7 downto 0); - - ------------------------------------------------------------------------------------------------- - -- AXIL Registers - ------------------------------------------------------------------------------------------------- - type AxilRegType is record - axilWriteSlave : AxiLiteWriteSlaveType; - axilReadSlave : AxiLiteReadSlaveType; - delay : slv(4 downto 0); - dataDelaySet : slv8Array(1 downto 0); - frameDelaySet : slv(1 downto 0); - freezeDebug : sl; - readoutDebug0 : slv16Array(NUM_CHANNELS_C-1 downto 0); - readoutDebug1 : slv16Array(NUM_CHANNELS_C-1 downto 0); - lockedCountRst : sl; - invert : sl; - negate : sl; - relock : slv(1 downto 0); - curDelayFrame : slv5Array(1 downto 0); - curDelayData : DelayDataArray(1 downto 0); - end record; - - constant AXIL_REG_INIT_C : AxilRegType := ( - axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C, - axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, - delay => toSlv(DEFAULT_DELAY_G, 5), - dataDelaySet => (others => (others => '1')), - frameDelaySet => "11", - freezeDebug => '0', - readoutDebug0 => (others => (others => '0')), - readoutDebug1 => (others => (others => '0')), - lockedCountRst => '0', - invert => toSl(INVERT_G), - negate => toSl(NEGATE_G), - relock => "00", - curDelayFrame => (others => (others => '0')), - curDelayData => (others => (others => (others => '0')))); - - signal lockedSync : slv(1 downto 0); - signal lockedFallCount : slv16Array(1 downto 0); - - signal axilR : AxilRegType := AXIL_REG_INIT_C; - signal axilRin : AxilRegType; - - ------------------------------------------------------------------------------------------------- - -- ADC Readout Clocked Registers - ------------------------------------------------------------------------------------------------- - type SyncStateType is (RESET_S, SYNCING_S, SYNCED_S); - - type AdcRegType is record - state : SyncStateType; - slip : sl; - count : slv(5 downto 0); - locked : sl; - reset : sl; - end record; - - type AdcRegArray is array (natural range <>) of AdcRegType; - - constant ADC_REG_INIT_C : AdcRegType := ( - state => RESET_S, - slip => '0', - count => (others => '0'), - locked => '0', - reset => '1'); - - signal adcR : AdcRegArray(1 downto 0) := (others => ADC_REG_INIT_C); - signal adcRin : AdcRegArray(1 downto 0); - signal adcValid : slv(1 downto 0); - - - -- Local Signals - signal tmpAdcClk : slv(1 downto 0); - signal adcBitClkIo : slv(1 downto 0); - signal adcBitClkIoInv : slv(1 downto 0); - signal adcBitClkR : slv(1 downto 0); - signal adcBitRst : slv(1 downto 0); - - signal adcFramePad : slv(1 downto 0); - signal adcFrame : slv8Array(1 downto 0); - signal adcFrameSync : slv8Array(1 downto 0); - signal adcDataPad : slv8Array(1 downto 0); - signal adcData : AdcDataArray(1 downto 0); - - signal curDelayFrame : slv5Array(1 downto 0); - signal curDelayData : DelayDataArray(1 downto 0); - - signal fifoWrData : slv16Array(NUM_CHANNELS_C-1 downto 0); - signal fifoDataValid : sl; - signal fifoDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataIn : slv(NUM_CHANNELS_C*16-1 downto 0); - signal fifoDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal debugDataValid : sl; - signal debugDataOut : slv(NUM_CHANNELS_C*16-1 downto 0); - signal debugDataTmp : slv16Array(NUM_CHANNELS_C-1 downto 0); - - signal invertSync : slv(1 downto 0); - signal negateSync : slv(1 downto 0); - signal relockSync : slv(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Synchronize adcR.locked across to axil clock domain and count falling edges on it - ------------------------------------------------------------------------------------------------- - SYNC_GEN : for i in 1 downto 0 generate - - SynchronizerOneShotCnt_1 : entity surf.SynchronizerOneShotCnt - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '0', - CNT_RST_EDGE_G => true, - CNT_WIDTH_G => 16) - port map ( - dataIn => adcR(i).locked, - rollOverEn => '0', - cntRst => axilR.lockedCountRst, - dataOut => open, - cntOut => lockedFallCount(i), - wrClk => adcBitClkR(i), - wrRst => '0', - rdClk => axilClk, - rdRst => axilRst); - - Synchronizer_1 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcR(i).locked, - dataOut => lockedSync(i)); - - SynchronizerVec_1 : entity surf.SynchronizerVector - generic map ( - TPD_G => TPD_G, - STAGES_G => 2, - WIDTH_G => 8) - port map ( - clk => axilClk, - rst => axilRst, - dataIn => adcFrame(i), - dataOut => adcFrameSync(i)); - - Synchronizer_2 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.invert, - dataOut => invertSync(i)); - - Synchronizer_4 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), - dataIn => axilR.negate, - dataOut => negateSync(i)); - - - Synchronizer_3 : entity surf.Synchronizer - generic map ( - TPD_G => TPD_G, - STAGES_G => 2) - port map ( - clk => adcBitClkR(i), --- rst => axilRst, - dataIn => axilR.relock(i), - dataOut => relockSync(i)); - - - end generate SYNC_GEN; - - - - ------------------------------------------------------------------------------------------------- - -- AXIL Interface - ------------------------------------------------------------------------------------------------- - axilComb : process (adcFrameSync, axilR, axilReadMaster, axilRst, - axilWriteMaster, curDelayData, curDelayFrame, - debugDataTmp, debugDataValid, lockedFallCount, - lockedSync) is - variable v : AxilRegType; - variable axilEp : AxiLiteEndpointType; - begin - v := axilR; - - v.dataDelaySet := (others => (others => '0')); - v.frameDelaySet := "00"; - - v.curDelayFrame := curDelayFrame; - v.curDelayData := curDelayData; - - -- Store last two samples read from ADC - if (debugDataValid = '1' and axilR.freezeDebug = '0') then - v.readoutDebug0 := debugDataTmp; - v.readoutDebug1 := axilR.readoutDebug0; - end if; - - axiSlaveWaitTxn(axilEp, axilWriteMaster, axilReadMaster, v.axilWriteSlave, v.axilReadSlave); - - -- Up to 8 delay registers - -- Write delay values to IDELAY primitives - -- All writes go to same r.delay register, - -- dataDelaySet(ch) or frameDelaySet enables the primitive write - for i in 1 downto 0 loop - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegister(axilEp, X"00"+toSlv((ch*8+i*4), 8), 0, v.delay); - axiWrDetect(axilEp, X"00"+toSlv((ch*8+i*4), 8), v.dataDelaySet(i)(ch)); - - axiSlaveRegisterR(axilEp, X"00"+toSlv((ch*8+i*4), 8), 0, axilR.curDelayData(i)(ch)); - end loop; - - axiSlaveRegister(axilEp, X"40"+toSlv(i*4, 8), 0, v.delay); - axiWrDetect(axilEp, X"40"+toSlv(i*4, 8), v.frameDelaySet(i)); - - axiSlaveRegisterR(axilEp, X"40"+toSlv(i*4, 8), 0, axilR.curDelayFrame(i)); - end loop; - - - - - -- Debug output to see how many times the shift has needed a relock - axiSlaveRegisterR(axilEp, X"50", 0, lockedFallCount(0)); - axiSlaveRegisterR(axilEp, X"50", 16, lockedSync(0)); - axiSlaveRegisterR(axilEp, X"54", 0, lockedFallCount(1)); - axiSlaveRegisterR(axilEp, X"54", 16, lockedSync(1)); - - axiSlaveRegisterR(axilEp, X"58", 0, adcFrameSync(0)); - axiSlaveRegisterR(axilEp, X"58", 8, adcFrameSync(1)); - - axiSlaveRegister(axilEp, X"5C", 0, v.lockedCountRst); - - axiSlaveRegister(axilEp, X"60", 0, v.invert); - axiSlaveRegister(axilEp, X"60", 1, v.negate); - - axiSlaveRegister(axilEp, X"70", 0, v.relock); - - -- Debug registers. Output the last 2 words received - for ch in 0 to NUM_CHANNELS_C-1 loop - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 0, axilR.readoutDebug0(ch)); - axiSlaveRegisterR(axilEp, X"80"+toSlv((ch*4), 8), 16, axilR.readoutDebug1(ch)); - end loop; - - axiSlaveRegister(axilEp, X"A0", 0, v.freezeDebug); - - axiSlaveDefault(axilEp, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); - - if (axilRst = '1') then - v := AXIL_REG_INIT_C; - end if; - - axilRin <= v; - axilWriteSlave <= axilR.axilWriteSlave; - axilReadSlave <= axilR.axilReadSlave; - - end process; - - axilSeq : process (axilClk) is - begin - if (rising_edge(axilClk)) then - axilR <= axilRin after TPD_G; - end if; - end process axilSeq; - - - - - GEN_PARTS : for i in 1 downto 0 generate - - ------------------------------------------------------------------------------------------------- - -- Create Clocks - ------------------------------------------------------------------------------------------------- - - AdcClk_I_Ibufds : IBUFDS - generic map ( - DIFF_TERM => true, - IOSTANDARD => "LVDS_25") - port map ( - I => adcSerial.dClkP(i), - IB => adcSerial.dClkN(i), - O => tmpAdcClk(i)); - - -- IO Clock - U_BUFIO : BUFIO - port map ( - I => tmpAdcClk(i), - O => adcBitClkIo(i)); - - adcBitClkIoInv(i) <= not adcBitClkIo(i); - - -- Regional clock - U_AdcBitClkR : BUFR - generic map ( - SIM_DEVICE => "7SERIES", - BUFR_DIVIDE => "4") - port map ( - I => tmpAdcClk(i), - O => adcBitClkR(i), - CE => '1', - CLR => '0'); - - -- Regional clock reset - ADC_BITCLK_RST_SYNC : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - RELEASE_DELAY_G => 5) - port map ( - clk => adcBitClkR(i), - asyncRst => adcClkRst, - syncRst => adcBitRst(i)); - - - ------------------------------------------------------------------------------------------------- - -- Deserializers - ------------------------------------------------------------------------------------------------- - - -- Frame signal input - U_FrameIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.fClkP(i), - IB => adcSerial.fClkN(i), - O => adcFramePad(i)); - - U_FRAME_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => adcR(i).reset, --adcBitRst(i), - slip => adcR(i).slip, - sysClk => axilClk, - curDelay => curDelayFrame(i), - setDelay => axilR.delay, - setValid => axilR.frameDelaySet(i), - iData => adcFramePad(i), - oData => adcFrame(i)); - - -------------------------------- - -- Data Input, 8 channels - -------------------------------- - GenData : for ch in NUM_CHANNELS_C-1 downto 0 generate - - -- Frame signal input - U_DataIn : IBUFDS - generic map ( - DIFF_TERM => true) - port map ( - I => adcSerial.chP(i)(ch), - IB => adcSerial.chN(i)(ch), - O => adcDataPad(i)(ch)); - - -- Optionally invert the pad input --- adcDataPad(i)(ch) <= adcDataPadOut(i)(ch) when ADC_INVERT_CH_G(i)(ch) = '0' else (not adcDataPadOut(i)(ch)); - - U_DATA_DESERIALIZER : entity surf.Ad9681Deserializer - generic map ( - TPD_G => TPD_G, - DEFAULT_DELAY_G => DEFAULT_DELAY_G, - IODELAY_GROUP_G => IODELAY_GROUP_G, - IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G) - port map ( - clkIo => adcBitClkIo(0), - clkIoInv => adcBitClkIoInv(0), - clkR => adcBitClkR(0), - rst => adcR(i).reset, --adcBitRst(i), - slip => adcR(i).slip, - sysClk => axilClk, - curDelay => curDelayData(i)(ch), - setDelay => axilR.delay, - setValid => axilR.dataDelaySet(i)(ch), - iData => adcDataPad(i)(ch), - oData => adcData(i)(ch)); - end generate; - - - - ------------------------------------------------------------------------------------------------- - -- ADC Bit Clocked Logic - ------------------------------------------------------------------------------------------------- - adcComb : process (adcFrame, adcR, relockSync) is - variable v : AdcRegType; - begin - v := adcR(i); - - ---------------------------------------------------------------------------------------------- - -- Slip bits until correct alignment seen - ---------------------------------------------------------------------------------------------- - v.slip := '0'; - v.reset := '0'; - v.locked := '0'; - - v.count := adcR(i).count + 1; - - case adcR(i).state is - when RESET_S => - v.reset := '1'; - if (adcR(i).count = "111111") then - v.state := SYNCING_S; - end if; - - when SYNCING_S => - if (adcR(i).count = "111111") then - if (adcFrame(i) = "11110000") then - v.state := SYNCED_S; - else - v.slip := '1'; - end if; - end if; - - when SYNCED_S => - v.locked := '1'; - v.count := (others => '0'); - if (adcFrame(i) /= "11110000") then - v.state := RESET_S; - end if; - - when others => null; - end case; - - if (relockSync(i) = '1') then - v.state := RESET_S; - end if; - - ---------------------------------------------------------------------------------------------- - -- Look for Frame rising edges and write data to fifos - ---------------------------------------------------------------------------------------------- - adcRin(i) <= v; - adcValid(i) <= adcR(i).locked; - - end process adcComb; - - adcSeq : process (adcBitClkR, adcBitRst) is - begin - if (adcBitRst(0) = '1') then - adcR(i) <= ADC_REG_INIT_C after TPD_G; - elsif (rising_edge(adcBitClkR(0))) then - adcR(i) <= adcRin(i) after TPD_G; - end if; - end process adcSeq; - - end generate; - - GLUE_COMB : process (adcData, adcValid, invertSync, negateSync) is - variable tmp : slv16Array(7 downto 0); - begin - for ch in NUM_CHANNELS_C-1 downto 0 loop - if (adcValid = "11") then - tmp(ch) := adcData(1)(ch) & adcData(0)(ch); - -- Locked, output adc data - if invertSync(0) = '1' then - -- Invert all bits but keep 2 LSBs clear - tmp(ch) := (X"FFFF" - tmp(ch)) and X"FFFC"; - elsif (negateSync(0) = '1') then - if (tmp(ch) = X"8000") then - -- Negative 1 case - tmp(Ch) := X"7FFC"; - else - tmp(ch) := (not(tmp(ch)(15 downto 2)) + 1) & "00"; - end if; - end if; - else - -- Not locked - tmp(ch) := (others => '1'); --"10" & "00000000000000"; - end if; - end loop; - fifoWrData <= tmp; - end process GLUE_COMB; - - --- Flatten fifoWrData onto fifoDataIn for FIFO --- Regroup fifoDataOut by channel into fifoDataTmp --- Format fifoDataTmp into AxiStream channels - glue : for i in NUM_CHANNELS_C-1 downto 0 generate - fifoDataIn(i*16+15 downto i*16) <= fifoWrData(i); - fifoDataTmp(i) <= fifoDataOut(i*16+15 downto i*16); - debugDataTmp(i) <= debugDataOut(i*16+15 downto i*16); - adcStreams(i).tdata(15 downto 0) <= fifoDataTmp(i); - adcStreams(i).tDest <= toSlv(i, 8); - adcStreams(i).tValid <= fifoDataValid; - end generate; - - -- Single fifo to synchronize adc data to the Stream clock - U_DataFifo : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => adcStreamClk, - rd_en => fifoDataValid, - valid => fifoDataValid, - dout => fifoDataOut); - - U_DataFifoDebug : entity surf.SynchronizerFifo - generic map ( - TPD_G => TPD_G, - MEMORY_TYPE_G => "distributed", - DATA_WIDTH_G => NUM_CHANNELS_C*16, - ADDR_WIDTH_G => 4, - INIT_G => "0") - port map ( - rst => adcBitRst(0), - wr_clk => adcBitClkR(0), - wr_en => '1', --Always write data - din => fifoDataIn, - rd_clk => axilClk, - rd_en => debugDataValid, - valid => debugDataValid, - dout => debugDataOut); - - -end rtl; - diff --git a/devices/AnalogDevices/ad9681/7Series/ruckus.tcl b/devices/AnalogDevices/ad9681/7Series/ruckus.tcl deleted file mode 100644 index e3e79e21f3..0000000000 --- a/devices/AnalogDevices/ad9681/7Series/ruckus.tcl +++ /dev/null @@ -1,5 +0,0 @@ -# Load RUCKUS library -source $::env(RUCKUS_PROC_TCL) - -# Load Source Code -loadSource -lib surf -dir "$::DIR_PATH/rtl" diff --git a/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd new file mode 100644 index 0000000000..a4e0474c80 --- /dev/null +++ b/devices/AnalogDevices/ad9681/core/Ad9681Readout.vhd @@ -0,0 +1,207 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: AD9681 serialized readout +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.Ad9681Pkg.all; +use surf.AdcDdrPkg.all; + +entity Ad9681Readout is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + CAPTURE_DCLK_IDX_G : natural range 0 to 1 := 0; + DATA_DELAY_INIT_G : NaturalArray(15 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(1 downto 0) := (others => 0); + ADC_INVERT_CH_G : Slv8Array(1 downto 0) := (others => (others => '0')); + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false; + LEFT_JUSTIFY_G : boolean := true); + port ( + axilClk : in sl; + axilRst : in sl; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + + adcClkRst : in sl; + idelayCtrlRdy : in sl := '0'; + adcSerial : in Ad9681SerialType; + + adcStreamClk : in sl; + adcStreamRst : in sl; + adcStreams : out AxiStreamMasterArray(7 downto 0)); +end entity Ad9681Readout; + +architecture rtl of Ad9681Readout is + + constant FRAME_PATTERN_C : slv(7 downto 0) := "11110000"; + constant DELAY_BITS_C : positive := adcDdrDelayBits(DEVICE_FAMILY_G); + constant DATA_FCO_MAP_C : NaturalArray(15 downto 0) := ( + 15 downto 8 => 1, + 7 downto 0 => 0); + + signal adcWordClk : sl; + signal adcWordRst : sl; + signal phyReset : sl; + signal delayReady : sl; + + signal dataP : slv(15 downto 0); + signal dataN : slv(15 downto 0); + signal dataWord : Slv16Array(15 downto 0); + signal dataValid : slv(15 downto 0); + signal sampleIn : Slv16Array(7 downto 0); + signal fcoWord : Slv16Array(1 downto 0); + signal fcoValid : slv(1 downto 0); + + signal bitSlip : slv(1 downto 0); + signal dataDelay : AdcDdrDelayArray(15 downto 0); + signal frameDelay : AdcDdrDelayArray(1 downto 0); + + -- Right-justified [13:0] streams as produced by AdcDdrCore. + signal coreStreams : AxiStreamMasterArray(7 downto 0); + +begin + + ------------------------------------------------------------------------------------------------- + -- Capture both serialized halves with one selected DCLK. The two DCLKs are + -- related ADC outputs, but combining their FPGA clock domains would make + -- sample coherence depend on an implicit inter-clock relationship. + ------------------------------------------------------------------------------------------------- + GEN_HALF : for i in 1 downto 0 generate + GEN_CHANNEL : for ch in 7 downto 0 generate + constant LANE_C : natural := (8*i)+ch; + begin + dataP(LANE_C) <= adcSerial.chP(i)(ch); + dataN(LANE_C) <= adcSerial.chN(i)(ch); + end generate GEN_CHANNEL; + end generate GEN_HALF; + + U_Phy : entity surf.AdcDdrPhy + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => 16, + FCO_LANES_G => 2, + SERIALIZATION_FACTOR_G => 8, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_C) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => adcSerial.dClkP(CAPTURE_DCLK_IDX_G), -- [in] + dClkN => adcSerial.dClkN(CAPTURE_DCLK_IDX_G), -- [in] + fcoP => adcSerial.fClkP, -- [in] + fcoN => adcSerial.fClkN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelay, -- [in] + fcoDelayWrite => frameDelay, -- [in] + captureClk => adcWordClk, -- [out] + captureRst => adcWordRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + + ------------------------------------------------------------------------------------------------- + -- In default two-lane mode, half 1 contains the upper eight serialized bits + -- and half 0 contains the lower six sample bits followed by two pad bits. + -- Strip those pad bits so the core receives the 14-bit ADC code in bits 13:0. + ------------------------------------------------------------------------------------------------- + GEN_ASSEMBLE : for ch in 7 downto 0 generate + sampleIn(ch) <= "00" & + ite(ADC_INVERT_CH_G(1)(ch) = '1', not dataWord(8+ch)(7 downto 0), + dataWord(8+ch)(7 downto 0)) & + ite(ADC_INVERT_CH_G(0)(ch) = '1', not dataWord(ch)(7 downto 2), + dataWord(ch)(7 downto 2)); + end generate GEN_ASSEMBLE; + + U_Core : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => 16, + FCO_LANES_G => 2, + CHANNELS_G => 8, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 8, + DELAY_BITS_G => DELAY_BITS_C, + DATA_DELAY_INIT_G => DATA_DELAY_INIT_G, + FCO_DELAY_INIT_G => FCO_DELAY_INIT_G, + FRAME_PATTERN_G => FRAME_PATTERN_C, + PATTERN_CHECK_G => PATTERN_CHECK_G, + OFFSET_BINARY_G => OFFSET_BINARY_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, -- [in] + axilRst => axilRst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + captureClk => adcWordClk, -- [in] + captureRst => adcWordRst, -- [in] + delayReady => delayReady, -- [in] + fcoWord => fcoWord, -- [in] + fcoValid => fcoValid, -- [in] + sampleValid => uAnd(dataValid), -- [in] + sampleIn => sampleIn, -- [in] + phyReset => phyReset, -- [out] + bitSlip => bitSlip, -- [out] + dataDelayWrite => dataDelay, -- [out] + fcoDelayWrite => frameDelay, -- [out] + streamClk => adcStreamClk, -- [in] + streamRst => adcStreamRst, -- [in] + streams => coreStreams); -- [out] + + ------------------------------------------------------------------------------------------------- + -- Sample justification + -- + -- AdcDdrCore emits the 14-bit ADC code right-justified in bits 13:0. The + -- previous Ad9681Readout and the warm-tdm DSP chain expect the sample + -- left-justified into bits 15:2 with the two LSBs cleared. Reproduce that + -- layout by default; keep the raw right-justified stream when disabled. + ------------------------------------------------------------------------------------------------- + GEN_JUSTIFY : if LEFT_JUSTIFY_G generate + GEN_CH : for ch in 7 downto 0 generate + justify : process (coreStreams) is + variable v : AxiStreamMasterType; + begin + v := coreStreams(ch); + v.tData(15 downto 0) := coreStreams(ch).tData(13 downto 0) & "00"; + adcStreams(ch) <= v; + end process justify; + end generate GEN_CH; + end generate GEN_JUSTIFY; + + GEN_RAW : if not LEFT_JUSTIFY_G generate + adcStreams <= coreStreams; + end generate GEN_RAW; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/ruckus.tcl b/devices/AnalogDevices/ad9681/ruckus.tcl index ec7c6fe333..f1d7eb4658 100644 --- a/devices/AnalogDevices/ad9681/ruckus.tcl +++ b/devices/AnalogDevices/ad9681/ruckus.tcl @@ -3,25 +3,4 @@ source $::env(RUCKUS_PROC_TCL) # Load Source Code loadSource -lib surf -dir "$::DIR_PATH/core" -loadSource -lib surf -sim_only -dir "$::DIR_PATH/sim" - -# Get the family type -set family [getFpgaFamily] - -if { ${family} eq {artix7} || - ${family} eq {kintex7} || - ${family} eq {virtex7} || - ${family} eq {zynq} } { - loadRuckusTcl "$::DIR_PATH/7Series" -} - -# if { ${family} eq {kintexu} || - # ${family} eq {virtexu} || - # ${family} eq {kintexuplus} || - # ${family} eq {virtexuplus} || - # ${family} eq {virtexuplusHBM} || - # ${family} eq {zynquplus} || - # ${family} eq {zynquplusRFSOC} || - # ${family} eq {qzynquplusRFSOC} } { - # loadRuckusTcl "$::DIR_PATH/UltraScale" -# } +loadRuckusTcl "$::DIR_PATH/sim" diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681.vhd deleted file mode 100644 index 35ff84735c..0000000000 --- a/devices/AnalogDevices/ad9681/sim/Ad9681.vhd +++ /dev/null @@ -1,628 +0,0 @@ -------------------------------------------------------------------------------- --- Title : AD9681 Simulation Module -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory --- Platform : --- Standard : VHDL'93/02 -------------------------------------------------------------------------------- --- Description: -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; -use ieee.std_logic_unsigned.all; -use ieee.std_logic_arith.all; - -library surf; -use surf.StdRtlPkg.all; -use surf.TextUtilPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681 is - - generic ( - TPD_G : time := 1 ns); - - port ( - clkP : in sl; - clkN : in sl; - - vin : in RealArray(7 downto 0); - - dP : out slv8array(1 downto 0); - dN : out slv8array(1 downto 0); - dcoP : out slv(1 downto 0); - dcoN : out slv(1 downto 0); - fcoP : out slv(1 downto 0); - fcoN : out slv(1 downto 0); - - sclk : in sl; - sdio : inout sl; - csb : in sl); -end entity Ad9681; - -architecture behavioral of Ad9681 is - - ------------------------------------------------------------------------------------------------- - -- Config and Sampling constant and signals - ------------------------------------------------------------------------------------------------- - constant PN_SHORT_TAPS_C : NaturalArray := (0 => 4, 1 => 8); -- X9+X5+1 - constant PN_SHORT_INIT_C : slv(8 downto 0) := "011011111"; - constant PN_LONG_TAPS_C : NaturalArray := (0 => 16, 1 => 22); -- X23+X18+1 - constant PN_LONG_INIT_C : slv(22 downto 0) := "01001101110000000101000"; - - -- ConfigSlave signals - signal wrEn : sl; - signal addr : slv(12 downto 0); - signal wrData : slv(31 downto 0); - signal byteValid : slv(3 downto 0); - - type GlobalConfigType is record - mode : slv(2 downto 0); - stabilizer : sl; - clockDivRatio : slv(2 downto 0); - outputLvds : sl; - outputInvert : sl; - termination : slv(1 downto 0); - driveStrength : sl; - lsbFirst : sl; - outputMode : slv(2 downto 0); - pllLowRateMode : sl; - sel2xFrame : sl; - bits : slv(1 downto 0); - binFormat : sl; - digitalFsAdj : slv(2 downto 0); - end record GlobalConfigType; - - constant GLOBAL_CONFIG_INIT_C : GlobalConfigType := ( - mode => "000", - stabilizer => '1', - clockDivRatio => "000", - outputLvds => '0', - outputInvert => '0', - termination => "00", - driveStrength => '0', - lsbFirst => '0', - outputMode => "011", - pllLowRateMode => '0', - sel2xFrame => '0', - bits => "00", - binFormat => '1', - digitalFsAdj => "100"); - - type ChannelConfigType is record - chopMode : sl; - pn23 : slv(22 downto 0); - resetPnLongGen : sl; - pn9 : slv(8 downto 0); - resetPnShortGen : sl; - userTestMode : slv(1 downto 0); - outputTestMode : slv(3 downto 0); - outputPhase : slv(3 downto 0); - inputPhase : slv(2 downto 0); - userPattern1 : slv(15 downto 0); - userPattern2 : slv(15 downto 0); - offsetAdjust : slv(7 downto 0); - outputReset : sl; - powerDown : sl; - end record ChannelConfigType; - - constant CHANNEL_CONFIG_INIT_C : ChannelConfigType := ( - chopMode => '0', - pn23 => PN_LONG_INIT_C, - resetPnLongGen => '0', - pn9 => PN_SHORT_INIT_C, - resetPnShortGen => '0', - userTestMode => "00", - outputTestMode => "0000", - outputPhase => "0011", - inputPhase => "000", - userPattern1 => X"0000", - userPattern2 => X"0000", - offsetAdjust => X"00", - outputReset => '0', - powerDown => '0'); - - type ChannelConfigArray is array (natural range <>) of ChannelConfigType; - - type DelayArray is array (15 downto 0) of RealArray(7 downto 0); - - type ConfigRegType is record - vinDelay : DelayArray; - sample : Slv16Array(7 downto 0); -- slv(13 downto 0); - rdData : slv(31 downto 0); - lsbFirst : sl; - softReset : sl; - channelConfigEn : slv(9 downto 0); - tmpGlobal : GlobalConfigType; - tmpChannel : ChannelConfigType; - global : GlobalConfigType; - channel : ChannelConfigArray(9 downto 0); - word : sl; - end record ConfigRegType; - - constant CONFIG_REG_INIT_C : ConfigRegType := ( - vinDelay => (others => (others => 0.0)), - sample => (others => "0000000000000000"), - rdData => X"00000000", - lsbFirst => '0', - softReset => '0', - channelConfigEn => "1111111111", - tmpGlobal => GLOBAL_CONFIG_INIT_C, - tmpChannel => CHANNEL_CONFIG_INIT_C, - global => GLOBAL_CONFIG_INIT_C, - channel => (others => CHANNEL_CONFIG_INIT_C), - word => '0'); - - signal r : ConfigRegType := CONFIG_REG_INIT_C; - signal rin : ConfigRegType; - - ------------------------------------------------------------------------------------------------- - -- Output constants and signals - ------------------------------------------------------------------------------------------------- --- constant DCLK_PERIOD_C : time := CLK_PERIOD_G / 7.0; - - signal pllRst : sl; - signal clk : sl; - signal locked : sl; - signal rst : sl; - signal clkFbOut : sl; - signal clkFbIn : sl; - signal dClkInt : sl; - signal dClk : sl; - signal fClkInt : sl; - signal fClk : sl; - signal dcoInt : sl; - signal dco : sl; - signal fcoInt : sl; - signal fco : sl; - signal serData : slv8array(1 downto 0); - -begin - - ------------------------------------------------------------------------------------------------- - -- Create local clocks - ------------------------------------------------------------------------------------------------- --- ClkRst_1 : entity surf.ClkRst --- generic map ( --- RST_HOLD_TIME_G => 50 us) --- port map ( --- rst => pllRst); - - process is - begin - pllRst <= '1'; - wait for 15 us; - pllRst <= '0'; - wait until locked = '0'; - end process; - - - CLK_BUFG : IBUFGDS - port map ( - I => clkP, - IB => clkN, - O => clk); - - plle2_adv_inst : PLLE2_ADV - generic map ( - BANDWIDTH => "HIGH", - COMPENSATION => "ZHOLD", - DIVCLK_DIVIDE => 1, - CLKFBOUT_MULT => 8, - CLKFBOUT_PHASE => 0.000, - CLKOUT0_DIVIDE => 8, - CLKOUT0_PHASE => 0.000, - CLKOUT0_DUTY_CYCLE => 0.500, - CLKOUT1_DIVIDE => 2, - CLKOUT1_PHASE => 0.000, - CLKOUT1_DUTY_CYCLE => 0.500, - CLKOUT2_DIVIDE => 2, - CLKOUT2_PHASE => 180.000, - CLKOUT2_DUTY_CYCLE => 0.500, - CLKOUT3_DIVIDE => 8, - CLKOUT3_PHASE => 0.000, - CLKOUT3_DUTY_CYCLE => 0.500, - CLKIN1_PERIOD => 8.0, - REF_JITTER1 => 0.010) - port map ( - -- Output clocks - CLKFBOUT => clkFbOut, - CLKOUT0 => fClkInt, - CLKOUT1 => dClkInt, - CLKOUT2 => dcoInt, -- Shifted serial clock for output - CLKOUT3 => fcoInt, - CLKOUT4 => open, - CLKOUT5 => open, - -- Input clock control - CLKFBIN => clkFbIn, - CLKIN1 => clk, - CLKIN2 => '0', - -- Tied to always select the primary input clock - CLKINSEL => '1', - -- Ports for dynamic reconfiguration - DADDR => (others => '0'), - DCLK => '0', - DEN => '0', - DI => (others => '0'), - DO => open, - DRDY => open, - DWE => '0', - -- Other control and status signals - LOCKED => locked, - PWRDWN => '0', - RST => pllRst); - - FB_BUFG : BUFG - port map ( - I => clkFbOut, - O => clkFbIn); - - FCLK_BUFG : BUFG - port map ( - I => fClkInt, - O => fClk); - - DCLK_BUFG : BUFG - port map ( - I => dClkInt, - O => dClk); - - DCO_BUFG : BUFG - port map ( - I => dcoInt, - O => dco); - - FCO_BUFG : BUFG - port map ( - I => fcoInt, - O => fco); - - RstSync_1 : entity surf.RstSync - generic map ( - TPD_G => TPD_G, - IN_POLARITY_G => '0', - OUT_POLARITY_G => '1', - RELEASE_DELAY_G => 10) - port map ( - clk => fClk, - asyncRst => locked, - syncRst => rst); - - ------------------------------------------------------------------------------------------------- - -- Instantiate configuration interface - ------------------------------------------------------------------------------------------------- - AdiConfigSlave_1 : entity surf.AdiConfigSlave - generic map ( - TPD_G => TPD_G) - port map ( - clk => fClk, - sclk => sclk, - sdio => sdio, - csb => csb, - wrEn => wrEn, - rdEn => open, - addr => addr, - wrData => wrData, - byteValid => byteValid, - rdData => r.rdData); - - ------------------------------------------------------------------------------------------------- - -- Configuration register logic - ------------------------------------------------------------------------------------------------- - comb : process (addr, r, vin, wrData, wrEn) is - variable v : ConfigRegType; - variable activeChannel : ChannelConfigType; - variable zero : slv(13 downto 0) := (others => '0'); - begin - v := r; - - for i in 7 downto 0 loop - v.vinDelay(0)(i) := vin(i); - for j in 15 downto 1 loop - v.vinDelay(j)(i) := r.vinDelay(j-1)(i); - end loop; - end loop; - - ---------------------------------------------------------------------------------------------- - -- Configuration Registers - ---------------------------------------------------------------------------------------------- - activeChannel := r.channel(0); - for i in 9 downto 0 loop - if (r.channelConfigEn(i) = '1') then - activeChannel := r.channel(i); - end if; - end loop; - - - v.rdData := (others => '0'); - case (addr(7 downto 0)) is - - when X"00" => -- chip_port_config - v.rdData(6) := r.lsbFirst; - v.rdData(5) := r.softReset; - v.rdData(4) := '1'; - v.rdData(3) := '1'; - v.rdData(2) := r.softReset; - v.rdData(1) := r.lsbFirst; - if (wrEn = '1') then - v.lsbFirst := wrData(6) or wrData(1); - v.softReset := wrData(5) or wrData(2); - end if; - - when X"01" => -- chip_id - v.rdData(7 downto 0) := X"8F"; - - when X"02" => -- chip_grade - v.rdData(6 downto 4) := "110"; - - ------------------------------------------------------------------------------------------- --- when X"04" => -- device_index_2 --- v.rdData(3 downto 0) := r.channelConfigEn(7 downto 4); --- if (wrEn = '1') then --- v.channelConfigEn(7 downto 4) := wrData(3 downto 0); --- end if; - - when X"05" => -- device_index_1 - v.rdData(3 downto 0) := r.channelConfigEn(3 downto 0); - v.rdData(4) := r.channelConfigEn(8); - v.rdData(5) := r.channelConfigEn(9); - if (wrEn = '1') then - v.channelConfigEn(3 downto 0) := wrData(3 downto 0); - v.channelConfigEn(7 downto 4) := wrData(3 downto 0); -- Check this - v.channelConfigEn(8) := wrData(4); - v.channelConfigEn(9) := wrData(5); - end if; - - when X"FF" => -- device update - if (wrEn = '1') then - v.global := r.tmpGlobal; - for i in 9 downto 0 loop - if (r.channelConfigEn(i) = '1') then - v.channel(i) := r.tmpChannel; - if (r.tmpChannel.resetPnLongGen = '1') then - v.channel(i).pn23 := PN_LONG_INIT_C; - end if; - if (r.tmpChannel.resetPnShortGen = '1') then - v.channel(i).pn9 := PN_SHORT_INIT_C; - end if; - end if; - end loop; - end if; - - ------------------------------------------------------------------------------------------- - when X"08" => -- modes - v.rdData(1 downto 0) := r.global.mode(1 downto 0); - v.rdData(5) := r.global.mode(2); - if (wrEn = '1') then - v.tmpGlobal.mode(1 downto 0) := wrData(1 downto 0); - v.tmpGlobal.mode(2) := wrData(5); - end if; - - when X"09" => -- clock - v.rdData(0) := r.global.stabilizer; - if (wrEn = '1') then - v.tmpGlobal.stabilizer := wrData(0); - end if; - - when X"0B" => - v.rdData(2 downto 0) := r.global.clockDivRatio; - if (wrEn = '1') then - v.tmpGlobal.clockDivRatio := wrData(2 downto 0); - end if; - - when X"0C" => - v.rdData(2) := activeChannel.chopMode; - if (wrEn = '1') then - v.tmpChannel.chopMode := wrData(2); - end if; - - - when X"0D" => -- test_io - v.rdData(7 downto 6) := activeChannel.userTestMode; - v.rdData(5) := activeChannel.resetPnLongGen; - v.rdData(4) := activeChannel.resetPnShortGen; - v.rdData(3 downto 0) := activeChannel.outputTestMode; - if (wrEn = '1') then - v.tmpChannel.userTestMode := wrData(7 downto 6); - v.tmpChannel.resetPnLongGen := wrData(5); - v.tmpChannel.resetPnShortGen := wrData(4); - v.tmpChannel.outputTestMode := wrData(3 downto 0); - end if; - - when X"10" => - v.rdData(7 downto 0) := activeChannel.offsetAdjust; - if (wrEn = '1') then - v.tmpChannel.offsetAdjust := wrData(7 downto 0); - end if; - - - when X"14" => -- output_mode - v.rdData(6) := r.global.outputLvds; - v.rdData(2) := r.global.outputInvert; - v.rdData(0) := r.global.binFormat; - if (wrEn = '1') then - v.tmpGlobal.outputLvds := wrData(6); - v.tmpGlobal.outputInvert := wrData(2); - v.tmpGlobal.binFormat := wrData(0); - end if; - - when X"15" => -- output_adjust - -- Not sure if this is global - v.rdData(5 downto 4) := r.global.termination; - v.rdData(0) := r.global.driveStrength; - if (wrEn = '1') then - v.tmpGlobal.termination := wrData(5 downto 4); - v.tmpGlobal.driveStrength := wrData(0); - end if; - - when X"16" => -- output_phase - v.rdData(6 downto 4) := activeChannel.inputPhase; - v.rdData(3 downto 0) := activeChannel.outputPhase; - if (wrEn = '1') then - v.tmpChannel.inputPhase := wrData(6 downto 4); - v.tmpChannel.outputPhase := wrData(3 downto 0); - end if; - - when X"18" => - v.rdData(2 downto 0) := r.global.digitalFsAdj; - if (wrEn = '1') then - v.tmpGlobal.digitalFsAdj := wrData(2 downto 0); - end if; - - - when X"19" => -- user_patt1_lsb - v.rdData(7 downto 0) := activeChannel.userPattern1(7 downto 0); - if (wrEn = '1') then - v.tmpChannel.userPattern1(7 downto 0) := wrData(7 downto 0); - end if; - - when X"1A" => -- user_patt1_msb - v.rdData(7 downto 0) := activeChannel.userPattern1(15 downto 8); - if (wrEn = '1') then - v.tmpChannel.userPattern1(15 downto 8) := wrData(7 downto 0); - end if; - - when X"1B" => -- user_patt2_lsb - v.rdData(7 downto 0) := activeChannel.userPattern2(7 downto 0); - if (wrEn = '1') then - v.tmpChannel.userPattern2(7 downto 0) := wrData(7 downto 0); - end if; - - when X"1C" => -- user_patt2_msb - v.rdData(7 downto 0) := activeChannel.userPattern2(15 downto 8); - if (wrEn = '1') then - v.tmpChannel.userPattern2(15 downto 8) := wrData(7 downto 0); - end if; - - when X"21" => -- serial_control - v.rdData(7) := r.global.lsbFirst; - v.rdData(6 downto 4) := r.global.outputMode; - v.rdData(3) := r.global.pllLowRateMode; - v.rdData(2) := r.global.sel2xFrame; - v.rdData(1 downto 0) := r.global.bits; - if (wrEn = '1') then - v.tmpGlobal.lsbFirst := wrData(7); - v.tmpGlobal.outputMode := wrData(6 downto 4); - v.tmpGlobal.pllLowRateMode := wrData(3); - v.tmpGlobal.sel2xFrame := wrData(2); - v.tmpGlobal.bits := wrData(1 downto 0); - end if; - - when X"22" => -- serial_ch_stat - v.rdData(1) := activeChannel.outputReset; - v.rdData(0) := activeChannel.powerDown; - if (wrEn = '1') then - v.tmpChannel.outputReset := wrData(1); - v.tmpChannel.powerDown := wrData(0); - end if; - - when others => - v.rdData := (others => '1'); - - end case; - - ---------------------------------------------------------------------------------------------- - -- ADC Sampling - ---------------------------------------------------------------------------------------------- - v.word := not r.word; - for i in 7 downto 0 loop - if (r.channel(i).powerDown = '0') then - case (r.channel(i).outputTestMode) is - when "0000" => -- normal - v.sample(i) := adcConversion(r.vinDelay(15)(i), -1.0, 1.0, 14, toBoolean(r.global.binFormat)) & "00"; - - -- Emulate chip behavior at -1 - if (r.vinDelay(15)(i) >= 1.0 or r.vinDelay(15)(i) <= -1.0) then - v.sample(i) := X"8000"; - end if; - when "0001" => -- midscale short - v.sample(i) := "1000000000000000"; - when "0010" => -- +FS short - v.sample(i) := "1111111111111100"; - when "0011" => -- -FS short - v.sample(i) := "0000000000000000"; - when "0100" => -- checkerboard - v.sample(i) := ite(r.word = '0', "1010101010101000", "0101010101010100"); - when "0101" => -- pn23 (not implemented) - v.sample(i) := (others => '0'); --(scrambler(zero, r.pn23, PN_LONG_TAPS_C, v.pn23, v.sample(i)); - when "0110" => -- pn9 (not implemented) - v.sample(i) := (others => '0'); --scrambler(zero, r.pn9, PN_SHORT_TAPS_C, v.pn9, v.sample(i)); - when "0111" => -- one/zero toggle - v.sample(i) := ite(r.word = '0', "1111111111111100", "0000000000000000"); - when "1000" => -- user input - v.sample(i) := ite(r.word = '0', r.channel(i).userPattern1, r.channel(i).userPattern2); - when "1001" => -- 1/0 bit toggle - v.sample(i) := "1010101010101000"; - when "1010" => -- 1x sync - v.sample(i) := "0000000111111100"; - when "1011" => -- one bit high - v.sample(i) := "1000000000000000"; - when "1100" => -- mixed bit frequency - v.sample(i) := "1010000110011100"; - when others => - v.sample(i) := (others => '0'); - end case; - - else - v.sample(i) := (others => '0'); - end if; - end loop; - - rin <= v; - - end process comb; - - seq : process (fClk) is - begin - if (rising_edge(fClk)) then - r <= rin after TPD_G; - end if; - end process seq; - - ------------------------------------------------------------------------------------------------- - -- Output - ------------------------------------------------------------------------------------------------- - BYTE_GEN : for i in 1 downto 0 generate - DATA_SERIALIZER_GEN : for ch in 7 downto 0 generate - Ad9681Serializer_1 : entity surf.Ad9681Serializer - port map ( - clk => dClk, - clkDiv => fClk, - rst => rst, - iData => r.sample(ch)(i*8+7 downto i*8), - oData => serData(i)(ch)); - - DATA_OUT_BUFF : OBUFDS - port map ( - I => serData(i)(ch), - O => dP(i)(ch), - OB => dN(i)(ch)); - end generate DATA_SERIALIZER_GEN; - - - FCLK_OUT_BUFF : entity surf.ClkOutBufDiff - port map ( - clkIn => fco, - clkOutP => fcoP(i), - clkOutN => fcoN(i)); - - DCLK_OUT_BUFF : entity surf.ClkOutBufDiff - port map ( - clkIn => dco, - clkOutP => dcoP(i), - clkOutN => dcoN(i)); - end generate; - - -end architecture behavioral; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd deleted file mode 100644 index c04585b9c0..0000000000 --- a/devices/AnalogDevices/ad9681/sim/Ad9681Serializer.vhd +++ /dev/null @@ -1,74 +0,0 @@ -------------------------------------------------------------------------------- --- Company : SLAC National Accelerator Laboratory -------------------------------------------------------------------------------- --- Description: 14 bit DDR deserializer using 7 series IDELAYE2 and ISERDESE2. -------------------------------------------------------------------------------- --- 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. -------------------------------------------------------------------------------- - -library ieee; -use ieee.std_logic_1164.all; - -library surf; -use surf.StdRtlPkg.all; - -library unisim; -use unisim.vcomponents.all; - -entity Ad9681Serializer is - port ( - clk : in sl; -- Serial High speed clock - clkDiv : in sl; -- Parallel low speed clock - rst : in sl; -- Reset - - iData : in slv(7 downto 0); - oData : out sl); -end entity Ad9681Serializer; - -architecture rtl of Ad9681Serializer is - -begin - - oserdese2_master : OSERDESE2 - generic map ( - DATA_RATE_OQ => "DDR", - DATA_RATE_TQ => "SDR", - DATA_WIDTH => 8, - TRISTATE_WIDTH => 1, - SERDES_MODE => "MASTER") - port map ( - D1 => iData(7), - D2 => iData(6), - D3 => iData(5), - D4 => iData(4), - D5 => iData(3), - D6 => iData(2), - D7 => iData(1), - D8 => iData(0), - T1 => '0', - T2 => '0', - T3 => '0', - T4 => '0', - SHIFTIN1 => '0', - SHIFTIN2 => '0', - SHIFTOUT1 => open, - SHIFTOUT2 => open, - OCE => '1', - CLK => clk, - CLKDIV => clkDiv, - OQ => oData, - TQ => open, - OFB => open, - TBYTEIN => '0', - TBYTEOUT => open, - TFB => open, - TCE => '0', - RST => rst); - -end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd new file mode 100644 index 0000000000..3f1c736ac6 --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd @@ -0,0 +1,260 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free pin-level AD9681 device simulation +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681Sim is + generic ( + TPD_G : time := 1 ns; + CLK_PERIOD_G : time := 8 ns; + DATA_PHASE_G : time := 0 ns; + FCO_PHASE_G : time := 0 ns; + DATA_SKEW_G : TimeArray(15 downto 0) := (others => 0 ns); + FCO_SKEW_G : TimeArray(1 downto 0) := (others => 0 ns); + JITTER_G : time := 0 ns; + TIMING_BIAS_G : time := 0 ns); + port ( + clkP : in sl; + clkN : in sl; + + vin : in RealArray(7 downto 0); + + dP : out Slv8Array(1 downto 0); + dN : out Slv8Array(1 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + + sclk : in sl; + sdio : inout sl; + csb : in sl); +end entity Ad9681Sim; + +architecture behavioral of Ad9681Sim is + + constant FRAME_PATTERN_C : slv(7 downto 0) := "11110000"; + constant HALF_BIT_TIME_C : time := CLK_PERIOD_G/16; + constant CONVERSION_LATENCY_C : positive := 16; + + -- The coherent serializer-frame handoff contributes the final sample clock + -- of the ADC's specified 16-clock conversion latency. + type NormalDataPipelineType is array (CONVERSION_LATENCY_C-2 downto 0) of Slv16Array(7 downto 0); + + signal spiWrEn : sl; + signal cfgAddr : slv(12 downto 0); + signal cfgWrData : slv(31 downto 0); + signal cfgByteValid : slv(3 downto 0); + signal cfgRdByte : slv(7 downto 0); + signal cfgRdData : slv(31 downto 0); + signal normalData : Slv16Array(7 downto 0) := (others => (others => '0')); + signal normalDataPipeline : NormalDataPipelineType := (others => (others => (others => '0'))); + signal delayedNormalData : Slv16Array(7 downto 0); + signal sampleData : Slv16Array(7 downto 0); + signal serialData : Slv8Array(1 downto 0); + signal serialDco : sl; + signal serialFco : slv(1 downto 0); + +begin + + assert HALF_BIT_TIME_C > 0 ns + report "Ad9681Sim requires CLK_PERIOD_G >= 16 simulator time units" + severity failure; + + assert JITTER_G >= 0 ns + report "Ad9681Sim requires nonnegative JITTER_G" + severity failure; + + assert TIMING_BIAS_G >= 0 ns + report "Ad9681Sim requires nonnegative TIMING_BIAS_G" + severity failure; + + GEN_DATA_TIMING_CHECK : for i in 15 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+DATA_PHASE_G+DATA_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := DATA_PHASE_G+DATA_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9681Sim data timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_DATA_TIMING_CHECK; + + GEN_FCO_TIMING_CHECK : for i in 1 downto 0 generate + constant EARLIEST_EDGE_C : time := + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(i)-JITTER_G; + constant LATEST_EDGE_C : time := FCO_PHASE_G+FCO_SKEW_G(i)+JITTER_G; + begin + assert EARLIEST_EDGE_C >= 0 ns and LATEST_EDGE_C < HALF_BIT_TIME_C + report "Ad9681Sim FCO timing must be schedulable and precede the DCO edge" + severity failure; + end generate GEN_FCO_TIMING_CHECK; + + GEN_NORMAL_DATA : for i in 7 downto 0 generate + adcConvert : process (vin(i)) is + variable analogInput : real; + begin + -- Real-valued board models can produce a transient NaN while their + -- concurrent amplifier stages settle at time zero. NaN is unordered, + -- so both comparisons are false. Substitute the low-scale input before + -- calling adcConversion(), whose math_real clamp does not handle NaN. + if (vin(i) < 0.0) or (vin(i) >= 0.0) then + analogInput := vin(i); + else + analogInput := 0.0; + end if; + normalData(i) <= "00" & adcConversion(analogInput, 0.0, 2.0, 14, false); + end process adcConvert; + end generate GEN_NORMAL_DATA; + + ------------------------------------------------------------------------------------------------ + -- The AD9681 specifies 16 sample clocks of conversion latency. Fifteen + -- stages are explicit here; sampleData is then captured once per complete + -- serializer frame below, providing the sixteenth sample-clock delay at the + -- pins. Test patterns are generated after this analog conversion pipeline. + ------------------------------------------------------------------------------------------------ + conversionPipeline : process (clkP) is + begin + if rising_edge(clkP) then + normalDataPipeline(0) <= normalData after TPD_G; + for i in 1 to CONVERSION_LATENCY_C-2 loop + normalDataPipeline(i) <= normalDataPipeline(i-1) after TPD_G; + end loop; + end if; + end process conversionPipeline; + + delayedNormalData <= normalDataPipeline(CONVERSION_LATENCY_C-2); + + cfgRdData <= x"000000" & cfgRdByte; + + U_Config : entity surf.AdiConfigSlave + generic map ( + TPD_G => TPD_G) + port map ( + clk => clkP, -- [in] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb, -- [in] + wrEn => spiWrEn, -- [out] + rdEn => open, -- [out] + addr => cfgAddr, -- [out] + wrData => cfgWrData, -- [out] + byteValid => cfgByteValid, -- [out] + rdData => cfgRdData); -- [in] + + ------------------------------------------------------------------------------------------------ + -- The physical AD9681 exposes a single SPI port that addresses both internal + -- four-channel banks. Applying each write to both banks in the same sample + -- clock keeps their per-channel PN generators reseeded coherently, matching + -- the real device; staggering the two banks would offset them by one sample. + ------------------------------------------------------------------------------------------------ + U_Core : entity surf.Ad9681SimCore + generic map ( + TPD_G => TPD_G) + port map ( + sampleClk => clkP, -- [in] + sampleRst => '0', -- [in] + sampleEnable => '1', -- [in] + normalData => delayedNormalData, -- [in] + cfgWrEn => spiWrEn, -- [in] + cfgAddr => cfgAddr(8 downto 0), -- [in] + cfgWrData => cfgWrData(7 downto 0), -- [in] + cfgRdData => cfgRdByte, -- [out] + sampleData => sampleData, -- [out] + sampleValid => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Each physical output group serializes one byte of every channel. DCO + -- remains binary and jitter-free. A common timing bias delays DCO, data, and + -- FCO equally so negative jitter remains schedulable without moving the + -- nominal sampling point. Data and FCO transitions also receive their + -- configured static phase/skew plus bounded deterministic jitter that + -- alternates between negative and positive displacement on each actual + -- transition. All pins remain binary so unknown values cannot escape through + -- system interfaces. + ------------------------------------------------------------------------------------------------ + serializer : process is + variable dco : sl := '0'; + variable frameData : Slv16Array(7 downto 0) := (others => (others => '0')); + variable dataCurrent : Slv8Array(1 downto 0) := (others => (others => '0')); + variable fcoCurrent : slv(1 downto 0) := (others => '0'); + variable dataJitterPositive : BooleanArray(15 downto 0) := (others => false); + variable fcoJitterPositive : BooleanArray(1 downto 0) := (others => false); + variable nextData : sl; + variable nextFco : sl; + variable edgeJitter : time; + begin + serialData <= (others => (others => '0')); + serialDco <= '0'; + serialFco <= (others => '0'); + wait until rising_edge(clkP); + loop + -- Capture one coherent output word per channel. sampleData updates + -- after the encode edge, so this also provides the last cycle of the + -- specified conversion latency without tearing alternating patterns. + frameData := sampleData; + for bitindex in 7 downto 0 loop + for grp in 1 downto 0 loop + for ch in 7 downto 0 loop + nextData := frameData(ch)(bitindex+(8*grp)); + if (nextData /= dataCurrent(grp)(ch)) then + if (dataJitterPositive((8*grp)+ch)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + dataJitterPositive((8*grp)+ch) := + not dataJitterPositive((8*grp)+ch); + serialData(grp)(ch) <= transport nextData after + TIMING_BIAS_G+DATA_PHASE_G+ + DATA_SKEW_G((8*grp)+ch)+edgeJitter; + dataCurrent(grp)(ch) := nextData; + end if; + end loop; + end loop; + + nextFco := FRAME_PATTERN_C(bitindex); + for grp in 1 downto 0 loop + if (nextFco /= fcoCurrent(grp)) then + if (fcoJitterPositive(grp)) then + edgeJitter := JITTER_G; + else + edgeJitter := -JITTER_G; + end if; + fcoJitterPositive(grp) := not fcoJitterPositive(grp); + serialFco(grp) <= transport nextFco after + TIMING_BIAS_G+FCO_PHASE_G+FCO_SKEW_G(grp)+edgeJitter; + fcoCurrent(grp) := nextFco; + end if; + end loop; + + wait for HALF_BIT_TIME_C; + dco := not dco; + serialDco <= transport dco after TIMING_BIAS_G; + wait for HALF_BIT_TIME_C; + end loop; + end loop; + end process serializer; + + dP <= serialData; + dN <= (not serialData(1), not serialData(0)); + dcoP <= (others => serialDco); + dcoN <= (others => not serialDco); + fcoP <= serialFco; + fcoN <= not serialFco; + +end architecture behavioral; diff --git a/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd new file mode 100644 index 0000000000..0d9368fca0 --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd @@ -0,0 +1,330 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Primitive-free AD9681 digital output model +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity Ad9681SimCore is + generic ( + TPD_G : time := 1 ns); + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in Slv16Array(7 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out Slv16Array(7 downto 0); + sampleValid : out sl); +end entity Ad9681SimCore; + +architecture rtl of Ad9681SimCore is + + constant PN9_SEED_C : slv(8 downto 0) := "011011111"; + constant PN23_SEED_C : slv(22 downto 0) := "01001101110000000101000"; + + constant SPI_CONFIG_ADDR_C : slv(8 downto 0) := '0' & X"00"; + constant CHIP_ID_ADDR_C : slv(8 downto 0) := '0' & X"01"; + constant CHIP_GRADE_ADDR_C : slv(8 downto 0) := '0' & X"02"; + constant DEVICE_INDEX_ADDR_C : slv(8 downto 0) := '0' & X"05"; + constant POWER_MODE_ADDR_C : slv(8 downto 0) := '0' & X"08"; + constant TEST_MODE_ADDR_C : slv(8 downto 0) := '0' & X"0D"; + constant OUTPUT_MODE_ADDR_C : slv(8 downto 0) := '0' & X"14"; + constant USER_PATTERN1_LSB_C : slv(8 downto 0) := '0' & X"19"; + constant USER_PATTERN1_MSB_C : slv(8 downto 0) := '0' & X"1A"; + constant USER_PATTERN2_LSB_C : slv(8 downto 0) := '0' & X"1B"; + constant USER_PATTERN2_MSB_C : slv(8 downto 0) := '0' & X"1C"; + constant SERIAL_OUTPUT_ADDR_C : slv(8 downto 0) := '0' & X"21"; + constant CHANNEL_STATUS_ADDR_C : slv(8 downto 0) := '0' & X"22"; + constant TRANSFER_ADDR_C : slv(8 downto 0) := '0' & X"FF"; + constant RESOLUTION_RATE_ADDR_C : slv(8 downto 0) := '1' & X"00"; + + type GlobalType is record + powerMode : slv(2 downto 0); + outputInvert : sl; + outputFormat : sl; + lsbFirst : sl; + outputMode : slv(2 downto 0); + select2x : sl; + outputBits : slv(1 downto 0); + end record GlobalType; + + constant GLOBAL_INIT_C : GlobalType := ( + powerMode => "000", + outputInvert => '0', + outputFormat => '0', + lsbFirst => '0', + outputMode => "011", + select2x => '0', + outputBits => "00"); + + type ChannelType is record + testMode : slv(3 downto 0); + userMode : slv(1 downto 0); + userPatternA : slv(15 downto 0); + userPatternB : slv(15 downto 0); + outputReset : sl; + powerDown : sl; + resetPn9 : sl; + resetPn23 : sl; + pn9 : slv(8 downto 0); + pn23 : slv(22 downto 0); + end record ChannelType; + + constant CHANNEL_INIT_C : ChannelType := ( + testMode => "0000", + userMode => "00", + userPatternA => (others => '0'), + userPatternB => (others => '0'), + outputReset => '0', + powerDown => '0', + resetPn9 => '0', + resetPn23 => '0', + pn9 => PN9_SEED_C, + pn23 => PN23_SEED_C); + + type ChannelArray is array (natural range <>) of ChannelType; + + type RegType is record + rdData : slv(7 downto 0); + selectMask : slv(5 downto 0); + global : GlobalType; + channel : ChannelArray(7 downto 0); + resolution : slv(6 downto 0); + tmpResolution : slv(6 downto 0); + toggle : sl; + data : Slv16Array(7 downto 0); + valid : sl; + end record RegType; + + -- Register 0x05 (device index) powers up at 0x3F, selecting every data + -- channel so the first local-register write reaches all channels at once. + constant REG_INIT_C : RegType := ( + rdData => (others => '0'), + selectMask => (others => '1'), + global => GLOBAL_INIT_C, + channel => (others => CHANNEL_INIT_C), + resolution => (others => '0'), + tmpResolution => (others => '0'), + toggle => '0', + data => (others => (others => '0')), + valid => '0'); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + ------------------------------------------------------------------------------------------------- + -- One SPI register file addresses all eight channels. Register 0x05 (device + -- index) selects which data channels a subsequent local-register write + -- affects; its four select bits each cover a channel pair (A1/A2 .. D1/D2), + -- so channel ch is selected by selectMask(ch/2). A single write reaches + -- every selected channel in the same cycle, matching the real device. All + -- writes take effect immediately except the 0x100 override, which is staged + -- until the 0xFF transfer strobe. + ------------------------------------------------------------------------------------------------- + comb : process (cfgAddr, cfgWrData, cfgWrEn, normalData, r, sampleEnable, sampleRst) is + variable active : ChannelType; + variable v : RegType; + variable word : slv(15 downto 0); + variable code : slv(13 downto 0); + begin + v := r; + v.valid := '0'; + + if (sampleRst = '1') then + v := REG_INIT_C; + else + if (cfgWrEn = '1') then + case cfgAddr is + when SPI_CONFIG_ADDR_C => + if (cfgWrData(5) = '1' or cfgWrData(2) = '1') then + v := REG_INIT_C; + end if; + when DEVICE_INDEX_ADDR_C => + -- Bits[3:0] select data channels A..D (each a pair); bits[5:4] + -- select the DCO/FCO clock channels and are retained only for + -- readback since this model has no separately timed clocks. + v.selectMask := cfgWrData(5 downto 0); + when POWER_MODE_ADDR_C => + v.global.powerMode(1 downto 0) := cfgWrData(1 downto 0); + v.global.powerMode(2) := cfgWrData(5); + when TEST_MODE_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + v.channel(i).userMode := cfgWrData(7 downto 6); + v.channel(i).resetPn23 := cfgWrData(5); + v.channel(i).resetPn9 := cfgWrData(4); + v.channel(i).testMode := cfgWrData(3 downto 0); + if (cfgWrData(4) = '1') then + v.channel(i).pn9 := PN9_SEED_C; + end if; + if (cfgWrData(5) = '1') then + v.channel(i).pn23 := PN23_SEED_C; + end if; + end if; + end loop; + when OUTPUT_MODE_ADDR_C => + v.global.outputInvert := cfgWrData(2); + v.global.outputFormat := cfgWrData(0); + when USER_PATTERN1_LSB_C | USER_PATTERN1_MSB_C | + USER_PATTERN2_LSB_C | USER_PATTERN2_MSB_C => + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + case cfgAddr is + when USER_PATTERN1_LSB_C => v.channel(i).userPatternA(7 downto 0) := cfgWrData; + when USER_PATTERN1_MSB_C => v.channel(i).userPatternA(15 downto 8) := cfgWrData; + when USER_PATTERN2_LSB_C => v.channel(i).userPatternB(7 downto 0) := cfgWrData; + when others => v.channel(i).userPatternB(15 downto 8) := cfgWrData; + end case; + end if; + end loop; + when SERIAL_OUTPUT_ADDR_C => + assert cfgWrData(6 downto 4) = "011" and cfgWrData(2) = '0' and + cfgWrData(1 downto 0) = "00" + report "Ad9681SimCore does not model this output format; continuing with " & + "16-bit DDR two-lane bytewise output" + severity warning; + v.global.lsbFirst := cfgWrData(7); + v.global.outputMode := cfgWrData(6 downto 4); + v.global.select2x := cfgWrData(2); + v.global.outputBits := cfgWrData(1 downto 0); + when CHANNEL_STATUS_ADDR_C => + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + v.channel(i).outputReset := cfgWrData(1); + v.channel(i).powerDown := cfgWrData(0); + end if; + end loop; + when TRANSFER_ADDR_C => + if (cfgWrData(0) = '1') then + v.resolution := r.tmpResolution; + end if; + when RESOLUTION_RATE_ADDR_C => + v.tmpResolution := cfgWrData(6 downto 0); + when others => null; + end case; + end if; + + if (sampleEnable = '1') then + v.toggle := not r.toggle; + v.valid := '1'; + for i in 7 downto 0 loop + case r.channel(i).testMode is + when "0000" => + code := normalData(i)(13 downto 0); + if (r.global.outputFormat = '1') then + code(13) := not code(13); + end if; + word := code & "00"; + when "0001" => word := "1000000000000000"; + when "0010" => word := "1111111111111100"; + when "0011" => word := (others => '0'); + when "0100" => word := ite(r.toggle = '0', "1010101010101000", "0101010101010100"); + when "0101" => + word := adcDdrPn23Word(r.channel(i).pn23, 14) & "00"; + if (r.channel(i).resetPn23 = '1') then + v.channel(i).pn23 := PN23_SEED_C; + else + v.channel(i).pn23 := adcDdrPn23Advance(r.channel(i).pn23, 14); + end if; + when "0110" => + word := adcDdrPn9Word(r.channel(i).pn9, 14) & "00"; + if (r.channel(i).resetPn9 = '1') then + v.channel(i).pn9 := PN9_SEED_C; + else + v.channel(i).pn9 := adcDdrPn9Advance(r.channel(i).pn9, 14); + end if; + when "0111" => word := ite(r.toggle = '0', "1111111111111100", "0000000000000000"); + when "1000" => word := ite(r.toggle = '0', r.channel(i).userPatternA, + r.channel(i).userPatternB); + when "1001" => word := "1010101010101000"; + when "1010" => word := "0000000111111100"; + when "1011" => word := "1000000000000000"; + when "1100" => word := "1010000110011100"; + when others => word := (others => '0'); + end case; + if (r.global.outputInvert = '1') then + word := not word; + end if; + if (r.global.lsbFirst = '1') then + word(15 downto 8) := bitReverse(word(15 downto 8)); + word(7 downto 0) := bitReverse(word(7 downto 0)); + end if; + if (r.global.powerMode /= "000" or r.channel(i).powerDown = '1' or + r.channel(i).outputReset = '1') then + word := (others => '0'); + end if; + v.data(i) := word; + end loop; + end if; + end if; + + -- A local-register read returns the lowest-numbered selected channel; the + -- datasheet specifies Channel A1 when every device-index bit is set. + active := r.channel(0); + for i in 7 downto 0 loop + if (r.selectMask(i/2) = '1') then + active := r.channel(i); + end if; + end loop; + v.rdData := (others => '0'); + case cfgAddr is + when SPI_CONFIG_ADDR_C => v.rdData := "00011000"; + when CHIP_ID_ADDR_C => v.rdData := X"8F"; + when CHIP_GRADE_ADDR_C => v.rdData(6 downto 4) := "110"; + when DEVICE_INDEX_ADDR_C => v.rdData(5 downto 0) := r.selectMask; + when POWER_MODE_ADDR_C => + v.rdData(1 downto 0) := r.global.powerMode(1 downto 0); + v.rdData(5) := r.global.powerMode(2); + when TEST_MODE_ADDR_C => v.rdData := active.userMode & active.resetPn23 & active.resetPn9 & active.testMode; + when OUTPUT_MODE_ADDR_C => + v.rdData(2) := r.global.outputInvert; + v.rdData(0) := r.global.outputFormat; + when USER_PATTERN1_LSB_C => v.rdData := active.userPatternA(7 downto 0); + when USER_PATTERN1_MSB_C => v.rdData := active.userPatternA(15 downto 8); + when USER_PATTERN2_LSB_C => v.rdData := active.userPatternB(7 downto 0); + when USER_PATTERN2_MSB_C => v.rdData := active.userPatternB(15 downto 8); + when SERIAL_OUTPUT_ADDR_C => + v.rdData(7) := r.global.lsbFirst; + v.rdData(6 downto 4) := r.global.outputMode; + v.rdData(2) := r.global.select2x; + v.rdData(1 downto 0) := r.global.outputBits; + when CHANNEL_STATUS_ADDR_C => v.rdData(1 downto 0) := active.outputReset & active.powerDown; + when TRANSFER_ADDR_C => v.rdData := (others => '0'); + when RESOLUTION_RATE_ADDR_C => v.rdData(6 downto 0) := r.resolution; + when others => v.rdData := (others => '1'); + end case; + rin <= v; + end process comb; + + seq : process (sampleClk) is + begin + if rising_edge(sampleClk) then + r <= rin after TPD_G; + end if; + end process seq; + + cfgRdData <= rin.rdData; + sampleData <= r.data; + sampleValid <= r.valid; + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/sim/ruckus.tcl b/devices/AnalogDevices/ad9681/sim/ruckus.tcl new file mode 100644 index 0000000000..961d78c6dd --- /dev/null +++ b/devices/AnalogDevices/ad9681/sim/ruckus.tcl @@ -0,0 +1,14 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9681SimCore.vhd" +loadSource -lib surf -sim_only -path "$::DIR_PATH/Ad9681Sim.vhd" diff --git a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd new file mode 100644 index 0000000000..a9b48c8bb8 --- /dev/null +++ b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd @@ -0,0 +1,60 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.Ad9681SimCore +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681SimCoreWrapper is + port ( + sampleClk : in sl; + sampleRst : in sl; + sampleEnable : in sl; + normalData : in slv(127 downto 0); + cfgWrEn : in sl; + cfgAddr : in slv(8 downto 0); + cfgWrData : in slv(7 downto 0); + cfgRdData : out slv(7 downto 0); + sampleData : out slv(127 downto 0); + sampleValid : out sl); +end entity Ad9681SimCoreWrapper; + +architecture rtl of Ad9681SimCoreWrapper is + + signal normalArray : Slv16Array(7 downto 0); + signal sampleArray : Slv16Array(7 downto 0); + +begin + + GEN_FLATTEN : for i in 7 downto 0 generate + normalArray(i) <= normalData((i*16)+15 downto i*16); + sampleData((i*16)+15 downto i*16) <= sampleArray(i); + end generate GEN_FLATTEN; + + U_DUT : entity surf.Ad9681SimCore + port map ( + sampleClk => sampleClk, -- [in] + sampleRst => sampleRst, -- [in] + sampleEnable => sampleEnable, -- [in] + normalData => normalArray, -- [in] + cfgWrEn => cfgWrEn, -- [in] + cfgAddr => cfgAddr, -- [in] + cfgWrData => cfgWrData, -- [in] + cfgRdData => cfgRdData, -- [out] + sampleData => sampleArray, -- [out] + sampleValid => sampleValid); -- [out] + +end architecture rtl; diff --git a/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd new file mode 100644 index 0000000000..63db988dc9 --- /dev/null +++ b/devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd @@ -0,0 +1,90 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened cocotb wrapper for surf.Ad9681Sim +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; + +entity Ad9681SimWrapper is + generic ( + CLK_PERIOD_G : time := 8 ns; + DATA_PHASE_PS_G : natural := 0; + FCO_PHASE_PS_G : natural := 0; + DATA_LANE0_SKEW_PS_G : natural := 0; + FCO_LANE0_SKEW_PS_G : natural := 0; + JITTER_PS_G : natural := 0; + TIMING_BIAS_PS_G : natural := 0); + port ( + clkP : in sl; + clkN : in sl; + normalData : in slv(127 downto 0); + dP : out slv(15 downto 0); + dN : out slv(15 downto 0); + dcoP : out slv(1 downto 0); + dcoN : out slv(1 downto 0); + fcoP : out slv(1 downto 0); + fcoN : out slv(1 downto 0); + sclk : in sl; + sdioDrive : in sl; + sdioEnable : in sl; + sdioRead : out sl; + csb : in sl); +end entity Ad9681SimWrapper; + +architecture rtl of Ad9681SimWrapper is + + signal vin : RealArray(7 downto 0); + signal dataP : Slv8Array(1 downto 0); + signal dataN : Slv8Array(1 downto 0); + signal sdio : sl; + +begin + + GEN_INPUT : for i in 7 downto 0 generate + vin(i) <= real(to_integer(unsigned(normalData((16*i)+13 downto 16*i))))*(2.0/16384.0); + end generate GEN_INPUT; + + dP <= dataP(1) & dataP(0); + dN <= dataN(1) & dataN(0); + + sdio <= sdioDrive when sdioEnable = '1' else 'Z'; + sdioRead <= sdio; + + U_DUT : entity surf.Ad9681Sim + generic map ( + CLK_PERIOD_G => CLK_PERIOD_G, + DATA_PHASE_G => DATA_PHASE_PS_G*1 ps, + FCO_PHASE_G => FCO_PHASE_PS_G*1 ps, + DATA_SKEW_G => (0 => DATA_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + FCO_SKEW_G => (0 => FCO_LANE0_SKEW_PS_G*1 ps, others => 0 ns), + JITTER_G => JITTER_PS_G*1 ps, + TIMING_BIAS_G => TIMING_BIAS_PS_G*1 ps) + port map ( + clkP => clkP, -- [in] + clkN => clkN, -- [in] + vin => vin, -- [in] + dP => dataP, -- [out] + dN => dataN, -- [out] + dcoP => dcoP, -- [out] + dcoN => dcoN, -- [out] + fcoP => fcoP, -- [out] + fcoN => fcoN, -- [out] + sclk => sclk, -- [in] + sdio => sdio, -- [inout] + csb => csb); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd new file mode 100644 index 0000000000..bfcf3c4841 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrDeserializer7Series.vhd @@ -0,0 +1,180 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: One delayed DDR input lane for AMD 7 Series FPGAs +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrDeserializer7Series is + generic ( + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + SERIALIZATION_FACTOR_G : positive := 14); + port ( + bitClk : in sl; + bitClkInv : in sl; + wordClk : in sl; + rst : in sl; + bitSlip : in sl; + + delayClk : in sl; + delayValue : in slv(4 downto 0); + delayLoad : in sl; + + serialData : in sl; + dataWord : out slv(SERIALIZATION_FACTOR_G-1 downto 0)); +end entity AdcDdrDeserializer7Series; + +architecture rtl of AdcDdrDeserializer7Series is + + signal delayedData : sl; + signal rawWord : slv(13 downto 0); + signal shift1 : sl; + signal shift2 : sl; + + attribute IODELAY_GROUP : string; + attribute IODELAY_GROUP of U_Delay : label is IODELAY_GROUP_G; + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 14 + report "AdcDdrDeserializer7Series supports only native DDR widths 4, 6, 8, 10, and 14" + severity failure; + + U_Delay : IDELAYE2 + generic map ( + DELAY_SRC => "IDATAIN", + HIGH_PERFORMANCE_MODE => "TRUE", + IDELAY_TYPE => "VAR_LOAD", + IDELAY_VALUE => 0, + REFCLK_FREQUENCY => IDELAYCTRL_FREQ_G, + SIGNAL_PATTERN => "DATA") + port map ( + C => delayClk, -- [in] + REGRST => '0', -- [in] + LD => delayLoad, -- [in] + CE => '0', -- [in] + INC => '1', -- [in] + CINVCTRL => '0', -- [in] + CNTVALUEIN => delayValue, -- [in] + IDATAIN => serialData, -- [in] + DATAIN => '0', -- [in] + LDPIPEEN => '0', -- [in] + DATAOUT => delayedData, -- [out] + CNTVALUEOUT => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- Every supported width uses the same master. Q outputs above the selected + -- width and width-expansion shift outputs are ignored when no slave exists. + ------------------------------------------------------------------------------------------------ + U_Master : ISERDESE2 + generic map ( + DATA_RATE => "DDR", + DATA_WIDTH => SERIALIZATION_FACTOR_G, + INTERFACE_TYPE => "NETWORKING", + DYN_CLKDIV_INV_EN => "FALSE", + DYN_CLK_INV_EN => "FALSE", + NUM_CE => 1, + OFB_USED => "FALSE", + IOBDELAY => "IFD", + SERDES_MODE => "MASTER") + port map ( + Q1 => rawWord(0), -- [out] + Q2 => rawWord(1), -- [out] + Q3 => rawWord(2), -- [out] + Q4 => rawWord(3), -- [out] + Q5 => rawWord(4), -- [out] + Q6 => rawWord(5), -- [out] + Q7 => rawWord(6), -- [out] + Q8 => rawWord(7), -- [out] + SHIFTOUT1 => shift1, -- [out] + SHIFTOUT2 => shift2, -- [out] + BITSLIP => bitSlip, -- [in] + CE1 => '1', -- [in] + CE2 => '1', -- [in] + CLK => bitClk, -- [in] + CLKB => bitClkInv, -- [in] + CLKDIV => wordClk, -- [in] + CLKDIVP => '0', -- [in] + D => '0', -- [in] + DDLY => delayedData, -- [in] + RST => rst, -- [in] + SHIFTIN1 => '0', -- [in] + SHIFTIN2 => '0', -- [in] + DYNCLKDIVSEL => '0', -- [in] + DYNCLKSEL => '0', -- [in] + OFB => '0', -- [in] + OCLK => '0', -- [in] + OCLKB => '0', -- [in] + O => open); -- [out] + + ------------------------------------------------------------------------------------------------ + -- The native 10- and 14-bit DDR modes use the dedicated master/slave width + -- expansion path. For width 10 only slave Q3/Q4 are meaningful; width 14 + -- additionally uses Q5 through Q8. + ------------------------------------------------------------------------------------------------ + GEN_WIDTH_WIDE : if SERIALIZATION_FACTOR_G = 10 or SERIALIZATION_FACTOR_G = 14 generate + begin + U_Slave : ISERDESE2 + generic map ( + DATA_RATE => "DDR", + DATA_WIDTH => SERIALIZATION_FACTOR_G, + INTERFACE_TYPE => "NETWORKING", + DYN_CLKDIV_INV_EN => "FALSE", + DYN_CLK_INV_EN => "FALSE", + NUM_CE => 1, + OFB_USED => "FALSE", + IOBDELAY => "IFD", + SERDES_MODE => "SLAVE") + port map ( + Q1 => open, -- [out] + Q2 => open, -- [out] + Q3 => rawWord(8), -- [out] + Q4 => rawWord(9), -- [out] + Q5 => rawWord(10), -- [out] + Q6 => rawWord(11), -- [out] + Q7 => rawWord(12), -- [out] + Q8 => rawWord(13), -- [out] + SHIFTOUT1 => open, -- [out] + SHIFTOUT2 => open, -- [out] + BITSLIP => bitSlip, -- [in] + CE1 => '1', -- [in] + CE2 => '1', -- [in] + CLK => bitClk, -- [in] + CLKB => bitClkInv, -- [in] + CLKDIV => wordClk, -- [in] + CLKDIVP => '0', -- [in] + D => '0', -- [in] + DDLY => '0', -- [in] + RST => rst, -- [in] + SHIFTIN1 => shift1, -- [in] + SHIFTIN2 => shift2, -- [in] + DYNCLKDIVSEL => '0', -- [in] + DYNCLKSEL => '0', -- [in] + OFB => '0', -- [in] + OCLK => '0', -- [in] + OCLKB => '0', -- [in] + O => open); -- [out] + end generate GEN_WIDTH_WIDE; + + dataWord <= rawWord(SERIALIZATION_FACTOR_G-1 downto 0); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd new file mode 100644 index 0000000000..9ddda80da7 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/rtl/AdcDdrPhy7Series.vhd @@ -0,0 +1,220 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Serialized DDR ADC physical input for AMD 7 Series FPGAs +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrPhy7Series is + generic ( + TPD_G : time := 1 ns; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl := '0'; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhy7Series; + +architecture rtl of AdcDdrPhy7Series is + + function bufrDivide (value : positive) return string is + begin + case value is + when 1 => return "1"; + when 2 => return "2"; + when 3 => return "3"; + when 4 => return "4"; + when 5 => return "5"; + when 6 => return "6"; + when 7 => return "7"; + when others => return "8"; + end case; + end function bufrDivide; + + signal dClkPad : sl; + signal bitClk : sl; + signal bitClkInv : sl; + signal wordClk : sl; + signal wordRst : sl; + signal deserReset : sl; + signal fcoPad : slv(FCO_LANES_G-1 downto 0); + signal dataPad : slv(DATA_LANES_G-1 downto 0); + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 14 + report "AdcDdrPhy7Series supports only native DDR widths 4, 6, 8, 10, and 14" + severity failure; + + GEN_MAP_CHECK : for i in DATA_LANES_G-1 downto 0 generate + assert DATA_FCO_MAP_G(i) < FCO_LANES_G + report "AdcDdrPhy7Series data-to-FCO mapping index is out of range" + severity failure; + end generate GEN_MAP_CHECK; + + U_DelayReadySync : entity surf.Synchronizer + generic map ( + TPD_G => TPD_G) + port map ( + clk => wordClk, -- [in] + rst => wordRst, -- [in] + dataIn => idelayCtrlRdy, -- [in] + dataOut => delayReady); -- [out] + + U_DcoInput : IBUFDS + generic map ( + DIFF_TERM => true, + IOSTANDARD => "LVDS_25") + port map ( + I => dClkP, -- [in] + IB => dClkN, -- [in] + O => dClkPad); -- [out] + + U_BitClock : BUFIO + port map ( + I => dClkPad, -- [in] + O => bitClk); -- [out] + + bitClkInv <= not bitClk; + + U_WordClock : BUFR + generic map ( + SIM_DEVICE => "7SERIES", + BUFR_DIVIDE => bufrDivide(SERIALIZATION_FACTOR_G/2)) + port map ( + I => dClkPad, -- [in] + O => wordClk, -- [out] + CE => '1', -- [in] + CLR => '0'); -- [in] + + U_WordReset : entity surf.RstSync + generic map ( + TPD_G => TPD_G, + RELEASE_DELAY_G => 5) + port map ( + clk => wordClk, -- [in] + asyncRst => adcClkRst, -- [in] + syncRst => wordRst); -- [out] + + deserReset <= wordRst or phyReset; + + -- The common PHY command carries the widest supported nine-bit value. Fail + -- on an invalid 7-Series load instead of silently discarding its upper bits. + GEN_FCO : for i in FCO_LANES_G-1 downto 0 generate + begin + assert fcoDelayWrite(i).load /= '1' or fcoDelayWrite(i).value(8 downto 5) = X"0" + report "AdcDdrPhy7Series FCO delay value exceeds the native five-bit range; " & + "the value will be truncated" + severity warning; + + U_Input : IBUFDS + generic map ( + DIFF_TERM => true) + port map ( + I => fcoP(i), -- [in] + IB => fcoN(i), -- [in] + O => fcoPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializer7Series + generic map ( + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(i), -- [in] + delayClk => wordClk, -- [in] + delayValue => fcoDelayWrite(i).value(4 downto 0), -- [in] + delayLoad => fcoDelayWrite(i).load, -- [in] + serialData => fcoPad(i), -- [in] + dataWord => fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0)); -- [out] + + fcoWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_FCO; + + GEN_DATA : for i in DATA_LANES_G-1 downto 0 generate + begin + assert dataDelayWrite(i).load /= '1' or dataDelayWrite(i).value(8 downto 5) = X"0" + report "AdcDdrPhy7Series data delay value exceeds the native five-bit range; " & + "the value will be truncated" + severity warning; + + U_Input : IBUFDS + generic map ( + DIFF_TERM => true) + port map ( + I => dataP(i), -- [in] + IB => dataN(i), -- [in] + O => dataPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializer7Series + generic map ( + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(DATA_FCO_MAP_G(i)), -- [in] + delayClk => wordClk, -- [in] + delayValue => dataDelayWrite(i).value(4 downto 0), -- [in] + delayLoad => dataDelayWrite(i).load, -- [in] + serialData => dataPad(i), -- [in] + dataWord => dataWord(i)(SERIALIZATION_FACTOR_G-1 downto 0)); -- [out] + + dataWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_DATA; + + captureClk <= wordClk; + captureRst <= wordRst; + dataValid <= (others => not deserReset); + fcoValid <= (others => not deserReset); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl b/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl new file mode 100644 index 0000000000..5c74f6b308 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/7Series/ruckus.tcl @@ -0,0 +1,13 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" diff --git a/devices/AnalogDevices/adcDdr/README.md b/devices/AnalogDevices/adcDdr/README.md new file mode 100644 index 0000000000..04961c27b2 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/README.md @@ -0,0 +1,271 @@ +# Serialized DDR ADC Readout + +`adcDdr` is the device-neutral receive and monitoring layer for Analog Devices +ADCs that send source-synchronous serialized DDR data with a forwarded data +clock (DCO) and frame clock (FCO). AD9249, AD9252, and AD9681 provide the +part-specific physical-lane topology around this common layer. + +## Layout + +- [`rtl/AdcDdrPhy.vhd`](rtl/AdcDdrPhy.vhd) selects the FPGA-family input PHY. +- [`rtl/AdcDdrCore.vhd`](rtl/AdcDdrCore.vhd) owns FCO word alignment, delay + controls, coherent samples, debug snapshots, counters, and the normalized + AXI-Lite map. +- [`rtl/AdcDdrPatternTester.vhd`](rtl/AdcDdrPatternTester.vhd) optionally checks + ADC test patterns in hardware over a bounded sample window. +- [`7Series/`](7Series/) and [`UltraScale/`](UltraScale/) contain the family + implementations of the programmable input delays and deserializers. +- [`sim/`](sim/) contains simulation-only pattern helpers. +- [`migration-guide.md`](migration-guide.md) maps the removed AD9681 and + AD9249 Group2 interfaces to their replacements. + +The PyRogue register model is +[`_AdcDdr.py`](../../../python/surf/devices/analog_devices/_AdcDdr.py), and the +device-neutral software calibration process is +[`_AdcDdrCalibration.py`](../../../python/surf/devices/analog_devices/_AdcDdrCalibration.py). +Device adapters in `_Ad9249.py`, `_Ad9252.py`, and `_Ad9681.py` supply the +physical-lane mapping and ADC configuration behavior. Their configuration +devices expose the same `DigitalReset()` and `ResetPNLong()` commands for +direct use from PyRogue and for the common calibration sequence. + +## Delay Controller Readiness + +The `idelayCtrlRdy` input defaults low so an omitted 7-Series connection holds +the capture PHY safely in reset. A 7-Series target must instantiate or reuse an +`IDELAYCTRL`, match its `IODELAY_GROUP`, and connect the controller's `RDY` +output. UltraScale and UltraScale+ use `IDELAYE3` count mode; their selected PHY +does not use `idelayCtrlRdy`. + +If readiness drops after startup, the common core clears frame alignment, +resets the deserializers, stops sample writes, and aborts an outstanding debug +snapshot with `SLVERR`. Programmed data and FCO delays remain retained. When +readiness returns, the core reloads every retained delay before releasing the +PHY and reacquiring alignment. + +The target owns `IDELAYCTRL` reset generation because it also owns the reference +clock and knows when that clock is stable. If `RDY` deasserts, the target must +reset the controller according to the AMD primitive requirements; the common +readout does not generate that reset request. + +## Calibration Model + +Calibration finds a stable sampling point for every FCO and physical data lane. +It uses the ADC's alternating checkerboard test mode by default. For a 14-bit +sample the two expected logical values are `0x2AAA` and `0x1555`; their order is +not important during individual lane scans, but it is enforced during final +full-channel qualification. + +The distinction between a physical lane and a logical channel matters. AD9249 +and AD9252 use one physical data lane per channel. AD9681 uses two lanes per +channel: one contributes logical bits `[5:0]`, and the other contributes bits +`[13:6]`. The calibration adapter supplies a channel number and meaningful-bit +mask for every physical lane so each contribution can be measured separately. + +### Full calibration algorithm + +The `Full calibration` operation performs these phases: + +1. **Preserve state.** Software reads the current ADC test mode and all FCO and + data delay settings. These values can be restored after a stop or failure. +2. **Scan each FCO lane.** For every tap from `DelayStart` through `DelayStop`, + inclusive, software programs that FCO delay, commands `Relock`, waits for + `SettleTime`, and tests the corresponding `LockedMask` bit. +3. **Extract FCO eyes.** Consecutive passing taps form candidate eyes. An eye + must be at least `MinimumEyeWidth` taps wide and must provide at least + `GuardBand` passing taps on both sides of its selected center. For an + even-width eye, the lower center tap is selected. When `CircularDelays` is + enabled, passing runs at the two ends of the scan are merged. All qualifying + FCO eyes are retained, ordered by widest eye and then lowest center tap; the + first eye is the initial choice. +4. **Enable and synchronize the checkerboard pattern.** The ADC is placed in + its device-specific test mode after the initial FCO delays have been selected + and frame lock has been reacquired. Calibration then calls the configuration + device's normalized `DigitalReset()` command, waits for the converter output + to settle, and relocks the FPGA receiver. This establishes one deterministic + sample epoch before any data eye is measured. +5. **Build safe data-lane sweep groups.** Lanes on different logical channels + can move together. Lanes on the same channel can also move together when + their meaningful-bit masks do not overlap. Lanes with overlapping masks are + put in separate groups so a failure remains attributable to one physical + lane. Consequently, all sixteen AD9681 lanes form one sweep group even + though each pair contributes to the same logical channel. +6. **Scan each data group.** At a given tap, all lanes in the group are updated + in one bulk transaction and then measured from the same settled sampling + point. Ordered four-sample snapshots acquire phase independently per lane so + one channel leaving its eye cannot truncate another channel's passing window. + A per-lane verdict uses only its logical sample-bit mask, so an unaligned + partner half does not contaminate it. + Each lane gets its own passing map and centered eye even though compatible + lanes are swept together. +7. **Qualify the assembled checkerboard samples.** With every data lane + centered, software reselects checkerboard, issues another `DigitalReset()`, + waits for the ADC output to settle, and relocks the FPGA receiver immediately + before checking the complete sample width. This sequence is repeated for + every alternate FCO-eye attempt, closing the interval in which a parallel + bank calibration could disturb shared ADC digital state. Channel zero + establishes the checkerboard A/B phase, and every logical channel must match + the same ordered sequence. This catches half-word or sample-epoch errors that + individual masked lane tests and a repetitive FCO word cannot detect. +8. **Run the optional deep checkerboard window.** When `UsePatternTester` is + enabled, the hardware pattern tester checks `PatternTesterSamples` complete + samples at line rate. Every channel must maintain one shared alternating + phase, and every selected FCO word observed during the window must match. + Per-channel word-error counts and accumulated bit-error masks, plus per-FCO + error counts, are retained in the final result. +9. **Qualify PN23 coherence and recurrence.** When `VerifyPn23` is enabled, + software switches the ADC to PN23, pulses the PN-long reset, and reads one + atomic four-sample debug snapshot. All logical channels must contain the + same four words. The reference channel's 56 captured bits must also satisfy + the `x^23 + x^18 + 1` recurrence after the first 23 bits establish its + arbitrary phase. This second condition rejects a common malformed sequence + that channel equality alone would accept. The checker considers the ADC's + possible sample-MSB format conversion and full output inversion; it does not + require the snapshot to begin at the PN seed. When `UsePatternTester` is + enabled, the selected transformation is then applied to a deep hardware + window. The tester acquires an arbitrary nonzero 23-bit history, checks all + subsequent reference bits against the recurrence, and verifies every other + channel remains word-for-word coherent with the reference. +10. **Try alternate FCO windows if necessary.** More than one FCO eye can appear + valid because the repetitive frame pattern may lock in equivalent-looking + unit intervals. If the snapshot, deep checkerboard, or PN23 qualification fails, + calibration tries the Cartesian product of the retained FCO eyes, relocking + and restoring checkerboard mode before each attempt. Data delays are not + rescanned because the FCO choice establishes frame/sample phase without + changing the data-eye centers. The first combination that passes both final + checks is retained. +11. **Publish or restore.** A successful full calibration restores the normal + ADC output mode but leaves the qualified FCO and data delays installed. A + stop, exception, or failed final qualification restores the original test + mode and delays. `ApplyResults` can later reinstall only a complete result + whose final qualification passed. + +This is an eye-centering algorithm, not a bit-error-rate characterization. A +passing tap means every bounded measurement requested by `SampleCount` passed; +it does not prove an arbitrarily low error rate. + +### Snapshot and deep measurements + +Every data tap and the initial full-channel qualification use atomic debug +snapshots. One `SampleCount` unit requests one four-sample snapshot, and the +same coherent samples are reused for every lane mask in a sweep group. The +checker enforces an ordered pattern independently per lane instead of merely +requiring both checkerboard values to appear. The final assembled snapshot and +deep hardware window then require one shared phase across all channels. + +`UsePatternTester` adds deep full-channel measurements after the centered +snapshots pass; it does not replace the tap-scan backend. It defaults to the +readout model's `patternCheck` construction parameter, which must match the RTL +`PATTERN_CHECK_G` generic, and remains user-writable. The bounded checker +in `AdcDdrPatternTester` evaluates `PatternTesterSamples` consecutive valid +samples without transferring them to software. Alternating mode uses one +reference channel for the complete channel group. PN23 mode checks the +reference recurrence from an arbitrary stream phase and checks every other +channel for coherence. Every selected FCO lane must also be observed and remain +error-free. The deep checker requires the `PatternCheck` capability. + +### Verification operations + +`Verify current` checks each installed FCO and data delay without retaining any +temporary changes. `Verify guard band` also checks the taps at `selected - +GuardBand` and `selected + GuardBand`. A requested guard point outside +`DelayStart` through `DelayStop` is a failure. Each lane is restored before the +next lane is tested because multiple physical lanes can contribute to one +logical sample; the complete original configuration is restored when the +verification operation ends. + +## Calibration Controls + +| Control | Default | Meaning | +|---|---:|---| +| `Operation` | `Full calibration` | Full scan, current-tap verification, or guard-band verification. | +| `DelayStart` | `0` | First delay tap included in a scan or allowed verification range. | +| `DelayStop` | Maximum tap | Last delay tap included in a scan or allowed verification range. | +| `MinimumEyeWidth` | `8` taps | Minimum physical passing-window width. | +| `GuardBand` | `2` taps | Required passing margin on each side of the selected center. | +| `CircularDelays` | `False` | Whether the first and last passing scan runs are one wrapped eye. | +| `SampleCount` | `2` | Number of four-sample measurement groups checked at each data tap. | +| `UsePatternTester` | `patternCheck` | Add deep hardware checkerboard and, when enabled, PN23 qualification after centering. | +| `PatternTesterSamples` | `4096` | Valid samples checked by each deep hardware window. | +| `VerifyPn23` | Device dependent | Enable PN23 coherence and recurrence qualification when the adapter provides a PN-long reset control. | +| `SettleTime` | `1 ms` | Wall-clock wait after delay, relock, or ADC test-mode changes. | +| `Debug` | `False` | Retain detailed per-tap diagnostics and publish one completed tree when the operation ends. | +| `Margin` | `Unavailable` | Single-line worst selected-eye margin from the most recent successful full calibration. | + +The full delay range is the safest initial scan. Narrow `DelayStart` and +`DelayStop` only when the board has a characterized region and the entire +expected eye, including failing boundary taps, remains visible. An eye touching +a scan boundary is reported as unbounded on that side unless circular scanning +merges it with the opposite boundary. + +Delay controls are reported in native `tap` units. A tap is not the same +physical interval on every FPGA family. In particular, the UltraScale PHY uses +`IDELAYE3` in uncalibrated `COUNT` mode, so software must not present those tap +counts as a portable time value. ADC sample rate describes the unit interval +but is not sufficient to convert an uncalibrated count into picoseconds. + +## Results and Diagnostics + +`Results` contains the selected eye, complete passing map, and margins for each +FCO and data lane. FCO entries also contain every retained candidate eye. The +`Final` entry contains full-channel qualification captures plus every attempted +FCO-eye combination. `Final.patternTester` reports whether the optional deep +window ran, its requested and checked sample counts, channel/FCO pass masks, +and detailed error counts and masks. When enabled, `Final.pn23` reports the four +samples from every channel, channel-coherence results, each recurrence +transformation tried, the selected valid transformation, and the nested deep +pattern-tester result. A failed scan may publish partial results for the lane +that could not produce a qualifying eye. + +After a successful full calibration, `Margin` reports the smallest left or +right margin across every selected FCO and data eye. `MarginReport()` prints a +per-lane table containing the eye range, selected tap, left and right margins, +worst-side margin, and bounded, scan-limit, or wrapped status. Both use native +`tap` units. + +`Diagnostics` contains the measurement backend, expected patterns, raw and +masked data captures, FCO words and lock masks, and the currently active scan +point. The private working tree is not repeatedly copied into the public +variable during a scan. A successful run publishes it once at termination when +`Debug` is enabled; a failed or stopped run publishes it regardless of the +debug setting so failure evidence is retained. + +`Outcome` reports `IDLE`, `RUNNING`, `PASSED`, `FAILED`, or `STOPPED` +independently of PyRogue's generic process state. Successful GUI-driven runs +finish with PyRogue's normal process message. `RunTime` and process progress +provide the remaining operational monitoring. + +## Device-Specific Behavior + +- **AD9249:** calibration is exposed per eight-channel bank because each bank + has its own DCO, FCO, delays, and capture domain. +- **AD9252:** one calibration process covers the configured logical channels; + ADC test-mode changes are committed with `DeviceUpdate`. +- **AD9681:** one process covers sixteen physical data lanes, eight assembled + logical channels, and two FCO lanes. Disjoint lower/upper sample masks permit + one common data sweep, while final shared-phase qualification verifies that + both serialized halves represent the same sample epoch. ADC test-mode changes + are committed with `DeviceUpdate`. + +## Relationship to Static Timing + +Runtime calibration compensates supported input-delay variation and chooses a +robust point inside the observed eye. It does not create timing constraints, +prove PCB skew limits, or guarantee that the FPGA DCO routing and I/O resources +are legal. + +SURF does not install target XDC for these readouts. The integrating project +owns the package-pin assignments, DCO clock definition, DDR rise/fall input +delays, clock-placement constraints, `IODELAY_GROUP` assignments, and any +board-specific skew budgets in its top-level constraints. Derive timing values +from the selected ADC mode, datasheet timing, PCB flight times, and actual +target hierarchy, then review both min and max timing. + +## Validation Status + +The common RTL, register model, calibration process, pattern tester, and +primitive-free device simulations have focused GHDL/PyRogue regressions. A full +32-tap AD9681 snapshot-based calibration has also completed in VCS +co-simulation: both FCO lanes and all sixteen physical data lanes produced +bounded eyes, and final coherent eight-channel checkerboard qualification +passed. This simulation evidence does not replace target implementation timing +or hardware validation. diff --git a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd new file mode 100644 index 0000000000..998e0c226e --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrDeserializerUltraScale.vhd @@ -0,0 +1,131 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: One delayed DDR input lane for AMD UltraScale FPGAs +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrDeserializerUltraScale is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + SERIALIZATION_FACTOR_G : positive := 14); + port ( + bitClk : in sl; + bitClkInv : in sl; + wordClk : in sl; + rst : in sl; + bitSlip : in sl; + + delayClk : in sl; + delayValue : in slv(8 downto 0); + delayLoad : in sl; + + serialData : in sl; + dataWord : out slv(SERIALIZATION_FACTOR_G-1 downto 0); + dataValid : out sl); +end entity AdcDdrDeserializerUltraScale; + +architecture rtl of AdcDdrDeserializerUltraScale is + + constant ISERDES_WIDTH_C : positive := ite(SERIALIZATION_FACTOR_G <= 6, 4, 8); + + signal delayedData : sl; + signal rawData : slv(7 downto 0); + signal gearboxData : slv(SERIALIZATION_FACTOR_G-1 downto 0); + +begin + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 12 or SERIALIZATION_FACTOR_G = 14 + report "AdcDdrDeserializerUltraScale supports only DDR widths 4, 6, 8, 10, 12, and 14" + severity failure; + + U_Delay : entity surf.Idelaye3Wrapper + generic map ( + CASCADE => "NONE", + DELAY_FORMAT => "COUNT", + DELAY_SRC => "IDATAIN", + DELAY_TYPE => "VAR_LOAD", + DELAY_VALUE => 0, + IS_CLK_INVERTED => '0', + IS_RST_INVERTED => '0', + SIM_DEVICE => DEVICE_FAMILY_G, + UPDATE_MODE => "ASYNC") + port map ( + BUSY => open, -- [out] + CASC_IN => '0', -- [in] + CASC_OUT => open, -- [out] + CASC_RETURN => '0', -- [in] + CNTVALUEOUT => open, -- [out] + DATAOUT => delayedData, -- [out] + CE => '0', -- [in] + CLK => delayClk, -- [in] + CNTVALUEIN => delayValue, -- [in] + DATAIN => '0', -- [in] + EN_VTC => '0', -- [in] + IDATAIN => serialData, -- [in] + INC => '0', -- [in] + LOAD => delayLoad, -- [in] + RST => rst); -- [in] + + U_Deserializer : ISERDESE3 + generic map ( + DATA_WIDTH => ISERDES_WIDTH_C, + FIFO_ENABLE => "FALSE", + FIFO_SYNC_MODE => "FALSE", + IS_CLK_B_INVERTED => '0', + IS_CLK_INVERTED => '0', + IS_RST_INVERTED => '0', + SIM_DEVICE => DEVICE_FAMILY_G) + port map ( + FIFO_EMPTY => open, -- [out] + INTERNAL_DIVCLK => open, -- [out] + Q => rawData, -- [out] + CLK => bitClk, -- [in] + CLKDIV => wordClk, -- [in] + CLK_B => bitClkInv, -- [in] + D => delayedData, -- [in] + FIFO_RD_CLK => '0', -- [in] + FIFO_RD_EN => '0', -- [in] + RST => rst); -- [in] + + U_Gearbox : entity surf.Gearbox + generic map ( + TPD_G => TPD_G, + SLAVE_WIDTH_G => ISERDES_WIDTH_C, + MASTER_WIDTH_G => SERIALIZATION_FACTOR_G, + MASTER_BIT_REVERSE_G => true) + port map ( + clk => wordClk, -- [in] + rst => rst, -- [in] + slaveData => rawData(ISERDES_WIDTH_C-1 downto 0), -- [in] + slaveValid => '1', -- [in] + slaveReady => open, -- [out] + startOfSeq => '0', -- [in] + slip => bitSlip, -- [in] + masterData => gearboxData, -- [out] + masterValid => dataValid, -- [out] + masterReady => '1'); -- [in] + + dataWord <= gearboxData; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd new file mode 100644 index 0000000000..3206d68960 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/rtl/AdcDdrPhyUltraScale.vhd @@ -0,0 +1,182 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Serialized DDR ADC physical input for AMD UltraScale FPGAs +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +library unisim; +use unisim.vcomponents.all; + +entity AdcDdrPhyUltraScale is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhyUltraScale; + +architecture rtl of AdcDdrPhyUltraScale is + + constant ISERDES_WIDTH_C : positive := ite(SERIALIZATION_FACTOR_G <= 6, 4, 8); + + signal bitClk : sl; + signal bitClkInv : sl; + signal wordClk : sl; + signal wordRst : sl; + signal deserReset : sl; + signal fcoPad : slv(FCO_LANES_G-1 downto 0); + signal dataPad : slv(DATA_LANES_G-1 downto 0); + +begin + + assert DEVICE_FAMILY_G = "ULTRASCALE" or DEVICE_FAMILY_G = "ULTRASCALE_PLUS" + report "AdcDdrPhyUltraScale DEVICE_FAMILY_G must be ULTRASCALE or ULTRASCALE_PLUS" + severity failure; + + assert SERIALIZATION_FACTOR_G = 4 or SERIALIZATION_FACTOR_G = 6 or + SERIALIZATION_FACTOR_G = 8 or SERIALIZATION_FACTOR_G = 10 or + SERIALIZATION_FACTOR_G = 12 or SERIALIZATION_FACTOR_G = 14 + report "AdcDdrPhyUltraScale supports only DDR widths 4, 6, 8, 10, 12, and 14" + severity failure; + + GEN_MAP_CHECK : for i in DATA_LANES_G-1 downto 0 generate + assert DATA_FCO_MAP_G(i) < FCO_LANES_G + report "AdcDdrPhyUltraScale data-to-FCO mapping index is out of range" + severity failure; + end generate GEN_MAP_CHECK; + + U_DcoInput : IBUFGDS + port map ( + I => dClkP, -- [in] + IB => dClkN, -- [in] + O => bitClk); -- [out] + + bitClkInv <= not bitClk; + + U_WordClock : BUFGCE_DIV + generic map ( + BUFGCE_DIVIDE => ISERDES_WIDTH_C/2, + IS_CE_INVERTED => '0', + IS_CLR_INVERTED => '0', + IS_I_INVERTED => '0') + port map ( + I => bitClk, -- [in] + O => wordClk, -- [out] + CE => '1', -- [in] + CLR => '0'); -- [in] + + U_WordReset : entity surf.RstSync + generic map ( + TPD_G => TPD_G, + RELEASE_DELAY_G => 5) + port map ( + clk => wordClk, -- [in] + asyncRst => adcClkRst, -- [in] + syncRst => wordRst); -- [out] + + deserReset <= wordRst or phyReset; + + GEN_FCO : for i in FCO_LANES_G-1 downto 0 generate + begin + U_Input : IBUFDS + port map ( + I => fcoP(i), -- [in] + IB => fcoN(i), -- [in] + O => fcoPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializerUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(i), -- [in] + delayClk => wordClk, -- [in] + delayValue => fcoDelayWrite(i).value, -- [in] + delayLoad => fcoDelayWrite(i).load, -- [in] + serialData => fcoPad(i), -- [in] + dataWord => fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0), -- [out] + dataValid => fcoValid(i)); -- [out] + + fcoWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_FCO; + + GEN_DATA : for i in DATA_LANES_G-1 downto 0 generate + begin + U_Input : IBUFDS + port map ( + I => dataP(i), -- [in] + IB => dataN(i), -- [in] + O => dataPad(i)); -- [out] + + U_Deserializer : entity surf.AdcDdrDeserializerUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G) + port map ( + bitClk => bitClk, -- [in] + bitClkInv => bitClkInv, -- [in] + wordClk => wordClk, -- [in] + rst => deserReset, -- [in] + bitSlip => bitSlip(DATA_FCO_MAP_G(i)), -- [in] + delayClk => wordClk, -- [in] + delayValue => dataDelayWrite(i).value, -- [in] + delayLoad => dataDelayWrite(i).load, -- [in] + serialData => dataPad(i), -- [in] + dataWord => dataWord(i)(SERIALIZATION_FACTOR_G-1 downto 0), -- [out] + dataValid => dataValid(i)); -- [out] + + dataWord(i)(15 downto SERIALIZATION_FACTOR_G) <= (others => '0'); + end generate GEN_DATA; + + captureClk <= wordClk; + captureRst <= wordRst; + delayReady <= not wordRst; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl b/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl new file mode 100644 index 0000000000..5c74f6b308 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/UltraScale/ruckus.tcl @@ -0,0 +1,13 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" diff --git a/devices/AnalogDevices/adcDdr/migration-guide.md b/devices/AnalogDevices/adcDdr/migration-guide.md new file mode 100644 index 0000000000..82044b8149 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/migration-guide.md @@ -0,0 +1,265 @@ +# Serialized DDR ADC Migration Guide + +## Scope + +This guide covers the coordinated migrations required by removal of: + +- the old 7-Series `Ad9681Readout` RTL and its corresponding PyRogue map; +- the transitional `Ad9681Readout2` development name; +- `Ad9249ReadoutGroup2` RTL and its PyRogue map. + +The replacements use the normalized `AdcDdr` register map. They are +not register-compatible aliases. Update RTL, AXI-Lite address allocation, +PyRogue construction, command paths, constraints, and characterized delay +values together. + +These downstream migrations are expected after the SURF release containing the +AdcDdr-based interfaces. A consumer that still uses a removed interface must stay +pinned to its previous SURF revision until its firmware and software are +migrated together. + +The permanently retained 7-Series and UltraScale `Ad9249ReadoutGroup` +interfaces are unaffected. Projects choosing to move those widely deployed +legacy interfaces to the AdcDdr-based readout should use the dedicated +[AD9249 readout migration guide](../ad9249/README.md). + +## Common Integration Changes + +Each AdcDdr readout region occupies `0x1000` bytes. Its normalized registers +extend through the pattern-tester window beginning at `0x800`. Do not retain an +old `0x100`-byte AXI-Lite slot or place the next endpoint inside this window. + +AdcDdr-based wrappers add two target-facing inputs: + +- `idelayCtrlRdy` reports that the target-owned input-delay controller is ready + and defaults low. Drive the real ready indication in 7-Series hardware. The + UltraScale/UltraScale+ PHY ignores this input. +- `adcStreamRst` resets the coherent stream-domain FIFO and output state. Drive + the reset synchronous to `adcStreamClk` according to the target reset plan. + +The target must also provide an `IDELAYCTRL` with an `IODELAY_GROUP` matching +`IODELAY_GROUP_G` where the selected FPGA family requires it. Source-synchronous +pin and timing constraints remain target-owned; update the project's top-level +XDC for its actual pinout, ADC mode, DCO frequency, datasheet timing, PCB skew, +and clocking hierarchy. + +If `IDELAYCTRL.RDY` deasserts after startup, the readout holds its PHY in reset, +clears alignment, stops sample writes, aborts an outstanding snapshot with +`SLVERR`, and waits for readiness to return before reloading every retained +delay. The target must reset `IDELAYCTRL` after its reference clock is stable; +the readout intentionally does not generate the target-owned controller reset. + +The normalized stream contract is: + +- one fixed two-byte transfer per logical channel and sample epoch; +- `tDest` equals the logical channel number; +- `tUser(0)` is asserted when the sample was captured while FCO alignment was + not locked; +- `tLast` remains deasserted; +- a stopped or slow stream clock can cause coherent group drops, reported by + `AnyOverflow` and `OverflowCount`; asserting `adcStreamRst` instead flushes + any samples queued in the stream-domain FIFO. + +AXI-Lite state is owned in the ADC capture domain. Do not access a readout while +its selected DCO is absent. + +## warm-tdm: AD9681 + +### RTL entity and generics + +The entity remains `surf.Ad9681Readout`, but it now refers to the family-neutral +wrapper in `ad9681/core/Ad9681Readout.vhd`. + +| Old generic | Replacement | +|---|---| +| `TPD_G` | `TPD_G` | +| `SIMULATION_G` | Remove; simulation selection belongs to the build/PHY environment. | +| `IODELAY_GROUP_G` | `IODELAY_GROUP_G` | +| `IDELAYCTRL_FREQ_G` | `IDELAYCTRL_FREQ_G` | +| `DEFAULT_DELAY_G` | Expand into all entries of `DATA_DELAY_INIT_G` and `FCO_DELAY_INIT_G`, then characterize independently. | +| `INVERT_G` | No blind one-to-one mapping. Use `ADC_INVERT_CH_G` for physical P/N lane inversion and `OFFSET_BINARY_G` for ADC numeric coding. | +| `NEGATE_G` | `NEGATE_G` | + +Set these new generics deliberately: + +- `DEVICE_FAMILY_G => "7SERIES"` for the current warm-tdm 7-Series target; +- `CAPTURE_DCLK_IDX_G => 0` to preserve the old DCLK0 single-domain behavior, + or `1` only after separately validating DCLK1; +- `PATTERN_CHECK_G => true` when hardware-assisted calibration is desired; +- `LEFT_JUSTIFY_G => true` to preserve warm-tdm's previous sample placement in + stream bits `[15:2]`. + +The old shared delay for half `i` affected its FCO and all eight data lanes. +For an initial like-for-like conversion, copy it to: + +```vhdl +DATA_DELAY_INIT_G => ( + 15 downto 8 => OLD_DELAY_1_C, + 7 downto 0 => OLD_DELAY_0_C), +FCO_DELAY_INIT_G => ( + 1 => OLD_DELAY_1_C, + 0 => OLD_DELAY_0_C), +``` + +Then replace those seed values with per-lane calibration results. Physical +data lane `(8*half)+channel` maps to `DATA_DELAY_INIT_G((8*half)+channel)`. + +Add the new ports to the instantiation: + +```vhdl +idelayCtrlRdy => adcIdelayCtrlRdy, -- [in] +adcStreamRst => adcStreamRst, -- [in] +``` + +All existing AXI-Lite, `adcClkRst`, `adcSerial`, `adcStreamClk`, and +eight-channel `adcStreams` connections retain their roles. + +### PyRogue and scripts + +Continue constructing `surf.devices.analog_devices.Ad9681Readout`, but replace +the old `fpga` and `channels` arguments with the RTL device-family selection: + +```python +self.add(surf.devices.analog_devices.Ad9681Readout( + name = 'Ad9681Readout', + offset = 0x00000000, + deviceFamily = '7SERIES', + enabled = True)) +``` + +The RTL and PyRogue models derive a five-bit delay value for `7SERIES` and a +nine-bit delay value for `ULTRASCALE` or `ULTRASCALE_PLUS`. + +Update script paths as follows: + +| Removed node or command | AdcDdr node or command | +|---|---| +| `Delay[i]` | `FcoDelay[i]` plus `DataDelay[(8*i)+ch]` for channels `0..7` | +| `EnUsrDelay` | Remove; programmed delays are always authoritative after startup. | +| `Relock()` | `Relock()` | +| `LostLockCountReset()` | `ClearCounters()` | +| `Locked[i]` | bit `i` of `LockedMask`; use `AllLocked` for the aggregate condition | +| `LostLockCount[i]` | `LostLockCount[i]` | +| `AdcFrameSync[i]` | `FcoWord[i]` | +| `AdcChannel[ch]` | call `Snapshot()`, then read the four coherent values in `DebugSample[ch]` | +| `AdcVoltage[ch]` | call `Snapshot()`, then use `DebugVoltage[ch]` for the oldest captured sample after configuring its range/format | +| `FreezeDebug` | Remove; `Snapshot()` publishes one atomic four-sample bank. | +| `Invert`, `Negate` | Set the corresponding compile-time wrapper semantics and ADC output format; there are no runtime normalized-map equivalents. | +| `ErrorDetCount[i]` | No direct equivalent; use `LostLockCount[i]`, lock state, and calibration/pattern-test diagnostics. | + +In warm-tdm initialization, the existing `Relock()` call can remain and the +immediately following `LostLockCountReset()` call should become +`ClearCounters()`. + +### AD9681 validation + +Before accepting the migration: + +1. Confirm the AXI crossbar reserves at least `0x1000` bytes. +2. Elaborate the target with exactly one `Ad9681Readout` entity declaration. +3. Check `idelayCtrlRdy`, DCLK selection, generated clocks, CDC, and both + source-synchronous input halves in Vivado reports. +4. Verify stream samples remain left-justified and preserve channel ordering. +5. Run full calibration, save per-lane/FCO centers, then run verify-current. +6. Exercise relock, counter clear, stopped-DCO diagnostics, and overflow status. + +## ldmx-firmware: AD9249 + +### RTL entity and generics + +Replace each one-bank `surf.Ad9249ReadoutGroup2` instantiation with +`surf.Ad9249ReadoutBank`. Do not use full `Ad9249Readout` unless one integration +owns both independent DCO/FCO banks and can allocate its `0x2000`-byte, +two-region map. + +| Group2 generic | AdcDdr bank replacement | +|---|---| +| `TPD_G` | `TPD_G` | +| `SIM_DEVICE_G` | `DEVICE_FAMILY_G`; use `"ULTRASCALE_PLUS"` for the current tracker target | +| `NUM_CHANNELS_G` | `NUM_CHANNELS_G` | +| `SIMULATION_G` | Remove; simulation selection belongs to the build/PHY environment. | +| `DEFAULT_DELAY_G` | Expand into every active `DATA_DELAY_INIT_G` entry and `FCO_DELAY_INIT_G(0)`. | +| `ADC_INVERT_CH_G` | `ADC_INVERT_CH_G` | + +The selected `DEVICE_FAMILY_G` also selects the native delay width. Provide +`IODELAY_GROUP_G`/`IDELAYCTRL_FREQ_G`, and decide `OFFSET_BINARY_G`, +`NEGATE_G`, and `PATTERN_CHECK_G` explicitly. + +For a like-for-like starting point from the old common delay: + +```vhdl +DATA_DELAY_INIT_G => (others => DEFAULT_DELAY_TAPS_C), +FCO_DELAY_INIT_G => (0 => DEFAULT_DELAY_TAPS_C), +``` + +Declare `DEFAULT_DELAY_TAPS_C` as a `natural` when converting the old +`slv(8 downto 0)` generic value. The readout checks that each natural tap count +fits the five- or nine-bit width selected by `DEVICE_FAMILY_G`. Characterized +per-lane values should replace the replicated seed after calibration. + +Add `idelayCtrlRdy` and `adcStreamRst` connections as described above. The +`Ad9249SerialGroupType`, AXI-Lite ports, stream clock, and per-channel stream +array retain their roles. AD9249 samples remain right-justified in +stream bits `[13:0]`, matching Group2's placement. + +### PyRogue and address layout + +Replace: + +```python +surf.devices.analog_devices.Ad9249ReadoutGroup2(...) +``` + +with: + +```python +surf.devices.analog_devices.Ad9249ReadoutBank( + name = f'Ad9249Readout[{i}]', + offset = i * 0x1000, + deviceFamily = 'ULTRASCALE_PLUS', + enabled = True) +``` + +The old tracker code used `i*0x100`; that stride must become at least +`i*0x1000`, and the containing AXI crossbar must expose the same expanded +windows. + +Update nodes and commands as follows: + +| Group2 node or command | AdcDdr bank node or command | +|---|---| +| `Delay` | `FcoDelay[0]` and each active `DataDelay[ch]` | +| `Relock()` | `Relock()` | +| `LostLockCountReset()` | `ClearCounters()` | +| `Locked` | `AllLocked` or bit 0 of `LockedMask` | +| `LostLockCount` | `LostLockCount[0]` | +| `AdcFrameSync` | `FcoWord[0]` | +| `AdcChannel[ch]` | call `Snapshot()`, then read `DebugSample[ch]` | +| `AdcVoltage[ch]` | call `Snapshot()`, then read `DebugVoltage[ch]` for the oldest captured sample | +| `FreezeDebug` | Remove; use atomic `Snapshot()`. | +| `Invert` | Configure `ADC_INVERT_CH_G`, `OFFSET_BINARY_G`, and the ADC output format deliberately. | +| `ErrorDetCount` | No direct equivalent; use `LostLockCount[0]` and pattern/calibration diagnostics. | + +### AD9249 validation + +Before accepting the LDMX migration: + +1. Confirm every bank receives a non-overlapping `0x1000` AXI-Lite window. +2. Confirm each physical bank still uses its own DCO/FCO capture domain. +3. Elaborate the UltraScale+ target and review delay-controller, clock, CDC, + and input timing reports. +4. Verify sample channel order, right justification, `tDest`, and unlock marking. +5. Run full calibration per bank, save independent FCO/data centers, then run + verify-current. +6. Exercise relock, counter clear, overflow, and stopped-clock diagnostics. + +## Removed and Retained Interfaces + +After this coordinated change, SURF no longer supplies +`Ad9249ReadoutGroup2`, `Ad9681Readout2`, or `Ad9681ReadoutManual`. The old +AD9681 7-Series implementation has been replaced by the family-neutral +`Ad9681Readout`. + +The existing 7-Series and UltraScale `Ad9249ReadoutGroup` RTL entities and +matching PyRogue class remain supported and unchanged. The public +names are final; no additional readout rename is planned as part of this work. diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd new file mode 100644 index 0000000000..88aae31bae --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd @@ -0,0 +1,636 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Common control and readout core for serialized DDR ADCs +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.std_logic_unsigned.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrCore is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := (others => '0'); + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + CHANNELS_G : positive := 8; + SAMPLE_WIDTH_G : positive range 2 to 16 := 14; + SERIALIZATION_FACTOR_G : positive := 14; + DELAY_BITS_G : positive range 1 to 9 := 5; + DATA_DELAY_INIT_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0); + FCO_DELAY_INIT_G : NaturalArray(FCO_LANES_G-1 downto 0) := (others => 0); + FIFO_ADDR_WIDTH_G : positive := 4; + FRAME_PATTERN_G : slv := "11111110000000"; + PATTERN_CHECK_G : boolean := true; + OFFSET_BINARY_G : boolean := false; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + + captureClk : in sl; + captureRst : in sl; + delayReady : in sl; + fcoWord : in Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : in slv(FCO_LANES_G-1 downto 0) := (others => '1'); + sampleValid : in sl; + sampleIn : in Slv16Array(CHANNELS_G-1 downto 0); + phyReset : out sl; + bitSlip : out slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : out AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : out AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + streamClk : in sl; + streamRst : in sl; + streams : out AxiStreamMasterArray(CHANNELS_G-1 downto 0)); +end entity AdcDdrCore; + +architecture rtl of AdcDdrCore is + + constant FIFO_WIDTH_C : positive := (16*CHANNELS_G)+1; + constant AXIS_CONFIG_C : AxiStreamConfigType := ( + TSTRB_EN_C => false, + TDATA_BYTES_C => 2, + TDEST_BITS_C => 8, + TID_BITS_C => 0, + TKEEP_MODE_C => TKEEP_FIXED_C, + TUSER_BITS_C => 1, + TUSER_MODE_C => TUSER_NORMAL_C); + constant AXIL_CONFIG_C : AxiLiteCrossbarMasterConfigArray(1 downto 0) := ( + 0 => ( + baseAddr => AXIL_BASE_ADDR_G, + addrBits => 11, + connectivity => x"0001"), + 1 => ( + baseAddr => AXIL_BASE_ADDR_G + resize(ADC_DDR_PATTERN_BASE_ADDR_C, 32), + addrBits => 8, + connectivity => x"0001")); + + type FcoCountArray is array (natural range <>) of natural range 0 to ADC_DDR_LOCK_MATCHES_C; + type FcoErrorArray is array (natural range <>) of natural range 0 to ADC_DDR_UNLOCK_ERRORS_C; + type FcoWaitArray is array (natural range <>) of natural range 0 to ADC_DDR_BITSLIP_INTERVAL_C-1; + + function delayValuesToSlv ( + values : NaturalArray; + delayClass : string) + return Slv9Array is + variable result : Slv9Array(values'range); + begin + for i in values'range loop + assert values(i) < 2**DELAY_BITS_G + report "AdcDdrCore " & delayClass & " initial delay exceeds DELAY_BITS_G" + severity failure; + result(i) := toSlv(values(i), 9); + end loop; + return result; + end function delayValuesToSlv; + + type RegType is record + phyReset : sl; + startupPending : sl; + resetHold : natural range 0 to ADC_DDR_RESET_HOLD_C; + relock : sl; + clearCounters : sl; + bitSlip : slv(FCO_LANES_G-1 downto 0); + locked : slv(FCO_LANES_G-1 downto 0); + matchCount : FcoCountArray(FCO_LANES_G-1 downto 0); + errorCount : FcoErrorArray(FCO_LANES_G-1 downto 0); + slipWait : FcoWaitArray(FCO_LANES_G-1 downto 0); + lostLockCount : Slv32Array(FCO_LANES_G-1 downto 0); + overflowCount : slv(31 downto 0); + overflow : sl; + dataDelay : Slv9Array(DATA_LANES_G-1 downto 0); + fcoDelay : Slv9Array(FCO_LANES_G-1 downto 0); + dataDelayLoad : slv(DATA_LANES_G-1 downto 0); + fcoDelayLoad : slv(FCO_LANES_G-1 downto 0); + debugBusy : sl; + debugIndex : natural range 0 to 3; + snapshotSeq : slv(31 downto 0); + debugWorking : Slv16Array((CHANNELS_G*4)-1 downto 0); + debugSample : Slv16Array((CHANNELS_G*4)-1 downto 0); + formatted : Slv16Array(CHANNELS_G-1 downto 0); + sampleValid : sl; + axilReadSlave : AxiLiteReadSlaveType; + axilWriteSlave : AxiLiteWriteSlaveType; + end record RegType; + + constant REG_INIT_C : RegType := ( + phyReset => '0', + startupPending => '1', + resetHold => ADC_DDR_RESET_HOLD_C, + relock => '0', + clearCounters => '0', + bitSlip => (others => '0'), + locked => (others => '0'), + matchCount => (others => 0), + errorCount => (others => 0), + slipWait => (others => 0), + lostLockCount => (others => (others => '0')), + overflowCount => (others => '0'), + overflow => '0', + dataDelay => delayValuesToSlv(DATA_DELAY_INIT_G, "data"), + fcoDelay => delayValuesToSlv(FCO_DELAY_INIT_G, "FCO"), + dataDelayLoad => (others => '0'), + fcoDelayLoad => (others => '0'), + debugBusy => '0', + debugIndex => 0, + snapshotSeq => (others => '0'), + debugWorking => (others => (others => '0')), + debugSample => (others => (others => '0')), + formatted => (others => (others => '0')), + sampleValid => '0', + axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + + signal syncAxilReadMaster : AxiLiteReadMasterType; + signal syncAxilReadSlave : AxiLiteReadSlaveType; + signal syncAxilWriteMaster : AxiLiteWriteMasterType; + signal syncAxilWriteSlave : AxiLiteWriteSlaveType; + + signal xbarMasterReadMasters : AxiLiteReadMasterArray(1 downto 0); + signal xbarMasterReadSlaves : AxiLiteReadSlaveArray(1 downto 0); + signal xbarMasterWriteMasters : AxiLiteWriteMasterArray(1 downto 0); + signal xbarMasterWriteSlaves : AxiLiteWriteSlaveArray(1 downto 0); + + signal fifoIn : slv(FIFO_WIDTH_C-1 downto 0); + signal fifoOut : slv(FIFO_WIDTH_C-1 downto 0); + signal fifoValid : sl; + signal fifoOverflow : sl; + +begin + + -- The frame-pattern width is part of the logical PHY contract. Catch a + -- device-wrapper mismatch at elaboration instead of silently truncating it. + assert FRAME_PATTERN_G'length = SERIALIZATION_FACTOR_G + report "FRAME_PATTERN_G length must equal SERIALIZATION_FACTOR_G" + severity failure; + + assert DATA_LANES_G <= 64 + report "Data-delay register window supports at most 64 lanes" + severity failure; + + assert FCO_LANES_G <= 16 + report "FCO register windows support at most 16 lanes" + severity failure; + + assert CHANNELS_G <= 32 + report "Debug register window supports at most 32 channels" + severity failure; + + assert not PATTERN_CHECK_G or CHANNELS_G <= 16 + report "Pattern result windows support at most 16 channels" + severity failure; + + assert not PATTERN_CHECK_G or FCO_LANES_G <= 16 + report "Pattern result windows support at most 16 FCO lanes" + severity failure; + + ------------------------------------------------------------------------------------------------- + -- Cross the complete AXI-Lite bus into the ADC word-clock domain. Keeping + -- the endpoint beside the capture state eliminates individual command and + -- status CDC paths and makes multi-bit register reads inherently coherent. + ------------------------------------------------------------------------------------------------- + U_AxiLiteAsync : entity surf.AxiLiteAsync + generic map ( + TPD_G => TPD_G) + port map ( + sAxiClk => axilClk, -- [in] + sAxiClkRst => axilRst, -- [in] + sAxiReadMaster => axilReadMaster, -- [in] + sAxiReadSlave => axilReadSlave, -- [out] + sAxiWriteMaster => axilWriteMaster, -- [in] + sAxiWriteSlave => axilWriteSlave, -- [out] + mAxiClk => captureClk, -- [in] + mAxiClkRst => captureRst, -- [in] + mAxiReadMaster => syncAxilReadMaster, -- [out] + mAxiReadSlave => syncAxilReadSlave, -- [in] + mAxiWriteMaster => syncAxilWriteMaster, -- [out] + mAxiWriteSlave => syncAxilWriteSlave); -- [in] + + U_AxiLiteCrossbar : entity surf.AxiLiteCrossbar + generic map ( + TPD_G => TPD_G, + NUM_SLAVE_SLOTS_G => 1, + NUM_MASTER_SLOTS_G => 2, + MASTERS_CONFIG_G => AXIL_CONFIG_C) + port map ( + axiClk => captureClk, -- [in] + axiClkRst => captureRst, -- [in] + sAxiReadMasters(0) => syncAxilReadMaster, -- [in] + sAxiReadSlaves(0) => syncAxilReadSlave, -- [out] + sAxiWriteMasters(0) => syncAxilWriteMaster, -- [in] + sAxiWriteSlaves(0) => syncAxilWriteSlave, -- [out] + mAxiReadMasters => xbarMasterReadMasters, -- [out] + mAxiReadSlaves => xbarMasterReadSlaves, -- [in] + mAxiWriteMasters => xbarMasterWriteMasters, -- [out] + mAxiWriteSlaves => xbarMasterWriteSlaves); -- [in] + + comb : process (captureRst, delayReady, fcoValid, fcoWord, fifoOverflow, r, + sampleIn, sampleValid, xbarMasterReadMasters, + xbarMasterWriteMasters) is + variable v : RegType; + variable ep : AxiLiteEndpointType; + variable word : slv(SAMPLE_WIDTH_G-1 downto 0); + variable snapshotTxn : sl; + begin + v := r; + + ---------------------------------------------------------------------------------------------- + -- Default all command-like outputs low. AXI writes below assert these + -- fields for exactly one captureClk cycle; sampleValid is pipelined with + -- the formatted sample written into the output FIFO. + ---------------------------------------------------------------------------------------------- + v.relock := '0'; + v.clearCounters := '0'; + v.bitSlip := (others => '0'); + v.dataDelayLoad := (others => '0'); + v.fcoDelayLoad := (others => '0'); + v.sampleValid := sampleValid; + + ---------------------------------------------------------------------------------------------- + -- FCO word alignment and continuous lock monitoring + -- + -- While unlocked, require consecutive matching frame words before + -- declaring lock. A mismatch requests bitslip only after the enforced + -- quiet interval. Once locked, consecutive errors drop lock and increment + -- the saturating lost-lock counter. + ---------------------------------------------------------------------------------------------- + for i in FCO_LANES_G-1 downto 0 loop + if (r.relock = '1' or r.phyReset = '1' or r.startupPending = '1' or + delayReady = '0') then + v.locked(i) := '0'; + v.matchCount(i) := 0; + v.errorCount(i) := 0; + v.slipWait(i) := 0; + elsif (fcoValid(i) = '1') then + if (r.locked(i) = '0') then + v.errorCount(i) := 0; + if (r.slipWait(i) /= 0) then + -- Do not evaluate the ISERDES output until the DDR BITSLIP + -- operation has propagated through its CLKDIV pipeline. + v.matchCount(i) := 0; + v.slipWait(i) := r.slipWait(i) - 1; + elsif (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) = FRAME_PATTERN_G) then + if (r.matchCount(i) = ADC_DDR_LOCK_MATCHES_C-1) then + v.matchCount(i) := ADC_DDR_LOCK_MATCHES_C; + v.locked(i) := '1'; + else + v.matchCount(i) := r.matchCount(i) + 1; + end if; + else + v.matchCount(i) := 0; + v.bitSlip(i) := '1'; + v.slipWait(i) := ADC_DDR_BITSLIP_INTERVAL_C-1; + end if; + else + v.matchCount(i) := ADC_DDR_LOCK_MATCHES_C; + v.slipWait(i) := 0; + if (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) = FRAME_PATTERN_G) then + v.errorCount(i) := 0; + elsif (r.errorCount(i) = ADC_DDR_UNLOCK_ERRORS_C-1) then + v.locked(i) := '0'; + v.matchCount(i) := 0; + v.errorCount(i) := 0; + if (r.lostLockCount(i) /= x"FFFFFFFF") then + v.lostLockCount(i) := r.lostLockCount(i) + 1; + end if; + else + v.errorCount(i) := r.errorCount(i) + 1; + end if; + end if; + end if; + end loop; + + ---------------------------------------------------------------------------------------------- + -- Event accounting + -- + -- The wide sample FIFO represents one coherent channel group, so one + -- sticky flag and counter account for every dropped sample group. + ---------------------------------------------------------------------------------------------- + if (fifoOverflow = '1') then + v.overflow := '1'; + if (r.overflowCount /= x"FFFFFFFF") then + v.overflowCount := r.overflowCount + 1; + end if; + end if; + + -- Clear is deliberately applied after event accumulation so software gets + -- a deterministic all-zero result even when an event is present that cycle. + if (r.clearCounters = '1') then + v.lostLockCount := (others => (others => '0')); + v.overflowCount := (others => '0'); + v.overflow := '0'; + end if; + + ---------------------------------------------------------------------------------------------- + -- Numeric sample formatting + -- + -- Physical lane polarity and word ordering are resolved by the device + -- wrapper. Here offset-binary conversion and optional arithmetic negation + -- operate only across the meaningful ADC width; upper transport bits stay zero. + ---------------------------------------------------------------------------------------------- + if (sampleValid = '1') then + for i in CHANNELS_G-1 downto 0 loop + word := sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0); + if (OFFSET_BINARY_G) then + word(SAMPLE_WIDTH_G-1) := not word(SAMPLE_WIDTH_G-1); + end if; + if (NEGATE_G) then + word := (not word) + 1; + end if; + v.formatted(i) := (others => '0'); + v.formatted(i)(SAMPLE_WIDTH_G-1 downto 0) := word; + end loop; + end if; + + ---------------------------------------------------------------------------------------------- + -- Atomic debug snapshot + -- + -- Four raw, assembled channel groups are collected before numeric format + -- conversion so physical-lane calibration is independent of offset-binary + -- conversion and arithmetic negation. Publishing the whole bank only + -- after the fourth group prevents AXI software from observing a snapshot + -- assembled from different requests. + ---------------------------------------------------------------------------------------------- + if (r.debugBusy = '1' and sampleValid = '1' and delayReady = '1') then + for i in CHANNELS_G-1 downto 0 loop + v.debugWorking((r.debugIndex*CHANNELS_G)+i) := + resize(sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0), 16); + end loop; + if (r.debugIndex = 3) then + v.debugBusy := '0'; + v.snapshotSeq := r.snapshotSeq + 1; + v.debugSample := v.debugWorking; + else + v.debugIndex := r.debugIndex + 1; + end if; + end if; + + ---------------------------------------------------------------------------------------------- + -- Normalized AXI-Lite register map + -- + -- Delay writes update the retained programmed value and create a one-cycle + -- load strobe for the corresponding PHY lane. Status and debug windows are + -- read directly in this clock domain, so no secondary status image exists. + ---------------------------------------------------------------------------------------------- + axiSlaveWaitTxn(ep, xbarMasterWriteMasters(0), xbarMasterReadMasters(0), + v.axilWriteSlave, v.axilReadSlave); + + -- A snapshot write remains outstanding until the complete four-sample + -- bank has been published. Holding both write-channel ready signals low + -- keeps the request stable without adding a second command/status CDC. + -- Reads remain available while the capture is active. + snapshotTxn := r.debugBusy; + if (r.debugBusy = '1') then + ep.axiStatus.writeEnable := '0'; + if (delayReady = '0') then + v.debugBusy := '0'; + v.debugIndex := 0; + axiSlaveWriteResponse(ep.axiWriteSlave, AXI_RESP_SLVERR_C); + elsif (sampleValid = '1' and r.debugIndex = 3) then + axiSlaveWriteResponse(ep.axiWriteSlave); + end if; + elsif (ep.axiStatus.writeEnable = '1' and + ep.axiWriteMaster.awaddr(11 downto 0) = ADC_DDR_SNAPSHOT_ADDR_C and + ep.axiWriteMaster.wstrb(0) = '1' and + ep.axiWriteMaster.wdata(0) = '1') then + snapshotTxn := '1'; + ep.axiStatus.writeEnable := '0'; + if (r.phyReset = '1' or r.startupPending = '1' or delayReady = '0') then + axiSlaveWriteResponse(ep.axiWriteSlave, AXI_RESP_SLVERR_C); + else + v.debugBusy := '1'; + v.debugIndex := 0; + end if; + end if; + + axiSlaveRegisterR(ep, ADC_DDR_VERSION_ADDR_C, 0, ADC_DDR_VERSION_C); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 0, toSlv(DATA_LANES_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 8, toSlv(FCO_LANES_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 16, toSlv(CHANNELS_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES0_ADDR_C, 24, toSlv(SAMPLE_WIDTH_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, 0, toSlv(DELAY_BITS_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, 8, toSlv(SERIALIZATION_FACTOR_G, 8)); + axiSlaveRegisterR(ep, ADC_DDR_CAPABILITIES1_ADDR_C, + ADC_DDR_CAP_PATTERN_CHECK_BIT_C, toSl(PATTERN_CHECK_G)); + axiSlaveRegister(ep, ADC_DDR_CAPTURE_RESET_ADDR_C, 0, v.phyReset); + axiSlaveRegister(ep, ADC_DDR_RELOCK_ADDR_C, 0, v.relock); + axiSlaveRegister(ep, ADC_DDR_CLEAR_COUNTERS_ADDR_C, 0, v.clearCounters); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_DELAY_READY_BIT_C, delayReady); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_ALL_LOCKED_BIT_C, uAnd(r.locked)); + axiSlaveRegisterR(ep, ADC_DDR_STATUS_ADDR_C, + ADC_DDR_STATUS_ANY_OVERFLOW_BIT_C, r.overflow); + axiSlaveRegisterR(ep, ADC_DDR_LOCKED_MASK_ADDR_C, 0, r.locked); + axiSlaveRegisterR(ep, ADC_DDR_SNAPSHOT_SEQUENCE_ADDR_C, 0, r.snapshotSeq); + + for i in DATA_LANES_G-1 downto 0 loop + axiSlaveRegister(ep, ADC_DDR_DATA_DELAY_ADDR_C+(4*i), 0, + v.dataDelay(i)(DELAY_BITS_G-1 downto 0)); + if (ep.axiStatus.writeEnable = '1' and + xbarMasterWriteMasters(0).awaddr(11 downto 0) = + ADC_DDR_DATA_DELAY_ADDR_C+(4*i)) then + v.dataDelayLoad(i) := '1'; + end if; + end loop; + for i in FCO_LANES_G-1 downto 0 loop + axiSlaveRegister(ep, ADC_DDR_FCO_DELAY_ADDR_C+(4*i), 0, + v.fcoDelay(i)(DELAY_BITS_G-1 downto 0)); + if (ep.axiStatus.writeEnable = '1' and + xbarMasterWriteMasters(0).awaddr(11 downto 0) = + ADC_DDR_FCO_DELAY_ADDR_C+(4*i)) then + v.fcoDelayLoad(i) := '1'; + end if; + axiSlaveRegisterR(ep, ADC_DDR_FCO_WORD_ADDR_C+(4*i), 0, fcoWord(i)); + axiSlaveRegisterR(ep, ADC_DDR_LOST_LOCK_COUNT_ADDR_C+(4*i), 0, r.lostLockCount(i)); + end loop; + axiSlaveRegisterR(ep, ADC_DDR_OVERFLOW_COUNT_ADDR_C, 0, r.overflowCount); + for i in CHANNELS_G-1 downto 0 loop + for j in 3 downto 0 loop + axiSlaveRegisterR(ep, ADC_DDR_DEBUG_ADDR_C+(16*i)+(4*j), 0, + r.debugSample((j*CHANNELS_G)+i)); + end loop; + end loop; + axiSlaveDefault(ep, v.axilWriteSlave, v.axilReadSlave, + AXI_RESP_DECERR_C, snapshotTxn); + + ---------------------------------------------------------------------------------------------- + -- Deserializer reset and alignment restart + -- + -- Every alignment attempt -- power-up, a lost delay controller, a manual + -- CaptureReset, or a Relock command -- runs the same sequence: hold the + -- PHY (deserializer) reset for a fixed number of captureClk/CLKDIV cycles + -- so a group's FCO and data deserializers all leave reset on the same + -- edge, then reload every retained delay and let alignment restart. A bare + -- Relock that only cleared the lock bits would re-run the bitslip search + -- from whatever independent phase each deserializer happened to hold, + -- aligning the FCO onto data lanes at an arbitrary relative offset. + -- + -- The FCO FSM above already holds its counters cleared and issues no + -- BITSLIP while startupPending is set, and delayReady/startupPending force + -- sampleValid low, so FIFO writes and snapshots stay suppressed until the + -- reset is released. Alignment therefore starts naturally on release from + -- cleared counters; no separate relock strobe is asserted here (that would + -- re-arm this sequence and loop). + ---------------------------------------------------------------------------------------------- + if (delayReady = '0' or v.phyReset = '1' or r.relock = '1') then + -- (Re)arm the sequence and keep the reset counter charged while any + -- trigger persists. + v.startupPending := '1'; + v.resetHold := ADC_DDR_RESET_HOLD_C; + elsif (r.startupPending = '1') then + if (r.resetHold /= 0) then + v.resetHold := r.resetHold - 1; + else + -- Reset held long enough with delays ready: release the PHY and + -- reload every retained delay on the same edge. + v.startupPending := '0'; + v.dataDelayLoad := (others => '1'); + v.fcoDelayLoad := (others => '1'); + end if; + end if; + + ---------------------------------------------------------------------------------------------- + -- Capture-domain reset and registered outputs + -- + -- phyReset is a software-controlled manual hold and must not reset this + -- endpoint; captureRst is the only reset for the core state and AXI + -- slave records. startupPending provides the hardware-owned startup hold. + ---------------------------------------------------------------------------------------------- + if (captureRst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + xbarMasterReadSlaves(0) <= r.axilReadSlave; + xbarMasterWriteSlaves(0) <= r.axilWriteSlave; + phyReset <= r.phyReset or r.startupPending; + bitSlip <= r.bitSlip; + for i in DATA_LANES_G-1 downto 0 loop + dataDelayWrite(i).value <= r.dataDelay(i); + dataDelayWrite(i).load <= r.dataDelayLoad(i); + end loop; + for i in FCO_LANES_G-1 downto 0 loop + fcoDelayWrite(i).value <= r.fcoDelay(i); + fcoDelayWrite(i).load <= r.fcoDelayLoad(i); + end loop; + end process comb; + + seq : process (captureClk) is + begin + if rising_edge(captureClk) then + r <= rin after TPD_G; + end if; + end process seq; + + GEN_PATTERN_CHECK : if (PATTERN_CHECK_G) generate + U_PatternTester : entity surf.AdcDdrPatternTester + generic map ( + TPD_G => TPD_G, + CHANNELS_G => CHANNELS_G, + FCO_LANES_G => FCO_LANES_G, + SAMPLE_WIDTH_G => SAMPLE_WIDTH_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + FRAME_PATTERN_G => FRAME_PATTERN_G) + port map ( + clk => captureClk, -- [in] + rst => captureRst, -- [in] + axilReadMaster => xbarMasterReadMasters(1), -- [in] + axilReadSlave => xbarMasterReadSlaves(1), -- [out] + axilWriteMaster => xbarMasterWriteMasters(1), -- [in] + axilWriteSlave => xbarMasterWriteSlaves(1), -- [out] + sampleValid => sampleValid, -- [in] + sampleIn => sampleIn, -- [in] + fcoValid => fcoValid, -- [in] + fcoWord => fcoWord); -- [in] + end generate GEN_PATTERN_CHECK; + + GEN_NO_PATTERN_CHECK : if (not PATTERN_CHECK_G) generate + xbarMasterReadSlaves(1) <= AXI_LITE_READ_SLAVE_EMPTY_DECERR_C; + xbarMasterWriteSlaves(1) <= AXI_LITE_WRITE_SLAVE_EMPTY_DECERR_C; + end generate GEN_NO_PATTERN_CHECK; + + ------------------------------------------------------------------------------------------------- + -- Coherent sample clock crossing + -- + -- Pack all channels plus one common alignment-error bit into a single FIFO + -- word. This preserves channel-to-channel sample association across unrelated + -- capture and stream clocks. + ------------------------------------------------------------------------------------------------- + fifoIn(FIFO_WIDTH_C-1) <= not uAnd(r.locked); + GEN_FIFO_IN : for i in CHANNELS_G-1 downto 0 generate + fifoIn((16*i)+15 downto 16*i) <= r.formatted(i); + end generate GEN_FIFO_IN; + + U_DataFifo : entity surf.FifoAsync + generic map ( + TPD_G => TPD_G, + MEMORY_TYPE_G => "distributed", + FWFT_EN_G => true, + DATA_WIDTH_G => FIFO_WIDTH_C, + ADDR_WIDTH_G => FIFO_ADDR_WIDTH_G) + port map ( + rst => captureRst or streamRst, -- [in] + wr_clk => captureClk, -- [in] + wr_en => r.sampleValid and delayReady and not r.phyReset and not r.startupPending, -- [in] + din => fifoIn, -- [in] + wr_data_count => open, -- [out] + wr_ack => open, -- [out] + overflow => fifoOverflow, -- [out] + prog_full => open, -- [out] + almost_full => open, -- [out] + full => open, -- [out] + not_full => open, -- [out] + rd_clk => streamClk, -- [in] + rd_en => fifoValid, -- [in] + dout => fifoOut, -- [out] + rd_data_count => open, -- [out] + valid => fifoValid, -- [out] + underflow => open, -- [out] + prog_empty => open, -- [out] + almost_empty => open, -- [out] + empty => open); -- [out] + + ------------------------------------------------------------------------------------------------- + -- Reconstruct one always-consumed AXI Stream output per logical channel. + -- tUser(0) reports that the capture group was not aligned without disturbing + -- sample cadence or imposing SSI framing on the stream. + ------------------------------------------------------------------------------------------------- + GEN_STREAM : for i in CHANNELS_G-1 downto 0 generate + format : process (fifoOut, fifoValid) is + variable v : AxiStreamMasterType; + begin + v := axiStreamMasterInit(AXIS_CONFIG_C); + v.tValid := fifoValid; + v.tData(15 downto 0) := fifoOut((16*i)+15 downto 16*i); + v.tDest := toSlv(i, 8); + v.tUser(0) := fifoOut(FIFO_WIDTH_C-1); + streams(i) <= v; + end process format; + end generate GEN_STREAM; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd new file mode 100644 index 0000000000..b64baef3d5 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd @@ -0,0 +1,435 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Parallel finite-window pattern checker for serialized ADC data +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrPatternTester is + generic ( + TPD_G : time := 1 ns; + CHANNELS_G : positive := 8; + FCO_LANES_G : positive := 1; + SAMPLE_WIDTH_G : positive range 2 to 16 := 14; + SERIALIZATION_FACTOR_G : positive := 14; + FRAME_PATTERN_G : slv := "11111110000000"); + port ( + clk : in sl; + rst : in sl; + axilReadMaster : in AxiLiteReadMasterType; + axilReadSlave : out AxiLiteReadSlaveType; + axilWriteMaster : in AxiLiteWriteMasterType; + axilWriteSlave : out AxiLiteWriteSlaveType; + sampleValid : in sl; + sampleIn : in Slv16Array(CHANNELS_G-1 downto 0); + fcoValid : in slv(FCO_LANES_G-1 downto 0); + fcoWord : in Slv16Array(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPatternTester; + +architecture rtl of AdcDdrPatternTester is + + constant PN23_MIN_SAMPLES_C : positive := (23/SAMPLE_WIDTH_G)+1; + + type RegType is record + start : sl; + abort : sl; + cfgAlternating : sl; + cfgPn23 : sl; + cfgReference : slv(7 downto 0); + cfgChannelMask : slv(CHANNELS_G-1 downto 0); + cfgFcoMask : slv(FCO_LANES_G-1 downto 0); + cfgDataMask : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgPatternA : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgPatternB : slv(SAMPLE_WIDTH_G-1 downto 0); + cfgSamples : slv(31 downto 0); + cfgTimeout : slv(31 downto 0); + busy : sl; + done : sl; + timedOut : sl; + configError : sl; + aborted : sl; + phaseAcquired : sl; + expectedPhase : sl; + allChannelsPass : sl; + allFcoPass : sl; + alternating : sl; + pn23 : sl; + referenceChannel : slv(7 downto 0); + channelMask : slv(CHANNELS_G-1 downto 0); + fcoMask : slv(FCO_LANES_G-1 downto 0); + dataMask : slv(SAMPLE_WIDTH_G-1 downto 0); + patternA : slv(SAMPLE_WIDTH_G-1 downto 0); + patternB : slv(SAMPLE_WIDTH_G-1 downto 0); + requestedSamples : slv(31 downto 0); + noValidTimeout : slv(31 downto 0); + noValidCount : slv(31 downto 0); + referenceWord : slv(SAMPLE_WIDTH_G-1 downto 0); + expectedWord : slv(SAMPLE_WIDTH_G-1 downto 0); + sampleErrorBits : Slv16Array(CHANNELS_G-1 downto 0); + pnHistory : slv(22 downto 0); + pnHistoryCount : natural range 0 to 23; + completionSeq : slv(31 downto 0); + checkedSamples : slv(31 downto 0); + channelPassed : slv(CHANNELS_G-1 downto 0); + fcoPassed : slv(FCO_LANES_G-1 downto 0); + fcoSeen : slv(FCO_LANES_G-1 downto 0); + wordErrorCount : Slv32Array(CHANNELS_G-1 downto 0); + bitErrorMask : Slv16Array(CHANNELS_G-1 downto 0); + fcoErrorCount : Slv32Array(FCO_LANES_G-1 downto 0); + axilReadSlave : AxiLiteReadSlaveType; + axilWriteSlave : AxiLiteWriteSlaveType; + end record RegType; + + constant REG_INIT_C : RegType := ( + start => '0', + abort => '0', + cfgAlternating => '0', + cfgPn23 => '0', + cfgReference => (others => '0'), + cfgChannelMask => (others => '1'), + cfgFcoMask => (others => '1'), + cfgDataMask => (others => '1'), + cfgPatternA => (others => '0'), + cfgPatternB => (others => '1'), + cfgSamples => x"00000100", + cfgTimeout => x"00000100", + busy => '0', + done => '0', + timedOut => '0', + configError => '0', + aborted => '0', + phaseAcquired => '0', + expectedPhase => '0', + allChannelsPass => '0', + allFcoPass => '0', + alternating => '0', + pn23 => '0', + referenceChannel => (others => '0'), + channelMask => (others => '0'), + fcoMask => (others => '0'), + dataMask => (others => '0'), + patternA => (others => '0'), + patternB => (others => '0'), + requestedSamples => (others => '0'), + noValidTimeout => (others => '0'), + noValidCount => (others => '0'), + referenceWord => (others => '0'), + expectedWord => (others => '0'), + sampleErrorBits => (others => (others => '0')), + pnHistory => (others => '0'), + pnHistoryCount => 0, + completionSeq => (others => '0'), + checkedSamples => (others => '0'), + channelPassed => (others => '0'), + fcoPassed => (others => '0'), + fcoSeen => (others => '0'), + wordErrorCount => (others => (others => '0')), + bitErrorMask => (others => (others => '0')), + fcoErrorCount => (others => (others => '0')), + axilReadSlave => AXI_LITE_READ_SLAVE_INIT_C, + axilWriteSlave => AXI_LITE_WRITE_SLAVE_INIT_C); + + signal r : RegType := REG_INIT_C; + signal rin : RegType; + +begin + + assert FRAME_PATTERN_G'length = SERIALIZATION_FACTOR_G + report "FRAME_PATTERN_G length must equal SERIALIZATION_FACTOR_G" + severity failure; + + comb : process (axilReadMaster, axilWriteMaster, fcoValid, fcoWord, r, rst, + sampleIn, sampleValid) is + variable v : RegType; + variable ep : AxiLiteEndpointType; + begin + v := r; + v.start := '0'; + v.abort := '0'; + v.done := '0'; + + if (r.busy = '1') then + for i in FCO_LANES_G-1 downto 0 loop + if (r.fcoMask(i) = '1' and fcoValid(i) = '1') then + v.fcoSeen(i) := '1'; + if (fcoWord(i)(SERIALIZATION_FACTOR_G-1 downto 0) /= FRAME_PATTERN_G) then + if (r.fcoErrorCount(i) /= x"FFFFFFFF") then + v.fcoErrorCount(i) := std_logic_vector(unsigned(r.fcoErrorCount(i)) + 1); + end if; + end if; + end if; + end loop; + + if (r.abort = '1') then + v.busy := '0'; + v.done := '1'; + v.aborted := '1'; + elsif (sampleValid = '1') then + v.noValidCount := (others => '0'); + v.referenceWord := sampleIn(to_integer(unsigned(r.referenceChannel))) + (SAMPLE_WIDTH_G-1 downto 0) and r.dataMask; + v.expectedWord := r.patternA and r.dataMask; + v.sampleErrorBits := (others => (others => '0')); + + if (r.pn23 = '1') then + v.referenceWord := v.referenceWord xor r.patternA; + for bitindex in SAMPLE_WIDTH_G-1 downto 0 loop + if (v.pnHistoryCount < 23) then + v.pnHistory := v.pnHistory(21 downto 0) & v.referenceWord(bitindex); + v.pnHistoryCount := v.pnHistoryCount + 1; + if (v.pnHistoryCount = 23) then + if uOr(v.pnHistory) = '1' then + v.phaseAcquired := '1'; + else + -- The all-zero state satisfies the recurrence but is + -- not part of the maximal-length PN23 sequence. + v.sampleErrorBits(to_integer(unsigned(r.referenceChannel))) + (SAMPLE_WIDTH_G-1 downto 0) := (others => '1'); + end if; + end if; + else + if (v.referenceWord(bitindex) /= + (v.pnHistory(22) xor v.pnHistory(17))) then + v.sampleErrorBits(to_integer(unsigned(r.referenceChannel)))(bitindex) := '1'; + end if; + v.pnHistory := v.pnHistory(21 downto 0) & v.referenceWord(bitindex); + end if; + end loop; + + elsif (r.alternating = '1') then + if (r.phaseAcquired = '1') then + if (r.expectedPhase = '0') then + v.expectedWord := r.patternA and r.dataMask; + else + v.expectedWord := r.patternB and r.dataMask; + end if; + v.expectedPhase := not r.expectedPhase; + else + if (v.referenceWord = (r.patternA and r.dataMask)) then + v.expectedWord := r.patternA and r.dataMask; + v.phaseAcquired := '1'; + v.expectedPhase := '1'; + elsif (v.referenceWord = (r.patternB and r.dataMask)) then + v.expectedWord := r.patternB and r.dataMask; + v.phaseAcquired := '1'; + v.expectedPhase := '0'; + end if; + end if; + end if; + + for i in CHANNELS_G-1 downto 0 loop + if (r.channelMask(i) = '1') then + if (r.pn23 = '1') then + if (i /= to_integer(unsigned(r.referenceChannel))) then + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := + (((sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) xor r.patternA) and + r.dataMask) xor v.referenceWord); + end if; + elsif (v.phaseAcquired = '0') then + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := r.dataMask; + else + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0) := + (sampleIn(i)(SAMPLE_WIDTH_G-1 downto 0) and r.dataMask) xor + v.expectedWord; + end if; + if (uOr(v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0)) = '1') then + if (r.wordErrorCount(i) /= x"FFFFFFFF") then + v.wordErrorCount(i) := std_logic_vector(unsigned(r.wordErrorCount(i)) + 1); + end if; + v.channelPassed(i) := '0'; + v.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) := + r.bitErrorMask(i)(SAMPLE_WIDTH_G-1 downto 0) or + v.sampleErrorBits(i)(SAMPLE_WIDTH_G-1 downto 0); + end if; + end if; + end loop; + + v.checkedSamples := std_logic_vector(unsigned(r.checkedSamples) + 1); + if (unsigned(r.checkedSamples) + 1 >= unsigned(r.requestedSamples)) then + v.busy := '0'; + v.done := '1'; + -- channelPassed is maintained per-sample above, so completion is + -- just an AND-reduce of the clean flags over the masked channels. + v.allChannelsPass := '1'; + for i in CHANNELS_G-1 downto 0 loop + if (r.channelMask(i) = '1' and v.channelPassed(i) = '0') then + v.allChannelsPass := '0'; + end if; + end loop; + v.allFcoPass := '1'; + for i in FCO_LANES_G-1 downto 0 loop + if (r.fcoMask(i) = '1') then + if (v.fcoSeen(i) = '1' and v.fcoErrorCount(i) = x"00000000") then + v.fcoPassed(i) := '1'; + else + v.fcoPassed(i) := '0'; + v.allFcoPass := '0'; + end if; + end if; + end loop; + if (r.pn23 = '1' and v.phaseAcquired = '0') then + v.allChannelsPass := '0'; + end if; + end if; + elsif (r.noValidTimeout /= x"00000000") then + if (unsigned(r.noValidCount) + 1 >= unsigned(r.noValidTimeout)) then + v.busy := '0'; + v.done := '1'; + v.timedOut := '1'; + else + v.noValidCount := std_logic_vector(unsigned(r.noValidCount) + 1); + end if; + end if; + + elsif (r.start = '1') then + v.busy := '0'; + v.timedOut := '0'; + v.configError := '0'; + v.aborted := '0'; + v.phaseAcquired := '0'; + v.expectedPhase := '0'; + v.allChannelsPass := '0'; + v.allFcoPass := '0'; + v.alternating := r.cfgAlternating; + v.pn23 := r.cfgPn23; + v.referenceChannel := r.cfgReference; + v.channelMask := r.cfgChannelMask; + v.fcoMask := r.cfgFcoMask; + v.dataMask := r.cfgDataMask; + v.patternA := r.cfgPatternA; + v.patternB := r.cfgPatternB; + v.requestedSamples := r.cfgSamples; + v.noValidTimeout := r.cfgTimeout; + v.noValidCount := (others => '0'); + v.referenceWord := (others => '0'); + v.expectedWord := (others => '0'); + v.sampleErrorBits := (others => (others => '0')); + v.pnHistory := (others => '0'); + v.pnHistoryCount := 0; + v.checkedSamples := (others => '0'); + -- channelPassed is a running "clean" flag: masked-in channels start + -- passing and clear on the first word error, so the completion decision + -- is a cheap reduce instead of a wide compare of the 32-bit counters. + v.channelPassed := r.cfgChannelMask; + v.fcoPassed := (others => '0'); + v.fcoSeen := (others => '0'); + v.wordErrorCount := (others => (others => '0')); + v.bitErrorMask := (others => (others => '0')); + v.fcoErrorCount := (others => (others => '0')); + if (r.cfgSamples = x"00000000" or uOr(r.cfgChannelMask) = '0' or + uOr(r.cfgDataMask) = '0') then + v.configError := '1'; + end if; + if (r.cfgAlternating = '1' and r.cfgPn23 = '1') then + v.configError := '1'; + end if; + if (r.cfgAlternating = '1' or r.cfgPn23 = '1') then + if (to_integer(unsigned(r.cfgReference)) >= CHANNELS_G) then + v.configError := '1'; + elsif (r.cfgChannelMask(to_integer(unsigned(r.cfgReference))) = '0') then + v.configError := '1'; + end if; + if (r.cfgAlternating = '1' and + (r.cfgPatternA and r.cfgDataMask) = (r.cfgPatternB and r.cfgDataMask)) then + v.configError := '1'; + end if; + end if; + if (r.cfgPn23 = '1') then + if uAnd(r.cfgDataMask) = '0' or + unsigned(r.cfgSamples) < to_unsigned(PN23_MIN_SAMPLES_C, 32) then + v.configError := '1'; + end if; + end if; + if (v.configError = '1') then + v.done := '1'; + else + v.busy := '1'; + v.phaseAcquired := not r.cfgAlternating and not r.cfgPn23; + end if; + end if; + + if (v.done = '1') then + v.completionSeq := std_logic_vector(unsigned(r.completionSeq) + 1); + end if; + + axiSlaveWaitTxn(ep, axilWriteMaster, axilReadMaster, + v.axilWriteSlave, v.axilReadSlave); + axiSlaveRegister(ep, ADC_DDR_PATTERN_START_ADDR_C, 0, v.start); + axiSlaveRegister(ep, ADC_DDR_PATTERN_ABORT_ADDR_C, 0, v.abort); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_ALTERNATING_BIT_C, v.cfgAlternating); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_PN23_BIT_C, v.cfgPn23); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CONFIG_ADDR_C, + ADC_DDR_PATTERN_REFERENCE_OFFSET_C, v.cfgReference); + axiSlaveRegister(ep, ADC_DDR_PATTERN_CHANNEL_MASK_ADDR_C, 0, v.cfgChannelMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_FCO_MASK_ADDR_C, 0, v.cfgFcoMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_DATA_MASK_ADDR_C, 0, v.cfgDataMask); + axiSlaveRegister(ep, ADC_DDR_PATTERN_A_ADDR_C, 0, v.cfgPatternA); + axiSlaveRegister(ep, ADC_DDR_PATTERN_B_ADDR_C, 0, v.cfgPatternB); + axiSlaveRegister(ep, ADC_DDR_PATTERN_SAMPLES_ADDR_C, 0, v.cfgSamples); + axiSlaveRegister(ep, ADC_DDR_PATTERN_TIMEOUT_ADDR_C, 0, v.cfgTimeout); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_BUSY_BIT_C, r.busy); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_TIMEOUT_BIT_C, r.timedOut); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_CONFIG_ERROR_BIT_C, r.configError); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_ABORTED_BIT_C, r.aborted); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_PHASE_ACQUIRED_BIT_C, r.phaseAcquired); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_CHANNEL_PASS_BIT_C, r.allChannelsPass); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_STATUS_ADDR_C, + ADC_DDR_PATTERN_FCO_PASS_BIT_C, r.allFcoPass); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_SEQUENCE_ADDR_C, 0, r.completionSeq); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_CHECKED_ADDR_C, 0, r.checkedSamples); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_CHANNEL_PASS_ADDR_C, 0, r.channelPassed); + axiSlaveRegisterR(ep, ADC_DDR_PATTERN_FCO_PASS_ADDR_C, 0, r.fcoPassed); + for i in CHANNELS_G-1 downto 0 loop + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_WORD_ERROR_ADDR_C)+(4*i)), + 0, r.wordErrorCount(i)); + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_BIT_ERROR_ADDR_C)+(4*i)), + 0, r.bitErrorMask(i)); + end loop; + for i in FCO_LANES_G-1 downto 0 loop + axiSlaveRegisterR(ep, std_logic_vector(unsigned(ADC_DDR_PATTERN_FCO_ERROR_ADDR_C)+(4*i)), + 0, r.fcoErrorCount(i)); + end loop; + axiSlaveDefault(ep, v.axilWriteSlave, v.axilReadSlave, AXI_RESP_DECERR_C); + + if (rst = '1') then + v := REG_INIT_C; + end if; + + rin <= v; + + axilReadSlave <= r.axilReadSlave; + axilWriteSlave <= r.axilWriteSlave; + end process comb; + + seq : process (clk) is + begin + if rising_edge(clk) then + r <= rin after TPD_G; + end if; + end process seq; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd new file mode 100644 index 0000000000..63cf9974ec --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd @@ -0,0 +1,191 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Device-family selector for the serialized DDR ADC PHY +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrPhy is + generic ( + TPD_G : time := 1 ns; + DEVICE_FAMILY_G : string := "ULTRASCALE"; + DATA_LANES_G : positive := 8; + FCO_LANES_G : positive := 1; + SERIALIZATION_FACTOR_G : positive := 14; + IODELAY_GROUP_G : string := "DEFAULT_GROUP"; + IDELAYCTRL_FREQ_G : real := 200.0; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0) := (others => 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl := '0'; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); +end entity AdcDdrPhy; + +architecture rtl of AdcDdrPhy is + + component AdcDdrPhy7Series is + generic ( + TPD_G : time; + DATA_LANES_G : positive; + FCO_LANES_G : positive; + SERIALIZATION_FACTOR_G : positive; + IODELAY_GROUP_G : string; + IDELAYCTRL_FREQ_G : real; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0)); + port ( + adcClkRst : in sl; + idelayCtrlRdy : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); + end component AdcDdrPhy7Series; + + component AdcDdrPhyUltraScale is + generic ( + TPD_G : time; + DEVICE_FAMILY_G : string; + DATA_LANES_G : positive; + FCO_LANES_G : positive; + SERIALIZATION_FACTOR_G : positive; + IODELAY_GROUP_G : string; + DATA_FCO_MAP_G : NaturalArray(DATA_LANES_G-1 downto 0)); + port ( + adcClkRst : in sl; + phyReset : in sl; + dClkP : in sl; + dClkN : in sl; + fcoP : in slv(FCO_LANES_G-1 downto 0); + fcoN : in slv(FCO_LANES_G-1 downto 0); + dataP : in slv(DATA_LANES_G-1 downto 0); + dataN : in slv(DATA_LANES_G-1 downto 0); + bitSlip : in slv(FCO_LANES_G-1 downto 0); + dataDelayWrite : in AdcDdrDelayArray(DATA_LANES_G-1 downto 0); + fcoDelayWrite : in AdcDdrDelayArray(FCO_LANES_G-1 downto 0); + captureClk : out sl; + captureRst : out sl; + delayReady : out sl; + dataWord : out Slv16Array(DATA_LANES_G-1 downto 0); + dataValid : out slv(DATA_LANES_G-1 downto 0); + fcoWord : out Slv16Array(FCO_LANES_G-1 downto 0); + fcoValid : out slv(FCO_LANES_G-1 downto 0)); + end component AdcDdrPhyUltraScale; + + constant ULTRASCALE_C : boolean := + DEVICE_FAMILY_G = "ULTRASCALE" or DEVICE_FAMILY_G = "ULTRASCALE_PLUS"; + +begin + + assert DEVICE_FAMILY_G = "7SERIES" or ULTRASCALE_C + report "AdcDdrPhy DEVICE_FAMILY_G must be 7SERIES, ULTRASCALE, or ULTRASCALE_PLUS" + severity failure; + + GEN_7SERIES : if DEVICE_FAMILY_G = "7SERIES" generate + U_Phy : AdcDdrPhy7Series + generic map ( + TPD_G => TPD_G, + DATA_LANES_G => DATA_LANES_G, + FCO_LANES_G => FCO_LANES_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + IDELAYCTRL_FREQ_G => IDELAYCTRL_FREQ_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_G) + port map ( + adcClkRst => adcClkRst, -- [in] + idelayCtrlRdy => idelayCtrlRdy, -- [in] + phyReset => phyReset, -- [in] + dClkP => dClkP, -- [in] + dClkN => dClkN, -- [in] + fcoP => fcoP, -- [in] + fcoN => fcoN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelayWrite, -- [in] + fcoDelayWrite => fcoDelayWrite, -- [in] + captureClk => captureClk, -- [out] + captureRst => captureRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + end generate GEN_7SERIES; + + GEN_ULTRASCALE : if ULTRASCALE_C generate + U_Phy : AdcDdrPhyUltraScale + generic map ( + TPD_G => TPD_G, + DEVICE_FAMILY_G => DEVICE_FAMILY_G, + DATA_LANES_G => DATA_LANES_G, + FCO_LANES_G => FCO_LANES_G, + SERIALIZATION_FACTOR_G => SERIALIZATION_FACTOR_G, + IODELAY_GROUP_G => IODELAY_GROUP_G, + DATA_FCO_MAP_G => DATA_FCO_MAP_G) + port map ( + adcClkRst => adcClkRst, -- [in] + phyReset => phyReset, -- [in] + dClkP => dClkP, -- [in] + dClkN => dClkN, -- [in] + fcoP => fcoP, -- [in] + fcoN => fcoN, -- [in] + dataP => dataP, -- [in] + dataN => dataN, -- [in] + bitSlip => bitSlip, -- [in] + dataDelayWrite => dataDelayWrite, -- [in] + fcoDelayWrite => fcoDelayWrite, -- [in] + captureClk => captureClk, -- [out] + captureRst => captureRst, -- [out] + delayReady => delayReady, -- [out] + dataWord => dataWord, -- [out] + dataValid => dataValid, -- [out] + fcoWord => fcoWord, -- [out] + fcoValid => fcoValid); -- [out] + end generate GEN_ULTRASCALE; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd new file mode 100644 index 0000000000..1f26b9a3fd --- /dev/null +++ b/devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd @@ -0,0 +1,128 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Common constants and types for serialized DDR ADC receivers +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package AdcDdrPkg is + + -- Register-map version and fixed alignment policy. Alignment thresholds are + -- shared because all supported devices expose a deterministic repeating FCO word. + constant ADC_DDR_VERSION_C : slv(31 downto 0) := x"00010000"; + constant ADC_DDR_LOCK_MATCHES_C : positive := 4; + constant ADC_DDR_UNLOCK_ERRORS_C : positive := 2; + -- ISERDESE2 DDR operation requires three quiet CLKDIV cycles after a + -- one-cycle BITSLIP request before the shifted word is evaluated. + constant ADC_DDR_BITSLIP_INTERVAL_C : positive := 8; + -- captureClk (CLKDIV) cycles the deserializer reset is held on every relock + -- so that a group's FCO and data deserializers all leave reset on the same + -- edge. Comfortably exceeds the minimum ISERDES/IDELAY reset pulse width. + constant ADC_DDR_RESET_HOLD_C : positive := 8; + + -- Return the native programmable input-delay width for an FPGA family. + function adcDdrDelayBits (deviceFamily : string) return positive; + + -- Base offsets for the normalized register windows. Per-lane and per-channel + -- entries use four-byte strides from these bases. + constant ADC_DDR_VERSION_ADDR_C : slv(11 downto 0) := X"000"; + constant ADC_DDR_CAPABILITIES0_ADDR_C : slv(11 downto 0) := X"004"; + constant ADC_DDR_CAPABILITIES1_ADDR_C : slv(11 downto 0) := X"008"; + constant ADC_DDR_CAPTURE_RESET_ADDR_C : slv(11 downto 0) := X"00C"; + constant ADC_DDR_RELOCK_ADDR_C : slv(11 downto 0) := X"010"; + constant ADC_DDR_SNAPSHOT_ADDR_C : slv(11 downto 0) := X"014"; + constant ADC_DDR_CLEAR_COUNTERS_ADDR_C : slv(11 downto 0) := X"018"; + constant ADC_DDR_STATUS_ADDR_C : slv(11 downto 0) := X"01C"; + constant ADC_DDR_LOCKED_MASK_ADDR_C : slv(11 downto 0) := X"020"; + constant ADC_DDR_SNAPSHOT_SEQUENCE_ADDR_C : slv(11 downto 0) := X"024"; + constant ADC_DDR_DATA_DELAY_ADDR_C : slv(11 downto 0) := X"100"; + constant ADC_DDR_FCO_DELAY_ADDR_C : slv(11 downto 0) := X"200"; + constant ADC_DDR_FCO_WORD_ADDR_C : slv(11 downto 0) := X"300"; + constant ADC_DDR_LOST_LOCK_COUNT_ADDR_C : slv(11 downto 0) := X"340"; + constant ADC_DDR_PATTERN_BASE_ADDR_C : slv(11 downto 0) := X"800"; + constant ADC_DDR_PATTERN_START_ADDR_C : slv(7 downto 0) := X"00"; + constant ADC_DDR_PATTERN_ABORT_ADDR_C : slv(7 downto 0) := X"04"; + constant ADC_DDR_PATTERN_CONFIG_ADDR_C : slv(7 downto 0) := X"08"; + constant ADC_DDR_PATTERN_CHANNEL_MASK_ADDR_C : slv(7 downto 0) := X"0C"; + constant ADC_DDR_PATTERN_FCO_MASK_ADDR_C : slv(7 downto 0) := X"10"; + constant ADC_DDR_PATTERN_DATA_MASK_ADDR_C : slv(7 downto 0) := X"14"; + constant ADC_DDR_PATTERN_A_ADDR_C : slv(7 downto 0) := X"18"; + constant ADC_DDR_PATTERN_B_ADDR_C : slv(7 downto 0) := X"1C"; + constant ADC_DDR_PATTERN_SAMPLES_ADDR_C : slv(7 downto 0) := X"20"; + constant ADC_DDR_PATTERN_TIMEOUT_ADDR_C : slv(7 downto 0) := X"24"; + constant ADC_DDR_PATTERN_STATUS_ADDR_C : slv(7 downto 0) := X"28"; + constant ADC_DDR_PATTERN_SEQUENCE_ADDR_C : slv(7 downto 0) := X"2C"; + constant ADC_DDR_PATTERN_CHECKED_ADDR_C : slv(7 downto 0) := X"30"; + constant ADC_DDR_PATTERN_CHANNEL_PASS_ADDR_C : slv(7 downto 0) := X"34"; + constant ADC_DDR_PATTERN_FCO_PASS_ADDR_C : slv(7 downto 0) := X"38"; + constant ADC_DDR_PATTERN_WORD_ERROR_ADDR_C : slv(7 downto 0) := X"40"; + constant ADC_DDR_PATTERN_BIT_ERROR_ADDR_C : slv(7 downto 0) := X"80"; + constant ADC_DDR_PATTERN_FCO_ERROR_ADDR_C : slv(7 downto 0) := X"C0"; + constant ADC_DDR_OVERFLOW_COUNT_ADDR_C : slv(11 downto 0) := X"500"; + constant ADC_DDR_DEBUG_ADDR_C : slv(11 downto 0) := X"600"; + + -- Capture capability feature-bit assignments. + constant ADC_DDR_CAP_PATTERN_CHECK_BIT_C : natural := 16; + + -- Status register bit assignments. + constant ADC_DDR_STATUS_DELAY_READY_BIT_C : natural := 1; + constant ADC_DDR_STATUS_ALL_LOCKED_BIT_C : natural := 2; + constant ADC_DDR_STATUS_ANY_OVERFLOW_BIT_C : natural := 3; + + -- Pattern measurement command and status bit assignments. + constant ADC_DDR_PATTERN_ALTERNATING_BIT_C : natural := 0; + constant ADC_DDR_PATTERN_PN23_BIT_C : natural := 1; + constant ADC_DDR_PATTERN_REFERENCE_OFFSET_C : natural := 8; + constant ADC_DDR_PATTERN_BUSY_BIT_C : natural := 0; + constant ADC_DDR_PATTERN_TIMEOUT_BIT_C : natural := 1; + constant ADC_DDR_PATTERN_CONFIG_ERROR_BIT_C : natural := 2; + constant ADC_DDR_PATTERN_ABORTED_BIT_C : natural := 3; + constant ADC_DDR_PATTERN_PHASE_ACQUIRED_BIT_C : natural := 4; + constant ADC_DDR_PATTERN_CHANNEL_PASS_BIT_C : natural := 5; + constant ADC_DDR_PATTERN_FCO_PASS_BIT_C : natural := 6; + + -- Runtime PHY delay command. The value width matches the widest supported + -- Xilinx input delay; load is a one-cycle capture-domain write strobe. + type AdcDdrDelayType is record + value : slv(8 downto 0); + load : sl; + end record AdcDdrDelayType; + + constant ADC_DDR_DELAY_INIT_C : AdcDdrDelayType := ( + value => (others => '0'), + load => '0'); + + type AdcDdrDelayArray is array (natural range <>) of AdcDdrDelayType; + +end package AdcDdrPkg; + +package body AdcDdrPkg is + + function adcDdrDelayBits (deviceFamily : string) return positive is + begin + if deviceFamily = "7SERIES" then + return 5; + elsif deviceFamily = "ULTRASCALE" or deviceFamily = "ULTRASCALE_PLUS" then + return 9; + else + assert false + report "Unsupported AdcDdr FPGA device family " & deviceFamily + severity failure; + return 5; + end if; + end function adcDdrDelayBits; + +end package body AdcDdrPkg; diff --git a/devices/AnalogDevices/adcDdr/ruckus.tcl b/devices/AnalogDevices/adcDdr/ruckus.tcl new file mode 100644 index 0000000000..7678ba1f07 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/ruckus.tcl @@ -0,0 +1,34 @@ +#----------------------------------------------------------------------------- +# 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. +#----------------------------------------------------------------------------- + +source $::env(RUCKUS_PROC_TCL) + +loadSource -lib surf -dir "$::DIR_PATH/rtl" -fileType "VHDL 2008" +loadSource -lib surf -sim_only -dir "$::DIR_PATH/sim" + +set family [getFpgaArch] + +if { ${family} eq {artix7} || + ${family} eq {kintex7} || + ${family} eq {virtex7} || + ${family} eq {zynq} } { + loadRuckusTcl "$::DIR_PATH/7Series" +} + +if { ${family} eq {kintexu} || + ${family} eq {virtexu} || + ${family} eq {kintexuplus} || + ${family} eq {zynquplus} || + ${family} eq {zynquplusRFSOC} || + ${family} eq {qzynquplusRFSOC} || + ${family} eq {virtexuplus} || + ${family} eq {virtexuplusHBM} } { + loadRuckusTcl "$::DIR_PATH/UltraScale" +} diff --git a/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd b/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd new file mode 100644 index 0000000000..eb2640b16f --- /dev/null +++ b/devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd @@ -0,0 +1,97 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Shared PN-sequence helpers for serialized DDR ADC models +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; + +package AdcDdrPatternPkg is + + function adcDdrPn9Next (state : slv(8 downto 0)) return slv; + function adcDdrPn23Next (state : slv(22 downto 0)) return slv; + function adcDdrPn9Advance (state : slv(8 downto 0); count : positive) return slv; + function adcDdrPn23Advance (state : slv(22 downto 0); count : positive) return slv; + function adcDdrPn9Word (state : slv(8 downto 0); width : positive) return slv; + function adcDdrPn23Word (state : slv(22 downto 0); width : positive) return slv; + +end package AdcDdrPatternPkg; + +package body AdcDdrPatternPkg is + + -- The PN helpers use an MSB-first Fibonacci representation throughout. A + -- caller can therefore generate the word visible on serialized ADC pins and + -- obtain the next state by advancing exactly the same number of bits. + function adcDdrPn9Next (state : slv(8 downto 0)) return slv is + variable result : slv(8 downto 0); + begin + -- Fibonacci realization of x^9 + x^5 + 1. The current MSB is the + -- serialized output bit and the register shifts toward the MSB. + result := state(7 downto 0) & (state(8) xor state(4)); + return result; + end function adcDdrPn9Next; + + function adcDdrPn23Next (state : slv(22 downto 0)) return slv is + variable result : slv(22 downto 0); + begin + -- Fibonacci realization of x^23 + x^18 + 1. + result := state(21 downto 0) & (state(22) xor state(17)); + return result; + end function adcDdrPn23Next; + + function adcDdrPn9Advance (state : slv(8 downto 0); count : positive) return slv is + variable result : slv(8 downto 0) := state; + begin + for i in 1 to count loop + result := adcDdrPn9Next(result); + end loop; + return result; + end function adcDdrPn9Advance; + + function adcDdrPn23Advance (state : slv(22 downto 0); count : positive) return slv is + variable result : slv(22 downto 0) := state; + begin + for i in 1 to count loop + result := adcDdrPn23Next(result); + end loop; + return result; + end function adcDdrPn23Advance; + + function adcDdrPn9Word (state : slv(8 downto 0); width : positive) return slv is + variable current : slv(8 downto 0) := state; + variable result : slv(width-1 downto 0); + begin + -- Fill from the result MSB downward so bit width-1 is the first bit that + -- would appear on an MSB-first serialized lane. + for i in width-1 downto 0 loop + result(i) := current(8); + current := adcDdrPn9Next(current); + end loop; + return result; + end function adcDdrPn9Word; + + function adcDdrPn23Word (state : slv(22 downto 0); width : positive) return slv is + variable current : slv(22 downto 0) := state; + variable result : slv(width-1 downto 0); + begin + -- Keep PN23 packing identical to PN9; only the polynomial/order differs. + for i in width-1 downto 0 loop + result(i) := current(22); + current := adcDdrPn23Next(current); + end loop; + return result; + end function adcDdrPn23Word; + +end package body AdcDdrPatternPkg; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd new file mode 100644 index 0000000000..0500db1ae5 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd @@ -0,0 +1,177 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.AdcDdrCore +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; +use surf.AxiStreamPkg.all; +use surf.AdcDdrPkg.all; + +entity AdcDdrCoreWrapper is + generic ( + TPD_G : time := 1 ns; + AXIL_BASE_ADDR_G : slv(31 downto 0) := x"00000000"; + PATTERN_CHECK_G : boolean := true; + NEGATE_G : boolean := false); + port ( + axilClk : in sl; + axilRst : in sl; + S_AXI_AWADDR : in slv(31 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(31 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl; + captureClk : in sl; + captureRst : in sl; + delayReady : in sl := '0'; + fcoWord : in slv(13 downto 0); + fcoValid : in sl; + sampleValid : in sl; + sampleIn : in slv(31 downto 0); + bitSlip : out sl; + dataDelayValue : out slv(31 downto 0); + dataDelayLoad : out slv(1 downto 0); + fcoDelayValue : out slv(15 downto 0); + fcoDelayLoad : out sl; + phyReset : out sl; + streamClk : in sl; + streamRst : in sl; + streamValid : out slv(1 downto 0); + streamData : out slv(31 downto 0); + streamKeep : out slv(3 downto 0); + streamDest : out slv(15 downto 0); + streamLast : out slv(1 downto 0); + streamUser : out slv(15 downto 0)); +end entity AdcDdrCoreWrapper; + +architecture rtl of AdcDdrCoreWrapper is + + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal sAxiAResetN : sl := '1'; + signal dataDelayWrite : AdcDdrDelayArray(1 downto 0); + signal fcoDelayWrite : AdcDdrDelayArray(0 downto 0); + signal sampleArray : Slv16Array(1 downto 0); + signal streamArray : AxiStreamMasterArray(1 downto 0); + signal bitSlipInt : slv(0 downto 0); + +begin + + sAxiAResetN <= not axilRst; + + U_ShimLayer : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + EN_ERROR_RESP => true, + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => 32) + port map ( + S_AXI_ACLK => axilClk, + S_AXI_ARESETN => sAxiAResetN, + S_AXI_AWADDR => S_AXI_AWADDR, + S_AXI_AWPROT => S_AXI_AWPROT, + S_AXI_AWVALID => S_AXI_AWVALID, + S_AXI_AWREADY => S_AXI_AWREADY, + S_AXI_WDATA => S_AXI_WDATA, + S_AXI_WSTRB => S_AXI_WSTRB, + S_AXI_WVALID => S_AXI_WVALID, + S_AXI_WREADY => S_AXI_WREADY, + S_AXI_BRESP => S_AXI_BRESP, + S_AXI_BVALID => S_AXI_BVALID, + S_AXI_BREADY => S_AXI_BREADY, + S_AXI_ARADDR => S_AXI_ARADDR, + S_AXI_ARPROT => S_AXI_ARPROT, + S_AXI_ARVALID => S_AXI_ARVALID, + S_AXI_ARREADY => S_AXI_ARREADY, + S_AXI_RDATA => S_AXI_RDATA, + S_AXI_RRESP => S_AXI_RRESP, + S_AXI_RVALID => S_AXI_RVALID, + S_AXI_RREADY => S_AXI_RREADY, + axilClk => open, + axilRst => open, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave); + + sampleArray(0) <= sampleIn(15 downto 0); + sampleArray(1) <= sampleIn(31 downto 16); + + U_DUT : entity surf.AdcDdrCore + generic map ( + TPD_G => TPD_G, + AXIL_BASE_ADDR_G => AXIL_BASE_ADDR_G, + DATA_LANES_G => 2, + FCO_LANES_G => 1, + CHANNELS_G => 2, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + PATTERN_CHECK_G => PATTERN_CHECK_G, + NEGATE_G => NEGATE_G) + port map ( + axilClk => axilClk, + axilRst => axilRst, + axilReadMaster => axilReadMaster, + axilReadSlave => axilReadSlave, + axilWriteMaster => axilWriteMaster, + axilWriteSlave => axilWriteSlave, + captureClk => captureClk, + captureRst => captureRst, + delayReady => delayReady, + fcoWord => (0 => "00" & fcoWord), + fcoValid => (0 => fcoValid), + sampleValid => sampleValid, + sampleIn => sampleArray, + phyReset => phyReset, + bitSlip => bitSlipInt, + dataDelayWrite => dataDelayWrite, + fcoDelayWrite => fcoDelayWrite, + streamClk => streamClk, + streamRst => streamRst, + streams => streamArray); + + bitSlip <= bitSlipInt(0); + GEN_FLATTEN : for i in 1 downto 0 generate + dataDelayValue((i*16)+15 downto i*16) <= resize(dataDelayWrite(i).value, 16); + dataDelayLoad(i) <= dataDelayWrite(i).load; + streamValid(i) <= streamArray(i).tValid; + streamData((i*16)+15 downto i*16) <= streamArray(i).tData(15 downto 0); + streamKeep((i*2)+1 downto i*2) <= streamArray(i).tKeep(1 downto 0); + streamDest((i*8)+7 downto i*8) <= streamArray(i).tDest; + streamLast(i) <= streamArray(i).tLast; + streamUser((i*8)+7 downto i*8) <= streamArray(i).tUser(7 downto 0); + end generate; + fcoDelayValue <= resize(fcoDelayWrite(0).value, 16); + fcoDelayLoad <= fcoDelayWrite(0).load; + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd new file mode 100644 index 0000000000..0e56905797 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd @@ -0,0 +1,43 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Simulation wrapper for surf.AdcDdrPatternPkg +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AdcDdrPatternPkg.all; + +entity AdcDdrPatternPkgTb is + generic ( + WORD_WIDTH_G : positive := 14); + port ( + pn9State : in slv(8 downto 0); + pn23State : in slv(22 downto 0); + pn9Next : out slv(8 downto 0); + pn23Next : out slv(22 downto 0); + pn9Word : out slv(WORD_WIDTH_G-1 downto 0); + pn23Word : out slv(WORD_WIDTH_G-1 downto 0)); +end entity AdcDdrPatternPkgTb; + +architecture rtl of AdcDdrPatternPkgTb is + +begin + + pn9Next <= adcDdrPn9Advance(pn9State, WORD_WIDTH_G); + pn23Next <= adcDdrPn23Advance(pn23State, WORD_WIDTH_G); + pn9Word <= adcDdrPn9Word(pn9State, WORD_WIDTH_G); + pn23Word <= adcDdrPn23Word(pn23State, WORD_WIDTH_G); + +end architecture rtl; diff --git a/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd new file mode 100644 index 0000000000..98dc7eda68 --- /dev/null +++ b/devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd @@ -0,0 +1,126 @@ +------------------------------------------------------------------------------- +-- Company : SLAC National Accelerator Laboratory +------------------------------------------------------------------------------- +-- Description: Flattened simulation wrapper for surf.AdcDdrPatternTester +------------------------------------------------------------------------------- +-- 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. +------------------------------------------------------------------------------- + +library ieee; +use ieee.std_logic_1164.all; + +library surf; +use surf.StdRtlPkg.all; +use surf.AxiLitePkg.all; + +entity AdcDdrPatternTesterWrapper is + generic ( + TPD_G : time := 1 ns); + port ( + clk : in sl; + rst : in sl; + S_AXI_AWADDR : in slv(11 downto 0); + S_AXI_AWPROT : in slv(2 downto 0); + S_AXI_AWVALID : in sl; + S_AXI_AWREADY : out sl; + S_AXI_WDATA : in slv(31 downto 0); + S_AXI_WSTRB : in slv(3 downto 0); + S_AXI_WVALID : in sl; + S_AXI_WREADY : out sl; + S_AXI_BRESP : out slv(1 downto 0); + S_AXI_BVALID : out sl; + S_AXI_BREADY : in sl; + S_AXI_ARADDR : in slv(11 downto 0); + S_AXI_ARPROT : in slv(2 downto 0); + S_AXI_ARVALID : in sl; + S_AXI_ARREADY : out sl; + S_AXI_RDATA : out slv(31 downto 0); + S_AXI_RRESP : out slv(1 downto 0); + S_AXI_RVALID : out sl; + S_AXI_RREADY : in sl; + sampleValid : in sl; + sampleIn : in slv(31 downto 0); + fcoValid : in sl; + fcoWord : in slv(13 downto 0)); +end entity AdcDdrPatternTesterWrapper; + +architecture rtl of AdcDdrPatternTesterWrapper is + + signal axilReadMaster : AxiLiteReadMasterType := AXI_LITE_READ_MASTER_INIT_C; + signal axilReadSlave : AxiLiteReadSlaveType := AXI_LITE_READ_SLAVE_INIT_C; + signal axilWriteMaster : AxiLiteWriteMasterType := AXI_LITE_WRITE_MASTER_INIT_C; + signal axilWriteSlave : AxiLiteWriteSlaveType := AXI_LITE_WRITE_SLAVE_INIT_C; + signal axiAResetN : sl := '1'; + signal sampleArray : Slv16Array(1 downto 0); + signal fcoArray : Slv16Array(0 downto 0); + +begin + + axiAResetN <= not rst; + + U_ShimLayer : entity surf.SlaveAxiLiteIpIntegrator + generic map ( + EN_ERROR_RESP => true, + HAS_PROT => 1, + HAS_WSTRB => 1, + ADDR_WIDTH => 12) + port map ( + S_AXI_ACLK => clk, -- [in] + S_AXI_ARESETN => axiAResetN, -- [in] + S_AXI_AWADDR => S_AXI_AWADDR, -- [in] + S_AXI_AWPROT => S_AXI_AWPROT, -- [in] + S_AXI_AWVALID => S_AXI_AWVALID, -- [in] + S_AXI_AWREADY => S_AXI_AWREADY, -- [out] + S_AXI_WDATA => S_AXI_WDATA, -- [in] + S_AXI_WSTRB => S_AXI_WSTRB, -- [in] + S_AXI_WVALID => S_AXI_WVALID, -- [in] + S_AXI_WREADY => S_AXI_WREADY, -- [out] + S_AXI_BRESP => S_AXI_BRESP, -- [out] + S_AXI_BVALID => S_AXI_BVALID, -- [out] + S_AXI_BREADY => S_AXI_BREADY, -- [in] + S_AXI_ARADDR => S_AXI_ARADDR, -- [in] + S_AXI_ARPROT => S_AXI_ARPROT, -- [in] + S_AXI_ARVALID => S_AXI_ARVALID, -- [in] + S_AXI_ARREADY => S_AXI_ARREADY, -- [out] + S_AXI_RDATA => S_AXI_RDATA, -- [out] + S_AXI_RRESP => S_AXI_RRESP, -- [out] + S_AXI_RVALID => S_AXI_RVALID, -- [out] + S_AXI_RREADY => S_AXI_RREADY, -- [in] + axilClk => open, -- [out] + axilRst => open, -- [out] + axilReadMaster => axilReadMaster, -- [out] + axilReadSlave => axilReadSlave, -- [in] + axilWriteMaster => axilWriteMaster, -- [out] + axilWriteSlave => axilWriteSlave); -- [in] + + sampleArray(0) <= sampleIn(15 downto 0); + sampleArray(1) <= sampleIn(31 downto 16); + fcoArray(0) <= "00" & fcoWord; + + U_DUT : entity surf.AdcDdrPatternTester + generic map ( + TPD_G => TPD_G, + CHANNELS_G => 2, + FCO_LANES_G => 1, + SAMPLE_WIDTH_G => 14, + SERIALIZATION_FACTOR_G => 14, + FRAME_PATTERN_G => "11111110000000") + port map ( + clk => clk, -- [in] + rst => rst, -- [in] + axilReadMaster => axilReadMaster, -- [in] + axilReadSlave => axilReadSlave, -- [out] + axilWriteMaster => axilWriteMaster, -- [in] + axilWriteSlave => axilWriteSlave, -- [out] + sampleValid => sampleValid, -- [in] + sampleIn => sampleArray, -- [in] + fcoValid => (0 => fcoValid), -- [in] + fcoWord => fcoArray); -- [in] + +end architecture rtl; diff --git a/devices/AnalogDevices/ruckus.tcl b/devices/AnalogDevices/ruckus.tcl index d4d6e53eee..e13dfead08 100644 --- a/devices/AnalogDevices/ruckus.tcl +++ b/devices/AnalogDevices/ruckus.tcl @@ -6,10 +6,12 @@ loadRuckusTcl "$::DIR_PATH/ad5541" loadRuckusTcl "$::DIR_PATH/ad5780" loadRuckusTcl "$::DIR_PATH/general" loadRuckusTcl "$::DIR_PATH/ltm4664" +loadRuckusTcl "$::DIR_PATH/adcDdr" # Check for non-zero Vivado version (in-case non-Vivado project) if { $::env(VIVADO_VERSION) > 0.0} { loadRuckusTcl "$::DIR_PATH/ad9467" + loadRuckusTcl "$::DIR_PATH/ad9252" loadRuckusTcl "$::DIR_PATH/ad9249" loadRuckusTcl "$::DIR_PATH/ad9681" diff --git a/devices/README.md b/devices/README.md index b4b2deee2b..22fe4f13e1 100644 --- a/devices/README.md +++ b/devices/README.md @@ -4,8 +4,12 @@ This tree contains vendor and component-specific RTL support. It is organized pr ## Layout -- Manufacturer folders such as `AnalogDevices/`, `Microchip/`, `Micron/`, `Silabs/`, `Ti/`, and `Xilinx/` hold individual device cores. +- Manufacturer folders such as [`AnalogDevices/`](AnalogDevices/), `Microchip/`, `Micron/`, `Silabs/`, `Ti/`, and `Xilinx/` hold individual device cores. - Device folders usually contain `rtl/` for synthesizable register/control logic, optional `sim/` models, optional FPGA-family implementation directories, and a local `ruckus.tcl`. - `transceivers/` holds generic pluggable transceiver support such as SFP/QSFP control and status blocks. Keep register maps and control names aligned with vendor data sheets and with the matching PyRogue modules under `python/surf/devices` when they exist. Add new device sources to the nearest `ruckus.tcl`. + +The Analog Devices tree includes the shared [`adcDdr`](AnalogDevices/adcDdr/) +source-synchronous readout and software-calibration infrastructure used by the +normalized AD9249, AD9252, and AD9681 implementations. diff --git a/docs/plans/README.md b/docs/plans/README.md index 1b88473851..856af91ede 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -20,3 +20,8 @@ Each task directory should capture: - Open risks, blockers, and next steps. Keep notes concise and factual. Do not store large simulator logs, generated outputs, build products, or waveform files here; summarize them and link to durable locations when needed. + +Before merging completed work, move enduring usage, methodology, and +maintenance guidance into the nearest permanent README. Remove the completed +task directory unless it still serves as an intentional design record or an +active follow-up handoff. diff --git a/docs/plans/serialized-ddr-adc-cleanup/README.md b/docs/plans/serialized-ddr-adc-cleanup/README.md new file mode 100644 index 0000000000..7a3e5f0bc1 --- /dev/null +++ b/docs/plans/serialized-ddr-adc-cleanup/README.md @@ -0,0 +1,84 @@ +# Serialized DDR ADC Cleanup Follow-Up + +## Goal + +Complete real-hardware validation of the normalized ADC DDR readout and keep +the alignment/calibration path deterministic, fast, and diagnostically useful. + +## Current Status + +- AD9681 register `0x100` fields remain intentionally absent from the PyRogue + model because hardware readback did not support verified writes. +- Relock now resets the deserializers for a bounded interval before reloading + retained delays and restarting FCO alignment. +- Hardware AD9681 calibration produces bounded eyes for all sixteen physical + data lanes and completes successfully. +- Calibration tap scans use coherent ordered snapshots. `UsePatternTester` + enables one deep final checkerboard window instead of replacing every tap + measurement with repeated hardware windows. +- AD9249 UltraScale hardware initially took about 60 seconds per bank with + debug diagnostics enabled, but was quick with debug disabled. Calibration + therefore retains the deterministic exhaustive scan and avoids repeatedly + publishing the growing diagnostics tree. +- AD9249 hardware showed channel-dependent checkerboard epochs even though a + PN23 snapshot was coherent across every channel. Pulsing the ADC bank's + digital reset while checkerboard mode was selected restored coherence. + Each ADC configuration model now exposes a normalized `DigitalReset()` + command. Common calibration calls that command before data-eye measurement + and restarts the FPGA receiver afterward. Because the failure was observed + primarily while eight banks calibrated in parallel, calibration now repeats + that checkerboard/reset/relock sequence immediately before every final + qualification attempt as well. This removes the long data-scan interval from + the ADC pattern-epoch assumption. + +## Calibration UI Contract + +- Delay thresholds and results explicitly use native `tap` units. UltraScale + uses `IDELAYE3` uncalibrated `COUNT` mode, so ADC sample rate alone cannot + turn those counts into a portable picosecond value. +- `Debug=True` retains the per-tap diagnostics privately and publishes one + completed copy at process termination. Failed and stopped operations still + publish their evidence when debug is disabled. +- Calibration exposes an explicit terminal `Outcome` (`PASSED`, `FAILED`, or + `STOPPED`). `Message` retains PyRogue's normal process status instead of + duplicating that outcome. + +## Deep Qualification Contract + +- `SampleCount` controls the shallow four-sample snapshot depth at each tap. +- `PatternTesterSamples` controls the final hardware checkerboard depth and + defaults to 4096 valid samples per deep window. +- `UsePatternTester` defaults to the readout model's `patternCheck` construction + parameter and remains user-writable. +- The deep result retains per-channel word-error counts and accumulated + bit-error masks plus per-FCO error counts. +- A deep-check failure participates in the existing alternate-FCO-eye retry. +- PN23 first uses the four-sample snapshot to select the ADC output/format + transformation. The hardware tester then acquires an arbitrary nonzero + 23-bit history and deeply checks reference-channel recurrence plus + word-for-word coherence across every other enabled channel. + +## Validation + +Focused PyRogue model and calibration regressions cover strict snapshot order, +deep-check success and failure, detailed error reporting, capability rejection, +alternate FCO-eye retries, shallow and deep PN23 qualification, and state +restoration. + +- `test_AdcDdrCalibration.py` and `test_AdcDdrModel.py`: 72 passed. +- `test_AdcDdrPatternTester.py`: 1 passed with GHDL/cocotb. +- VSG: no violations in `AdcDdrPkg.vhd` or `AdcDdrPatternTester.vhd`. + +## Open Hardware Checks + +- Compare normal calibration run time with deep qualification disabled and + enabled. +- Confirm a 4096-sample deep window reports all-zero channel and FCO errors on + the target board. +- Confirm the 4096-sample PN23 window acquires phase and reports all-zero + reference recurrence and cross-channel coherence errors. +- Confirm repeated AD9249 calibrations retain shared checkerboard phase after + the automatic digital-datapath reset, including all eight banks running in + parallel and alternating between banks. +- Increase `PatternTesterSamples` if a longer bounded checkerboard stress test + is useful; this remains qualification, not a claimed BER measurement. diff --git a/protocols/i2c/rtl/I2cMaster.vhd b/protocols/i2c/rtl/I2cMaster.vhd index 026e84cbe0..6507eac4c5 100644 --- a/protocols/i2c/rtl/I2cMaster.vhd +++ b/protocols/i2c/rtl/I2cMaster.vhd @@ -260,7 +260,10 @@ begin when WAIT_ADDR_ACK_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; if (byteCtrlOut.cmdAck = '1') then -- Master sent the command if (byteCtrlOut.ackOut = '0') then -- Slave ack'd the transfer @@ -302,7 +305,10 @@ begin when WAIT_READ_DATA_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; v.byteCtrlIn.stop := r.byteCtrlIn.stop; -- Hold stop or it wont get seen v.byteCtrlIn.ackIn := r.byteCtrlIn.ackIn; -- This too @@ -331,7 +337,10 @@ begin end if; when WAIT_WRITE_ACK_S => - v.timer := r.timer + 1; + -- Saturate the timer to prevent an out of range value + if (r.timer /= TIMEOUT_C) then + v.timer := r.timer + 1; + end if; v.byteCtrlIn.stop := r.byteCtrlIn.stop; if (byteCtrlOut.cmdAck = '1') then -- Master sent the command diff --git a/protocols/rssi/v1/rtl/RssiConnFsm.vhd b/protocols/rssi/v1/rtl/RssiConnFsm.vhd index b00b28b38b..421d9d8f47 100644 --- a/protocols/rssi/v1/rtl/RssiConnFsm.vhd +++ b/protocols/rssi/v1/rtl/RssiConnFsm.vhd @@ -40,7 +40,7 @@ entity RssiConnFsm is -- WINDOW_ADDR_SIZE_G : positive := 3; SEGMENT_ADDR_SIZE_G : positive := 7 -- 2^SEGMENT_ADDR_SIZE_G = Number of 64 bit wide data words - ); + ); port ( clk_i : in sl; rst_i : in sl; @@ -95,10 +95,11 @@ entity RssiConnFsm is -- Status signals peerTout_o : out sl; paramReject_o : out sl - ); + ); end entity RssiConnFsm; architecture rtl of RssiConnFsm is + -- constant SAMPLES_PER_TIME_C : integer := integer(TIMEOUT_UNIT_G * CLK_FREQUENCY_G); -- @@ -112,7 +113,7 @@ architecture rtl of RssiConnFsm is WAIT_ACK_S, SEND_RST_S, OPEN_S - ); + ); type RegType is record connActive : sl; @@ -136,7 +137,6 @@ architecture rtl of RssiConnFsm is --- state : StateType; connState : slv(3 downto 0); - end record RegType; constant REG_INIT_C : RegType := ( @@ -165,6 +165,42 @@ architecture rtl of RssiConnFsm is signal r : RegType := REG_INIT_C; signal rin : RegType; + function validPeerParams(param : RssiParamType) return boolean is + begin + return ( + param.maxOutsSeg /= 0 and + conv_integer(param.maxSegSize) >= 8 and + param.retransTout /= 0 and + param.cumulAckTout /= 0 and + param.nullSegTout /= 0); + end function validPeerParams; + + function clampWindowSize(value : slv(7 downto 0)) return integer is + variable size : integer; + begin + size := conv_integer(value); + if (size < 1) then + return 1; + elsif (size > 2 ** WINDOW_ADDR_SIZE_G) then + return 2 ** WINDOW_ADDR_SIZE_G; + else + return size; + end if; + end function clampWindowSize; + + function clampBufferSize(value : slv(15 downto 0)) return integer is + variable size : integer; + begin + size := conv_integer(value(15 downto 3)); -- Divide by 8 + if (size < 1) then + return 1; + elsif (size > 2 ** SEGMENT_ADDR_SIZE_G) then + return 2 ** SEGMENT_ADDR_SIZE_G; + else + return size; + end if; + end function clampBufferSize; + begin @@ -234,14 +270,17 @@ begin v.sndAck := '0'; v.sndRst := '0'; v.txAckF := '0'; - v.timeoutCntr := r.timeoutCntr + 1; + if (r.timeoutCntr /= RETRANS_TOUT_G * SAMPLES_PER_TIME_C) then + v.timeoutCntr := r.timeoutCntr + 1; + end if; -- if (rxValid_i = '1' and rxFlags_i.syn = '1' and rxFlags_i.ack = '1') then -- Check parameters if ( rxRssiParam_i.version = appRssiParam_i.version and -- Version match rxRssiParam_i.chksumEn = appRssiParam_i.chksumEn and -- Checksum match - rxRssiParam_i.timeoutUnit = appRssiParam_i.timeoutUnit -- Timeout unit match + rxRssiParam_i.timeoutUnit = appRssiParam_i.timeoutUnit and -- Timeout unit match + validPeerParams(rxRssiParam_i) = true ) then -- Accept the parameters from the server @@ -249,19 +288,19 @@ begin -- Autoneg the maxOutsSeg if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then - v.txWindowSize := conv_integer(appRssiParam_i.maxOutsSeg); + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; else - v.txWindowSize := conv_integer(rxRssiParam_i.maxOutsSeg); + v.txWindowSize := clampWindowSize(rxRssiParam_i.maxOutsSeg); v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; end if; -- Autoneg the maxSegSize if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then - v.txBufferSize := conv_integer(appRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; else - v.txBufferSize := conv_integer(rxRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 + v.txBufferSize := clampBufferSize(rxRssiParam_i.maxSegSize); v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; end if; @@ -327,43 +366,43 @@ begin -- if (rxValid_i = '1' and rxFlags_i.syn = '1') then - -- Accept the parameters from the client - v.rssiParam := rxRssiParam_i; - - -- Autoneg the maxOutsSeg - if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then - v.txWindowSize := conv_integer(appRssiParam_i.maxOutsSeg); - v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; - else - v.txWindowSize := conv_integer(rxRssiParam_i.maxOutsSeg); - v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; - end if; - - -- Autoneg the maxSegSize - if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then - v.txBufferSize := conv_integer(appRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 - v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; - else - v.txBufferSize := conv_integer(rxRssiParam_i.maxSegSize(15 downto 3)); -- Divide by 8 - v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; - end if; - - -- - v.paramReject := '0'; - -- Check parameters that have to match if ( rxRssiParam_i.version /= appRssiParam_i.version or -- Version equality rxRssiParam_i.chksumEn /= appRssiParam_i.chksumEn or -- Checksum match - rxRssiParam_i.timeoutUnit /= appRssiParam_i.timeoutUnit -- Timeout unit match + rxRssiParam_i.timeoutUnit /= appRssiParam_i.timeoutUnit or -- Timeout unit match + validPeerParams(rxRssiParam_i) = false ) then -- Propose different parameters (overwrite) - v.rssiParam.version := appRssiParam_i.version; - v.rssiParam.timeoutUnit := appRssiParam_i.timeoutUnit; - v.rssiParam.chksumEn := appRssiParam_i.chksumEn; + v.rssiParam := appRssiParam_i; + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); -- - v.paramReject := '1'; + v.paramReject := '1'; + else + -- Accept the parameters from the client + v.rssiParam := rxRssiParam_i; + + -- Autoneg the maxOutsSeg + if (rxRssiParam_i.maxOutsSeg > appRssiParam_i.maxOutsSeg) then + v.txWindowSize := clampWindowSize(appRssiParam_i.maxOutsSeg); + v.rssiParam.maxOutsSeg := appRssiParam_i.maxOutsSeg; + else + v.txWindowSize := clampWindowSize(rxRssiParam_i.maxOutsSeg); + v.rssiParam.maxOutsSeg := rxRssiParam_i.maxOutsSeg; + end if; + + -- Autoneg the maxSegSize + if (rxRssiParam_i.maxSegSize > appRssiParam_i.maxSegSize) then + v.txBufferSize := clampBufferSize(appRssiParam_i.maxSegSize); + v.rssiParam.maxSegSize := appRssiParam_i.maxSegSize; + else + v.txBufferSize := clampBufferSize(rxRssiParam_i.maxSegSize); + v.rssiParam.maxSegSize := rxRssiParam_i.maxSegSize; + end if; + + v.paramReject := '0'; end if; -- Go to ACK state v.state := SEND_SYN_ACK_S; @@ -398,7 +437,9 @@ begin v.txAckF := '0'; v.paramReject := '0'; -- - v.timeoutCntr := r.timeoutCntr+1; + if (r.timeoutCntr /= RETRANS_TOUT_G * SAMPLES_PER_TIME_C) then + v.timeoutCntr := r.timeoutCntr + 1; + end if; -- v.rssiParam := r.rssiParam; @@ -507,5 +548,7 @@ begin connState_o <= r.connState; peerTout_o <= r.peerTout; paramReject_o <= r.paramReject; + --------------------------------------------------------------------- + end architecture rtl; diff --git a/protocols/rssi/v1/rtl/RssiCore.vhd b/protocols/rssi/v1/rtl/RssiCore.vhd index faa7ef90b0..bfb17eff14 100644 --- a/protocols/rssi/v1/rtl/RssiCore.vhd +++ b/protocols/rssi/v1/rtl/RssiCore.vhd @@ -88,8 +88,7 @@ entity RssiCore is -- Counters MAX_RETRANS_CNT_G : positive := 2; - MAX_CUM_ACK_CNT_G : positive := 3 - ); + MAX_CUM_ACK_CNT_G : positive := 3); port ( clk_i : in sl; rst_i : in sl; @@ -128,6 +127,10 @@ end entity RssiCore; architecture rtl of RssiCore is constant BUFFER_ADDR_WIDTH_C : positive := (SEGMENT_ADDR_SIZE_G+WINDOW_ADDR_SIZE_G); + -- Pause one segment early, padded by 16 words. The padding is capped at half + -- a segment so that segment buffers smaller than 32 words keep a valid, + -- proportional threshold instead of going non-positive. + constant FIFO_PAUSE_THRESH_C : positive := (2**SEGMENT_ADDR_SIZE_G) - minimum(16, 2**(SEGMENT_ADDR_SIZE_G-1)); -- Busy Flags signal s_localBusy : sl; @@ -291,8 +294,8 @@ architecture rtl of RssiCore is -- attribute dont_touch of s_mAppAxisCtrl : signal is "TRUE"; -- attribute dont_touch of s_mTspAxisCtrl : signal is "TRUE"; ----------------------------------------------------------------------- begin + -- Assertions to check generics assert (1 <= MAX_NUM_OUTS_SEG_G and MAX_NUM_OUTS_SEG_G <= (2**WINDOW_ADDR_SIZE_G)) report "MAX_NUM_OUTS_SEG_G should be less or equal to 2**WINDOW_ADDR_SIZE_G" severity failure; assert (8 <= MAX_SEG_SIZE_G and MAX_SEG_SIZE_G <= (2**SEGMENT_ADDR_SIZE_G)*8) report "MAX_SEG_SIZE_G should be less or equal to (2**SEGMENT_ADDR_SIZE_G)*8" severity failure; @@ -695,8 +698,7 @@ begin generic map ( TPD_G => TPD_G, DATA_WIDTH_G => 64, - CSUM_WIDTH_G => 16 - ) + CSUM_WIDTH_G => 16) port map ( clk_i => clk_i, rst_i => rst_i, @@ -783,8 +785,7 @@ begin generic map ( TPD_G => TPD_G, DATA_WIDTH_G => 64, - CSUM_WIDTH_G => 16 - ) + CSUM_WIDTH_G => 16) port map ( clk_i => clk_i, rst_i => rst_i, @@ -828,7 +829,7 @@ begin MEMORY_TYPE_G => "block", FIFO_ADDR_WIDTH_G => SEGMENT_ADDR_SIZE_G+1, -- Enough to store 2 segments FIFO_FIXED_THRESH_G => true, - FIFO_PAUSE_THRESH_G => (2**SEGMENT_ADDR_SIZE_G) - 16, -- Threshold at 1 segment minus padding + FIFO_PAUSE_THRESH_G => FIFO_PAUSE_THRESH_C, -- Threshold at 1 segment minus padding INT_WIDTH_SELECT_G => "CUSTOM", INT_DATA_WIDTH_G => RSSI_WORD_WIDTH_C, SLAVE_AXI_CONFIG_G => RSSI_AXIS_CONFIG_C, @@ -860,7 +861,7 @@ begin MEMORY_TYPE_G => "block", FIFO_ADDR_WIDTH_G => SEGMENT_ADDR_SIZE_G+1, -- Enough to store 2 segments FIFO_FIXED_THRESH_G => true, - FIFO_PAUSE_THRESH_G => (2**SEGMENT_ADDR_SIZE_G) - 16, -- Threshold at 1 segment minus padding + FIFO_PAUSE_THRESH_G => FIFO_PAUSE_THRESH_C, -- Threshold at 1 segment minus padding INT_WIDTH_SELECT_G => "CUSTOM", INT_DATA_WIDTH_G => RSSI_WORD_WIDTH_C, SLAVE_AXI_CONFIG_G => RSSI_AXIS_CONFIG_C, diff --git a/protocols/rssi/v1/rtl/RssiMonitor.vhd b/protocols/rssi/v1/rtl/RssiMonitor.vhd index 5a4ffe6167..6aba795975 100644 --- a/protocols/rssi/v1/rtl/RssiMonitor.vhd +++ b/protocols/rssi/v1/rtl/RssiMonitor.vhd @@ -52,8 +52,8 @@ entity RssiMonitor is STATUS_WIDTH_G : positive := 8; CNT_WIDTH_G : positive := 32; RETRANSMIT_ENABLE_G : boolean := true - -- - ); + -- + ); port ( clk_i : in sl; rst_i : in sl; @@ -185,7 +185,9 @@ architecture rtl of RssiMonitor is signal rin : RegType; signal s_status : slv(STATUS_WIDTH_G - 1 downto 0); -- + begin + -- Status assignment s_status(0) <= r.retransMax and r.sndResend and not r.sndResendD1; s_status(1) <= r.nullTout; @@ -312,8 +314,6 @@ begin if (connActive_i = '0' or (rxValid_i = '1' and rxFlags_i.data = '1') or (rxValid_i = '1' and rxFlags_i.nul = '1') or - (rxValid_i = '1' and rxFlags_i.ack = '1') or - (rxValid_i = '1' and rxFlags_i.busy = '1') or RETRANSMIT_ENABLE_G = false -- Disable null timeout ) then v.nullToutCnt := (others => '0'); @@ -356,7 +356,7 @@ begin dataHeadSt_i = '1' or rstHeadSt_i = '1' or nullHeadSt_i = '1' or - (rxLastSeqN_i - r.lastAckSeqN) = 0 + ((rxLastSeqN_i - r.lastAckSeqN) = 0 and localBusy_i = '0') ) then v.ackToutCnt := (others => '0'); elsif ((rxLastSeqN_i - r.lastAckSeqN) > 0 and (rxLastSeqN_i - r.lastAckSeqN) <= rxWindowSize_i) or (localBusy_i = '1') then @@ -374,8 +374,16 @@ begin ) then v.sndAck := '0'; + -- Periodic BUSY acknowledgment request. The RSSI page recommends + -- Retransmission Timeout/2 so the peer keeps its retransmission timer + -- reset while this receiver remains busy. + elsif (localBusy_i = '1' and (rxLastSeqN_i - r.lastAckSeqN) = 0 and + r.ackToutCnt >= ((conv_integer(rssiParam_i.retransTout)*SAMPLES_PER_TIME_C)/2)) then + v.sndAck := '1'; + -- Timeout acknowledgment request - elsif (r.ackToutCnt >= (conv_integer(rssiParam_i.cumulAckTout)* SAMPLES_PER_TIME_C)) then + elsif ((rxLastSeqN_i - r.lastAckSeqN) > 0 and + r.ackToutCnt >= (conv_integer(rssiParam_i.cumulAckTout)* SAMPLES_PER_TIME_C)) then v.sndAck := '1'; -- Cumulative acknowledgment request @@ -460,4 +468,5 @@ begin resendCnt_o <= r.resendCnt; reconCnt_o <= r.reconCnt; --------------------------------------------------------------------- + end architecture rtl; diff --git a/protocols/rssi/v1/rtl/RssiTxFsm.vhd b/protocols/rssi/v1/rtl/RssiTxFsm.vhd index 61449bf991..3554dacf3a 100644 --- a/protocols/rssi/v1/rtl/RssiTxFsm.vhd +++ b/protocols/rssi/v1/rtl/RssiTxFsm.vhd @@ -64,7 +64,7 @@ entity RssiTxFsm is DATA_HEADER_SIZE_G : natural := 8; HEADER_CHKSUM_EN_G : boolean := true - ); + ); port ( clk_i : in sl; rst_i : in sl; @@ -153,7 +153,7 @@ entity RssiTxFsm is -- Segment buffer indicator bufferEmpty_o : out sl - ); + ); end entity RssiTxFsm; architecture rtl of RssiTxFsm is @@ -185,7 +185,7 @@ architecture rtl of RssiTxFsm is RESEND_H_S, RESEND_DATA_S, RESEND_PP_S - ); + ); type AppStateType is ( IDLE_S, @@ -193,17 +193,16 @@ architecture rtl of RssiTxFsm is SEG_RCV_S, SEG_RDY_S, SEG_LEN_ERR - ); + ); type AckStateType is ( IDLE_S, ERR_S, ACK_S --EACK_S, - ); + ); type RegType is record - -- Buffer window handling and acknowledgment control ----------------------------------------- windowArray : WindowTypeArray(0 to 2 ** WINDOW_ADDR_SIZE_G-1); @@ -275,7 +274,7 @@ architecture rtl of RssiTxFsm is tspSsiSlave : SsiSlaveType; -- State Machine - tspState : tspStateType; + tspState : TspStateType; txTspState : slv(7 downto 0); end record RegType; @@ -358,6 +357,7 @@ architecture rtl of RssiTxFsm is signal rin : RegType; signal s_chksum : slv(chksum_i'range); signal s_headerAndChksum : slv(RSSI_WORD_WIDTH_C*8-1 downto 0); + signal s_corruptHeader : slv(RSSI_WORD_WIDTH_C*8-1 downto 0); -- attribute dont_touch : string; -- attribute dont_touch of r : signal is "TRUE"; @@ -369,13 +369,15 @@ begin -- Send all 0 if checksum disabled s_chksum <= ite(HEADER_CHKSUM_EN_G, chksum_i, (chksum_i'range => '0')); s_headerAndChksum <= rdHeaderData_i(63 downto 16) & s_chksum(15 downto 0); + s_corruptHeader <= s_headerAndChksum xor x"000000000000FFFF"; ----------------------------------------------------------------------------------------------- comb : process (ackN_i, ack_i, appSsiMaster_i, bufferSize_i, chksumValid_i, closed_i, connActive_i, headerLength_i, headerRdy_i, initSeqN_i, injectFault_i, r, rdBuffData_i, rdHeaderData_i, - rst_i, s_headerAndChksum, sndAck_i, sndNull_i, sndResend_i, - sndRst_i, sndSyn_i, tspSsiSlave_i, windowSize_i) is + rst_i, s_corruptHeader, s_headerAndChksum, sndAck_i, + sndNull_i, sndResend_i, sndRst_i, sndSyn_i, tspSsiSlave_i, + windowSize_i) is variable v : RegType; @@ -892,7 +894,7 @@ begin v.tspState := RESEND_INIT_S; elsif (sndAck_i = '1') then v.tspState := ACK_H_S; - elsif (sndNull_i = '1' and r.bufferFull = '0' and r.appBusy = '0') then + elsif (sndNull_i = '1' and r.bufferEmpty = '1' and r.appBusy = '0') then v.tspState := NULL_WE_S; elsif (connActive_i = '0') then v.tspState := INIT_S; @@ -943,6 +945,7 @@ begin -- Add checksum v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); v.tspSsiMaster.valid := '1'; + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; @@ -1007,8 +1010,13 @@ begin v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; - -- Add checksum to last two bytes - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + if (r.injectFaultReg = '1') then + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); + else + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + end if; + + v.injectFaultReg := '0'; -- if connActive_i = '0' then @@ -1178,8 +1186,13 @@ begin v.tspSsiMaster.eof := '1'; v.tspSsiMaster.eofe := '0'; - -- Add checksum to last two bytes - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + if (r.injectFaultReg = '1') then + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); + else + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); + end if; + + v.injectFaultReg := '0'; -- Increment seqN v.nextSeqN := r.nextSeqN+1; -- Increment SEQ number at the end of segment transmission @@ -1261,13 +1274,14 @@ begin v.tspSsiMaster.valid := '1'; v.tspSsiMaster.sof := '1'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.eof := '0'; v.tspSsiMaster.eofe := '0'; -- Inject fault into checksum if (r.injectFaultReg = '1') then - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum) xor (s_headerAndChksum'range => '1'); -- Flip bits in checksum! Point of fault injection! + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); else v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); -- Add checksum to last two bytes end if; @@ -1307,6 +1321,7 @@ begin -- Other SSI parameters v.tspSsiMaster.sof := '0'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := rdBuffData_i; @@ -1440,11 +1455,12 @@ begin v.tspSsiMaster.sof := '1'; v.tspSsiMaster.valid := '1'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); -- Inject fault into checksum if (r.injectFaultReg = '1') then - v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum) xor (s_headerAndChksum'range => '1'); -- Flip bits in checksum! Point of fault injection! + v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_corruptHeader); else v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := endianSwap64(s_headerAndChksum); -- Add checksum to last two bytes end if; @@ -1501,6 +1517,7 @@ begin -- Other SSI parameters v.tspSsiMaster.sof := '0'; v.tspSsiMaster.strb := (others => '1'); + v.tspSsiMaster.keep(RSSI_WORD_WIDTH_C-1 downto 0) := (others => '1'); v.tspSsiMaster.dest := (others => '0'); v.tspSsiMaster.data(RSSI_WORD_WIDTH_C*8-1 downto 0) := rdBuffData_i; @@ -1639,4 +1656,5 @@ begin end process seq; --------------------------------------------------------------------- + end architecture rtl; diff --git a/python/README.md b/python/README.md index 58e78350e8..fe894473aa 100644 --- a/python/README.md +++ b/python/README.md @@ -1,6 +1,6 @@ # Python -The Python package lives under `python/surf` and is installed as `surf`. It primarily contains PyRogue device descriptions and small support utilities that mirror SURF RTL register maps. +The Python package lives under `python/surf` and is installed as `surf`. It primarily contains PyRogue device descriptions and small support utilities that mirror SURF RTL register maps. Python 3.10 or newer is required, matching the supported range of current Rogue releases. ## Layout @@ -12,3 +12,12 @@ The Python package lives under `python/surf` and is installed as `surf`. It prim - `surf/misc/` and `surf/dsp/`: smaller utilities and DSP-related support. Implementation modules usually use private filenames such as `_AxiVersion.py` and are re-exported from package `__init__.py` files. Keep register names, offsets, bit offsets, modes, and descriptions synchronized with the corresponding RTL packages and user-facing hardware documentation. + +## PyRogue API Style + +New and substantially revised PyRogue modules should follow the current Rogue Python style: + +- Add `from __future__ import annotations` and type all function and method parameters and return values, including private helpers. Use Python 3.10 union syntax such as `Node | None`, and use `Any` where PyRogue's dynamic node plumbing makes a more specific annotation misleading. +- Document public classes and functions with NumPy-style docstrings. Put constructor arguments in the class-level `Parameters` section and add `Returns`, `Raises`, and `Notes` sections when they clarify the interface. +- Keep private-method docstrings concise unless the method has a non-obvious contract. +- Use annotations to state accepted Python types. Runtime validation should focus on meaningful hardware constraints, such as supported lane counts and delay widths, instead of duplicating the type system. diff --git a/python/surf/devices/analog_devices/_Ad9249.py b/python/surf/devices/analog_devices/_Ad9249.py index 67bffc451e..d6fbe2058e 100644 --- a/python/surf/devices/analog_devices/_Ad9249.py +++ b/python/surf/devices/analog_devices/_Ad9249.py @@ -1,8 +1,8 @@ #----------------------------------------------------------------------------- -# Title : PyRogue _ad9249 Module +# Title : PyRogue AD9249 model #----------------------------------------------------------------------------- # Description: -# PyRogue _ad9249 Module +# AD9249 configuration, readout, and calibration models. #----------------------------------------------------------------------------- # This file is part of 'SLAC Firmware Standard Library'. # It is subject to the license terms in the LICENSE.txt file found in the @@ -13,14 +13,31 @@ # the terms contained in the LICENSE.txt file. #----------------------------------------------------------------------------- -import pyrogue as pr -import rogue.interfaces.memory as rim +from __future__ import annotations + import math +from typing import Any + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices class Ad9249ConfigGroup(pr.Device): - def __init__(self, - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """Configuration registers for one eight-channel AD9249 bank. + + Parameters + ---------- + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + description: str = 'Configure one side of an AD9249 ADC', + **kwargs: Any) -> None: + """Create one AD9249 bank configuration model.""" super().__init__(description=description, **kwargs) @@ -180,6 +197,27 @@ def __init__(self, }, )) + self.add(pr.RemoteVariable( + name = 'ResetPNShort', + description = 'Reset the PN9 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 4, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'ResetPNLongReg', + description = 'Reset the PN23 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 5, + base = pr.Bool, + mode = 'RW', + hidden = True, + )) + self.add(pr.RemoteVariable( name = 'OffsetAdjust', description = 'Output offset adjustment in LSB steps', @@ -250,21 +288,104 @@ def __init__(self, base = pr.Bool, )) + self.add(pr.RemoteCommand( + name = 'DeviceUpdate', + description = 'Transfers the resolution/sample-rate override into the ADC', + offset = (0xFF*4), + function = pr.BaseCommand.touchOne, + )) + + self.add(pr.RemoteVariable( + name = 'ResolutionSampleRateOverride', + description = 'Enables the resolution and maximum sample-rate override', + offset = (0x100*4), + bitSize = 1, + bitOffset = 6, + base = pr.Bool, + )) + + self.add(pr.RemoteVariable( + name = 'Resolution', + description = 'Selects ADC resolution when the override is enabled', + offset = (0x100*4), + bitSize = 2, + bitOffset = 4, + enum = { + 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit + 0b01: '14 bits', + 0b10: '12 bits', + }, + )) + + self.add(pr.RemoteVariable( + name = 'SampleRate', + description = 'Selects maximum ADC sample rate when the override is enabled', + offset = (0x100*4), + bitSize = 3, + bitOffset = 0, + enum = { + 0b000: '20 MSPS', + 0b001: '40 MSPS', + 0b010: '50 MSPS', + 0b011: '65 MSPS', + }, + )) + + analog_devices.addAdcDdrResetCommands( + self, self.InternalPdwnMode, self.ResetPNLongReg) + + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() + class Ad9249ChipConfig(pr.Device): - def __init__(self, - name = 'Ad9249ChipConfig', - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """Configuration model containing both banks of one AD9249. + + Parameters + ---------- + name : str, optional + PyRogue device name. + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + name: str = 'Ad9249ChipConfig', + description: str = 'Configure one side of an AD9249 ADC', + **kwargs: Any) -> None: + """Create the two-bank configuration model.""" + super().__init__(name=name, description=description, **kwargs) - self.add(Ad9249ConfigGroup('BankConfig[0]', 0x0000)) - self.add(Ad9249ConfigGroup('BankConfig[1]', 0x0200)) + self.add(Ad9249ConfigGroup(name='BankConfig[0]', offset=0x0000)) + self.add(Ad9249ConfigGroup(name='BankConfig[1]', offset=0x0200)) class Ad9249Config(pr.Device): - def __init__(self, - name = 'Ad9249Config', - description = 'Configuration of Ad9249 AD', - chips = 1, - **kwargs): + """Configuration model for one or more AD9249 devices. + + Parameters + ---------- + name : str, optional + PyRogue device name. + description : str, optional + PyRogue device description. + chips : int, optional + Number of AD9249 devices represented by the firmware register map. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + name: str = 'Ad9249Config', + description: str = 'Configuration of Ad9249 ADC', + chips: int = 1, + **kwargs: Any) -> None: + """Create the AD9249 configuration model.""" + super().__init__(name=name, description=description, **kwargs) PDWN_ADDR = int(pow(2,11+math.log(chips*2,2))) @@ -296,415 +417,141 @@ def __init__(self, self.add(Ad9249ConfigGroup(name=f'Ad9249ChipBankConfig0[{i}]', offset=i*0x1000)) self.add(Ad9249ConfigGroup(name=f'Ad9249ChipBankConfig1[{i}]', offset=i*0x1000+0x0800)) -class Ad9249ReadoutGroup(pr.Device): - def __init__(self, - name = 'Ad9249ReadoutGroup', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'ChannelDelay[{i}]', - description = f'IDELAY value for serial channel {i}', - offset = i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - self.add(pr.RemoteVariable( - name = 'FrameDelay', - description = 'IDELAY value for FCO', - offset = 0x20, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - self.add(pr.RemoteVariable( - name = 'LostLockCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Locked', - description = 'Readout has locked on to the frame boundary', - offset = 0x30, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'AdcFrame', - description = 'Last deserialized FCO value for debug', - offset = 0x34, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x40, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:_x}', - mode = 'RO', - )) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x38, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - @staticmethod - def setDelay(var, value, write): - iValue = value + 512 - var.dependencies[0].set(iValue, write) - var.dependencies[0].set(value, write) - - @staticmethod - def getDelay(var, read): - return var.dependencies[0].get(read=read) - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - freeze = isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) - if freeze: - self.FreezeDebug(1) - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - if freeze: - self.FreezeDebug(0) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - - -class Ad9249ReadoutGroup2(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 9 - else: - delayBits = 6 - - - self.add(pr.RemoteVariable( - name = 'Delay', - description = 'IDELAY value', - offset = 0x00, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - groups = ['NoConfig'], - )) - - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x20, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.toggle)) - - self.add(pr.RemoteVariable( - name = 'ErrorDetCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30, - disp = '{:d}', - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'LostLockCount', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Locked', - description = 'Readout has locked on to the frame boundary', - offset = 0x50, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'AdcFrameSync', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 14, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads +class Ad9249ReadoutBank(analog_devices.AdcDdr): + """Normalized register model for one eight-channel AD9249 output bank. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + numChannels: int = 8, + patternCheck: bool = True, + **kwargs: Any) -> None: + """Create one normalized AD9249 bank readout. + + ``numChannels`` must match the RTL ``NUM_CHANNELS_G`` generic; the + firmware only implements delay/data registers for the instantiated + lanes, so reading a wider map returns an AXI decode error. """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - - -class AdcTester(pr.Device): - def __init__(self, description='ADC Pattern Tester Registers', **kwargs): - """Create AdcTester""" - super().__init__(description=description, **kwargs) - - # Creation. memBase is either the register bus server (srp, rce mapped memory, etc) or the device which - # contains this object. In most cases the parent and memBase are the same but they can be - # different in more complex bus structures. They will also be different for the top most node. - # The setMemBase call can be used to update the memBase for this Device. All sub-devices and local - # blocks will be updated. - - ############################################# - # Create block / variable combinations - ############################################# - - - #Setup registers & variables - self.add(pr.RemoteVariable( - name = 'TestChannel', - description = 'Test Channel Select', - offset = 0x00000000, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestDataMask', - description = 'Test Data Mask', - offset = 0x00000004, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestPattern', - description = 'Test Pattern', - offset = 0x00000008, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestSamples', - description = 'Test Samples Number', - offset = 0x0000000C, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestTimeout', - description = 'Test Timeout', - offset = 0x00000010, - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestRequest', - description = 'Test Request', - offset = 0x00000014, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'TestPassed', - description = 'Test Passed Flag', - offset = 0x00000018, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'TestFailed', - description = 'Test Failed Flag', - offset = 0x0000001C, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RO', - )) - ##################################### - # Create commands - ##################################### - - # A command has an associated function. The function can be a series of - # python commands in a string. Function calls are executed in the command scope - # the passed arg is available as 'arg'. Use 'dev' to get to device scope. - # A command can also be a call to a local function with local scope. - # The command object and the arg are passed - - @staticmethod - def frequencyConverter(self): - def func(dev, var): - return '{:.3f} kHz'.format(1/(self.clkPeriod * self._count(var.dependencies)) * 1e-3) - return func + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'One normalized AD9249 output bank') + super().__init__( + dataLanes = numChannels, + fcoLanes = 1, + channels = numChannels, + sampleBits = 14, + serializationFactor = 14, + delayBits = delayBits, + patternCheck = patternCheck, + **kwargs) + + +class Ad9249ReadoutBankCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for one normalized AD9249 output bank. + + Parameters + ---------- + config : Ad9249ConfigGroup + Configuration device for the corresponding ADC bank. + readout : Ad9249ReadoutBank + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9249ConfigGroup, + readout: Ad9249ReadoutBank, + **kwargs: Any) -> None: + """Create one AD9249 bank calibration process.""" + + if not isinstance(config, Ad9249ConfigGroup): + raise TypeError('config must be an Ad9249ConfigGroup') + if not isinstance(readout, Ad9249ReadoutBank): + raise TypeError('readout must be an Ad9249ReadoutBank') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(readout._dataLanes)), + **kwargs) + + +class Ad9249Readout(pr.Device): + """Full AD9249 readout containing two independent normalized banks. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, + **kwargs: Any) -> None: + """Create the complete normalized AD9249 readout.""" + + analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'Complete normalized 16-channel AD9249 readout') + super().__init__(**kwargs) + + for i in range(2): + self.add(Ad9249ReadoutBank( + name = f'Bank[{i}]', + offset = 0x1000*i, + deviceFamily = deviceFamily, + patternCheck = patternCheck)) + + +class Ad9249ReadoutCalibration(pr.Device): + """Container for both AD9249 bank calibration processes. + + Parameters + ---------- + config : Ad9249Config + Configuration device containing both ADC banks. + readout : Ad9249Readout + Complete normalized AD9249 readout. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + config: Ad9249Config, + readout: Ad9249Readout, + **kwargs: Any) -> None: + """Create both AD9249 bank calibration processes.""" + + if not isinstance(config, Ad9249Config): + raise TypeError('config must be an Ad9249Config') + if not isinstance(readout, Ad9249Readout): + raise TypeError('readout must be an Ad9249Readout') + kwargs.setdefault('description', 'Calibration processes for both AD9249 output banks') + super().__init__(**kwargs) + + for bank in range(2): + self.add(Ad9249ReadoutBankCalibration( + name = f'Bank[{bank}]', + config = config.BankConfig[bank], + readout = readout.Bank[bank])) diff --git a/python/surf/devices/analog_devices/_Ad9249Legacy.py b/python/surf/devices/analog_devices/_Ad9249Legacy.py new file mode 100644 index 0000000000..6e915cd32e --- /dev/null +++ b/python/surf/devices/analog_devices/_Ad9249Legacy.py @@ -0,0 +1,284 @@ +#----------------------------------------------------------------------------- +# Title : Legacy AD9249 readout interfaces +#----------------------------------------------------------------------------- +# Description: +# PyRogue register maps retained for the legacy Ad9249ReadoutGroup RTL. +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +import pyrogue as pr +import rogue.interfaces.memory as rim + + +__all__ = [ + 'Ad9249ReadoutGroup', + 'AdcTester', +] + + +class Ad9249ReadoutGroup(pr.Device): + def __init__(self, + name = 'Ad9249ReadoutGroup', + description = 'Configure readout of 1 bank of an AD9249', + fpga = '7series', + channels = 8, + **kwargs): + assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 1 and 8' + super().__init__(name=name, description=description, **kwargs) + + if fpga == '7series': + delayBits = 6 + elif fpga == 'ultrascale': + delayBits = 10 + else: + delayBits = 6 + + for i in range(channels): + self.add(pr.RemoteVariable( + name = f'ChannelDelay[{i}]', + description = f'IDELAY value for serial channel {i}', + offset = i*4, + bitSize = delayBits, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + verify = False, + )) + + self.add(pr.RemoteVariable( + name = 'FrameDelay', + description = 'IDELAY value for FCO', + offset = 0x20, + bitSize = delayBits, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + verify = False, + )) + + self.add(pr.RemoteVariable( + name = 'LostLockCount', + description = 'Number of times that frame lock has been lost since reset', + offset = 0x30, + bitSize = 16, + bitOffset = 0, + base = pr.UInt, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'Locked', + description = 'Readout has locked on to the frame boundary', + offset = 0x30, + bitSize = 1, + bitOffset = 16, + base = pr.Bool, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'AdcFrame', + description = 'Last deserialized FCO value for debug', + offset = 0x34, + bitSize = 16, + bitOffset = 0, + base = pr.UInt, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'Invert', + description = 'Optional ADC data inversion (offset binary only)', + offset = 0x40, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RW', + )) + + for i in range(channels): + self.add(pr.RemoteVariable( + name = f'AdcChannel[{i:d}]', + description = f'Last deserialized channel {i:d} ADC value for debug', + offset = 0x80 + (i*4), + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + disp = '{:_x}', + mode = 'RO', + )) + + self.add(pr.RemoteCommand( + name = 'LostLockCountReset', + description = 'Reset LostLockCount', + function = pr.BaseCommand.toggle, + offset = 0x38, + bitSize = 1, + bitOffset = 0, + )) + + self.add(pr.RemoteCommand( + name='FreezeDebug', + description='Freeze all of the AdcChannel registers', + hidden=True, + offset=0xA0, + bitSize=1, + bitOffset=0, + base=pr.UInt, + function=pr.RemoteCommand.touch)) + + @staticmethod + def setDelay(var, value, write): + iValue = value + 512 + var.dependencies[0].set(iValue, write) + var.dependencies[0].set(value, write) + + @staticmethod + def getDelay(var, read): + return var.dependencies[0].get(read=read) + + def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): + """ + Perform background reads + """ + checkEach = checkEach or self.forceCheckEach + + if variable is not None: + freeze = isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) + if freeze: + self.FreezeDebug(1) + pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) + if freeze: + self.FreezeDebug(0) + + else: + self.FreezeDebug(1) + for block in self._blocks: + if block.bulkOpEn: + pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) + self.FreezeDebug(0) + + if recurse: + for key,value in self.devices.items(): + value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) + + +class AdcTester(pr.Device): + def __init__(self, description='ADC Pattern Tester Registers', **kwargs): + """Create AdcTester""" + super().__init__(description=description, **kwargs) + + # Creation. memBase is either the register bus server (srp, rce mapped memory, etc) or the device which + # contains this object. In most cases the parent and memBase are the same but they can be + # different in more complex bus structures. They will also be different for the top most node. + # The setMemBase call can be used to update the memBase for this Device. All sub-devices and local + # blocks will be updated. + + ############################################# + # Create block / variable combinations + ############################################# + + + #Setup registers & variables + self.add(pr.RemoteVariable( + name = 'TestChannel', + description = 'Test Channel Select', + offset = 0x00000000, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestDataMask', + description = 'Test Data Mask', + offset = 0x00000004, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestPattern', + description = 'Test Pattern', + offset = 0x00000008, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestSamples', + description = 'Test Samples Number', + offset = 0x0000000C, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestTimeout', + description = 'Test Timeout', + offset = 0x00000010, + bitSize = 32, + bitOffset = 0, + base = pr.UInt, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestRequest', + description = 'Test Request', + offset = 0x00000014, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'TestPassed', + description = 'Test Passed Flag', + offset = 0x00000018, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RO', + )) + + self.add(pr.RemoteVariable( + name = 'TestFailed', + description = 'Test Failed Flag', + offset = 0x0000001C, + bitSize = 1, + bitOffset = 0, + base = pr.Bool, + mode = 'RO', + )) + + ##################################### + # Create commands + ##################################### + + # A command has an associated function. The function can be a series of + # python commands in a string. Function calls are executed in the command scope + # the passed arg is available as 'arg'. Use 'dev' to get to device scope. + # A command can also be a call to a local function with local scope. + # The command object and the arg are passed + + @staticmethod + def frequencyConverter(self): + def func(dev, var): + return '{:.3f} kHz'.format(1/(self.clkPeriod * self._count(var.dependencies)) * 1e-3) + return func diff --git a/python/surf/devices/analog_devices/_Ad9252.py b/python/surf/devices/analog_devices/_Ad9252.py new file mode 100644 index 0000000000..394b23982c --- /dev/null +++ b/python/surf/devices/analog_devices/_Ad9252.py @@ -0,0 +1,400 @@ +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices + +class Ad9252Config(pr.Device): + """PyRogue configuration model for the AD9252 ADC. + + Parameters + ---------- + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__(self, **kwargs: Any) -> None: + """Create the AD9252 configuration model.""" + + super().__init__(description="AD9252 ADC object.",**kwargs) + + +# self.add(pr.RemoteVariable( +# name = "ConfigEn", +# description='Set to ''True'' to enable register writes to ADC.', +# offset=0x00, +# bitSize=1, +# bitOffset=0, +# base =pr.Bool)) + + self.add(pr.RemoteVariable( + name = "ChipId", + description='Read only chip ID value.', + offset=0x04, + bitSize=8, + bitOffset=0, + mode = "RO")) + + self.add(pr.RemoteVariable( + name="ChipGrade", + description='Read only chip grade value.', + offset=0x08, + bitSize=3, + bitOffset=4, + mode="RO")) + + + self.add(pr.RemoteVariable( + name="PowerDownMode", + description='Set power mode of device.', + offset=0x20, + bitSize=2, + bitOffset=0, + base=pr.UInt, + enum = { + 0: "Chip Run", + 1: "Full Power Down", + 2: "Standby", + 3: "Digital Reset"})) + + self.add(pr.RemoteVariable( + name = "DutyCycleStabilizer", + description='Turns on internal duty cycle stabilizer. (default=True).', + offset=0x24, + bitSize=1, + bitOffset=0, + base =pr.Bool)) + + + self.add(pr.RemoteVariable( + name="DevIndexMask[7:4]", + offset=0x10, + bitSize=4, + bitOffset=0, + base=pr.UInt, + disp='{:#b}')) + + self.add(pr.RemoteVariable( + name="DevIndexMask[3:0]", + offset=0x14, + bitSize=4, + bitOffset=0, + base=pr.UInt, + disp='{:#b}')) + + self.add(pr.RemoteVariable( + name="DevIndexMask[DCO:FCO]", + offset=0x14, + bitSize=2, + bitOffset=0x4, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="OutputTestMode", + description='Set output test mode.', + offset=0x34, + bitSize=4, + bitOffset=0, + base=pr.UInt, + enum={ + 0: "Off", + 1: "Midscale Short", + 2: "Positive FS", + 3: "Negative FS", + 4: "Alternating checkerboard", + 5: "PN23", + 6: "PN9", + 7: "1/0-word toggle", + 8: "User Input", + 9: "1/0-bit Toggle", + 10: "1x sync", + 11: "One bit high", + 12: "mixed bit frequency"})) + + self.add(pr.RemoteVariable( + name='ResetPNShort', + description='Reset PN short gen test mode', + offset=0x34, + bitSize=1, + bitOffset=4, + base=pr.Bool)) + + self.add(pr.RemoteVariable( + name='ResetPNLongReg', + description='Reset PN long gen test mode', + offset=0x34, + bitSize=1, + bitOffset=5, + base=pr.Bool, + hidden=True)) + + + self.add(pr.RemoteVariable( + name='UserTestMode', + description='Sets user test mode of all channels', + offset=0x34, + bitSize=2, + bitOffset=6, + base=pr.UInt, + enum={ + 0: 'Off', + 1: 'OnSingAlternate', + 2: 'OnSingleOnce', + 3: 'OnAlternateOnce'})) + + + self.add(pr.RemoteVariable( + name='OutputFormat', + description='Set output format. binary or twos complement.', + offset=0x50, + bitSize=2, + bitOffset=0, + base=pr.UInt, + enum={ + 1: 'Twos Compliment', + 0: 'Offset Binary'})) + + self.add(pr.RemoteVariable( + name='OutputInvert', + description='Enable output inversion.', + offset=0x50, + bitSize=1, + bitOffset=2, + base=pr.Bool)) + + self.add(pr.RemoteVariable( + name='OutputMode', + description='Set output mode of device. Default=LVDS.', + offset=0x50, + bitSize=1, + bitOffset=6, + base=pr.UInt, + enum={ + 0:'LVDS ANSI-644', + 1:'LVDS Low Power'})) + + self.add(pr.RemoteVariable( + name='DcoFcoDrive2x', + description='Set DCO and FCO output drive strength.', + offset=0x54, + bitSize=1, + bitOffset=0, + base = pr.Bool)) + + + self.add(pr.RemoteVariable( + name='OutputTermDrive', + description='Set output driver termination.', + offset=0x54, + bitSize=2, + bitOffset=4, + base = pr.UInt, + enum={ + 0:'none', + 1:'200 Ohms', + 2:'100 Ohms', + 3:'100 Ohms'})) + + self.add(pr.RemoteVariable( + name='OutputPhase', + description='Set output phase adjustment.', + offset=0x58, + bitSize=4, + bitOffset=0, + base = pr.UInt, + enum={ + 0:'0 deg to edge', + 1:'60 deg to edge', + 2:'120 deg to edge', + 3:'180 deg to edge', + 4:'unused1', + 5:'300 deg to edge', + 6:'360 deg to edge', + 7:'unused2', + 8:'480 deg to edge', + 9:'540 deg to edge', + 10:'600 deg to edge', + 11:'660 deg to edge'})) + +# def convPattern(raw): +# def convert(): +# return raw.value() +# return convert + + self.add(pr.RemoteVariable( + name="UserPattern1Lsb", + offset=0x64, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern1Msb", + offset=0x68, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern2Lsb", + offset=0x6C, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + self.add(pr.RemoteVariable( + name="UserPattern2Msb", + offset=0x70, + bitSize=8, + bitOffset=0, + base=pr.UInt)) + + # self.add(pr.LinkVariable( +# name='UserPattern1', +# description='Set user test pattern 1 data.', +# linkedGet=convPattern(self.UserPattern1Raw), +# dependencies=[self.UserPattern1Raw])) + +# self.add(pr.LinkVariable( +# name='UserPattern2', +# description='Set user test pattern 2 data.', +# linkedGet=convPattern(self.UserPattern2Raw), +# dependencies=[self.UserPattern2Raw])) + + self.add(pr.RemoteVariable( + name='SerialBits', + description='Set number of serial bits.', + offset=0x84, + bitSize=3, + bitOffset=0, + base = pr.UInt, + enum={ + 0:'14 bits', + 1:'8 bits', + 2:'10 bits', + 3:'12 bits', + 4:'14 bits'})) + + self.add(pr.RemoteVariable( + name='LowEncodeRate', + description='Set low rate less than 10mbs mode.', + offset=0x84, + bitSize=1, + bitOffset=3, + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name='SerialLsbFirst', + description='Set LSB first mode of device.', + offset=0x84, + bitSize=1, + bitOffset=7, + base = pr.Bool)) + + + self.add(pr.RemoteVariable( + name='ChPowerDown', + description='Set channel power down.', + offset=0x88, + bitSize=1, + bitOffset=0, + base = pr.Bool)) + + self.add(pr.RemoteCommand( + name='DeviceUpdate', + description='Transfers buffered SPI register values into the ADC', + offset=0x3FC, + function=pr.BaseCommand.touchOne, + )) + + analog_devices.addAdcDdrResetCommands( + self, self.PowerDownMode, self.ResetPNLongReg, self.DeviceUpdate) + + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() + + +class Ad9252Readout(analog_devices.AdcDdr): + """Normalized AD9252 readout with one data lane per enabled channel. + + Parameters + ---------- + channels : int, optional + Number of ADC channels exposed by the firmware. + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + channels: int = 8, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, + **kwargs: Any) -> None: + """Create the normalized AD9252 readout.""" + + if not 1 <= channels <= 8: + raise ValueError('channels must be from 1 through 8') + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'AD9252 serialized DDR readout') + super().__init__( + dataLanes = channels, + fcoLanes = 1, + channels = channels, + sampleBits = 14, + serializationFactor = 14, + delayBits = delayBits, + patternCheck = patternCheck, + **kwargs) + + +class Ad9252ReadoutCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for a normalized AD9252 readout. + + Parameters + ---------- + config : Ad9252Config + ADC configuration device. + readout : Ad9252Readout + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9252Config, + readout: Ad9252Readout, + **kwargs: Any) -> None: + """Create the AD9252 calibration process.""" + + if not isinstance(config, Ad9252Config): + raise TypeError('config must be an Ad9252Config') + if not isinstance(readout, Ad9252Readout): + raise TypeError('readout must be an Ad9252Readout') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(readout._channels)), + configUpdate = config.DeviceUpdate, + **kwargs) diff --git a/python/surf/devices/analog_devices/_Ad9681.py b/python/surf/devices/analog_devices/_Ad9681.py index 7a0618e299..ec74a0fdf6 100644 --- a/python/surf/devices/analog_devices/_Ad9681.py +++ b/python/surf/devices/analog_devices/_Ad9681.py @@ -1,8 +1,8 @@ #----------------------------------------------------------------------------- -# Title : PyRogue _ad9249 Module +# Title : PyRogue AD9681 model #----------------------------------------------------------------------------- # Description: -# PyRogue _ad9249 Module +# AD9681 configuration, normalized readout, and calibration models. #----------------------------------------------------------------------------- # This file is part of 'SLAC Firmware Standard Library'. # It is subject to the license terms in the LICENSE.txt file found in the @@ -13,18 +13,34 @@ # the terms contained in the LICENSE.txt file. #----------------------------------------------------------------------------- +from __future__ import annotations + +from typing import Any + import pyrogue as pr -import rogue.interfaces.memory as rim -# import math + +import surf.devices.analog_devices as analog_devices class Ad9681Config(pr.Device): - def __init__(self, - description = 'Configure one side of an AD9249 ADC', - **kwargs): + """PyRogue configuration model for the AD9681 ADC. + + Parameters + ---------- + description : str, optional + PyRogue device description. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + description: str = 'Configure an AD9681 ADC', + **kwargs: Any) -> None: + """Create the AD9681 configuration model.""" super().__init__(description=description, **kwargs) - # AD9249 bank configuration registers + # AD9681 configuration registers self.add(pr.RemoteVariable( name = 'ChipId', description = 'ADC chip identification register', @@ -171,6 +187,27 @@ def __init__(self, }, )) + self.add(pr.RemoteVariable( + name = 'ResetPNShort', + description = 'Reset the PN9 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 4, + base = pr.Bool, + mode = 'RW', + )) + + self.add(pr.RemoteVariable( + name = 'ResetPNLongReg', + description = 'Reset the PN23 test-pattern generator', + offset = (0x0D*4), + bitSize = 1, + bitOffset = 5, + base = pr.Bool, + mode = 'RW', + hidden = True, + )) + self.add(pr.RemoteVariable( name = 'OffsetAdjust', description = 'Output offset adjustment in LSB steps', @@ -347,383 +384,130 @@ def __init__(self, }, )) + # Register 0x100 (resolution/sample-rate override) is a transfer-staged + # register: the datasheet specifies it is not applied until the 0xFF + # transfer strobe, and a read returns the old value until then. That + # breaks pyrogue's immediate write-verify, and the override is not needed + # for normal full-rate operation, so these fields are disabled for now. + # Re-enable with verify=False (and route through DeviceUpdate) if the + # override is ever required. + # self.add(pr.RemoteVariable( + # name = 'ResolutionSampleRateOverride', + # description = 'Enables the resolution and maximum sample-rate override', + # offset = (0x100*4), + # bitSize = 1, + # bitOffset = 6, + # base = pr.Bool, + # )) + + # self.add(pr.RemoteVariable( + # name = 'Resolution', + # description = 'Selects ADC resolution when the override is enabled', + # offset = (0x100*4), + # bitSize = 2, + # bitOffset = 4, + # enum = { + # 0b00: 'Default (14 bits)', # power-up/reset value; effective 14-bit + # 0b01: '14 bits', + # 0b10: '12 bits', + # }, + # )) + + # self.add(pr.RemoteVariable( + # name = 'SampleRate', + # description = 'Selects maximum ADC sample rate when the override is enabled', + # offset = (0x100*4), + # bitSize = 3, + # bitOffset = 0, + # enum = { + # 0b000: '20 MSPS', + # 0b001: '40 MSPS', + # 0b010: '50 MSPS', + # 0b011: '65 MSPS', + # 0b100: '80 MSPS', + # 0b101: '105 MSPS', + # 0b110: '125 MSPS', + # }, + # )) + self.add(pr.RemoteCommand( name='DeviceUpdate', - description='Transfers SPI register values to internal device shadow registers', + description='Transfers the resolution/sample-rate override into the ADC', offset=0x3FC, - function=pr.BaseCommand.touchZero, - )) - - def writeBlocks(self, force=False, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - pr.Device.writeBlocks(self, force=force, recurse=True, variable=variable, checkEach=checkEach, index=index) - self.DeviceUpdate() - - - - -class Ad9681ReadoutManual(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - - for ch in range(channels): - for i in range(2): - self.add(pr.RemoteVariable( - name = f'ChannelDelay[{ch}][{i}]', - description = f'IDELAY value for serial channel {ch}_{i}', - offset = ch*8 + i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'FrameDelay[{i}]', - description = f'IDELAY value for FCO_{i}', - offset = 0x40 + i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - @self.command() - def AllDelay0(arg): - self.FrameDelay[0].set(arg) - for ch in range(8): - self.ChannelDelay[ch][0].set(arg) - - @self.command() - def AllDelay1(arg): - self.FrameDelay[1].set(arg) - for ch in range(8): - self.ChannelDelay[ch][1].set(arg) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'LostLockCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50+ 4*i, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Locked[{i}]', - description = 'Readout has locked on to the frame boundary', - offset = 0x50+ 4*i, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'AdcFrameSync[{i}]', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 8, - bitOffset = i*8, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'Negate', - description = "Optional ADC data negation (two's complement)", - offset = 0x60, - bitSize = 1, - bitOffset = 1, - base = pr.Bool, - mode = 'RW', + function=pr.BaseCommand.touchOne, )) + analog_devices.addAdcDdrResetCommands( + self, self.InternalPdwnMode, self.ResetPNLongReg) - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, - bitOffset = 0, - )) - - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x70, - bitSize=2, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.createToggle([0, 3, 0]))) - - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - freeze = False #isinstance(variable, list) and any(v.name.startswith('AdcChannel') for v in variable) - if freeze: - self.FreezeDebug(1) - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - if freeze: - self.FreezeDebug(0) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) - -class Ad9681Readout(pr.Device): - def __init__(self, - name = 'Ad9249Readout', - description = 'Configure readout of 1 bank of an AD9249', - fpga = '7series', - channels = 8, - **kwargs): - - assert (channels > 0 and channels <= 8), f'channels ({channels}) must be between 0 and 8' - super().__init__(name=name, description=description, **kwargs) - - if fpga == '7series': - delayBits = 6 - elif fpga == 'ultrascale': - delayBits = 10 - else: - delayBits = 6 - - self.add(pr.RemoteVariable( - name = 'EnUsrDelay', - description = 'Enable manual delay value', - offset = 0x20, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - verify = True, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Delay[{i}]', - description = f'IDELAY value for serial channel {i}', - offset = i*4, - bitSize = delayBits, - bitOffset = 0, - base = pr.UInt, - mode = 'RW', - verify = False, - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'ErrorDetCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x30+ 4*i, - disp = '{:d}', - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'LostLockCount[{i}]', - description = 'Number of times that frame lock has been lost since reset', - offset = 0x50+ 4*i, - bitSize = 16, - bitOffset = 0, - base = pr.UInt, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'Locked[{i}]', - description = 'Readout has locked on to the frame boundary', - offset = 0x50+ 4*i, - bitSize = 1, - bitOffset = 16, - base = pr.Bool, - mode = 'RO', - )) - - for i in range(2): - self.add(pr.RemoteVariable( - name = f'AdcFrameSync[{i}]', - description = 'Last deserialized FCO value for debug', - offset = 0x58, - bitSize = 8, - bitOffset = i*8, - base = pr.UInt, - mode = 'RO', - )) - - self.add(pr.RemoteVariable( - name = 'Invert', - description = 'Optional ADC data inversion (offset binary only)', - offset = 0x60, - bitSize = 1, - bitOffset = 0, - base = pr.Bool, - mode = 'RW', - )) - - self.add(pr.RemoteVariable( - name = 'Negate', - description = "Optional ADC data negation (two's complement)", - offset = 0x60, - bitSize = 1, - bitOffset = 1, - base = pr.Bool, - mode = 'RW', - )) - - - for i in range(channels): - self.add(pr.RemoteVariable( - name = f'AdcChannel[{i:d}]', - description = f'Last deserialized channel {i:d} ADC value for debug', - offset = 0x80 + (i*4), - bitSize = 32, - bitOffset = 0, - base = pr.UInt, - disp = '{:09_x}', - mode = 'RO', - )) - - for i in range(channels): - self.add(pr.LinkVariable( - name = f'AdcVoltage[{i}]', - description = f'Converted voltage for ADC channel {i}', - mode = 'RO', - disp = '{:1.9f}', - variable = self.AdcChannel[i], - linkedGet = lambda read, check, r=self.AdcChannel[i]: 2*pr.twosComplement(r.get(read=read, check=check)>>18, 14)/2**14, - units = 'V')) - - self.add(pr.RemoteCommand( - name = 'LostLockCountReset', - description = 'Reset LostLockCount', - function = pr.BaseCommand.toggle, - offset = 0x5C, - bitSize = 1, - bitOffset = 0, - )) + def writeBlocks(self, **kwargs: Any) -> None: + """Write pending blocks and transfer them into the ADC.""" + super().writeBlocks(**kwargs) + self.DeviceUpdate() - self.add(pr.RemoteCommand( - name='FreezeDebug', - description='Freeze all of the AdcChannel registers', - hidden=True, - offset=0xA0, - bitSize=1, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.touch)) - self.add(pr.RemoteCommand( - name='Relock', - description='Triggers ADC readout relock sequence', - hidden=False, - offset=0x70, - bitSize=2, - bitOffset=0, - base=pr.UInt, - function=pr.RemoteCommand.createToggle([0, 3, 0]))) - - - def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs): - """ - Perform background reads - """ - checkEach = checkEach or self.forceCheckEach - - if variable is not None: - pr.startTransaction(variable._block, type=rim.Read, checkEach=checkEach, variable=variable, index=index, **kwargs) - - else: - self.FreezeDebug(1) - for block in self._blocks: - if block.bulkOpEn: - pr.startTransaction(block, type=rim.Read, checkEach=checkEach, **kwargs) - self.FreezeDebug(0) - - if recurse: - for key,value in self.devices.items(): - value.readBlocks(recurse=True, checkEach=checkEach, **kwargs) +class Ad9681Readout(analog_devices.AdcDdr): + """Normalized register model for the eight-channel AD9681 readout. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'}, optional + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. + **kwargs : Any + Additional arguments forwarded to ``AdcDdr``. + """ + + def __init__( + self, + *, + deviceFamily: analog_devices.AdcDdrDeviceFamily = 'ULTRASCALE', + patternCheck: bool = True, + **kwargs: Any) -> None: + """Create the normalized AD9681 readout.""" + + delayBits = analog_devices.adcDdrDelayBits(deviceFamily) + kwargs.setdefault('description', 'AD9681 serialized DDR readout') + super().__init__( + dataLanes = 16, + fcoLanes = 2, + channels = 8, + sampleBits = 14, + serializationFactor = 8, + delayBits = delayBits, + patternCheck = patternCheck, + **kwargs) + + +class Ad9681ReadoutCalibration(analog_devices.AdcDdrCalibration): + """Calibration process for a normalized AD9681 readout. + + Parameters + ---------- + config : Ad9681Config + ADC configuration device. + readout : Ad9681Readout + Normalized readout to calibrate. + **kwargs : Any + Additional arguments forwarded to ``AdcDdrCalibration``. + """ + + def __init__( + self, + *, + config: Ad9681Config, + readout: Ad9681Readout, + **kwargs: Any) -> None: + """Create the AD9681 calibration process.""" + + if not isinstance(config, Ad9681Config): + raise TypeError('config must be an Ad9681Config') + if not isinstance(readout, Ad9681Readout): + raise TypeError('readout must be an Ad9681Readout') + super().__init__( + config = config, + readout = readout, + dataLaneToChannel = tuple(range(8))+tuple(range(8)), + dataLaneMasks = (0x003F,)*8+(0x3FC0,)*8, + **kwargs) diff --git a/python/surf/devices/analog_devices/_AdcDdr.py b/python/surf/devices/analog_devices/_AdcDdr.py new file mode 100644 index 0000000000..c716166672 --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdr.py @@ -0,0 +1,516 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC readout +#----------------------------------------------------------------------------- +# Description: +# PyRogue model for the normalized AdcDdr capture and monitoring register map. +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any, Literal, Mapping, Sequence + +import pyrogue as pr + +import surf.devices.analog_devices as analog_devices + + +AdcDdrDeviceFamily = Literal['7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'] + + +def adcDdrDelayBits(deviceFamily: AdcDdrDeviceFamily) -> int: + """Return the native input-delay width for an FPGA device family. + + Parameters + ---------- + deviceFamily : {'7SERIES', 'ULTRASCALE', 'ULTRASCALE_PLUS'} + FPGA device family selected by RTL ``DEVICE_FAMILY_G``. + + Returns + ------- + int + Five for 7-Series or nine for UltraScale and UltraScale+. + + Raises + ------ + ValueError + If ``deviceFamily`` is not supported by the AdcDdr PHY. + """ + + try: + return { + '7SERIES': 5, + 'ULTRASCALE': 9, + 'ULTRASCALE_PLUS': 9, + }[deviceFamily] + except KeyError as exc: + raise ValueError( + 'deviceFamily must be 7SERIES, ULTRASCALE, or ULTRASCALE_PLUS') from exc + + +def _formatDebugSnapshot(samples: Sequence[int], sampleBits: int) -> str: + """Format one debug snapshot as grouped hexadecimal ADC samples.""" + + width = (sampleBits+3)//4 + mask = (1 << sampleBits)-1 + return '0x' + '_'.join(f'{sample & mask:0{width}X}' for sample in samples) + + +def _convertDebugVoltage( + sample: int, + sampleBits: int, + inputRange: float, + offsetBinary: bool) -> float: + """Convert one ADC code to a signed voltage.""" + + modulus = 1 << sampleBits + sign = 1 << (sampleBits-1) + mask = modulus-1 + code = sample & mask + if offsetBinary: + signed = code-sign + elif code & sign: + signed = code-modulus + else: + signed = code + return inputRange*signed/modulus + + +class AdcDdr(pr.Device): + """PyRogue model for a normalized serialized DDR ADC readout. + + Parameters + ---------- + dataLanes : int, optional + Number of serialized ADC data lanes. + fcoLanes : int, optional + Number of serialized frame-clock lanes. + channels : int, optional + Number of logical ADC channels. + sampleBits : int, optional + Number of meaningful bits in each ADC sample. + serializationFactor : int, optional + Number of serialized bits captured per data lane. + delayBits : int, optional + Width of each programmable input-delay value. + patternCheck : bool, optional + Whether RTL ``PATTERN_CHECK_G`` includes the hardware pattern tester. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def checkGeometry(self) -> None: + """Verify the model's construction matches the RTL capability registers. + + The register map's field widths and scan ranges are fixed at + construction from generics such as ``deviceFamily``. If those do not + match the geometry the RTL actually built, individual accesses fail far + from the cause -- a too-wide ``delayBits`` model, for example, lets a + scan program a tap the hardware register cannot hold, and only the + readback verify deep inside a calibration reports it. Reading the + normalized read-only capability registers here turns that into one + explicit, early error that names the mismatched field. + + Raises + ------ + RuntimeError + If any advertised capability disagrees with this model. + """ + + expected = { + 'DataLanes': self._dataLanes, + 'FcoLanes': self._fcoLanes, + 'Channels': self._channels, + 'SampleBits': self._sampleBits, + 'SerializationFactor': self._serializationFactor, + 'DelayBits': self._delayBits, + 'PatternCheck': self._patternCheck, + } + mismatches = [] + for name, modelValue in expected.items(): + hwValue = int(self.node(name).get(read=True)) + if hwValue != modelValue: + mismatches.append(f'{name}: model={modelValue}, hardware={hwValue}') + if mismatches: + raise RuntimeError( + f'{self.path}: AdcDdr register model does not match the RTL ' + 'capability registers; check the deviceFamily, patternCheck, ' + 'and other construction generics against the firmware. ' + 'Mismatches -- ' + + '; '.join(mismatches)) + + def _getDataDelays(self, read: bool) -> list[int]: + """Return all data-lane delay values.""" + + return [int(delay) for delay in self.DataDelayBulk.get(read=read)] + + def _setDataDelays(self, values: Mapping[int, int]) -> None: + """Set and verify a collection of data-lane delay values.""" + + for lane, value in sorted(values.items()): + self.DataDelayBulk.set(value, index=lane, write=False) + self.writeAndVerifyBlocks( + recurse = False, + variable = self.DataDelayBulk) + + def _getDebugSamples(self, read: bool) -> list[list[int]]: + """Return four debug samples for every logical channel.""" + + flat = self.DebugSampleRaw.get(read=read) + return [ + [int(sample) for sample in flat[4*channel:4*(channel+1)]] + for channel in range(self._channels) + ] + + def _snapshot(self, cmd: Any) -> list[str]: + """Trigger and format one coherent debug snapshot.""" + + cmd.set(1) + self._getDebugSamples(read=True) + return [ + self.nodes[f'DebugSample[{channel}]'].get(read=False) + for channel in range(self._channels) + ] + + def __init__(self, *, + dataLanes: int = 8, + fcoLanes: int = 1, + channels: int = 8, + sampleBits: int = 14, + serializationFactor: int = 14, + delayBits: int = 5, + patternCheck: bool = True, + **kwargs: Any) -> None: + """Create the normalized ADC readout model.""" + + for name, value, minimum, maximum in ( + ('dataLanes', dataLanes, 1, 64), + ('fcoLanes', fcoLanes, 1, 16), + ('channels', channels, 1, 16), + ('sampleBits', sampleBits, 2, 16), + ('serializationFactor', serializationFactor, 1, 16), + ('delayBits', delayBits, 1, 9)): + if not minimum <= value <= maximum: + raise ValueError(f'{name} must be from {minimum} through {maximum}') + + kwargs.setdefault('description', 'Serialized DDR ADC capture and monitoring') + super().__init__(**kwargs) + + self._dataLanes = dataLanes + self._fcoLanes = fcoLanes + self._channels = channels + self._sampleBits = sampleBits + self._serializationFactor = serializationFactor + self._delayBits = delayBits + self._patternCheck = patternCheck + + self.add(pr.RemoteVariable( + name = 'Version', + description = 'Normalized AdcDdr register-map version', + offset = 0x000, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:#010x}')) + + self.add(pr.RemoteVariable( + name = 'DataLanes', + description = 'Number of serialized ADC data lanes', + offset = 0x004, + bitSize = 8, + bitOffset = 0, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'FcoLanes', + description = 'Number of serialized ADC frame-clock lanes', + offset = 0x004, + bitSize = 8, + bitOffset = 8, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'Channels', + description = 'Number of logical ADC channels', + offset = 0x004, + bitSize = 8, + bitOffset = 16, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'SampleBits', + description = 'Number of meaningful bits in each ADC sample', + offset = 0x004, + bitSize = 8, + bitOffset = 24, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'DelayBits', + description = 'Width of each programmable input-delay value', + offset = 0x008, + bitSize = 8, + bitOffset = 0, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'SerializationFactor', + description = 'Number of serialized bits captured per data lane', + offset = 0x008, + bitSize = 8, + bitOffset = 8, + mode = 'RO', + base = pr.UInt, + disp = '{:d}', + hidden = True)) + + self.add(pr.RemoteVariable( + name = 'PatternCheck', + description = 'Hardware pattern measurement engine is present', + offset = 0x008, + bitSize = 1, + bitOffset = 16, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'CaptureReset', + description = 'Manually hold the ADC PHY and capture behavior in reset', + offset = 0x00C, + bitSize = 1, + bitOffset = 0, + mode = 'RW', + base = pr.Bool)) + + self.add(pr.RemoteCommand( + name = 'Relock', + description = 'Restart FCO word alignment without changing delays', + offset = 0x010, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.LocalVariable( + name = 'DebugVoltageRange', + description = 'Differential full-scale input range used for debug voltage conversion', + value = 2.0, + minimum = 0.0, + units = 'V', + disp = '{:1.6f}', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DebugVoltageFormat', + description = 'ADC output coding used for debug voltage conversion', + value = 1, + enum = { + 0: 'Offset Binary', + 1: 'Twos Complement', + }, + mode = 'RW')) + + self.add(pr.RemoteCommand( + name = 'Snapshot', + description = 'Block until four coherent debug samples are captured, then read them', + offset = 0x014, + bitSize = 1, + bitOffset = 0, + function = self._snapshot)) + + self.add(pr.RemoteCommand( + name = 'ClearCounters', + description = 'Clear event counters and sticky overflow status', + offset = 0x018, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteVariable( + name = 'DelayReady', + description = 'Input-delay controller is ready; low holds the capture PHY in reset', + offset = 0x01C, + bitSize = 1, + bitOffset = 1, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllLocked', + description = 'All FCO lanes are word aligned', + offset = 0x01C, + bitSize = 1, + bitOffset = 2, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AnyOverflow', + description = 'One or more coherent samples were dropped', + offset = 0x01C, + bitSize = 1, + bitOffset = 3, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'LockedMask', + offset = 0x020, + bitSize = fcoLanes, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'SnapshotSequence', + description = 'Completed atomic debug-snapshot count', + offset = 0x024, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(analog_devices.AdcDdrPatternTester( + name = 'PatternTester', + offset = 0x800, + channels = channels, + fcoLanes = fcoLanes, + sampleBits = sampleBits, + expand = False)) + + self.add(pr.RemoteVariable( + name = 'DataDelayBulk', + description = 'Array of programmed input-delay values for all serialized data lanes', + offset = 0x100, + bitSize = 32*dataLanes, + valueBits = delayBits, + numValues = dataLanes, + valueStride = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = (1 << delayBits) - 1, + hidden = True)) + + for lane in range(dataLanes): + self.add(pr.LinkVariable( + name = f'DataDelay[{lane}]', + description = f'Programmed input-delay value for serialized data lane {lane}', + variable = self.DataDelayBulk, + linkedGet = lambda read, check, lane=lane: self.DataDelayBulk.get( + index=lane, read=read, check=check), + linkedSet = lambda value, write, verify, check, lane=lane: + self.DataDelayBulk.set( + value, + index=lane, + write=write, + verify=verify, + check=check))) + + self.addRemoteVariables( + name = 'FcoDelay', + description = 'Programmed input-delay value for each FCO lane', + number = fcoLanes, + stride = 4, + offset = 0x200, + bitSize = delayBits, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = (1 << delayBits) - 1) + + self.addRemoteVariables( + name = 'FcoWord', + description = 'Most recent deserialized FCO word', + number = fcoLanes, + stride = 4, + offset = 0x300, + bitSize = serializationFactor, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}') + + self.addRemoteVariables( + name = 'LostLockCount', + number = fcoLanes, + stride = 4, + offset = 0x340, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') + + self.add(pr.RemoteVariable( + name = 'OverflowCount', + description = 'Saturating coherent sample-group FIFO overflow count', + offset = 0x500, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'DebugSampleRaw', + description = ( + 'Flattened channel-major raw pre-format atomic samples; ' + 'four oldest-to-newest samples per logical channel'), + offset = 0x600, + bitSize = 32*4*channels, + valueBits = sampleBits, + numValues = 4*channels, + valueStride = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}', + hidden = True)) + + for channel in range(channels): + self.add(pr.LinkVariable( + name = f'DebugSample[{channel}]', + description = ( + f'Raw pre-format atomic samples for logical channel {channel}; ' + 'oldest to newest'), + mode = 'RO', + dependencies = [self.DebugSampleRaw], + linkedGet = lambda read, channel=channel: _formatDebugSnapshot( + self._getDebugSamples(read=read)[channel], sampleBits))) + + for channel in range(channels): + self.add(pr.LinkVariable( + name = f'DebugVoltage[{channel}]', + description = f'Differential input voltage for logical channel {channel}', + mode = 'RO', + units = 'V', + disp = '{:1.6f}', + dependencies = [ + self.DebugSampleRaw, + self.DebugVoltageRange, + self.DebugVoltageFormat, + ], + linkedGet = lambda read, channel=channel: _convertDebugVoltage( + self._getDebugSamples(read=read)[channel][0], + sampleBits, + self.DebugVoltageRange.get(read=read), + self.DebugVoltageFormat.get(read=read) == 0))) diff --git a/python/surf/devices/analog_devices/_AdcDdrCalibration.py b/python/surf/devices/analog_devices/_AdcDdrCalibration.py new file mode 100644 index 0000000000..ea4661e5b0 --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdrCalibration.py @@ -0,0 +1,1544 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC calibration helpers +#----------------------------------------------------------------------------- +# Description: +# Device-neutral eye analysis and calibration result types for AdcDdr. +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +import copy +from dataclasses import asdict, dataclass +import itertools +import threading +import time +from typing import Any, Callable, Iterable, Mapping, Sequence + +import pyrogue as pr + + +class _AdcDdrCalibrationStopped(Exception): + """Internal control flow for a user-requested process stop.""" + + +def addAdcDdrResetCommands( + device: Any, + powerMode: Any, + pn23Reset: Any, + configUpdate: Callable[[], None] | None = None) -> None: + """Add normalized digital and PN23 reset commands to an ADC config device.""" + + def digitalReset() -> None: + powerMode.setDisp('Digital Reset', write=True) + try: + if configUpdate is not None: + configUpdate() + # Retain a small explicit hold in addition to the blocking SPI + # transactions that assert and release the reset state. + time.sleep(0.001) + finally: + powerMode.setDisp('Chip Run', write=True) + if configUpdate is not None: + configUpdate() + + def resetPnLong() -> None: + pn23Reset.set(True, write=True) + try: + if configUpdate is not None: + configUpdate() + finally: + pn23Reset.set(False, write=True) + if configUpdate is not None: + configUpdate() + + device.add(pr.LocalCommand( + name = 'DigitalReset', + description = 'Pulse the ADC digital datapath reset and return to normal operation', + function = digitalReset)) + + device.add(pr.LocalCommand( + name = 'ResetPNLong', + description = 'Synchronously restart the ADC PN23 test-pattern generator', + function = resetPnLong)) + + +@dataclass(frozen=True) +class AdcDdrEye: + """One contiguous passing delay window and its chosen sampling tap. + + Parameters + ---------- + start : int + First passing tap in the window. + end : int + Last passing tap in the window. + width : int + Number of passing taps in the window. + selected : int + Center tap selected for sampling. + leftMargin : int + Passing taps between ``selected`` and ``start``. + rightMargin : int + Passing taps between ``selected`` and ``end``. + wraps : bool + Whether the window crosses the end of a circular delay range. + leftBounded : bool + Whether a failing tap was observed beyond the left edge. + rightBounded : bool + Whether a failing tap was observed beyond the right edge. + """ + + start: int + end: int + width: int + selected: int + leftMargin: int + rightMargin: int + wraps: bool + leftBounded: bool + rightBounded: bool + + def contains(self, tap: int) -> bool: + """Return whether ``tap`` lies inside this eye, including wraparound.""" + + if self.wraps: + return tap >= self.start or tap <= self.end + return self.start <= tap <= self.end + + def asDict(self) -> dict[str, Any]: + """Return a PyRogue-friendly dictionary representation.""" + + return asdict(self) + + +def findAdcDdrEyes( + passing: Mapping[int, bool], + *, + minimumWidth: int = 1, + guardBand: int = 0, + circular: bool = False) -> tuple[AdcDdrEye, ...]: + """Return every qualifying window from a consecutive tap scan. + + Parameters + ---------- + passing : Mapping[int, bool] + Pass/fail result for each consecutively scanned delay tap. + minimumWidth : int, optional + Minimum accepted eye width in taps. + guardBand : int, optional + Required passing taps on both sides of the selected center. + circular : bool, optional + Merge passing runs at the high and low scan boundaries. + + Returns + ------- + tuple[AdcDdrEye, ...] + Qualifying eyes ordered by decreasing width and increasing center tap. + + Raises + ------ + ValueError + If the scan controls or tap domain are invalid. + TypeError + If a delay tap index is not an integer. + RuntimeError + If the scan contains no qualifying passing window. + + Notes + ----- + For an even-width window, the lower of its two center positions is used. + """ + + if minimumWidth < 1: + raise ValueError('minimumWidth must be at least one tap') + if guardBand < 0: + raise ValueError('guardBand must not be negative') + if not passing: + raise ValueError('passing scan must not be empty') + + # A contiguous numeric domain makes physical edges and margins meaningful. + # Reject sparse maps rather than accidentally treating an untested gap as + # adjacent delay taps. + taps = sorted(passing) + if any(not isinstance(tap, int) for tap in taps): + raise TypeError('delay tap indices must be integers') + if any(right != left+1 for left, right in zip(taps, taps[1:])): + raise ValueError('passing scan tap indices must be consecutive') + + # Collapse the boolean scan into maximal passing runs. Keeping every run is + # important for FCO calibration because different unit intervals can each + # contain a valid eye even though only one can be the initial selection. + runs = [] + run = [] + for tap in taps: + if passing[tap]: + run.append(tap) + elif run: + runs.append(run) + run = [] + if run: + runs.append(run) + + if not runs: + raise RuntimeError('scan contains no passing delay window') + + scanStart = taps[0] + scanEnd = taps[-1] + # On circular delay elements, the high-end and low-end runs are two pieces + # of one physical eye. Preserve their high-to-low ordering so start > end is + # the explicit wrapped-eye representation used by AdcDdrEye.contains(). + if circular and len(runs) > 1 and runs[0][0] == scanStart and runs[-1][-1] == scanEnd: + runs = runs[1:-1] + [runs[-1] + runs[0]] + + # GuardBand is a center-margin requirement, so it implies a minimum total + # width even when MinimumEyeWidth is configured to a smaller value. + requiredWidth = max(minimumWidth, (2*guardBand)+1) + eyes = [] + for candidate in runs: + if len(candidate) < requiredWidth: + continue + centerIndex = (len(candidate)-1)//2 + start = candidate[0] + end = candidate[-1] + wraps = start > end + eyes.append(AdcDdrEye( + start = start, + end = end, + width = len(candidate), + selected = candidate[centerIndex], + leftMargin = centerIndex, + rightMargin = len(candidate)-centerIndex-1, + wraps = wraps, + leftBounded = wraps or start != scanStart, + rightBounded = wraps or end != scanEnd)) + + if not eyes: + raise RuntimeError( + f'scan contains no passing delay window at least {requiredWidth} taps wide') + + # This ordering makes itertools.product() try the historical preferred eye + # combination first, followed by progressively less-preferred alternatives. + return tuple(sorted(eyes, key=lambda eye: (-eye.width, eye.selected))) + + +def findAdcDdrEye( + passing: Mapping[int, bool], + *, + minimumWidth: int = 1, + guardBand: int = 0, + circular: bool = False) -> AdcDdrEye: + """Select the highest-priority qualifying eye from a tap scan. + + Parameters + ---------- + passing : Mapping[int, bool] + Pass/fail result for each consecutively scanned delay tap. + minimumWidth : int, optional + Minimum accepted eye width in taps. + guardBand : int, optional + Required passing taps on both sides of the selected center. + circular : bool, optional + Merge passing runs at the high and low scan boundaries. + + Returns + ------- + AdcDdrEye + Highest-priority qualifying eye. + """ + + return findAdcDdrEyes( + passing, + minimumWidth=minimumWidth, + guardBand=guardBand, + circular=circular)[0] + + +def checkAdcDdrPn23(samples: Sequence[int], sampleBits: int) -> dict[str, Any]: + """Check an arbitrary-phase, MSB-first PN23 sample sequence. + + Parameters + ---------- + samples : Sequence[int] + Complete logical ADC samples in capture order. + sampleBits : int + Number of meaningful bits in each sample. + + Returns + ------- + dict[str, Any] + Pass status and diagnostics for each tested output transformation. + + Raises + ------ + ValueError + If ``sampleBits`` is invalid or too few bits were captured. + + Notes + ----- + The first 23 captured bits establish the unknown LFSR phase. Remaining + bits must satisfy ``x^23 + x^18 + 1``. All four combinations of output and + format inversion are considered explicitly. + """ + + if sampleBits < 1: + raise ValueError('sampleBits must be positive') + if len(samples)*sampleBits <= 23: + raise ValueError('PN23 verification requires more than 23 captured bits') + + sampleMask = (1 << sampleBits)-1 + transformations = ( + ('asCaptured', 0), + ('formatMsbInverted', 1 << (sampleBits-1)), + ('outputInverted', sampleMask), + ('outputAndFormatInverted', sampleMask ^ (1 << (sampleBits-1))), + ) + attempts = [] + for name, xorMask in transformations: + # ADC PN words are serialized MSB first. Flatten complete logical words + # in that same order so word boundaries disappear for the recurrence. + bits = [ + ((sample ^ xorMask) >> bit) & 1 + for sample in samples + for bit in range(sampleBits-1, -1, -1) + ] + initialState = sum(bit << (22-index) for index, bit in enumerate(bits[:23])) + errorBits = [ + index + for index in range(23, len(bits)) + if bits[index] != (bits[index-23] ^ bits[index-18]) + ] + # The all-zero state satisfies the linear recurrence but is not part of + # the maximal-length PN23 sequence and must not qualify a dead data bus. + passed = initialState != 0 and not errorBits + attempt = { + 'name': name, + 'xorMask': xorMask, + 'initialState': initialState, + 'checkedBits': len(bits)-23, + 'errorBits': errorBits, + 'passed': passed, + } + attempts.append(attempt) + if passed: + return { + 'passed': True, + 'selected': copy.deepcopy(attempt), + 'attempts': attempts, + } + + return { + 'passed': False, + 'selected': None, + 'attempts': attempts, + } + + +class AdcDdrCalibration(pr.Process): + """Software-driven FCO and per-data-lane delay calibration. + + The process scans FCO eyes first, centers compatible data lanes in parallel, + and finally qualifies the complete logical sample with a shared checkerboard + phase plus optional PN23 coherence and recurrence. Device adapters describe + how physical lanes map into logical channels and which sample bits each lane + contributes. + + Parameters + ---------- + config : Any + ADC configuration device containing ``OutputTestMode``. + readout : Any + Normalized ``AdcDdr`` readout device. + dataLaneToChannel : Sequence[int], optional + Logical channel contributed by each physical data lane. + dataLaneMasks : Sequence[int], optional + Logical sample-bit mask contributed by each physical data lane. + testMode : int, optional + ADC checkerboard test-mode value. + expectedPatterns : Iterable[int], optional + Expected checkerboard sample values. + configUpdate : Callable[[], None], optional + Callback that transfers staged ADC configuration writes. + pn23Mode : int, optional + ADC PN23 test-mode value. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Process``. + """ + + FULL_C = 0 + VERIFY_CURRENT_C = 1 + VERIFY_GUARD_BAND_C = 2 + OUTCOME_IDLE_C = 0 + OUTCOME_RUNNING_C = 1 + OUTCOME_PASSED_C = 2 + OUTCOME_FAILED_C = 3 + OUTCOME_STOPPED_C = 4 + RUN_TIME_UPDATE_INTERVAL_C = 1.0 + PATTERN_TESTER_TIMEOUT_C = 256 + + def __init__( + self, + *, + config: Any, + readout: Any, + dataLaneToChannel: Sequence[int] | None = None, + dataLaneMasks: Sequence[int] | None = None, + testMode: int = 4, + expectedPatterns: Iterable[int] | None = None, + configUpdate: Callable[[], None] | None = None, + pn23Mode: int = 5, + **kwargs: Any) -> None: + """Construct a calibration process for one normalized ADC readout.""" + + self._config = config + self._readout = readout + self._dataLanes = readout._dataLanes + self._fcoLanes = readout._fcoLanes + self._channels = readout._channels + + # One-lane ADCs use the natural lane-to-channel mapping. Multi-lane ADC + # adapters, such as AD9681, explicitly map both physical halves back to + # the same set of logical channels. + if dataLaneToChannel is None: + dataLaneToChannel = [lane % self._channels for lane in range(self._dataLanes)] + if len(dataLaneToChannel) != self._dataLanes: + raise ValueError('dataLaneToChannel must contain one entry per data lane') + if any(channel < 0 or channel >= self._channels for channel in dataLaneToChannel): + raise ValueError('dataLaneToChannel contains an out-of-range channel') + self._dataLaneToChannel = tuple(dataLaneToChannel) + + # The mask identifies which logical sample bits are actually carried by + # a physical lane. It lets a lane be judged while its partner half is + # still outside its eye. + if dataLaneMasks is None: + dataLaneMasks = [(1 << readout._sampleBits)-1] * self._dataLanes + if len(dataLaneMasks) != self._dataLanes: + raise ValueError('dataLaneMasks must contain one entry per data lane') + sampleMask = (1 << readout._sampleBits)-1 + if any(mask <= 0 or mask & ~sampleMask for mask in dataLaneMasks): + raise ValueError('dataLaneMasks contains an invalid sample-bit mask') + self._dataLaneMasks = tuple(dataLaneMasks) + # Greedily build the largest safe parallel sweep groups. Lanes on + # different channels never interfere. Lanes on the same channel can + # also move together when their masks are disjoint, as with AD9681's + # lower-six-bit and upper-eight-bit halves. Overlapping contributors + # remain in separate groups so an error can be attributed to one lane. + groups = [] + for lane, (channel, mask) in enumerate( + zip(self._dataLaneToChannel, self._dataLaneMasks)): + for group in groups: + if all( + self._dataLaneToChannel[other] != channel or + not (self._dataLaneMasks[other] & mask) + for other in group): + group.append(lane) + break + else: + groups.append([lane]) + self._dataLaneGroups = tuple(tuple(group) for group in groups) + + # Default to the ADC checkerboard mode. A frozenset intentionally drops + # phase ordering here; per-lane eye scans need only observe both values. + # The final full-sample check restores strict shared phase ordering. + if expectedPatterns is None: + checkerA = sum(1 << bit for bit in range(readout._sampleBits) if bit % 2) + checkerB = ((1 << readout._sampleBits)-1) ^ checkerA + expectedPatterns = (checkerA, checkerB) + self._expectedPatterns = frozenset(expectedPatterns) + if not self._expectedPatterns: + raise ValueError('expectedPatterns must not be empty') + self._testMode = testMode + self._configUpdate = configUpdate + self._pn23Mode = pn23Mode + self._runDiagnostics = {} + self._lastCaptureDiagnostics = {} + self._deepPatternTesterActive = False + self._marginResults = {} + + kwargs.setdefault( + 'description', + 'Measure, verify, and apply normalized AdcDdr input-delay settings') + super().__init__(function=self._runCalibration, **kwargs) + + # User controls, retained results, and diagnostics are LocalVariables; + # all hardware access remains behind the normalized readout/config APIs. + self.add(pr.LocalVariable( + name = 'Operation', + description = 'Calibration or verification operation', + value = self.FULL_C, + enum = { + self.FULL_C: 'Full calibration', + self.VERIFY_CURRENT_C: 'Verify current', + self.VERIFY_GUARD_BAND_C: 'Verify guard band', + }, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DelayStart', + description = 'First delay tap included in a full scan', + value = 0, + minimum = 0, + maximum = (1 << readout._delayBits)-1, + units = 'tap', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'DelayStop', + description = 'Last delay tap included in a full scan', + value = (1 << readout._delayBits)-1, + minimum = 0, + maximum = (1 << readout._delayBits)-1, + units = 'tap', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'UsePatternTester', + description = ( + 'Run deep hardware checkerboard and optional PN23 qualification ' + 'after centering'), + value = readout._patternCheck, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'PatternTesterSamples', + description = 'Valid samples checked by each deep hardware qualification window', + value = 4096, + minimum = 1, + maximum = 0xFFFFFFFF, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'SampleCount', + description = 'Four-sample groups checked at each data tap', + value = 2, + minimum = 1, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'VerifyPn23', + description = ( + 'After checkerboard alignment, compare one four-sample PN23 ' + 'snapshot across channels and verify its recurrence'), + value = callable(getattr(config, 'ResetPNLong', None)), + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'Outcome', + description = 'Explicit result of the current or most recent operation', + value = self.OUTCOME_IDLE_C, + enum = { + self.OUTCOME_IDLE_C: 'IDLE', + self.OUTCOME_RUNNING_C: 'RUNNING', + self.OUTCOME_PASSED_C: 'PASSED', + self.OUTCOME_FAILED_C: 'FAILED', + self.OUTCOME_STOPPED_C: 'STOPPED', + }, + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'RunTime', + description = 'Elapsed wall-clock duration of the current or last operation', + value = 0.0, + units = 's', + disp = '{:1.6f}', + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'SettleTime', + description = 'Delay after changing a tap, relocking, or selecting test mode', + value = 0.001, + minimum = 0.0, + units = 's', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'MinimumEyeWidth', + description = 'Minimum passing width accepted by a full scan', + value = 8, + minimum = 1, + units = 'tap', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'GuardBand', + description = 'Passing taps required on both sides of a selected tap', + value = 2, + minimum = 0, + units = 'tap', + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'CircularDelays', + description = 'Merge passing windows at the delay scan boundaries', + value = False, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'Debug', + description = 'Retain and publish detailed diagnostics when the operation ends', + value = False, + mode = 'RW')) + + self.add(pr.LocalVariable( + name = 'Diagnostics', + description = 'Per-tap FCO words, lock state, and raw/masked data samples', + value = {}, + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'Results', + description = 'FCO and data-lane results from the last operation', + value = {}, + mode = 'RO')) + + self.add(pr.LocalVariable( + name = 'Margin', + description = 'Worst margin from the most recent successful full calibration', + value = 'Unavailable', + mode = 'RO')) + + self.add(pr.LocalCommand( + name = 'MarginReport', + description = 'Print selected FCO and data-eye margins from the last full calibration', + function = self.marginReport)) + + self.add(pr.LocalCommand( + name = 'ApplyResults', + description = 'Reapply selected taps from the last full calibration', + function = self.applyResults)) + + def _process(self) -> None: + """Translate a user-requested stop into a normal process outcome.""" + + try: + super()._process() + except _AdcDdrCalibrationStopped: + # Process.Stop is an expected user action. The calibration + # function has already restored the test mode and original delays + # in its finally block, so retain the partial progress without + # reporting the stop as an execution error. + self.Message.set('Calibration stopped') + + def _checkRun(self) -> None: + """Raise the private stop exception at cooperative cancellation points.""" + + if not self._runEn: + raise _AdcDdrCalibrationStopped + + def _recordDiagnostic( + self, + kind: str, + lane: int, + tap: int, + detail: dict[str, Any]) -> None: + """Retain one per-tap diagnostic in the private working tree.""" + + # Publishing this growing tree at every tap makes a long scan + # increasingly expensive. The process wrapper publishes one immutable + # copy after success, failure, or cancellation instead. + self._runDiagnostics[kind].setdefault(lane, {})[tap] = detail + self._runDiagnostics['Current'] = { + 'kind': kind, + 'lane': lane, + 'tap': tap, + } + + def _configurePatternTester( + self, + channels: Sequence[int], + mask: int, + samples: int, + *, + pn23: bool = False, + xorMask: int = 0) -> None: + """Program one hardware measurement window for a channel/mask set.""" + + patterns = sorted(self._expectedPatterns) + tester = self._readout.PatternTester + # Alternating patterns share one phase reference across every enabled + # channel. This prevents each logical channel from independently calling + # an opposite checkerboard phase valid. + tester.Alternating.set(not pn23 and len(patterns) == 2, write=True) + tester.Pn23.set(pn23, write=True) + tester.ReferenceChannel.set(channels[0], write=True) + tester.ChannelMask.set(sum(1 << channel for channel in channels), write=True) + tester.FcoMask.set((1 << self._fcoLanes)-1, write=True) + tester.DataMask.set(mask, write=True) + tester.PatternA.set(xorMask if pn23 else patterns[0], write=True) + tester.PatternB.set(0 if pn23 else patterns[-1], write=True) + tester.Samples.set(samples, write=True) + # Do not inherit mutable state from manual uses of the child device. + # This timeout detects a stopped producer without limiting a healthy + # continuous sample window. + tester.Timeout.set(self.PATTERN_TESTER_TIMEOUT_C, write=True) + + def _runPatternTester(self) -> dict[str, Any]: + """Start one hardware window and return its stable retained results.""" + + tester = self._readout.PatternTester + # Sequence is the completion handshake. It avoids depending on the exact + # cycle in which Busy asserts or clears across the AXI-Lite boundary. + sequence = int(tester.Sequence.get(read=True)) + tester.Start() + while True: + if not self._runEn: + tester.Abort() + self._checkRun() + current = int(tester.Sequence.get(read=True)) + if current != sequence: + break + + # Force one final hardware transaction before reading the cached status + # fields populated by the completed measurement. + tester.Busy.get(read=True) + status = { + 'busy': bool(tester.Busy.get(read=False)), + 'timedOut': bool(tester.TimedOut.get(read=False)), + 'configError': bool(tester.ConfigError.get(read=False)), + 'aborted': bool(tester.Aborted.get(read=False)), + 'phaseAcquired': bool(tester.PhaseAcquired.get(read=False)), + 'allChannelsPass': bool(tester.AllChannelsPass.get(read=False)), + 'allFcoPass': bool(tester.AllFcoPass.get(read=False)), + } + if status['timedOut']: + raise RuntimeError('ADC pattern tester timed out waiting for valid samples') + if status['configError']: + raise RuntimeError('ADC pattern tester rejected its measurement configuration') + if status['aborted']: + raise RuntimeError('ADC pattern tester measurement was aborted') + + channelPassed = int(tester.ChannelPassed.get(read=True)) + fcoPassed = int(tester.FcoPassed.get(read=True)) + channels = { + channel: { + 'passed': bool(channelPassed & (1 << channel)), + 'wordErrorCount': int(tester.WordErrorCount[channel].get(read=True)), + 'bitErrorMask': int(tester.BitErrorMask[channel].get(read=True)), + } + for channel in range(self._channels) + } + fco = { + lane: { + 'passed': bool(fcoPassed & (1 << lane)), + 'errorCount': int(tester.FcoErrorCount[lane].get(read=True)), + } + for lane in range(self._fcoLanes) + } + return { + 'sequence': current, + 'checkedSamples': int(tester.CheckedSamples.get(read=True)), + 'channelPassed': channelPassed, + 'fcoPassed': fcoPassed, + 'channels': channels, + 'fco': fco, + 'status': status, + } + + def _captureGroupPassesSnapshot( + self, + lanes: Sequence[int]) -> dict[int, dict[str, Any]]: + """Measure ordered per-lane patterns from atomic snapshots.""" + + patterns = sorted(self._expectedPatterns) + details = { + lane: { + 'channel': self._dataLaneToChannel[lane], + 'mask': self._dataLaneMasks[lane], + 'expected': sorted({pattern & self._dataLaneMasks[lane] + for pattern in patterns}), + 'captures': [], + 'passed': True, + } + for lane in lanes + } + channels = sorted({self._dataLaneToChannel[lane] for lane in lanes}) + for _ in range(self.SampleCount.value()): + # Snapshot publishes every logical channel atomically. Read it once + # and reuse that coherent capture for every lane-specific mask. + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + rawByChannel = { + channel: snapshot[channel] + for channel in channels + } + for lane, detail in details.items(): + mask = detail['mask'] + maskedPatterns = detail['expected'] + raw = rawByChannel[detail['channel']] + masked = [sample & mask for sample in raw] + try: + phase = maskedPatterns.index(masked[0]) + except ValueError: + phase = None + expected = [] + else: + expected = [ + maskedPatterns[(phase+index) % len(maskedPatterns)] + for index in range(len(masked)) + ] + + # Each physical lane acquires phase independently during the + # scan so one channel leaving its eye cannot truncate another + # channel's passing window. The assembled final checks impose + # one shared phase after every lane has been centered. + capturePassed = phase is not None and masked == expected + detail['captures'].append({ + 'sequence': snapshotSequence, + 'phase': phase, + 'expectedSequence': expected, + 'raw': raw, + 'masked': masked, + 'passed': capturePassed, + }) + detail['passed'] &= capturePassed + return details + + def _captureGroupPasses( + self, + lanes: Sequence[int]) -> dict[int, dict[str, Any]]: + """Measure one grouped data tap using coherent debug snapshots.""" + + return self._captureGroupPassesSnapshot(lanes) + + def _capturePasses(self, lane: int) -> bool: + """Compatibility helper for verifying one physical data lane.""" + + detail = self._captureGroupPasses((lane,))[lane] + self._lastCaptureDiagnostics = detail + return detail['passed'] + + def _captureFinalPasses(self) -> dict[str, Any]: + """Qualify complete logical samples with one shared pattern phase.""" + + mask = (1 << self._readout._sampleBits)-1 + patterns = sorted(self._expectedPatterns) + detail = { + 'mask': mask, + 'expected': patterns, + 'referenceChannel': 0, + 'captures': [], + 'passed': True, + } + + for _ in range(self.SampleCount.value()): + self._checkRun() + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + rawByChannel = { + channel: snapshot[channel] + for channel in range(self._channels) + } + maskedByChannel = { + channel: [sample & mask for sample in raw] + for channel, raw in rawByChannel.items() + } + + # Acquire the A/B phase once from channel zero, then require every + # channel—and therefore every assembled physical half—to match the + # same ordered sequence. This detects split-lane sample epochs that + # an FCO pattern cannot distinguish by itself. + reference = maskedByChannel[detail['referenceChannel']] + try: + phase = patterns.index(reference[0]) + except ValueError: + phase = None + expected = [] + else: + expected = [ + patterns[(phase+index) % len(patterns)] + for index in range(len(reference)) + ] + + channels = { + channel: { + 'raw': rawByChannel[channel], + 'masked': masked, + 'passed': phase is not None and masked == expected, + } + for channel, masked in maskedByChannel.items() + } + capturePassed = all(channel['passed'] for channel in channels.values()) + detail['captures'].append({ + 'sequence': snapshotSequence, + 'phase': phase, + 'expectedSequence': expected, + 'channels': channels, + 'passed': capturePassed, + }) + detail['passed'] &= capturePassed + + return detail + + def _captureDeepPatternPasses(self) -> dict[str, Any]: + """Run one deep full-channel checkerboard measurement in hardware.""" + + mask = (1 << self._readout._sampleBits)-1 + channels = list(range(self._channels)) + samples = int(self.PatternTesterSamples.value()) + self._configurePatternTester(channels, mask, samples) + measurement = self._runPatternTester() + measurement['enabled'] = True + measurement['performed'] = True + measurement['mode'] = 'Checkerboard' + measurement['requestedSamples'] = samples + measurement['passed'] = ( + measurement['checkedSamples'] == samples and + measurement['status']['allChannelsPass'] and + measurement['status']['allFcoPass']) + return measurement + + def _captureDeepPn23Passes(self, xorMask: int) -> dict[str, Any]: + """Run one arbitrary-phase PN23 recurrence/coherence window.""" + + mask = (1 << self._readout._sampleBits)-1 + channels = list(range(self._channels)) + samples = int(self.PatternTesterSamples.value()) + self._configurePatternTester( + channels, + mask, + samples, + pn23=True, + xorMask=xorMask) + measurement = self._runPatternTester() + measurement['enabled'] = True + measurement['performed'] = True + measurement['mode'] = 'Pn23' + measurement['xorMask'] = xorMask + measurement['requestedSamples'] = samples + measurement['passed'] = ( + measurement['checkedSamples'] == samples and + measurement['status']['phaseAcquired'] and + measurement['status']['allChannelsPass'] and + measurement['status']['allFcoPass']) + return measurement + + def _setPn23Mode(self) -> None: + """Select PN23 and synchronously restart every selected generator.""" + + resetPnLong = getattr(self._config, 'ResetPNLong', None) + if not callable(resetPnLong): + raise RuntimeError('PN23 verification requires config.ResetPNLong()') + + self._setTestMode(self._pn23Mode) + resetPnLong() + + def _capturePn23Passes(self) -> dict[str, Any]: + """Check channel coherence and PN23 recurrence in one atomic snapshot.""" + + self._checkRun() + self._readout.Snapshot() + snapshotSequence = self._readout.SnapshotSequence.get(read=False) + snapshot = self._readout._getDebugSamples(read=False) + channels = { + channel: [int(sample) for sample in snapshot[channel]] + for channel in range(self._channels) + } + referenceChannel = 0 + reference = channels[referenceChannel] + recurrence = checkAdcDdrPn23(reference, self._readout._sampleBits) + channelResults = { + channel: { + 'samples': samples, + 'matchesReference': samples == reference, + } + for channel, samples in channels.items() + } + coherencePassed = all( + result['matchesReference'] + for result in channelResults.values()) + return { + 'sequence': snapshotSequence, + 'referenceChannel': referenceChannel, + 'channels': channelResults, + 'coherencePassed': coherencePassed, + 'recurrence': recurrence, + 'passed': coherencePassed and recurrence['passed'], + } + + def _setTestMode(self, value: int) -> None: + """Select an ADC output pattern and issue any required device update.""" + + self._config.OutputTestMode.set(value, write=True) + if self._configUpdate is not None: + # Some ADC register interfaces shadow writes until a transfer/update + # command is issued. Device adapters provide that command here. + self._configUpdate() + + def _enterAlignmentPattern(self, settle: float) -> None: + """Select checkerboard and establish a deterministic ADC sample epoch.""" + + self._setTestMode(self._testMode) + time.sleep(settle) + digitalReset = getattr(self._config, 'DigitalReset', None) + if not callable(digitalReset): + raise RuntimeError('ADC calibration requires config.DigitalReset()') + # Alternating patterns can originate in independent channel-local + # digital pipelines. Reset those pipelines only after the pattern is + # selected, then restart FPGA word alignment against the newly + # synchronized ADC output. + self.Message.set('Resetting ADC digital datapath for pattern alignment') + digitalReset() + time.sleep(settle) + self._readout.Relock() + time.sleep(settle) + + def _scanFco( + self, + lane: int, + taps: Sequence[int], + settle: float) -> dict[int, bool]: + """Return the lock verdict at every requested tap for one FCO lane.""" + + passing = {} + for tap in taps: + self._checkRun() + self._readout.FcoDelay[lane].set(tap, write=True) + # Changing FCO timing invalidates the frame-word lock. Reacquire it + # at each tap before treating LockedMask as the measurement result. + self._readout.Relock() + time.sleep(settle) + lockedMask = self._readout.LockedMask.get(read=True) + passing[tap] = bool(lockedMask & (1 << lane)) + if self.Debug.value(): + word = self._readout.FcoWord[lane].get(read=True) + self._recordDiagnostic('Fco', lane, tap, { + 'passed': passing[tap], + 'lockedMask': lockedMask, + 'word': word, + }) + self.Message.set( + f'FCO lane {lane}, tap {tap}: ' + f'{"pass" if passing[tap] else "fail"}, ' + f'word=0x{word:X}, lockedMask=0x{lockedMask:X}') + self.incrementSteps() + return passing + + def _scanDataGroup( + self, + lanes: Sequence[int], + taps: Sequence[int], + settle: float) -> dict[int, dict[int, bool]]: + """Sweep a compatible physical-lane group through one common tap range.""" + + passing = {lane: {} for lane in lanes} + for tap in taps: + self._checkRun() + # All group members move before the capture, so every lane verdict + # in this iteration describes the same physical sampling instant. + self._readout._setDataDelays({lane: tap for lane in lanes}) + time.sleep(settle) + details = self._captureGroupPasses(lanes) + for lane, detail in details.items(): + passing[lane][tap] = detail['passed'] + self._runDiagnostics['Data'].setdefault(lane, {})[tap] = detail + self._runDiagnostics['Current'] = { + 'kind': 'Data', + 'lanes': list(lanes), + 'tap': tap, + } + if self.Debug.value(): + passed = [lane for lane in lanes if details[lane]['passed']] + failed = [lane for lane in lanes if not details[lane]['passed']] + self.Message.set( + f'Data lanes {list(lanes)}, tap {tap}: ' + f'passing={passed}, failing={failed}') + self.incrementSteps() + return passing + + def _fullCalibration( + self, + taps: Sequence[int], + settle: float) -> dict[str, Any]: + """Scan, center, and jointly qualify every FCO and data lane.""" + + results = {'Fco': {}, 'Data': {}} + fcoEyes = {} + minimum = self.MinimumEyeWidth.value() + guard = self.GuardBand.value() + circular = self.CircularDelays.value() + + # FCO lanes and compatible data groups each consume one tap scan. FCO + # combination retries are data-dependent, so they are intentionally not + # included in the deterministic progress-bar total. + self.setTotalSteps(len(taps)*(self._fcoLanes+len(self._dataLaneGroups))+1) + self.setStep(0) + + # Locate all qualifying FCO windows. The preferred (widest) eye is + # installed immediately, while alternatives are retained for the final + # cross-lane phase qualification below. + for lane in range(self._fcoLanes): + self.Message.set(f'Scanning FCO lane {lane}') + passing = self._scanFco(lane, taps, settle) + try: + eyes = findAdcDdrEyes( + passing, + minimumWidth=minimum, + guardBand=guard, + circular=circular) + except RuntimeError as exc: + results['Fco'][lane] = { + 'passing': passing, + 'diagnostics': copy.deepcopy(self._runDiagnostics['Fco'].get(lane, {})), + } + self.Results.set(results) + raise RuntimeError(f'FCO lane {lane}: {exc}') from exc + fcoEyes[lane] = eyes + results['Fco'][lane] = { + 'eye': eyes[0].asDict(), + 'eyes': [eye.asDict() for eye in eyes], + 'passing': passing, + } + self._readout.FcoDelay[lane].set(eyes[0].selected, write=True) + + self._readout.Relock() + time.sleep(settle) + + # Put the ADC into a known alternating pattern only after FCO lock is + # established. Each overlap-safe lane group is swept once and centered + # before the next group is measured. + self._enterAlignmentPattern(settle) + for lanes in self._dataLaneGroups: + self.Message.set(f'Scanning data lanes {list(lanes)}') + groupPassing = self._scanDataGroup(lanes, taps, settle) + groupEyes = {} + for lane in lanes: + passing = groupPassing[lane] + try: + eye = findAdcDdrEye( + passing, + minimumWidth=minimum, + guardBand=guard, + circular=circular) + except RuntimeError as exc: + results['Data'][lane] = { + 'passing': passing, + 'diagnostics': copy.deepcopy( + self._runDiagnostics['Data'].get(lane, {})), + } + self.Results.set(results) + raise RuntimeError(f'Data lane {lane}: {exc}') from exc + groupEyes[lane] = eye + results['Data'][lane] = {'eye': eye.asDict(), 'passing': passing} + # Center the complete group before scanning any remaining lanes + # whose overlapping sample masks require a separate pass. + self._readout._setDataDelays({ + lane: eye.selected + for lane, eye in groupEyes.items() + }) + + # Per-lane FCO lock cannot distinguish equivalent frame windows that + # imply different sample epochs. Try the Cartesian product of retained + # eyes and accept the first combination whose fully assembled channel + # data shares one ordered checkerboard phase. + self.Message.set('Checking final full-channel pattern alignment') + combinations = itertools.product(*( + fcoEyes[lane] + for lane in range(self._fcoLanes) + )) + attempts = [] + final = None + for index, combination in enumerate(combinations): + self._checkRun() + if index != 0: + # Data-eye locations are independent of which equivalent FCO + # window establishes frame phase, so retries only move FCO taps + # before repeating final qualification; the expensive data + # sweeps do not need repeating. + selected = { + lane: eye.selected + for lane, eye in enumerate(combination) + } + self.Message.set( + f'Retrying FCO eye combination ' + f'{[selected[lane] for lane in range(self._fcoLanes)]}') + for lane, tap in selected.items(): + self._readout.FcoDelay[lane].set(tap, write=True) + + # Re-establish the ADC pattern epoch immediately before every final + # attempt. This closes the long window between the reset preceding + # data-eye scans and final qualification, during which another bank + # calibrating in parallel may disturb shared ADC digital state. It + # also restores checkerboard after a prior attempt reached PN23. + self._enterAlignmentPattern(settle) + + checkerboard = self._captureFinalPasses() + candidate = copy.deepcopy(checkerboard) + candidate['snapshotPassed'] = checkerboard['passed'] + candidate['patternTester'] = { + 'enabled': self._deepPatternTesterActive, + 'performed': False, + 'requestedSamples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + 'passed': not self._deepPatternTesterActive, + } + if checkerboard['passed'] and self._deepPatternTesterActive: + self.Message.set('Running deep hardware checkerboard qualification') + candidate['patternTester'] = self._captureDeepPatternPasses() + candidate['checkerboardPassed'] = ( + checkerboard['passed'] and candidate['patternTester']['passed']) + candidate['pn23'] = { + 'enabled': bool(self.VerifyPn23.value()), + 'performed': False, + 'passed': not bool(self.VerifyPn23.value()), + } + if candidate['checkerboardPassed'] and self.VerifyPn23.value(): + self.Message.set('Checking final PN23 channel coherence and recurrence') + self._setPn23Mode() + time.sleep(settle) + pn23 = self._capturePn23Passes() + pn23['enabled'] = True + pn23['performed'] = True + pn23['snapshotPassed'] = pn23['passed'] + pn23['patternTester'] = { + 'enabled': self._deepPatternTesterActive, + 'performed': False, + 'requestedSamples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + 'passed': not self._deepPatternTesterActive, + } + if pn23['snapshotPassed'] and self._deepPatternTesterActive: + self.Message.set('Running deep hardware PN23 qualification') + pn23['patternTester'] = self._captureDeepPn23Passes( + int(pn23['recurrence']['selected']['xorMask'])) + pn23['passed'] = ( + pn23['snapshotPassed'] and pn23['patternTester']['passed']) + candidate['pn23'] = pn23 + candidate['passed'] = pn23['passed'] + else: + candidate['passed'] = ( + candidate['checkerboardPassed'] and candidate['pn23']['passed']) + + # Retain every attempted combination and its raw qualification so a + # failed calibration explains exactly which phase choices were tried. + attempts.append({ + 'fcoDelays': [eye.selected for eye in combination], + 'fcoEyes': [eye.asDict() for eye in combination], + 'passed': candidate['passed'], + 'qualification': copy.deepcopy(candidate), + }) + if candidate['passed']: + final = candidate + # Publish the eyes actually left in hardware, which may differ + # from the individually preferred eyes selected above. + for lane, eye in enumerate(combination): + results['Fco'][lane]['eye'] = eye.asDict() + break + + if final is None: + final = copy.deepcopy(attempts[-1]['qualification']) + final['attempts'] = attempts + results['Final'] = final + self._runDiagnostics['Final'] = copy.deepcopy(final) + self._runDiagnostics['Current'] = {'kind': 'Final'} + self.incrementSteps() + if not final['passed']: + self.Results.set(results) + raise RuntimeError('Final full-channel pattern qualification failed') + return results + + def _verifyCurrent( + self, + originalFco: Sequence[int], + originalData: Sequence[int], + settle: float, + guard: int) -> tuple[dict[str, Any], bool]: + """Check installed taps, optionally including symmetric guard points.""" + + start = self.DelayStart.value() + stop = self.DelayStop.value() + results = {'Fco': {}, 'Data': {}} + passed = True + candidatesPerLane = 1 if guard == 0 else 3 + + # Verification checks every lane independently. Unlike a full scan, it + # previously did not initialize or advance the Process progress fields, + # which left the UI parked at step one for the entire operation. + self.setTotalSteps(candidatesPerLane*(len(originalFco)+len(originalData))) + self.setStep(0) + + self._enterAlignmentPattern(settle) + for kind, currentValues in ( + ('Fco', originalFco), + ('Data', originalData)): + for lane, selected in enumerate(currentValues): + checked = {} + # Guard-band verification samples the selected point and the + # requested distance on either side. A point outside the user's + # allowed scan domain is an explicit failure, not a skipped test. + candidates = [selected] if guard == 0 else [selected-guard, selected, selected+guard] + for tap in candidates: + self._checkRun() + self.Message.set( + f'Verifying {kind} lane {lane}, tap {tap} ' + f'(selected {selected})') + if tap < start or tap > stop: + checked[tap] = False + elif kind == 'Fco': + self._readout.FcoDelay[lane].set(tap, write=True) + self._readout.Relock() + time.sleep(settle) + checked[tap] = bool( + self._readout.LockedMask.get(read=True) & (1 << lane)) + else: + self._readout._setDataDelays({lane: tap}) + time.sleep(settle) + checked[tap] = self._capturePasses(lane) + self.incrementSteps() + lanePassed = all(checked.values()) + results[kind][lane] = { + 'selected': selected, + 'guardBand': guard, + 'checked': checked, + 'passed': lanePassed, + } + passed &= lanePassed + # Restore this lane before testing the next one. Multiple physical + # lanes can contribute to the same logical sample, so leaving one + # at its final guard candidate can invalidate the next lane's test. + if kind == 'Fco': + self._readout.FcoDelay[lane].set(selected, write=True) + self._readout.Relock() + else: + self._readout._setDataDelays({lane: selected}) + time.sleep(settle) + return results, passed + + def _updateRunTime(self, runStart: float, stopEvent: threading.Event) -> None: + """Publish elapsed wall time without burdening individual scan loops.""" + + while not stopEvent.wait(self.RUN_TIME_UPDATE_INTERVAL_C): + self.RunTime.set(time.monotonic()-runStart) + + def _runCalibration(self, *, dev: Any) -> dict[str, Any]: + """Wrap the operation with a live elapsed-time monitor.""" + + runStart = time.monotonic() + timerStop = threading.Event() + publishDiagnostics = bool(self.Debug.value()) + self._runDiagnostics = {} + self.Diagnostics.set({}) + timerThread = threading.Thread( + target = self._updateRunTime, + args = (runStart, timerStop), + daemon = True) + self.RunTime.set(0.0) + self.Outcome.set(self.OUTCOME_RUNNING_C) + timerThread.start() + try: + results = self._runCalibrationImpl(dev=dev) + except _AdcDdrCalibrationStopped: + self.Outcome.set(self.OUTCOME_STOPPED_C) + publishDiagnostics = True + raise + except Exception as exc: + self.Outcome.set(self.OUTCOME_FAILED_C) + self.Message.set(f'FAILED: {exc}') + publishDiagnostics = True + raise + else: + self.Outcome.set(self.OUTCOME_PASSED_C) + return results + finally: + runTime = time.monotonic()-runStart + timerStop.set() + timerThread.join() + self.RunTime.set(runTime) + if publishDiagnostics: + self.Diagnostics.set(copy.deepcopy(self._runDiagnostics)) + + def _runCalibrationImpl(self, *, dev: Any) -> dict[str, Any]: + """Validate controls, execute the selected operation, and restore state.""" + + # Fail early and clearly if the register model was constructed with a + # geometry that disagrees with the RTL (for example a deviceFamily that + # sets the wrong delay width). Otherwise a mismatch only surfaces as a + # cryptic readback verify error partway through the scan. + self._readout.checkGeometry() + + start = self.DelayStart.value() + stop = self.DelayStop.value() + settle = self.SettleTime.value() + if start > stop: + raise ValueError('DelayStart must not be greater than DelayStop') + + operation = self.Operation.value() + self._deepPatternTesterActive = ( + bool(self.UsePatternTester.value()) and operation == self.FULL_C) + if self._deepPatternTesterActive: + # Fail before changing ADC state if the selected readout cannot + # perform the requested deep hardware qualification. + if not bool(self._readout.PatternCheck.get(read=True)): + raise RuntimeError('ADC pattern tester is not present in this readout') + if len(self._expectedPatterns) > 2: + raise RuntimeError( + 'ADC pattern tester supports at most two expected patterns') + pn23Minimum = (23//self._readout._sampleBits)+1 + if (self.VerifyPn23.value() and + self.PatternTesterSamples.value() < pn23Minimum): + raise ValueError( + f'PatternTesterSamples must be at least {pn23Minimum} ' + f'for {self._readout._sampleBits}-bit PN23 verification') + + # Snapshot every mutable hardware setting before the operation. Verify, + # failure, and stop paths restore these values in the common finally + # block; only a successful full calibration retains newly selected taps. + originalMode = self._config.OutputTestMode.get(read=True) + originalFco = [variable.get(read=True) for variable in self._readout.FcoDelay.values()] + originalData = self._readout._getDataDelays(read=True) + results = {} + retainResults = False + self._runDiagnostics = { + 'TestMode': self._testMode, + 'ExpectedPatterns': sorted(self._expectedPatterns), + 'MeasurementBackend': 'Snapshot', + 'DeepPatternTester': { + 'enabled': self._deepPatternTesterActive, + 'samples': ( + int(self.PatternTesterSamples.value()) + if self._deepPatternTesterActive else 0), + }, + 'VerifyPn23': bool(self.VerifyPn23.value()), + 'Fco': {}, + 'Data': {}, + 'Final': {}, + 'Current': {}, + } + self.Results.set({}) + + try: + if operation == self.FULL_C: + results = self._fullCalibration(list(range(start, stop+1)), settle) + elif operation == self.VERIFY_CURRENT_C: + results, passed = self._verifyCurrent(originalFco, originalData, settle, 0) + if not passed: + self.Results.set(results) + raise RuntimeError('Current ADC delay verification failed') + elif operation == self.VERIFY_GUARD_BAND_C: + results, passed = self._verifyCurrent( + originalFco, originalData, settle, self.GuardBand.value()) + if not passed: + self.Results.set(results) + raise RuntimeError('ADC delay guard-band verification failed') + else: + raise ValueError(f'Unsupported calibration operation {operation}') + self.Results.set(results) + retainResults = operation == self.FULL_C + if retainResults: + self._marginResults = copy.deepcopy(results) + self.Margin.set(self._marginHeadline(results)) + return results + finally: + # Test mode is always temporary. Delay settings remain installed + # only after a successful full calibration so ApplyResults and + # downstream software see a coherent, qualified configuration. + self._setTestMode(originalMode) + if not retainResults: + for variable, value in zip(self._readout.FcoDelay.values(), originalFco): + variable.set(value, write=True) + self._readout._setDataDelays(dict(enumerate(originalData))) + self._readout.Relock() + + @staticmethod + def _marginRows(results: Mapping[str, Any]) -> list[dict[str, Any]]: + """Extract selected-eye margins from one complete calibration result.""" + + if not results or not results.get('Final', {}).get('passed', False): + raise RuntimeError('No successful full-calibration margin result is available') + + rows = [] + for kind, label in (('Fco', 'FCO'), ('Data', 'DATA')): + for lane, result in sorted( + results.get(kind, {}).items(), key=lambda item: int(item[0])): + eye = result.get('eye') + if eye is None: + raise RuntimeError( + 'No successful full-calibration margin result is available') + rows.append({ + 'type': label, + 'lane': int(lane), + 'start': int(eye['start']), + 'end': int(eye['end']), + 'selected': int(eye['selected']), + 'leftMargin': int(eye['leftMargin']), + 'rightMargin': int(eye['rightMargin']), + 'wraps': bool(eye['wraps']), + 'leftBounded': bool(eye['leftBounded']), + 'rightBounded': bool(eye['rightBounded']), + }) + if not rows: + raise RuntimeError('No successful full-calibration margin result is available') + return rows + + @classmethod + def _marginHeadline(cls, results: Mapping[str, Any]) -> str: + """Return one compact minimum-margin summary in native tap units.""" + + rows = cls._marginRows(results) + margin = min( + min(row['leftMargin'], row['rightMargin']) + for row in rows) + qualifiers = [] + if any(not row['leftBounded'] or not row['rightBounded'] for row in rows): + qualifiers.append('scan-limited eye(s)') + if any(row['wraps'] for row in rows): + qualifiers.append('wrapped eye(s)') + suffix = f"; {', '.join(qualifiers)}" if qualifiers else '' + unit = 'tap' if margin == 1 else 'taps' + return f'{margin} {unit} minimum{suffix}' + + def marginReport(self) -> None: + """Print a compact selected-eye margin table in native tap units.""" + + rows = self._marginRows(self._marginResults) + lines = [ + f'{self.path} ADC alignment margins (native tap units)', + 'Type Lane Eye Selected Left Right Worst Bounds', + ] + for row in rows: + bounds = [] + if row['wraps']: + bounds.append('wrapped') + if not row['leftBounded']: + bounds.append('left scan limit') + if not row['rightBounded']: + bounds.append('right scan limit') + if not bounds: + bounds.append('bounded') + eye = f"{row['start']}..{row['end']}" + lines.append( + f"{row['type']:<5} {row['lane']:>4} {eye:<10} " + f"{row['selected']:>8} {row['leftMargin']:>4} " + f"{row['rightMargin']:>5} " + f"{min(row['leftMargin'], row['rightMargin']):>5} " + f"{', '.join(bounds)}") + lines.append(f'Headline: {self._marginHeadline(self._marginResults)}') + print('\n'.join(lines)) + + def applyResults(self) -> None: + """Reinstall the selected taps from the last complete calibration.""" + + results = self.Results.value() + # Partial scans and failed final qualifications can contain useful + # diagnostics, but must never be applied as a hardware configuration. + if (not results or not results.get('Final', {}).get('passed', False) or + any('eye' not in lane for kind in ('Fco', 'Data') + for lane in results.get(kind, {}).values())): + raise RuntimeError('No complete full-calibration result is available') + for lane, result in results['Fco'].items(): + self._readout.FcoDelay[int(lane)].set(result['eye']['selected'], write=True) + self._readout._setDataDelays({ + int(lane): result['eye']['selected'] + for lane, result in results['Data'].items() + }) + self._readout.Relock() diff --git a/python/surf/devices/analog_devices/_AdcDdrPatternTester.py b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py new file mode 100644 index 0000000000..e622222434 --- /dev/null +++ b/python/surf/devices/analog_devices/_AdcDdrPatternTester.py @@ -0,0 +1,297 @@ +#----------------------------------------------------------------------------- +# Title : Serialized DDR ADC pattern tester +#----------------------------------------------------------------------------- +# Description: +# PyRogue model for the AdcDdr finite-window pattern measurement engine. +#----------------------------------------------------------------------------- +# This file is part of the '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. +#----------------------------------------------------------------------------- + +from __future__ import annotations + +from typing import Any + +import pyrogue as pr + + +class AdcDdrPatternTester(pr.Device): + """PyRogue model for the finite-window ADC pattern tester. + + Parameters + ---------- + channels : int + Number of logical ADC channels. + fcoLanes : int + Number of frame-clock lanes. + sampleBits : int + Number of meaningful bits in each ADC sample. + **kwargs : Any + Additional arguments forwarded to ``pyrogue.Device``. + """ + + def __init__( + self, + *, + channels: int, + fcoLanes: int, + sampleBits: int, + **kwargs: Any) -> None: + """Create the ADC pattern-tester model.""" + + for name, value, minimum, maximum in ( + ('channels', channels, 1, 16), + ('fcoLanes', fcoLanes, 1, 16), + ('sampleBits', sampleBits, 2, 16)): + if not minimum <= value <= maximum: + raise ValueError(f'{name} must be from {minimum} through {maximum}') + + kwargs.setdefault('description', 'Parallel finite-window ADC pattern measurement') + super().__init__(**kwargs) + + self.add(pr.RemoteCommand( + name = 'Start', + description = 'Start a finite pattern measurement window', + offset = 0x000, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteCommand( + name = 'Abort', + description = 'Abort the active pattern measurement window', + offset = 0x004, + bitSize = 1, + bitOffset = 0, + function = pr.RemoteCommand.touchOne)) + + self.add(pr.RemoteVariable( + name = 'Alternating', + description = 'Compare a shared-phase alternating A/B pattern instead of constant A', + offset = 0x008, + bitSize = 1, + bitOffset = 0, + mode = 'RW', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'Pn23', + description = 'Check arbitrary-phase PN23 recurrence and cross-channel coherence', + offset = 0x008, + bitSize = 1, + bitOffset = 1, + mode = 'RW', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'ReferenceChannel', + description = 'Enabled channel used for shared A/B phase or PN23 recurrence', + offset = 0x008, + bitSize = 8, + bitOffset = 8, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 0, + maximum = channels-1)) + + self.add(pr.RemoteVariable( + name = 'ChannelMask', + description = 'Logical channels enabled for parallel pattern comparison', + offset = 0x00C, + bitSize = channels, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'FcoMask', + description = 'FCO lanes enabled for mismatch counting', + offset = 0x010, + bitSize = fcoLanes, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'DataMask', + description = 'Sample bits included in each comparison', + offset = 0x014, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'PatternA', + description = 'Constant/first alternating pattern, or PN23 sample XOR mask', + offset = 0x018, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'PatternB', + description = 'Second alternating pattern', + offset = 0x01C, + bitSize = sampleBits, + mode = 'RW', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'Samples', + description = 'Number of valid sample groups requested for the measurement', + offset = 0x020, + bitSize = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}', + minimum = 1)) + + self.add(pr.RemoteVariable( + name = 'Timeout', + description = 'Consecutive capture clocks without sampleValid; zero disables timeout', + offset = 0x024, + bitSize = 32, + mode = 'RW', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'Busy', + description = 'A pattern measurement window is active', + offset = 0x028, + bitSize = 1, + bitOffset = 0, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'TimedOut', + description = 'The last window ended without sampleValid', + offset = 0x028, + bitSize = 1, + bitOffset = 1, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'ConfigError', + description = 'The last start had invalid configuration', + offset = 0x028, + bitSize = 1, + bitOffset = 2, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'Aborted', + description = 'The last window was aborted', + offset = 0x028, + bitSize = 1, + bitOffset = 3, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'PhaseAcquired', + description = 'Shared alternating-pattern phase was acquired', + offset = 0x028, + bitSize = 1, + bitOffset = 4, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllChannelsPass', + description = 'Every enabled logical channel passed', + offset = 0x028, + bitSize = 1, + bitOffset = 5, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'AllFcoPass', + description = 'Every enabled FCO lane was observed and had no mismatches', + offset = 0x028, + bitSize = 1, + bitOffset = 6, + mode = 'RO', + base = pr.Bool)) + + self.add(pr.RemoteVariable( + name = 'Sequence', + description = 'Completed pattern-window count including error and abort outcomes', + offset = 0x02C, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'CheckedSamples', + description = 'Valid sample groups checked by the completed or active window', + offset = 0x030, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}')) + + self.add(pr.RemoteVariable( + name = 'ChannelPassed', + description = 'One pass bit per enabled logical channel', + offset = 0x034, + bitSize = channels, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.add(pr.RemoteVariable( + name = 'FcoPassed', + description = 'One pass bit per observed, mismatch-free enabled FCO lane', + offset = 0x038, + bitSize = fcoLanes, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}')) + + self.addRemoteVariables( + name = 'WordErrorCount', + description = 'Saturating pattern word-error count for each logical channel', + number = channels, + stride = 4, + offset = 0x040, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') + + self.addRemoteVariables( + name = 'BitErrorMask', + description = 'Accumulated failing sample-bit mask for each logical channel', + number = channels, + stride = 4, + offset = 0x080, + bitSize = sampleBits, + mode = 'RO', + base = pr.UInt, + disp = '{:#x}') + + self.addRemoteVariables( + name = 'FcoErrorCount', + description = 'Saturating FCO mismatch count for each enabled FCO lane', + number = fcoLanes, + stride = 4, + offset = 0x0C0, + bitSize = 32, + mode = 'RO', + base = pr.UInt, + disp = '{:d}') diff --git a/python/surf/devices/analog_devices/__init__.py b/python/surf/devices/analog_devices/__init__.py index b4f2e34ec7..102023b200 100644 --- a/python/surf/devices/analog_devices/__init__.py +++ b/python/surf/devices/analog_devices/__init__.py @@ -7,10 +7,15 @@ ## may be copied, modified, propagated, or distributed except according to ## the terms contained in the LICENSE.txt file. ############################################################################## -from surf.devices.analog_devices._Ad9249 import * -from surf.devices.analog_devices._Ad5780 import * -from surf.devices.analog_devices._AttHmc624 import * -from surf.devices.analog_devices._Adt7420 import * -from surf.devices.analog_devices._Ad9681 import * -from surf.devices.analog_devices._Ad5541 import * -from surf.devices.analog_devices._Ltm4664 import * +from surf.devices.analog_devices._AdcDdrPatternTester import * +from surf.devices.analog_devices._AdcDdr import * +from surf.devices.analog_devices._AdcDdrCalibration import * +from surf.devices.analog_devices._Ad9249 import * +from surf.devices.analog_devices._Ad9249Legacy import * +from surf.devices.analog_devices._Ad9252 import * +from surf.devices.analog_devices._Ad5780 import * +from surf.devices.analog_devices._AttHmc624 import * +from surf.devices.analog_devices._Adt7420 import * +from surf.devices.analog_devices._Ad9681 import * +from surf.devices.analog_devices._Ad5541 import * +from surf.devices.analog_devices._Ltm4664 import * diff --git a/scripts/setup_regression_env.sh b/scripts/setup_regression_env.sh index 0bb38a2caa..d2cb1767ac 100755 --- a/scripts/setup_regression_env.sh +++ b/scripts/setup_regression_env.sh @@ -80,7 +80,9 @@ Activate the environment with: Then prepare HDL sources with: make MODULES="${ROOT_DIR}" import -Then run regressions with: - python -m pytest -v -n auto --dist=worksteal tests/axi tests/base tests/dsp - python -m pytest -v tests/test_*.py +Then check the regression structure and run the narrowest relevant suite: + python -m tests.common.compliance_audit check tests + python -m pytest -n 0 -q tests//test_.py + +See tests/README.md for the full methodology and parallel/full-suite commands. EOF diff --git a/setup.py b/setup.py index d8767c7aba..c4629eba3f 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ setup ( name='surf', version=pyVer, + python_requires='>=3.10', packages=['surf', 'surf/axi', 'surf/devices', @@ -53,4 +54,3 @@ 'surf/protocols/ssi', ], package_dir={'':'python'}, ) - diff --git a/tests/README.md b/tests/README.md index bd4d8f84cd..8b0160def7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,51 @@ This directory holds Python-authored regressions for synthesizable SURF RTL. The default stack is `pytest + cocotb + GHDL + ruckus`; VHDL should only be used for thin wrappers, shims, or required simulation models. +This README is the authoritative guide for new SURF regression work. Historical +task plans and module queues are not prerequisites and do not define the next +module that must be tested. Add or deepen coverage when a subsystem is being +changed, when a bug needs a permanent reproducer, or when a contributor chooses +an uncovered module to improve. + +## Quick Start + +If the local Python/GHDL/ruckus environment has not been prepared, run the +repository setup helper first: + +```bash +./scripts/setup_regression_env.sh +``` + +The script checks the required host tools, creates `.venv`, installs the Python +requirements, and locates or clones ruckus. Review its output, activate the +environment if desired, and then follow the workflow below. + +1. Read this README and the nearest subsystem README, if one exists. +2. Search the surrounding tests and helper modules before writing new drivers + or protocol models. +3. Identify the externally visible contract and the smallest useful DUT or + wrapper boundary. +4. Write the module-specific `Test methodology` block before implementing the + test. It should make the intended sweep, stimulus, checks, and timing clear. +5. Import the HDL sources and run the narrowest useful pytest target: + + ```bash + make MODULES="$PWD" import + ./.venv/bin/python -m pytest -n 0 -q tests//test_.py + ``` + +6. Lint every edited VHDL file and run the relevant subsystem regression before + handing the change off. + +Additional references: + +- [`tests/common/README.md`](common/README.md) documents the shared runner, + parameter cases, environment parsing, and clock helpers. +- [`tests/protocols/README.md`](protocols/README.md) documents protocol-oracle, + layering, malformed-frame, and integration-test practices. +- Subsystem READMEs may define protocol- or simulator-specific commands, but + they should extend rather than replace this guide. + ## Layout - Keep executable tests under subsystem packages, such as `tests/base/fifo/`, @@ -71,9 +116,9 @@ falls back to running the complete `tests/` tree. ## Python Test Files -Every checked-in cocotb test file should start with the standard SLAC/SURF -license header followed immediately by a module-specific `Test methodology` -block: +Every new or substantially edited cocotb test file should start with the +standard SLAC/SURF license header followed immediately by a module-specific +`Test methodology` block. The concise form is: ```python ############################################################################## @@ -90,7 +135,12 @@ block: ``` Do not use generic methodology text. The block should tell a reader what this -specific bench proves and what it intentionally does not prove. +specific bench proves and what it intentionally does not prove. Complex +protocol or integration tests may expand the headings (`Purpose`, `DUT shape`, +`Protocol checks`, `Parameter strategy`, and similar), but must still make the +scope/configuration, DUT boundary, stimulus, checks, and timing assumptions +easy to find. A prose methodology with the same information is acceptable in a +legacy file; use the labeled form for new work. Use in-body comments at the major coroutine steps: clock startup, reset, stimulus phases, backpressure, trigger waits, and result checks. Keep comments @@ -110,6 +160,54 @@ Common structure: - A final pytest wrapper named for the RTL target, calling `run_surf_vhdl_test(test_file=__file__, ...)`. +Keep the cocotb entrypoints and the pytest wrapper in the same file unless a +subsystem has a documented reason to separate them. Pytest owns build +parameters and simulator launches; cocotb owns cycle-level stimulus and checks. + +## Designing The Test + +Start from the public contract, not the current implementation. Read the entity +ports and generics, the nearest package and README, and any applicable protocol +or register-map specification. Then choose the smallest boundary that can prove +the behavior: + +- Test reusable leaves directly when their behavior is observable without a + large integration topology. +- Use an integration test when arbitration, CDC, configuration propagation, or + interaction between already-tested leaves is the actual contract. +- Do not replay a leaf's complete packet grammar or parameter matrix through + every higher-level wrapper. Higher-level tests should focus on what that layer + adds. +- Treat compile/elaboration-only smoke coverage as useful but distinct from a + functional regression. A functional test needs meaningful stimulus and + assertions. +- Treat package declarations as transitively covered unless an important + function or procedure needs a small wrapper and a direct behavioral test. + +For a bug regression, verify when practical that the new test fails against the +known-bad RTL and passes with the fix. If that comparison cannot be run, +document why and identify the assertion that would catch the original defect. +Reaching the formerly failing code path without checking its externally visible +effect is not sufficient regression coverage. + +A focused regression normally covers the relevant subset of: + +- reset assertion, release, and recovery; +- nominal data or control flow; +- backpressure and accepted-handshake timing; +- frame, burst, or transaction boundaries; +- payload ordering and byte enables such as `TKEEP`/`TSTRB`; +- sidebands such as `TLAST`, `TDEST`, `TID`, SOF, and EOFE; +- invalid inputs, error responses, overflow, timeout, or recovery behavior; +- representative generic or clock-domain configurations. + +Use deterministic directed cases for protocol rules and boundary conditions. +Randomized cases are valuable after a trustworthy reference model exists, but +they do not replace readable directed regressions for known contracts and bugs. +Seed every randomized case explicitly. Keep the seed fixed or pass it through a +named environment value, and include the effective seed in a failure message or +log so the exact stimulus can be reproduced. + ## Parameter Sweeps Prefer curated matrices over broad Cartesian products. A good sweep covers @@ -126,6 +224,19 @@ Pass only HDL generics as `parameters`. Put Python-only case metadata in `extra_env`, or use `hdl_parameters_from(parameters)` when a case dictionary contains both. +Prefer a pytest node that names one cocotb scenario or one coherent scenario +group. Normally let cocotb run all applicable entrypoints in that group. When +separate pytest nodes intentionally select one cocotb scenario, pass a selector +through `extra_env` (for example `COCOTB_TESTCASE` or a documented +subsystem-specific variable), give the selector a deterministic default, and +make sure it participates in the simulation-build identity. A focused pytest +node should not silently rerun unrelated cocotb scenarios. + +An entrypoint that is inapplicable to the current parameter set must be +explicitly skipped or excluded by pytest/cocotb selection. Do not return +successfully before exercising the behavior and assertions named by the test; +that records a no-op as a pass and obscures what the regression actually ran. + ## Reuse And Helpers Before writing transaction code, search nearby helpers and related subsystems. @@ -150,13 +261,45 @@ deassert the source immediately. Use `start_lockstep_clocks()` for `COMMON_CLK_G` or similar wrappers that expect truly shared clock edges. Do not start two independent same-period clock -coroutines when the DUT contract is common-clock behavior. +coroutines when the DUT contract is common-clock behavior. Retain its returned +task on the bench (for example, `self._clock_task = start_lockstep_clocks(...)`) +so ownership remains explicit just like any other lifetime agent. + +## Isolation And Coroutine Lifecycle + +Each cocotb entrypoint must establish its own defined starting state. Initialize +every testbench-driven input, reset the DUT when it has a meaningful reset, and +clear Python-side queues, scoreboards, and monitor state. Do not depend on the +execution order of cocotb entrypoints or on state left by an earlier test. + +Every finite transaction task started with `cocotb.start_soon()` must be awaited +before the test completes. A monitor, protocol peer, or other task intended to +run for the lifetime of the test should be retained by the bench, named for its +purpose, and documented as a lifetime agent. Give benches that own several such +agents an explicit cleanup method when they need orderly cancellation or can +hold an external resource. External processes, sockets, ports, and files always +require bounded setup/teardown and cleanup on assertion failure. + +Use operation-specific cycle limits or `with_timeout()` for protocol progress. +Add `timeout_time`/`timeout_unit` to complex concurrent or integration +entrypoints as a final deadlock watchdog. Small finite leaf tests do not need a +decorator timeout when every possible wait is already bounded. ## Assertions And Timing Assert externally visible behavior, not implementation accidents. Good checks usually include payload bytes, `TKEEP`, `TLAST`, `TUSER`/SOF/EOFE bits, address or ID sidebands, response codes, counters, or accepted-handshake timing. +For a complex or parameterized check, include enough context in the failure to +identify the case, transaction or beat index, expected value, observed value, +and random seed when applicable. + +Initialize reset and every testbench-driven control/data input before the first +active clock edge, preferably with `setimmediatevalue()` during bench setup. +Hold reset for an explicit number of clock edges, release it on the intended +edge, and allow any documented pipeline settling time before sampling outputs. +This prevents unresolved startup values from turning into simulator-dependent +stimulus. Use bounded waits and explicit timeouts for protocol progress. Avoid open-ended `while True` loops unless they are wrapped by `with_timeout()` or a @@ -166,13 +309,38 @@ When a contract includes backpressure, burst length, sideband propagation, or arbitration order, monitor accepted handshakes directly. Final memory contents alone are not enough for timing-visible behavior. -Account for `TPD_G`, registered outputs, and GHDL scheduling. Sampling exactly -on a clock edge can create false failures; most helpers settle with a short -`Timer` after `RisingEdge()`. - -Known RTL issues or intentionally open coverage should be explicit. If a bench -is checked in skipped or opt-in, document the condition and gate it with a clear -environment variable such as `RUN_KNOWN_ISSUE_TESTS`. +Account for `TPD_G`, registered outputs, and GHDL scheduling. After an edge, use +`ReadOnly()` when only delta-cycle settling is required. When the RTL schedules +a real nonzero `after TPD_G`, wait for that configured propagation delay and +then sample the stable value. Keep this distinction visible in a shared helper; +do not add an unexplained fixed delay merely to make a race disappear. + +The common helpers make that choice explicit. Use +`sample_after_delta_cycles(clock)` only when the next action is a read-only +observation of logic that settles without simulated time advancing; the +coroutine returns in cocotb's read-only phase, so do not drive signals from that +phase. Use `sample_after_tpd(clock, propagation_time=..., unit=...)` when the +RTL has a real `after TPD_G` assignment. Its default is the common SURF +one-nanosecond delay, but pass the elaborated value when a test changes `TPD_G`. +For a deliberate nonzero stimulus phase offset, such as asserting an +asynchronous input between clock edges, use +`wait_after_edge_offset(clock, offset_time=..., unit=...)`. This advances real +simulated time but does not claim that the delay models `TPD_G` propagation. + +Keep skip reasons and opt-in coverage explicit, and distinguish why a case is +not in the default run: + +- Gate a regression for an unresolved RTL defect with a clear variable such as + `RUN_KNOWN_ISSUE_TESTS`. Name the tracked defect, expected failure, and + condition for restoring the case to default coverage in the methodology or + local README, and promote the case with the fix. +- Gate an unusually long soak or stress matrix with a separately named + `RUN_*_EXTENDED_TESTS` variable; do not label stable-but-slow coverage as a + known issue. +- Use `pytest.skip()` or `pytest.mark.skipif()` for genuinely optional external + tools, licenses, platforms, or production libraries, and state the exact + missing prerequisite. A required CI job should provision that prerequisite + and treat an unexpected skip as a failure. ## Running Tests @@ -186,8 +354,16 @@ make MODULES="$PWD" import Run `make ... import` when the imported HDL source cache is missing or stale. Use `-n 0` for focused debug runs when serial simulator logs matter. -After any command that launches pytest, cocotb, GHDL, or another simulator -runner, check for stale simulator child processes before starting another run. +Assume the suite will run under pytest-xdist. Each case must have an isolated +simulation build directory and must not compete for a fixed port, ready file, +result file, or other process-global resource. Tests that launch peer processes +must use bounded startup/shutdown waits and clean them up in a `finally` block +or shared teardown helper, including after an assertion fails. + +The runner or test fixture should clean up simulator children and external +peers during normal execution. After an interrupted or hung run, check for stale +processes before retrying so they cannot retain a build directory, port, or +license. ## VHDL Wrappers @@ -224,3 +400,48 @@ command to confirm the file is clean. VHDL packages are usually covered transitively through modules that use them. Add a dedicated package wrapper only when a behavioral function or procedure is important and not reached naturally through existing DUT coverage. + +There is no active repository-wide queue of modules that must be completed in a +fixed order. When selecting new work, prefer high-reuse modules, code being +modified, untested bug fixes, and simulator-friendly leaves that establish +helpers useful to later integration tests. Vendor-heavy or mixed-language +blocks may be deferred when the standard GHDL flow cannot exercise their real +dependencies; document that limitation near the subsystem rather than adding a +test double that changes the DUT boundary. + +## Completion Checklist + +Before considering a new regression ready: + +- The Python file has the standard license header, a specific methodology + block, and comments around non-obvious cocotb sequencing. +- The test asserts behavior rather than merely reaching the end of simulation. +- A bug regression was shown to fail on known-bad RTL when practical, or the + limitation and defect-catching assertion are documented. +- Inapplicable scenarios are selected out or reported as skipped; no entrypoint + silently returns before exercising its named behavior. +- Parameter-specific terminal branches use ordinary structured control flow + when practical. A necessary early terminal branch has completed assertions + and an immediate `# Terminal scenario:` comment explaining why those checks + are its complete contract. +- Every wait is bounded directly or by a helper with a cycle/time limit. +- Reset, backpressure, sidebands, and error/boundary cases relevant to the DUT + are covered or explicitly documented as out of scope. +- Shared helpers were reused or extended instead of duplicated. +- Finite background tasks are awaited, lifetime agents have explicit ownership, + and external resources are cleaned up on failure. +- Random stimulus is seeded and failures report enough information to replay + the case. +- The case is safe under pytest-xdist: build artifacts and external resources + are isolated, and child processes are cleaned up on failure. +- Any retained VHDL wrapper is thin, locally documented, clean under + `vsg-linter.yml`, and reachable through its intended source path: the nearest + ruckus manifest for build-facing HDL or `extra_vhdl_sources` for a + cocotb-only wrapper. +- `extra_vhdl_sources` does not repeat production HDL already supplied by the + ruckus import. +- The focused test and the nearest practical subsystem suite pass. +- `git diff --check` is clean; after an interrupted run, no stale simulator or + peer process remains. +- The nearest README is updated if the test introduces a new layout, helper, + simulator requirement, deferred dependency, or non-obvious invocation. diff --git a/tests/axi/axi4/test_AxiMemTester.py b/tests/axi/axi4/test_AxiMemTester.py index d5672c0d85..9fe8efb6ec 100644 --- a/tests/axi/axi4/test_AxiMemTester.py +++ b/tests/axi/axi4/test_AxiMemTester.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteMaster, AxiRam, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Reset both the control plane and the AXI memory side before starting @@ -116,7 +116,6 @@ def test_AxiMemTester(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiMemTesterIpIntegrator.vhd", ], diff --git a/tests/axi/axi4/test_AxiMonAxiL.py b/tests/axi/axi4/test_AxiMonAxiL.py index 25cc7aff7f..00b600744e 100644 --- a/tests/axi/axi4/test_AxiMonAxiL.py +++ b/tests/axi/axi4/test_AxiMonAxiL.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32 @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axiRst.value = 1 @@ -89,7 +89,6 @@ def test_AxiMonAxiL(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiMonAxiLIpIntegrator.vhd", ], }, diff --git a/tests/axi/axi4/test_AxiRam.py b/tests/axi/axi4/test_AxiRam.py index f946d7c3fa..c689be369a 100644 --- a/tests/axi/axi4/test_AxiRam.py +++ b/tests/axi/axi4/test_AxiRam.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -36,8 +37,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/axi4/test_AxiRateGen.py b/tests/axi/axi4/test_AxiRateGen.py index 87e9403a8e..786945bbb7 100644 --- a/tests/axi/axi4/test_AxiRateGen.py +++ b/tests/axi/axi4/test_AxiRateGen.py @@ -25,7 +25,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteMaster, AxiRam from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -48,7 +50,7 @@ def __init__(self, dut): self.ar_handshakes = [] self.r_handshakes = [] - start_lockstep_clocks(dut.axiClk, dut.axilClk, period_ns=5.0) + self._clock_task = start_lockstep_clocks(dut.axiClk, dut.axilClk, period_ns=5.0) dut.axiRst.setimmediatevalue(1) dut.axilRst.setimmediatevalue(1) @@ -60,16 +62,19 @@ def __init__(self, dut): ) self.axi_ram = AxiRam(AxiBus.from_prefix(dut, "M_AXI"), dut.axiClk, dut.axiRst, size=2**16) - cocotb.start_soon(self._track_cycles()) - cocotb.start_soon(self._monitor_aw()) - cocotb.start_soon(self._monitor_w()) - cocotb.start_soon(self._monitor_ar()) - cocotb.start_soon(self._monitor_r()) + # These monitors are lifetime agents; cocotb cancels them when the + # entrypoint ends, and the bench retains them for explicit ownership. + self._monitor_tasks = ( + cocotb.start_soon(self._track_cycles()), + cocotb.start_soon(self._monitor_aw()), + cocotb.start_soon(self._monitor_w()), + cocotb.start_soon(self._monitor_ar()), + cocotb.start_soon(self._monitor_r()), + ) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold both reset domains active together because this regression keeps @@ -95,14 +100,15 @@ async def wait_for_count(self, store, expected: int, *, limit_cycles: int, label raise AssertionError(f"Timed out waiting for {label}: expected {expected}, saw {len(store)}") async def _track_cycles(self): + """Lifetime agent: count AXI cycles until cocotb ends the test.""" while True: await RisingEdge(self.dut.axiClk) self.cycle_count += 1 async def _monitor_aw(self): + """Lifetime agent: record accepted write addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_handshakes.append( ( @@ -115,9 +121,9 @@ async def _monitor_aw(self): ) async def _monitor_w(self): + """Lifetime agent: record accepted write data for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_WVALID.value) and logic_int(self.dut.M_AXI_WREADY.value): self.w_handshakes.append( ( @@ -128,9 +134,9 @@ async def _monitor_w(self): ) async def _monitor_ar(self): + """Lifetime agent: record accepted read addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_ARVALID.value) and logic_int(self.dut.M_AXI_ARREADY.value): self.ar_handshakes.append( ( @@ -143,9 +149,9 @@ async def _monitor_ar(self): ) async def _monitor_r(self): + """Lifetime agent: record accepted read data for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_RVALID.value) and logic_int(self.dut.M_AXI_RREADY.value): self.r_handshakes.append( ( @@ -235,7 +241,6 @@ def test_AxiRateGen(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiRateGenIpIntegrator.vhd", ], diff --git a/tests/axi/axi4/test_AxiReadEmulate.py b/tests/axi/axi4/test_AxiReadEmulate.py index 49e4cc50c6..4298007dae 100644 --- a/tests/axi/axi4/test_AxiReadEmulate.py +++ b/tests/axi/axi4/test_AxiReadEmulate.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi4/test_AxiReadPathFifo.py b/tests/axi/axi4/test_AxiReadPathFifo.py index 76a3da4cf9..9339a7610b 100644 --- a/tests/axi/axi4/test_AxiReadPathFifo.py +++ b/tests/axi/axi4/test_AxiReadPathFifo.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -55,8 +56,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.sAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.sAxiClk) async def wait_for(self, predicate, *, cycles: int, message: str): for _ in range(cycles): diff --git a/tests/axi/axi4/test_AxiReadPathMux.py b/tests/axi/axi4/test_AxiReadPathMux.py index 5a2af0e2ef..83201c6788 100644 --- a/tests/axi/axi4/test_AxiReadPathMux.py +++ b/tests/axi/axi4/test_AxiReadPathMux.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test @@ -40,12 +41,12 @@ def __init__(self, dut): self.s1 = SourcePort(dut, "S1_AXI") self.ram = None - cocotb.start_soon(self._monitor_ar()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_ar()) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold reset long enough for the shim layers and the DUT state machine @@ -60,9 +61,9 @@ def start_agents(self): self.ram = AxiRamRead(AxiReadBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_ar(self): + """Lifetime agent: record accepted read addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if int(self.dut.M_AXI_ARVALID.value) and int(self.dut.M_AXI_ARREADY.value): self.ar_handshakes.append( ( @@ -97,24 +98,20 @@ async def issue_read(self, address: int) -> bytes: getattr(self.dut, f"{self.prefix}_ARVALID").value = 1 while not int(getattr(self.dut, f"{self.prefix}_ARREADY").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_ARVALID").value = 0 getattr(self.dut, f"{self.prefix}_RREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_RVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) data = int(getattr(self.dut, f"{self.prefix}_RDATA").value).to_bytes(4, "little") assert int(getattr(self.dut, f"{self.prefix}_RRESP").value) == 0 assert int(getattr(self.dut, f"{self.prefix}_RLAST").value) == 1 - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_RREADY").value = 0 return data diff --git a/tests/axi/axi4/test_AxiResize.py b/tests/axi/axi4/test_AxiResize.py index c3b926dffa..125bf5037d 100644 --- a/tests/axi/axi4/test_AxiResize.py +++ b/tests/axi/axi4/test_AxiResize.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiMaster, AxiRam from tests.common.regression_utils import parameter_case, run_surf_vhdl_test @@ -53,13 +55,15 @@ def __init__(self, dut): self.master = None self.ram = None - cocotb.start_soon(self._monitor_aw()) - cocotb.start_soon(self._monitor_ar()) + # Lifetime monitors retained by the bench until cocotb ends the test. + self._monitor_tasks = ( + cocotb.start_soon(self._monitor_aw()), + cocotb.start_soon(self._monitor_ar()), + ) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the wrapper shims and the resize state so each parameter case @@ -76,9 +80,9 @@ def start_agents(self): self.ram = AxiRam(AxiBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_aw(self): + """Lifetime agent: record resized write metadata for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_meta.append( ( @@ -89,9 +93,9 @@ async def _monitor_aw(self): ) async def _monitor_ar(self): + """Lifetime agent: record resized read metadata for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_ARVALID.value) and logic_int(self.dut.M_AXI_ARREADY.value): self.ar_meta.append( ( diff --git a/tests/axi/axi4/test_AxiRingBuffer.py b/tests/axi/axi4/test_AxiRingBuffer.py index 2c7134a3e9..fc50f83f9c 100644 --- a/tests/axi/axi4/test_AxiRingBuffer.py +++ b/tests/axi/axi4/test_AxiRingBuffer.py @@ -21,7 +21,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiRam, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -45,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.dataRst.value = 1 @@ -106,7 +107,6 @@ def test_AxiRingBuffer(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/axi4/ip_integrator/AxiRingBufferIpIntegrator.vhd", ], diff --git a/tests/axi/axi4/test_AxiWriteEmulate.py b/tests/axi/axi4/test_AxiWriteEmulate.py index e5029db29d..c3347da143 100644 --- a/tests/axi/axi4/test_AxiWriteEmulate.py +++ b/tests/axi/axi4/test_AxiWriteEmulate.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -35,8 +36,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 diff --git a/tests/axi/axi4/test_AxiWritePathFifo.py b/tests/axi/axi4/test_AxiWritePathFifo.py index 4f21c97991..d8a79e7f4b 100644 --- a/tests/axi/axi4/test_AxiWritePathFifo.py +++ b/tests/axi/axi4/test_AxiWritePathFifo.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -58,8 +59,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.sAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.sAxiClk) async def wait_for(self, predicate, *, cycles: int, message: str): for _ in range(cycles): diff --git a/tests/axi/axi4/test_AxiWritePathMux.py b/tests/axi/axi4/test_AxiWritePathMux.py index a51c9cce31..bbb57725d4 100644 --- a/tests/axi/axi4/test_AxiWritePathMux.py +++ b/tests/axi/axi4/test_AxiWritePathMux.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test @@ -47,12 +48,12 @@ def __init__(self, dut): self.s1 = SourcePort(dut, "S1_AXI") self.ram = None - cocotb.start_soon(self._monitor_aw()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_aw()) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset both the wrapper shims and the mux state machine before any @@ -67,9 +68,9 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _monitor_aw(self): + """Lifetime agent: record accepted write addresses for this test.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_handshakes.append( ( @@ -117,8 +118,7 @@ async def issue_write(self, address: int, payload: bytes): aw_done = False w_done = False while not (aw_done and w_done): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) aw_done = aw_done or ( int(getattr(self.dut, f"{self.prefix}_AWVALID").value) and int(getattr(self.dut, f"{self.prefix}_AWREADY").value) @@ -134,12 +134,10 @@ async def issue_write(self, address: int, payload: bytes): getattr(self.dut, f"{self.prefix}_BREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_BVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) resp = int(getattr(self.dut, f"{self.prefix}_BRESP").value) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_BREADY").value = 0 return resp diff --git a/tests/axi/axi_lite/README.md b/tests/axi/axi_lite/README.md new file mode 100644 index 0000000000..4412ce4eeb --- /dev/null +++ b/tests/axi/axi_lite/README.md @@ -0,0 +1,33 @@ +# AXI-Lite Regressions + +This directory contains cocotb regressions for the reusable AXI-Lite cores in +[`axi/axi-lite/`](../../../axi/axi-lite/README.md). Tests use thin +IP-integrator wrappers where a flattened simulator interface is needed. + +Run the directory with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/axi/axi_lite +``` + +Use `-n 0` with a single test file when serial simulator logs are useful. + +## `AxiLiteAsync` + +`test_AxiLiteAsync.py` covers common-clock pass-through and four asynchronous +configurations: active-high reset, active-low reset, asynchronous reset, and +pipelined FIFO outputs. Its scenarios verify: + +- ordinary read/write round trips and recovery after reset; +- local error responses while the master domain is reset; +- no replay of rejected or already queued requests after recovery; +- no stale response after a slave-domain reset; +- correct AW/W ordering when a reset splits a write transaction; and +- the one-pending-beat limit on AR, AW, and W, including VALID held while READY + is low. + +The asynchronous reset tests use a gateable master clock so the slave domain +can remain live while the remote domain is unavailable. Accepted handshakes are +monitored on both sides; final memory contents alone are not used to infer that +a rejected transaction stayed out of the downstream interface. diff --git a/tests/axi/axi_lite/test_AxiDualPortRam.py b/tests/axi/axi_lite/test_AxiDualPortRam.py index f9f4d4500a..d6ac35b8a1 100644 --- a/tests/axi/axi_lite/test_AxiDualPortRam.py +++ b/tests/axi/axi_lite/test_AxiDualPortRam.py @@ -138,14 +138,11 @@ async def axi_round_trip_and_sys_read_test(dut): assert sys_data == int.from_bytes(expected_data, "little") -@cocotb.test() +@cocotb.test(skip=not env_flag("SYS_WR_EN", default=False)) async def sys_write_visibility_test(dut): tb = TB(dut) await tb.reset() - if not tb.sys_wr_en: - return - await tb.sys_write(addr=6, data=0xAABBCCDD, we_mask=(1 << tb.byte_count) - 1) rd_txn = await tb.axil.read(6 << 2, 4) assert rd_txn.resp == AxiResp.OKAY diff --git a/tests/axi/axi_lite/test_AxiLiteAsync.py b/tests/axi/axi_lite/test_AxiLiteAsync.py index 5e31bc9880..0b186b0522 100644 --- a/tests/axi/axi_lite/test_AxiLiteAsync.py +++ b/tests/axi/axi_lite/test_AxiLiteAsync.py @@ -9,59 +9,172 @@ ############################################################################## # Test methodology: -# - Sweep: Keep a narrow common-clock wrapper-focused case that proves the -# cocotb-facing bridge topology and stable pass-through behavior without -# trying to force the less simulator-stable asynchronous reset branches into -# the initial regression batch. -# - Stimulus: Drive AXI-Lite writes and reads through the slave-side port into -# a cocotb RAM attached to the master-side port, then assert only the master +# - Sweep: Cover common-clock pass-through plus asynchronous active-high, +# active-low, asynchronous-reset, and pipelined FIFO configurations. +# - Stimulus: Drive AXI-Lite writes and reads through the slave-side port into a +# cocotb RAM attached to the master-side port, then assert only the master # reset while the slave side remains live in the asynchronous case. # - Checks: Successful transactions must round-trip through the bridge into the -# backing RAM, common-clock reset must restart the path cleanly, and -# post-reset traffic must recover without stale responses. +# backing RAM, common-clock reset must restart the path cleanly, post-reset +# traffic must recover without stale responses, requests held while READY is +# low must not cross the bridge, and a transaction rejected while the master +# domain is reset must never execute downstream afterwards. # - Timing: The bench drives both bridge clocks from one lockstep coroutine so -# `COMMON_CLK_G=true` is exercised as a true shared-clock configuration. +# `COMMON_CLK_G=true` is exercised as a true shared-clock configuration. The +# asynchronous case drives mAxiClk from a gateable coroutine so the test can +# hold the master domain still while the slave domain keeps running. import os import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge, Timer, with_timeout from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import ( + cancel_and_join_tasks, env_flag, env_sl, parameter_case, run_surf_vhdl_test, + sample_after_tpd, start_lockstep_clocks, ) -class TB: +# cocotb resolves `skip` when the decorator runs, so the configuration has to be +# read at import time rather than from inside a test body. +COMMON_CLK = env_flag("COMMON_CLK_G", default=False) + +# Bound every slave-side transaction so a missing fail-fast response is reported +# as a test failure instead of hanging the regression. +TXN_TIMEOUT_US = 20 + +# Distinct addresses keep the baseline, rejected, and recovery accesses from +# aliasing each other in the backing RAM. +BASELINE_ADDR = 0x040 +REJECTED_WRITE_ADDR = 0x044 +REJECTED_READ_ADDR = 0x048 +RECOVERY_ADDR = 0x04C + + +class GatedClock: + """Free-running clock that the test can stop and restart. + + `cocotb.clock.Clock` cannot be paused, and the remote-reset scenario needs + mAxiClk held low while sAxiClk keeps running. + """ + + def __init__(self, signal, period_ns): + self.signal = signal + self.half_period_ns = period_ns / 2 + self.enabled = True + signal.setimmediatevalue(0) + self._clock_task = cocotb.start_soon(self._drive()) + + async def _drive(self): + """Lifetime agent: drive the gateable clock until cocotb ends the test.""" + while True: + await Timer(self.half_period_ns, unit="ns") + if not self.enabled: + self.signal.value = 0 + continue + self.signal.value = 1 + await Timer(self.half_period_ns, unit="ns") + self.signal.value = 0 + + def stop(self): + self.enabled = False + self.signal.value = 0 + + def start(self): + self.enabled = True + + +class SourcePortMonitor: + """Counts handshakes on the slave-side AXI-Lite port. + + The bridge must never present a response that the slave side did not ask + for, so the test compares accepted requests against completed responses. + """ + def __init__(self, dut): self.dut = dut - self.common_clk = env_flag("COMMON_CLK_G", default=False) + self.counts = {"AR": 0, "R": 0, "AW": 0, "W": 0, "B": 0} + self._monitor_task = cocotb.start_soon(self._run()) + + @staticmethod + def _high(signal) -> bool: + try: + return int(signal.value) == 1 + except ValueError: + return False + + def _handshake(self, valid, ready) -> bool: + return self._high(valid) and self._high(ready) + + async def _run(self): + """Lifetime agent: monitor source handshakes until cocotb ends the test.""" + dut = self.dut + channels = ( + ("AR", dut.S_AXI_ARVALID, dut.S_AXI_ARREADY), + ("R", dut.S_AXI_RVALID, dut.S_AXI_RREADY), + ("AW", dut.S_AXI_AWVALID, dut.S_AXI_AWREADY), + ("W", dut.S_AXI_WVALID, dut.S_AXI_WREADY), + ("B", dut.S_AXI_BVALID, dut.S_AXI_BREADY), + ) + while True: + # Sample at the clock edge, before combinational logic reacts to it. + await RisingEdge(dut.sAxiClk) + for name, valid, ready in channels: + if self._handshake(valid, ready): + self.counts[name] += 1 + + +class TB: + def __init__(self, dut, drive_master=True): + self.dut = dut + self.common_clk = COMMON_CLK self.pipe_stages = int(os.environ["PIPE_STAGES_G"]) self.reset_active = env_sl("RST_POLARITY_G", default=1) + self.m_clk = None if self.common_clk: - start_lockstep_clocks(dut.sAxiClk, dut.mAxiClk, period_ns=6.0) + self._clock_task = start_lockstep_clocks(dut.sAxiClk, dut.mAxiClk, period_ns=6.0) else: - cocotb.start_soon(Clock(dut.sAxiClk, 8.0, unit="ns").start()) - cocotb.start_soon(Clock(dut.mAxiClk, 5.0, unit="ns").start()) + self._clock_task = cocotb.start_soon(Clock(dut.sAxiClk, 8.0, unit="ns").start()) + # Gateable so the remote-reset test can hold the master domain still. + self.m_clk = GatedClock(dut.mAxiClk, 5.0) dut.sAxiClkRst.setimmediatevalue(self.reset_active_value()) dut.mAxiClkRst.setimmediatevalue(self.reset_active_value()) - self.axil = AxiLiteMaster( - bus=AxiLiteBus.from_prefix(dut, "S_AXI"), - clock=dut.sAxiClk, - reset=dut.sAxiClkRst, - reset_active_level=bool(self.reset_active), - ) + if drive_master: + self.axil = AxiLiteMaster( + bus=AxiLiteBus.from_prefix(dut, "S_AXI"), + clock=dut.sAxiClk, + reset=dut.sAxiClkRst, + reset_active_level=bool(self.reset_active), + ) + else: + # Channel level tests drive the slave side port by hand, so the + # cocotbext master must not be driving the same signals. + self.axil = None + for signal in ( + dut.S_AXI_AWADDR, dut.S_AXI_AWPROT, dut.S_AXI_AWVALID, + dut.S_AXI_WDATA, dut.S_AXI_WSTRB, dut.S_AXI_WVALID, + dut.S_AXI_BREADY, + dut.S_AXI_ARADDR, dut.S_AXI_ARPROT, dut.S_AXI_ARVALID, + dut.S_AXI_RREADY, + ): + signal.setimmediatevalue(0) + self.slave = SimpleAxiLiteSlave(dut, self.reset_active) + self.source = SourcePortMonitor(dut) + + async def close(self) -> None: + await self.slave.close() def reset_active_value(self) -> int: return self.reset_active @@ -82,6 +195,54 @@ async def m_cycle(self, count=1): await RisingEdge(self.dut.mAxiClk) await self.settle() + async def write(self, addr, payload): + # Every slave-side access is bounded; a bridge that never answers is a + # failure, not a reason to stall the regression. + return await with_timeout( + self.axil.write(addr, payload), TXN_TIMEOUT_US, "us" + ) + + async def read(self, addr, length): + return await with_timeout( + self.axil.read(addr, length), TXN_TIMEOUT_US, "us" + ) + + async def drive_handshake(self, valid, ready, what, limit=64): + # Hold valid until the edge where ready is also high, sampling ready + # before the clock edge so the check matches the transfer itself. + valid.value = 1 + for _ in range(limit): + await RisingEdge(self.dut.sAxiClk) + accepted = int(ready.value) == 1 + await self.settle() + if accepted: + valid.value = 0 + return + valid.value = 0 + raise AssertionError(f"{what} was never accepted") + + async def await_high(self, signal, what, limit=64): + for _ in range(limit): + await self.s_cycle() + if int(signal.value) == 1: + return + raise AssertionError(f"{what} never asserted") + + async def consume(self, valid, ready, what, limit=128): + # Mirror of drive_handshake for the response direction: raise ready until + # the edge where valid is also high, then drop it again. + ready.value = 1 + for _ in range(limit): + await RisingEdge(self.dut.sAxiClk) + taken = int(valid.value) == 1 + await self.settle() + if taken: + ready.value = 0 + await self.settle() + return + ready.value = 0 + raise AssertionError(f"{what} was never returned") + async def reset(self): # Hold both domains in reset together so the bridge and RAM start from # a known empty state before each scenario. @@ -108,6 +269,9 @@ def __init__(self, dut, reset_active): self.dut = dut self.reset_active = reset_active self.mem = {} + # Ordered record of everything this slave accepted, so the test can + # prove a rejected request never reached the far side of the bridge. + self.handshakes = [] dut.M_AXI_AWREADY.setimmediatevalue(0) dut.M_AXI_WREADY.setimmediatevalue(0) @@ -118,8 +282,14 @@ def __init__(self, dut, reset_active): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + # The read/write responders are lifetime protocol peers owned by TB. + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) + + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) def in_reset(self) -> bool: try: @@ -127,10 +297,12 @@ def in_reset(self) -> bool: except ValueError: return True + def addresses_seen(self, channel) -> list: + return [value for kind, value in self.handshakes if kind == channel] + async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.mAxiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.mAxiClk) async def _wait_while_reset(self): while self.in_reset(): @@ -142,6 +314,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -154,6 +327,7 @@ async def _run_write(self): self.dut.M_AXI_AWREADY.value = 1 await self.cycle(1) self.dut.M_AXI_AWREADY.value = 0 + self.handshakes.append(("AW", awaddr)) while not int(self.dut.M_AXI_WVALID.value): await self._wait_while_reset() @@ -173,6 +347,7 @@ async def _run_write(self): self.dut.M_AXI_WREADY.value = 1 await self.cycle(1) self.dut.M_AXI_WREADY.value = 0 + self.handshakes.append(("W", wdata)) self.dut.M_AXI_BRESP.value = int(AxiResp.OKAY) self.dut.M_AXI_BVALID.value = 1 @@ -184,6 +359,7 @@ async def _run_write(self): self.dut.M_AXI_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() @@ -196,6 +372,7 @@ async def _run_read(self): self.dut.M_AXI_ARREADY.value = 1 await self.cycle(1) self.dut.M_AXI_ARREADY.value = 0 + self.handshakes.append(("AR", araddr)) self.dut.M_AXI_RDATA.value = self.mem.get(araddr, 0) self.dut.M_AXI_RRESP.value = int(AxiResp.OKAY) @@ -211,56 +388,662 @@ async def _run_read(self): @cocotb.test() async def bridge_round_trip_test(dut): tb = TB(dut) - await tb.reset() + try: + await tb.reset() - transactions = [ - (0x000, b"\x11\x22\x33\x44"), - (0x008, b"\xAA\xBB"), - (0x010, b"\x10\x20\x30\x40"), - ] + transactions = [ + (0x000, b"\x11\x22\x33\x44"), + (0x008, b"\xAA\xBB"), + (0x010, b"\x10\x20\x30\x40"), + ] - # Sweep a few aligned accesses so the test proves the slave-side bus can - # drive data through the bridge into the master-side backing RAM. - for addr, payload in transactions: - wr_txn = await tb.axil.write(addr, payload) - assert wr_txn.resp == AxiResp.OKAY - assert tb.slave.mem[addr].to_bytes(4, "little")[: len(payload)] == payload + # Sweep a few aligned accesses so the test proves the slave-side bus + # can drive data through the bridge into the master-side backing RAM. + for addr, payload in transactions: + wr_txn = await tb.write(addr, payload) + assert wr_txn.resp == AxiResp.OKAY + assert tb.slave.mem[addr].to_bytes(4, "little")[: len(payload)] == payload - rd_txn = await tb.axil.read(addr, len(payload)) - assert rd_txn.resp == AxiResp.OKAY - assert rd_txn.data == payload + rd_txn = await tb.read(addr, len(payload)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == payload + finally: + await tb.close() @cocotb.test() async def reset_behavior_test(dut): + tb = TB(dut) + try: + await tb.reset() + + baseline = b"\x5A\xA5\xC3\x3C" + wr_txn = await tb.write(0x020, baseline) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.read(0x020, len(baseline)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == baseline + + # In common-clock mode the DUT reduces to direct pass-through, so the + # reset coverage is restart-and-recover rather than remote-domain + # error shaping. + self_reset = tb.reset_active_value() + self_release = tb.reset_inactive_value() + tb.dut.sAxiClkRst.value = self_reset + tb.dut.mAxiClkRst.value = self_reset + await tb.s_cycle(3) + tb.dut.sAxiClkRst.value = self_release + tb.dut.mAxiClkRst.value = self_release + # Both resets are released together here, so in the asynchronous case + # each one still has to cross into the opposite domain before the bridge + # stops reporting the remote side as reset. + await tb.s_cycle(16) + await tb.m_cycle(16) + + recovery = b"\x89\x67\x45\x23" + wr_txn = await tb.write(0x024, recovery) + assert wr_txn.resp == AxiResp.OKAY + rd_txn = await tb.read(0x024, len(recovery)) + assert rd_txn.resp == AxiResp.OKAY + assert rd_txn.data == recovery + finally: + await tb.close() + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_ghost_test(dut): + """A transaction rejected while the remote domain is reset must never run. + + `COMMON_CLK_G=true` reduces the bridge to direct pass-through with no request + FIFOs, so this scenario only exists in the asynchronous configuration. + """ tb = TB(dut) await tb.reset() - baseline = b"\x5A\xA5\xC3\x3C" - wr_txn = await tb.axil.write(0x020, baseline) + # Prove the bridge is healthy before the fault is injected. + baseline = b"\x01\x02\x03\x04" + wr_txn = await tb.write(BASELINE_ADDR, baseline) assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x020, len(baseline)) + rd_txn = await tb.read(BASELINE_ADDR, len(baseline)) assert rd_txn.resp == AxiResp.OKAY assert rd_txn.data == baseline - # In common-clock mode the DUT reduces to direct pass-through, so the - # reset coverage is restart-and-recover rather than remote-domain error - # shaping. - self_reset = tb.reset_active_value() - self_release = tb.reset_inactive_value() - tb.dut.sAxiClkRst.value = self_reset - tb.dut.mAxiClkRst.value = self_reset - await tb.s_cycle(3) - tb.dut.sAxiClkRst.value = self_release - tb.dut.mAxiClkRst.value = self_release - await tb.s_cycle(3) - - recovery = b"\x89\x67\x45\x23" - wr_txn = await tb.axil.write(0x024, recovery) + # Let the downstream slave model return to its idle state before its clock + # is taken away. + await tb.m_cycle(4) + + # Stop the master clock, then assert only the master reset. The slave domain + # keeps running, so the bridge has to fail these accesses locally. + tb.m_clk.stop() + tb.dut.mAxiClkRst.value = tb.reset_active_value() + + # Give mAxiClkRst time to synchronize into the slave domain. + await tb.s_cycle(16) + + downstream_before = list(tb.slave.handshakes) + mem_before = dict(tb.slave.mem) + + # Both accesses must fail fast, inside the bounded transaction timeout. + rejected = b"\xDE\xAD\xBE\xEF" + wr_txn = await tb.write(REJECTED_WRITE_ADDR, rejected) + assert wr_txn.resp == AxiResp.SLVERR, ( + f"write during remote reset returned {wr_txn.resp!r}, expected SLVERR" + ) + rd_txn = await tb.read(REJECTED_READ_ADDR, 4) + assert rd_txn.resp == AxiResp.SLVERR, ( + f"read during remote reset returned {rd_txn.resp!r}, expected SLVERR" + ) + + # Nothing can have reached the downstream slave yet: its clock is stopped. + assert tb.slave.handshakes == downstream_before, ( + "downstream slave saw activity while mAxiClk was stopped" + ) + assert tb.slave.mem == mem_before, ( + "downstream memory changed while mAxiClk was stopped" + ) + + # Restart the master clock while the master reset is still asserted. + tb.m_clk.start() + await tb.m_cycle(8) + + # Release the master reset and let both sides finish coming out of reset. + tb.dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + # The rejected requests must not have been replayed downstream. + replayed = [ + entry + for entry in tb.slave.handshakes[len(downstream_before):] + if entry in (("AW", REJECTED_WRITE_ADDR), ("AR", REJECTED_READ_ADDR)) + ] + assert not replayed, ( + f"request rejected with SLVERR was replayed downstream after recovery: {replayed}" + ) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + "write rejected with SLVERR modified downstream memory after recovery" + ) + assert tb.slave.mem == mem_before, ( + "downstream memory changed after recovery without a new transaction" + ) + + # A fresh access must still work, and must not consume a stale response left + # over from the rejected pair. + recovery = b"\x0F\x1E\x2D\x3C" + wr_txn = await tb.write(RECOVERY_ADDR, recovery) assert wr_txn.resp == AxiResp.OKAY - rd_txn = await tb.axil.read(0x024, len(recovery)) + rd_txn = await tb.read(RECOVERY_ADDR, len(recovery)) assert rd_txn.resp == AxiResp.OKAY assert rd_txn.data == recovery + assert tb.slave.mem[RECOVERY_ADDR].to_bytes(4, "little") == recovery + + # Every response the bridge produced has to map to a request it accepted. + counts = tb.source.counts + assert counts["R"] == counts["AR"], ( + f"bridge returned {counts['R']} read responses for {counts['AR']} accepted " + "read requests" + ) + assert counts["B"] == counts["AW"], ( + f"bridge returned {counts['B']} write responses for {counts['AW']} accepted " + "write addresses" + ) + assert counts["W"] == counts["AW"], ( + f"bridge accepted {counts['W']} write data beats for {counts['AW']} accepted " + "write addresses" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_write_order_test(dut): + """A local write response must wait for both AW and W, arriving separately. + + The bridge carries the write address and the write data in separate FIFOs, so + the error response has to be paired explicitly instead of being asserted as + soon as the remote domain resets. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Hold the remote domain still and in reset. + tb.m_clk.stop() + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # Present the write address on its own. + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW during remote reset" + ) + + # No write data has been accepted yet, so there must be no write response. + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write response asserted before any write data was accepted" + ) + + # Now present the write data. + dut.S_AXI_WDATA.value = 0xA5A5A5A5 + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W during remote reset" + ) + + # The paired response must appear, and must carry the error code. + await tb.await_high(dut.S_AXI_BVALID, "write response during remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR), ( + f"write response during remote reset carried {int(dut.S_AXI_BRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + + # Accept it, then confirm a single write produced a single response. + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write response repeated for a single accepted write" + ) + + # The same pairing rule applies to reads: no response without an accepted AR. + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read response asserted with no read address accepted" + ) + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR during remote reset" + ) + await tb.await_high(dut.S_AXI_RVALID, "read response during remote reset") + assert int(dut.S_AXI_RRESP.value) == int(AxiResp.SLVERR) + + dut.S_AXI_RREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_RREADY.value = 0 + for _ in range(8): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read response repeated for a single accepted read" + ) + + # Nothing may have reached the master side, and nothing may be replayed once + # the remote domain recovers. + assert tb.slave.handshakes == [], ( + f"master side saw activity while held in reset: {tb.slave.handshakes}" + ) + + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + assert tb.slave.handshakes == [], ( + f"rejected request replayed downstream after recovery: {tb.slave.handshakes}" + ) + assert tb.slave.mem == {}, ( + f"rejected write reached downstream memory: {tb.slave.mem}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_inflight_flush_test(dut): + """A request queued before the remote reset must not survive it. + + The request FIFOs are written from the slave domain but drained from the + master domain, so a transaction can already be sitting in them when + mAxiClkRst asserts. Gating new requests is not enough; the queued one has to + be discarded rather than replayed once the remote domain recovers. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Freeze the master domain while its reset is still released, so the bridge + # accepts and queues the write but cannot forward it yet. + tb.m_clk.stop() + await tb.s_cycle(4) + + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW before remote reset" + ) + dut.S_AXI_WDATA.value = 0xDEADBEEF + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W before remote reset" + ) + + # The write is queued and still unanswered, and cannot have reached the far + # side because that clock is stopped. + assert int(dut.S_AXI_BVALID.value) == 0, ( + "write answered while the master domain was still expected to handle it" + ) + assert tb.slave.handshakes == [], ( + f"master side saw activity with its clock stopped: {tb.slave.handshakes}" + ) + + # Now reset the remote domain underneath the queued write. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # The bridge still owes a response for it, and it must be the error response. + await tb.await_high(dut.S_AXI_BVALID, "write response after remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR), ( + f"queued write answered with {int(dut.S_AXI_BRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Recover and confirm the queued write was discarded, not replayed. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + assert tb.slave.handshakes == [], ( + f"write queued before the remote reset was replayed downstream: " + f"{tb.slave.handshakes}" + ) + assert tb.slave.mem == {}, ( + f"write queued before the remote reset reached memory: {tb.slave.mem}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def remote_reset_orphan_pairing_test(dut): + """An orphaned write address must not pair with a later write data beat. + + The write address and the write data cross the bridge in separate FIFOs, so a + write that straddles the remote reset can leave an address queued with no + data behind it. If that address is not discarded, the next write's data is + committed to the wrong location. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Queue the address only, with the master domain frozen. + tb.m_clk.stop() + await tb.s_cycle(4) + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "orphan AW") + + # Reset the remote domain with that address queued and no data sent yet. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # Supply the data now. The bridge answers locally and both halves are dropped. + dut.S_AXI_WDATA.value = 0xDEADBEEF + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W during remote reset" + ) + await tb.await_high(dut.S_AXI_BVALID, "write response during remote reset") + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.SLVERR) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Recover. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + + # Issue a fresh write to a different address. + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "recovery AW") + dut.S_AXI_WDATA.value = 0x0F1E2D3C + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "recovery W") + + # Let the master side act before checking, so a misdirected write is reported + # as exactly that rather than as a missing response. + await tb.m_cycle(64) + + assert ("AW", REJECTED_WRITE_ADDR) not in tb.slave.handshakes, ( + f"abandoned write address reached the master side: {tb.slave.handshakes}" + ) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + f"recovery write data was committed to the abandoned address: {tb.slave.mem}" + ) + assert tb.slave.mem == {RECOVERY_ADDR: 0x0F1E2D3C}, ( + f"recovery write did not land correctly: {tb.slave.mem}" + ) + + await tb.await_high(dut.S_AXI_BVALID, "recovery write response", limit=128) + assert int(dut.S_AXI_BRESP.value) == int(AxiResp.OKAY) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + +@cocotb.test(skip=COMMON_CLK) +async def source_reset_stale_response_test(dut): + """A response queued when the slave domain resets must be discarded. + + The response FIFOs are written from the master domain, so a completed + response can still be queued when sAxiClkRst asserts. If it survives the + reset, the next read after recovery consumes it and returns another + address's data. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Seed two addresses with distinct data. + seeds = ((BASELINE_ADDR, 0x11111111), (RECOVERY_ADDR, 0x22222222)) + for addr, data in seeds: + dut.S_AXI_AWADDR.value = addr + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, f"seed AW {addr:#05x}" + ) + dut.S_AXI_WDATA.value = data + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, f"seed W {addr:#05x}" + ) + await tb.await_high(dut.S_AXI_BVALID, f"seed B {addr:#05x}", limit=128) + dut.S_AXI_BREADY.value = 1 + await tb.s_cycle() + dut.S_AXI_BREADY.value = 0 + + # Read one address but never take the response, so it sits in the response + # FIFO on the slave side. + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "stale AR") + await tb.await_high(dut.S_AXI_RVALID, "stale read response", limit=128) + + # Reset the slave domain with that response still queued. + dut.sAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(8) + dut.sAxiClkRst.value = tb.reset_inactive_value() + await tb.s_cycle(16) + await tb.m_cycle(16) + + # A fresh read must return its own data, not the abandoned response. + dut.S_AXI_ARADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "recovery AR") + await tb.await_high(dut.S_AXI_RVALID, "recovery read response", limit=128) + assert int(dut.S_AXI_RDATA.value) == 0x22222222, ( + f"read after a slave-domain reset returned " + f"{int(dut.S_AXI_RDATA.value):#010x}, expected 0x22222222 for its own " + "address" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def source_reset_clears_outstanding_test(dut): + """A slave-domain reset must clear the bridge's outstanding transaction state. + + The outstanding counts decide whether the bridge owes a local error response. + If a slave-domain reset leaves them stale, the next remote reset answers a + transaction the slave side already abandoned, which is a response with no + request behind it. + + This is the case that actually exercises the reset path of the registered + logic, through the sequential process when RST_ASYNC_G is true and through + the combinational next-state path when it is false. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Freeze the master domain so no real response can ever be produced, then + # leave a read accepted and unanswered. + tb.m_clk.stop() + await tb.s_cycle(4) + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR before slave reset" + ) + assert int(dut.S_AXI_RVALID.value) == 0, ( + "read answered while the master domain was frozen" + ) + + # Reset the slave domain. That abandons the read, so the bridge no longer + # owes anything for it. + dut.sAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(8) + dut.sAxiClkRst.value = tb.reset_inactive_value() + await tb.s_cycle(16) + + # Now reset the remote domain, which puts the bridge into local-answer mode. + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + # With the outstanding state cleared there is nothing to answer. + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "bridge answered a read that was abandoned by the slave-domain reset" + ) + assert int(dut.S_AXI_BVALID.value) == 0, ( + "bridge produced a write response with no write outstanding" + ) + + # And nothing may reach the master side once it recovers. + tb.m_clk.start() + await tb.m_cycle(8) + dut.mAxiClkRst.value = tb.reset_inactive_value() + await tb.m_cycle(16) + await tb.s_cycle(16) + assert tb.slave.handshakes == [], ( + f"abandoned read reached the master side after recovery: " + f"{tb.slave.handshakes}" + ) + + +@cocotb.test(skip=COMMON_CLK) +async def single_outstanding_bound_test(dut): + """The bridge allows one transaction per channel in flight, like the crossbar. + + AxiLiteCrossbar does not release a slave slot until the response completes, + so AxiLiteAsync matches that bound. It enforces the bound with its own ready + outputs rather than trusting the master to honour it, which is what lets the + remote-reset responder be a single flag per channel and still answer exactly + once per accepted request. + """ + tb = TB(dut, drive_master=False) + await tb.reset() + + # Normal operation: a second read must not be accepted while the first is + # still unanswered. + dut.S_AXI_RREADY.value = 0 + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "first AR") + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + dut.S_AXI_ARVALID.value = 1 + for _ in range(32): + await tb.s_cycle() + assert int(dut.S_AXI_ARREADY.value) == 0, ( + "bridge accepted a second read while the first was still unanswered" + ) + dut.S_AXI_ARVALID.value = 0 + await tb.settle() + assert REJECTED_READ_ADDR not in tb.slave.addresses_seen("AR"), ( + "read held while ARREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) + + # Answer the first read, after which the next one is accepted normally. + await tb.await_high(dut.S_AXI_RVALID, "first read response", limit=128) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "first read response") + + dut.S_AXI_ARADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "second AR") + await tb.await_high(dut.S_AXI_RVALID, "second read response", limit=128) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "second read response") + + # The same bound applies to writes: no second address while one is pending. + dut.S_AXI_AWADDR.value = BASELINE_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "first AW") + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + dut.S_AXI_AWVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_AWREADY.value) == 0, ( + "bridge accepted a second write address while one was still pending" + ) + dut.S_AXI_AWVALID.value = 0 + await tb.settle() + dut.S_AXI_WDATA.value = 0x5A5A5A5A + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "first W") + await tb.await_high(dut.S_AXI_BVALID, "first write response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "first write response") + + # Once the first response releases the downstream slave, none of the address + # beats held above while AWREADY was low may appear there. + await tb.m_cycle(16) + assert RECOVERY_ADDR not in tb.slave.addresses_seen("AW"), ( + "write address held while AWREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) + + # Exercise the same rule on W independently. Queue one accepted data beat + # before its address, then hold a different beat while WREADY is low. + first_wdata = 0x11223344 + blocked_wdata = 0x55667788 + dut.S_AXI_WDATA.value = first_wdata + dut.S_AXI_WSTRB.value = 0xF + await tb.drive_handshake(dut.S_AXI_WVALID, dut.S_AXI_WREADY, "W before AW") + + dut.S_AXI_WDATA.value = blocked_wdata + dut.S_AXI_WVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_WREADY.value) == 0, ( + "bridge accepted a second write data beat while one was still pending" + ) + dut.S_AXI_WVALID.value = 0 + await tb.settle() + + # Complete the first W with its address and consume its response. + dut.S_AXI_AWADDR.value = RECOVERY_ADDR + await tb.drive_handshake(dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW after W") + await tb.await_high(dut.S_AXI_BVALID, "W-before-AW response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "W-before-AW response") + assert tb.slave.mem[RECOVERY_ADDR] == first_wdata + + # A fresh address must wait for a fresh W; an unaccepted data beat left in + # the FIFO would instead pair with this address and modify memory. + dut.S_AXI_AWADDR.value = REJECTED_WRITE_ADDR + await tb.drive_handshake( + dut.S_AXI_AWVALID, dut.S_AXI_AWREADY, "AW after blocked W" + ) + await tb.m_cycle(32) + assert REJECTED_WRITE_ADDR not in tb.slave.mem, ( + "write data held while WREADY was low crossed to the master side: " + f"{tb.slave.handshakes}" + ) + + # Finish the legitimate write so the following remote-reset scenario starts + # with no partial transaction in either interface. + dut.S_AXI_WDATA.value = 0x99AABBCC + await tb.drive_handshake( + dut.S_AXI_WVALID, dut.S_AXI_WREADY, "fresh W after blocked W" + ) + await tb.await_high(dut.S_AXI_BVALID, "fresh write response", limit=128) + await tb.consume(dut.S_AXI_BVALID, dut.S_AXI_BREADY, "fresh write response") + + # Error mode: the same bound holds, and the accepted read is answered once. + tb.m_clk.stop() + dut.mAxiClkRst.value = tb.reset_active_value() + await tb.s_cycle(16) + + dut.S_AXI_ARADDR.value = REJECTED_READ_ADDR + await tb.drive_handshake( + dut.S_AXI_ARVALID, dut.S_AXI_ARREADY, "AR during remote reset" + ) + + dut.S_AXI_ARVALID.value = 1 + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_ARREADY.value) == 0, ( + "bridge accepted a second read during remote reset while the first " + "was still unanswered" + ) + dut.S_AXI_ARVALID.value = 0 + await tb.settle() + + await tb.await_high(dut.S_AXI_RVALID, "error response during remote reset") + assert int(dut.S_AXI_RRESP.value) == int(AxiResp.SLVERR), ( + f"remote-reset read answered with {int(dut.S_AXI_RRESP.value)}, " + f"expected {int(AxiResp.SLVERR)}" + ) + await tb.consume(dut.S_AXI_RVALID, dut.S_AXI_RREADY, "error response") + + for _ in range(16): + await tb.s_cycle() + assert int(dut.S_AXI_RVALID.value) == 0, ( + "error response repeated for a single accepted read" + ) PARAMETER_SWEEP = [ @@ -272,6 +1055,45 @@ async def reset_behavior_test(dut): RST_ASYNC_G="false", RST_POLARITY_G="'1'", ), + parameter_case( + "async_active_high", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'1'", + ), + # Active LOW covers the reset-polarity handling in the bridge; the remote + # reset comparisons are only exercised for one sense per case. + parameter_case( + "async_active_low", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'0'", + ), + # Asynchronous reset reaches the registered logic through the sequential + # process instead of the combinational next-state path, so it needs its own + # case to be executed at all. + parameter_case( + "async_rst_async", + COMMON_CLK_G="false", + PIPE_STAGES_G="0", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="true", + RST_POLARITY_G="'1'", + ), + # A non-zero PIPE_STAGES_G adds output registers to every channel FIFO and + # widens the worst-case outstanding transaction count. + parameter_case( + "async_pipelined", + COMMON_CLK_G="false", + PIPE_STAGES_G="2", + NUM_ADDR_BITS_G="12", + RST_ASYNC_G="false", + RST_POLARITY_G="'1'", + ), ] @@ -282,7 +1104,4 @@ def test_AxiLiteAsync(parameters): toplevel="surf.axiliteasyncipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteAsyncIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteCrossbar.py b/tests/axi/axi_lite/test_AxiLiteCrossbar.py index 8f7ac3f485..a2ff328288 100644 --- a/tests/axi/axi_lite/test_AxiLiteCrossbar.py +++ b/tests/axi/axi_lite/test_AxiLiteCrossbar.py @@ -176,7 +176,4 @@ def test_AxiLiteCrossbar(): run_surf_vhdl_test( test_file=__file__, toplevel="surf.axilitecrossbaripintegrator", - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteCrossbarIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPop.py index a8877ee9dd..65bd70b7ff 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPop.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -43,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the AXI-Lite side and the exposed FIFO interface together so @@ -108,5 +108,4 @@ def test_AxiLiteFifoPop(parameters): toplevel="surf.axilitefifopopipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPopIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPush.py b/tests/axi/axi_lite/test_AxiLiteFifoPush.py index d56f84d2e7..767e6eed04 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPush.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPush.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold both the AXI-Lite and push FIFO sides in reset long enough for @@ -108,5 +108,4 @@ def test_AxiLiteFifoPush(parameters): toplevel="surf.axilitefifopushipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPushIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py index 179e4c0769..b1abfe8a0b 100644 --- a/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py +++ b/tests/axi/axi_lite/test_AxiLiteFifoPushPop.py @@ -21,7 +21,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset all visible domains together before any descriptor-style FIFO @@ -137,5 +137,4 @@ def test_AxiLiteFifoPushPop(parameters): toplevel="surf.axilitefifopushpopipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteFifoPushPopIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteMaster.py b/tests/axi/axi_lite/test_AxiLiteMaster.py index d75a22482e..146a8ae651 100644 --- a/tests/axi/axi_lite/test_AxiLiteMaster.py +++ b/tests/axi/axi_lite/test_AxiLiteMaster.py @@ -26,7 +26,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import cancel_and_join_tasks, sample_after_tpd from cocotbext.axi import AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -51,8 +52,14 @@ def __init__(self, dut, reset_active): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + # The read/write responders are lifetime protocol peers owned by TB. + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) + + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) def in_reset(self) -> bool: try: @@ -62,8 +69,7 @@ def in_reset(self) -> bool: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self): while self.in_reset(): @@ -75,6 +81,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -124,6 +131,7 @@ async def _run_write(self): self.dut.M_AXI_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() @@ -167,6 +175,9 @@ def __init__(self, dut): self.slave = SimpleAxiLiteSlave(dut, self.reset_active) + async def close(self) -> None: + await self.slave.close() + def reset_active_value(self) -> int: return self.reset_active @@ -175,8 +186,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Drive reset and hold the request inputs idle so the state machine @@ -214,46 +224,56 @@ async def issue_request(self, *, rnw: bool, address: int, wr_data: int = 0): @cocotb.test() async def write_read_round_trip_test(dut): tb = TB(dut) - await tb.reset() + try: + await tb.reset() - ack_resp, _ = await tb.issue_request(rnw=False, address=0x24, wr_data=0x11223344) - assert ack_resp == int(AxiResp.OKAY) - assert tb.slave.mem[0x24] == 0x11223344 - assert tb.slave.last_write == (0x24, 0x11223344, 0xF, 0) + ack_resp, _ = await tb.issue_request(rnw=False, address=0x24, wr_data=0x11223344) + assert ack_resp == int(AxiResp.OKAY) + assert tb.slave.mem[0x24] == 0x11223344 + assert tb.slave.last_write == (0x24, 0x11223344, 0xF, 0) - ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x24) - assert ack_resp == int(AxiResp.OKAY) - assert ack_data == 0x11223344 - assert tb.slave.last_read == (0x24, 0) + ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x24) + assert ack_resp == int(AxiResp.OKAY) + assert ack_data == 0x11223344 + assert tb.slave.last_read == (0x24, 0) + finally: + await tb.close() @cocotb.test() async def error_and_idle_reset_test(dut): tb = TB(dut) - await tb.reset() - - tb.slave.mem[0x40] = 0xCAFEBABE - tb.slave.write_resp = AxiResp.SLVERR - tb.slave.read_resp = AxiResp.SLVERR - - ack_resp, _ = await tb.issue_request(rnw=False, address=0x40, wr_data=0xDEADBEEF) - assert ack_resp == int(AxiResp.SLVERR) - assert tb.slave.mem[0x40] == 0xCAFEBABE - - ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x40) - assert ack_resp == int(AxiResp.SLVERR) - assert ack_data == 0xCAFEBABE - - # Reassert reset after the error path so the test proves the DUT returns - # its request and ack outputs to the idle state cleanly. - tb.dut.axilRst.value = tb.reset_active_value() - await tb.cycle(2) - assert int(tb.dut.ackDone.value) == 0 - assert int(tb.dut.M_AXI_AWVALID.value) == 0 - assert int(tb.dut.M_AXI_WVALID.value) == 0 - assert int(tb.dut.M_AXI_ARVALID.value) == 0 - assert int(tb.dut.M_AXI_BREADY.value) == 1 - assert int(tb.dut.M_AXI_RREADY.value) == 1 + try: + await tb.reset() + + tb.slave.mem[0x40] = 0xCAFEBABE + tb.slave.write_resp = AxiResp.SLVERR + tb.slave.read_resp = AxiResp.SLVERR + + ack_resp, _ = await tb.issue_request( + rnw=False, + address=0x40, + wr_data=0xDEADBEEF, + ) + assert ack_resp == int(AxiResp.SLVERR) + assert tb.slave.mem[0x40] == 0xCAFEBABE + + ack_resp, ack_data = await tb.issue_request(rnw=True, address=0x40) + assert ack_resp == int(AxiResp.SLVERR) + assert ack_data == 0xCAFEBABE + + # Reassert reset after the error path so the test proves the DUT + # returns its request and ack outputs to the idle state cleanly. + tb.dut.axilRst.value = tb.reset_active_value() + await tb.cycle(2) + assert int(tb.dut.ackDone.value) == 0 + assert int(tb.dut.M_AXI_AWVALID.value) == 0 + assert int(tb.dut.M_AXI_WVALID.value) == 0 + assert int(tb.dut.M_AXI_ARVALID.value) == 0 + assert int(tb.dut.M_AXI_BREADY.value) == 1 + assert int(tb.dut.M_AXI_RREADY.value) == 1 + finally: + await tb.close() PARAMETER_SWEEP = [ @@ -277,7 +297,4 @@ def test_AxiLiteMaster(parameters): toplevel="surf.axilitemasteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteMasterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py index aada53d232..38630f72ff 100644 --- a/tests/axi/axi_lite/test_AxiLiteMasterProxy.py +++ b/tests/axi/axi_lite/test_AxiLiteMasterProxy.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -92,10 +92,4 @@ def test_AxiLiteMasterProxy(parameters): toplevel="surf.axilitemasterproxyipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-lite/ip_integrator/AxiLiteMasterProxyIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py index 737939ff7d..c2636d0c28 100644 --- a/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py +++ b/tests/axi/axi_lite/test_AxiLiteRamSyncStatusVector.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32 @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.wrRst.value = 1 @@ -87,9 +87,4 @@ def test_AxiLiteRamSyncStatusVector(parameters): toplevel="surf.axiliteramsyncstatusvectoripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/AxiLiteRamSyncStatusVectorIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRegs.py b/tests/axi/axi_lite/test_AxiLiteRegs.py index 41ece05c7b..7a48e98735 100644 --- a/tests/axi/axi_lite/test_AxiLiteRegs.py +++ b/tests/axi/axi_lite/test_AxiLiteRegs.py @@ -28,7 +28,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_flag, env_sl, parameter_case, run_surf_vhdl_test @@ -62,8 +63,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Hold the flattened register inputs stable while reset is asserted so @@ -159,7 +159,4 @@ def test_AxiLiteRegs(parameters): toplevel="surf.axiliteregsipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteRegsIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRespTimer.py b/tests/axi/axi_lite/test_AxiLiteRespTimer.py index dd680e8763..569f4b43b9 100644 --- a/tests/axi/axi_lite/test_AxiLiteRespTimer.py +++ b/tests/axi/axi_lite/test_AxiLiteRespTimer.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -55,8 +56,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) @@ -121,7 +121,4 @@ def test_AxiLiteRespTimer(parameters): toplevel="surf.axiliteresptimeripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteRespTimerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py index e743d30a7d..4a9ad5c2a5 100644 --- a/tests/axi/axi_lite/test_AxiLiteRingBuffer.py +++ b/tests/axi/axi_lite/test_AxiLiteRingBuffer.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Return both domains and the external control pins to a known idle @@ -129,5 +129,4 @@ def test_AxiLiteRingBuffer(parameters): toplevel="surf.axiliteringbufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["axi/axi-lite/ip_integrator/AxiLiteRingBufferIpIntegrator.vhd"]}, ) diff --git a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py index 38cddb2de4..679cc4c52b 100644 --- a/tests/axi/axi_lite/test_AxiLiteSequencerRam.py +++ b/tests/axi/axi_lite/test_AxiLiteSequencerRam.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test @@ -43,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(1) @@ -94,10 +94,4 @@ def test_AxiLiteSequencerRam(parameters): toplevel="surf.axilitesequencerramipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-lite/ip_integrator/AxiLiteSequencerRamIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteSlave.py b/tests/axi/axi_lite/test_AxiLiteSlave.py index 2a51a11ea8..6956b999ad 100644 --- a/tests/axi/axi_lite/test_AxiLiteSlave.py +++ b/tests/axi/axi_lite/test_AxiLiteSlave.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -57,8 +58,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) @@ -140,7 +140,4 @@ def test_AxiLiteSlave(parameters): toplevel="surf.axiliteslaveipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteSlaveIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py index 63dc108fbd..1ad641aad9 100644 --- a/tests/axi/axi_lite/test_AxiLiteWriteFilter.py +++ b/tests/axi/axi_lite/test_AxiLiteWriteFilter.py @@ -26,7 +26,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import env_sl, parameter_case, run_surf_vhdl_test @@ -45,7 +46,8 @@ def __init__(self, dut, reset_active): dut.M_AXI_BVALID.setimmediatevalue(0) dut.M_AXI_BRESP.setimmediatevalue(0) - cocotb.start_soon(self._run()) + # Lifetime AXI-Lite responder retained by the bus model. + self._responder_task = cocotb.start_soon(self._run()) def in_reset(self) -> bool: try: @@ -55,8 +57,7 @@ def in_reset(self) -> bool: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self): while self.in_reset(): @@ -66,6 +67,7 @@ async def _wait_while_reset(self): await self.cycle(1) async def _run(self): + """Lifetime agent: respond to downstream writes until the test ends.""" while True: await self._wait_while_reset() @@ -130,8 +132,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axilRst.setimmediatevalue(self.reset_active_value()) @@ -193,7 +194,4 @@ def test_AxiLiteWriteFilter(parameters): "FILTER_ADDR_0_G": "416", }, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-lite/ip_integrator/AxiLiteWriteFilterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_lite/test_AxiVersion.py b/tests/axi/axi_lite/test_AxiVersion.py index 7f1a418cdd..2f3511f137 100644 --- a/tests/axi/axi_lite/test_AxiVersion.py +++ b/tests/axi/axi_lite/test_AxiVersion.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -51,8 +52,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def reset(self): self.dut.S_AXI_ARESETN.setimmediatevalue(0) diff --git a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py index 95604324e1..2e61f98dcd 100644 --- a/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py +++ b/tests/axi/axi_stream/test_AxiStreamBatchingFifo.py @@ -21,7 +21,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -40,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -92,12 +93,4 @@ def test_AxiStreamBatchingFifo(parameters): toplevel="surf.axistreambatchingfifoipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamBatchingFifoIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamCombiner.py b/tests/axi/axi_stream/test_AxiStreamCombiner.py index 50f6117b4d..d499ea84ce 100644 --- a/tests/axi/axi_stream/test_AxiStreamCombiner.py +++ b/tests/axi/axi_stream/test_AxiStreamCombiner.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -44,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -105,7 +105,4 @@ def test_AxiStreamCombiner(parameters): toplevel="surf.axistreamcombineripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamCombinerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamCompact.py b/tests/axi/axi_stream/test_AxiStreamCompact.py index c409f2d297..28c58f1164 100644 --- a/tests/axi/axi_stream/test_AxiStreamCompact.py +++ b/tests/axi/axi_stream/test_AxiStreamCompact.py @@ -25,8 +25,10 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + +from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -48,12 +50,12 @@ def __init__(self, dut): dut.S_AXIS_TID.setimmediatevalue(0) dut.S_AXIS_TUSER.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(1) - cocotb.start_soon(self._monitor()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor()) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -62,9 +64,9 @@ async def reset(self): await self.cycle(3) async def _monitor(self): + """Lifetime agent: collect compacted output beats until the test ends.""" while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) if int(self.dut.M_AXIS_TVALID.value) and int(self.dut.M_AXIS_TREADY.value): self.rx_beats.append( ( @@ -85,11 +87,7 @@ async def drive_beat(self, *, data: int, keep: int, last: int, dest: int, tid: i self.dut.S_AXIS_TID.value = tid self.dut.S_AXIS_TUSER.value = user & self.user_mask self.dut.S_AXIS_TVALID.value = 1 - while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") - if int(self.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(self.dut.S_AXIS_TREADY, clk=self.dut.axisClk) self.dut.S_AXIS_TVALID.value = 0 async def drive_payload(self, payload: bytes, *, chunk_size: int, dest: int, tid: int, user: int): @@ -185,7 +183,4 @@ def test_AxiStreamCompact(parameters): toplevel="surf.axistreamcompactipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamCompactIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamConcat.py b/tests/axi/axi_stream/test_AxiStreamConcat.py index 81bf16f0be..9fa0390d23 100644 --- a/tests/axi/axi_stream/test_AxiStreamConcat.py +++ b/tests/axi/axi_stream/test_AxiStreamConcat.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -42,8 +43,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -80,7 +80,4 @@ def test_AxiStreamConcat(parameters): toplevel="surf.axistreamconcatipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamConcatIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamDeMux.py b/tests/axi/axi_stream/test_AxiStreamDeMux.py index e30024a99c..9d46cfe145 100644 --- a/tests/axi/axi_stream/test_AxiStreamDeMux.py +++ b/tests/axi/axi_stream/test_AxiStreamDeMux.py @@ -364,7 +364,4 @@ def test_AxiStreamDeMux(case): toplevel="surf.axistreamdemuxipintegrator", parameters=case["parameters"], extra_env=extra_env, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamDeMuxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py index 626bd4b6ce..af5fb526ab 100644 --- a/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py +++ b/tests/axi/axi_stream/test_AxiStreamFifoV2IpIntegrator.py @@ -39,6 +39,7 @@ from cocotb.triggers import RisingEdge, Timer, with_timeout from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource +from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import env_flag, run_surf_vhdl_test, start_lockstep_clocks @@ -103,7 +104,11 @@ def __init__(self, dut): self.rx_beat_users = [] if self.clock_mode == "lockstep": - start_lockstep_clocks(dut.S_AXIS_ACLK, dut.M_AXIS_ACLK, period_ns=5.0) + self._clock_task = start_lockstep_clocks( + dut.S_AXIS_ACLK, + dut.M_AXIS_ACLK, + period_ns=5.0, + ) else: cocotb.start_soon(Clock(dut.S_AXIS_ACLK, 5.0, unit="ns").start()) cocotb.start_soon(Clock(dut.M_AXIS_ACLK, 7.0, unit="ns").start()) @@ -120,8 +125,11 @@ def __init__(self, dut): dut.M_AXIS_TREADY.setimmediatevalue(0) dut.fifoPauseThresh.setimmediatevalue((1 << self.fifo_addr_width) - 1) - cocotb.start_soon(self._monitor_source_handshakes()) - cocotb.start_soon(self._monitor_sink_handshakes()) + # Lifetime handshake monitors retained by the bench. + self._monitor_tasks = ( + cocotb.start_soon(self._monitor_source_handshakes()), + cocotb.start_soon(self._monitor_sink_handshakes()), + ) async def settle(self): await Timer(1, unit="ns") @@ -137,6 +145,7 @@ async def cycle_sink(self, count=1): await self.settle() async def _monitor_source_handshakes(self): + """Lifetime agent: record source handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.S_AXIS_ACLK) @@ -146,6 +155,7 @@ async def _monitor_source_handshakes(self): self.tx_cycles.append(cycle) async def _monitor_sink_handshakes(self): + """Lifetime agent: record sink handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.M_AXIS_ACLK) @@ -230,11 +240,10 @@ async def send_manual_frame(self, payload: bytes, *, tid: int, tdest: int, tuser tdest=tdest, tuser=tuser_values[index], ) - while True: - await RisingEdge(self.dut.S_AXIS_ACLK) - await self.settle() - if int(self.dut.S_AXIS_TVALID.value) and int(self.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready( + self.dut.S_AXIS_TREADY, + clk=self.dut.S_AXIS_ACLK, + ) self.clear_source() await self.cycle_source(1) @@ -333,12 +342,9 @@ async def stream_round_trip_test(dut): assert int(tb.dut.sAxisOverflow.value) == 0 -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_METADATA_TRUNCATION", default=False)) async def metadata_truncation_test(dut): tb = TB(dut) - if not tb.test_metadata_truncation: - return - await tb.reset() tb.start_agents() @@ -356,12 +362,9 @@ async def metadata_truncation_test(dut): assert scalar_tuser(rx_frame.tuser) == (scalar_tuser(frame.tuser) & mask(tb.m_user_width)) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_FRAME_READY", default=False)) async def frame_ready_release_and_last_user_test(dut): tb = TB(dut) - if not tb.test_frame_ready: - return - await tb.reset() tb.clear_samples() tb.dut.M_AXIS_TREADY.value = 1 @@ -372,20 +375,12 @@ async def frame_ready_release_and_last_user_test(dut): for index, data_byte in enumerate(payload[:-1]): tb.drive_source_beat(data_byte, last=False, tid=0x1, tdest=0x2, tuser=beat_users[index]) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) assert tb.rx_cycles == [] assert int(tb.dut.M_AXIS_TVALID.value) == 0 tb.drive_source_beat(payload[-1], last=True, tid=0x1, tdest=0x2, tuser=beat_users[-1]) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) tb.clear_source() received = await with_timeout(capture_task, 2, "us") @@ -397,31 +392,20 @@ async def frame_ready_release_and_last_user_test(dut): assert received["last_sideband"] == [beat_users[-1]] * len(payload) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_THRESHOLD_PREFILL", default=False)) async def threshold_prefill_release_test(dut): tb = TB(dut) - if not tb.test_threshold_prefill: - return - await tb.reset() tb.dut.M_AXIS_TREADY.value = 0 payload = b"\x21\x22\x23" for data_byte in payload[: tb.valid_thold - 1]: tb.drive_source_beat(data_byte, last=False, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) assert int(tb.dut.M_AXIS_TVALID.value) == 0 tb.drive_source_beat(payload[tb.valid_thold - 1], last=False, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) await tb.wait_for_output_valid(timeout_cycles=4) assert int(tb.dut.M_AXIS_TVALID.value) == 1 @@ -437,12 +421,9 @@ async def threshold_prefill_release_test(dut): await tb.cycle_sink(2) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_BURST_BEHAVIOR", default=False)) async def burst_mode_release_test(dut): tb = TB(dut) - if not tb.test_burst_behavior: - return - await tb.reset() tb.clear_samples() tb.dut.M_AXIS_TREADY.value = 1 @@ -468,23 +449,16 @@ async def burst_mode_release_test(dut): assert any(later > earlier + 1 for earlier, later in zip(tb.rx_cycles, tb.rx_cycles[1:])) -@cocotb.test() +@cocotb.test(skip=not env_flag("TEST_DYNAMIC_PAUSE", default=False)) async def dynamic_pause_threshold_test(dut): tb = TB(dut) - if not tb.test_dynamic_pause: - return - await tb.reset() tb.dut.fifoPauseThresh.value = 1 tb.dut.M_AXIS_TREADY.value = 0 for data_byte in [0x40, 0x41, 0x42]: tb.drive_source_beat(data_byte, last=True, tid=0x1, tdest=0x1, tuser=0) - while True: - await RisingEdge(tb.dut.S_AXIS_ACLK) - await tb.settle() - if int(tb.dut.S_AXIS_TVALID.value) and int(tb.dut.S_AXIS_TREADY.value): - break + await wait_sampled_ready(tb.dut.S_AXIS_TREADY, clk=tb.dut.S_AXIS_ACLK) if int(tb.dut.sAxisPause.value): break diff --git a/tests/axi/axi_stream/test_AxiStreamFlush.py b/tests/axi/axi_stream/test_AxiStreamFlush.py index c26edda82c..743425062d 100644 --- a/tests/axi/axi_stream/test_AxiStreamFlush.py +++ b/tests/axi/axi_stream/test_AxiStreamFlush.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -108,7 +108,4 @@ def test_AxiStreamFlush(parameters): toplevel="surf.axistreamflushipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamFlushIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py index 76e71b8cb7..abd1d55e62 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameBuffer.py @@ -31,7 +31,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks, parameter_case @@ -99,8 +101,7 @@ def from_generics(cls, dut): async def cycle(self, clk, count=1): for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) # Wait for one cycle of the slowest clock async def cycleSlowest(self, count=1): @@ -298,11 +299,4 @@ def test_AxiStreamFrameBuffer(parameters): toplevel="surf.axistreamframebufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamFrameBufferIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py index 4455a550a3..7e6f8faf0c 100644 --- a/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py +++ b/tests/axi/axi_stream/test_AxiStreamFrameRateLimiter.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -87,7 +87,4 @@ def test_AxiStreamFrameRateLimiter(parameters): toplevel="surf.axistreamframeratelimiteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamFrameRateLimiterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearbox.py b/tests/axi/axi_stream/test_AxiStreamGearbox.py index 54c83810f2..f82d026955 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearbox.py +++ b/tests/axi/axi_stream/test_AxiStreamGearbox.py @@ -80,7 +80,4 @@ def test_AxiStreamGearbox(parameters): toplevel="surf.axistreamgearboxipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py index 80ffa85c3d..8cbec10ecf 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxPack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxPack.py @@ -23,7 +23,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.axi_stream.gearbox_reference import pack_words, words_to_bytes @@ -43,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -75,7 +76,4 @@ def test_AxiStreamGearboxPack(parameters): toplevel="surf.axistreamgearboxpackipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxPackIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py index d2831c64db..646764c2f2 100644 --- a/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py +++ b/tests/axi/axi_stream/test_AxiStreamGearboxUnpack.py @@ -24,7 +24,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.axi.axi_stream.gearbox_reference import unpack_words, words_to_bytes @@ -44,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -76,7 +77,4 @@ def test_AxiStreamGearboxUnpack(parameters): toplevel="surf.axistreamgearboxunpackipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamGearboxUnpackIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMon.py b/tests/axi/axi_stream/test_AxiStreamMon.py index 8368a80497..ab18e882d3 100644 --- a/tests/axi/axi_stream/test_AxiStreamMon.py +++ b/tests/axi/axi_stream/test_AxiStreamMon.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -78,10 +78,4 @@ def test_AxiStreamMon(parameters): toplevel="surf.axistreammonipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamMonIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py index d6af03fcab..758c54cefe 100644 --- a/tests/axi/axi_stream/test_AxiStreamMonAxiL.py +++ b/tests/axi/axi_stream/test_AxiStreamMonAxiL.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.axi.utils import axil_read_u32 @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axisRst.value = 1 @@ -87,11 +87,4 @@ def test_AxiStreamMonAxiL(parameters): toplevel="surf.axistreammonaxilipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamMonAxiLIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamMux.py b/tests/axi/axi_stream/test_AxiStreamMux.py index 33f4d5c567..c57c67cfa8 100644 --- a/tests/axi/axi_stream/test_AxiStreamMux.py +++ b/tests/axi/axi_stream/test_AxiStreamMux.py @@ -451,7 +451,4 @@ def test_AxiStreamMux(case): toplevel="surf.axistreammuxipintegrator", parameters=case["parameters"], extra_env=extra_env, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamMuxIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamPipeline.py b/tests/axi/axi_stream/test_AxiStreamPipeline.py index 996a8c5271..0282690174 100644 --- a/tests/axi/axi_stream/test_AxiStreamPipeline.py +++ b/tests/axi/axi_stream/test_AxiStreamPipeline.py @@ -66,9 +66,10 @@ def __init__(self, dut): # Record the cycle where source and sink handshakes complete so the # tests can talk about pipeline latency in exact clock cycles. - cocotb.start_soon(self._monitor_handshakes()) + self._monitor_task = cocotb.start_soon(self._monitor_handshakes()) async def _monitor_handshakes(self): + """Lifetime agent: record pipeline handshakes until the test ends.""" cycle = 0 while True: await RisingEdge(self.dut.axisClk) @@ -273,17 +274,11 @@ async def latency_and_backpressure_test(dut): tb.drive_source_idle() -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def reset_behavior_test(dut): tb = TB(dut) await tb.reset() - # The zero-stage generate path is purely combinational, so reset should not - # be treated as a stateful flush path in this bench. Only the registered - # cases below are expected to clear buffered data on reset. - if tb.pipe_stages == 0: - return - dut.M_AXIS_TREADY.value = 0 tb.drive_source( valid=1, @@ -353,7 +348,4 @@ def test_AxiStreamPipeline(parameters): toplevel="surf.axistreampipelineipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamPipelineIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py index 032c49c1f7..18da81add0 100644 --- a/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py +++ b/tests/axi/axi_stream/test_AxiStreamPrbsFlowCtrl.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.setimmediatevalue(1) @@ -81,7 +81,4 @@ def test_AxiStreamPrbsFlowCtrl(parameters): toplevel="surf.axistreamprbsflowctrlipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamPrbsFlowCtrlIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamRepeater.py b/tests/axi/axi_stream/test_AxiStreamRepeater.py index 56cdfb383e..f14e82bec1 100644 --- a/tests/axi/axi_stream/test_AxiStreamRepeater.py +++ b/tests/axi/axi_stream/test_AxiStreamRepeater.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -82,7 +82,4 @@ def test_AxiStreamRepeater(parameters): toplevel="surf.axistreamrepeateripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamRepeaterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamResize.py b/tests/axi/axi_stream/test_AxiStreamResize.py index 09ba9fc1cc..475923249c 100644 --- a/tests/axi/axi_stream/test_AxiStreamResize.py +++ b/tests/axi/axi_stream/test_AxiStreamResize.py @@ -60,7 +60,8 @@ def __init__(self, dut): dut.S_SIDE_BAND.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(0) - cocotb.start_soon(self._monitor_sideband()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_sideband()) def reset_active_value(self) -> int: return self.reset_active @@ -77,6 +78,7 @@ async def cycle(self, count=1): await self.settle() async def _monitor_sideband(self): + """Lifetime agent: collect resized sidebands until the test ends.""" while True: await RisingEdge(self.dut.axisClk) await self.settle() @@ -186,17 +188,16 @@ async def backpressure_and_reset_test(dut): tb.dut.M_AXIS_TREADY.value = 1 await send_task await tb.wait_for_output_clear(timeout_cycles=4) - return - - await send_task - tb.dut.axisRst.value = tb.reset_active_value() - await tb.wait_for_output_clear(timeout_cycles=tb.pipe_stages + 8) - assert int(tb.dut.M_AXIS_TVALID.value) == 0 - assert int(tb.dut.M_SIDE_BAND.value) == 0 + else: + await send_task + tb.dut.axisRst.value = tb.reset_active_value() + await tb.wait_for_output_clear(timeout_cycles=tb.pipe_stages + 8) + assert int(tb.dut.M_AXIS_TVALID.value) == 0 + assert int(tb.dut.M_SIDE_BAND.value) == 0 - tb.dut.axisRst.value = tb.reset_inactive_value() - tb.dut.M_AXIS_TREADY.value = 1 - await tb.cycle(2) + tb.dut.axisRst.value = tb.reset_inactive_value() + tb.dut.M_AXIS_TREADY.value = 1 + await tb.cycle(2) PARAMETER_SWEEP = [ @@ -237,7 +238,4 @@ def test_AxiStreamResize(parameters): toplevel="surf.axistreamresizeipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamResizeIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py index 66fa11e503..52aecbd354 100644 --- a/tests/axi/axi_stream/test_AxiStreamRingBuffer.py +++ b/tests/axi/axi_stream/test_AxiStreamRingBuffer.py @@ -22,7 +22,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp, AxiStreamBus, AxiStreamSink from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -46,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.dataRst.value = 1 @@ -109,11 +110,4 @@ def test_AxiStreamRingBuffer(parameters): toplevel="surf.axistreamringbufferipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamRingBufferIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamScatterGather.py b/tests/axi/axi_stream/test_AxiStreamScatterGather.py index 6768ac38f6..d20fb78f11 100644 --- a/tests/axi/axi_stream/test_AxiStreamScatterGather.py +++ b/tests/axi/axi_stream/test_AxiStreamScatterGather.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Reset the stream and AXI-Lite paths together so the first sequence of @@ -133,12 +133,4 @@ def test_AxiStreamScatterGather(parameters): toplevel="surf.axistreamscattergatheripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/AxiStreamScatterGatherIpIntegrator.vhd", - ], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamShift.py b/tests/axi/axi_stream/test_AxiStreamShift.py index b7c4575039..f6e04424f2 100644 --- a/tests/axi/axi_stream/test_AxiStreamShift.py +++ b/tests/axi/axi_stream/test_AxiStreamShift.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -84,7 +84,4 @@ def test_AxiStreamShift(parameters): toplevel="surf.axistreamshiftipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamShiftIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamSplitter.py b/tests/axi/axi_stream/test_AxiStreamSplitter.py index ae114a2396..a376789eec 100644 --- a/tests/axi/axi_stream/test_AxiStreamSplitter.py +++ b/tests/axi/axi_stream/test_AxiStreamSplitter.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -44,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -84,7 +84,4 @@ def test_AxiStreamSplitter(parameters): toplevel="surf.axistreamsplitteripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamSplitterIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTap.py b/tests/axi/axi_stream/test_AxiStreamTap.py index 611055127c..19cca10937 100644 --- a/tests/axi/axi_stream/test_AxiStreamTap.py +++ b/tests/axi/axi_stream/test_AxiStreamTap.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -96,7 +96,4 @@ def test_AxiStreamTap(parameters): toplevel="surf.axistreamtapipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTapIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTimer.py b/tests/axi/axi_stream/test_AxiStreamTimer.py index 45fe2667c3..3ecffdb972 100644 --- a/tests/axi/axi_stream/test_AxiStreamTimer.py +++ b/tests/axi/axi_stream/test_AxiStreamTimer.py @@ -19,7 +19,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -46,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -102,7 +102,4 @@ def test_AxiStreamTimer(parameters): toplevel="surf.axistreamtimeripintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTimerIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py index 24dda10397..3414916087 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerAppend.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -37,8 +38,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -76,7 +76,4 @@ def test_AxiStreamTrailerAppend(parameters): toplevel="surf.axistreamtrailerappendipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTrailerAppendIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py index 3551745fbd..f8df9c5086 100644 --- a/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py +++ b/tests/axi/axi_stream/test_AxiStreamTrailerRemove.py @@ -19,7 +19,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -36,8 +37,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def reset(self): self.dut.axisRst.setimmediatevalue(1) @@ -72,7 +72,4 @@ def test_AxiStreamTrailerRemove(parameters): toplevel="surf.axistreamtrailerremoveipintegrator", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": ["axi/axi-stream/ip_integrator/AxiStreamTrailerRemoveIpIntegrator.vhd"], - }, ) diff --git a/tests/axi/bridge/test_AxiLiteToIpBus.py b/tests/axi/bridge/test_AxiLiteToIpBus.py index 55f98e5825..b120ac5ac7 100644 --- a/tests/axi/bridge/test_AxiLiteToIpBus.py +++ b/tests/axi/bridge/test_AxiLiteToIpBus.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -37,12 +38,12 @@ def __init__(self, dut): dut.ipbRdata.setimmediatevalue(0) dut.ipbAck.setimmediatevalue(0) dut.ipbErr.setimmediatevalue(0) - cocotb.start_soon(self._ipb_model()) + # Lifetime IPbus protocol peer retained by the bench. + self._ipb_task = cocotb.start_soon(self._ipb_model()) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -55,11 +56,11 @@ def start_agents(self): self.axi = AxiLiteMaster(AxiLiteBus.from_prefix(self.dut, "S_AXI"), self.dut.axiClk, self.dut.axiRst) async def _ipb_model(self): + """Lifetime agent: serve IPbus requests until the test ends.""" pending = None delay = 0 while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.ipbAck.value = 0 if pending is None and int(self.dut.ipbStrobe.value): pending = { diff --git a/tests/axi/bridge/test_AxiToAxiLite.py b/tests/axi/bridge/test_AxiToAxiLite.py index 5714b1e5ca..6d3ff8f04a 100644 --- a/tests/axi/bridge/test_AxiToAxiLite.py +++ b/tests/axi/bridge/test_AxiToAxiLite.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiBus, AxiLiteBus, AxiLiteRam, AxiMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -40,8 +41,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): # Hold reset across the bridge and both shim layers before issuing the diff --git a/tests/axi/bridge/test_IpBusToAxiLite.py b/tests/axi/bridge/test_IpBusToAxiLite.py index fa5faebf1a..846ba93d17 100644 --- a/tests/axi/bridge/test_IpBusToAxiLite.py +++ b/tests/axi/bridge/test_IpBusToAxiLite.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -59,11 +59,14 @@ async def ipb_request(self, *, address: int, write: bool, data: int = 0): self.dut.ipbWdata.value = data self.dut.ipbWrite.value = int(write) self.dut.ipbStrobe.value = 1 - while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + for _ in range(1024): + await sample_after_tpd(self.dut.axiClk) if int(self.dut.ipbAck.value): break + else: + raise AssertionError( + f"Timed out waiting for IPbus acknowledgement at address 0x{address:08x}" + ) result = (int(self.dut.ipbRdata.value), int(self.dut.ipbErr.value)) self.dut.ipbStrobe.value = 0 self.dut.ipbWrite.value = 0 diff --git a/tests/axi/bridge/test_SlvArraytoAxiLite.py b/tests/axi/bridge/test_SlvArraytoAxiLite.py index 5b00ce66cf..79907176cc 100644 --- a/tests/axi/bridge/test_SlvArraytoAxiLite.py +++ b/tests/axi/bridge/test_SlvArraytoAxiLite.py @@ -20,7 +20,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteRam from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): # Hold the producer side and AXI-Lite side in reset together so the diff --git a/tests/axi/dma/test_AxiStreamDma.py b/tests/axi/dma/test_AxiStreamDma.py index 104ba133ae..7e57be84b5 100644 --- a/tests/axi/dma/test_AxiStreamDma.py +++ b/tests/axi/dma/test_AxiStreamDma.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -89,7 +89,6 @@ def test_AxiStreamDma(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaIpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaFifo.py b/tests/axi/dma/test_AxiStreamDmaFifo.py index 0f5676beea..c791a5e953 100644 --- a/tests/axi/dma/test_AxiStreamDmaFifo.py +++ b/tests/axi/dma/test_AxiStreamDmaFifo.py @@ -21,7 +21,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -38,8 +39,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -84,7 +84,6 @@ def test_AxiStreamDmaFifo(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaFifoIpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaRead.py b/tests/axi/dma/test_AxiStreamDmaRead.py index 05a3da4703..8c5c91674b 100644 --- a/tests/axi/dma/test_AxiStreamDmaRead.py +++ b/tests/axi/dma/test_AxiStreamDmaRead.py @@ -20,7 +20,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus from tests.common.regression_utils import run_surf_vhdl_test @@ -47,8 +48,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) diff --git a/tests/axi/dma/test_AxiStreamDmaRingRead.py b/tests/axi/dma/test_AxiStreamDmaRingRead.py index 4df421fc04..d960bc8077 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingRead.py +++ b/tests/axi/dma/test_AxiStreamDmaRingRead.py @@ -20,7 +20,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiLiteBus, AxiLiteRam, @@ -51,8 +53,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axilRst.value = 1 @@ -136,9 +137,6 @@ def test_AxiStreamDmaRingRead(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/MasterAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaRingReadIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaRingWrite.py b/tests/axi/dma/test_AxiStreamDmaRingWrite.py index ceccee2033..2398a6e8fc 100644 --- a/tests/axi/dma/test_AxiStreamDmaRingWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaRingWrite.py @@ -21,7 +21,9 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiLiteBus, AxiLiteMaster, @@ -55,8 +57,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axilRst.value = 1 @@ -139,9 +140,6 @@ def test_AxiStreamDmaRingWrite(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaRingWriteIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2.py b/tests/axi/dma/test_AxiStreamDmaV2.py index 951c655c04..9de6336258 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2.py +++ b/tests/axi/dma/test_AxiStreamDmaV2.py @@ -23,7 +23,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -39,8 +40,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -98,7 +98,6 @@ def test_AxiStreamDmaV2(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2IpIntegrator.vhd", ], }, diff --git a/tests/axi/dma/test_AxiStreamDmaV2Desc.py b/tests/axi/dma/test_AxiStreamDmaV2Desc.py index eb1389e044..1febb38cbe 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Desc.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Desc.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test @@ -53,8 +54,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -111,7 +111,6 @@ def test_AxiStreamDmaV2Desc(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2DescIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py index adf747a452..d2b9e757bc 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Fifo.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Fifo.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.common.regression_utils import run_surf_vhdl_test, start_lockstep_clocks @@ -41,8 +42,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self): self.dut.axiRst.value = 1 @@ -132,9 +132,6 @@ def test_AxiStreamDmaV2Fifo(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2FifoIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py index deee5ac728..ee8d5a411c 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py +++ b/tests/axi/dma/test_AxiStreamDmaV2FifoLoopback.py @@ -26,7 +26,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import ( AxiBus, AxiRam, @@ -58,8 +59,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -112,9 +112,6 @@ def test_AxiStreamDmaV2FifoLoopback(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaV2FifoIpIntegrator.vhd", ], diff --git a/tests/axi/dma/test_AxiStreamDmaV2Read.py b/tests/axi/dma/test_AxiStreamDmaV2Read.py index 12a33d0f1c..158943b0b3 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Read.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Read.py @@ -19,22 +19,15 @@ # forced internal state advance. import os -from pathlib import Path import cocotb import pytest -from cocotb_test.simulator import run from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamRead, AxiReadBus, AxiStreamBus, AxiStreamSink -from tests.common.regression_utils import ( - COMMON_VHDL_COMPILE_ARGS, - TESTS_ROOT, - build_vhdl_sources, - merge_vhdl_sources, - cocotb_module_name_from_test_file, -) +from tests.common.regression_utils import run_surf_vhdl_test class TB: @@ -52,8 +45,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -150,27 +142,15 @@ async def single_descriptor_read_test(dut): ), ], ) -def test_AxiStreamDmaV2Read(case_env, request): - test_file = Path(__file__) - rel_parent = test_file.resolve().relative_to(TESTS_ROOT).parent - - run( - module=cocotb_module_name_from_test_file(test_file), +def test_AxiStreamDmaV2Read(case_env): + run_surf_vhdl_test( + test_file=__file__, toplevel="surf.axistreamdmav2readipintegrator", - toplevel_lang="vhdl", - vhdl_sources=merge_vhdl_sources( - build_vhdl_sources(), - { - "surf": [ - "axi/dma/rtl/v2/AxiStreamDmaV2Read.vhd", - "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", - "axi/dma/ip_integrator/AxiStreamDmaV2ReadIpIntegrator.vhd", - ], - }, - ), - parameters={}, - sim_build=str((TESTS_ROOT / "sim_build" / rel_parent / f"{test_file.stem}.{request.node.callspec.id}")), - extra_env={key: str(value) for key, value in case_env.items()}, - simulator="ghdl", - vhdl_compile_args=COMMON_VHDL_COMPILE_ARGS, + extra_env=case_env, + extra_vhdl_sources={ + "surf": [ + "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", + "axi/dma/ip_integrator/AxiStreamDmaV2ReadIpIntegrator.vhd", + ], + }, ) diff --git a/tests/axi/dma/test_AxiStreamDmaV2Write.py b/tests/axi/dma/test_AxiStreamDmaV2Write.py index 1ce6fbfc4d..0748652c2e 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2Write.py +++ b/tests/axi/dma/test_AxiStreamDmaV2Write.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiWriteBus from tests.common.regression_utils import hdl_parameters_from, run_surf_vhdl_test @@ -50,13 +51,15 @@ def __init__(self, dut): dut.axiWriteCtrlOver.setimmediatevalue(0) dut.dmaWrDescAckValid.setimmediatevalue(0) dut.dmaWrDescRetAck.setimmediatevalue(0) - cocotb.start_soon(self._descriptor_responder()) - cocotb.start_soon(self._monitor_aw()) + # Lifetime descriptor peer and handshake monitor owned by the bench. + self._lifetime_tasks = ( + cocotb.start_soon(self._descriptor_responder()), + cocotb.start_soon(self._monitor_aw()), + ) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -71,11 +74,11 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2**16) async def _descriptor_responder(self): + """Lifetime agent: acknowledge DMA descriptors until the test ends.""" max_size = int(os.environ.get("DESC_MAX_SIZE", "32"), 0) timeout = int(os.environ.get("DESC_TIMEOUT", "32"), 0) while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.dmaWrDescAckValid.value = 0 if int(self.dut.dmaWrDescReqValid.value): self.dut.dmaWrDescAckAddress.value = int(os.environ.get("WRITE_ADDR", "0x40"), 0) @@ -89,9 +92,9 @@ async def _descriptor_responder(self): self.dut.dmaWrDescAckValid.value = 1 async def _monitor_aw(self): + """Lifetime agent: record DMA write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_log.append( ( diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py index c3e30cafed..1c9c6b37d0 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteContinue.py @@ -28,7 +28,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiStreamBus, AxiStreamFrame, AxiStreamSource, AxiWriteBus from tests.common.regression_utils import hdl_parameters_from, run_surf_vhdl_test @@ -63,16 +64,18 @@ def __init__(self, dut): dut.axiWriteCtrlOver.setimmediatevalue(0) dut.dmaWrDescAckValid.setimmediatevalue(0) dut.dmaWrDescRetAck.setimmediatevalue(0) - cocotb.start_soon(self._descriptor_responder()) - cocotb.start_soon(self._monitor_aw()) + # Lifetime descriptor peer and handshake monitor owned by the bench. + self._lifetime_tasks = ( + cocotb.start_soon(self._descriptor_responder()), + cocotb.start_soon(self._monitor_aw()), + ) def buf_addr(self, i): return self.base + i * self.stride async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -87,10 +90,10 @@ def start_agents(self): self.ram = AxiRamWrite(AxiWriteBus.from_prefix(self.dut, "M_AXI"), self.dut.axiClk, self.dut.axiRst, size=2 ** 16) async def _descriptor_responder(self): + """Lifetime agent: acknowledge DMA descriptors until the test ends.""" acked = False while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) self.dut.dmaWrDescAckValid.value = 0 req = int(self.dut.dmaWrDescReqValid.value) if not req: @@ -110,9 +113,9 @@ async def _descriptor_responder(self): self.dut.dmaWrDescAckValid.value = 1 async def _monitor_aw(self): + """Lifetime agent: record DMA write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_log.append((int(self.dut.M_AXI_AWADDR.value), int(self.dut.M_AXI_AWLEN.value))) diff --git a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py index 5dd9fc06f7..87c364693b 100644 --- a/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py +++ b/tests/axi/dma/test_AxiStreamDmaV2WriteMux.py @@ -24,7 +24,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiResp, AxiWriteBus from tests.common.regression_utils import run_surf_vhdl_test @@ -75,8 +76,7 @@ async def issue_write(self, address: int, payload: bytes): aw_done = False w_done = False while not (aw_done and w_done): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) aw_done = aw_done or ( logic_int(getattr(self.dut, f"{self.prefix}_AWVALID").value) and logic_int(getattr(self.dut, f"{self.prefix}_AWREADY").value) @@ -92,11 +92,9 @@ async def issue_write(self, address: int, payload: bytes): getattr(self.dut, f"{self.prefix}_BREADY").value = 1 while not logic_int(getattr(self.dut, f"{self.prefix}_BVALID").value): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) resp = int(getattr(self.dut, f"{self.prefix}_BRESP").value) - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) getattr(self.dut, f"{self.prefix}_BREADY").value = 0 return resp @@ -113,12 +111,12 @@ def __init__(self, dut): dut.axiRst.setimmediatevalue(1) dut.mAxiWriteCtrlPause.setimmediatevalue(0) dut.mAxiWriteCtrlOver.setimmediatevalue(0) - cocotb.start_soon(self._monitor_aw()) + # Lifetime monitor retained by the bench until cocotb ends the test. + self._monitor_task = cocotb.start_soon(self._monitor_aw()) async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.setimmediatevalue(1) @@ -136,9 +134,9 @@ def start_agents(self): ) async def _monitor_aw(self): + """Lifetime agent: record muxed write addresses until the test ends.""" while True: - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) if logic_int(self.dut.M_AXI_AWVALID.value) and logic_int(self.dut.M_AXI_AWREADY.value): self.aw_order.append(int(self.dut.M_AXI_AWADDR.value)) diff --git a/tests/axi/dma/test_AxiStreamDmaWrite.py b/tests/axi/dma/test_AxiStreamDmaWrite.py index 16dc78d950..8f24f22f24 100644 --- a/tests/axi/dma/test_AxiStreamDmaWrite.py +++ b/tests/axi/dma/test_AxiStreamDmaWrite.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiRamWrite, AxiWriteBus, AxiStreamBus, AxiStreamFrame, AxiStreamSource from tests.common.regression_utils import run_surf_vhdl_test @@ -45,8 +46,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.axiClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axiClk) async def reset(self): self.dut.axiRst.value = 1 @@ -96,7 +96,6 @@ def test_AxiStreamDmaWrite(parameters): extra_vhdl_sources={ "surf": [ "axi/axi4/ip_integrator/MasterAxiIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", "axi/dma/ip_integrator/AxiStreamDmaWriteIpIntegrator.vhd", ], }, diff --git a/tests/axi/utils.py b/tests/axi/utils.py index 0c17184993..102eb47478 100644 --- a/tests/axi/utils.py +++ b/tests/axi/utils.py @@ -8,7 +8,7 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd def ring_buffer_axil_addr(bus_index: int, buf: int = 0, high: int = 0) -> int: @@ -46,8 +46,7 @@ async def wait_sampled_ready( # source must hold its current beat stable until a clock edge confirms that # the sink presented `TREADY`. for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(settle_time_ns, unit="ns") + await sample_after_tpd(clk, propagation_time=settle_time_ns) if int(ready_signal.value) == 1: return diff --git a/tests/base/delay/test_SlvDelay.py b/tests/base/delay/test_SlvDelay.py index 434c0eafff..0649e12dfc 100644 --- a/tests/base/delay/test_SlvDelay.py +++ b/tests/base/delay/test_SlvDelay.py @@ -127,17 +127,11 @@ async def reset(self) -> None: await self.cycle() -@cocotb.test() +@cocotb.test(skip=env_flag("REG_OUTPUT_G", default=False)) async def programmable_delay_test(dut): tb = TB(dut) await tb.reset() - # REG_OUTPUT_G adds an extra registered stage on top of the programmable - # delay line. The behavioral coverage for that option is handled by the - # hold and reset tests, while this test stays focused on the selectable tap. - if tb.reg_output: - return - # Change the selected tap while feeding distinct words so the test proves # the mux picks the requested historical sample, not just the newest word. await tb.cycle(din=0x11, delay=0) diff --git a/tests/base/delay/test_SlvDelayFifo.py b/tests/base/delay/test_SlvDelayFifo.py index 45be4259e2..35b2588a00 100644 --- a/tests/base/delay/test_SlvDelayFifo.py +++ b/tests/base/delay/test_SlvDelayFifo.py @@ -120,11 +120,8 @@ async def timestamped_outputs_preserve_programmed_order_test(dut): assert await tb.wait_for_output() == 0x22 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_RESET_FLUSH", default=False)) async def reset_flushes_pending_entries_test(dut): - if not env_flag("CHECK_RESET_FLUSH", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/delay/test_SlvDelayRam.py b/tests/base/delay/test_SlvDelayRam.py index dc91b1d776..b8b4c73785 100644 --- a/tests/base/delay/test_SlvDelayRam.py +++ b/tests/base/delay/test_SlvDelayRam.py @@ -149,11 +149,8 @@ async def enable_hold_test(dut): assert int(dut.dout.value) == held -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_DYNAMIC_DELAY_CHANGE", default=False)) async def dynamic_delay_change_requires_reset_test(dut): - if not env_flag("CHECK_DYNAMIC_DELAY_CHANGE", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/fifo/test_Fifo.py b/tests/base/fifo/test_Fifo.py index b1a0f4cf40..838bfbabf3 100644 --- a/tests/base/fifo/test_Fifo.py +++ b/tests/base/fifo/test_Fifo.py @@ -141,12 +141,9 @@ async def wrapper_branch_ordering_test(dut): assert observed == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("GEN_SYNC_FIFO_G", default=False)) async def sync_count_alias_test(dut): tb = TB(dut) - if not tb.sync_fifo: - return - await tb.reset() # The sync wrapper aliases both public count ports to the same internal diff --git a/tests/base/fifo/test_FifoAsync.py b/tests/base/fifo/test_FifoAsync.py index 9b96ecd66d..33591fd770 100644 --- a/tests/base/fifo/test_FifoAsync.py +++ b/tests/base/fifo/test_FifoAsync.py @@ -38,6 +38,7 @@ hdl_parameters_from, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) @@ -87,14 +88,12 @@ async def write_word(self, value: int) -> None: self.dut.wr_en.value = 1 await RisingEdge(self.dut.wr_clk) self.dut.wr_en.value = 0 - await RisingEdge(self.dut.wr_clk) # Let FWFT outputs settle before the next operation samples status. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.wr_clk, propagation_time=2) async def cycle_rd(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.rd_clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.rd_clk, propagation_time=2) async def read_word(self) -> int: if self.fwft_enabled: @@ -174,7 +173,7 @@ async def basic_ordering_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_FULL_EMPTY", default=True)) async def full_empty_flag_test(dut): tb = TB( dut, @@ -183,9 +182,6 @@ async def full_empty_flag_test(dut): ) await tb.reset() - if not env_flag("CHECK_FULL_EMPTY", default=True): - return - # Standard mode exposes one slot less than the raw address space, while # FWFT can present one prefetched word beyond the underlying storage count. entry_capacity = (2 ** int(os.environ["ADDR_WIDTH_G"])) - (0 if tb.fwft_enabled else 1) @@ -200,11 +196,8 @@ async def full_empty_flag_test(dut): await with_timeout(tb._wait_empty(), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_THRESHOLD_FLAGS", default=False)) async def threshold_flag_test(dut): - if not env_flag("CHECK_THRESHOLD_FLAGS", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), @@ -241,11 +234,8 @@ async def threshold_flag_test(dut): await with_timeout(tb._wait_prog_empty(1), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STRESS_BEHAVIOR", default=False)) async def burst_backpressure_and_reset_test(dut): - if not env_flag("CHECK_STRESS_BEHAVIOR", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), @@ -287,11 +277,8 @@ async def burst_backpressure_and_reset_test(dut): assert await tb.read_word() == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_NEAR_FULL_TURNOVER", default=False)) async def near_full_turnover_test(dut): - if not env_flag("CHECK_NEAR_FULL_TURNOVER", default=False): - return - tb = TB( dut, wr_clk_period_ns=float(os.environ["WR_CLK_PERIOD_NS"]), diff --git a/tests/base/fifo/test_FifoCascade.py b/tests/base/fifo/test_FifoCascade.py index 7cd9b9948c..3e2923a89f 100644 --- a/tests/base/fifo/test_FifoCascade.py +++ b/tests/base/fifo/test_FifoCascade.py @@ -166,11 +166,8 @@ async def stage_vector_mapping_test(dut): assert top_prog_full == int(dut.prog_full.value) -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STAGE_PRESSURE", default=False)) async def staged_pressure_recovery_test(dut): - if not env_flag("CHECK_STAGE_PRESSURE", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/fifo/test_FifoMux.py b/tests/base/fifo/test_FifoMux.py index e5038c1f54..91dd95b5cf 100644 --- a/tests/base/fifo/test_FifoMux.py +++ b/tests/base/fifo/test_FifoMux.py @@ -179,14 +179,16 @@ async def width_conversion_test(dut): assert observed == expected -@cocotb.test() +@cocotb.test( + skip=( + int(os.environ.get("RD_DATA_WIDTH_G", "1")) + <= int(os.environ.get("WR_DATA_WIDTH_G", "1")) + ), +) async def write_packer_reset_test(dut): tb = TB(dut) await tb.reset() - if tb.rd_width <= tb.wr_width: - return - # When the wrapper is packing several narrow writes into one wider FIFO # word, a reset must discard the partial aggregate rather than letting stale # pre-reset fragments leak into the next output word. diff --git a/tests/base/fifo/test_FifoOutputPipeline.py b/tests/base/fifo/test_FifoOutputPipeline.py index 88c60a7b1f..7a8511e9a7 100644 --- a/tests/base/fifo/test_FifoOutputPipeline.py +++ b/tests/base/fifo/test_FifoOutputPipeline.py @@ -59,8 +59,10 @@ def __init__(self, dut): # Start the main DUT clock. For pipelined cases, also start the little # Python coroutine that emulates the upstream FWFT source. cocotb.start_soon(Clock(dut.clk, self.clk_period_ns, unit="ns").start()) + self._source_task = None if self.pipe_stages > 0: - cocotb.start_soon(self._drive_source()) + # Lifetime source model retained by the bench. + self._source_task = cocotb.start_soon(self._drive_source()) def reset_active_value(self) -> int: return self.rst_polarity @@ -98,6 +100,7 @@ def feed_words(self, words: list[int]) -> None: self.source_words.extend(words) async def _drive_source(self) -> None: + """Lifetime agent: emulate the upstream FWFT source for this test.""" # This coroutine emulates the upstream FIFO/source interface. It keeps # `sValid/sData` aligned to the DUT's `sRdEn` requests. next_valid = 0 @@ -143,12 +146,9 @@ async def collect_words(self, expected_count: int, *, timeout_cycles: int = 80) raise AssertionError(f"Timed out collecting {expected_count} output words") -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) != 0) async def zero_latency_passthrough_test(dut): tb = TB(dut) - if tb.pipe_stages != 0: - return - # With zero stages, this block is combinational: data and handshaking should # pass straight through without waiting for a clock edge. dut.rst.value = tb.reset_inactive_value() @@ -162,12 +162,9 @@ async def zero_latency_passthrough_test(dut): assert int(dut.sRdEn.value) == 1 -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def ordering_test(dut): tb = TB(dut) - if tb.pipe_stages == 0: - return - await tb.reset() # Preload a short stream and then let the downstream consumer read @@ -179,12 +176,9 @@ async def ordering_test(dut): assert await tb.collect_words(len(expected)) == expected -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) < 2) async def backpressure_test(dut): tb = TB(dut) - if tb.pipe_stages < 2: - return - await tb.reset() # This test toggles downstream readiness to make sure the DUT can hold data @@ -231,12 +225,9 @@ async def backpressure_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=int(os.environ.get("PIPE_STAGES_G", "0")) == 0) async def reset_behavior_test(dut): tb = TB(dut) - if tb.pipe_stages == 0: - return - await tb.reset() # Fill the pipeline with a few words and consume one so we know the DUT has diff --git a/tests/base/fifo/test_FifoRdFsm.py b/tests/base/fifo/test_FifoRdFsm.py index 2479fb742a..52ea7fbe7b 100644 --- a/tests/base/fifo/test_FifoRdFsm.py +++ b/tests/base/fifo/test_FifoRdFsm.py @@ -108,17 +108,11 @@ async def read_pulse(self) -> tuple[int, int]: return sampled_valid, sampled_underflow -@cocotb.test() +@cocotb.test(skip=env_flag("FWFT_EN_G", default=False)) async def count_and_flag_test(dut): tb = TB(dut) await tb.reset() - if tb.fwft_enabled: - # FWFT mode has a different visible contract because occupancy moves - # through an internal prefetch pipeline before the consumer sees data. - # The dedicated FWFT test below covers that behavior more directly. - return - await tb.initialize_writer(3) assert int(dut.empty.value) == 0 assert int(dut.almost_empty.value) == 0 @@ -131,12 +125,9 @@ async def count_and_flag_test(dut): assert int(dut.prog_empty.value) == (1 if 1 < tb.empty_threshold else 0) -@cocotb.test() +@cocotb.test(skip=env_flag("FWFT_EN_G", default=False)) async def standard_read_and_underflow_test(dut): tb = TB(dut) - if tb.fwft_enabled: - return - await tb.reset() await tb.initialize_writer(2) @@ -158,12 +149,9 @@ async def standard_read_and_underflow_test(dut): assert int(dut.empty.value) == 1 -@cocotb.test() +@cocotb.test(skip=not env_flag("FWFT_EN_G", default=False)) async def fwft_prefetch_test(dut): tb = TB(dut) - if not tb.fwft_enabled: - return - await tb.reset() await tb.initialize_writer(1) diff --git a/tests/base/fifo/test_FifoSync.py b/tests/base/fifo/test_FifoSync.py index aa3222471c..83903600e5 100644 --- a/tests/base/fifo/test_FifoSync.py +++ b/tests/base/fifo/test_FifoSync.py @@ -34,6 +34,7 @@ hdl_parameters_from, parameter_case, run_surf_vhdl_test, + sample_after_tpd, ) @@ -79,8 +80,7 @@ async def write_word(self, value: int) -> None: self.dut.wr_en.value = 1 await RisingEdge(self.dut.clk) self.dut.wr_en.value = 0 - await RisingEdge(self.dut.clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk, propagation_time=2) async def read_word(self) -> int: if self.fwft_enabled: @@ -142,8 +142,7 @@ async def simultaneous_cycle(self, write_value: int) -> int: await RisingEdge(self.dut.clk) self.dut.wr_en.value = 0 self.dut.rd_en.value = 0 - await RisingEdge(self.dut.clk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk, propagation_time=2) return read_value @@ -163,14 +162,11 @@ async def basic_ordering_test(dut): assert received == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_FULL_EMPTY", default=True)) async def full_empty_flag_test(dut): tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() - if not env_flag("CHECK_FULL_EMPTY", default=True): - return - # FifoSync uses the same user-visible capacity convention as FifoAsync: # standard mode exposes N-1 entries, FWFT mode exposes N entries. entry_capacity = (2 ** int(os.environ["ADDR_WIDTH_G"])) - (0 if tb.fwft_enabled else 1) @@ -185,11 +181,8 @@ async def full_empty_flag_test(dut): await with_timeout(tb._wait_empty(), 5, "us") -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_THRESHOLD_FLAGS", default=False)) async def threshold_flag_test(dut): - if not env_flag("CHECK_THRESHOLD_FLAGS", default=False): - return - tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() @@ -225,17 +218,15 @@ async def threshold_flag_test(dut): await with_timeout(tb._wait_prog_empty(1), 5, "us") -@cocotb.test() +@cocotb.test( + skip=( + not env_flag("CHECK_SIMULTANEOUS_BOUNDARY", default=False) + or not env_flag("FWFT_EN_G", default=False) + ), +) async def simultaneous_boundary_test(dut): - if not env_flag("CHECK_SIMULTANEOUS_BOUNDARY", default=False): - return - tb = TB(dut, clk_period_ns=float(os.environ["CLK_PERIOD_NS"])) await tb.reset() - - if not tb.fwft_enabled: - return - capacity = 2 ** int(os.environ["ADDR_WIDTH_G"]) seed_values = [0x30 + index for index in range(capacity - 1)] for value in seed_values: diff --git a/tests/base/general/test_Arbiter.py b/tests/base/general/test_Arbiter.py index 700034fb72..5723def5e7 100644 --- a/tests/base/general/test_Arbiter.py +++ b/tests/base/general/test_Arbiter.py @@ -163,11 +163,8 @@ async def reset_behavior_test(dut): assert int(dut.valid.value) == 0 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_STARVATION_ROTATION", default=False)) async def starvation_rotation_test(dut): - if not env_flag("CHECK_STARVATION_ROTATION", default=False): - return - tb = TB(dut) await tb.reset() diff --git a/tests/base/ram/test_DualPortRam.py b/tests/base/ram/test_DualPortRam.py index 26b1ceed43..cd172bb7b1 100644 --- a/tests/base/ram/test_DualPortRam.py +++ b/tests/base/ram/test_DualPortRam.py @@ -105,14 +105,11 @@ async def port_a_and_b_readback_test(dut): assert await tb.read_b(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=env_flag("DOA_REG_G", default=False)) async def port_a_mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if tb.doa_reg_enabled: - return - # Seed address 1 so `douta` is already showing a different value before # the write-under-test. That makes the three read-during-write modes # visibly distinguishable on port A. @@ -175,11 +172,8 @@ async def byte_write_and_reset_test(dut): assert await tb.read_b(4) == 0xCAFE -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_CROSS_PORT_COLLISION", default=False)) async def cross_port_collision_test(dut): - if not env_flag("CHECK_CROSS_PORT_COLLISION", default=False): - return - tb = TB(dut) await tb.warmup() diff --git a/tests/base/ram/test_LutRam.py b/tests/base/ram/test_LutRam.py index a2a8403b0d..5d72a41a5a 100644 --- a/tests/base/ram/test_LutRam.py +++ b/tests/base/ram/test_LutRam.py @@ -168,13 +168,10 @@ async def multiport_read_visibility_test(dut): assert await tb.read_c(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=not env_flag("REG_EN_G", default=True)) async def mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if not tb.reg_enabled: - return - await tb.write_a(0, 0x1111) await tb.write_a(1, 0xAAAA) assert await tb.read_a(1) == 0xAAAA @@ -198,13 +195,10 @@ async def mode_semantics_test(dut): assert await tb.read_b(0) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) await tb.warmup() - if not tb.byte_write_enabled: - return - await tb.write_a(3, 0xABCD) await tb.write_a(3, 0x00EF, byte_mask=0b01) assert await tb.read_b(3) == 0xABEF @@ -213,13 +207,10 @@ async def byte_write_enable_test(dut): assert await tb.read_b(3) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("REG_EN_G", default=True)) async def reset_behavior_test(dut): tb = TB(dut) await tb.warmup() - if not tb.reg_enabled: - return - await tb.write_a(4, 0xCAFE) assert await tb.read_b(4) == 0xCAFE diff --git a/tests/base/ram/test_SimpleDualPortRam.py b/tests/base/ram/test_SimpleDualPortRam.py index 1075b71a70..b3a7ee9159 100644 --- a/tests/base/ram/test_SimpleDualPortRam.py +++ b/tests/base/ram/test_SimpleDualPortRam.py @@ -103,12 +103,9 @@ async def basic_read_write_test(dut): assert await tb.read_word(1) == 0x1234 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) - if not tb.byte_write_enabled: - return - # First write a full word, then overwrite only one byte at a time to prove # the byte-mask wiring is being honored. await tb.write_word(2, 0xABCD) @@ -119,11 +116,8 @@ async def byte_write_enable_test(dut): assert await tb.read_word(2) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_READ_ENABLE_HOLD", default=False)) async def read_enable_hold_test(dut): - if not env_flag("CHECK_READ_ENABLE_HOLD", default=False): - return - tb = TB(dut) await tb.cycle_a(1) await tb.cycle_b(1) @@ -143,11 +137,8 @@ async def read_enable_hold_test(dut): assert await tb.read_word(1) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_REGISTERED_ENB_HOLD", default=False)) async def registered_read_enable_hold_test(dut): - if not env_flag("CHECK_REGISTERED_ENB_HOLD", default=False): - return - tb = TB(dut) await tb.cycle_a(1) await tb.cycle_b(1) @@ -169,12 +160,14 @@ async def registered_read_enable_hold_test(dut): assert await tb.read_word(1) == 0x3333 -@cocotb.test() +@cocotb.test( + skip=( + not env_flag("DOB_REG_G", default=False) + and int(os.environ.get("READ_LATENCY_G", "1")) != 2 + ), +) async def registered_output_hold_test(dut): tb = TB(dut) - if not tb.dob_reg_enabled: - return - # Seed two addresses so we can prove the output register can hold its old # value even after the read address changes. await tb.write_word(0, 0x1111) diff --git a/tests/base/ram/test_TrueDualPortRam.py b/tests/base/ram/test_TrueDualPortRam.py index b38c2c7a3d..8c22993125 100644 --- a/tests/base/ram/test_TrueDualPortRam.py +++ b/tests/base/ram/test_TrueDualPortRam.py @@ -39,6 +39,15 @@ ) +def output_register_enabled(port: str) -> bool: + latency = int(os.environ.get(f"READ_LATENCY_{port}_G", "-1")) + if latency < 0: + latency = int(os.environ.get("READ_LATENCY_G", "1")) + if env_flag(f"DO{port}_REG_G", default=False) and latency == 1: + latency = 2 + return latency == 2 + + class TB(DualClockRamTB): def __init__(self, dut): super().__init__(dut) @@ -135,13 +144,10 @@ async def cross_port_read_write_test(dut): assert await tb.read_b(2) == 0x5678 -@cocotb.test() +@cocotb.test(skip=output_register_enabled("A")) async def mode_semantics_test(dut): tb = TB(dut) await tb.warmup() - if tb.doa_reg_enabled: - return - # Seed two locations and intentionally leave `douta` showing the value from # address 1 before writing address 0. That setup makes the three RAM modes # produce visibly different output behavior on the write cycle. @@ -170,13 +176,10 @@ async def mode_semantics_test(dut): assert await tb.read_b(0) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not env_flag("BYTE_WR_EN_G", default=False)) async def byte_write_enable_test(dut): tb = TB(dut) await tb.warmup() - if not tb.byte_write_enabled: - return - await tb.write_a(3, 0xABCD) await tb.write_b(3, 0x00EF, byte_mask=0b01) assert await tb.read_a(3) == 0xABEF @@ -185,11 +188,8 @@ async def byte_write_enable_test(dut): assert await tb.read_a(3) == 0x12EF -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_DUAL_WRITE_COLLISION", default=False)) async def dual_write_collision_test(dut): - if not env_flag("CHECK_DUAL_WRITE_COLLISION", default=False): - return - tb = TB(dut) await tb.warmup() @@ -225,13 +225,10 @@ async def dual_write_collision_test(dut): assert await tb.read_b(5) == 0x7777 -@cocotb.test() +@cocotb.test(skip=not output_register_enabled("B")) async def registered_output_hold_test(dut): tb = TB(dut) await tb.warmup() - if not tb.dob_reg_enabled: - return - await tb.write_a(0, 0x1111) await tb.write_a(1, 0x2222) assert await tb.read_b(0) == 0x1111 @@ -248,13 +245,10 @@ async def registered_output_hold_test(dut): assert int(dut.doutb.value) == 0x2222 -@cocotb.test() +@cocotb.test(skip=not output_register_enabled("A")) async def registered_output_hold_a_test(dut): tb = TB(dut) await tb.warmup() - if not tb.doa_reg_enabled: - return - await tb.write_b(0, 0x1111) await tb.write_b(1, 0x2222) assert await tb.read_a(0) == 0x1111 diff --git a/tests/base/sync/sync_test_utils.py b/tests/base/sync/sync_test_utils.py index e35d2c7c65..3f5f56b437 100644 --- a/tests/base/sync/sync_test_utils.py +++ b/tests/base/sync/sync_test_utils.py @@ -14,7 +14,7 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, env_sl +from tests.common.regression_utils import env_flag, env_sl, sample_after_tpd class SynchronizerLikeTB: @@ -67,8 +67,7 @@ async def cycle(self, count: int = 1) -> None: # signal has its next rising transition". This is the core way cocotb # synchronizes Python code to simulated hardware time. for _ in range(count): - await RisingEdge(self.dut.clk) - await self.settle() + await sample_after_tpd(self.dut.clk, propagation_time=2) async def reset(self) -> None: # Drive reset active first so the DUT starts from its known reset state. diff --git a/tests/base/sync/test_Synchronizer.py b/tests/base/sync/test_Synchronizer.py index 01b7e47480..1d7b5564e5 100644 --- a/tests/base/sync/test_Synchronizer.py +++ b/tests/base/sync/test_Synchronizer.py @@ -28,21 +28,18 @@ from tests.base.sync.sync_test_utils import SynchronizerLikeTB from tests.common.regression_utils import ( + env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test, ) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def propagation_latency_test(dut): # Each `@cocotb.test()` function is an async coroutine. cocotb starts it, # gives it the HDL `dut`, and advances simulation time whenever we `await`. tb = SynchronizerLikeTB(dut, width=1) - if tb.bypass_enabled: - # The bypass case has different behavior and is covered by its own test. - return - await tb.reset() # After reset, the synchronizer should present the reset/default value. @@ -54,12 +51,9 @@ async def propagation_latency_test(dut): await tb.drive_and_expect_after_latency(0) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def reset_behavior_test(dut): tb = SynchronizerLikeTB(dut, width=1) - if tb.bypass_enabled: - return - # First prove the DUT can leave reset and pass a non-default value through. await tb.reset() await tb.drive_and_expect_after_latency(1) @@ -89,12 +83,9 @@ async def reset_behavior_test(dut): assert int(dut.dataOut.value) == tb.expected_output(1) -@cocotb.test() +@cocotb.test(skip=not env_flag("BYPASS_SYNC_G", default=False)) async def bypass_mode_test(dut): tb = SynchronizerLikeTB(dut, width=1) - if not tb.bypass_enabled: - return - # In bypass mode, the DUT is combinational from input to output, so no # clock edge is needed to observe each new value. dut.rst.value = tb.reset_inactive_value() diff --git a/tests/base/sync/test_SynchronizerFifo.py b/tests/base/sync/test_SynchronizerFifo.py index 6e6919cf9c..9f8fec95c1 100644 --- a/tests/base/sync/test_SynchronizerFifo.py +++ b/tests/base/sync/test_SynchronizerFifo.py @@ -168,26 +168,22 @@ async def data_order_test(dut): # pass-through, so ordering reduces to immediate combinational delivery. for value in values: await tb.expect_common_clock_passthrough(value) - return - - # In dual-clock mode the DUT behaves like a tiny asynchronous FIFO. Write a - # known sequence, then verify reads emerge in the same order. - for value in values: - await tb.write(value) + else: + # In dual-clock mode the DUT behaves like a tiny asynchronous FIFO. + # Write a known sequence, then verify reads emerge in the same order. + for value in values: + await tb.write(value) - observed = [] - for _ in values: - observed.append(await tb.read()) + observed = [] + for _ in values: + observed.append(await tb.read()) - assert observed == values + assert observed == values -@cocotb.test() +@cocotb.test(skip=not env_flag("COMMON_CLK_G", default=False)) async def common_clock_bypass_test(dut): tb = TB(dut) - if not tb.common_clk: - return - await tb.reset() await tb.expect_common_clock_passthrough(0x5A) @@ -204,11 +200,8 @@ async def reset_value_test(dut): assert observed == tb.init_value -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_ASYNC_BURST_GAPS", default=False)) async def async_burst_read_gap_test(dut): - if not env_flag("CHECK_ASYNC_BURST_GAPS", default=False): - return - tb = TB(dut) await tb.reset() assert not tb.common_clk @@ -235,11 +228,8 @@ async def async_burst_read_gap_test(dut): assert await tb.read_with_pause() == expected -@cocotb.test() +@cocotb.test(skip=not env_flag("CHECK_RESET_WHILE_PREFETCHED", default=False)) async def reset_while_prefetched_test(dut): - if not env_flag("CHECK_RESET_WHILE_PREFETCHED", default=False): - return - tb = TB(dut) await tb.reset() assert not tb.common_clk diff --git a/tests/base/sync/test_SynchronizerVector.py b/tests/base/sync/test_SynchronizerVector.py index a9d8c1795e..ea26259db2 100644 --- a/tests/base/sync/test_SynchronizerVector.py +++ b/tests/base/sync/test_SynchronizerVector.py @@ -29,20 +29,18 @@ from tests.base.sync.sync_test_utils import SynchronizerLikeTB from tests.common.regression_utils import ( + env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test, ) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def propagation_latency_test(dut): # This test is structurally the same as the scalar synchronizer test; the # difference is that the data path now carries a whole vector at once. tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if tb.bypass_enabled: - return - await tb.reset() assert int(dut.dataOut.value) == tb.expected_output(0) @@ -52,12 +50,9 @@ async def propagation_latency_test(dut): await tb.drive_and_expect_after_latency(0b010101 & tb.mask) -@cocotb.test() +@cocotb.test(skip=env_flag("BYPASS_SYNC_G", default=False)) async def reset_behavior_test(dut): tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if tb.bypass_enabled: - return - # Fill every bit with 1s so reset behavior is obvious on the output. await tb.reset() await tb.drive_and_expect_after_latency(tb.mask) @@ -84,12 +79,9 @@ async def reset_behavior_test(dut): assert int(dut.dataOut.value) == tb.expected_output(tb.mask) -@cocotb.test() +@cocotb.test(skip=not env_flag("BYPASS_SYNC_G", default=False)) async def bypass_mode_test(dut): tb = SynchronizerLikeTB(dut, width=int(os.environ["WIDTH_G"])) - if not tb.bypass_enabled: - return - # In bypass mode, just sample a few representative vector values directly. dut.rst.value = tb.reset_inactive_value() for value in (0, 0b101001 & tb.mask, tb.mask): diff --git a/tests/common/README.md b/tests/common/README.md new file mode 100644 index 0000000000..d5c2d66ff2 --- /dev/null +++ b/tests/common/README.md @@ -0,0 +1,211 @@ +# Common Regression Infrastructure + +The helpers in this directory provide the shared pytest/cocotb launch path for +SURF regressions. Start with the repository-wide [regression style +guide](../README.md) and use this page when wiring a new Python test into GHDL. + +## Standard Runner + +`run_surf_vhdl_test()` in `regression_utils.py` launches GHDL through +`cocotb-test`, loads the ruckus-imported SURF libraries, selects the cocotb +module from `test_file`, and gives each parameter case its own simulation build +directory. + +A normal pytest wrapper looks like this: + +```python +PARAMETER_SWEEP = [ + parameter_case("default", DATA_WIDTH_G=16, RST_ASYNC_G=False), + parameter_case("async_reset", DATA_WIDTH_G=16, RST_ASYNC_G=True), +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_my_target(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.MyTargetWrapper", + parameters=hdl_parameters_from(parameters), + extra_env=parameters, + ) +``` + +Use `parameters` only for VHDL generics. Use `extra_env` for Python-side case +metadata. When one dictionary contains both, pass it through +`hdl_parameters_from()` before giving it to the simulator. + +Use `extra_vhdl_sources` only for a cocotb-only wrapper or simulation model whose +design unit is absent from the ruckus import. Check the imported source tree +under `build/SRC_VHDL/` before adding a path. Production and reusable wrapper RTL +belongs in the nearest `ruckus.tcl`; do not use this argument to hide a missing +build-manifest entry or repeat an imported design unit. Compiling the same unit +from both paths can redefine it, make compile order significant, or leave a +cached build using a different source than the reviewer expects. + +The shared source merge rejects an extra path that resolves to a file already +present in the same library. It also rejects a different extra file that +redeclares an imported entity, package, or configuration in that library, so +variable-generated lists cannot bypass the literal-source audit. There is no +override: give a test-only unit a distinct name, or fix the ruckus/source-list +boundary instead of relying on compile order. + +The default build path includes `parameters` and `extra_env`, and the shared +runner hashes path components that would be unsafe or excessively long. Use +`sim_build_key` only when a subsystem requires a deliberately stable or more +meaningful build identity. A custom key must still distinguish every +concurrently runnable compile configuration and selected cocotb scenario; never +point incompatible variants at the same build directory. + +Leave `force_compile=False` for normal regressions. Set it only when a +documented source-topology or simulator-cache limitation makes reuse unsafe; +it is not a substitute for a unique build identity. + +The shared runner is the default for VHDL/GHDL regressions. A direct +`cocotb-test` or simulator-specific runner is justified only when the flow +needs capabilities the shared path cannot express, such as a mixed-language +top, a vendor simulator, precompiled libraries, or explicit external-process +lifecycle control. Document the exception beside the custom runner or in the +subsystem README, and retain the common source, compile-option, result-file, +and build-isolation conventions where they apply. + +## Shared Helpers + +- `parameter_case()` creates readable pytest IDs for curated cases. +- `hdl_parameters_from()` filters mixed case dictionaries to keys ending in + `_G`. +- `env_flag()`, `env_sl()`, `env_int()`, `env_hex()`, and `env_float()` parse + simulator environment values consistently inside cocotb coroutines. +- `cocotb_test_filter()`, `cocotb_test_filter_excluding()`, and + `cocotb_filtered_env()` build explicit scenario groups while preserving an + externally requested focused selector in the simulation-build identity. +- `start_lockstep_clocks()` drives multiple logically common clocks from one + coroutine so their edges cannot drift. It returns the lifetime task; retain + that task on the bench that owns the clock domains. +- `cancel_and_join_tasks()` cancels a bench's owned lifetime tasks, awaits all + of their termination paths, suppresses expected cancellation, and propagates + an unexpected task failure after every task has been joined. +- `build_vhdl_sources()` and `merge_vhdl_sources()` are runner plumbing; tests + should normally reach them only through `run_surf_vhdl_test()`. + +Bus and protocol transaction helpers live closer to their users: + +- `tests/axi/utils.py` contains shared AXI-family handshake utilities. +- `tests/protocols//*_test_utils.py` contains protocol frames, + reference models, sources, sinks, and scoreboards. +- Subsystem helpers should implement mechanics, while policy assertions remain + visible in the module test. + +## Build And Debug Workflow + +Run the ruckus import after source-list changes or when `build/SRC_VHDL` is +missing or stale: + +```bash +make MODULES="$PWD" import +``` + +Use a serial focused run for readable simulator logs: + +```bash +./.venv/bin/python -m pytest -n 0 -q tests//test_.py +``` + +Use parallel execution for a stable subsystem suite: + +```bash +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/ +``` + +Do not call the runner directly from a cocotb coroutine. Pytest launches the +simulator; cocotb code runs inside it. Keep every protocol-progress wait bounded +so a broken handshake becomes a useful failure instead of a hung worker. + +When a pytest wrapper selects one of several cocotb entrypoints, pass the +selector in `extra_env`. Because the shared runner includes `extra_env` in the +default build path, selected scenarios remain isolated under pytest-xdist. Give +the selector a deterministic default and make each pytest node run only the +scenario or coherent scenario group named by that node; do not use a bare return +inside an otherwise selected cocotb test to turn an inapplicable case into a +pass. + +Retain every task returned by `cocotb.start_soon()`. Await finite producers, +consumers, and transactions before the entrypoint completes. Store monitors or +protocol peers intended to run for the whole test on the bench, document them as +lifetime agents, and provide cleanup when cancellation order matters or an +agent owns a socket, process, file, or other external resource. + +Give an intentional open-ended agent a function docstring containing +`Lifetime agent:` followed by its purpose and termination owner. The compliance +audit recognizes that explicit classification; do not use the marker on a +finite handshake, receive loop, or transaction that needs a cycle/time limit. + +Prefer ordinary `if`/`else` structure when a parameter selects different test +behavior. If a fully checked parameter-specific branch must terminate early, +put a `# Terminal scenario:` comment immediately above its bare return and state +why the assertions above are that parameter's complete contract. The blocking +audit rejects every other post-activity bare return as ambiguous. + +Use `sample_after_delta_cycles()` and `sample_after_tpd()` from +`regression_utils.py` to state why a test samples after a clock edge. The former +enters cocotb's read-only phase for delta-settled observation; the latter waits +real simulated time for a VHDL `after TPD_G` update. A reusable propagation +helper carries a `Propagation sampling:` docstring so the audit can distinguish +that reviewed timing contract from an unexplained edge-plus-timer sequence. +Use `wait_after_edge_offset()` instead when real simulated time intentionally +places stimulus between edges; its `Real-time timing:` contract is likewise +recognized without pretending that the offset is output propagation. + +## Compliance Audit And Preservation Reports + +`compliance_audit.py` provides a read-only structural audit and a reproducible +inventory for cleanup work. The audit reports screening signals; ambiguous +items such as lifetime tasks, open-ended agent loops, and post-edge delays still +require review rather than mechanical replacement. + +Run an audit for the whole active test tree or one subsystem: + +```bash +./.venv/bin/python -m tests.common.compliance_audit audit tests +./.venv/bin/python -m tests.common.compliance_audit audit tests/protocols/batcher +``` + +Capture a preservation report before changing a subsystem, capture it again +afterward, and compare the two: + +```bash +./.venv/bin/python -m tests.common.compliance_audit \ + inventory tests/protocols/batcher --output /tmp/batcher-before.json +./.venv/bin/python -m tests.common.compliance_audit \ + inventory tests/protocols/batcher --output /tmp/batcher-after.json +./.venv/bin/python -m tests.common.compliance_audit \ + compare /tmp/batcher-before.json /tmp/batcher-after.json +``` + +The comparison exits unsuccessfully when a pytest function, cocotb entrypoint, +parameter ID, environment gate/selector, skip, or decorator timeout disappears. +An intentional rename, move, split, or consolidation therefore remains visible +and needs an explicit before/after mapping in the change description. Added +coverage is reported but does not make the command fail. + +The checked-in `compliance_baseline.json` ratchets the rules that are reliable +enough to enforce structurally: methodology presence, ordinary direct-runner +exceptions, literal VHDL sources duplicated from the ruckus import, and a bare +entrypoint return reached before any awaited simulator activity or assertion. +It also rejects an unretained non-clock `cocotb.start_soon()` call and an +unclassified `while True` loop, an unexplained post-edge real-time delay, and +an ambiguous post-activity bare return. Intentional lifetime loops must carry +the `Lifetime agent:` docstring contract described above; finite operations +must use a direct cycle/time bound instead. Reviewed propagation sampling and +real-time offsets must use the named helpers or documentation contracts above. +Run the check after importing the HDL tree: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m tests.common.compliance_audit check tests +``` + +The check fails when a file introduces a new finding or exceeds its existing +per-rule count. Removing a legacy finding is allowed and reported as a baseline +reduction; update the baseline in the same cleanup change so the violation +cannot return. Do not regenerate the whole baseline to accept an unrelated new +finding. diff --git a/tests/common/compliance_audit.py b/tests/common/compliance_audit.py new file mode 100644 index 0000000000..e99bac66ca --- /dev/null +++ b/tests/common/compliance_audit.py @@ -0,0 +1,862 @@ +############################################################################## +## 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. +############################################################################## + +from __future__ import annotations + +import argparse +import ast +from collections import Counter +from dataclasses import asdict, dataclass +import json +from pathlib import Path +import re +import sys +from typing import Iterable, Iterator + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCHEMA_VERSION = 1 +DEFAULT_BASELINE = Path(__file__).with_name("compliance_baseline.json") + +ENFORCED_RULES = { + "bare-return", + "direct-runner", + "duplicate-imported-source", + "edge-then-timer", + "early-bare-return", + "missing-methodology", + "open-ended-loop", + "unretained-start-soon", +} + +EXCLUDED_DIRECTORY_NAMES = { + "__pycache__", + "legacy", + "sim_build", +} + +METHODOLOGY_MARKER = "Test methodology:" +LIFETIME_AGENT_MARKER = "Lifetime agent:" +PROPAGATION_SAMPLING_MARKER = "Propagation sampling:" +REAL_TIME_TIMING_MARKER = "Real-time timing:" +TERMINAL_SCENARIO_MARKER = "# Terminal scenario:" +ENVIRONMENT_CONTROL_RE = re.compile( + r"^(?:RUN_[A-Z0-9_]*_TESTS|COCOTB_TEST_FILTER|COCOTB_TESTCASE|[A-Z0-9_]*TESTCASE)$" +) + +DOCUMENTED_DIRECT_RUNNERS = { + "tests/common/regression_utils.py", + "tests/simlink/ghdl/simlink_test_utils.py", +} + + +@dataclass(frozen=True, order=True) +class Finding: + rule: str + path: str + line: int + symbol: str + detail: str + + def to_dict(self) -> dict[str, object]: + return asdict(self) + + +@dataclass(frozen=True) +class FileInventory: + cocotb_entrypoints: tuple[str, ...] + pytest_functions: tuple[str, ...] + parameter_ids: tuple[str, ...] + environment_controls: tuple[str, ...] + skipped_functions: tuple[str, ...] + timeout_entrypoints: tuple[str, ...] + + def to_dict(self) -> dict[str, object]: + return { + key: list(value) + for key, value in asdict(self).items() + } + + +@dataclass(frozen=True) +class PreservationDelta: + removed: dict[str, dict[str, tuple[str, ...]]] + added: dict[str, dict[str, tuple[str, ...]]] + + @property + def has_removals(self) -> bool: + return bool(self.removed) + + def to_dict(self) -> dict[str, object]: + return { + "removed": _nested_tuples_to_lists(self.removed), + "added": _nested_tuples_to_lists(self.added), + } + + +@dataclass(frozen=True) +class BaselineDelta: + new: dict[str, dict[str, int]] + reduced: dict[str, dict[str, int]] + + @property + def has_new(self) -> bool: + return bool(self.new) + + def to_dict(self) -> dict[str, object]: + return { + "new": self.new, + "reduced": self.reduced, + } + + +def _nested_tuples_to_lists( + values: dict[str, dict[str, tuple[str, ...]]], +) -> dict[str, dict[str, list[str]]]: + return { + path: { + category: list(items) + for category, items in categories.items() + } + for path, categories in values.items() + } + + +def _has_terminal_scenario_marker(source_lines: list[str], return_line: int) -> bool: + for line in reversed(source_lines[max(0, return_line - 4) : return_line - 1]): + stripped = line.strip() + if not stripped.startswith("#"): + break + if stripped.startswith(TERMINAL_SCENARIO_MARKER): + return True + return False + + +def _qualified_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _qualified_name(node.value) + return f"{prefix}.{node.attr}" if prefix else node.attr + if isinstance(node, ast.Call): + return _qualified_name(node.func) + return "" + + +def _is_cocotb_test(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + target = decorator.func if isinstance(decorator, ast.Call) else decorator + if _qualified_name(target) == "cocotb.test": + return True + return False + + +def _has_timeout(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + if not isinstance(decorator, ast.Call): + continue + if _qualified_name(decorator.func) != "cocotb.test": + continue + if any(keyword.arg == "timeout_time" for keyword in decorator.keywords): + return True + return False + + +def _has_skip(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + for decorator in function.decorator_list: + name = _qualified_name(decorator) + if any(part.startswith("skip") for part in name.split(".")): + return True + + for node in _walk_function(function): + if isinstance(node, ast.Call) and _qualified_name(node.func) == "pytest.skip": + return True + return False + + +def _is_lifetime_agent(function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + docstring = ast.get_docstring(function, clean=False) + return docstring is not None and LIFETIME_AGENT_MARKER in docstring + + +class _FunctionWalker(ast.NodeVisitor): + def __init__(self, root: ast.FunctionDef | ast.AsyncFunctionDef): + self.root = root + self.nodes: list[ast.AST] = [] + + def generic_visit(self, node: ast.AST) -> None: + self.nodes.append(node) + super().generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + if node is self.root: + self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + if node is self.root: + self.generic_visit(node) + + def visit_Lambda(self, node: ast.Lambda) -> None: + self.nodes.append(node) + + +def _walk_function( + function: ast.FunctionDef | ast.AsyncFunctionDef, +) -> tuple[ast.AST, ...]: + walker = _FunctionWalker(function) + walker.visit(function) + return tuple(walker.nodes) + + +def _top_level_functions( + tree: ast.Module, +) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]: + return tuple( + node + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ) + + +def _all_functions( + tree: ast.Module, +) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]: + return tuple( + sorted( + ( + node + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ), + key=lambda node: (node.lineno, node.name), + ) + ) + + +def _string_constants(node: ast.AST) -> Iterator[tuple[str, int]]: + for child in ast.walk(node): + if isinstance(child, ast.Constant) and isinstance(child.value, str): + yield child.value, child.lineno + + +def _parameter_ids(tree: ast.Module) -> tuple[str, ...]: + result = set() + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + + name = _qualified_name(node.func) + if name == "pytest.param": + for keyword in node.keywords: + if keyword.arg == "id" and isinstance(keyword.value, ast.Constant): + if isinstance(keyword.value.value, str): + result.add(keyword.value.value) + elif name.endswith("parameter_case") and node.args: + first = node.args[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + result.add(first.value) + return tuple(sorted(result)) + + +def _environment_controls(tree: ast.Module) -> tuple[str, ...]: + controls = { + value + for value, _ in _string_constants(tree) + if ENVIRONMENT_CONTROL_RE.fullmatch(value) + } + if any( + isinstance(node, ast.Call) and _qualified_name(node.func).endswith("cocotb_filtered_env") + for node in ast.walk(tree) + ): + controls.add("COCOTB_TEST_FILTER") + return tuple(sorted(controls)) + + +def inventory_source(source: str, path: str = "test.py") -> FileInventory: + tree = ast.parse(source, filename=path) + functions = _top_level_functions(tree) + cocotb_functions = tuple(function for function in functions if _is_cocotb_test(function)) + + return FileInventory( + cocotb_entrypoints=tuple(sorted(function.name for function in cocotb_functions)), + pytest_functions=tuple( + sorted( + function.name + for function in functions + if function.name.startswith("test_") and not _is_cocotb_test(function) + ) + ), + parameter_ids=_parameter_ids(tree), + environment_controls=_environment_controls(tree), + skipped_functions=tuple( + sorted(function.name for function in functions if _has_skip(function)) + ), + timeout_entrypoints=tuple( + sorted(function.name for function in cocotb_functions if _has_timeout(function)) + ), + ) + + +def _repo_relative(path: Path, repo_root: Path) -> str: + try: + return path.resolve().relative_to(repo_root.resolve()).as_posix() + except ValueError: + return path.resolve().as_posix() + + +def discover_python_files( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> tuple[Path, ...]: + result = set() + requested = tuple(paths) or ("tests",) + + for raw_path in requested: + path = Path(raw_path) + if not path.is_absolute(): + path = repo_root / path + + if path.is_file(): + if path.suffix == ".py": + result.add(path.resolve()) + continue + + if not path.exists(): + raise FileNotFoundError(path) + + for candidate in path.rglob("*.py"): + relative_parts = candidate.relative_to(path).parts + if any(part in EXCLUDED_DIRECTORY_NAMES for part in relative_parts): + continue + result.add(candidate.resolve()) + + return tuple(sorted(result)) + + +def inventory_paths( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> dict[str, FileInventory]: + report = {} + for path in discover_python_files(paths, repo_root): + source = path.read_text(encoding="utf-8") + inventory = inventory_source(source, _repo_relative(path, repo_root)) + if any(asdict(inventory).values()): + report[_repo_relative(path, repo_root)] = inventory + return dict(sorted(report.items())) + + +def _parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]: + return { + child: parent + for parent in ast.walk(tree) + for child in ast.iter_child_nodes(parent) + } + + +def _is_clock_start(call: ast.Call) -> bool: + if not call.args: + return False + try: + expression = ast.unparse(call.args[0]) + except AttributeError: + return False + return "Clock(" in expression or "start_lockstep_clocks" in expression + + +def _await_call_name(statement: ast.stmt) -> str: + if not isinstance(statement, ast.Expr) or not isinstance(statement.value, ast.Await): + return "" + awaited = statement.value.value + if not isinstance(awaited, ast.Call): + return "" + return _qualified_name(awaited.func) + + +def _statement_lists(node: ast.AST) -> Iterator[list[ast.stmt]]: + for _, value in ast.iter_fields(node): + if isinstance(value, list) and value and all(isinstance(item, ast.stmt) for item in value): + yield value + for statement in value: + yield from _statement_lists(statement) + elif isinstance(value, ast.AST): + yield from _statement_lists(value) + + +def _direct_runner_aliases(tree: ast.Module) -> set[str]: + aliases = set() + for node in tree.body: + if not isinstance(node, ast.ImportFrom) or node.module != "cocotb_test.simulator": + continue + for name in node.names: + if name.name == "run": + aliases.add(name.asname or name.name) + return aliases + + +def _imported_source_paths(repo_root: Path) -> frozenset[Path]: + paths = set() + build_root = repo_root / "build" / "SRC_VHDL" + if not build_root.exists(): + return frozenset() + for library in build_root.iterdir(): + if not library.is_dir(): + continue + for path in library.iterdir(): + if path.is_file(): + paths.add(path.resolve()) + return frozenset(paths) + + +def audit_source( + source: str, + path: str = "test.py", + *, + repo_root: Path = REPO_ROOT, + imported_sources: frozenset[Path] | None = None, +) -> tuple[Finding, ...]: + tree = ast.parse(source, filename=path) + source_lines = source.splitlines() + findings = [] + functions = _top_level_functions(tree) + cocotb_functions = tuple(function for function in functions if _is_cocotb_test(function)) + + if cocotb_functions: + first_entrypoint_line = min(function.lineno for function in cocotb_functions) + header = "\n".join(source.splitlines()[:first_entrypoint_line]) + if METHODOLOGY_MARKER not in header: + findings.append( + Finding( + "missing-methodology", + path, + 1, + "", + "cocotb test file has no Test methodology block before its first entrypoint", + ) + ) + + for function in cocotb_functions: + function_nodes = _walk_function(function) + for node in function_nodes: + if isinstance(node, ast.Return) and node.value is None: + has_prior_test_activity = any( + isinstance(prior, (ast.Assert, ast.Await)) + and prior.lineno < node.lineno + for prior in function_nodes + ) + rule = "bare-return" if has_prior_test_activity else "early-bare-return" + if ( + rule == "bare-return" + and _has_terminal_scenario_marker(source_lines, node.lineno) + ): + continue + findings.append( + Finding( + rule, + path, + node.lineno, + function.name, + "bare return in cocotb entrypoint; confirm that the scenario cannot pass without its named checks", + ) + ) + + for function in _all_functions(tree): + parents = _parent_map(function) + lifetime_agent = _is_lifetime_agent(function) + timing_docstring = ast.get_docstring(function) or "" + classified_real_time = any( + marker in timing_docstring + for marker in (PROPAGATION_SAMPLING_MARKER, REAL_TIME_TIMING_MARKER) + ) + for node in _walk_function(function): + if isinstance(node, ast.While) and isinstance(node.test, ast.Constant): + if node.test.value is True and not lifetime_agent: + findings.append( + Finding( + "open-ended-loop", + path, + node.lineno, + function.name, + "while True requires a bounded enclosing timeout or lifetime-agent classification", + ) + ) + + if not isinstance(node, ast.Call) or _qualified_name(node.func) != "cocotb.start_soon": + continue + if _is_clock_start(node): + continue + if isinstance(parents.get(node), ast.Expr): + findings.append( + Finding( + "unretained-start-soon", + path, + node.lineno, + function.name, + "classify as finite work or a lifetime agent and make ownership explicit", + ) + ) + + for statements in _statement_lists(function): + for first, second in zip(statements, statements[1:]): + if ( + _await_call_name(first).endswith("RisingEdge") + and _await_call_name(second).endswith("Timer") + and not classified_real_time + ): + findings.append( + Finding( + "edge-then-timer", + path, + second.lineno, + function.name, + "classify the delay as delta-cycle settling or a real modeled propagation delay", + ) + ) + + relative_path = path.replace("\\", "/") + direct_aliases = _direct_runner_aliases(tree) + if direct_aliases and relative_path not in DOCUMENTED_DIRECT_RUNNERS: + for node in ast.walk(tree): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + if node.func.id in direct_aliases: + findings.append( + Finding( + "direct-runner", + path, + node.lineno, + "", + "use run_surf_vhdl_test() or document the capability the shared runner cannot express", + ) + ) + + if imported_sources is None: + imported_sources = _imported_source_paths(repo_root) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg != "extra_vhdl_sources": + continue + for literal, line in _string_constants(keyword.value): + if not literal.lower().endswith((".vhd", ".vhdl")): + continue + source_path = Path(literal) + if not source_path.is_absolute(): + source_path = repo_root / source_path + if source_path.exists() and source_path.resolve() in imported_sources: + findings.append( + Finding( + "duplicate-imported-source", + path, + line, + "", + f"{literal} is already supplied by build/SRC_VHDL", + ) + ) + + return tuple(sorted(set(findings))) + + +def audit_paths( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> tuple[Finding, ...]: + imported_sources = _imported_source_paths(repo_root) + findings = [] + for source_path in discover_python_files(paths, repo_root): + relative_path = _repo_relative(source_path, repo_root) + try: + findings.extend( + audit_source( + source_path.read_text(encoding="utf-8"), + relative_path, + repo_root=repo_root, + imported_sources=imported_sources, + ) + ) + except SyntaxError as exc: + findings.append( + Finding( + "syntax-error", + relative_path, + exc.lineno or 1, + "", + exc.msg, + ) + ) + return tuple(sorted(findings)) + + +def preservation_report( + paths: Iterable[str | Path], + repo_root: Path = REPO_ROOT, +) -> dict[str, object]: + inventories = inventory_paths(paths, repo_root) + return { + "schema_version": SCHEMA_VERSION, + "files": { + path: inventory.to_dict() + for path, inventory in inventories.items() + }, + } + + +def compare_preservation_reports( + before: dict[str, object], + after: dict[str, object], +) -> PreservationDelta: + if before.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported before-report schema") + if after.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported after-report schema") + + before_files = before.get("files") + after_files = after.get("files") + if not isinstance(before_files, dict) or not isinstance(after_files, dict): + raise ValueError("preservation reports must contain a files object") + + removed: dict[str, dict[str, tuple[str, ...]]] = {} + added: dict[str, dict[str, tuple[str, ...]]] = {} + all_paths = sorted(set(before_files) | set(after_files)) + for path in all_paths: + before_inventory = before_files.get(path, {}) + after_inventory = after_files.get(path, {}) + if not isinstance(before_inventory, dict) or not isinstance(after_inventory, dict): + raise ValueError(f"invalid inventory for {path}") + + categories = sorted(set(before_inventory) | set(after_inventory)) + for category in categories: + before_values = set(before_inventory.get(category, [])) + after_values = set(after_inventory.get(category, [])) + removed_values = tuple(sorted(before_values - after_values)) + added_values = tuple(sorted(after_values - before_values)) + if removed_values: + removed.setdefault(path, {})[category] = removed_values + if added_values: + added.setdefault(path, {})[category] = added_values + + return PreservationDelta(removed=removed, added=added) + + +def compliance_baseline(findings: Iterable[Finding]) -> dict[str, object]: + counts: dict[str, Counter[str]] = { + rule: Counter() + for rule in sorted(ENFORCED_RULES) + } + for finding in findings: + if finding.rule in ENFORCED_RULES: + counts[finding.rule][finding.path] += 1 + + return { + "schema_version": SCHEMA_VERSION, + "rules": { + rule: dict(sorted(paths.items())) + for rule, paths in counts.items() + }, + } + + +def compare_compliance_baseline( + baseline: dict[str, object], + findings: Iterable[Finding], +) -> BaselineDelta: + if baseline.get("schema_version") != SCHEMA_VERSION: + raise ValueError("unsupported compliance-baseline schema") + baseline_rules = baseline.get("rules") + if not isinstance(baseline_rules, dict): + raise ValueError("compliance baseline must contain a rules object") + + current = compliance_baseline(findings)["rules"] + new: dict[str, dict[str, int]] = {} + reduced: dict[str, dict[str, int]] = {} + for rule in sorted(ENFORCED_RULES): + allowed_paths = baseline_rules.get(rule, {}) + current_paths = current.get(rule, {}) + if not isinstance(allowed_paths, dict) or not isinstance(current_paths, dict): + raise ValueError(f"invalid baseline counts for {rule}") + for path in sorted(set(allowed_paths) | set(current_paths)): + allowed_count = allowed_paths.get(path, 0) + current_count = current_paths.get(path, 0) + if not isinstance(allowed_count, int) or not isinstance(current_count, int): + raise ValueError(f"invalid baseline count for {rule}:{path}") + if current_count > allowed_count: + new.setdefault(rule, {})[path] = current_count - allowed_count + elif current_count < allowed_count: + reduced.setdefault(rule, {})[path] = allowed_count - current_count + + return BaselineDelta(new=new, reduced=reduced) + + +def _write_output(content: str, output: str | None) -> None: + if output is None: + print(content) + else: + Path(output).write_text(f"{content}\n", encoding="utf-8") + + +def _audit_text(findings: tuple[Finding, ...]) -> str: + counts = Counter(finding.rule for finding in findings) + lines = [ + f"{finding.path}:{finding.line}: {finding.rule}: {finding.symbol}: {finding.detail}" + for finding in findings + ] + lines.append("") + lines.append(f"{len(findings)} finding(s) across {len(counts)} rule(s)") + for rule, count in sorted(counts.items()): + lines.append(f" {rule}: {count}") + return "\n".join(lines) + + +def _delta_text(delta: PreservationDelta) -> str: + lines = [] + for label, values in (("removed", delta.removed), ("added", delta.added)): + for path, categories in values.items(): + for category, items in categories.items(): + for item in items: + lines.append(f"{label}: {path}: {category}: {item}") + if not lines: + return "No preservation-report differences." + return "\n".join(lines) + + +def _baseline_delta_text(delta: BaselineDelta) -> str: + lines = [] + for label, values in (("new", delta.new), ("reduced", delta.reduced)): + for rule, paths in values.items(): + for path, count in paths.items(): + lines.append(f"{label}: {rule}: {path}: {count}") + if not lines: + return "No enforced compliance-baseline differences." + return "\n".join(lines) + + +def _require_imported_source_tree(repo_root: Path) -> None: + if not (repo_root / "build" / "SRC_VHDL").exists(): + raise FileNotFoundError( + "missing build/SRC_VHDL; run `make MODULES=\"$PWD\" import` before baseline checks" + ) + + +def main(argv: list[str] | None = None, repo_root: Path = REPO_ROOT) -> int: + parser = argparse.ArgumentParser( + description="Audit SURF regression structure and compare test-preservation reports." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + audit_parser = subparsers.add_parser("audit", help="report compliance screening signals") + audit_parser.add_argument("paths", nargs="*", default=["tests"]) + audit_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + audit_parser.add_argument("--output", help="write the report to this path") + + inventory_parser = subparsers.add_parser( + "inventory", + help="record pytest/cocotb names, parameters, gates, skips, and timeouts", + ) + inventory_parser.add_argument("paths", nargs="*", default=["tests"]) + inventory_parser.add_argument("--output", help="write the JSON report to this path") + + baseline_parser = subparsers.add_parser( + "baseline", + help="record current counts for reliable, enforceable audit rules", + ) + baseline_parser.add_argument("paths", nargs="*", default=["tests"]) + baseline_parser.add_argument("--output", required=True, help="write the JSON baseline here") + + check_parser = subparsers.add_parser( + "check", + help="fail when enforced findings exceed the checked-in legacy baseline", + ) + check_parser.add_argument("paths", nargs="*", default=["tests"]) + check_parser.add_argument( + "--baseline", + default=str(DEFAULT_BASELINE), + help="legacy baseline JSON path", + ) + check_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + check_parser.add_argument("--output", help="write the comparison to this path") + + compare_parser = subparsers.add_parser( + "compare", + help="compare two preservation reports and fail when coverage identifiers disappear", + ) + compare_parser.add_argument("before") + compare_parser.add_argument("after") + compare_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") + compare_parser.add_argument("--output", help="write the comparison to this path") + + args = parser.parse_args(argv) + try: + if args.command == "audit": + findings = audit_paths(args.paths, repo_root) + if args.json: + content = json.dumps( + { + "schema_version": SCHEMA_VERSION, + "findings": [finding.to_dict() for finding in findings], + }, + indent=2, + sort_keys=True, + ) + else: + content = _audit_text(findings) + _write_output(content, args.output) + return 0 + + if args.command == "inventory": + content = json.dumps( + preservation_report(args.paths, repo_root), + indent=2, + sort_keys=True, + ) + _write_output(content, args.output) + return 0 + + if args.command == "baseline": + _require_imported_source_tree(repo_root) + content = json.dumps( + compliance_baseline(audit_paths(args.paths, repo_root)), + indent=2, + sort_keys=True, + ) + _write_output(content, args.output) + return 0 + + if args.command == "check": + _require_imported_source_tree(repo_root) + baseline = json.loads(Path(args.baseline).read_text(encoding="utf-8")) + delta = compare_compliance_baseline( + baseline, + audit_paths(args.paths, repo_root), + ) + content = ( + json.dumps(delta.to_dict(), indent=2, sort_keys=True) + if args.json + else _baseline_delta_text(delta) + ) + _write_output(content, args.output) + return 1 if delta.has_new else 0 + + before = json.loads(Path(args.before).read_text(encoding="utf-8")) + after = json.loads(Path(args.after).read_text(encoding="utf-8")) + delta = compare_preservation_reports(before, after) + content = ( + json.dumps(delta.to_dict(), indent=2, sort_keys=True) + if args.json + else _delta_text(delta) + ) + _write_output(content, args.output) + return 1 if delta.has_removals else 0 + except (FileNotFoundError, SyntaxError, ValueError, json.JSONDecodeError) as exc: + parser.error(str(exc)) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/common/compliance_baseline.json b/tests/common/compliance_baseline.json new file mode 100644 index 0000000000..694daa3f3b --- /dev/null +++ b/tests/common/compliance_baseline.json @@ -0,0 +1,13 @@ +{ + "rules": { + "bare-return": {}, + "direct-runner": {}, + "duplicate-imported-source": {}, + "edge-then-timer": {}, + "early-bare-return": {}, + "missing-methodology": {}, + "open-ended-loop": {}, + "unretained-start-soon": {} + }, + "schema_version": 1 +} diff --git a/tests/common/regression_utils.py b/tests/common/regression_utils.py index 40cce48bb3..3368ce3340 100644 --- a/tests/common/regression_utils.py +++ b/tests/common/regression_utils.py @@ -10,10 +10,12 @@ from __future__ import annotations +from asyncio import CancelledError from functools import lru_cache import hashlib import os from pathlib import Path +import re import shlex import subprocess @@ -36,6 +38,14 @@ OPTIONAL_GHDL_WARNINGS = ("elaboration", "hide", "specs") +PRIMARY_VHDL_UNIT_RE = re.compile( + r"(?im)^(?:" + r"entity\s+(?P[a-z][a-z0-9_]*)\s+is\b|" + r"package\s+(?!body\b)(?P[a-z][a-z0-9_]*)\s+is\b|" + r"configuration\s+(?P[a-z][a-z0-9_]*)\s+of\b" + r")" +) + @lru_cache(maxsize=1) def _supported_ghdl_warning_names() -> frozenset[str]: @@ -70,11 +80,46 @@ def _optional_ghdl_warning_flags() -> list[str]: ] -def start_lockstep_clocks(*signals, period_ns: float) -> None: +async def sample_after_delta_cycles(clock) -> None: + """Wait for a rising edge and sample after combinational delta settling.""" + from cocotb.triggers import ReadOnly, RisingEdge + + await RisingEdge(clock) + await ReadOnly() + + +async def sample_after_tpd( + clock, + *, + propagation_time: float = 1.0, + unit: str = "ns", +) -> None: + """Propagation sampling: wait past a real VHDL ``after TPD_G`` delay.""" + from cocotb.triggers import RisingEdge, Timer + + await RisingEdge(clock) + await Timer(propagation_time, unit=unit) + + +async def wait_after_edge_offset( + clock, + *, + offset_time: float, + unit: str = "ns", +) -> None: + """Real-time timing: move stimulus by a deliberate offset after an edge.""" + from cocotb.triggers import RisingEdge, Timer + + await RisingEdge(clock) + await Timer(offset_time, unit=unit) + + +def start_lockstep_clocks(*signals, period_ns: float): import cocotb from cocotb.triggers import Timer async def drive() -> None: + """Lifetime agent: drive all requested clocks until the test ends.""" half_period_ns = period_ns / 2 for signal in signals: signal.value = 0 @@ -90,7 +135,27 @@ async def drive() -> None: # Drive logically-common clocks from one coroutine so COMMON_CLK_G tests # really exercise a shared clock, not two same-period oscillators that can # drift in phase relative to each other. - cocotb.start_soon(drive()) + return cocotb.start_soon(drive()) + + +async def cancel_and_join_tasks(tasks) -> None: + """Cancel owned lifetime tasks, await termination, and surface failures.""" + tasks = tuple(tasks) + for task in tasks: + task.cancel() + + first_error = None + for task in tasks: + try: + await task + except CancelledError: + pass + except BaseException as error: + if first_error is None: + first_error = error + + if first_error is not None: + raise first_error def env_flag(name: str, *, default: bool) -> bool: @@ -158,6 +223,39 @@ def hdl_parameters_from(parameters: dict[str, object]) -> dict[str, object]: } +def cocotb_test_filter(*test_names: str) -> str: + if not test_names: + raise ValueError("At least one cocotb test name is required") + alternatives = "|".join(re.escape(name) for name in test_names) + return rf"(?:{alternatives})$" + + +def cocotb_test_filter_excluding(*test_names: str) -> str: + if not test_names: + raise ValueError("At least one cocotb test name is required") + alternatives = "|".join(re.escape(name) for name in test_names) + return rf"^(?!.*(?:{alternatives})$).*" + + +def cocotb_filtered_env( + extra_env: dict[str, object], + test_filter: str, +) -> dict[str, object]: + result = dict(extra_env) + external_selectors = { + name: os.environ[name] + for name in ("COCOTB_TESTCASE", "COCOTB_TEST_FILTER") + if name in os.environ + } + if len(external_selectors) > 1: + raise ValueError("Specify only one of COCOTB_TESTCASE or COCOTB_TEST_FILTER") + if external_selectors: + result.update(external_selectors) + else: + result["COCOTB_TEST_FILTER"] = test_filter + return result + + def build_vhdl_sources() -> dict[str, list[str]]: surf_dir = BUILD_SRC_ROOT / "surf" ruckus_dir = BUILD_SRC_ROOT / "ruckus" @@ -173,6 +271,35 @@ def build_vhdl_sources() -> dict[str, list[str]]: } +def _resolved_source_path(path: str | Path) -> Path: + source = Path(path) + if not source.is_absolute(): + source = REPO_ROOT / source + return source.resolve() + + +@lru_cache(maxsize=None) +def _primary_vhdl_units(path: Path) -> frozenset[str]: + if path.suffix.lower() not in {".vhd", ".vhdl"}: + return frozenset() + + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + return frozenset() + + source_without_comments = "\n".join( + line.split("--", 1)[0] + for line in source.splitlines() + ) + return frozenset( + name.lower() + for match in PRIMARY_VHDL_UNIT_RE.finditer(source_without_comments) + for name in match.groupdict().values() + if name is not None + ) + + def merge_vhdl_sources( base_sources: dict[str, list[str]], extra_sources: dict[str, list[str]] | None, @@ -181,11 +308,46 @@ def merge_vhdl_sources( return base_sources merged = {library: list(paths) for library, paths in base_sources.items()} + resolved_by_library = { + library: {_resolved_source_path(path) for path in paths} + for library, paths in base_sources.items() + } + units_by_library = { + library: { + unit: _resolved_source_path(path) + for path in paths + for unit in _primary_vhdl_units(_resolved_source_path(path)) + } + for library, paths in base_sources.items() + } + for library, paths in extra_sources.items(): merged.setdefault(library, []) + resolved_by_library.setdefault(library, set()) + units_by_library.setdefault(library, {}) # Append test-local sources after the imported SURF library so wrappers # can instantiate the real RTL that was already compiled above. - merged[library].extend(str(Path(path)) for path in paths) + for path in paths: + resolved = _resolved_source_path(path) + if resolved in resolved_by_library[library]: + raise ValueError( + f"Extra VHDL source {path} duplicates {resolved} " + f"already present in library {library}" + ) + + units = _primary_vhdl_units(resolved) + duplicate_units = sorted(units & units_by_library[library].keys()) + if duplicate_units: + unit = duplicate_units[0] + previous = units_by_library[library][unit] + raise ValueError( + f"Extra VHDL source {path} declares {unit}, already declared " + f"by {previous} in library {library}" + ) + + merged[library].append(str(Path(path))) + resolved_by_library[library].add(resolved) + units_by_library[library].update({unit: resolved for unit in units}) return merged diff --git a/tests/common/test_compliance_audit.py b/tests/common/test_compliance_audit.py new file mode 100644 index 0000000000..7e404898b3 --- /dev/null +++ b/tests/common/test_compliance_audit.py @@ -0,0 +1,487 @@ +############################################################################## +## 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. +############################################################################## + +from __future__ import annotations + +import json + +from tests.common import compliance_audit + + +LICENSE_AND_METHODOLOGY = """\ +############################################################################## +## This file is part of 'SLAC Firmware Standard Library'. +############################################################################## + +# Test methodology: +# - Sweep: Example. +# - Stimulus: Example. +# - Checks: Example. +# - Timing: Example. +""" + + +def _rules(findings): + return [finding.rule for finding in findings] + + +def test_inventory_records_public_test_identifiers_and_controls(): + source = LICENSE_AND_METHODOLOGY + """ +import cocotb +import pytest + +RUN = "RUN_SAMPLE_KNOWN_ISSUE_TESTS" +SELECTOR = "COCOTB_TESTCASE" +FILTER = "COCOTB_TEST_FILTER" + +@cocotb.test(timeout_time=2, timeout_unit="us") +async def transfer_test(dut): + pass + +@pytest.mark.skipif(True, reason="example") +@pytest.mark.parametrize("parameters", [pytest.param({}, id="default")]) +def test_wrapper(parameters): + pass +""" + + inventory = compliance_audit.inventory_source(source) + + assert inventory.cocotb_entrypoints == ("transfer_test",) + assert inventory.pytest_functions == ("test_wrapper",) + assert inventory.parameter_ids == ("default",) + assert inventory.environment_controls == ( + "COCOTB_TESTCASE", + "COCOTB_TEST_FILTER", + "RUN_SAMPLE_KNOWN_ISSUE_TESTS", + ) + assert inventory.skipped_functions == ("test_wrapper",) + assert inventory.timeout_entrypoints == ("transfer_test",) + + +def test_inventory_recognizes_shared_filter_helper_as_selector_control(): + source = """ +def test_wrapper(parameters): + run(extra_env=cocotb_filtered_env(parameters, "default_test$")) +""" + + inventory = compliance_audit.inventory_source(source) + + assert inventory.environment_controls == ("COCOTB_TEST_FILTER",) + + +def test_audit_reports_high_and_low_confidence_screening_signals(): + source = """ +import cocotb +from cocotb.triggers import RisingEdge, Timer + +@cocotb.test() +async def routed_case(dut): + if int(dut.mode.value) == 0: + return + cocotb.start_soon(run_monitor()) + while True: + await RisingEdge(dut.clk) + await Timer(1, unit="ns") +""" + + findings = compliance_audit.audit_source(source) + + assert set(_rules(findings)) == { + "early-bare-return", + "missing-methodology", + "unretained-start-soon", + "open-ended-loop", + "edge-then-timer", + } + assert len(findings) == 5 + assert {finding.symbol for finding in findings} == {"", "routed_case"} + + +def test_audit_classifies_returns_by_prior_test_activity_not_line_distance(): + source = LICENSE_AND_METHODOLOGY + ''' +import cocotb + +@cocotb.test() +async def prestimulus_return(dut): + """A long description can move a no-op return far below the decorator. + + The rule must still recognize that the function has performed no awaited + simulator operation and made no assertion before taking this branch. + """ + mode = 1 + if mode: + return + +@cocotb.test() +async def successful_terminal_path(dut): + await exercise_named_behavior(dut) + assert dut.done.value + if dut.short_path.value: + return +''' + + findings = compliance_audit.audit_source(source) + returns = [finding for finding in findings if "bare-return" in finding.rule] + + assert {(finding.rule, finding.symbol) for finding in returns} == { + ("early-bare-return", "prestimulus_return"), + ("bare-return", "successful_terminal_path"), + } + + +def test_audit_accepts_an_explicit_post_check_terminal_scenario(): + source = LICENSE_AND_METHODOLOGY + ''' +import cocotb + +@cocotb.test() +async def parameter_terminal_path(dut): + await exercise_named_behavior(dut) + assert dut.done.value + # Terminal scenario: these checks are the complete contract for this parameter. + return +''' + + findings = compliance_audit.audit_source(source) + + assert "bare-return" not in _rules(findings) + + +def test_audit_ignores_retained_tasks_and_clock_tasks(): + source = LICENSE_AND_METHODOLOGY + """ +import cocotb +from cocotb.clock import Clock + +@cocotb.test() +async def task_case(dut): + monitor_task = cocotb.start_soon(run_monitor()) + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await monitor_task +""" + + findings = compliance_audit.audit_source(source) + + assert "unretained-start-soon" not in _rules(findings) + + +def test_audit_distinguishes_documented_lifetime_agents_from_unbounded_operations(): + source = ''' +async def monitor(): + """Lifetime agent: observe accepted transfers until the test ends.""" + while True: + await sample_transfer() + +async def receive_transaction(): + while True: + await sample_transfer() + if complete(): + break +''' + + findings = compliance_audit.audit_source(source) + loops = [finding for finding in findings if finding.rule == "open-ended-loop"] + + assert [(finding.symbol, finding.line) for finding in loops] == [ + ("receive_transaction", 8), + ] + + +def test_audit_distinguishes_classified_propagation_sampling_from_ambiguous_delay(): + source = ''' +from cocotb.triggers import RisingEdge, Timer + +async def sample_after_tpd(clk): + """Propagation sampling: wait past the configured RTL output delay.""" + await RisingEdge(clk) + await Timer(1, unit="ns") + +async def ambiguous_sample(clk): + await RisingEdge(clk) + await Timer(1, unit="ns") + +async def wait_after_edge_offset(clk): + """Real-time timing: place asynchronous stimulus between clock edges.""" + await RisingEdge(clk) + await Timer(2, unit="ns") +''' + + findings = compliance_audit.audit_source(source) + delays = [finding for finding in findings if finding.rule == "edge-then-timer"] + + assert [(finding.symbol, finding.line) for finding in delays] == [ + ("ambiguous_sample", 11), + ] + + +def test_audit_reports_direct_runner_except_documented_simlink_helper(): + source = """ +from cocotb_test.simulator import run + +def test_wrapper(): + run(toplevel="surf.target") +""" + + ordinary = compliance_audit.audit_source( + source, + "tests/axi/test_target.py", + ) + simlink = compliance_audit.audit_source( + source, + "tests/simlink/ghdl/simlink_test_utils.py", + ) + + assert _rules(ordinary) == ["direct-runner"] + assert simlink == () + + +def test_audit_detects_literal_source_already_in_import(tmp_path): + source_path = tmp_path / "base" / "rtl" / "Target.vhd" + source_path.parent.mkdir(parents=True) + source_path.write_text("entity Target is end entity;\n", encoding="utf-8") + + source = """ +def test_wrapper(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.target", + extra_vhdl_sources={"surf": ["base/rtl/Target.vhd"]}, + ) +""" + + findings = compliance_audit.audit_source( + source, + "tests/base/test_Target.py", + repo_root=tmp_path, + imported_sources=frozenset({source_path.resolve()}), + ) + + assert _rules(findings) == ["duplicate-imported-source"] + assert "base/rtl/Target.vhd" in findings[0].detail + + +def test_discovery_excludes_legacy_build_and_cache_directories(tmp_path): + included = tmp_path / "tests" / "base" / "test_included.py" + included.parent.mkdir(parents=True) + included.write_text("def test_included(): pass\n", encoding="utf-8") + + for directory in ("legacy", "sim_build", "__pycache__"): + excluded = tmp_path / "tests" / directory / "test_excluded.py" + excluded.parent.mkdir(parents=True) + excluded.write_text("def test_excluded(): pass\n", encoding="utf-8") + + discovered = compliance_audit.discover_python_files(("tests",), tmp_path) + + assert discovered == (included.resolve(),) + + +def test_preservation_comparison_reports_removed_and_added_identifiers(): + before = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["old_case"], + "pytest_functions": ["test_target"], + }, + }, + } + after = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["new_case"], + "pytest_functions": ["test_target"], + }, + }, + } + + delta = compliance_audit.compare_preservation_reports(before, after) + + assert delta.removed == { + "tests/test_target.py": {"cocotb_entrypoints": ("old_case",)}, + } + assert delta.added == { + "tests/test_target.py": {"cocotb_entrypoints": ("new_case",)}, + } + assert delta.has_removals is True + + +def test_compliance_baseline_allows_cleanup_but_rejects_new_findings(): + baseline = { + "schema_version": compliance_audit.SCHEMA_VERSION, + "rules": { + "bare-return": {}, + "direct-runner": {"tests/old.py": 1}, + "duplicate-imported-source": {"tests/sources.py": 2}, + "early-bare-return": {"tests/ambiguous.py": 1}, + "missing-methodology": {}, + "open-ended-loop": {}, + "unretained-start-soon": {}, + }, + } + findings = ( + compliance_audit.Finding( + "duplicate-imported-source", + "tests/sources.py", + 10, + "", + "existing", + ), + compliance_audit.Finding( + "missing-methodology", + "tests/new.py", + 1, + "", + "new", + ), + compliance_audit.Finding( + "early-bare-return", + "tests/ambiguous.py", + 20, + "scenario", + "not enforced yet", + ), + ) + + delta = compliance_audit.compare_compliance_baseline(baseline, findings) + + assert delta.new == {"missing-methodology": {"tests/new.py": 1}} + assert delta.reduced == { + "direct-runner": {"tests/old.py": 1}, + "duplicate-imported-source": {"tests/sources.py": 1}, + } + assert delta.has_new is True + + +def test_compliance_baseline_counts_findings_by_rule_and_path(): + findings = ( + compliance_audit.Finding( + "duplicate-imported-source", + "tests/source.py", + 10, + "", + "one", + ), + compliance_audit.Finding( + "duplicate-imported-source", + "tests/source.py", + 11, + "", + "two", + ), + compliance_audit.Finding( + "edge-then-timer", + "tests/timing.py", + 12, + "scenario", + "reported only", + ), + ) + + baseline = compliance_audit.compliance_baseline(findings) + + assert baseline == { + "schema_version": compliance_audit.SCHEMA_VERSION, + "rules": { + "bare-return": {}, + "direct-runner": {}, + "duplicate-imported-source": {"tests/source.py": 2}, + "edge-then-timer": {"tests/timing.py": 1}, + "early-bare-return": {}, + "missing-methodology": {}, + "open-ended-loop": {}, + "unretained-start-soon": {}, + }, + } + + +def test_repository_does_not_exceed_compliance_baseline(): + baseline = json.loads( + compliance_audit.DEFAULT_BASELINE.read_text(encoding="utf-8") + ) + findings = compliance_audit.audit_paths(("tests",)) + + delta = compliance_audit.compare_compliance_baseline(baseline, findings) + + assert not delta.has_new, compliance_audit._baseline_delta_text(delta) + + +def test_inventory_cli_writes_reproducible_json(tmp_path): + test_file = tmp_path / "tests" / "test_target.py" + test_file.parent.mkdir() + test_file.write_text( + LICENSE_AND_METHODOLOGY + + """ +import cocotb + +@cocotb.test() +async def target_case(dut): + pass + +def test_target(): + pass +""", + encoding="utf-8", + ) + output = tmp_path / "inventory.json" + + result = compliance_audit.main( + ["inventory", "tests", "--output", str(output)], + repo_root=tmp_path, + ) + + assert result == 0 + report = json.loads(output.read_text(encoding="utf-8")) + assert report == { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": { + "tests/test_target.py": { + "cocotb_entrypoints": ["target_case"], + "environment_controls": [], + "parameter_ids": [], + "pytest_functions": ["test_target"], + "skipped_functions": [], + "timeout_entrypoints": [], + }, + }, + } + + +def test_compare_cli_fails_when_identifiers_are_removed(tmp_path): + before = tmp_path / "before.json" + after = tmp_path / "after.json" + output = tmp_path / "delta.json" + before.write_text( + json.dumps( + { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": {"tests/test.py": {"pytest_functions": ["test_one"]}}, + } + ), + encoding="utf-8", + ) + after.write_text( + json.dumps( + { + "schema_version": compliance_audit.SCHEMA_VERSION, + "files": {"tests/test.py": {"pytest_functions": []}}, + } + ), + encoding="utf-8", + ) + + result = compliance_audit.main( + ["compare", str(before), str(after), "--json", "--output", str(output)], + repo_root=tmp_path, + ) + + assert result == 1 + delta = json.loads(output.read_text(encoding="utf-8")) + assert delta["removed"] == { + "tests/test.py": {"pytest_functions": ["test_one"]}, + } diff --git a/tests/common/test_regression_utils.py b/tests/common/test_regression_utils.py new file mode 100644 index 0000000000..9217edba94 --- /dev/null +++ b/tests/common/test_regression_utils.py @@ -0,0 +1,183 @@ +############################################################################## +## 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. +############################################################################## + +import asyncio +import re + +import pytest + +from tests.common.regression_utils import ( + cancel_and_join_tasks, + cocotb_filtered_env, + cocotb_test_filter, + cocotb_test_filter_excluding, + merge_vhdl_sources, +) + + +class FakeTask: + def __init__(self, name, events, *, error=None): + self.name = name + self.events = events + self.error = error + self.cancelled = False + + def cancel(self): + self.events.append(("cancel", self.name)) + self.cancelled = True + + def __await__(self): + async def join(): + self.events.append(("join", self.name)) + if self.error is not None: + raise self.error + if self.cancelled: + raise asyncio.CancelledError + + return join().__await__() + + +def test_cancel_and_join_tasks_cancels_all_before_joining(): + events = [] + tasks = (FakeTask("write", events), FakeTask("read", events)) + + asyncio.run(cancel_and_join_tasks(tasks)) + + assert events == [ + ("cancel", "write"), + ("cancel", "read"), + ("join", "write"), + ("join", "read"), + ] + + +def test_cancel_and_join_tasks_joins_all_before_raising_failure(): + events = [] + tasks = ( + FakeTask("write", events, error=RuntimeError("write responder failed")), + FakeTask("read", events), + ) + + with pytest.raises(RuntimeError, match="write responder failed"): + asyncio.run(cancel_and_join_tasks(tasks)) + + assert ("join", "read") in events + + +def test_cocotb_test_filter_matches_only_named_entrypoints(): + pattern = cocotb_test_filter("server_opens_test", "server_closes_test") + + assert re.search(pattern, "tests.protocol.server_opens_test") + assert re.search(pattern, "tests.protocol.server_closes_test") + assert not re.search(pattern, "tests.protocol.client_opens_test") + assert not re.search(pattern, "tests.protocol.server_opens_test_extra") + + +def test_cocotb_test_filter_excluding_rejects_only_named_entrypoints(): + pattern = cocotb_test_filter_excluding("routed_only_test", "extended_test") + + assert re.search(pattern, "tests.protocol.default_test") + assert not re.search(pattern, "tests.protocol.routed_only_test") + assert not re.search(pattern, "tests.protocol.extended_test") + assert re.search(pattern, "tests.protocol.extended_test_extra") + + +@pytest.mark.parametrize("builder", (cocotb_test_filter, cocotb_test_filter_excluding)) +def test_cocotb_filter_builders_require_a_test_name(builder): + with pytest.raises(ValueError, match="At least one"): + builder() + + +def test_cocotb_filtered_env_adds_filter_without_mutating_input(monkeypatch): + monkeypatch.delenv("COCOTB_TESTCASE", raising=False) + monkeypatch.delenv("COCOTB_TEST_FILTER", raising=False) + original = {"MODE_G": "ROUTED"} + + result = cocotb_filtered_env(original, "routed_.*_test$") + + assert result == { + "MODE_G": "ROUTED", + "COCOTB_TEST_FILTER": "routed_.*_test$", + } + assert original == {"MODE_G": "ROUTED"} + + +@pytest.mark.parametrize("selector", ("COCOTB_TESTCASE", "COCOTB_TEST_FILTER")) +def test_cocotb_filtered_env_preserves_external_selection(monkeypatch, selector): + monkeypatch.setenv(selector, "focused_test") + + assert cocotb_filtered_env({"MODE_G": "ROUTED"}, "default_.*") == { + "MODE_G": "ROUTED", + selector: "focused_test", + } + + +def test_cocotb_filtered_env_rejects_conflicting_external_selectors(monkeypatch): + monkeypatch.setenv("COCOTB_TESTCASE", "one_test") + monkeypatch.setenv("COCOTB_TEST_FILTER", "other_test") + + with pytest.raises(ValueError, match="Specify only one"): + cocotb_filtered_env({}, "default_.*") + + +def test_merge_vhdl_sources_rejects_same_resolved_file(tmp_path): + source = tmp_path / "Imported.vhd" + source.write_text("entity Imported is end entity;\n", encoding="utf-8") + alias = tmp_path / "Alias.vhd" + alias.symlink_to(source) + + with pytest.raises(ValueError, match="duplicates.*already present"): + merge_vhdl_sources( + {"surf": [str(source)]}, + {"surf": [str(alias)]}, + ) + + +def test_merge_vhdl_sources_rejects_duplicate_primary_unit(tmp_path): + imported = tmp_path / "Imported.vhd" + imported.write_text("entity SharedUnit is end entity;\n", encoding="utf-8") + extra = tmp_path / "Extra.vhd" + extra.write_text( + "-- entity CommentOnly is end entity;\n" + "entity SharedUnit is end entity;\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="declares sharedunit, already declared"): + merge_vhdl_sources( + {"surf": [str(imported)]}, + {"surf": [str(extra)]}, + ) + + +def test_merge_vhdl_sources_keeps_unique_units_and_library_boundaries(tmp_path): + imported = tmp_path / "Imported.vhd" + imported.write_text( + "package SharedPkg is end package;\n" + "entity Imported is end entity;\n", + encoding="utf-8", + ) + extra = tmp_path / "Extra.vhd" + extra.write_text("entity Extra is end entity;\n", encoding="utf-8") + other_library = tmp_path / "OtherLibrary.vhd" + other_library.write_text("entity Imported is end entity;\n", encoding="utf-8") + + merged = merge_vhdl_sources( + {"surf": [str(imported)]}, + { + "surf": [str(extra)], + "testlib": [str(other_library)], + }, + ) + + assert merged == { + "surf": [str(imported), str(extra)], + "testlib": [str(other_library)], + } diff --git a/tests/devices/__init__.py b/tests/devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/analog_devices/__init__.py b/tests/devices/analog_devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/analog_devices/test_Ad9249Sim.py b/tests/devices/analog_devices/test_Ad9249Sim.py new file mode 100644 index 0000000000..24da6dc93c --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249Sim.py @@ -0,0 +1,154 @@ +############################################################################## +## 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: Exercise conversion latency and the system-facing AD9249 shape with +# two serialized banks, differential DCO/FCO/data pins, real conversion, and +# a sample-alternating checkerboard pattern. +# - Stimulus: Drive a differential encode clock and independent channel codes, +# then program only the second CSB bank for a deterministic full-scale pattern. +# - Checks: Recovered words, coherent checkerboard frames, FCO framing, +# complementary pins, and bank-isolated SPI are checked without Xilinx models. +# - Timing: Normal conversions appear exactly 16 sample clocks after capture; +# both banks otherwise use ideal centered DCO/FCO timing in this test. + +import cocotb +from cocotb.triggers import Edge, Timer, with_timeout + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, bank, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0b10 if bank == 0 else 0b01 + await Timer(100, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + await Timer(100, unit="ns") + dut.csb.value = 0b11 + dut.sdioEnable.value = 0 + await Timer(200, unit="ns") + + +async def wait_fco_rise(dut, bank): + previous = (int(dut.fcoP.value) >> bank) & 1 + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") + current = (int(dut.fcoP.value) >> bank) & 1 + if previous == 0 and current == 1: + return + previous = current + assert False, f"FCO bank {bank} did not produce a rising edge" + + +async def capture_bank(dut, bank): + await wait_fco_rise(dut, bank) + words = [0] * 8 + frame = 0 + for _ in range(14): + await Edge(dut.dcoP) + data = int(dut.dP.value) >> (8 * bank) + frame = (frame << 1) | ((int(dut.fcoP.value) >> bank) & 1) + for channel in range(8): + words[channel] = (words[channel] << 1) | ((data >> channel) & 1) + return words, frame + + +@cocotb.test() +async def ad9249_pin_level_device_sim_test(dut): + normal = [0x100 + i for i in range(16)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 0b11 + clock_task = cocotb.start_soon(differential_clock(dut)) + # Allow the model's time-zero output initialization to settle before any + # std_logic_vector is converted to an integer by the frame collector. + await Timer(1, unit="ns") + + # A normal conversion captured with the first frame must remain absent for + # 16 complete output frames, then appear on the seventeenth frame. + for _ in range(16): + bank0, frame0 = await capture_bank(dut, 0) + assert bank0 == [0] * 8 + assert frame0 == 0b11111110000000 + + bank0, frame0 = await capture_bank(dut, 0) + bank1, frame1 = await capture_bank(dut, 1) + assert bank0 == normal[:8] + assert bank1 == normal[8:] + assert frame0 == 0b11111110000000 + assert frame1 == 0b11111110000000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + # Both groups share SCLK/SDIO, but their independent CSB pins must isolate + # configuration. Program only bank 1 for positive full-scale output. + await spi_write(dut, 1, 0x0D, 0x02) + bank0, _ = await capture_bank(dut, 0) + bank1, _ = await capture_bank(dut, 1) + assert bank0 == normal[:8] + assert bank1 == [0x3FFF] * 8 + + # Alternating test words must be captured once per frame rather than read + # live across serialization, which would combine both checkerboard phases. + await spi_write(dut, 1, 0x0D, 0x04) + checkerboard = [] + for _ in range(4): + bank1, _ = await capture_bank(dut, 1) + assert len(set(bank1)) == 1 + assert bank1[0] in (0x2AAA, 0x1555) + checkerboard.append(bank1[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9249Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9249SimCore.py b/tests/devices/analog_devices/test_Ad9249SimCore.py new file mode 100644 index 0000000000..d341d0e604 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249SimCore.py @@ -0,0 +1,190 @@ +############################################################################## +## 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: Exercise one eight-channel AD9249 bank's defaults, device indexing, +# immediate writes, transferred resolution/rate override, user patterns, PN9, +# inversion, bit order, and power-down. +# - Stimulus: Write the primitive-free byte-register interface while sampling +# independent normal words for every bank channel. +# - Checks: Identity/default readback, selected-channel isolation, pattern +# values, PN reset hold/release, global transforms, soft reset, and +# suppression and the staged 0x100 update are checked. +# - Timing: Configuration writes and samples share the model sample clock; +# only the AD9249 resolution/rate override requires device update. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" + await FallingEdge(dut.sampleClk) + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, addr): + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +@cocotb.test() +async def ad9249_bank_register_and_pattern_test(dut): + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum((0x200 + i) << (16 * i) for i in range(8)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + assert await read(dut, 0x01) == 0x92 + assert await read(dut, 0x02) == 0x30 + assert await read(dut, 0x04) == 0x0F + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x200 + i for i in range(8)] + + # Register 0x100 is staged until the transfer strobe at register 0xFF. + await write(dut, 0x100, 0x63) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x100) == 0x63 + + # Two's-complement normal-data mode flips the 14-bit code MSB. Test + # patterns are checked separately because their format applicability differs. + await write(dut, 0x14, 0x01) + data = await sample(dut) + assert channel(data, 1) == (0x201 ^ 0x2000) + await write(dut, 0x00, 0x04) + + # Select only channels 0 and 3. AD9249 writes become visible immediately. + await write(dut, 0x04, 0x00) + await write(dut, 0x05, 0x09) + await write(dut, 0x0D, 0x04) + data = await sample(dut) + assert channel(data, 0) in (0x2AAA, 0x1555) + assert channel(data, 3) == channel(data, 0) + assert channel(data, 1) == 0x201 + + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x08) + first = channel(await sample(dut), 0) + second = channel(await sample(dut), 0) + assert {first, second} == {0x0123, 0x0456} + + # PN23 reset remains readable and holds the generator at its seed until + # software clears the level. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) + pn23_second = pn_word( + pn_advance(pn23_seed, order=23, tap=18), order=23, tap=18) + await write(dut, 0x0D, 0x25) + assert await read(dut, 0x0D) == 0x25 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_first + await write(dut, 0x0D, 0x05) + assert await read(dut, 0x0D) == 0x05 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_second + + # Apply the same hold/release check to PN9, then retain the existing global + # output inversion and LSB-first serialization coverage. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0x80) + assert await read(dut, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = int(f"{pn_word(pn9_seed):014b}"[::-1], 2) ^ 0x3FFF + pn9_second = int( + f"{pn_word(pn_advance(pn9_seed)):014b}"[::-1], 2) ^ 0x3FFF + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_first + await write(dut, 0x0D, 0x06) + assert await read(dut, 0x0D) == 0x06 + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_second + + await write(dut, 0x22, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0 + assert channel(data, 3) == 0 + normal_reversed = int(f"{0x201:014b}"[::-1], 2) + assert channel(data, 1) == (normal_reversed ^ 0x3FFF) + + # Global power modes suppress every output bank channel. + await write(dut, 0x08, 0x01) + assert await sample(dut) == 0 + + # Soft reset restores selection, normal data, inversion, and bit order. + await write(dut, 0x00, 0x04) + assert await read(dut, 0x04) == 0x0F + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x200 + i for i in range(8)] + + +def test_Ad9249SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9249SimTiming.py b/tests/devices/analog_devices/test_Ad9249SimTiming.py new file mode 100644 index 0000000000..0d62f6792f --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9249SimTiming.py @@ -0,0 +1,131 @@ +############################################################################## +## 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: Exercise per-lane/per-bank static skew and bounded deterministic +# jitter in both source-synchronous AD9249 output banks. +# - Stimulus: Serialize transition-rich normal samples after the 16-clock +# conversion pipeline; skew data lane 0 and FCO bank 0 relative to bank 1. +# - Checks: DCO remains ideal, skew is preserved, jitter alternates around each +# nominal transition cadence, and every differential output remains binary. +# - Timing: Bias/jitter are 50 ps, data skew is 100 ps, and FCO skew is 150 ps. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def intervals(edges): + return [current-previous for previous, current in zip(edges, edges[1:])] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +def contains_near(values, expected, tolerance=2): + return any(abs(value-expected) <= tolerance for value in values) + + +@cocotb.test() +async def ad9249_binary_skew_and_jitter_test(dut): + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(16)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 0b11 + clock_task = cocotb.start_soon(differential_clock(dut)) + + await Timer(480, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco0_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + fco1_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 1, 6)) + # Include a DCO edge after the last, much slower FCO transition so every + # measured source edge has a following sampling edge for setup calculation. + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 48)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco0_edges = await fco0_task + fco1_edges = await fco1_task + dco_edges = await dco_task + + assert all(abs((a-b)-100) <= 1 for a, b in zip(lane0_edges, lane1_edges)) + assert all(abs((a-b)-150) <= 1 for a, b in zip(fco0_edges, fco1_edges)) + assert all(abs(interval-1714) <= 1 for interval in intervals(dco_edges)) + + data_setup = setup_times(lane1_edges, dco_edges) + assert contains_near(data_setup, 807) + assert contains_near(data_setup, 907) + assert contains_near(intervals(lane1_edges), 1614) + assert contains_near(intervals(lane1_edges), 1814) + assert contains_near(intervals(fco1_edges), 11900) + assert contains_near(intervals(fco1_edges), 12100) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9249SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9249simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_LANE0_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249SimCore.vhd", + "devices/AnalogDevices/ad9249/sim/Ad9249Sim.vhd", + "devices/AnalogDevices/ad9249/wrappers/Ad9249SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252Sim.py b/tests/devices/analog_devices/test_Ad9252Sim.py new file mode 100644 index 0000000000..d143ec8088 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252Sim.py @@ -0,0 +1,147 @@ +############################################################################## +## 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: Exercise conversion latency and the system-facing AD9252 pin shape in +# normal, deterministic, and sample-alternating checkerboard modes. +# - Stimulus: Drive differential encode clock and real-valued channel inputs, +# then configure one channel through the actual shared SDIO interface. +# - Checks: Recover coherent serialized words and framing, verify differential +# complements, and confirm staged SPI configuration reaches the output pins. +# - Timing: Normal conversions appear exactly eight sample clocks after capture; +# data and FCO otherwise use ideal centered timing in this test. + +import cocotb +from cocotb.triggers import Edge, Timer, with_timeout + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0 + await Timer(100, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(80, unit="ns") + dut.sclk.value = 1 + await Timer(80, unit="ns") + dut.sclk.value = 0 + await Timer(100, unit="ns") + dut.csb.value = 1 + dut.sdioEnable.value = 0 + await Timer(200, unit="ns") + + +async def capture_frame(dut): + previous = int(dut.fcoP.value) + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") + current = int(dut.fcoP.value) + if previous == 0 and current == 1: + break + previous = current + else: + assert False, "FCO did not produce a rising edge" + words = [0] * 8 + frame = 0 + for _ in range(14): + await Edge(dut.dcoP) + data = int(dut.dP.value) + frame = (frame << 1) | int(dut.fcoP.value) + for channel in range(8): + words[channel] = (words[channel] << 1) | ((data >> channel) & 1) + return words, frame + + +@cocotb.test() +async def ad9252_pin_level_device_sim_test(dut): + normal = [0x100 + i for i in range(8)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + clock_task = cocotb.start_soon(differential_clock(dut)) + # Allow the model's time-zero output initialization to settle before any + # std_logic value is converted to an integer by the frame collector. + await Timer(1, unit="ns") + + # Seven explicit conversion registers plus the coherent frame handoff model + # the specified eight-clock latency at the device pins. + for _ in range(8): + words, frame = await capture_frame(dut) + assert words == [0] * 8 + assert frame == 0b11111110000000 + + words, frame = await capture_frame(dut) + assert words == normal + assert frame == 0b11111110000000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFF) + assert int(dut.dcoN.value) == (not int(dut.dcoP.value)) + assert int(dut.fcoN.value) == (not int(dut.fcoP.value)) + + await spi_write(dut, 0x05, 0x01) + await spi_write(dut, 0x0D, 0x02) + await spi_write(dut, 0xFF, 0x01) + words, _ = await capture_frame(dut) + assert words[0] == 0x3FFF + assert words[1:] == normal[1:] + + # Alternating output words must remain coherent across each serialized + # frame even though the core toggles the pattern every sample clock. + await spi_write(dut, 0x0D, 0x04) + await spi_write(dut, 0xFF, 0x01) + checkerboard = [] + for _ in range(4): + words, _ = await capture_frame(dut) + assert words[0] in (0x2AAA, 0x1555) + assert words[1:] == normal[1:] + checkerboard.append(words[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9252Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252SimCore.py b/tests/devices/analog_devices/test_Ad9252SimCore.py new file mode 100644 index 0000000000..a4fb53cb78 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252SimCore.py @@ -0,0 +1,176 @@ +############################################################################## +## 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: Exercise AD9252 defaults, channel indexing, buffered transfer, +# user patterns, PN9, inversion, bit order, and channel power controls. +# - Stimulus: Write the logical register interface and issue device-update +# transfers while sampling eight independent normal input words. +# - Checks: Reads expose identity/defaults, writes have no effect before +# transfer, selected channels update together, PN resets hold/release, and +# unselected channels do not change. +# - Timing: Register writes and each output sample occur on the sample clock. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" + await FallingEdge(dut.sampleClk) + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, addr): + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +@cocotb.test() +async def ad9252_register_and_pattern_test(dut): + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum((0x100 + i) << (16 * i) for i in range(8)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + assert await read(dut, 0x01) == 0x09 + assert await read(dut, 0x02) == 0x30 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [0x100 + i for i in range(8)] + + # Select channels 0 and 3, stage checkerboard, and prove transfer is required. + await write(dut, 0x05, 0x09) + await write(dut, 0x0D, 0x04) + data = await sample(dut) + assert channel(data, 0) == 0x100 + await write(dut, 0xFF, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0x2AAA + assert channel(data, 3) == 0x2AAA + assert channel(data, 1) == 0x101 + + # User pattern updates selected channels atomically. + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x08) + await write(dut, 0xFF, 0x01) + first = channel(await sample(dut), 0) + second = channel(await sample(dut), 0) + assert {first, second} == {0x0123, 0x0456} + + # PN23 reset is published by device update and remains asserted until a + # second staged write/update releases the generator from its seed. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) + pn23_state_1 = pn_advance(pn23_seed, order=23, tap=18) + pn23_state_2 = pn_advance(pn23_state_1, order=23, tap=18) + pn23_second = pn_word(pn23_state_1, order=23, tap=18) + pn23_third = pn_word(pn23_state_2, order=23, tap=18) + await write(dut, 0x0D, 0x25) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x25 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_first + await write(dut, 0x0D, 0x05) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x05 + assert channel(await sample(dut), 0) == pn23_first + assert channel(await sample(dut), 0) == pn23_second + # Re-publishing unchanged shadow configuration must not rewind live PN + # state maintained independently by each selected channel. + await write(dut, 0xFF, 0x01) + assert channel(await sample(dut), 0) == pn23_third + + # Apply the same hold/release check to PN9 together with global inversion + # and LSB-first bit reversal. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0x80) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = int(f"{pn_word(pn9_seed):014b}"[::-1], 2) ^ 0x3FFF + pn9_second = int( + f"{pn_word(pn_advance(pn9_seed)):014b}"[::-1], 2) ^ 0x3FFF + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_first + await write(dut, 0x0D, 0x06) + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x0D) == 0x06 + assert channel(await sample(dut), 0) == pn9_first + assert channel(await sample(dut), 0) == pn9_second + + # Power-down is per selected channel and leaves other channels running. + await write(dut, 0x22, 0x01) + await write(dut, 0xFF, 0x01) + data = await sample(dut) + assert channel(data, 0) == 0 + assert channel(data, 3) == 0 + normal_reversed = int(f"{0x101:014b}"[::-1], 2) + assert channel(data, 1) == (normal_reversed ^ 0x3FFF) + + +def test_Ad9252SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9252SimTiming.py b/tests/devices/analog_devices/test_Ad9252SimTiming.py new file mode 100644 index 0000000000..3ca4eef611 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9252SimTiming.py @@ -0,0 +1,131 @@ +############################################################################## +## 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: Exercise per-lane/FCO static skew and bounded deterministic jitter in +# the primitive-free AD9252 pin-level model. +# - Stimulus: Serialize transition-rich normal samples after the eight-clock +# conversion pipeline; skew data lane 0 and the single FCO output. +# - Checks: DCO remains ideal, programmed skew changes setup time, jitter +# alternates around nominal transition cadence, and differential pins resolve. +# - Timing: Bias/jitter are 50 ps, data skew is 100 ps, and FCO skew is 150 ps. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=24): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def intervals(edges): + return [current-previous for previous, current in zip(edges, edges[1:])] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +def contains_near(values, expected, tolerance=2): + return any(abs(value-expected) <= tolerance for value in values) + + +@cocotb.test() +async def ad9252_binary_skew_and_jitter_test(dut): + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(8)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + clock_task = cocotb.start_soon(differential_clock(dut)) + + await Timer(288, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + # Include a DCO edge after the last, much slower FCO transition so every + # measured source edge has a following sampling edge for setup calculation. + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 48)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco_edges = await fco_task + dco_edges = await dco_task + + assert all(abs((a-b)-100) <= 1 for a, b in zip(lane0_edges, lane1_edges)) + assert all(abs(interval-1714) <= 1 for interval in intervals(dco_edges)) + + data_setup = setup_times(lane1_edges, dco_edges) + fco_setup = setup_times(fco_edges, dco_edges) + assert contains_near(data_setup, 807) + assert contains_near(data_setup, 907) + assert contains_near(fco_setup, 657) + assert contains_near(fco_setup, 757) + assert contains_near(intervals(lane1_edges), 1614) + assert contains_near(intervals(lane1_edges), 1814) + assert contains_near(intervals(fco_edges), 11900) + assert contains_near(intervals(fco_edges), 12100) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFF) + assert int(dut.dcoN.value) == (not int(dut.dcoP.value)) + assert int(dut.fcoN.value) == (not int(dut.fcoP.value)) + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9252SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9252simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252SimCore.vhd", + "devices/AnalogDevices/ad9252/sim/Ad9252Sim.vhd", + "devices/AnalogDevices/ad9252/wrappers/Ad9252SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681Sim.py b/tests/devices/analog_devices/test_Ad9681Sim.py new file mode 100644 index 0000000000..5424870a09 --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681Sim.py @@ -0,0 +1,153 @@ +############################################################################## +## 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: Exercise conversion latency and the system-facing AD9681 two-byte-lane +# pin shape, including a sample-alternating checkerboard pattern. +# - Stimulus: Drive differential encode clock and independent real-valued +# channels, then issue immediate writes through the actual shared SDIO pins. +# - Checks: Recombine both serialized bytes, verify coherent checkerboard words, +# framing and differential complements, and confirm SPI updates are applied +# across both channel halves. +# - Timing: Normal conversions appear exactly 16 sample clocks after capture; +# both byte groups share an ideal centered DCO/FCO waveform. + +import cocotb +from cocotb.triggers import Edge, Timer, with_timeout + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=8): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def spi_write(dut, address, value): + header = address & 0x1FFF + dut.sclk.value = 0 + dut.sdioEnable.value = 1 + dut.csb.value = 0 + await Timer(50, unit="ns") + for bit in range(15, -1, -1): + dut.sdioDrive.value = (header >> bit) & 1 + await Timer(30, unit="ns") + dut.sclk.value = 1 + await Timer(30, unit="ns") + dut.sclk.value = 0 + for bit in range(7, -1, -1): + dut.sdioDrive.value = (value >> bit) & 1 + await Timer(30, unit="ns") + dut.sclk.value = 1 + await Timer(30, unit="ns") + dut.sclk.value = 0 + await Timer(50, unit="ns") + dut.csb.value = 1 + dut.sdioEnable.value = 0 + await Timer(100, unit="ns") + + +async def capture_frame(dut): + previous = int(dut.fcoP.value) & 1 + for _ in range(3): + await with_timeout(Edge(dut.fcoP), 100, "ns") + current = int(dut.fcoP.value) & 1 + if previous == 0 and current == 1: + break + previous = current + else: + assert False, "FCO did not produce a rising edge" + low = [0] * 8 + high = [0] * 8 + frame = 0 + for _ in range(8): + await Edge(dut.dcoP) + data = int(dut.dP.value) + frame = (frame << 1) | (int(dut.fcoP.value) & 1) + for channel in range(8): + low[channel] = (low[channel] << 1) | ((data >> channel) & 1) + high[channel] = (high[channel] << 1) | ((data >> (8 + channel)) & 1) + return [(high[i] << 8) | low[i] for i in range(8)], frame + + +@cocotb.test() +async def ad9681_pin_level_device_sim_test(dut): + normal = [0x200 + i for i in range(8)] + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + clock_task = cocotb.start_soon(differential_clock(dut)) + await Timer(1, unit="ns") + + # A normal conversion captured with the first frame must remain absent for + # 16 complete output frames, then appear on the seventeenth frame. + for _ in range(16): + words, frame = await capture_frame(dut) + assert words == [0] * 8 + assert frame == 0b11110000 + + words, frame = await capture_frame(dut) + assert words == [value << 2 for value in normal] + assert frame == 0b11110000 + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + + # Device index bit 0 selects data channel pair A (physical channels 0 and + # 1). The test mode applies to exactly that pair and leaves the rest on + # normal data, confirming per-channel selection through the actual SPI pins. + await spi_write(dut, 0x05, 0x01) + await spi_write(dut, 0x0D, 0x02) + words, _ = await capture_frame(dut) + assert words[0] == 0xFFFC + assert words[1] == 0xFFFC + assert words[2:] == [value << 2 for value in normal[2:]] + + # Reselect every channel, then apply the alternating checkerboard. Words are + # latched once per frame; reading sampleData live for each serialized bit + # would tear the two patterns together. A single broadcast write keeps every + # channel coherent, so channels 0 and 4 must show the same phase each frame. + await spi_write(dut, 0x05, 0x3F) + await spi_write(dut, 0x0D, 0x04) + checkerboard = [] + for _ in range(4): + words, _ = await capture_frame(dut) + assert words[0] == words[4] + assert words[0] in (0xAAA8, 0x5554) + checkerboard.append(words[0]) + assert checkerboard[0] == checkerboard[2] + assert checkerboard[1] == checkerboard[3] + assert checkerboard[0] != checkerboard[1] + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9681Sim(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simwrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681SimCore.py b/tests/devices/analog_devices/test_Ad9681SimCore.py new file mode 100644 index 0000000000..7c03bb6a3c --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681SimCore.py @@ -0,0 +1,220 @@ +############################################################################## +## 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: Exercise default two-lane words, the register 0x05 device index that +# selects data channels, the staged resolution/rate override, user and PN +# patterns, inversion, bit order, and power for all eight channels. +# - Stimulus: Write the primitive-free byte-register interface while sampling +# independent normal codes for all eight channels. +# - Checks: Padding placement, identity, immediate ordinary-register updates, +# device-index channel isolation, 0x100 transfer semantics, unsupported-format +# fallback, pattern values, PN reset hold/release, transforms, soft reset, and +# suppression are checked. +# - Timing: Configuration and samples share one clock; only the resolution/rate +# override remains hidden until the register 0xFF transfer write. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer + +from tests.common.regression_utils import run_surf_vhdl_test + + +async def write(dut, addr, data): + """Propagation sampling: deassert writes after registered TPD updates.""" + await FallingEdge(dut.sampleClk) + dut.cfgAddr.value = addr + dut.cfgWrData.value = data + dut.cfgWrEn.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.cfgWrEn.value = 0 + + +async def read(dut, addr): + dut.cfgAddr.value = addr + await Timer(1, unit="ns") + return int(dut.cfgRdData.value) + + +async def sample(dut): + """Propagation sampling: read each registered sample after its TPD update.""" + await FallingEdge(dut.sampleClk) + dut.sampleEnable.value = 1 + await RisingEdge(dut.sampleClk) + await Timer(2, unit="ns") + dut.sampleEnable.value = 0 + return int(dut.sampleData.value) + + +def channel(data, index): + return (data >> (16 * index)) & 0xFFFF + + +def pn_word(state, order=9, tap=5, width=14): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return word + + +def pn_advance(state, order=9, tap=5, width=14): + for _ in range(width): + state = ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + return state + + +def reverse_bytes(word): + upper = int(f"{(word >> 8) & 0xFF:08b}"[::-1], 2) + lower = int(f"{word & 0xFF:08b}"[::-1], 2) + return (upper << 8) | lower + + +@cocotb.test() +async def ad9681_register_index_and_pattern_test(dut): + normal = [0x200 + i for i in range(8)] + dut.sampleRst.value = 1 + dut.sampleEnable.value = 0 + dut.cfgWrEn.value = 0 + dut.cfgAddr.value = 0 + dut.cfgWrData.value = 0 + dut.normalData.value = sum(value << (16 * i) for i, value in enumerate(normal)) + cocotb.start_soon(Clock(dut.sampleClk, 8, unit="ns").start()) + for _ in range(2): + await RisingEdge(dut.sampleClk) + dut.sampleRst.value = 0 + + # Identity and defaults. Register 0x05 powers up at 0x3F so every data and + # clock channel receives the next write. + assert await read(dut, 0x01) == 0x8F + assert await read(dut, 0x02) == 0x60 + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + assert all((channel(data, i) & 0x3) == 0 for i in range(8)) + + # Register 0x100 is staged until the transfer strobe at Register 0xFF. + await write(dut, 0x100, 0x66) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x00) + assert await read(dut, 0x100) == 0x00 + await write(dut, 0xFF, 0x01) + assert await read(dut, 0x100) == 0x66 + + # Unsupported output formats warn immediately but retain register + # readback and continue using the model's fixed two-lane bytewise format. + await write(dut, 0x21, 0x00) + assert await read(dut, 0x21) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + await write(dut, 0x21, 0x30) + + # Device index bits[3:0] select data channels A..D, each a pair (channel ch + # is gated by bit ch/2). Selecting only channel B (bit 1) applies a test + # mode to channels 2 and 3 and leaves the others on normal data. + await write(dut, 0x05, 0x02) + await write(dut, 0x0D, 0x02) + data = await sample(dut) + assert channel(data, 2) == 0xFFFC + assert channel(data, 3) == 0xFFFC + assert channel(data, 0) == normal[0] << 2 + assert channel(data, 4) == normal[4] << 2 + + # A local read with a single channel selected returns that channel's copy. + assert await read(dut, 0x0D) == 0x02 + # Restore full selection; the datasheet returns Channel A1 for an all-set + # read, so 0x0D reads back channel 0's (still default) test mode. + await write(dut, 0x05, 0x3F) + await write(dut, 0x0D, 0x00) + + # Program every channel for alternating user words in one broadcast write. + # A single snapshot holds one shared toggle phase, so all channels match; + # consecutive snapshots alternate. Capturing one sample and indexing every + # channel keeps the shared state at a single instant. + await write(dut, 0x19, 0x23) + await write(dut, 0x1A, 0x01) + await write(dut, 0x1B, 0x56) + await write(dut, 0x1C, 0x04) + await write(dut, 0x0D, 0x48) + first = await sample(dut) + second = await sample(dut) + assert all(channel(first, i) == channel(first, 0) for i in range(8)) + assert all(channel(second, i) == channel(second, 0) for i in range(8)) + assert {channel(first, 0), channel(second, 0)} == {0x0123, 0x0456} + + # PN23 reset is a retained level, not a self-clearing command. While it is + # asserted, the selected generators stay at their seed; clearing it releases + # a repeatable sequence from that seed. Because one broadcast write reseeds + # all channels in the same cycle, every channel stays coherent. + pn23_seed = 0b01001101110000000101000 + pn23_first = pn_word(pn23_seed, order=23, tap=18) << 2 + pn23_state_1 = pn_advance(pn23_seed, order=23, tap=18) + pn23_state_2 = pn_advance(pn23_state_1, order=23, tap=18) + pn23_second = pn_word(pn23_state_1, order=23, tap=18) << 2 + pn23_third = pn_word(pn23_state_2, order=23, tap=18) << 2 + def all_channels(data): + return [channel(data, i) for i in range(8)] + + await write(dut, 0x0D, 0x25) + assert await read(dut, 0x0D) == 0x25 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + await write(dut, 0x0D, 0x05) + assert await read(dut, 0x0D) == 0x05 + assert all_channels(await sample(dut)) == [pn23_first] * 8 + assert all_channels(await sample(dut)) == [pn23_second] * 8 + # Transferring unchanged resolution/rate configuration must not rewind the + # live PN state maintained independently by each channel. + await write(dut, 0xFF, 0x01) + assert all_channels(await sample(dut)) == [pn23_third] * 8 + + # Apply the same hold/release check to the PN9 generator, together with + # global inversion and byte-local LSB-first ordering. + await write(dut, 0x0D, 0x16) + await write(dut, 0x14, 0x04) + await write(dut, 0x21, 0xB0) + assert await read(dut, 0x0D) == 0x16 + pn9_seed = 0b011011111 + pn9_first = reverse_bytes((pn_word(pn9_seed) << 2) ^ 0xFFFF) + pn9_second = reverse_bytes( + (pn_word(pn_advance(pn9_seed)) << 2) ^ 0xFFFF) + assert all_channels(await sample(dut)) == [pn9_first] * 8 + assert all_channels(await sample(dut)) == [pn9_first] * 8 + await write(dut, 0x0D, 0x06) + assert await read(dut, 0x0D) == 0x06 + assert all_channels(await sample(dut)) == [pn9_first] * 8 + assert all_channels(await sample(dut)) == [pn9_second] * 8 + + # A power-mode write immediately suppresses all channels. Soft reset + # immediately restores default two-lane normal output and override state. + await write(dut, 0x08, 0x01) + assert await sample(dut) == 0 + await write(dut, 0x00, 0x04) + assert await read(dut, 0x05) == 0x3F + assert await read(dut, 0x100) == 0x00 + data = await sample(dut) + assert [channel(data, i) for i in range(8)] == [value << 2 for value in normal] + + +def test_Ad9681SimCore(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simcorewrapper", + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimCoreWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_Ad9681SimTiming.py b/tests/devices/analog_devices/test_Ad9681SimTiming.py new file mode 100644 index 0000000000..cb7768a37d --- /dev/null +++ b/tests/devices/analog_devices/test_Ad9681SimTiming.py @@ -0,0 +1,135 @@ +############################################################################## +## 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: Exercise static per-lane/FCO transition skew together with bounded +# deterministic jitter in the primitive-free AD9681 pin-level model. +# - Stimulus: Serialize a repeating transition-rich normal sample with a common +# timing bias; one data lane and one FCO lane receive additional static skew. +# - Checks: DCO retains its exact period, the unskewed data/FCO setup times are +# centered at 500 ps, paired lanes retain their programmed skew, transition +# intervals alternate around their nominal cadence, and every differential +# output remains binary and complementary. +# - Timing: Common bias and jitter are both 50 ps, data lane 0 adds 100 ps, and +# FCO lane 0 adds 150 ps relative to their corresponding lane 1 outputs. + +import cocotb +from cocotb.triggers import Edge, Timer +from cocotb.utils import get_sim_time + +from tests.common.regression_utils import cancel_and_join_tasks, run_surf_vhdl_test + + +async def differential_clock(dut, period_ns=8): + """Lifetime agent: drive the encode clock until the owning test cancels it.""" + half = period_ns / 2 + while True: + dut.clkP.value = 0 + dut.clkN.value = 1 + await Timer(half, unit="ns") + dut.clkP.value = 1 + dut.clkN.value = 0 + await Timer(half, unit="ns") + + +async def collect_bit_edges(signal, bit, count): + previous = (int(signal.value) >> bit) & 1 + edges = [] + while len(edges) < count: + await Edge(signal) + current = (int(signal.value) >> bit) & 1 + if current != previous: + edges.append(int(get_sim_time(unit="ps"))) + previous = current + return edges + + +def short_intervals(edges, maximum_ps): + return [ + current-previous + for previous, current in zip(edges, edges[1:]) + if current-previous < maximum_ps + ] + + +def setup_times(edges, dco_edges): + return [next(dco-edge for dco in dco_edges if dco > edge) for edge in edges] + + +@cocotb.test() +async def ad9681_binary_skew_and_jitter_test(dut): + # 0x2AAA becomes 0xAAA8 on each 16-bit serialized channel, providing + # repeated transitions in both physical byte groups without SPI setup. + dut.normalData.value = sum(0x2AAA << (16*channel) for channel in range(8)) + dut.sclk.value = 0 + dut.sdioDrive.value = 0 + dut.sdioEnable.value = 0 + dut.csb.value = 1 + clock_task = cocotb.start_soon(differential_clock(dut)) + + # Allow the modeled 16-sample ADC conversion pipeline to fill before + # collecting data edges and their corresponding DCO sampling edges. + await Timer(160, unit="ns") + + lane0_task = cocotb.start_soon(collect_bit_edges(dut.dP, 0, 16)) + lane1_task = cocotb.start_soon(collect_bit_edges(dut.dP, 1, 16)) + fco0_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 0, 6)) + fco1_task = cocotb.start_soon(collect_bit_edges(dut.fcoP, 1, 6)) + dco_task = cocotb.start_soon(collect_bit_edges(dut.dcoP, 0, 32)) + + lane0_edges = await lane0_task + lane1_edges = await lane1_task + fco0_edges = await fco0_task + fco1_edges = await fco1_task + dco_edges = await dco_task + + assert all(a-b == 100 for a, b in zip(lane0_edges, lane1_edges)) + assert all(a-b == 150 for a, b in zip(fco0_edges, fco1_edges)) + assert all(interval == 1000 for interval in short_intervals(dco_edges, 1500)) + assert set(setup_times(lane1_edges, dco_edges)) == {450, 550} + assert set(setup_times(fco1_edges, dco_edges)) == {450, 550} + + # Alternating -50/+50 ps displacement creates 900/1100 ps data intervals + # and 3900/4100 ps FCO intervals around their nominal cadence. + assert {900, 1100}.issubset(set(short_intervals(lane1_edges, 1500))) + assert {3900, 4100}.issubset(set(short_intervals(fco1_edges, 5000))) + + assert dut.dP.value.is_resolvable + assert dut.dN.value.is_resolvable + assert dut.dcoP.value.is_resolvable + assert dut.dcoN.value.is_resolvable + assert dut.fcoP.value.is_resolvable + assert dut.fcoN.value.is_resolvable + assert int(dut.dN.value) == ((~int(dut.dP.value)) & 0xFFFF) + assert int(dut.dcoN.value) == ((~int(dut.dcoP.value)) & 0x3) + assert int(dut.fcoN.value) == ((~int(dut.fcoP.value)) & 0x3) + await cancel_and_join_tasks((clock_task,)) + + +def test_Ad9681SimTiming(): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.ad9681simwrapper", + parameters={ + "DATA_LANE0_SKEW_PS_G": 100, + "FCO_LANE0_SKEW_PS_G": 150, + "JITTER_PS_G": 50, + "TIMING_BIAS_PS_G": 50, + }, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/general/rtl/AdiConfigSlave.vhd", + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681SimCore.vhd", + "devices/AnalogDevices/ad9681/sim/Ad9681Sim.vhd", + "devices/AnalogDevices/ad9681/wrappers/Ad9681SimWrapper.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrCalibration.py b/tests/devices/analog_devices/test_AdcDdrCalibration.py new file mode 100644 index 0000000000..b690b15392 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrCalibration.py @@ -0,0 +1,1252 @@ +############################################################################## +## 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: Exercise ordinary, tied, boundary, full-range, narrow, guarded, and +# circular passing windows over small representative delay ranges. +# - Stimulus: Supply deterministic tap-to-pass maps directly to the common eye +# selector without PyRogue hardware access. +# - Checks: Verify selected taps, physical margins, boundary visibility, +# wraparound reporting, deterministic ties, invalid-input rejection, and +# topology-aware, overlap-safe parallel grouping plus restoration between +# guard checks for coupled physical lanes. Exercise strict ordered snapshot +# scanning, deep hardware qualification, verification progress, final +# qualification and live run-time reporting. +# - Timing: Fake register access and zero settling keep process tests short; the +# live timer regression temporarily reduces its update interval. + +import copy +import threading +import time + +import pytest + +pr = pytest.importorskip( + 'pyrogue', + reason='ADC DDR calibration tests require Rogue/PyRogue') + +from surf.devices.analog_devices import ( # noqa: E402 + AdcDdrCalibration, + checkAdcDdrPn23, + findAdcDdrEye, + findAdcDdrEyes, +) + + +class FakeVariable: + def __init__(self, value=0, getter=None): + self._value = value + self._getter = getter + + def get(self, read=False): + return self._getter() if self._getter is not None else self._value + + def set(self, value, write=False): + self._value = value + + def value(self): + return self._value + + +class FakeConfig: + def __init__(self, pn23=False): + self.OutputTestMode = FakeVariable(0) + self.ResetPNLongReg = FakeVariable(False) + self.pn23ResetValues = [] + self.digitalResetValues = [] + self.updateCount = 0 + if not pn23: + self.ResetPNLong = None + + def update(self): + self.updateCount += 1 + + def DigitalReset(self): + self.digitalResetValues.extend((3, 0)) + + def ResetPNLong(self): + self.pn23ResetValues.extend((True, False)) + + +class FakePatternTester: + def __init__(self, readout): + self._readout = readout + self.Alternating = FakeVariable(False) + self.Pn23 = FakeVariable(False) + self.ReferenceChannel = FakeVariable(0) + self.ChannelMask = FakeVariable(0) + self.FcoMask = FakeVariable(0) + self.DataMask = FakeVariable(0) + self.PatternA = FakeVariable(0) + self.PatternB = FakeVariable(0) + self.Samples = FakeVariable(0) + self.Timeout = FakeVariable(0) + self.Sequence = FakeVariable(0) + self.CheckedSamples = FakeVariable(0) + self.ChannelPassed = FakeVariable(0) + self.FcoPassed = FakeVariable(0) + self.WordErrorCount = {index: FakeVariable(0) for index in range(16)} + self.BitErrorMask = {index: FakeVariable(0) for index in range(16)} + self.FcoErrorCount = {index: FakeVariable(0) for index in range(16)} + self.Busy = FakeVariable(False) + self.TimedOut = FakeVariable(False) + self.ConfigError = FakeVariable(False) + self.Aborted = FakeVariable(False) + self.PhaseAcquired = FakeVariable(False) + self.AllChannelsPass = FakeVariable(False) + self.AllFcoPass = FakeVariable(False) + self.measurements = [] + + def Start(self): + alternating = bool(self.Alternating.value()) + pn23 = bool(self.Pn23.value()) + mask = self.DataMask.value() + patternA = self.PatternA.value() & mask + patternB = self.PatternB.value() & mask + sampleCount = self.Samples.value() + channelMask = self.ChannelMask.value() + referenceChannel = self.ReferenceChannel.value() + rawReference = [ + self._readout._debugSample(referenceChannel, index) & mask + for index in range(sampleCount) + ] + reference = [sample ^ patternA for sample in rawReference] if pn23 else rawReference + pnErrorBits = [0] * sampleCount + if pn23: + bits = [ + (sample >> bitIndex) & 1 + for sample in reference + for bitIndex in range(self._readout._sampleBits-1, -1, -1) + ] + phase = len(bits) >= 23 and any(bits[:23]) + for index in range(23, len(bits)): + if bits[index] != (bits[index-23] ^ bits[index-18]): + wordIndex = index // self._readout._sampleBits + bitIndex = self._readout._sampleBits-1-(index % self._readout._sampleBits) + pnErrorBits[wordIndex] |= 1 << bitIndex + if len(bits) >= 23 and not any(bits[:23]): + firstCompleteWord = 22 // self._readout._sampleBits + pnErrorBits[firstCompleteWord] = mask + elif not alternating: + phase = 0 + elif reference and reference[0] == patternA: + phase = 0 + elif reference and reference[0] == patternB: + phase = 1 + else: + phase = None + channelPassed = 0 + for channel in range(self._readout._channels): + self.WordErrorCount[channel].set(0) + self.BitErrorMask[channel].set(0) + if not channelMask & (1 << channel): + continue + samples = [ + self._readout._debugSample(channel, index) & mask + for index in range(sampleCount) + ] + if pn23: + if channel == referenceChannel: + errorBits = pnErrorBits + else: + transformed = [sample ^ patternA for sample in samples] + errorBits = [ + sample ^ wanted + for sample, wanted in zip(transformed, reference) + ] + else: + expected = [ + patternA if not alternating else (patternA, patternB)[(phase+index) % 2] + for index in range(sampleCount) + ] if phase is not None else [None] * sampleCount + errorBits = [ + mask if wanted is None else sample ^ wanted + for sample, wanted in zip(samples, expected) + ] + wordErrors = sum(bool(bits) for bits in errorBits) + bitErrors = 0 + for bits in errorBits: + bitErrors |= bits + self.WordErrorCount[channel].set(wordErrors) + self.BitErrorMask[channel].set(bitErrors) + if wordErrors == 0: + channelPassed |= 1 << channel + + fcoMask = self.FcoMask.value() + fcoPassed = self._readout._lockedMask() & fcoMask + for lane in range(self._readout._fcoLanes): + self.FcoErrorCount[lane].set( + 0 if fcoPassed & (1 << lane) else 1) + self.ChannelPassed.set(channelPassed) + self.FcoPassed.set(fcoPassed) + self.CheckedSamples.set(sampleCount) + self.Busy.set(False) + self.TimedOut.set(False) + self.ConfigError.set(False) + self.Aborted.set(False) + self.PhaseAcquired.set(phase is not None) + self.AllChannelsPass.set((channelPassed & channelMask) == channelMask) + self.AllFcoPass.set((fcoPassed & fcoMask) == fcoMask) + self.Sequence.set(self.Sequence.value()+1) + self.measurements.append({ + 'delays': tuple( + variable.value() for variable in self._readout.DataDelay.values()), + 'channelMask': channelMask, + 'dataMask': mask, + 'pn23': pn23, + 'samples': sampleCount, + 'timeout': self.Timeout.value(), + }) + + def Abort(self): + self.Busy.set(False) + self.Aborted.set(True) + self.Sequence.set(self.Sequence.value()+1) + + +class FakeReadout: + def __init__(self): + self._dataLanes = 2 + self._fcoLanes = 1 + self._channels = 2 + self._sampleBits = 14 + self._delayBits = 3 + self._patternCheck = True + self.FcoDelay = {0: FakeVariable(3)} + self.FcoWord = {0: FakeVariable(0x3F00)} + self.DataDelay = {0: FakeVariable(2), 1: FakeVariable(4)} + self.PatternCheck = FakeVariable(True) + self.SnapshotSequence = FakeVariable(0) + self.LockedMask = FakeVariable(getter=self._lockedMask) + self.nodes = { + f'DebugSampleRaw[{channel}][{index}]': FakeVariable( + getter=lambda channel=channel, index=index: self._debugSample(channel, index)) + for channel in range(2) + for index in range(4) + } + self.relockCount = 0 + self.snapshotDelays = [] + self.dataDelayWrites = [] + self.PatternTester = FakePatternTester(self) + + def _lockedMask(self): + return int(2 <= self.FcoDelay[0].value() <= 5) + + def _debugSample(self, channel, index): + tap = self.DataDelay[channel].value() + passing = (1 <= tap <= 4) if channel == 0 else (3 <= tap <= 7) + if not passing: + return 0 + return 0x2AAA if index % 2 == 0 else 0x1555 + + def _getDebugSamples(self, read=False): + return [ + [ + self.nodes[f'DebugSampleRaw[{channel}][{index}]'].get(read=read) + for index in range(4) + ] + for channel in range(self._channels) + ] + + def _getDataDelays(self, read=False): + return [variable.get(read=read) for variable in self.DataDelay.values()] + + def _setDataDelays(self, values): + self.dataDelayWrites.append(dict(values)) + for lane, value in values.items(): + self.DataDelay[lane].set(value, write=True) + + def Snapshot(self): + self.snapshotDelays.append(tuple( + variable.value() for variable in self.DataDelay.values())) + self.SnapshotSequence.set(self.SnapshotSequence.value()+1) + + def Relock(self): + self.relockCount += 1 + + def checkGeometry(self): + # These fakes are constructed consistent with their own geometry, so the + # model/RTL capability cross-check is vacuously satisfied here. + pass + + +class CoupledFakeReadout(FakeReadout): + def __init__(self): + super().__init__() + self._channels = 1 + self.nodes = { + f'DebugSampleRaw[0][{index}]': FakeVariable( + getter=lambda index=index: self._debugSample(0, index)) + for index in range(4) + } + + def _debugSample(self, channel, index): + # Both physical lanes form one logical sample. Each lane's guard points + # pass against the selected value of the other lane, but the combination + # of both upper guard points does not. + passing = not ( + self.DataDelay[0].value() == 3 and + self.DataDelay[1].value() == 5) + if not passing: + return 0 + return 0x2AAA if index % 2 == 0 else 0x1555 + + +class MaskedCoupledFakeReadout(FakeReadout): + def __init__(self): + super().__init__() + self._channels = 1 + self.DataDelay = {0: FakeVariable(0), 1: FakeVariable(0)} + self.nodes = { + f'DebugSampleRaw[0][{index}]': FakeVariable( + getter=lambda index=index: self._debugSample(0, index)) + for index in range(4) + } + + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + sample |= expected & 0x3FC0 + return sample + + +class PhaseMismatchedCoupledFakeReadout(MaskedCoupledFakeReadout): + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + opposite = 0x1555 if index % 2 == 0 else 0x2AAA + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + sample |= opposite & 0x3FC0 + return sample + + +class DeepPatternFaultReadout(FakeReadout): + def _debugSample(self, channel, index): + sample = super()._debugSample(channel, index) + if channel == 1 and index == 20: + sample ^= 0x4 + return sample + + +class MultiWindowFcoFakeReadout(MaskedCoupledFakeReadout): + def __init__(self): + super().__init__() + self._fcoLanes = 2 + self.FcoDelay = {0: FakeVariable(0), 1: FakeVariable(0)} + self.FcoWord = {0: FakeVariable(0xF0), 1: FakeVariable(0xF0)} + + def _lockedMask(self): + mask = 0 + if self.FcoDelay[0].value() in (1, 2, 3, 5, 6): + mask |= 0x1 + if self.FcoDelay[1].value() in (1, 2, 4, 5, 6): + mask |= 0x2 + return mask + + def _fcoPhaseAligned(self): + lowWindow = ( + self.FcoDelay[0].value() in (1, 2, 3) and + self.FcoDelay[1].value() in (1, 2)) + highWindow = ( + self.FcoDelay[0].value() in (5, 6) and + self.FcoDelay[1].value() in (4, 5, 6)) + return lowWindow or highWindow + + def _debugSample(self, channel, index): + expected = 0x2AAA if index % 2 == 0 else 0x1555 + opposite = 0x1555 if index % 2 == 0 else 0x2AAA + sample = 0 + if 1 <= self.DataDelay[0].value() <= 3: + sample |= expected & 0x003F + if 4 <= self.DataDelay[1].value() <= 6: + upper = expected if self._fcoPhaseAligned() else opposite + sample |= upper & 0x3FC0 + return sample + + +def _pn23Words(state=0x654321, count=4, width=14): + mask = (1 << 23)-1 + words = [] + for _ in range(count): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> 22) & 1) + state = ( + ((state << 1) & mask) | + (((state >> 22) ^ (state >> 17)) & 1)) + words.append(word) + return words + + +class Pn23FakeReadout(FakeReadout): + def __init__(self, config, fault=None): + super().__init__() + self._config = config + self._fault = fault + self._pn23 = _pn23Words(count=5000) + + def _debugSample(self, channel, index): + if self._config.OutputTestMode.value() != 5: + return super()._debugSample(channel, index) + if self._fault == 'channelShift' and channel == 1: + return self._pn23[index+1] + sample = self._pn23[index] + if self._fault == 'commonCorruption' and index == 2: + sample ^= 0x1 + if self._fault == 'deepCommonCorruption' and index == 20: + sample ^= 0x4 + return sample + + +class Pn23MultiWindowFcoFakeReadout(MultiWindowFcoFakeReadout): + def __init__(self, config): + super().__init__() + self._config = config + self._pn23 = _pn23Words(count=5000) + + def _fcoPhaseAligned(self): + # Both preferred and alternate FCO combinations look valid under the + # repetitive checkerboard; PN23 must disambiguate them. + return True + + def _debugSample(self, channel, index): + if self._config.OutputTestMode.value() != 5: + return super()._debugSample(channel, index) + sample = self._pn23[index] + if self.FcoDelay[1].value() == 5 and index == 2: + sample ^= 0x1 + return sample + + +def test_selects_center_and_reports_margins(): + eye = findAdcDdrEye({0: False, 1: True, 2: True, 3: True, 4: True, 5: False}) + + assert eye.start == 1 + assert eye.end == 4 + assert eye.width == 4 + assert eye.selected == 2 + assert eye.leftMargin == 1 + assert eye.rightMargin == 2 + assert eye.leftBounded + assert eye.rightBounded + assert eye.contains(3) + assert not eye.contains(5) + + +def test_tie_selects_lowest_center(): + eye = findAdcDdrEye({ + 0: False, + 1: True, + 2: True, + 3: False, + 4: False, + 5: True, + 6: True, + 7: False, + }) + + assert (eye.start, eye.end, eye.selected) == (1, 2, 1) + + +def test_returns_all_qualifying_eyes_in_priority_order(): + eyes = findAdcDdrEyes({ + 0: False, + 1: True, + 2: True, + 3: False, + 4: True, + 5: True, + 6: True, + 7: False, + }) + + assert [(eye.start, eye.end, eye.selected) for eye in eyes] == [ + (4, 6, 5), + (1, 2, 1), + ] + assert findAdcDdrEye({tap: True for tap in range(4)}) == findAdcDdrEyes( + {tap: True for tap in range(4)})[0] + + +def test_pn23_recurrence_acquires_arbitrary_phase_and_handles_formatting(): + words = _pn23Words() + + result = checkAdcDdrPn23(words, 14) + assert result['passed'] + assert result['selected']['name'] == 'asCaptured' + assert result['selected']['checkedBits'] == 33 + + # Datasheet PN23 values are shown after two's-complement formatting, which + # flips the logical sample MSB relative to the underlying PN bit stream. + formatted = [0x1FFF, 0x1FE0, 0x2001, 0x1C00] + result = checkAdcDdrPn23(formatted, 14) + assert result['passed'] + assert result['selected']['name'] == 'formatMsbInverted' + + corrupted = list(words) + corrupted[2] ^= 0x1 + assert not checkAdcDdrPn23(corrupted, 14)['passed'] + assert not checkAdcDdrPn23([0, 0, 0, 0], 14)['passed'] + + +def test_minimum_width_and_guard_band_reject_narrow_windows(): + passing = {tap: 1 <= tap <= 3 for tap in range(5)} + + assert findAdcDdrEye(passing, minimumWidth=3).width == 3 + assert findAdcDdrEye(passing, guardBand=1).selected == 2 + with pytest.raises(RuntimeError, match='at least 4 taps'): + findAdcDdrEye(passing, minimumWidth=4) + with pytest.raises(RuntimeError, match='at least 5 taps'): + findAdcDdrEye(passing, guardBand=2) + + +def test_scan_boundary_visibility(): + left = findAdcDdrEye({0: True, 1: True, 2: False, 3: False}) + right = findAdcDdrEye({0: False, 1: False, 2: True, 3: True}) + full = findAdcDdrEye({tap: True for tap in range(4)}) + + assert not left.leftBounded and left.rightBounded + assert right.leftBounded and not right.rightBounded + assert not full.leftBounded and not full.rightBounded + + +def test_circular_window_merges_scan_boundaries(): + passing = {tap: tap in (0, 1, 6, 7) for tap in range(8)} + linear = findAdcDdrEye(passing) + circular = findAdcDdrEye(passing, circular=True) + + assert linear.width == 2 + assert (circular.start, circular.end) == (6, 1) + assert circular.width == 4 + assert circular.selected == 7 + assert circular.leftMargin == 1 + assert circular.rightMargin == 2 + assert circular.wraps + assert circular.leftBounded and circular.rightBounded + assert circular.contains(0) + assert circular.contains(7) + assert not circular.contains(3) + + +@pytest.mark.parametrize( + ('passing', 'kwargs', 'exception', 'message'), + [ + ({}, {}, ValueError, 'must not be empty'), + ({0: False, 1: False}, {}, RuntimeError, 'no passing'), + ({0: True, 2: True}, {}, ValueError, 'consecutive'), + ({0: True}, {'minimumWidth': 0}, ValueError, 'at least one'), + ({0: True}, {'guardBand': -1}, ValueError, 'must not be negative'), + ]) +def test_rejects_invalid_scans(passing, kwargs, exception, message): + with pytest.raises(exception, match=message): + findAdcDdrEye(passing, **kwargs) + + +def make_calibration(*, usePatternTester=False): + config = FakeConfig() + readout = FakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + calibration.UsePatternTester.set(usePatternTester) + calibration._testRoot = root + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + return calibration, config, readout, root + + +def test_ad9681_topology_builds_one_parallel_lane_group(): + config = FakeConfig() + readout = FakeReadout() + readout._dataLanes = 16 + readout._channels = 8 + + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=tuple(range(8))+tuple(range(8)), + dataLaneMasks=(0x003F,)*8+(0x3FC0,)*8) + + assert calibration._dataLaneGroups == (tuple(range(16)),) + + +@pytest.fixture +def calibration_fixture(): + calibration, config, readout, root = make_calibration() + try: + yield calibration, config, readout + finally: + calibration._runEn = False + root.stop() + + +def test_full_calibration_applies_results_and_can_reapply(calibration_fixture): + calibration, config, readout = calibration_fixture + + assert calibration.Debug.value() is False + assert calibration.Margin.value() == 'Unavailable' + assert calibration.UsePatternTester.value() is False + assert calibration.Outcome.value() == calibration.OUTCOME_IDLE_C + results = calibration._runCalibration(dev=calibration) + + assert results['Fco'][0]['eye']['selected'] == 3 + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + assert config.OutputTestMode.value() == 0 + assert config.updateCount == 3 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + assert calibration._dataLaneGroups == ((0, 1),) + assert readout.dataDelayWrites[:8] == [ + {0: tap, 1: tap} + for tap in range(8) + ] + assert readout.dataDelayWrites[8] == {0: 2, 1: 5} + assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] + assert calibration.RunTime.value() > 0.0 + assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C + assert calibration.Margin.value() == '1 tap minimum; scan-limited eye(s)' + + readout.FcoDelay[0].set(0) + readout.DataDelay[0].set(0) + readout.DataDelay[1].set(0) + calibration.applyResults() + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + + +def test_margin_report_is_compact_and_interpretable(calibration_fixture, capsys): + calibration, _, _ = calibration_fixture + calibration._runCalibration(dev=calibration) + + calibration.MarginReport() + + report = capsys.readouterr().out + assert 'ADC alignment margins (native tap units)' in report + assert 'Type Lane Eye Selected Left Right Worst Bounds' in report + assert 'FCO 0 2..5 3 1 2 1 bounded' in report + assert 'DATA 0 1..4 2 1 2 1 bounded' in report + assert 'DATA 1 3..7 5 2 2 2 right scan limit' in report + assert 'Headline: 1 tap minimum; scan-limited eye(s)' in report + + +def test_margin_report_requires_successful_full_calibration(calibration_fixture): + calibration, _, _ = calibration_fixture + + with pytest.raises(RuntimeError, match='No successful full-calibration margin result'): + calibration.MarginReport() + + +def test_pattern_alignment_reset_runs_after_checkerboard_selection(): + events = [] + calibration, config, readout, root = make_calibration() + originalReset = config.DigitalReset + + def record(): + events.append({ + 'mode': config.OutputTestMode.value(), + 'snapshots': len(readout.snapshotDelays), + 'relocks': readout.relockCount, + }) + return originalReset() + + config.DigitalReset = record + try: + calibration._runCalibration(dev=calibration) + + assert events == [ + {'mode': 4, 'snapshots': 0, 'relocks': 9}, + {'mode': 4, 'snapshots': 8, 'relocks': 10}, + ] + assert config.digitalResetValues == [3, 0, 3, 0] + assert config.updateCount == 3 + # The receiver restarts after each ADC digital reset has been released. + assert readout.relockCount >= events[-1]['relocks']+1 + finally: + calibration._runEn = False + root.stop() + + +def test_diagnostics_publish_only_at_process_boundaries( + calibration_fixture, monkeypatch): + calibration, _, _ = calibration_fixture + calibration.Debug.set(True) + publications = [] + originalSet = calibration.Diagnostics.set + + def record(value, *args, **kwargs): + publications.append(copy.deepcopy(value)) + return originalSet(value, *args, **kwargs) + + monkeypatch.setattr(calibration.Diagnostics, 'set', record) + + calibration._runCalibration(dev=calibration) + + assert len(publications) == 2 + assert publications[0] == {} + assert publications[1]['Current'] == {'kind': 'Final'} + assert publications[1]['Final']['passed'] + + +def test_process_gui_message_keeps_framework_status(calibration_fixture): + calibration, _, _ = calibration_fixture + + calibration._process() + + assert calibration.Outcome.value() == calibration.OUTCOME_PASSED_C + assert calibration.Message.value() == 'Done' + + +def test_full_calibration_can_add_deep_pattern_qualification(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.Debug.set(True) + calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) + + results = calibration._runCalibration(dev=calibration) + + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + assert readout.snapshotDelays == [(tap, tap) for tap in range(8)] + [(2, 5)] + assert len(readout.PatternTester.measurements) == 1 + assert readout.PatternTester.measurements[0] == { + 'delays': (2, 5), + 'channelMask': 0x3, + 'dataMask': 0x3FFF, + 'pn23': False, + 'samples': 32, + 'timeout': calibration.PATTERN_TESTER_TIMEOUT_C, + } + deep = results['Final']['patternTester'] + assert deep['enabled'] and deep['performed'] and deep['passed'] + assert deep['requestedSamples'] == 32 + assert deep['checkedSamples'] == 32 + assert deep['channels'][0]['wordErrorCount'] == 0 + diagnostics = calibration.Diagnostics.value() + assert diagnostics['MeasurementBackend'] == 'Snapshot' + assert diagnostics['DeepPatternTester'] == {'enabled': True, 'samples': 32} + assert calibration.RunTime.value() > 0.0 + + +def test_snapshot_data_scan_requires_ordered_shared_phase(calibration_fixture): + calibration, _, readout = calibration_fixture + sequence = (0x2AAA, 0x2AAA, 0x1555, 0x1555) + readout._debugSample = lambda channel, index: sequence[index] + + details = calibration._captureGroupPasses((0, 1)) + + assert not details[0]['passed'] + assert not details[1]['passed'] + assert details[0]['captures'][0]['expectedSequence'] == [ + 0x2AAA, 0x1555, 0x2AAA, 0x1555] + + +def test_deep_pattern_error_is_reported_and_fails_final_qualification(): + config = FakeConfig() + readout = DeepPatternFaultReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + deep = calibration.Results.value()['Final']['patternTester'] + assert deep['performed'] and not deep['passed'] + assert deep['channels'][0]['wordErrorCount'] == 0 + assert deep['channels'][1]['wordErrorCount'] == 1 + assert deep['channels'][1]['bitErrorMask'] == 0x4 + assert deep['fco'][0] == {'passed': True, 'errorCount': 0} + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_full_calibration_adds_snapshot_pn23_qualification(usePatternTester): + config = FakeConfig(pn23=True) + readout = Pn23FakeReadout(config) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert calibration.VerifyPn23.value() + assert results['Final']['checkerboardPassed'] + assert results['Final']['pn23']['enabled'] + assert results['Final']['pn23']['performed'] + assert results['Final']['pn23']['coherencePassed'] + assert results['Final']['pn23']['recurrence']['passed'] + assert results['Final']['pn23']['passed'] + assert results['Final']['passed'] + assert config.OutputTestMode.value() == 0 + assert config.pn23ResetValues == [True, False] + # PN23 always starts with one atomic snapshot. Deep qualification adds + # one checkerboard and one PN23 tester window after centering. + assert readout.snapshotDelays[-1] == (2, 5) + assert len(readout.PatternTester.measurements) == 2*usePatternTester + if usePatternTester: + deep = results['Final']['pn23']['patternTester'] + assert deep['mode'] == 'Pn23' + assert deep['checkedSamples'] == 4096 + assert deep['status']['phaseAcquired'] + assert readout.PatternTester.measurements[-1]['pn23'] + finally: + calibration._runEn = False + root.stop() + + +def test_deep_pn23_error_beyond_snapshot_is_reported(): + config = FakeConfig(pn23=True) + readout = Pn23FakeReadout(config, fault='deepCommonCorruption') + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(True) + calibration.PatternTesterSamples.set(32) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + pn23 = calibration.Results.value()['Final']['pn23'] + assert pn23['snapshotPassed'] + deep = pn23['patternTester'] + assert deep['performed'] and not deep['passed'] + assert deep['status']['phaseAcquired'] + assert deep['channels'][0]['wordErrorCount'] >= 1 + assert deep['channels'][0]['bitErrorMask'] & 0x4 + assert deep['channels'][1]['wordErrorCount'] == 0 + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize( + ('fault', 'coherencePassed', 'recurrencePassed'), + [ + ('channelShift', False, True), + ('commonCorruption', True, False), + ]) +def test_pn23_qualification_rejects_relative_and_common_errors( + fault, coherencePassed, recurrencePassed): + config = FakeConfig(pn23=True) + readout = Pn23FakeReadout(config, fault=fault) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + pn23 = calibration.Results.value()['Final']['pn23'] + assert pn23['coherencePassed'] is coherencePassed + assert pn23['recurrence']['passed'] is recurrencePassed + assert not pn23['passed'] + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + finally: + calibration._runEn = False + root.stop() + + +def test_pattern_tester_selection_requires_hardware_capability(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.UsePatternTester.set(True) + readout.PatternCheck.set(False) + + with pytest.raises(RuntimeError, match='pattern tester is not present'): + calibration._runCalibration(dev=calibration) + + assert calibration.RunTime.value() > 0.0 + + +def test_run_time_updates_while_calibration_is_running(calibration_fixture): + calibration, _, _ = calibration_fixture + started = threading.Event() + release = threading.Event() + calibration.RUN_TIME_UPDATE_INTERVAL_C = 0.01 + + def operation(*, dev): + started.set() + assert release.wait(1.0) + + calibration._runCalibrationImpl = operation + worker = threading.Thread( + target=calibration._runCalibration, + kwargs={'dev': calibration}) + worker.start() + try: + assert started.wait(1.0) + deadline = time.monotonic()+1.0 + while calibration.RunTime.value() == 0.0 and time.monotonic() < deadline: + time.sleep(0.005) + runningTime = calibration.RunTime.value() + assert runningTime > 0.0 + finally: + release.set() + worker.join(1.0) + + assert not worker.is_alive() + assert calibration.RunTime.value() >= runningTime + + +def test_failed_calibration_restores_mode_and_all_delays(calibration_fixture): + calibration, config, readout = calibration_fixture + calibration.MinimumEyeWidth.set(6) + config.OutputTestMode.set(7) + + with pytest.raises(RuntimeError, match='at least 6 taps'): + calibration._runCalibration(dev=calibration) + + assert config.OutputTestMode.value() == 7 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.RunTime.value() > 0.0 + assert calibration.Outcome.value() == calibration.OUTCOME_FAILED_C + assert calibration.Message.value().startswith('FAILED:') + + +def test_failed_data_scan_reports_lane_and_retains_partial_results(calibration_fixture): + calibration, _, readout = calibration_fixture + calibration.Debug.set(True) + for node in readout.nodes.values(): + node._getter = lambda: 0 + + with pytest.raises(RuntimeError, match='Data lane 0: scan contains no passing'): + calibration._runCalibration(dev=calibration) + + results = calibration.Results.value() + assert results['Fco'][0]['eye']['selected'] == 3 + assert not any(results['Data'][0]['passing'].values()) + assert results['Data'][0]['diagnostics'][0]['expected'] == [0x1555, 0x2AAA] + assert results['Data'][0]['diagnostics'][0]['captures'][0]['raw'] == [0, 0, 0, 0] + diagnostics = calibration.Diagnostics.value() + assert diagnostics['Fco'][0][0]['word'] == 0x3F00 + assert diagnostics['Current'] == {'kind': 'Data', 'lanes': [0, 1], 'tap': 7} + + +def test_verify_current_and_guard_band_restore_hardware_state(calibration_fixture): + calibration, config, readout = calibration_fixture + calibration.Operation.set(calibration.VERIFY_CURRENT_C) + + results = calibration._runCalibration(dev=calibration) + assert all(result['passed'] for lanes in results.values() for result in lanes.values()) + assert calibration.TotalSteps.value() == 3 + assert calibration.Step.value() == 3 + assert calibration.Progress.value() == 1.0 + + calibration.Operation.set(calibration.VERIFY_GUARD_BAND_C) + calibration.GuardBand.set(2) + with pytest.raises(RuntimeError, match='guard-band verification failed'): + calibration._runCalibration(dev=calibration) + + assert not calibration.Results.value()['Data'][0]['passed'] + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.TotalSteps.value() == 9 + assert calibration.Step.value() == 9 + assert calibration.Progress.value() == 1.0 + assert calibration.RunTime.value() > 0.0 + + +def test_guard_verification_restores_each_coupled_lane_before_advancing(): + config = FakeConfig() + readout = CoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + assert calibration._dataLaneGroups == ((0,), (1,)) + results, passed = calibration._verifyCurrent([3], [2, 4], 0.0, 1) + + assert passed + assert all(result['passed'] for lanes in results.values() for result in lanes.values()) + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_full_calibration_masks_unaligned_partner_lane(usePatternTester): + config = FakeConfig() + readout = MaskedCoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert calibration._dataLaneGroups == ((0, 1),) + assert results['Data'][0]['eye']['selected'] == 2 + assert results['Data'][1]['eye']['selected'] == 5 + assert results['Final']['passed'] + if usePatternTester: + assert [measurement['delays'] for measurement in + readout.PatternTester.measurements] == [(2, 5)] + assert readout.PatternTester.measurements[0]['dataMask'] == 0x3FFF + assert readout.snapshotDelays == ( + [(tap, tap) for tap in range(8)] + + [(2, 5)]) + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + finally: + calibration._runEn = False + root.stop() + + +def test_final_qualification_rejects_split_lane_phase_mismatch(): + config = FakeConfig() + readout = PhaseMismatchedCoupledFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(3) + calibration.GuardBand.set(1) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + with pytest.raises(RuntimeError, match='Final full-channel pattern qualification failed'): + calibration._runCalibration(dev=calibration) + + results = calibration.Results.value() + assert all('eye' in results['Data'][lane] for lane in range(2)) + assert not results['Final']['passed'] + assert not results['Final']['captures'][0]['channels'][0]['passed'] + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 0 + assert readout.DataDelay[1].value() == 0 + with pytest.raises(RuntimeError, match='No complete full-calibration result'): + calibration.applyResults() + finally: + calibration._runEn = False + root.stop() + + +@pytest.mark.parametrize('usePatternTester', [False, True]) +def test_final_qualification_retries_alternate_fco_eye_combinations(usePatternTester): + config = FakeConfig() + readout = MultiWindowFcoFakeReadout() + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(2) + calibration.GuardBand.set(0) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration.UsePatternTester.set(usePatternTester) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + assert len(results['Fco'][0]['eyes']) == 2 + assert len(results['Fco'][1]['eyes']) == 2 + assert results['Final']['passed'] + assert [attempt['fcoDelays'] for attempt in results['Final']['attempts']] == [ + [2, 5], + [2, 1], + ] + assert not results['Final']['attempts'][0]['passed'] + assert results['Final']['attempts'][1]['passed'] + assert results['Fco'][0]['eye']['selected'] == 2 + assert results['Fco'][1]['eye']['selected'] == 1 + assert readout.FcoDelay[0].value() == 2 + assert readout.FcoDelay[1].value() == 1 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 5 + # One reset precedes the data-eye scan and one immediately precedes + # each of the two final FCO-combination attempts. + assert config.digitalResetValues == [3, 0, 3, 0, 3, 0] + finally: + calibration._runEn = False + root.stop() + + +def test_pn23_failure_retries_alternate_fco_eye_combination_in_checkerboard_mode(): + config = FakeConfig(pn23=True) + readout = Pn23MultiWindowFcoFakeReadout(config) + calibration = AdcDdrCalibration( + name='Calibration', + config=config, + readout=readout, + dataLaneToChannel=(0, 0), + dataLaneMasks=(0x003F, 0x3FC0), + configUpdate=config.update) + root = pr.Root(name='Root', pollEn=False) + root.add(calibration) + root.start() + try: + calibration.DelayStop.set(7) + calibration.MinimumEyeWidth.set(2) + calibration.GuardBand.set(0) + calibration.SampleCount.set(1) + calibration.SettleTime.set(0.0) + calibration._runEn = True + + results = calibration._runCalibration(dev=calibration) + + attempts = results['Final']['attempts'] + assert [attempt['fcoDelays'] for attempt in attempts] == [[2, 5], [2, 1]] + assert all( + attempt['qualification']['checkerboardPassed'] + for attempt in attempts) + assert not attempts[0]['qualification']['pn23']['passed'] + assert attempts[1]['qualification']['pn23']['passed'] + assert attempts[1]['passed'] + assert results['Fco'][1]['eye']['selected'] == 1 + finally: + calibration._runEn = False + root.stop() + + +def test_cancellation_is_graceful_and_restores_hardware(calibration_fixture): + calibration, config, readout = calibration_fixture + originalCheck = calibration._checkRun + checks = 0 + + def stop_during_fco_scan(): + nonlocal checks + checks += 1 + if checks == 3: + calibration._runEn = False + originalCheck() + + calibration._checkRun = stop_during_fco_scan + calibration._process() + + assert calibration.Message.value() == 'Calibration stopped' + assert calibration.Outcome.value() == calibration.OUTCOME_STOPPED_C + assert calibration.Step.value() == 2 + assert calibration.Progress.value() < 1.0 + assert config.OutputTestMode.value() == 0 + assert readout.FcoDelay[0].value() == 3 + assert readout.DataDelay[0].value() == 2 + assert readout.DataDelay[1].value() == 4 + assert calibration.RunTime.value() > 0.0 diff --git a/tests/devices/analog_devices/test_AdcDdrCore.py b/tests/devices/analog_devices/test_AdcDdrCore.py new file mode 100644 index 0000000000..0d2cf3b80e --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrCore.py @@ -0,0 +1,374 @@ +############################################################################## +## 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: Integrate two data lanes/channels and one 14-bit FCO lane with the +# optional pattern engine both enabled and disabled. +# - Stimulus: Access the relocatable block at a nonzero absolute AXI address, +# verify hardware-owned startup waits for delay readiness and loads every +# configured delay, exercise manual and readiness-loss reset/reload paths and +# the required DDR bitslip quiet interval, acquire lock, stream samples, load +# every delay class, and snapshot sample history. +# - Checks: AXI status follows alignment, streams preserve both channels and +# sidebands, delay requests are width-limited and retained across readiness +# loss, an in-flight snapshot aborts with SLVERR, snapshot writes reject reset +# state and otherwise block until publication, and AXI transactions execute +# coherently in the capture domain; the pattern window is capability-gated +# and reports its shared-phase result through AXI-Lite. +# - Timing: AXI-Lite is crossed as a complete bus into the capture clock domain; +# one wide FIFO crosses coherent channel samples to the stream clock. + +import os + +import cocotb +import pytest +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp + +from tests.axi.utils import axil_read_u32 as _axil_read_u32 +from tests.axi.utils import axil_write_u32 as _axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test + + +AXIL_BASE_ADDR = 0xC100_0000 + + +def negate14(value): + return (-value) & 0x3FFF + + +async def axil_read_u32(axil, address): + return await _axil_read_u32(axil, AXIL_BASE_ADDR + address) + + +async def axil_write_u32(axil, address, value): + await _axil_write_u32(axil, AXIL_BASE_ADDR + address, value) + + +async def axil_poll(axil, address, predicate, limit=128): + for _ in range(limit): + value = await axil_read_u32(axil, address) + if predicate(value): + return value + assert False, f"AXI register 0x{address:03X} did not reach expected state" + + +async def wait_load(clock, signal, mask, limit=80): + """Propagation sampling: observe registered delay-load pulses after TPD.""" + for _ in range(limit): + await RisingEdge(clock) + await Timer(2, unit="ns") + if int(signal.value) & mask: + return + assert False, "delay-load pulse was not observed" + + +@cocotb.test() +async def core_integration_test(dut): + """Propagation sampling: check registered DUT outputs after their TPD updates.""" + dut.axilRst.value = 1 + dut.captureRst.value = 1 + dut.streamRst.value = 1 + dut.delayReady.value = 0 + dut.fcoWord.value = 0b11111110000000 + dut.fcoValid.value = 0 + dut.sampleValid.value = 0 + dut.sampleIn.value = 0 + cocotb.start_soon(Clock(dut.axilClk, 5, unit="ns").start()) + cocotb.start_soon(Clock(dut.captureClk, 8, unit="ns").start()) + cocotb.start_soon(Clock(dut.streamClk, 11, unit="ns").start()) + + for _ in range(5): + await RisingEdge(dut.axilClk) + dut.axilRst.value = 0 + dut.captureRst.value = 0 + dut.streamRst.value = 0 + await Timer(500, unit="ns") + axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "S_AXI"), dut.axilClk, dut.axilRst) + await RisingEdge(dut.axilClk) + assert int(dut.S_AXI_BVALID.value) == 0 + await Timer(200, unit="ns") + status = await axil_read_u32(axil, 0x01C) + assert status & 0x07 == 0 + assert await axil_read_u32(axil, 0x00C) & 0x01 == 0 + assert int(dut.phyReset.value) == 1 + response = await axil.write(AXIL_BASE_ADDR + 0x014, (0x01).to_bytes(4, 'little')) + assert response.resp == AxiResp.SLVERR + assert await axil_read_u32(axil, 0x024) == 0 + + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + + status = await axil_read_u32(axil, 0x01C) + assert status & 0x07 == 0x02 + + # A 7-series DDR ISERDES needs three quiet CLKDIV cycles after each + # one-cycle BITSLIP request before another request can be issued. + await FallingEdge(dut.captureClk) + dut.fcoWord.value = 0 + dut.fcoValid.value = 1 + slip_cycles = [] + for cycle in range(20): + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + if int(dut.bitSlip.value): + slip_cycles.append(cycle) + if len(slip_cycles) == 1: + # A transient pre-settle match must not end the quiet interval. + dut.fcoWord.value = 0b11111110000000 + if len(slip_cycles) == 3: + break + if len(slip_cycles) == 1 and cycle == slip_cycles[0]+1: + dut.fcoWord.value = 0 + assert len(slip_cycles) == 3 + assert all(b-a >= 4 for a, b in zip(slip_cycles, slip_cycles[1:])) + + await FallingEdge(dut.captureClk) + dut.fcoWord.value = 0b11111110000000 + status = await axil_poll(axil, 0x01C, lambda value: (value & 0x07) == 0x06) + assert status & 0x07 == 0x06 + assert await axil_read_u32(axil, 0x300) == 0b11111110000000 + + # Manual reset remains available, but release is also hardware sequenced + # and reapplies every retained delay without any additional software step. + await axil_write_u32(axil, 0x00C, 0x01) + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0) + assert int(dut.phyReset.value) == 1 + dut.delayReady.value = 0 + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await axil_write_u32(axil, 0x00C, 0) + for _ in range(5): + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 1 + assert int(dut.dataDelayLoad.value) == 0 + assert int(dut.fcoDelayLoad.value) == 0 + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0x04) + + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x2345_1234 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + for _ in range(40): + await FallingEdge(dut.streamClk) + await Timer(1, unit="ns") + if int(dut.streamValid.value) == 3: + assert int(dut.streamData.value) == (negate14(0x2345) << 16) | negate14(0x1234) + assert int(dut.streamKeep.value) == 0xF + assert int(dut.streamDest.value) == 0x0100 + assert int(dut.streamLast.value) == 0 + assert int(dut.streamUser.value) == 0 + break + else: + assert False, "sample did not cross to stream clock" + + # Preserve sample cadence during loss of alignment and mark each affected + # channel with ordinary AXI Stream tUser(0), without SSI framing. + dut.fcoWord.value = 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0) + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x2567_3456 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + for _ in range(40): + await FallingEdge(dut.streamClk) + await Timer(1, unit="ns") + if int(dut.streamValid.value) == 3: + assert int(dut.streamData.value) == (negate14(0x2567) << 16) | negate14(0x3456) + assert int(dut.streamLast.value) == 0 + assert int(dut.streamUser.value) == 0x0101 + break + else: + assert False, "unaligned sample did not cross to stream clock" + + dut.fcoWord.value = 0b11111110000000 + await axil_poll(axil, 0x01C, lambda value: (value & 0x04) == 0x04) + + # Observe each acknowledged load and feed the applied value back as the + # logical PHY's current-delay status. + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 1)) + await axil_write_u32(axil, 0x100, 0x12) + await load + assert int(dut.dataDelayValue.value) & 0xFFFF == 0x12 + + # The default five-bit core must never forward discarded AXI write bits to + # the shared nine-bit PHY command. + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 1)) + await axil_write_u32(axil, 0x100, 0x1FF) + await load + assert int(dut.dataDelayValue.value) & 0xFFFF == 0x1F + assert await axil_read_u32(axil, 0x100) == 0x1F + + load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 2)) + await axil_write_u32(axil, 0x104, 0x13) + await load + + load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 1)) + await axil_write_u32(axil, 0x200, 0x14) + await load + await axil_poll(axil, 0x104, lambda value: value == 0x13) + await axil_poll(axil, 0x200, lambda value: value == 0x14) + + # Loss of delay readiness is a hardware-owned reset request. Abort an + # outstanding snapshot, clear alignment, suppress samples, and retain the + # programmed delays for automatic reload when readiness returns. + snapshot = cocotb.start_soon( + axil.write(AXIL_BASE_ADDR + 0x014, (0x01).to_bytes(4, 'little'))) + await Timer(500, unit="ns") + assert not snapshot.done() + for index in range(3): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = ((0x0300 + index) << 16) | (0x0200 + index) + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + assert not snapshot.done() + + # Drop readiness with what would otherwise be the publishing fourth sample. + # Readiness loss takes priority, so the snapshot sequence must not advance. + await FallingEdge(dut.captureClk) + dut.delayReady.value = 0 + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + response = await snapshot + assert response.resp == AxiResp.SLVERR + assert int(dut.phyReset.value) == 1 + assert await axil_read_u32(axil, 0x01C) & 0x07 == 0 + assert await axil_read_u32(axil, 0x024) == 0 + assert await axil_read_u32(axil, 0x100) == 0x1F + assert await axil_read_u32(axil, 0x104) == 0x13 + assert await axil_read_u32(axil, 0x200) == 0x14 + + await FallingEdge(dut.captureClk) + dut.sampleIn.value = 0x0123_0234 + dut.sampleValid.value = 1 + for _ in range(4): + await RisingEdge(dut.captureClk) + dut.sampleValid.value = 0 + for _ in range(20): + await RisingEdge(dut.streamClk) + await Timer(1, unit="ns") + assert int(dut.streamValid.value) == 0 + + data_load = cocotb.start_soon(wait_load(dut.captureClk, dut.dataDelayLoad, 0x3)) + fco_load = cocotb.start_soon(wait_load(dut.captureClk, dut.fcoDelayLoad, 0x1)) + await FallingEdge(dut.captureClk) + dut.delayReady.value = 1 + await data_load + await fco_load + assert int(dut.dataDelayValue.value) == 0x0013_001F + assert int(dut.fcoDelayValue.value) == 0x0014 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + assert int(dut.phyReset.value) == 0 + await axil_poll(axil, 0x01C, lambda value: (value & 0x07) == 0x06) + + dut.sampleIn.value = 0x2000_1000 + snapshot = cocotb.start_soon(axil_write_u32(axil, 0x014, 0x01)) + await Timer(500, unit="ns") + assert not snapshot.done() + for index in range(4): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = ((0x2000 + index) << 16) | (0x1000 + index) + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + if index != 3: + assert not snapshot.done() + await snapshot + assert await axil_read_u32(axil, 0x024) == 1 + assert await axil_read_u32(axil, 0x600) == 0x1000 + assert await axil_read_u32(axil, 0x610) == 0x2000 + assert await axil_read_u32(axil, 0x000) == 0x00010000 + assert await axil_read_u32(axil, 0x004) == 0x0E020102 + assert await axil_read_u32(axil, 0x500) == 0 + response = await axil.read(AXIL_BASE_ADDR + 0x028, 4) + assert response.resp == AxiResp.DECERR + pattern_check = os.getenv('PATTERN_CHECK_G', 'true').lower() == 'true' + assert await axil_read_u32(axil, 0x008) == (0x00010E05 if pattern_check else 0x00000E05) + + if not pattern_check: + response = await axil.read(AXIL_BASE_ADDR + 0x800, 4) + assert response.resp == AxiResp.DECERR + # Terminal scenario: absent pattern hardware and DECERR complete this case. + return + + # Run one shared-phase alternating-pattern window through the integrated + # AXI-Lite register map. Both channels acquire B first and then A. + await axil_write_u32(axil, 0x808, 0x00000001) + await axil_write_u32(axil, 0x80C, 0x00000003) + await axil_write_u32(axil, 0x810, 0x00000001) + await axil_write_u32(axil, 0x814, 0x00003FFF) + await axil_write_u32(axil, 0x818, 0x00001555) + await axil_write_u32(axil, 0x81C, 0x00002AAA) + await axil_write_u32(axil, 0x820, 0x00000002) + await axil_write_u32(axil, 0x824, 0x00000020) + await axil_write_u32(axil, 0x800, 0x00000001) + await axil_poll(axil, 0x828, lambda value: value & 0x01) + for sample in (0x2AAA, 0x1555): + await FallingEdge(dut.captureClk) + dut.sampleIn.value = (sample << 16) | sample + dut.sampleValid.value = 1 + await RisingEdge(dut.captureClk) + await Timer(2, unit="ns") + dut.sampleValid.value = 0 + await axil_poll(axil, 0x82C, lambda value: value == 1) + assert await axil_read_u32(axil, 0x828) == 0x70 + assert await axil_read_u32(axil, 0x830) == 2 + assert await axil_read_u32(axil, 0x834) == 3 + assert await axil_read_u32(axil, 0x838) == 1 + assert await axil_read_u32(axil, 0x840) == 0 + assert await axil_read_u32(axil, 0x844) == 0 + assert await axil_read_u32(axil, 0x8C0) == 0 + + +@pytest.mark.parametrize('pattern_check', (True, False)) +def test_AdcDdrCore(pattern_check): + sources = [ + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPhy.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd", + "devices/AnalogDevices/adcDdr/rtl/AdcDdrCore.vhd", + "devices/AnalogDevices/adcDdr/wrappers/AdcDdrCoreWrapper.vhd", + ] + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.adcddrcorewrapper", + parameters={ + 'AXIL_BASE_ADDR_G': f'{AXIL_BASE_ADDR:032b}', + 'NEGATE_G': True, + 'PATTERN_CHECK_G': pattern_check, + }, + extra_vhdl_sources={"surf": sources}, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrModel.py b/tests/devices/analog_devices/test_AdcDdrModel.py new file mode 100644 index 0000000000..c764aa9c2f --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrModel.py @@ -0,0 +1,636 @@ +############################################################################## +## 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: Construct a non-default normalized map and all three device-specific +# calibration adapters, including the two-lane AD9681 topology. +# - Stimulus: Use Rogue's memory emulator to write control and every delay lane; +# construct calibration adapters with real device configuration models. +# - Checks: Verify register offsets, field widths/modes, memory readback, range +# enforcement, blocking-command snapshot transaction count, bulk raw-snapshot +# reads and grouped delay writes, staged-update callbacks, and physical-lane +# channel mappings. +# - Timing: Memory transactions are synchronous software operations; ADC mode, +# snapshot, and settling behavior is covered by the calibration mock test. + +import pytest + +pr = pytest.importorskip( + 'pyrogue', + reason='ADC DDR model tests require Rogue/PyRogue') +rogue = pytest.importorskip( + 'rogue', + reason='ADC DDR model tests require Rogue/PyRogue') +rim = pytest.importorskip( + 'rogue.interfaces.memory', + reason='ADC DDR model tests require Rogue/PyRogue') + +from surf.devices.analog_devices import ( # noqa: E402 + Ad9249Config, + Ad9249ConfigGroup, + Ad9249ChipConfig, + Ad9249Readout, + Ad9249ReadoutCalibration, + Ad9249ReadoutBank, + Ad9249ReadoutBankCalibration, + Ad9252Config, + Ad9252Readout, + Ad9252ReadoutCalibration, + Ad9681Config, + Ad9681Readout, + Ad9681ReadoutCalibration, + AdcDdr, + adcDdrDelayBits, +) + + +class CountingMemory(rim.Slave): + def __init__(self, size=0x10000): + super().__init__(4, size) + self._memory = bytearray(size) + self.transactions = [] + + def _doTransaction(self, transaction): + address = transaction.address() + size = transaction.size() + transactionType = transaction.type() + self.transactions.append((transactionType, address, size)) + with transaction.lock(): + if transactionType in (rim.Write, rim.Post): + data = bytearray(size) + transaction.getData(data, 0) + self._memory[address:address+size] = data + else: + transaction.setData(self._memory[address:address+size], 0) + transaction.done() + + +@pytest.fixture +def memory_backed_readout(): + memory = rim.Emulate(4, 0x10000) + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name = 'Readout', + memBase = memory, + dataLanes = 3, + fcoLanes = 2, + channels = 2, + sampleBits = 14, + serializationFactor = 8, + delayBits = 9) + root.add(readout) + root.start() + try: + yield readout + finally: + root.stop() + + +def test_normalized_map_geometry_and_memory_readback(memory_backed_readout): + readout = memory_backed_readout + + assert readout.Version.offset == 0x000 + for name, offset, bitOffset, bitSize in ( + ('DataLanes', 0x004, 0, 8), + ('FcoLanes', 0x004, 8, 8), + ('Channels', 0x004, 16, 8), + ('SampleBits', 0x004, 24, 8), + ('DelayBits', 0x008, 0, 8), + ('SerializationFactor', 0x008, 8, 8), + ('PatternCheck', 0x008, 16, 1)): + variable = readout.nodes[name] + assert variable.offset == offset + assert variable.bitOffset == [bitOffset] + assert variable.bitSize == [bitSize] + assert variable.mode == 'RO' + for name in ('DataLanes', 'FcoLanes', 'Channels', 'SampleBits', 'SerializationFactor'): + assert readout.nodes[name].hidden + assert not readout.DelayBits.hidden + assert 'Capabilities0' not in readout.nodes + assert 'Capabilities1' not in readout.nodes + assert readout.CaptureReset.offset == 0x00C + assert readout.Relock.offset == 0x010 + assert readout.Snapshot.offset == 0x014 + assert readout.ClearCounters.offset == 0x018 + assert readout.DelayReady.offset == 0x01C + assert readout.LockedMask.offset == 0x020 + assert readout.SnapshotSequence.offset == 0x024 + assert readout.Relock.bitOffset == [0] + assert readout.Snapshot.bitOffset == [0] + assert readout.ClearCounters.bitOffset == [0] + pattern = readout.PatternTester + assert pattern.offset == 0x800 + assert pattern.Start.offset == 0x000 + assert pattern.Abort.offset == 0x004 + assert pattern.Start.bitOffset == [0] + assert pattern.Abort.bitOffset == [0] + assert pattern.Alternating.address == 0x808 + assert pattern.Pn23.address == 0x808 + assert pattern.Pn23.bitOffset == [1] + assert pattern.ReferenceChannel.bitOffset == [8] + assert pattern.ChannelMask.bitSize == [2] + assert pattern.FcoMask.bitSize == [2] + assert pattern.DataMask.bitSize == [14] + assert pattern.Samples.address == 0x820 + assert pattern.Busy.address == 0x828 + assert pattern.AllFcoPass.bitOffset == [6] + assert pattern.Sequence.address == 0x82C + assert pattern.ChannelPassed.address == 0x834 + assert pattern.WordErrorCount[1].address == 0x844 + assert pattern.BitErrorMask[1].address == 0x884 + assert pattern.FcoErrorCount[1].address == 0x8C4 + assert not any(name.startswith('Pattern') and + name not in ('PatternCheck', 'PatternTester') + for name in readout.nodes) + assert readout.LockedMask.bitSize == [2] + assert 'ClockPresent' not in readout.nodes + assert 'OverflowMask' not in readout.nodes + assert readout.DataDelayBulk.offset == 0x100 + assert readout.DataDelayBulk.numValues == 3 + assert readout.DataDelayBulk.valueBits == 9 + assert readout.DataDelayBulk.valueStride == 32 + assert readout.DataDelayBulk.hidden + assert readout.DataDelay[2].mode == 'RW' + assert readout.DataDelay[2].minimum == 0 + assert readout.DataDelay[2].maximum == 511 + assert readout.FcoDelay[1].offset == 0x204 + assert readout.FcoWord[1].offset == 0x304 + assert readout.FcoWord[1].bitSize == [8] + assert readout.LostLockCount[1].offset == 0x344 + assert readout.OverflowCount.offset == 0x500 + assert readout.DebugSampleRaw.offset == 0x600 + assert readout.DebugSampleRaw.numValues == 8 + assert readout.DebugSampleRaw.valueBits == 14 + assert readout.DebugSampleRaw.valueStride == 32 + assert readout.DebugSampleRaw.varBytes == 32 + assert readout.nodes['DebugVoltage[1]'].units == 'V' + assert readout.DebugVoltageRange.get() == 2.0 + assert readout.DebugVoltageFormat.get() == 1 + assert isinstance(readout.nodes['DebugVoltage[0]'].get(read=True), float) + nodeNames = list(readout.nodes) + assert max(nodeNames.index(f'DebugSample[{channel}]') for channel in range(2)) < ( + min(nodeNames.index(f'DebugVoltage[{channel}]') for channel in range(2))) + assert readout.Version.mode == 'RO' + assert isinstance(readout.DataDelay[0], pr.LinkVariable) + + readout.CaptureReset.set(True) + assert readout.CaptureReset.get(read=True) is True + pattern.Alternating.set(True) + pattern.ChannelMask.set(3) + pattern.DataMask.set(0x3FFF) + assert pattern.Alternating.get(read=True) is True + assert pattern.ChannelMask.get(read=True) == 3 + assert pattern.DataMask.get(read=True) == 0x3FFF + for lane, value in enumerate((17, 255, 511)): + readout.DataDelay[lane].set(value) + assert readout.DataDelay[lane].get(read=True) == value + for lane, value in enumerate((31, 300)): + readout.FcoDelay[lane].set(value) + assert readout.FcoDelay[lane].get(read=True) == value + + with pytest.raises(rogue.GeneralError, match='Value range error'): + readout.DataDelay[0].set(512) + + +def test_capability_check_includes_pattern_tester(): + memory = CountingMemory() + geometry = 3 | (2 << 8) | (2 << 16) | (14 << 24) + capabilities = 9 | (8 << 8) | (1 << 16) + memory._memory[0x004:0x008] = geometry.to_bytes(4, 'little') + memory._memory[0x008:0x00C] = capabilities.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name = 'Readout', + memBase = memory, + dataLanes = 3, + fcoLanes = 2, + channels = 2, + sampleBits = 14, + serializationFactor = 8, + delayBits = 9, + patternCheck = True) + root.add(readout) + root.start() + try: + readout.checkGeometry() + readout._patternCheck = False + with pytest.raises( + RuntimeError, + match='PatternCheck: model=False, hardware=1'): + readout.checkGeometry() + finally: + root.stop() + + +def test_debug_sample_array_uses_one_bulk_transaction(): + memory = CountingMemory() + expected = [0x100+index for index in range(32)] + for index, sample in enumerate(expected): + offset = 0x600 + 4*index + memory._memory[offset:offset+4] = sample.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + samples = readout.DebugSampleRaw.get(read=True) + + assert samples.shape == (32,) + assert samples.tolist() == expected + assert readout._getDebugSamples(read=False) == [ + expected[4*channel:4*(channel+1)] + for channel in range(8) + ] + assert readout.nodes['DebugSample[1]'].get(read=False) == ( + '0x0104_0105_0106_0107') + assert readout.nodes['DebugVoltage[1]'].get(read=False) == 0.03173828125 + assert memory.transactions == [(rim.Read, 0x600, 128)] + assert readout.DebugSampleRaw._block.size == 128 + finally: + root.stop() + + +@pytest.mark.parametrize( + ('sample', 'inputRange', 'offsetBinary', 'expected'), + ( + (0x0000, 2.0, False, 0.0), + (0x1FFF, 2.0, False, 0.9998779296875), + (0x2000, 2.0, False, -1.0), + (0x0000, 2.0, True, -1.0), + (0x3FFF, 2.0, True, 0.9998779296875), + (0x0800, 1.0, False, 0.125), + ), +) +def test_debug_voltage_conversion_through_public_model( + sample, inputRange, offsetBinary, expected): + memory = CountingMemory() + memory._memory[0x600:0x604] = sample.to_bytes(4, 'little') + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=1, + sampleBits=14) + root.add(readout) + root.start() + try: + readout.DebugVoltageRange.set(inputRange) + readout.DebugVoltageFormat.set(0 if offsetBinary else 1) + + assert readout.nodes['DebugVoltage[0]'].get(read=True) == expected + finally: + root.stop() + + +def test_snapshot_uses_one_command_and_one_bulk_read(): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + snapshots = readout.Snapshot() + + assert snapshots == ['0x0000_0000_0000_0000']*8 + assert memory.transactions == [ + (rim.Write, 0x014, 4), + (rim.Read, 0x600, 128), + ] + finally: + root.stop() + + +def test_data_delay_bulk_view_preserves_scalar_access_and_groups_transactions(): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + readout = AdcDdr( + name='Readout', + memBase=memory, + dataLanes=16, + channels=8, + sampleBits=14) + root.add(readout) + root.start() + try: + memory.transactions.clear() + readout.DataDelay[3].set(7, write=True) + + assert memory.transactions == [ + (rim.Write, 0x10C, 4), + (rim.Verify, 0x10C, 4), + ] + assert readout.DataDelayBulk.get(read=False, index=3) == 7 + + memory.transactions.clear() + readout._setDataDelays({lane: 11 for lane in range(8)}) + + assert memory.transactions == [ + (rim.Write, 0x100, 32), + (rim.Verify, 0x100, 32), + ] + assert readout._getDataDelays(read=False) == [11]*8 + [0]*8 + assert [readout.DataDelay[lane].get(read=False) for lane in range(16)] == ( + [11]*8 + [0]*8) + finally: + root.stop() + + +def test_device_specific_calibration_adapters(): + ad9249Config = Ad9249ConfigGroup(name='Ad9249Config') + ad9249Readout = Ad9249ReadoutBank(name='Ad9249Readout') + ad9249 = Ad9249ReadoutBankCalibration( + name='Ad9249Calibration', + config=ad9249Config, + readout=ad9249Readout) + assert ad9249._dataLaneToChannel == tuple(range(8)) + assert ad9249._configUpdate is None + assert ad9249Config.DigitalReset.name == 'DigitalReset' + assert ad9249Config.ResetPNLong.name == 'ResetPNLong' + + ad9249Full = Ad9249ReadoutCalibration( + name='Ad9249FullCalibration', + config=Ad9249Config(name='Ad9249FullConfig'), + readout=Ad9249Readout(name='Ad9249FullReadout')) + assert len(ad9249Full.Bank) == 2 + assert all(isinstance(process, Ad9249ReadoutBankCalibration) + for process in ad9249Full.Bank.values()) + + ad9252Config = Ad9252Config(name='Ad9252Config') + ad9252Readout = Ad9252Readout(name='Ad9252Readout') + ad9252 = Ad9252ReadoutCalibration( + name='Ad9252Calibration', + config=ad9252Config, + readout=ad9252Readout) + assert ad9252._dataLaneToChannel == tuple(range(8)) + assert ad9252._configUpdate == ad9252Config.DeviceUpdate + assert ad9252Config.DigitalReset.name == 'DigitalReset' + assert ad9252Config.ResetPNLong.name == 'ResetPNLong' + + ad9681Config = Ad9681Config(name='Ad9681Config') + ad9681Readout = Ad9681Readout(name='Ad9681Readout') + ad9681 = Ad9681ReadoutCalibration( + name='Ad9681Calibration', + config=ad9681Config, + readout=ad9681Readout) + assert ad9681._dataLaneToChannel == tuple(range(8))+tuple(range(8)) + assert ad9681._configUpdate is None + assert ad9681Config.DigitalReset.name == 'DigitalReset' + assert ad9681Config.ResetPNLong.name == 'ResetPNLong' + assert ad9681.DelayStart.units == 'tap' + assert ad9681.DelayStop.units == 'tap' + assert ad9681.MinimumEyeWidth.units == 'tap' + assert ad9681.GuardBand.units == 'tap' + assert ad9681.UsePatternTester.value() is True + assert ad9681.UsePatternTester.mode == 'RW' + assert ad9681.PatternTesterSamples.value() == 4096 + assert ad9681.PatternTesterSamples.mode == 'RW' + assert ad9681.Outcome.value() == ad9681.OUTCOME_IDLE_C + assert ad9681.Outcome.enum == { + 0: 'IDLE', + 1: 'RUNNING', + 2: 'PASSED', + 3: 'FAILED', + 4: 'STOPPED', + } + assert ad9681.Outcome.mode == 'RO' + assert ad9681.RunTime.value() == 0.0 + assert ad9681.RunTime.mode == 'RO' + assert ad9681.RunTime.units == 's' + + ad9681WithoutPatternTester = Ad9681ReadoutCalibration( + name='Ad9681CalibrationWithoutPatternTester', + config=Ad9681Config(name='Ad9681ConfigWithoutPatternTester'), + readout=Ad9681Readout( + name='Ad9681ReadoutWithoutPatternTester', + patternCheck=False)) + assert ad9681WithoutPatternTester.UsePatternTester.value() is False + + +def test_ad9252_config_register_fields(): + config = Ad9252Config(name='Config') + + assert config.UserTestMode.offset == 0x34 + assert config.UserTestMode.bitOffset == [6] + assert config.UserTestMode.bitSize == [2] + assert config.DcoFcoDrive2x.description == 'Set DCO and FCO output drive strength.' + + +def test_ad9249_chip_config_constructs_both_banks(): + config = Ad9249ChipConfig() + + assert config.BankConfig[0].offset == 0x0000 + assert config.BankConfig[1].offset == 0x0200 + + +def test_ad9249_resolution_sample_rate_override_register_fields(): + config = Ad9249ConfigGroup(name='Config') + + assert config.DeviceUpdate.offset == 0x3FC + assert config.ResolutionSampleRateOverride.offset == 0x400 + assert config.ResolutionSampleRateOverride.bitOffset == [6] + assert config.ResolutionSampleRateOverride.bitSize == [1] + assert config.Resolution.offset == 0x400 + assert config.Resolution.bitOffset == [4] + assert config.Resolution.bitSize == [2] + assert config.SampleRate.offset == 0x400 + assert config.SampleRate.bitOffset == [0] + assert config.SampleRate.bitSize == [3] + + +def test_ad9681_resolution_sample_rate_override_register_is_disabled(): + config = Ad9681Config(name='Config') + + assert config.DeviceUpdate.offset == 0x3FC + assert 'ResolutionSampleRateOverride' not in config.nodes + assert 'Resolution' not in config.nodes + assert 'SampleRate' not in config.nodes + + +@pytest.mark.parametrize('configType', (Ad9249ConfigGroup, Ad9252Config, Ad9681Config)) +def test_config_write_blocks_forwards_arguments(monkeypatch, configType): + calls = [] + + def writeBlocks(self, **kwargs): + calls.append(kwargs) + + monkeypatch.setattr(pr.Device, 'writeBlocks', writeBlocks) + config = configType(name='Config') + updates = [] + config.DeviceUpdate = lambda: updates.append(True) + variable = object() + + config.writeBlocks( + force=True, + recurse=False, + variable=variable, + checkEach=True, + index=3, + testOption='forwarded') + + assert calls == [{ + 'force': True, + 'recurse': False, + 'variable': variable, + 'checkEach': True, + 'index': 3, + 'testOption': 'forwarded', + }] + assert updates == [True] + + +@pytest.mark.parametrize('configType', (Ad9249ConfigGroup, Ad9252Config, Ad9681Config)) +def test_device_update_writes_transfer_strobe(configType): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + config = configType(name='Config', memBase=memory) + root.add(config) + root.start() + try: + memory.transactions.clear() + config.DeviceUpdate() + + assert int.from_bytes(memory._memory[0x3FC:0x400], 'little') == 1 + assert memory.transactions == [(rim.Write, 0x3FC, 4)] + finally: + root.stop() + + +@pytest.mark.parametrize( + ('configType', 'powerModeName'), + ( + (Ad9249ConfigGroup, 'InternalPdwnMode'), + (Ad9252Config, 'PowerDownMode'), + (Ad9681Config, 'InternalPdwnMode'), + )) +def test_digital_reset_uses_common_power_mode_enums(configType, powerModeName): + memory = CountingMemory() + root = pr.Root(name='Root', pollEn=False) + config = configType(name='Config', memBase=memory) + root.add(config) + root.start() + try: + powerMode = getattr(config, powerModeName) + + assert powerMode.enum[3] == 'Digital Reset' + assert powerMode.enum[0] == 'Chip Run' + + config.DigitalReset() + + assert powerMode.getDisp(read=False) == 'Chip Run' + finally: + root.stop() + + +def test_device_specific_readout_geometry(): + ad9249 = Ad9249Readout(name='Ad9249') + assert len(ad9249.Bank) == 2 + for bank, readout in ad9249.Bank.items(): + assert readout.offset == 0x1000*bank + assert ( + readout._dataLanes, + readout._fcoLanes, + readout._channels, + readout._sampleBits, + readout._serializationFactor, + readout._delayBits, + ) == (8, 1, 8, 14, 14, 9) + + ad9252 = Ad9252Readout(name='Ad9252') + assert ( + ad9252._dataLanes, + ad9252._fcoLanes, + ad9252._channels, + ad9252._sampleBits, + ad9252._serializationFactor, + ad9252._delayBits, + ) == (8, 1, 8, 14, 14, 9) + + ad9681 = Ad9681Readout(name='Ad9681') + assert ( + ad9681._dataLanes, + ad9681._fcoLanes, + ad9681._channels, + ad9681._sampleBits, + ad9681._serializationFactor, + ad9681._delayBits, + ) == (16, 2, 8, 14, 8, 9) + assert ad9681._patternCheck is True + + assert Ad9252Readout( + name='Ad9252WithoutPatternTester', + patternCheck=False)._patternCheck is False + assert Ad9681Readout( + name='Ad9681WithoutPatternTester', + patternCheck=False)._patternCheck is False + + ad9249WithoutPatternTester = Ad9249Readout( + name='Ad9249WithoutPatternTester', + patternCheck=False) + assert all( + readout._patternCheck is False + for readout in ad9249WithoutPatternTester.Bank.values()) + + +@pytest.mark.parametrize( + ('deviceFamily', 'delayBits'), + ( + ('7SERIES', 5), + ('ULTRASCALE', 9), + ('ULTRASCALE_PLUS', 9), + ), +) +def test_device_family_selects_native_delay_width(deviceFamily, delayBits): + assert adcDdrDelayBits(deviceFamily) == delayBits + assert Ad9249ReadoutBank( + name='Ad9249', deviceFamily=deviceFamily)._delayBits == delayBits + assert Ad9252Readout( + name='Ad9252', deviceFamily=deviceFamily)._delayBits == delayBits + assert Ad9681Readout( + name='Ad9681', deviceFamily=deviceFamily)._delayBits == delayBits + + +@pytest.mark.parametrize( + ('constructor', 'kwargs', 'message'), + ( + (AdcDdr, {'dataLanes': 65}, 'dataLanes'), + (AdcDdr, {'channels': 0}, 'channels'), + (AdcDdr, {'sampleBits': 17}, 'sampleBits'), + (AdcDdr, {'serializationFactor': 17}, 'serializationFactor'), + (Ad9249ReadoutBank, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + (Ad9252Readout, {'channels': 9}, 'channels'), + (Ad9252Readout, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + (Ad9681Readout, {'deviceFamily': 'SPARTAN6'}, 'deviceFamily'), + ), +) +def test_readout_geometry_rejects_values_outside_rtl_limits( + constructor, kwargs, message): + with pytest.raises(ValueError, match=message): + constructor(name='Readout', **kwargs) + + +def test_ad9681_adapter_requires_device_specific_readout(): + config = Ad9681Config(name='Config') + wrong = AdcDdr(name='Wrong', dataLanes=8, fcoLanes=1, channels=8) + + with pytest.raises(TypeError, match='must be an Ad9681Readout'): + Ad9681ReadoutCalibration( + name='Calibration', + config=config, + readout=wrong) diff --git a/tests/devices/analog_devices/test_AdcDdrPatternPkg.py b/tests/devices/analog_devices/test_AdcDdrPatternPkg.py new file mode 100644 index 0000000000..a4cc0f0175 --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrPatternPkg.py @@ -0,0 +1,87 @@ +############################################################################## +## 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: Generate 8-, 14-, and 16-bit words used by supported ADC modes. +# - Stimulus: Feed all-ones and asymmetric nonzero PN9/PN23 states through many +# consecutive word advances. +# - Checks: Every word and next state matches independent polynomial models; +# the PN9 one-bit recurrence also proves its full 511-state nonzero period. +# - Timing: Package functions are combinational and checked after settling. + +import os + +import cocotb +import pytest +from cocotb.triggers import Timer + +from tests.common.regression_utils import hdl_parameters_from, parameter_case, run_surf_vhdl_test + + +def pn_next(state, order, tap): + return ((state << 1) & ((1 << order) - 1)) | (((state >> (order - 1)) ^ (state >> (tap - 1))) & 1) + + +def pn_word(state, order, tap, width): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> (order - 1)) & 1) + state = pn_next(state, order, tap) + return word, state + + +@cocotb.test() +async def pn_pattern_test(dut): + width = int(os.environ["WORD_WIDTH_G"]) + states = [(0x1FF, 0x7FFFFF), (0x12D, 0x654321)] + + for pn9, pn23 in states: + for _ in range(64): + dut.pn9State.value = pn9 + dut.pn23State.value = pn23 + await Timer(1, unit="ns") + expected9, next9 = pn_word(pn9, 9, 5, width) + expected23, next23 = pn_word(pn23, 23, 18, width) + assert int(dut.pn9Word.value) == expected9 + assert int(dut.pn23Word.value) == expected23 + assert int(dut.pn9Next.value) == next9 + assert int(dut.pn23Next.value) == next23 + pn9, pn23 = next9, next23 + + state = 0x1FF + visited = set() + for _ in range(511): + assert state not in visited and state != 0 + visited.add(state) + state = pn_next(state, 9, 5) + assert state == 0x1FF + + +PARAMETER_SWEEP = [ + parameter_case("word_8", WORD_WIDTH_G="8"), + parameter_case("word_14", WORD_WIDTH_G="14"), + parameter_case("word_16", WORD_WIDTH_G="16"), +] + + +@pytest.mark.parametrize("parameters", PARAMETER_SWEEP) +def test_AdcDdrPatternPkg(parameters): + run_surf_vhdl_test( + test_file=__file__, + toplevel="surf.adcddrpatternpkgtb", + parameters=hdl_parameters_from(parameters), + extra_env=parameters, + extra_vhdl_sources={ + "surf": [ + "devices/AnalogDevices/adcDdr/sim/AdcDdrPatternPkg.vhd", + "devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternPkgTb.vhd", + ], + }, + ) diff --git a/tests/devices/analog_devices/test_AdcDdrPatternTester.py b/tests/devices/analog_devices/test_AdcDdrPatternTester.py new file mode 100644 index 0000000000..406d759a2e --- /dev/null +++ b/tests/devices/analog_devices/test_AdcDdrPatternTester.py @@ -0,0 +1,316 @@ +############################################################################## +## 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: Exercise two logical channels and one FCO lane in constant, +# alternating, and arbitrary-phase PN23 modes, followed by every terminal +# error class. +# - Stimulus: Configure and start finite windows through the module's AXI-Lite +# interface, provide valid sample groups with shared A/B phase and PN23 +# recurrence, inject recurrence, channel-coherence, and FCO errors, suppress +# FCO validity, then withhold valid samples or abort. +# - Checks: Verify configuration readback, completion sequencing, common sample +# counts, phase acquisition, channel/FCO pass masks, error counters, +# accumulated bit-error masks, configuration errors, timeout, and abort. +# - Timing: AXI-Lite and the primitive-free checker share the capture clock; +# gaps in sampleValid exercise the programmable no-valid timeout. + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotbext.axi import AxiLiteBus, AxiLiteMaster + +from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.common.regression_utils import run_surf_vhdl_test + + +FRAME_PATTERN = 0b11111110000000 + +START_ADDR = 0x00 +ABORT_ADDR = 0x04 +CONFIG_ADDR = 0x08 +CHANNEL_MASK_ADDR = 0x0C +FCO_MASK_ADDR = 0x10 +DATA_MASK_ADDR = 0x14 +PATTERN_A_ADDR = 0x18 +PATTERN_B_ADDR = 0x1C +SAMPLES_ADDR = 0x20 +TIMEOUT_ADDR = 0x24 +STATUS_ADDR = 0x28 +SEQUENCE_ADDR = 0x2C +CHECKED_ADDR = 0x30 +CHANNEL_PASS_ADDR = 0x34 +FCO_PASS_ADDR = 0x38 +WORD_ERROR_ADDR = 0x40 +BIT_ERROR_ADDR = 0x80 +FCO_ERROR_ADDR = 0xC0 + + +async def axil_poll(axil, address, predicate, limit=128): + for _ in range(limit): + value = await axil_read_u32(axil, address) + if predicate(value): + return value + assert False, f'AXI register 0x{address:03X} did not reach expected state' + + +async def send_sample(dut, channel0, channel1): + """Propagation sampling: hold each sample through the registered TPD update.""" + await FallingEdge(dut.clk) + dut.sampleIn.value = (channel1 << 16) | channel0 + dut.sampleValid.value = 1 + await RisingEdge(dut.clk) + await Timer(2, unit='ns') + dut.sampleValid.value = 0 + + +def pn23_words(state=0x654321, count=8, width=14): + mask = (1 << 23)-1 + words = [] + for _ in range(count): + word = 0 + for _ in range(width): + word = (word << 1) | ((state >> 22) & 1) + state = ( + ((state << 1) & mask) | + (((state >> 22) ^ (state >> 17)) & 1)) + words.append(word) + return words + + +async def configure( + axil, + *, + alternating, + samples, + timeout, + pn23=False, + dataMask=0x3FFF, + patternA=0x1555, + patternB=0x2AAA): + await axil_write_u32(axil, CONFIG_ADDR, int(alternating) | (int(pn23) << 1)) + await axil_write_u32(axil, CHANNEL_MASK_ADDR, 3) + await axil_write_u32(axil, FCO_MASK_ADDR, 1) + await axil_write_u32(axil, DATA_MASK_ADDR, dataMask) + await axil_write_u32(axil, PATTERN_A_ADDR, patternA) + await axil_write_u32(axil, PATTERN_B_ADDR, patternB) + await axil_write_u32(axil, SAMPLES_ADDR, samples) + await axil_write_u32(axil, TIMEOUT_ADDR, timeout) + + +async def start_window(axil): + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + await axil_poll(axil, STATUS_ADDR, lambda value: value & 1) + return sequence + + +async def wait_done(axil, sequence): + await axil_poll(axil, SEQUENCE_ADDR, lambda value: value != sequence) + return await axil_read_u32(axil, STATUS_ADDR) + + +@cocotb.test() +async def pattern_tester_test(dut): + dut.rst.value = 1 + dut.sampleValid.value = 0 + dut.sampleIn.value = 0 + dut.fcoValid.value = 1 + dut.fcoWord.value = FRAME_PATTERN + cocotb.start_soon(Clock(dut.clk, 8, unit='ns').start()) + + for _ in range(5): + await RisingEdge(dut.clk) + dut.rst.value = 0 + await RisingEdge(dut.clk) + axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, 'S_AXI'), dut.clk, dut.rst) + + # Constant mode ignores unmasked transport bits and checks both channels. + await configure(axil, alternating=False, samples=3, timeout=100) + await axil_write_u32(axil, DATA_MASK_ADDR, 0x3F) + await axil_write_u32(axil, PATTERN_A_ADDR, 0x15) + assert await axil_read_u32(axil, DATA_MASK_ADDR) == 0x3F + sequence = await start_window(axil) + for _ in range(3): + await send_sample(dut, 0x3015, 0x2015) + status = await wait_done(axil, sequence) + assert status == 0x70 + assert await axil_read_u32(axil, CHECKED_ADDR) == 3 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) == 0 + + # A reference word matching neither A nor B fails every enabled channel + # without acquiring phase. A later match acquires one group-wide phase but + # does not erase the first sample's errors. + await configure(axil, alternating=True, samples=2, timeout=100) + sequence = await start_window(axil) + await send_sample(dut, 0, 0) + assert await axil_read_u32(axil, STATUS_ADDR) & 0x10 == 0 + await send_sample(dut, 0x2AAA, 0x2AAA) + status = await wait_done(axil, sequence) + assert status & 0x10 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) == 0x3FFF + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 0x3FFF + + # Channel 1 accumulates one exact failing bit and the FCO lane records one + # mismatch without disturbing the shared alternating phase. + await configure(axil, alternating=True, samples=3, timeout=100) + sequence = await start_window(axil) + await send_sample(dut, 0x2AAA, 0x2AAA) + dut.fcoWord.value = FRAME_PATTERN ^ 1 + await send_sample(dut, 0x1555, 0x1551) + dut.fcoWord.value = FRAME_PATTERN + await send_sample(dut, 0x2AAA, 0x2AAA) + status = await wait_done(axil, sequence) + assert status == 0x10 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 1 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 4 + assert await axil_read_u32(axil, FCO_ERROR_ADDR) == 1 + + # PN23 mode acquires an arbitrary nonzero 23-bit prefix, applies PatternA + # as an input XOR mask, and then checks recurrence plus channel coherence. + words = pn23_words() + xorMask = 0x2000 + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + assert await axil_read_u32(axil, CONFIG_ADDR) & 0x3 == 0x2 + sequence = await start_window(axil) + for word in words: + await send_sample(dut, word ^ xorMask, word ^ xorMask) + status = await wait_done(axil, sequence) + assert status == 0x70 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 0 + + # A common corruption preserves channel coherence but violates the PN23 + # recurrence on the reference channel and reports its physical bit lane. + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + sequence = await start_window(axil) + for index, word in enumerate(words): + sample = (word ^ xorMask) ^ (4 if index == 4 else 0) + await send_sample(dut, sample, sample) + status = await wait_done(axil, sequence) + assert status == 0x50 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 2 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) >= 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR) & 4 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 0 + + # A relative error leaves the reference recurrence valid while identifying + # the non-reference channel and failing bit. + await configure( + axil, + alternating=False, + pn23=True, + samples=len(words), + timeout=100, + patternA=xorMask, + patternB=0) + sequence = await start_window(axil) + for index, word in enumerate(words): + sample = word ^ xorMask + await send_sample(dut, sample, sample ^ (8 if index == 5 else 0)) + status = await wait_done(axil, sequence) + assert status == 0x50 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 1 + assert await axil_read_u32(axil, WORD_ERROR_ADDR) == 0 + assert await axil_read_u32(axil, WORD_ERROR_ADDR+4) == 1 + assert await axil_read_u32(axil, BIT_ERROR_ADDR+4) == 8 + + # A selected FCO lane must be observed during the window to pass. Missing + # fcoValid is distinct from a mismatch and therefore leaves its error count + # at zero while clearing the FCO pass result. + await configure(axil, alternating=False, samples=2, timeout=100) + dut.fcoValid.value = 0 + sequence = await start_window(axil) + for _ in range(2): + await send_sample(dut, 0x1555, 0x1555) + status = await wait_done(axil, sequence) + assert status == 0x30 + assert await axil_read_u32(axil, CHANNEL_PASS_ADDR) == 3 + assert await axil_read_u32(axil, FCO_PASS_ADDR) == 0 + assert await axil_read_u32(axil, FCO_ERROR_ADDR) == 0 + dut.fcoValid.value = 1 + + # A zero sample request is rejected without entering busy. + await configure(axil, alternating=False, samples=0, timeout=100) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x04 + + # PN23 requires exclusive mode selection, every sample bit, and enough + # samples to acquire its 23-bit history. + for alternating, dataMask, samples in ( + (True, 0x3FFF, 3), + (False, 0x3FFE, 3), + (False, 0x3FFF, 1)): + await configure( + axil, + alternating=alternating, + pn23=True, + samples=samples, + timeout=100, + dataMask=dataMask) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x04 + + # Three consecutive capture clocks without sampleValid terminate a window. + await configure(axil, alternating=False, samples=2, timeout=3) + sequence = await axil_read_u32(axil, SEQUENCE_ADDR) + await axil_write_u32(axil, START_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x12 + assert await axil_read_u32(axil, CHECKED_ADDR) == 0 + + # Abort is a distinct terminal result and leaves partial counts readable. + await configure(axil, alternating=False, samples=2, timeout=0) + sequence = await start_window(axil) + await send_sample(dut, 0x1555, 0x1555) + await axil_write_u32(axil, ABORT_ADDR, 1) + status = await wait_done(axil, sequence) + assert status == 0x18 + assert await axil_read_u32(axil, CHECKED_ADDR) == 1 + + +def test_AdcDdrPatternTester(): + sources = [ + 'devices/AnalogDevices/adcDdr/rtl/AdcDdrPkg.vhd', + 'devices/AnalogDevices/adcDdr/rtl/AdcDdrPatternTester.vhd', + 'devices/AnalogDevices/adcDdr/wrappers/AdcDdrPatternTesterWrapper.vhd', + ] + run_surf_vhdl_test( + test_file=__file__, + toplevel='surf.adcddrpatterntesterwrapper', + extra_vhdl_sources={'surf': sources}, + ) diff --git a/tests/dsp/generic/dsp_test_utils.py b/tests/dsp/generic/dsp_test_utils.py index 869f9a9f1d..364c5456c9 100644 --- a/tests/dsp/generic/dsp_test_utils.py +++ b/tests/dsp/generic/dsp_test_utils.py @@ -12,15 +12,14 @@ from collections import deque -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd SIM_SETTLE_NS = 2 async def tick(clk, *, count: int = 1, settle_ns: int = SIM_SETTLE_NS) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(settle_ns, unit="ns") + await sample_after_tpd(clk, propagation_time=settle_ns) def signed_samples(width: int) -> list[int]: diff --git a/tests/dsp/generic/test_BoxcarFilter.py b/tests/dsp/generic/test_BoxcarFilter.py index 8179fa9537..181664d0f8 100644 --- a/tests/dsp/generic/test_BoxcarFilter.py +++ b/tests/dsp/generic/test_BoxcarFilter.py @@ -24,7 +24,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import boxcar_filter_reference, to_unsigned, truncate_signed @@ -45,8 +47,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 diff --git a/tests/dsp/generic/test_BoxcarIntegrator.py b/tests/dsp/generic/test_BoxcarIntegrator.py index 190227bb6b..bb1b345ad6 100644 --- a/tests/dsp/generic/test_BoxcarIntegrator.py +++ b/tests/dsp/generic/test_BoxcarIntegrator.py @@ -211,5 +211,4 @@ def test_BoxcarIntegrator(parameters): toplevel="surf.boxcarintegrator", parameters=hdl_parameters_from(parameters), extra_env=parameters, - extra_vhdl_sources={"surf": ["dsp/generic/fixed/BoxcarIntegrator.vhd"]}, ) diff --git a/tests/dsp/generic/test_DspPreSubMult.py b/tests/dsp/generic/test_DspPreSubMult.py index b7af821764..b74b26e804 100644 --- a/tests/dsp/generic/test_DspPreSubMult.py +++ b/tests/dsp/generic/test_DspPreSubMult.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, env_sl, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import signed_samples, to_unsigned, truncate_signed @@ -58,8 +60,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = self.reset_active_value() diff --git a/tests/dsp/generic/test_DspSquareDiffMult.py b/tests/dsp/generic/test_DspSquareDiffMult.py index 45995d85e4..97f8eb53e6 100644 --- a/tests/dsp/generic/test_DspSquareDiffMult.py +++ b/tests/dsp/generic/test_DspSquareDiffMult.py @@ -25,7 +25,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, env_sl, hdl_parameters_from, parameter_case, run_surf_vhdl_test from tests.dsp.generic.dsp_test_utils import signed_samples, to_unsigned, truncate_signed @@ -54,8 +56,7 @@ def reset_inactive_value(self) -> int: async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = self.reset_active_value() diff --git a/tests/dsp/generic/test_FirFilterMultiChannel.py b/tests/dsp/generic/test_FirFilterMultiChannel.py index f1ecb11676..e3d8aab016 100644 --- a/tests/dsp/generic/test_FirFilterMultiChannel.py +++ b/tests/dsp/generic/test_FirFilterMultiChannel.py @@ -195,12 +195,6 @@ def test_FirFilterMultiChannel(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/SlaveAxiStreamIpIntegrator.vhd", - "axi/axi-stream/ip_integrator/MasterAxiStreamIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", - "dsp/generic/fixed/FirFilterMultiChannel.vhd", str(parameters["WRAPPER_PATH"]), ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannel.py b/tests/dsp/generic/test_FirFilterSingleChannel.py index 9081cb3281..8b7f9dd7ae 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannel.py +++ b/tests/dsp/generic/test_FirFilterSingleChannel.py @@ -192,9 +192,6 @@ def test_FirFilterSingleChannel(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelTestWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py b/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py index 1132d9ac97..47a615d0fc 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py +++ b/tests/dsp/generic/test_FirFilterSingleChannelLowPass.py @@ -195,9 +195,6 @@ def test_FirFilterSingleChannelLowPass(): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py index 67060e2399..c6d84c6c91 100644 --- a/tests/dsp/generic/test_FirFilterSingleChannelTiming.py +++ b/tests/dsp/generic/test_FirFilterSingleChannelTiming.py @@ -208,9 +208,9 @@ async def output_hold_test(dut): int(dut.dout.value), int(dut.sbOut.value), ) - return - - raise AssertionError("Never reached a non-zero visible FIR output to hold") + break + else: + raise AssertionError("Never reached a non-zero visible FIR output to hold") PARAMETER_SWEEP = [ @@ -261,9 +261,6 @@ def test_FirFilterSingleChannelTiming(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", - "dsp/generic/fixed/FirFilterTap.vhd", - "dsp/generic/fixed/FirFilterSingleChannel.vhd", "dsp/generic/wrappers/FirFilterSingleChannelWrapper.vhd", ] }, diff --git a/tests/dsp/generic/test_FirFilterTap.py b/tests/dsp/generic/test_FirFilterTap.py index d1684bcf2e..23903b3abd 100644 --- a/tests/dsp/generic/test_FirFilterTap.py +++ b/tests/dsp/generic/test_FirFilterTap.py @@ -134,7 +134,6 @@ def test_FirFilterTap(parameters): }, extra_vhdl_sources={ "surf": [ - "dsp/generic/fixed/FirFilterTap.vhd", "dsp/generic/wrappers/FirFilterTapTestWrapper.vhd", ] }, diff --git a/tests/ethernet/EthMacCore/ethmac_test_utils.py b/tests/ethernet/EthMacCore/ethmac_test_utils.py index d556721070..8c4420e996 100644 --- a/tests/ethernet/EthMacCore/ethmac_test_utils.py +++ b/tests/ethernet/EthMacCore/ethmac_test_utils.py @@ -19,6 +19,7 @@ from cocotb.triggers import RisingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd # Shared EMAC helpers centralize the flattened lane ordering and the common @@ -161,8 +162,7 @@ async def recv(self, *, clk, ready_signal=None, keep_ready: bool = False) -> Ema if ready_signal is not None: ready_signal.value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if ready_signal is not None and not keep_ready: ready_signal.value = 0 return beat @@ -384,8 +384,7 @@ def start_clock(signal, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_dut(dut, *, clk_name: str = "ethClk", rst_name: str = "ethRst") -> None: @@ -452,8 +451,7 @@ async def send_frame_burst( await send_contiguous_frame(endpoint, frame, clk=clk) if index != len(frames) - 1: for _ in range(inter_frame_gap_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def recv_frame(endpoint: FlatEmacEndpoint, *, clk, ready_signal=None, timeout_cycles: int = 64) -> list[EmacBeat]: @@ -463,8 +461,7 @@ async def recv_frame(endpoint: FlatEmacEndpoint, *, clk, ready_signal=None, time if ready_signal is not None: ready_signal.value = 1 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TValid").value) == 1: beat = endpoint.snapshot() beats.append(beat) diff --git a/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py b/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py index f2b29e017c..3156c2ffb7 100644 --- a/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py +++ b/tests/ethernet/EthMacCore/test_EthCrc32Parallel.py @@ -31,16 +31,20 @@ from cocotb.triggers import RisingEdge, Timer from tests.base.crc.crc_test_utils import crc_out_from_remainder, crc_update, pack_active_bytes -from tests.common.regression_utils import hdl_parameters_from, parameter_case, run_surf_vhdl_test +from tests.common.regression_utils import ( + hdl_parameters_from, + parameter_case, + run_surf_vhdl_test, + sample_after_tpd, +) from tests.ethernet.EthMacCore.ethmac_test_utils import ETHMAC_RTL_SOURCES async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) # The CRC block registers state with `after TPD_G`, so leave a small # margin beyond that delay before sampling outputs in Python. - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) async def apply_word(dut, *, clk, byte_width: int, payload: list[int]) -> int: @@ -54,8 +58,7 @@ async def apply_word(dut, *, clk, byte_width: int, payload: list[int]) -> int: await Timer(2, unit="ns") # The resulting CRC is available on the following edge. - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) return int(dut.crcOut.value) diff --git a/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py b/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py index fc493935e2..a3741f3d10 100644 --- a/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py +++ b/tests/ethernet/EthMacCore/test_EthMacFlowCtrl.py @@ -25,7 +25,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test from tests.ethernet.EthMacCore.ethmac_test_utils import ETHMAC_RTL_SOURCES @@ -36,8 +37,7 @@ async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) @cocotb.test() diff --git a/tests/ethernet/EthMacCore/test_EthMacRxImport.py b/tests/ethernet/EthMacCore/test_EthMacRxImport.py index f10941372d..6aec82169a 100644 --- a/tests/ethernet/EthMacCore/test_EthMacRxImport.py +++ b/tests/ethernet/EthMacCore/test_EthMacRxImport.py @@ -79,6 +79,8 @@ async def eth_mac_rx_import_test(dut): await expect_no_output(sink, clk=bench.clk, cycles=24) assert int(dut.rxCountEn.value) == 0 assert int(dut.rxCrcError.value) == 0 + # Terminal scenario: these no-output/status checks are the complete + # contract for the intentionally disconnected XLGMII import placeholder. return min_frame = build_ethernet_frame( diff --git a/tests/ethernet/EthMacCore/test_EthMacTxExport.py b/tests/ethernet/EthMacCore/test_EthMacTxExport.py index e3f73132e5..3431b621ff 100644 --- a/tests/ethernet/EthMacCore/test_EthMacTxExport.py +++ b/tests/ethernet/EthMacCore/test_EthMacTxExport.py @@ -82,6 +82,8 @@ async def eth_mac_tx_export_test(dut): assert int(dut.txCountEn.value) == 0 assert int(dut.txUnderRun.value) == 0 assert int(dut.txLinkNotReady.value) == 0 + # Terminal scenario: these no-output/status checks are the complete + # contract for the intentionally disconnected XLGMII export placeholder. return min_frame = build_ethernet_frame( diff --git a/tests/ethernet/RawEthFramer/raw_eth_test_utils.py b/tests/ethernet/RawEthFramer/raw_eth_test_utils.py index 344b675bcb..f62d046537 100644 --- a/tests/ethernet/RawEthFramer/raw_eth_test_utils.py +++ b/tests/ethernet/RawEthFramer/raw_eth_test_utils.py @@ -16,6 +16,8 @@ from cocotbext.axi import AxiLiteBus, AxiLiteMaster from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import axil_read_u32, axil_write_u32, wait_sampled_ready from tests.ethernet.EthMacCore.ethmac_test_utils import ( FlatEmacEndpoint, @@ -359,6 +361,5 @@ async def wait_lookup_request( async def pulse_signal(signal, *, clk, cycles: int = 1) -> None: signal.value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) signal.value = 0 diff --git a/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py b/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py index 993b8d4bf8..3d3965c894 100644 --- a/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py +++ b/tests/ethernet/RoCEv2/test_EthMacRxCheckICrc.py @@ -29,7 +29,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -59,8 +60,7 @@ async def capture_crc_errors(dut, *, clk, timeout_cycles: int = 64) -> list[int] errors = [] for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(dut.mAxisTValid.value) == 1 and int(dut.mAxisTReady.value) == 1: errors.append(int(dut.mAxisCrcError.value)) if int(dut.mAxisTLast.value) == 1: diff --git a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py index d732bf50a2..c742a0fb6e 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2AxiStreamRdmaCore.py @@ -8,8 +8,20 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -# Test methodology -# ---------------- +# Test methodology: +# - Sweep: Cover single and high-occupancy traffic, response/work-request +# backpressure, engine stall/restart, partial and oversized frames, dynamic +# lengths, partial-byte enables, retry replay, ring wrap, and counter reset. +# - Stimulus: Push deterministic 32-byte AXI Stream beats while a configurable +# in-order engine peer accepts work requests, issues DMA reads, drains their +# responses, and returns work completions. +# - Checks: Scoreboard every replayed byte, first/last marker, byte enable, +# opcode/immediate field, error indication, work-request length, and relevant +# AXI-Lite success/error/oversize/frame counters. +# - Timing: Engine latency and backpressure build FIFO occupancy deliberately; +# transaction progress has cycle limits and each liveness scenario has a +# simulated-time watchdog so a datapath wedge fails diagnostically. +# # RoCEv2AxiStreamRdma buffers an inbound AXI-Stream payload in a store-and-forward # repack FIFO, issues one RDMA-SEND-with-immediate work request per complete packet, # serves the engine's DMA read by draining that packet into the 290-bit dmaReadResp, counts @@ -30,7 +42,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -57,6 +71,8 @@ BEAT_BYTES = 32 CLK_NS = 6.4 DATA_MASK = (1 << 256) - 1 +DMA_RESPONSE_TIMEOUT_CYCLES = 65_536 +PROGRESS_TIMEOUT_CYCLES = 65_536 def beat_pattern(counter: int) -> bytes: @@ -132,8 +148,16 @@ def __init__(self, dut): dut.S_WORKCOMP_ID.value = 0 async def _edge(self): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) + + async def _wait_asserted(self, signal, name: str) -> None: + for _ in range(PROGRESS_TIMEOUT_CYCLES): + if int(signal.value): + return + await self._edge() + raise AssertionError( + f"Timed out after {PROGRESS_TIMEOUT_CYCLES} cycles waiting for {name}" + ) async def reset(self): self.dut.rst.value = 1 @@ -171,8 +195,7 @@ async def push_packets(self, num_packets: int, beats_per_packet: int): dut.S_AXIS_TVALID.value = 1 ctr += 1 await self._edge() - while int(dut.S_AXIS_TREADY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_AXIS_TREADY, "S_AXIS_TREADY") dut.S_AXIS_TVALID.value = 0 dut.S_AXIS_TLAST.value = 0 @@ -187,8 +210,7 @@ async def push_partial(self, nbeats: int): dut.S_AXIS_TLAST.value = 0 dut.S_AXIS_TVALID.value = 1 await self._edge() - while int(dut.S_AXIS_TREADY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_AXIS_TREADY, "S_AXIS_TREADY") dut.S_AXIS_TVALID.value = 0 # --- engine emulator (single, in-order) ------------------------------------- @@ -200,8 +222,7 @@ async def _accept_workreq(self, backpressure=0): await RisingEdge(dut.clk) dut.M_WORKREQ_READY.value = 1 await self._edge() - while int(dut.M_WORKREQ_VALID.value) == 0: - await self._edge() + await self._wait_asserted(dut.M_WORKREQ_VALID, "M_WORKREQ_VALID") wr = { "id": int(dut.M_WORKREQ_ID.value), "opcode": int(dut.M_WORKREQ_OPCODE.value), @@ -220,22 +241,19 @@ async def _issue_dmareadreq(self, wr): dut.S_DMAREADREQ_WRID.value = wr["id"] dut.S_DMAREADREQ_SQPN.value = 0x10 await self._edge() - while int(dut.S_DMAREADREQ_READY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_DMAREADREQ_READY, "S_DMAREADREQ_READY") dut.S_DMAREADREQ_VALID.value = 0 async def _drain_resp(self, cfg): dut = self.dut beats = [] is_err = 0 - n = 0 - while True: + for n in range(DMA_RESPONSE_TIMEOUT_CYCLES): # optional backpressure if cfg.resp_backpressure and (n % cfg.resp_backpressure == cfg.resp_backpressure - 1): dut.M_DMAREADRESP_READY.value = 0 else: dut.M_DMAREADRESP_READY.value = 1 - n += 1 await self._edge() if int(dut.M_DMAREADRESP_VALID.value) == 1 and int(dut.M_DMAREADRESP_READY.value) == 1: ds = int(dut.M_DMAREADRESP_DATASTREAM.value) @@ -246,6 +264,12 @@ async def _drain_resp(self, cfg): beats.append((data, is_first, is_last)) if is_last: break + else: + dut.M_DMAREADRESP_READY.value = 0 + raise AssertionError( + "Timed out waiting for final DMA read response beat; " + f"received {len(beats)} beats" + ) dut.M_DMAREADRESP_READY.value = 0 return beats, is_err @@ -255,11 +279,11 @@ async def _issue_workcomp(self, wr): dut.S_WORKCOMP_STATUS.value = 0 dut.S_WORKCOMP_ID.value = wr["id"] await self._edge() - while int(dut.S_WORKCOMP_READY.value) == 0: - await self._edge() + await self._wait_asserted(dut.S_WORKCOMP_READY, "S_WORKCOMP_READY") dut.S_WORKCOMP_VALID.value = 0 async def engine(self, cfg, sb=None): + """Lifetime agent: service RDMA work until its owning test cancels it.""" idx = 0 while True: wr = await self._accept_workreq(cfg.workreq_backpressure) @@ -285,25 +309,33 @@ async def _run(dut, *, cfg, num_packets, beats_per_packet, watchdog_ns): await tb.reset() await tb.configure(beats_per_packet * BEAT_BYTES) sb = Scoreboard(beats_per_packet) - cocotb.start_soon(tb.engine(cfg, sb)) - cocotb.start_soon(tb.push_packets(num_packets, beats_per_packet)) + engine_task = cocotb.start_soon(tb.engine(cfg, sb)) + producer_task = cocotb.start_soon(tb.push_packets(num_packets, beats_per_packet)) async def wait_done(): while sb.packets < num_packets: await RisingEdge(dut.clk) # A wedge in the DUT stalls dmaReadResp -> wait_done never completes -> timeout. - await with_timeout(wait_done(), watchdog_ns, "ns") + try: + await with_timeout(wait_done(), watchdog_ns, "ns") + await producer_task - for _ in range(64): # let the final workComp settle - await RisingEdge(dut.clk) - succ = await axil_read_u32(tb.axil, REG_SUCCESS) - unsucc = await axil_read_u32(tb.axil, REG_UNSUCCESS) + for _ in range(64): # let the final workComp settle + await RisingEdge(dut.clk) + succ = await axil_read_u32(tb.axil, REG_SUCCESS) + unsucc = await axil_read_u32(tb.axil, REG_UNSUCCESS) - assert not sb.errors, f"scoreboard errors (first 10): {sb.errors[:10]}" - assert sb.packets == num_packets, f"only {sb.packets}/{num_packets} packets drained" - assert succ == num_packets, f"SuccessCounter={succ} != {num_packets}" - assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + assert not sb.errors, f"scoreboard errors (first 10): {sb.errors[:10]}" + assert sb.packets == num_packets, f"only {sb.packets}/{num_packets} packets drained" + assert succ == num_packets, f"SuccessCounter={succ} != {num_packets}" + assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + finally: + # The engine is a lifetime protocol peer; the producer is finite but + # must also be cancelled if the watchdog aborts its transaction. + engine_task.cancel() + if not producer_task.done(): + producer_task.cancel() @cocotb.test() @@ -393,7 +425,7 @@ async def engine_teardown_then_restart(dut): # disarm window drains/drops whatever the source was pushing, so kill the old # flood and idle the bus before re-arming. await axil_write_u32(tb.axil, REG_DISPATCH_ENABLE, 0) - flood.kill() + flood.cancel() dut.S_AXIS_TVALID.value = 0 dut.S_AXIS_TLAST.value = 0 for _ in range(20): @@ -403,15 +435,20 @@ async def engine_teardown_then_restart(dut): await axil_write_u32(tb.axil, REG_DISPATCH_ENABLE, 1) # A fresh, healthy engine takes over, fed by a fresh packet flood. - cocotb.start_soon(tb.engine(Cfg(readreq_latency=10, workcomp_latency=4))) - cocotb.start_soon(tb.push_packets(32, bpp)) + engine_task = cocotb.start_soon(tb.engine(Cfg(readreq_latency=10, workcomp_latency=4))) + refill_task = cocotb.start_soon(tb.push_packets(32, bpp)) async def wait_live(n): while int(await axil_read_u32(tb.axil, REG_SUCCESS)) < n: await RisingEdge(dut.clk) # Liveness: at least 4 completions after the restart, or the wedge stands. - await with_timeout(wait_live(4), 600_000, "ns") + try: + await with_timeout(wait_live(4), 600_000, "ns") + finally: + # Both agents intentionally run only for the liveness window. + engine_task.cancel() + refill_task.cancel() @cocotb.test() @@ -442,20 +479,26 @@ async def partial_packet_then_rearm(dut): # The clean stream must validate exactly — the stale partial is gone. n = 8 sb = Scoreboard(bpp) - cocotb.start_soon(tb.engine(Cfg(readreq_latency=8, workcomp_latency=4), sb)) - cocotb.start_soon(tb.push_packets(n, bpp)) + engine_task = cocotb.start_soon(tb.engine(Cfg(readreq_latency=8, workcomp_latency=4), sb)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) async def wait_done(): while sb.packets < n: await RisingEdge(dut.clk) - await with_timeout(wait_done(), 600_000, "ns") - for _ in range(64): - await RisingEdge(dut.clk) - unsucc = int(await axil_read_u32(tb.axil, REG_UNSUCCESS)) - assert not sb.errors, f"partial packet fused into the stream: {sb.errors[:10]}" - assert sb.packets == n, f"only {sb.packets}/{n} packets drained" - assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + try: + await with_timeout(wait_done(), 600_000, "ns") + await producer_task + for _ in range(64): + await RisingEdge(dut.clk) + unsucc = int(await axil_read_u32(tb.axil, REG_UNSUCCESS)) + assert not sb.errors, f"partial packet fused into the stream: {sb.errors[:10]}" + assert sb.packets == n, f"only {sb.packets}/{n} packets drained" + assert unsucc == 0, f"UnsuccessCounter={unsucc} != 0" + finally: + engine_task.cancel() + if not producer_task.done(): + producer_task.cancel() @cocotb.test() @@ -470,7 +513,7 @@ async def send_opcode_and_zeroed_reth(dut): n = 16 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(n, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) for k in range(n): wr = await tb._accept_workreq() assert wr["opcode"] == 0x3, f"pkt {k}: opCode 0x{wr['opcode']:x} != 0x3 (SEND_WITH_IMM)" @@ -484,6 +527,7 @@ async def send_opcode_and_zeroed_reth(dut): for _ in range(2): await RisingEdge(dut.clk) await tb._issue_workcomp(wr) + await producer_task @cocotb.test() @@ -501,7 +545,7 @@ async def immediate_carries_channel_and_slot(dut): length = bpp * BEAT_BYTES await tb.configure(length, addrwrap=wrap) - cocotb.start_soon(tb.push_packets(n, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(n, bpp)) for k in range(n): wr = await tb._accept_workreq() immdt = wr["immdt"] @@ -517,6 +561,7 @@ async def immediate_carries_channel_and_slot(dut): for _ in range(2): await RisingEdge(dut.clk) await tb._issue_workcomp(wr) + await producer_task @cocotb.test() @@ -530,7 +575,7 @@ async def retry_rereads_same_payload(dut): await tb.reset() bpp = 4 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(8, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(8, bpp)) wr = await tb._accept_workreq() # First read of this wr_id. @@ -547,6 +592,7 @@ async def retry_rereads_same_payload(dut): # Complete it (frees the slot) and confirm one success completion was counted. await tb._issue_workcomp(wr) + await producer_task for _ in range(64): await RisingEdge(dut.clk) assert int(await axil_read_u32(tb.axil, REG_SUCCESS)) == 1 @@ -566,7 +612,7 @@ async def ring_backpressure(dut): bpp = 2 total = RING_SLOTS + 6 await tb.configure(bpp * BEAT_BYTES) - cocotb.start_soon(tb.push_packets(total, bpp)) + producer_task = cocotb.start_soon(tb.push_packets(total, bpp)) # Accept WRs WITHOUT completing them (freePtr frozen) -> the ring fills to # RING_SLOTS and the dispatcher must then stall. @@ -582,12 +628,14 @@ async def watch_extra(): await tb._accept_workreq() extra_seen["hit"] = True - t = cocotb.start_soon(watch_extra()) - for _ in range(3000): - await RisingEdge(dut.clk) - assert not extra_seen["hit"], \ - f"dispatch exceeded the ring bound: a {RING_SLOTS + 1}th WR issued with the ring full" - t.kill() + extra_watch_task = cocotb.start_soon(watch_extra()) + try: + for _ in range(3000): + await RisingEdge(dut.clk) + assert not extra_seen["hit"], \ + f"dispatch exceeded the ring bound: a {RING_SLOTS + 1}th WR issued with the ring full" + finally: + extra_watch_task.cancel() dut.M_WORKREQ_READY.value = 0 # Release the gate: complete the held WRs -> freePtr advances -> ring drains. @@ -599,6 +647,8 @@ async def watch_extra(): wr = await with_timeout(tb._accept_workreq(), 400_000, "ns") await tb._issue_workcomp(wr) + await producer_task + for _ in range(64): await RisingEdge(dut.clk) assert int(await axil_read_u32(tb.axil, REG_SUCCESS)) == total, \ @@ -632,12 +682,11 @@ async def push_seq(): d.S_AXIS_TVALID.value = 1 ctr += 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_seq()) + producer_task = cocotb.start_soon(push_seq()) # The over-cap packet produces NO workReq (dropped). The first (and only) workReq is # the FOLLOWING normal packet -- it must dispatch cleanly carrying ITS bytes (proving @@ -654,6 +703,7 @@ async def push_seq(): exp = endian_swap_32(beat_pattern(over_beats + i)) assert data == exp, f"following pkt beat {i}: 0x{data:064x} != 0x{exp:064x}" await tb._issue_workcomp(wr) + await producer_task for _ in range(64): await RisingEdge(dut.clk) @@ -699,12 +749,11 @@ async def push_seq(): d.S_AXIS_TVALID.value = 1 ctr += 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_seq()) + producer_task = cocotb.start_soon(push_seq()) ctr = 0 for k, nbeats in enumerate(sizes): @@ -722,6 +771,8 @@ async def push_seq(): assert data == exp, f"pkt {k} beat {i}: 0x{data:064x} != 0x{exp:064x}" await tb._issue_workcomp(wr) + await producer_task + for _ in range(64): await RisingEdge(dut.clk) succ = int(await axil_read_u32(tb.axil, REG_SUCCESS)) @@ -756,12 +807,11 @@ async def push_frame(): d.S_AXIS_TLAST.value = 1 if last else 0 d.S_AXIS_TVALID.value = 1 await tb._edge() - while int(d.S_AXIS_TREADY.value) == 0: - await tb._edge() + await tb._wait_asserted(d.S_AXIS_TREADY, "S_AXIS_TREADY") d.S_AXIS_TVALID.value = 0 d.S_AXIS_TLAST.value = 0 - cocotb.start_soon(push_frame()) + producer_task = cocotb.start_soon(push_frame()) wr = await with_timeout(tb._accept_workreq(), 200_000, "ns") assert wr["len"] == total_bytes, f"workReq.len {wr['len']} != {total_bytes} (byte-exact)" @@ -772,13 +822,12 @@ async def push_frame(): dut.S_DMAREADREQ_WRID.value = wr["id"] dut.S_DMAREADREQ_SQPN.value = 0x10 await tb._edge() - while int(dut.S_DMAREADREQ_READY.value) == 0: - await tb._edge() + await tb._wait_asserted(dut.S_DMAREADREQ_READY, "S_DMAREADREQ_READY") dut.S_DMAREADREQ_VALID.value = 0 beats = [] is_err = 0 - while True: + for _ in range(DMA_RESPONSE_TIMEOUT_CYCLES): dut.M_DMAREADRESP_READY.value = 1 await tb._edge() if int(dut.M_DMAREADRESP_VALID.value) and int(dut.M_DMAREADRESP_READY.value): @@ -787,7 +836,14 @@ async def push_frame(): beats.append(((ds >> 34) & DATA_MASK, (ds >> 2) & 0xFFFFFFFF, ds & 1)) if ds & 1: break + else: + dut.M_DMAREADRESP_READY.value = 0 + raise AssertionError( + "Timed out waiting for partial-frame DMA response; " + f"received {len(beats)} beats" + ) dut.M_DMAREADRESP_READY.value = 0 + await producer_task def bitrev32(x): return int(f"{x:032b}"[::-1], 2) diff --git a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py index da7327bbb4..743bcbfcea 100644 --- a/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py +++ b/tests/ethernet/RoCEv2/test_RoCEv2Dcqcn.py @@ -8,8 +8,18 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## -# Test methodology -# ---------------- +# Test methodology: +# - Sweep: Compare the no-congestion baseline with one CNP event while holding +# rate recovery outside the observation window. +# - Stimulus: Drive full-rate one-beat frames, compress the DCQCN update +# intervals through AXI-Lite, and inject one synchronized CNP pulse. +# - Checks: Use both the AXI-Lite Rc/cnpCnt state and accepted M_AXIS beat rate; +# require the baseline token rate and the expected approximately 50-percent +# reduction after one CNP. +# - Timing: Count accepted beats over fixed 4000-clock windows. Each scenario is +# enclosed by a simulated-time watchdog and cancels its lifetime source on +# both success and failure. +# # DCQCN CNP rate-control bench for RoCEv2Dcqcn (via RoCEv2DcqcnWrapper). The bench # substitutes the RoCEv2Engine.cnp_received source with a TB-driven flat `cnp` port # and proves the congestion-control behavior with a DUAL PREDICATE: @@ -38,7 +48,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -111,8 +123,7 @@ def __init__(self, dut): dut.M_AXIS_TREADY.value = 1 async def _edge(self): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 @@ -132,6 +143,7 @@ async def configure_intervals(self, rate_inc_interval): # --- ingress source: 32-byte beats full-rate, honoring S_AXIS_TREADY ----- async def drive_beats(self): + """Lifetime agent: drive full-rate ingress until its test cancels it.""" dut = self.dut ctr = 0 while True: @@ -149,8 +161,7 @@ async def count_beats(self, window_clk): dut = self.dut n = 0 for _ in range(window_clk): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.M_AXIS_TVALID.value) and int(dut.M_AXIS_TREADY.value): n += 1 return n @@ -179,7 +190,8 @@ async def baseline_no_cnp(dut): await tb.reset() await tb.configure_intervals(RATE_INC_INTERVAL_HOLD) - cocotb.start_soon(tb.drive_beats()) + # The full-rate source is a lifetime agent for this measurement window. + source_task = cocotb.start_soon(tb.drive_beats()) async def body(): # Let the FIFO prime and the TokenBucket reach steady state. @@ -194,7 +206,10 @@ async def body(): assert 0.20 <= rate <= 0.30, \ f"baseline egress {rate:.4f} b/clk outside the ~0.25 token-bucket band ({beats}/{COUNT_WINDOW})" - await with_timeout(body(), 200_000, "ns") + try: + await with_timeout(body(), 200_000, "ns") + finally: + source_task.cancel() @cocotb.test() @@ -211,7 +226,8 @@ async def single_cnp_halves(dut): # measurement window, isolating the single 50% cut from the slow recovery. await tb.configure_intervals(RATE_INC_INTERVAL_HOLD) - cocotb.start_soon(tb.drive_beats()) + # The full-rate source is a lifetime agent for this measurement window. + source_task = cocotb.start_soon(tb.drive_beats()) async def body(): # Establish the LINE_RATE baseline. @@ -243,7 +259,10 @@ async def body(): assert after >= base * 0.35, \ f"post-CNP egress {after} collapsed below the expected half-rate {base} (over-throttled)" - await with_timeout(body(), 300_000, "ns") + try: + await with_timeout(body(), 300_000, "ns") + finally: + source_task.cancel() @pytest.mark.parametrize("parameters", [pytest.param({}, id="rocev2_dcqcn")]) diff --git a/tests/ethernet/RoCEv2/test_RoceConfigurator.py b/tests/ethernet/RoCEv2/test_RoceConfigurator.py index 0014a1e146..223fe268f0 100644 --- a/tests/ethernet/RoCEv2/test_RoceConfigurator.py +++ b/tests/ethernet/RoCEv2/test_RoceConfigurator.py @@ -27,10 +27,11 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster -from tests.axi.utils import axil_read_u32, axil_write_u32 +from tests.axi.utils import axil_read_u32, axil_write_u32, wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test from tests.ethernet.RoCEv2.roce_test_utils import axil_read_wide, axil_write_wide, roce_rtl_sources @@ -53,8 +54,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self): self.dut.rst.value = 1 @@ -69,12 +69,10 @@ def start_agents(self): async def wait_for_metadata_request(self, *, timeout_cycles: int = 64) -> int: self.dut.M_META_REQ_TREADY.value = 1 for _ in range(timeout_cycles): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) if int(self.dut.M_META_REQ_TVALID.value) == 1: value = int(self.dut.M_META_REQ_TDATA.value) - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) self.dut.M_META_REQ_TREADY.value = 0 return value self.dut.M_META_REQ_TREADY.value = 0 @@ -103,12 +101,8 @@ async def roce_configurator_axil_to_metadata_stream_test(dut): # read-only response register bank update as expected. dut.S_META_RESP_TDATA.value = first_response dut.S_META_RESP_TVALID.value = 1 - while True: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") - if int(dut.S_META_RESP_TREADY.value) == 1: - dut.S_META_RESP_TVALID.value = 0 - break + await wait_sampled_ready(dut.S_META_RESP_TREADY, clk=dut.clk) + dut.S_META_RESP_TVALID.value = 0 await tb.cycle(2) assert (await axil_read_u32(tb.axil, 0xF00) >> 1) & 0x1 == 1 diff --git a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py index b833e2c639..3c21dabf25 100644 --- a/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py +++ b/tests/ethernet/RoCEv2/test_RoceResizeAndSwap.py @@ -30,7 +30,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiStreamBus, AxiStreamFrame, AxiStreamSink, AxiStreamSource from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test @@ -65,17 +66,17 @@ def __init__(self, dut): dut.S_SIDE_BAND.setimmediatevalue(0) dut.M_AXIS_TREADY.setimmediatevalue(0) - cocotb.start_soon(self._monitor_sideband()) + # Lifetime sideband monitor retained by the bench. + self._monitor_task = cocotb.start_soon(self._monitor_sideband()) async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def _monitor_sideband(self): + """Lifetime agent: collect RoCE sidebands until the test ends.""" while True: - await RisingEdge(self.dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) if int(self.dut.M_AXIS_TVALID.value) == 1 and int(self.dut.M_AXIS_TREADY.value) == 1: self.rx_sidebands.append(int(self.dut.M_SIDE_BAND.value)) diff --git a/tests/protocols/README.md b/tests/protocols/README.md new file mode 100644 index 0000000000..a557cd2dd8 --- /dev/null +++ b/tests/protocols/README.md @@ -0,0 +1,96 @@ +# Protocol Regression Guidance + +Protocol tests follow the repository-wide [cocotb regression style +guide](../README.md). This page captures the additional practices that apply to +framed, reliable, layered, or specification-defined protocols. + +## Build One Shared Protocol Oracle + +Put packet constants, field encoders/decoders, checksum or CRC reference code, +and mechanical source/sink helpers in the subsystem's `*_test_utils.py` module. +Derive them from the normative protocol specification or the established SURF +package definitions, not from copied literals in individual tests. + +Anchor the oracle with small published or hand-worked known-answer vectors +before trusting it for randomized or integration traffic. Keep at least one +vector independent of the DUT implementation and, where practical, independent +of the helper's encoder path as well. A Python model that transliterates the RTL +line by line can reproduce the same defect and is not an independent oracle. + +Keep policy assertions in the test that names the behavior. A helper may build +a DATA frame or calculate its checksum; the test should still say that an +out-of-order frame must be dropped, an EOFE marker must propagate, or a timeout +must increment a particular counter. This keeps helpers from becoming an +unreviewed second implementation of the DUT. + +When the specification and current RTL disagree, make the distinction explicit: + +- A clear specification requirement should become a directed regression and, + when necessary, an RTL fix. +- An ambiguous behavior should first be a characterization test with its scope + documented. +- A deliberately narrower SURF profile should be described in the subsystem + README and test methodology rather than presented as full protocol + compliance. + +## Test By Layer + +Prefer a progression that establishes trustworthy lower-level behavior before +large integration scenarios: + +1. Field packing, checksums/CRCs, encoders, and decoders. +2. Leaf transmit and receive state machines. +3. Flow control, retries, timeouts, register interfaces, and CDC boundaries. +4. Core or wrapper integration, routing, and multi-stream interaction. + +An integration test should focus on what the integrated layer adds. Reuse the +same protocol oracle, but do not duplicate every leaf permutation at the top +level. + +As a suite grows, split it by coherent behavior rather than by an arbitrary +line limit: encoding, transmit, receive, flow control/recovery, register +control, and integration are useful boundaries. Keep shared frames and +mechanics in the oracle module, while each test module owns the assertions and +methodology for the behavior named by that file. + +## Required Protocol Cases + +Choose the cases relevant to the DUT, including: + +- minimum, typical, maximum, and non-word-aligned payload sizes; +- exact and partial final beats with correct `TKEEP`/`TSTRB`; +- SOF, EOF, EOFE, `TLAST`, `TDEST`, `TID`, and per-byte `TUSER` propagation; +- output backpressure and input idle gaps; +- malformed headers, lengths, flags, checksums/CRCs, and trailers; +- truncated frames, early/late termination, and recovery on the next frame; +- retry, acknowledgment, busy, timeout, overflow, and drop behavior; +- reset while idle and, where meaningful, reset with a partial transaction; +- sequence/tag wraparound and representative parameter boundaries; +- multi-lane or multi-stream ordering and arbitration when supported. + +Assert both positive and negative behavior. For invalid traffic, verify not only +that an error is reported but also that forbidden payload or control output is +not emitted and that subsequent valid traffic recovers. + +## Ready/Valid Discipline + +A source must hold data and all sidebands stable until an accepting clock edge. +A sink applying backpressure should verify that the DUT does the same. Use the +shared sampled-ready helper or a suitable `cocotbext.axi` endpoint instead of +open-coding subtly different handshake loops. + +Monitor accepted handshakes when timing, arbitration, or frame boundaries are +part of the contract. Comparing only final payload bytes can miss duplicated +beats, dropped sidebands, premature `TLAST`, or incorrect ordering. + +## Wrappers And Integration Models + +Keep wrapper HDL limited to record flattening, deterministic tie-offs, +simulator-friendly generics, or the smallest required topology. Packet +generation, retry peers, scoreboards, and assertions belong in Python. + +Use real protocol dependencies at the chosen DUT boundary. Do not replace a +generated core or lower protocol layer with a permissive test double and then +claim coverage of the full assembly. If the standard GHDL flow cannot compile +the real mixed-language or vendor dependency, document the deferral in the +subsystem README and test the accessible leaves directly. diff --git a/tests/protocols/batcher/README.md b/tests/protocols/batcher/README.md new file mode 100644 index 0000000000..c22adc76d8 --- /dev/null +++ b/tests/protocols/batcher/README.md @@ -0,0 +1,43 @@ +# Batcher Regressions + +These tests follow the repository-wide [regression style guide](../../README.md) +and [protocol guidance](../README.md). Shared AXI Stream beats, source/sink +drivers, byte compaction, and V2 superframe reference helpers live in +`batcher_test_utils.py`. + +The suite is layered deliberately: + +- `test_AxiStreamBatcher.py` proves the leaf V2 byte-stream contract, subframe + metadata, termination controls, and output stability under backpressure. +- `test_AxiStreamBatcherAxil.py` proves reset values, register readback, CDC + behavior, and the stream-side effects of threshold, gap, soft-reset, and + blowoff controls. It reuses the leaf oracle instead of repeating the full + packet matrix. +- `test_AxiStreamBatcherEventBuilder.py` focuses on indexed/routed source + selection, TDEST remapping, transition frames, alignment checks, timeout and + bypass/drop policy, counters, and multi-input progress. + +Keep future additions at the narrowest layer that owns the behavior. Packet +grammar belongs in the leaf test; register behavior belongs in the AXI-Lite +test; arbitration, routing, and cross-source policy belong in the event-builder +test. Extend `batcher_test_utils.py` for reusable mechanics, but leave the +policy being asserted visible in the individual test. + +Routed and unrouted configurations do not make every scenario applicable. Make +that relationship explicit in the pytest parameter/selector matrix, or report +the scenario as skipped with the configuration in the reason. Do not enter a +cocotb test and return successfully before its named routing, remapping, or +transition behavior has been exercised. + +The event-builder pytest wrapper uses `COCOTB_TEST_FILTER` to exclude the routed +transition-frame scenario from the INDEXED configuration before simulation. +All other event-builder scenarios apply to both modes. Add future mode-specific +cases to that explicit selection policy instead of branching out of the cocotb +entrypoint. + +Run the suite with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/batcher +``` diff --git a/tests/protocols/batcher/batcher_test_utils.py b/tests/protocols/batcher/batcher_test_utils.py index 38b5d544eb..84bc1c5f83 100644 --- a/tests/protocols/batcher/batcher_test_utils.py +++ b/tests/protocols/batcher/batcher_test_utils.py @@ -14,9 +14,10 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd @dataclass @@ -83,8 +84,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 256) -> AxisBeat: await Timer(1, unit="ns") if int(self._sig("TVALID").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TVALID").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -92,8 +92,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 256) -> AxisBeat: async def recv(self, *, clk, keep_ready: bool = False) -> AxisBeat: self._sig("TREADY").value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if not keep_ready: self._sig("TREADY").value = 0 return beat @@ -105,8 +104,7 @@ def start_batcher_clock(dut, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_batcher_dut(dut, *, cycles: int = 4) -> None: @@ -224,8 +222,7 @@ async def recv_beats(endpoint: FlatAxisEndpoint, *, clk, count: int) -> list[Axi async def expect_no_valid(endpoint: FlatAxisEndpoint, *, clk, cycles: int) -> None: endpoint._sig("TREADY").value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert int(endpoint._sig("TVALID").value) == 0 endpoint._sig("TREADY").value = 0 @@ -242,12 +239,10 @@ async def recv_until_last_with_backpressure( for _ in range(max_beats): beat = await endpoint.wait_valid(clk=clk) for _ in range(hold_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert endpoint.snapshot() == beat endpoint._sig("TREADY").value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint._sig("TREADY").value = 0 beats.append(beat) if beat.last: diff --git a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py index 92587dd30a..f863708fc6 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherAxil.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherAxil.py @@ -334,7 +334,6 @@ def test_AxiStreamBatcherAxil(parameters): extra_env=parameters, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "protocols/batcher/wrappers/AxiStreamBatcherAxilWrapper.vhd", ], }, diff --git a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py index 2fd9a23b86..f20b47941a 100644 --- a/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py +++ b/tests/protocols/batcher/test_AxiStreamBatcherEventBuilder.py @@ -362,8 +362,6 @@ async def bypass_skips_source_and_recovers_test(dut): async def routed_transition_frame_preempts_event_test(dut): tb = TB(dut) await tb.reset() - if tb.mode != "ROUTED": - return transition = _frame(bytes(range(0x80, 0x85)), dest=tb.trans_tdest, first_user=0x91, last_user=0xF1) blocked = AxisBeat( @@ -660,14 +658,22 @@ async def align_check_survives_missing_reference_source_with_timeout_test(dut): ], ) def test_AxiStreamBatcherEventBuilder(parameters): + extra_env = dict(parameters) + if parameters["MODE_G"] != "ROUTED": + # This scenario exercises the routed transition-frame policy and has no + # contract in INDEXED mode. Exclude it before simulation rather than + # recording a cocotb pass that performed no checks. + extra_env["COCOTB_TEST_FILTER"] = ( + r"^(?!.*routed_transition_frame_preempts_event_test$).*" + ) + run_surf_vhdl_test( test_file=__file__, toplevel="surf.axistreambatchereventbuilderwrapper", parameters=parameters, - extra_env=parameters, + extra_env=extra_env, extra_vhdl_sources={ "surf": [ - "axi/axi-lite/ip_integrator/SlaveAxiLiteIpIntegrator.vhd", "protocols/batcher/wrappers/AxiStreamBatcherEventBuilderWrapper.vhd", ], }, diff --git a/tests/protocols/coaxpress/coaxpress_test_utils.py b/tests/protocols/coaxpress/coaxpress_test_utils.py index fc0d6ce969..e700efeef2 100644 --- a/tests/protocols/coaxpress/coaxpress_test_utils.py +++ b/tests/protocols/coaxpress/coaxpress_test_utils.py @@ -16,9 +16,9 @@ import cocotb from cocotb.clock import Clock from cocotb.handle import Immediate -from cocotb.triggers import RisingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd CXP_IDLE = 0xB53C3CBC @@ -158,8 +158,7 @@ def set_initial_values(dut, values: dict[str, int]) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_signals(dut, *, clk, reset_names: tuple[str, ...], assert_cycles: int = 4, release_cycles: int = 2) -> None: @@ -208,8 +207,7 @@ async def send_rx_word( dut.rxLinkUp.value = link_up dut.rxData.value = data dut.rxDataK.value = data_k - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if capture is not None and valid_name is not None: snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: @@ -255,8 +253,7 @@ async def collect_stream_bytes( if ready_name is not None: getattr(dut, ready_name).value = 1 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(getattr(dut, valid_name).value) == 1: payload.append(int(getattr(dut, data_name).value)) if len(payload) >= count: @@ -293,8 +290,7 @@ async def send_axis_beats_no_ready( getattr(dut, f"{prefix}TData").value = beat.data getattr(dut, f"{prefix}TKeep").value = beat.keep getattr(dut, f"{prefix}TLast").value = beat.last - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if capture is not None and valid_name is not None: snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: @@ -315,8 +311,7 @@ async def collect_pulses( ) -> list[dict[str, int]]: observed: list[dict[str, int]] = [] for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) snapshot = pulse_snapshot(dut, valid_name=valid_name, field_names=field_names) if snapshot is not None: observed.append(snapshot) diff --git a/tests/protocols/coaxpress/test_CoaXPressConfig.py b/tests/protocols/coaxpress/test_CoaXPressConfig.py index 9e5d598bf1..6cdbcebdb5 100644 --- a/tests/protocols/coaxpress/test_CoaXPressConfig.py +++ b/tests/protocols/coaxpress/test_CoaXPressConfig.py @@ -23,7 +23,9 @@ # rather than assuming an ideal one-cycle transfer through the assembly. import cocotb -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -52,8 +54,7 @@ async def _drive_cfg_rx_completion(dut, value: int, *, hold_cycles: int = 8) -> dut.cfgRxTData.value = value dut.cfgRxTValid.value = 1 for _ in range(hold_cycles): - await RisingEdge(dut.cfgClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.cfgClk) dut.cfgRxTValid.value = 0 dut.cfgRxTData.value = 0 diff --git a/tests/protocols/coaxpress/test_CoaXPressCore.py b/tests/protocols/coaxpress/test_CoaXPressCore.py index 3770cb0b80..8357a17db9 100644 --- a/tests/protocols/coaxpress/test_CoaXPressCore.py +++ b/tests/protocols/coaxpress/test_CoaXPressCore.py @@ -29,11 +29,12 @@ import os import cocotb -from cocotb.triggers import Event, RisingEdge, Timer, with_timeout +from cocotb.triggers import Event, with_timeout from cocotb.utils import get_sim_time from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.common.regression_utils import env_flag, env_int, run_surf_vhdl_test, start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_IDLE, CXP_IDLE_K, @@ -195,18 +196,18 @@ async def _send_image_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: + """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return counts[key] += int(signal.value) async def _trace_first_signal_high(signal, clk, stop_event: Event, trace: dict[str, object], capture) -> None: + """Lifetime agent: observe a signal until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return if trace["seen"] or int(signal.value) == 0: diff --git a/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py b/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py index e37fa725a9..4826573ecc 100644 --- a/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py +++ b/tests/protocols/coaxpress/test_CoaXPressEventAckMsg.py @@ -22,7 +22,8 @@ # accepted handshakes once `TREADY` is asserted. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -54,16 +55,14 @@ def _expected_event_ack_bytes(tag: int) -> list[tuple[int, int, int]]: async def _pulse_event_ack(dut, tag: int) -> None: dut.eventTag.value = tag dut.eventAck.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.eventAck.value = 0 async def _collect_handshakes(dut, *, count: int, timeout_cycles: int) -> list[tuple[int, int, int]]: observed: list[tuple[int, int, int]] = [] for _ in range(timeout_cycles): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.eventAckTValid.value) == 1 and int(dut.eventAckTReady.value) == 1: observed.append( ( @@ -93,8 +92,7 @@ async def coaxpress_event_ack_msg_serialize_and_backpressure_test(dut): stalled_byte = None for _ in range(8): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if int(dut.eventAckTValid.value) == 1: sample = ( int(dut.eventAckTData.value), @@ -109,8 +107,7 @@ async def coaxpress_event_ack_msg_serialize_and_backpressure_test(dut): assert stalled_byte == (word_to_bytes(CXP_SOP)[0], 1, 0) for _ in range(2): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert ( int(dut.eventAckTData.value), int(dut.eventAckTK.value), diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py index 75e655f89b..e9abb61a35 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridge.py @@ -25,7 +25,8 @@ # checks robust to gearbox latency while still validating real output order. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -98,15 +99,13 @@ async def _setup_bridge(dut) -> None: async def _drive_rx64(dut, rxd: int, rxc: int) -> None: dut.xgmiiRxd.value = rxd dut.xgmiiRxc.value = rxc - await RisingEdge(dut.rxClk156) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk156) async def _capture_rx_words(dut, *, cycles: int) -> list[tuple[int, int]]: observed: list[tuple[int, int]] = [] for _ in range(cycles): - await RisingEdge(dut.rxClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk312) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): observed.append(sample) @@ -125,14 +124,12 @@ async def coaxpress_over_fiber_bridge_top_level_integration_test(dut): async def capture_tx_words(cycles: int) -> None: for _ in range(cycles): - await RisingEdge(dut.txClk156) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk156) tx_observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) async def capture_rx_words(cycles: int) -> None: for _ in range(cycles): - await RisingEdge(dut.rxClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk312) sample = (int(dut.rxData.value), int(dut.rxDataK.value)) if sample != (CXP_IDLE, CXP_IDLE_K): rx_observed.append(sample) @@ -143,10 +140,8 @@ async def capture_rx_words(cycles: int) -> None: dut.txLsData.value = 0xA5 dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.txClk312) - await Timer(1, unit="ns") - await RisingEdge(dut.txClk312) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk312) + await sample_after_tpd(dut.txClk312) dut.txLsValid.value = 0 await cycle(dut.rxClk156, 3) diff --git a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py index 3ae2b30e85..94f55efc6a 100644 --- a/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py +++ b/tests/protocols/coaxpress/test_CoaXPressOverFiberBridgeTx.py @@ -21,7 +21,8 @@ # actual start, payload, terminate, and return-to-idle ordering. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -77,8 +78,7 @@ async def coaxpress_over_fiber_bridge_tx_packet_format_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) capture = cocotb.start_soon(capture_words(20)) @@ -86,8 +86,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = 0xA5 dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await cycle(dut.clk, 6) @@ -96,8 +95,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = 0x5C dut.txLsDataK.value = 1 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await capture @@ -147,8 +145,7 @@ async def coaxpress_over_fiber_bridge_tx_partial_lane_enable_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) capture = cocotb.start_soon(capture_words(8)) @@ -156,8 +153,7 @@ async def capture_words(count: int) -> None: dut.txLsData.value = CXP_K28_1 dut.txLsDataK.value = 1 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await capture @@ -195,8 +191,7 @@ async def coaxpress_over_fiber_bridge_tx_lane_enable_idle_rotation_test(dut): async def capture_words(count: int) -> None: while len(observed) < count: - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) observed.append((int(dut.xgmiiTxd.value), int(dut.xgmiiTxc.value))) async def send_byte(byte: int, lane_enable: int) -> None: @@ -204,8 +199,7 @@ async def send_byte(byte: int, lane_enable: int) -> None: dut.txLsData.value = byte dut.txLsDataK.value = 0 dut.txLsValid.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.txLsValid.value = 0 await cycle(dut.clk, 4) diff --git a/tests/protocols/coaxpress/test_CoaXPressRx.py b/tests/protocols/coaxpress/test_CoaXPressRx.py index fcef05d0e0..08f688ac22 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRx.py +++ b/tests/protocols/coaxpress/test_CoaXPressRx.py @@ -26,10 +26,11 @@ import os import cocotb -from cocotb.triggers import Event, RisingEdge, Timer +from cocotb.triggers import Event import pytest from tests.common.regression_utils import env_flag, env_int, parameter_case, run_surf_vhdl_test, start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd from tests.protocols.coaxpress.coaxpress_test_utils import ( CXP_EOP, CXP_IDLE, @@ -378,9 +379,9 @@ async def _send_one_lane_frame( async def _count_signal_high_cycles(signal, clk, stop_event: Event, counts: dict[str, int], key: str) -> None: + """Lifetime agent: count asserted cycles until the owner sets stop_event.""" while True: - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if stop_event.is_set(): return counts[key] += int(signal.value) @@ -438,10 +439,8 @@ async def _pulse_rx_fsm_reset(dut, *, cycles: int = 4) -> None: dut.rxFsmRst.value = 0 -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_one_lane_integration_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -550,11 +549,8 @@ async def coaxpress_rx_one_lane_integration_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 2) async def coaxpress_rx_two_lane_mux_rotation_test(dut): - if env_int("NUM_LANES_G", default=1) != 2: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -669,11 +665,13 @@ async def capture(cycle_index: int) -> None: # Opt-in investigation benches. These stay behind RUN_KNOWN_ISSUE_TESTS until # the remaining 4-lane short-frame boundary issue in CoaXPressRxHsFsm is fixed. # -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -739,11 +737,13 @@ async def coaxpress_rx_four_lane_fsm_error_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -797,11 +797,13 @@ async def coaxpress_rx_four_lane_clean_rotation_known_issue_test(dut): assert [beat[2] for beat in data_beats] == [0, 0, 1] * 4, data_beats -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -887,11 +889,13 @@ async def coaxpress_rx_four_lane_fsm_error_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -982,11 +986,13 @@ async def coaxpress_rx_four_lane_overflow_reset_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, (signal_counts, observed_recovery_last) -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 4 + ), +) async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, @@ -1079,11 +1085,13 @@ async def coaxpress_rx_four_lane_overflow_recovery_known_issue_test(dut): assert observed_recovery_last == [0, 0, 1] * 4, observed_recovery_last -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 1 + ), +) async def coaxpress_rx_repeated_single_line_frame_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_lockstep_clocks(dut.dataClk, dut.cfgClk, dut.txClk, dut.rxClk, period_ns=4.0) set_initial_values( dut, diff --git a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py index 07d11be1c8..3df1d76014 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxHsFsm.py @@ -25,7 +25,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import env_int, parameter_case, run_surf_vhdl_test @@ -214,10 +215,8 @@ def _expected_header_data_from_fields( ) -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -249,8 +248,7 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -266,10 +264,8 @@ async def coaxpress_rx_hs_fsm_header_and_lines_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -320,8 +316,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -334,11 +329,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_recovery_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 1) async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -400,8 +392,7 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(6): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [{"hdrTData": _expected_header_data(), "hdrTLast": 1, "hdrTSof": 1}], ( @@ -414,11 +405,8 @@ async def coaxpress_rx_hs_fsm_malformed_header_drops_following_line_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 2) async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): - if env_int("NUM_LANES_G", default=1) != 2: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -491,8 +479,7 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(8): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) assert header_beats == [ @@ -526,11 +513,8 @@ async def coaxpress_rx_hs_fsm_two_lane_step_alignment_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) @@ -624,9 +608,8 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): dut.sAxisTKeep.value = lane_keep_mask([0, 1, 2, 3]) dut.sAxisTLast.value = 0 shared_beat_cycles = 0 - while True: - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + for _ in range(1024): + await sample_after_tpd(dut.rxClk) shared_beat_cycles += 1 error_seen |= int(dut.rxFsmError.value) == 1 trace.append( @@ -636,6 +619,11 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) if int(dut.sAxisTReady.value) == 1: break + else: + raise AssertionError( + "Timed out waiting for shared tail/marker beat acceptance; " + f"trace tail={trace[-8:]}" + ) dut.sAxisTValid.value = 0 dut.sAxisTData.value = 0 dut.sAxisTKeep.value = 0 @@ -649,8 +637,7 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): _capture_outputs(dut, header_beats=header_beats, data_beats=data_beats) for _ in range(8): - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) error_seen |= int(dut.rxFsmError.value) == 1 trace.append( f"idle ready={int(dut.sAxisTReady.value)} err={int(dut.rxFsmError.value)} " @@ -669,11 +656,13 @@ async def coaxpress_rx_hs_fsm_quad_lane_tail_marker_type_same_beat_test(dut): ], trace -@cocotb.test(skip=os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1") +@cocotb.test( + skip=( + os.getenv("RUN_KNOWN_ISSUE_TESTS") != "1" + or env_int("NUM_LANES_G", default=1) != 1 + ), +) async def coaxpress_rx_hs_fsm_repeated_single_line_frame_known_issue_test(dut): - if env_int("NUM_LANES_G", default=1) != 1: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.rxFsmRst.setimmediatevalue(0) diff --git a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py index bba263f324..114f59d8b4 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxLaneMux.py @@ -23,7 +23,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import env_int, parameter_case, run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import pack_words, reset_dut, start_clock @@ -80,8 +81,7 @@ async def coaxpress_rx_lane_mux_round_robin_test(dut): _set_lane_inputs(dut, current, num_lanes=num_lanes) dut.mAxisTReady.value = 0 if cycle_index < 2 else 1 - await RisingEdge(dut.rxClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rxClk) ready_bits = int(dut.sAxisTReady.value) for lane, queue in enumerate(lane_queues): diff --git a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py index b3e3e6d69f..a41adbd105 100644 --- a/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py +++ b/tests/protocols/coaxpress/test_CoaXPressRxWordPacker.py @@ -76,56 +76,54 @@ async def coaxpress_rx_word_packer_repack_test(dut): {"mAxisTData": 0x11223344, "mAxisTKeep": 0xF, "mAxisTLast": 0}, {"mAxisTData": 0x55667788, "mAxisTKeep": 0xF, "mAxisTLast": 1}, ] - return - - # The wider case intentionally starts on lane 1, fills one output beat, - # then spills into a short final beat on the next cycle. - await send_axis_beats_no_ready( - dut, - beats=[ - AxisBeat( - data=pack_words([0x0, 0xAAA00001, 0xBBB00002, 0xCCC00003]), - keep=0xFFF0, - last=0, - ), - AxisBeat( - data=pack_words([0xDDD00004, 0xEEE00005, 0xFFF00006]), - keep=0x0FFF, - last=1, - ), - ], - clk=dut.rxClk, - capture=observed, - valid_name="mAxisTValid", - field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), - ) - observed.extend( - await collect_pulses( + else: + # The wider case intentionally starts on lane 1, fills one output beat, + # then spills into a short final beat on the next cycle. + await send_axis_beats_no_ready( dut, + beats=[ + AxisBeat( + data=pack_words([0x0, 0xAAA00001, 0xBBB00002, 0xCCC00003]), + keep=0xFFF0, + last=0, + ), + AxisBeat( + data=pack_words([0xDDD00004, 0xEEE00005, 0xFFF00006]), + keep=0x0FFF, + last=1, + ), + ], clk=dut.rxClk, - cycles=6, + capture=observed, valid_name="mAxisTValid", field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), ) - ) + observed.extend( + await collect_pulses( + dut, + clk=dut.rxClk, + cycles=6, + valid_name="mAxisTValid", + field_names=("mAxisTData", "mAxisTKeep", "mAxisTLast"), + ) + ) - assert observed == [ - { - "mAxisTData": pack_words([0xAAA00001, 0xBBB00002, 0xCCC00003, 0xDDD00004]), - "mAxisTKeep": keep_for_words(4), - "mAxisTLast": 0, - }, - { - "mAxisTData": pack_words([0xEEE00005, 0xFFF00006]), - "mAxisTKeep": keep_for_words(2), - "mAxisTLast": 1, - }, - ] + assert observed == [ + { + "mAxisTData": pack_words([0xAAA00001, 0xBBB00002, 0xCCC00003, 0xDDD00004]), + "mAxisTKeep": keep_for_words(4), + "mAxisTLast": 0, + }, + { + "mAxisTData": pack_words([0xEEE00005, 0xFFF00006]), + "mAxisTKeep": keep_for_words(2), + "mAxisTLast": 1, + }, + ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) == 1) async def coaxpress_rx_word_packer_reset_flush_test(dut): - num_lanes = env_int("NUM_LANES_G", default=1) start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -134,9 +132,6 @@ async def coaxpress_rx_word_packer_reset_flush_test(dut): dut.sAxisTLast.setimmediatevalue(0) await reset_dut(dut) - if num_lanes == 1: - return - # Leave a half-full packed word buffered, then reset and confirm the next # frame starts cleanly rather than draining stale payload. await send_axis_beats_no_ready( @@ -177,11 +172,8 @@ async def coaxpress_rx_word_packer_reset_flush_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_three_word_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -224,11 +216,8 @@ async def coaxpress_rx_word_packer_three_word_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_two_plus_one_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -276,11 +265,8 @@ async def coaxpress_rx_word_packer_two_plus_one_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_offset_two_plus_one_last_beat_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) @@ -328,11 +314,8 @@ async def coaxpress_rx_word_packer_offset_two_plus_one_last_beat_test(dut): ] -@cocotb.test() +@cocotb.test(skip=env_int("NUM_LANES_G", default=1) != 4) async def coaxpress_rx_word_packer_back_to_back_offset_short_frames_test(dut): - if env_int("NUM_LANES_G", default=1) != 4: - return - start_clock(dut.rxClk) dut.rxRst.setimmediatevalue(1) dut.sAxisTValid.setimmediatevalue(0) diff --git a/tests/protocols/coaxpress/test_CoaXPressTx.py b/tests/protocols/coaxpress/test_CoaXPressTx.py index 8bf428bd1d..51b9a69260 100644 --- a/tests/protocols/coaxpress/test_CoaXPressTx.py +++ b/tests/protocols/coaxpress/test_CoaXPressTx.py @@ -26,6 +26,8 @@ import cocotb from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.coaxpress.coaxpress_test_utils import ( @@ -83,15 +85,13 @@ async def _drive_cfg_packet(dut, beats: list[tuple[int, int]]) -> None: async def _pulse_event_ack(dut, tag: int) -> None: dut.eventTag.value = tag dut.eventAck.value = 1 - await RisingEdge(dut.cfgClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.cfgClk) dut.eventAck.value = 0 async def _pulse_sw_trigger(dut) -> None: dut.swTrig.value = 1 - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) dut.swTrig.value = 0 @@ -99,8 +99,7 @@ async def _collect_tx_bytes(dut, *, count: int, timeout_cycles: int) -> tuple[li observed: list[tuple[int, int, int]] = [] tx_trig_drop_seen = False for cycle_index in range(timeout_cycles): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txLsValid.value) == 1: diff --git a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py index cc8918333a..9e6ca5e747 100644 --- a/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py +++ b/tests/protocols/coaxpress/test_CoaXPressTxLsFsm.py @@ -23,7 +23,8 @@ # so cadence and serialized ordering are checked on the real byte timeline. import cocotb -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready from tests.common.regression_utils import run_surf_vhdl_test @@ -64,8 +65,7 @@ async def _drive_cfg_bytes(dut, beats: list[tuple[int, int]]) -> None: async def _collect_strobes(dut, *, count: int, timeout_cycles: int) -> list[tuple[int, int, int]]: observed: list[tuple[int, int, int]] = [] for cycle_index in range(timeout_cycles): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txStrobe.value) == 1: observed.append((cycle_index, int(dut.txData.value), int(dut.txDataK.value))) if len(observed) == count: @@ -75,8 +75,7 @@ async def _collect_strobes(dut, *, count: int, timeout_cycles: int) -> list[tupl async def _pulse_trigger(dut) -> None: dut.txTrig.value = 1 - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) dut.txTrig.value = 0 @@ -108,8 +107,9 @@ async def coaxpress_tx_ls_fsm_idle_and_config_cadence_test(dut): dut.txRate.setimmediatevalue(1) await reset_dut(dut, clk_name="txClk", reset_names=("txRst",)) - cocotb.start_soon(_drive_cfg_bytes(dut, [(0x33, 0), (0xDC, 1)])) + cfg_task = cocotb.start_soon(_drive_cfg_bytes(dut, [(0x33, 0), (0xDC, 1)])) observed = await _collect_strobes(dut, count=6, timeout_cycles=600) + await cfg_task assert [(data, is_k) for _, data, is_k in observed[:4]] == IDLE_SEQUENCE assert [(data, is_k) for _, data, is_k in observed[4:]] == [(0x33, 0), (0xDC, 1)] @@ -137,13 +137,12 @@ async def pulse_again_mid_message() -> None: await cycle(dut.txClk, 200) await _pulse_trigger(dut) - cocotb.start_soon(pulse_again_mid_message()) + retrigger_task = cocotb.start_soon(pulse_again_mid_message()) strobes: list[tuple[int, int, int]] = [] tx_trig_drop_seen = False for cycle_index in range(1400): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txStrobe.value) == 1: @@ -151,6 +150,8 @@ async def pulse_again_mid_message() -> None: if len(strobes) >= 14 and tx_trig_drop_seen: break + await retrigger_task + first_trigger = None second_trigger = None for start in range(len(strobes) - 5): @@ -223,8 +224,7 @@ async def coaxpress_tx_ls_fsm_pulse_width_update_terminates_active_trigger_test( tx_trig_drop_seen = False for cycle_index in range(1800): - await RisingEdge(dut.txClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.txClk) if int(dut.txTrigDrop.value) == 1: tx_trig_drop_seen = True if int(dut.txStrobe.value) == 1: diff --git a/tests/protocols/jesd204b/jesd204b_test_utils.py b/tests/protocols/jesd204b/jesd204b_test_utils.py index 9a36afe539..aa7f8a9805 100644 --- a/tests/protocols/jesd204b/jesd204b_test_utils.py +++ b/tests/protocols/jesd204b/jesd204b_test_utils.py @@ -40,7 +40,7 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from collections import deque from tests.common.regression_utils import run_surf_vhdl_test # noqa: F401 – re-exported for bench files @@ -702,8 +702,7 @@ async def drive_gt_lane_from_timeline( for data_32b, datak_4b in timeline[segment][start_idx:]: data_port.value = data_32b datak_port.value = datak_4b - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def wait_nSync(dut, *, value: int, clk, timeout_cycles: int = 128) -> None: @@ -723,8 +722,7 @@ async def wait_nSync(dut, *, value: int, clk, timeout_cycles: int = 128) -> None timeout_cycles: Maximum rising edges to wait. """ for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(dut.nSync_o.value) == value: return raise AssertionError( @@ -751,8 +749,7 @@ async def wait_data_valid_all( timeout_cycles: Maximum rising edges to wait. """ for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if all( int(getattr(dut, f"dataValid_{i}_o").value) == 1 for i in range(l_g) @@ -814,8 +811,7 @@ async def forward_gt_loopback( ] cycle = 0 while stop_event is None or not stop_event.is_set(): - await RisingEdge(clk) - await Timer(1, unit="ns") # TPD_G=1 ns registered-output settle + await sample_after_tpd(clk) # TPD_G=1 ns registered-output settle # nSync forwarding: RX nSync_o (sl) -> TX nSync_TX_i (slv L_G-1:0). # Replicate single-bit nSync_RX_o to all l_g TX lanes so both lanes advance # through SYNC_S->ILAS when nSync_o asserts. Writing plain int(nSync_RX_o) @@ -922,8 +918,7 @@ async def measure_lmfc_period(dut, *, clk, timeout_cycles: int = 512) -> int: """ # Wait for first rising edge of lmfc_o for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if dut.lmfc_o.value == 1: break else: @@ -934,8 +929,7 @@ async def measure_lmfc_period(dut, *, clk, timeout_cycles: int = 512) -> int: # Count cycles to second rising edge count = 0 for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) count += 1 if dut.lmfc_o.value == 1: return count @@ -969,8 +963,7 @@ def __init__(self, dut, *, clock_period_ns: float = CLOCK_PERIOD_NS) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.clk) async def reset(self, cycles: int = 4) -> None: """Assert rst for `cycles` clock cycles, then deassert.""" diff --git a/tests/protocols/jesd204b/test_Jesd16bTo32b.py b/tests/protocols/jesd204b/test_Jesd16bTo32b.py index 82cc945c53..b6f4540ee1 100644 --- a/tests/protocols/jesd204b/test_Jesd16bTo32b.py +++ b/tests/protocols/jesd204b/test_Jesd16bTo32b.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( hdl_parameters_from, @@ -83,26 +85,22 @@ async def reset(self) -> None: self.dut.rdRst.value = 1 # Hold reset across several wrClk cycles for _ in range(6): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.wrRst.value = 0 # Hold rdRst for a couple more rdClk cycles to be safe for _ in range(4): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) self.dut.rdRst.value = 0 # Quiet settling time after reset deassertion for _ in range(4): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_word(self, data: int, trig: int) -> None: """Drive one 16-bit word on the write side on the next wrClk edge.""" self.dut.validIn.value = 1 self.dut.dataIn.value = data self.dut.trigIn.value = trig - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_pair(self, first: int, second: int, trig_first: int = 1, trig_second: int = 0) -> None: @@ -119,8 +117,7 @@ async def write_pair(self, first: int, second: int, async def wait_valid_out(self) -> None: """Block until validOut asserts on the rdClk domain (bounded).""" for _ in range(_VALID_TIMEOUT_CYCLES): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 1: return raise AssertionError( @@ -163,8 +160,7 @@ async def word_order_and_trig_test(dut): # Wait for validOut to deassert before next pair for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break @@ -219,19 +215,16 @@ async def overflow_underflow_quiet_test(dut): dut.validIn.value = 1 dut.dataIn.value = 0xA000 + pair_idx dut.trigIn.value = 1 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) dut.dataIn.value = 0xB000 + pair_idx dut.trigIn.value = 0 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) # Gap — deassert validIn for 2 wrClk cycles (accumulator resets) dut.validIn.value = 0 for _ in range(2): - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) overflow_seen |= int(dut.overflow.value) # Read side sampling @@ -241,8 +234,7 @@ async def overflow_underflow_quiet_test(dut): # Wait for validOut to deassert for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break @@ -259,8 +251,7 @@ async def overflow_underflow_quiet_test(dut): overflow_seen |= int(dut.overflow.value) # Wait for validOut to deassert before next pair for _ in range(8): - await RisingEdge(dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.rdClk) if int(dut.validOut.value) == 0: break diff --git a/tests/protocols/jesd204b/test_Jesd204bLoopback.py b/tests/protocols/jesd204b/test_Jesd204bLoopback.py index 51b472fe09..b438e287e4 100644 --- a/tests/protocols/jesd204b/test_Jesd204bLoopback.py +++ b/tests/protocols/jesd204b/test_Jesd204bLoopback.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import Event, RisingEdge, Timer +from cocotb.triggers import Event, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster @@ -142,14 +144,12 @@ def __init__(self, dut, l_g: int) -> None: async def axi_cycle(self, n: int = 1) -> None: """Wait n AXI clock cycles with TPD settle.""" for _ in range(n): - await RisingEdge(self.dut.S_AXI_TX_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_TX_ACLK) async def dev_cycle(self, n: int = 1) -> None: """Wait n devClk cycles with TPD settle.""" for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: """Assert both AXI and dev resets, hold, then deassert.""" @@ -213,7 +213,8 @@ async def drive_loopback_link_up( the arrival-phase sweep). Default 0 for the data-integrity test. Returns: - (stop_event, golden_capture) so callers can stop coroutine and tap wire. + (stop_event, loopback_task, golden_capture) so callers can stop and + await the lifetime forwarding agent and tap the captured wire data. """ dut = tb.dut @@ -251,7 +252,7 @@ async def drive_loopback_link_up( if golden_capture is None: golden_capture = [] stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback( dut, l_g, @@ -275,7 +276,15 @@ async def drive_loopback_link_up( # Step 6: wait for all lanes to reach DATA state await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) - return stop_event, golden_capture + return stop_event, loopback_task, golden_capture + + +async def stop_loopback_agent(tb, stop_event, loopback_task) -> None: + """Stop and join a forwarding agent while preserving link settle time.""" + + stop_event.set() + await loopback_task + await tb.dev_cycle(1) # --------------------------------------------------------------------------- @@ -314,7 +323,7 @@ async def test_jesd204b_loopback(dut): # Link-up phase # ----------------------------------------------------------------------- golden_capture = [] - stop_event, golden_capture = await drive_loopback_link_up( + stop_event, loopback_task, golden_capture = await drive_loopback_link_up( tb, l_g, subclass, scr_enable, golden_capture=golden_capture ) @@ -491,8 +500,7 @@ def _endianswap4(x: int) -> int: ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- @@ -524,7 +532,7 @@ async def check_latency_step(axil_rx, *, lane: int = 0) -> int: # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_dlat_sweep(dut): """Full-LMFC-wrap arrival-phase sweep. @@ -554,10 +562,6 @@ async def test_jesd204b_dlat_sweep(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - # Sweep requires Subclass 1 LMFC alignment; SC0 has no deterministic latency - return - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() @@ -569,7 +573,7 @@ async def test_jesd204b_dlat_sweep(dut): # Each iteration starts from a clean elastic buffer (tb.reset() re-asserts # devRst_i which drives s_bufRst via JesdRxLane.vhd:165). # ----------------------------------------------------------------------- - stop_event, _ = await drive_loopback_link_up( + stop_event, loopback_task, _ = await drive_loopback_link_up( tb, l_g, subclass, scr_enable, delay_cycles=d ) @@ -616,8 +620,7 @@ async def test_jesd204b_dlat_sweep(dut): # forwarding coroutine can cause premature SYNC_S → HOLD_S transitions # when the FSM restarts after devRst deasserts. # ----------------------------------------------------------------------- - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # Drive K28.5 (CGS) to all RX GT inputs before reset. This prevents the RX # FSM from seeing stale non-K data during the next drive_loopback_link_up's # setup phase (before the new forwarding coroutine starts), which would @@ -737,7 +740,7 @@ async def _wait_data_valid_drop(tb: Jesd204bLoopbackTB, l_g: int, timeout: int = # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_resync_matrix(dut): """Marked-sample latency invariant across all four resync paths. @@ -760,9 +763,6 @@ async def test_jesd204b_resync_matrix(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - return # requires SC1 deterministic latency - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() @@ -776,7 +776,12 @@ async def test_jesd204b_resync_matrix(dut): # ----------------------------------------------------------------------- # Step 1: Initial link-up + baseline latency measurement # ----------------------------------------------------------------------- - stop_event, _ = await drive_loopback_link_up(tb, l_g, subclass, scr_enable) + stop_event, loopback_task, _ = await drive_loopback_link_up( + tb, + l_g, + subclass, + scr_enable, + ) baseline = await measure_marked_latency(tb, l_g) # ----------------------------------------------------------------------- @@ -785,22 +790,21 @@ async def test_jesd204b_resync_matrix(dut): # ----------------------------------------------------------------------- k28_5_word = 0xBCBCBCBC - async def _restart_link(se): + async def _restart_link(se, task): """Stop coroutine se, drive K28.5 to RX GT inputs, restart coroutine, pulse sysRef.""" - se.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, se, task) for _ln in range(l_g): getattr(dut, f"gtRxData_{_ln}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{_ln}_i").value = 0xF new_se = Event() - cocotb.start_soon( + new_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=new_se) ) await tb.dev_cycle(2) dut.sysRef_i.value = 1 await tb.dev_cycle(16) dut.sysRef_i.value = 0 - return new_se + return new_se, new_task # ----------------------------------------------------------------------- # Step 2: Four resync paths, each at a different LMFC offset @@ -815,8 +819,7 @@ async def _restart_link(se): # devClk edge (forwarding loop writes nSync_TX_i each cycle from nSync_RX_o). await tb.dev_cycle(lmfc_offsets[0]) # Step a1: stop coroutine and drive K28.5 (RX sees K28.5 -> exits DATA_S). - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for _ln in range(l_g): getattr(dut, f"gtRxData_{_ln}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{_ln}_i").value = 0xF @@ -826,7 +829,7 @@ async def _restart_link(se): await tb.dev_cycle(8) # synchronizer latency (~3-4 cycles) # Step a3: restart forwarding coroutine + pulse sysRef to re-link. stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -849,7 +852,7 @@ async def _restart_link(se): await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) # re-enable RX # Restart link: stop/restart coroutine + pulse sysRef (SC1 requires sysRef to advance # from IDLE_S; JesdSyncFsmRx.vhd:189-191). - stop_event = await _restart_link(stop_event) + stop_event, loopback_task = await _restart_link(stop_event, loopback_task) await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) latency_b = await measure_marked_latency(tb, l_g) assert latency_b == baseline, ( @@ -864,7 +867,7 @@ async def _restart_link(se): await _wait_data_valid_drop(tb, l_g) await write_tx_cdc(tb, TX_ENABLE_ADDR, enable_mask) # re-enable TX # SC1 requires sysRef for IDLE_S -> SYSREF_S on both tops. - stop_event = await _restart_link(stop_event) + stop_event, loopback_task = await _restart_link(stop_event, loopback_task) await wait_data_valid_all(dut, l_g, clk=dut.devClk_i, timeout_cycles=4096) latency_c = await measure_marked_latency(tb, l_g) assert latency_c == baseline, ( @@ -882,8 +885,7 @@ async def _restart_link(se): # Step d1: write gtReset bit (CommonCtrl bit 2) to signal the GT reset intent. await write_rx_cdc(tb, RX_COMMON_ADDR, rx_ctrl | (1 << 2)) # Step d2: stop forwarding coroutine so we can manually control rstDone. - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # Step d3: deassert rstDone on all RX GT inputs (simulate GT transceiver reset). # RX FSM DATA_S: gtReady_i=0 -> IDLE_S. for lane in range(l_g): @@ -901,7 +903,7 @@ async def _restart_link(se): # to exit DATA_S and start emitting K28.5, then 4 more cycles for kStable to recover. # Wait 16 cycles before sysRef to ensure kStable=1 when sysRef fires. stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(16) # wait for TX to exit DATA_S and kStable to recover @@ -916,8 +918,7 @@ async def _restart_link(se): ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- @@ -925,7 +926,7 @@ async def _restart_link(se): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_jesd204b_rst02(dut): """Behavior contract: sticky error latches, counter resume, status tracking. @@ -943,9 +944,6 @@ async def test_jesd204b_rst02(dut): subclass = env_int("SUBCLASS", default=1) scr_enable = env_sl("SCR_ENABLE", default=0) - if subclass != 1: - return # requires SC1 for deterministic behavior - tb = Jesd204bLoopbackTB(dut, l_g) await tb.reset() @@ -1005,14 +1003,18 @@ def disp_err_injection_fn(cycle, lane, data, datak): # drive_loopback_link_up starts a coroutine WITHOUT injection_fn; we stop # and restart it so the injection_fn is active for subsequent cycles. # Use the _restart_link helper pattern: stop → K28.5 → new coroutine → sysRef. - stop_event, _ = await drive_loopback_link_up(tb, l_g, subclass, scr_enable) + stop_event, loopback_task, _ = await drive_loopback_link_up( + tb, + l_g, + subclass, + scr_enable, + ) # Stop the existing coroutine; restart with injection_fn (armed=False initially). k28_5_word = 0xBCBCBCBC # STOP coroutine first (so it cannot override the manual nSync_TX_i write). # Then set nSync_TX_i=0 so TX exits DATA_S. Then drive K28.5 so RX exits # DATA_S via kStable. This matches the pattern from the resync matrix path (a). - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF @@ -1020,7 +1022,7 @@ def disp_err_injection_fn(cycle, lane, data, datak): dut.nSync_TX_i.value = 0 # TX DATA_S sees nSync=0 → exits to IDLE_S await tb.dev_cycle(8) # synchronizer latency ~4 cycles, extra margin stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback( dut, l_g, clk=dut.devClk_i, injection_fn=disp_err_injection_fn, @@ -1086,13 +1088,12 @@ def disp_err_injection_fn(cycle, lane, data, datak): # SC1 requires sysRef pulse: stop current coroutine, drive K28.5, restart, pulse sysRef. k28_5_word = 0xBCBCBCBC await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -1137,13 +1138,12 @@ def disp_err_injection_fn(cycle, lane, data, datak): await _wait_data_valid_drop(tb, l_g) # Re-enable and pulse sysRef for SC1 re-link. await write_rx_cdc(tb, RX_ENABLE_ADDR, enable_mask) - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = k28_5_word getattr(dut, f"gtRxDataK_{lane}_i").value = 0xF stop_event = Event() - cocotb.start_soon( + loopback_task = cocotb.start_soon( forward_gt_loopback(dut, l_g, clk=dut.devClk_i, stop_event=stop_event) ) await tb.dev_cycle(2) @@ -1181,8 +1181,7 @@ def disp_err_injection_fn(cycle, lane, data, datak): ) # Stop forwarding coroutine - stop_event.set() - await tb.dev_cycle(2) + await stop_loopback_agent(tb, stop_event, loopback_task) # --------------------------------------------------------------------------- diff --git a/tests/protocols/jesd204b/test_Jesd32bTo16b.py b/tests/protocols/jesd204b/test_Jesd32bTo16b.py index 6139818d95..2f034e6d03 100644 --- a/tests/protocols/jesd204b/test_Jesd32bTo16b.py +++ b/tests/protocols/jesd204b/test_Jesd32bTo16b.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( hdl_parameters_from, @@ -79,25 +81,21 @@ async def reset(self) -> None: self.dut.wrRst.value = 1 self.dut.rdRst.value = 1 for _ in range(6): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.wrRst.value = 0 for _ in range(4): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) self.dut.rdRst.value = 0 # Quiet settling time after reset deassertion for _ in range(4): - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) async def write_word(self, data: int, trig: int) -> None: """Drive one 32-bit word with the given trig[1:0] value on the write side.""" self.dut.validIn.value = 1 self.dut.dataIn.value = data self.dut.trigIn.value = trig - await RisingEdge(self.dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.wrClk) self.dut.validIn.value = 0 await Timer(1, unit="ns") @@ -108,8 +106,7 @@ async def wait_valid_out(self) -> None: valid signal — so we poll rdClk rising edges after Timer(1, "ns"). """ for _ in range(_VALID_TIMEOUT_CYCLES): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 1: return raise AssertionError( @@ -119,8 +116,7 @@ async def wait_valid_out(self) -> None: async def wait_valid_deassert(self, max_cycles: int = 16) -> None: """Wait until validOut deasserts (bounded).""" for _ in range(max_cycles): - await RisingEdge(self.dut.rdClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.rdClk) if int(self.dut.validOut.value) == 0: return @@ -228,13 +224,11 @@ async def overflow_underflow_quiet_test(dut): dut.validIn.value = 1 dut.dataIn.value = 0xA000_0000 + word_idx dut.trigIn.value = 0b01 - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) # Gap: deassert validIn for 2 cycles dut.validIn.value = 0 for _ in range(2): - await RisingEdge(dut.wrClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.wrClk) overflow_seen |= int(dut.overflow.value) # Collect the two output halves diff --git a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py index eb1401ec73..d866ae7c72 100644 --- a/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py +++ b/tests/protocols/jesd204b/test_JesdAlignFrRepCh.py @@ -35,7 +35,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -98,7 +99,7 @@ def _init_dut(dut, *, scr_enable: int) -> JesdTB: # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 0) async def test_char03_restore_nonscrambled(dut): """Non-scrambled: original data recovered, charisk cleared (§5.3.3.4.2). @@ -110,11 +111,6 @@ async def test_char03_restore_nonscrambled(dut): Original data recovered, no residual control chars. """ f = env_int("F_G", default=2) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # Non-scrambled path only for scr=0 - if scr_enable != 0: - return tb = _init_dut(dut, scr_enable=0) await tb.reset() @@ -133,8 +129,7 @@ async def test_char03_restore_nonscrambled(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) @@ -177,7 +172,7 @@ async def test_char03_restore_nonscrambled(dut): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 1) async def test_char03_restore_scrambled(dut): """Scrambled: original data recovered, charisk cleared (§5.3.3.4.3). @@ -192,11 +187,6 @@ async def test_char03_restore_scrambled(dut): Original data recovered, no residual control chars. """ f = env_int("F_G", default=2) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # Scrambled path only for scr=1 - if scr_enable != 1: - return tb = _init_dut(dut, scr_enable=1) await tb.reset() @@ -213,8 +203,7 @@ async def test_char03_restore_scrambled(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) @@ -263,8 +252,7 @@ async def test_char03_align_error(dut): for _ in range(4): dut.dataRx_i.value = 0x11111111 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Inject a K-char (K28.5 = 0xBC) at byte 0 — K28.5 is not /F/ or /A/, # so it will NOT be consumed by char restoration → residual charisk → alignErr. @@ -273,8 +261,7 @@ async def test_char03_align_error(dut): for _ in range(4): dut.dataRx_i.value = k_word dut.chariskRx_i.value = 0x1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # alignErr_o is combinatorial — sample it immediately after settle assert int(dut.alignErr_o.value) == 1, ( @@ -310,8 +297,7 @@ async def test_char03_position_error(dut): for _ in range(4): dut.dataRx_i.value = 0x55555555 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Drive alignFrame_i=1 with K_CHAR at byte 3 only (data[31:24] = K_CHAR) # and chariskRx_i=0x8 (bit 3 set for byte 3). @@ -322,16 +308,14 @@ async def test_char03_position_error(dut): dut.alignFrame_i.value = 1 dut.dataRx_i.value = illegal_word dut.chariskRx_i.value = 0x8 # only byte 3 flagged - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.alignFrame_i.value = 0 # positionErr_o is combinatorial from r.position. # After one more clock, r.position = 0xF → positionErr_o = 1. dut.dataRx_i.value = 0x55555555 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert int(dut.positionErr_o.value) == 1, ( "positionErr: expected positionErr_o=1 after alignFrame with " @@ -362,9 +346,9 @@ async def test_char03_random_soak(dut): await tb.reset() # Fixed seed for reproducibility - random.seed(0xC0C0_BABE) + rng = random.Random(0xC0C0_BABE) n_words = 64 - stimulus = [random.randint(0, 0xFFFFFFFF) for _ in range(n_words)] + stimulus = [rng.randint(0, 0xFFFFFFFF) for _ in range(n_words)] golden = predict_char_restoration(stimulus, f=f, scr=bool(scr_enable), lfsr_init=0) @@ -376,8 +360,7 @@ async def test_char03_random_soak(dut): for cycle_i in range(total_cycles): dut.dataRx_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 dut.chariskRx_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if cycle_i >= _LATENCY: got_raw.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) diff --git a/tests/protocols/jesd204b/test_JesdIlasGen.py b/tests/protocols/jesd204b/test_JesdIlasGen.py index 49cc99353a..b27699e19e 100644 --- a/tests/protocols/jesd204b/test_JesdIlasGen.py +++ b/tests/protocols/jesd204b/test_JesdIlasGen.py @@ -36,7 +36,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -102,8 +103,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=2048): """Bounded poll for signal == value; raise AssertionError on timeout.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -133,8 +133,7 @@ async def capture_ilas_words(dut, tb, *, k, f, num_mf=4): # together with the first pulse is an alignment the real FSM never # produces (post-merge reconciliation of plans 03-03/03-04). dut.lmfc_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.ilas_i.value = 1 dut.lmfc_i.value = 0 @@ -151,8 +150,7 @@ async def capture_ilas_words(dut, tb, *, k, f, num_mf=4): else: dut.lmfc_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) data = int(dut.ilasData_o.value) datak = int(dut.ilasK_o.value) diff --git a/tests/protocols/jesd204b/test_JesdLmfcGen.py b/tests/protocols/jesd204b/test_JesdLmfcGen.py index 1a8a469ce8..b5741dc179 100644 --- a/tests/protocols/jesd204b/test_JesdLmfcGen.py +++ b/tests/protocols/jesd204b/test_JesdLmfcGen.py @@ -27,6 +27,8 @@ import pytest from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.common.regression_utils import ( env_int, parameter_case, @@ -79,11 +81,9 @@ async def test_period(dut): # Align: assert SYSREF rising edge with nSync_i='0' dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # deassert after one cycle # measure_lmfc_period waits for the first lmfc_o pulse then counts to the @@ -103,9 +103,9 @@ async def test_period(dut): @cocotb.test() async def test_sysref_realign_clause_a(dut): - """SYSREF-gating clause (a): SYSREF edge while nSync_i='0' realigns the counter. + """Real-time timing: measure SYSREF realignment at explicit sim-time offsets. - Verifies: + SYSREF-gating clause (a) verifies: - sysrefRe_o='1' exactly 1 cc after the SYSREF rising edge - lmfc_o='0' at that same cycle (not yet) - lmfc_o='1' exactly 2 cc after the SYSREF rising edge (off-by-one guard) @@ -191,16 +191,13 @@ async def test_sysref_gate_clause_b(dut): # --- Align with nSync_i='0' --- dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Wait for the alignment lmfc pulse (2 cc after edge) - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Now lmfc_o should be high (cycle N+2) # Let the counter free-run; measure period to confirm alignment @@ -216,8 +213,7 @@ async def test_sysref_gate_clause_b(dut): await tb.cycle(half) # Inject SYSREF rising edge (nSync_i='1' → counter should NOT reset) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Measure the period after the gated edge — should be unchanged @@ -248,16 +244,13 @@ async def test_sysref_phase_neutral_clause_c(dut): # --- Initial alignment --- dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 # Wait for the alignment lmfc pulse - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) # Let it free-run for exactly one full period so we know the phase measured_baseline = await measure_lmfc_period(dut, clk=dut.clk) @@ -270,13 +263,11 @@ async def test_sysref_phase_neutral_clause_c(dut): # cycles away. Inject SYSREF at that same moment (nSync_i='0'). # Wait for the next lmfc_o pulse while simultaneously injecting SYSREF. for _ in range(512): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) if dut.lmfc_o.value == 1: # We are at a period boundary — inject SYSREF now dut.sysref_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 0 break else: @@ -292,9 +283,10 @@ async def test_sysref_phase_neutral_clause_c(dut): @cocotb.test() async def test_sysref_re_pulse_clause_d(dut): - """SYSREF-gating clause (d): sysrefRe_o is a single-cycle pulse on every SYSREF rising edge. + """Real-time timing: sample the SYSREF pulse away from TPD boundaries. - Verifies the single-cycle pulse in both nSync states (gated and active). + SYSREF-gating clause (d) verifies the single-cycle pulse in both nSync + states (gated and active). """ dut.nSync_i.value = 1 dut.sysref_i.value = 0 @@ -307,8 +299,7 @@ async def test_sysref_re_pulse_clause_d(dut): # Sample sysrefRe_o mid-cycle (Timer(6ns) past the rising edge) to avoid the # TPD=1ns boundary race at RisingEdge+1ns when r.sysrefRe transitions. dut.nSync_i.value = 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge (cycle N) # Cycle N+1: sample 6ns into the clock cycle — solidly in the sysrefRe='1' window await RisingEdge(dut.clk) # latch at edge N+1 (r.sysrefRe = 1 registered) @@ -329,8 +320,7 @@ async def test_sysref_re_pulse_clause_d(dut): # --- Test with nSync_i='1' (gated — sysrefRe_o still pulses) --- dut.nSync_i.value = 1 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.sysref_i.value = 1 # rising edge (cycle N) # Cycle N+1: sample mid-cycle — sysrefRe_o should be high (pulse regardless of nSync) await RisingEdge(dut.clk) diff --git a/tests/protocols/jesd204b/test_JesdRxLane.py b/tests/protocols/jesd204b/test_JesdRxLane.py index 41a0511ec8..d244dd1f49 100644 --- a/tests/protocols/jesd204b/test_JesdRxLane.py +++ b/tests/protocols/jesd204b/test_JesdRxLane.py @@ -52,7 +52,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -186,8 +187,7 @@ def __init__(self, dut) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, cycles: int = 4) -> None: """Assert devRst_i for cycles clock cycles, then deassert.""" @@ -205,8 +205,7 @@ async def reset(self, cycles: int = 4) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): """Wait up to timeout_cycles for signal to equal value (1 ns settle).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -217,8 +216,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 128): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( @@ -401,8 +399,7 @@ async def drive_timeline( dut.gtRxData_i.value = 0 dut.gtRxDataK_i.value = 0 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) if cycle_i >= _LATENCY: got_samples.append(int(dut.sampleData_o.value) & _GT_WORD_MASK) diff --git a/tests/protocols/jesd204b/test_JesdRxReg.py b/tests/protocols/jesd204b/test_JesdRxReg.py index 4a2a2ff897..bef0ed6ed4 100644 --- a/tests/protocols/jesd204b/test_JesdRxReg.py +++ b/tests/protocols/jesd204b/test_JesdRxReg.py @@ -37,7 +37,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp @@ -127,13 +129,11 @@ def __init__(self, dut, l_g: int) -> None: async def axi_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def dev_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: self.dut.S_AXI_ARESETN.value = 0 @@ -183,8 +183,7 @@ async def assert_decerr(axil_master: AxiLiteMaster, address: int) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 128): """Wait up to timeout_cycles for signal to equal value.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -271,8 +270,7 @@ async def drive_rx_link_up( for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = data_32b getattr(dut, f"gtRxDataK_{lane}_i").value = datak_4b - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Enable scrEnable if scrambled link-up: write commonCtrl bit5=1 if scr: @@ -287,8 +285,7 @@ async def drive_rx_link_up( for lane in range(l_g): getattr(dut, f"gtRxData_{lane}_i").value = data_32b getattr(dut, f"gtRxDataK_{lane}_i").value = datak_4b - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) data_cycle_count += 1 # Keep driving last data word while waiting for dataValid to assert @@ -514,8 +511,7 @@ async def measure_sysref_offset(delay_val: int) -> int: offset = 0 max_cycles = 600 for _ in range(max_cycles): - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) offset += 1 if int(dut.sysRefDbg_o.value) == 1: break diff --git a/tests/protocols/jesd204b/test_JesdScramblerWrapper.py b/tests/protocols/jesd204b/test_JesdScramblerWrapper.py index a0e1c2e76c..e3f57fe1c3 100644 --- a/tests/protocols/jesd204b/test_JesdScramblerWrapper.py +++ b/tests/protocols/jesd204b/test_JesdScramblerWrapper.py @@ -28,7 +28,7 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import parameter_case, run_surf_vhdl_test from tests.protocols.jesd204b.jesd204b_test_utils import ( @@ -133,8 +133,7 @@ async def run_scrambler_pattern( total_cycles = n + _DRAIN for i in range(total_cycles): dut.sampleData_i.value = input_words[i] if i < n else 0 - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) raw_tx.append(int(dut.txData_o.value)) if int(dut.rxValid_o.value) == 1: raw_rx.append(int(dut.rxData_o.value)) @@ -212,8 +211,7 @@ async def _run_pattern( # Assertion 3: Error outputs deasserted during steady-state data # ----------------------------------------------------------------------- # Sample once after settling - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) assert int(dut.rxAlignErr_o.value) == 0, ( f"[{case_name}] rxAlignErr_o unexpectedly asserted in steady state" ) @@ -256,8 +254,7 @@ async def scrambler_known_answer_vectors(dut): """Known-answer assertion: ties the bench to hand-computed LFSR anchors.""" dut.rst.value = 1 cocotb.start_soon(Clock(dut.clk, 10.0, unit="ns").start()) - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for input_word, lfsr_init, expected_scrambled in KNOWN_ANSWER_VECTORS: diff --git a/tests/protocols/jesd204b/test_JesdSyncFsmRx.py b/tests/protocols/jesd204b/test_JesdSyncFsmRx.py index 4ff7b1eb3e..5332975e5b 100644 --- a/tests/protocols/jesd204b/test_JesdSyncFsmRx.py +++ b/tests/protocols/jesd204b/test_JesdSyncFsmRx.py @@ -28,7 +28,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -66,8 +67,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=32): """Wait up to timeout_cycles for signal to reach value (1ns settle after each edge).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if signal.value == value: return raise AssertionError( diff --git a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py index 3099df4245..d5110151bc 100644 --- a/tests/protocols/jesd204b/test_JesdSyncFsmTx.py +++ b/tests/protocols/jesd204b/test_JesdSyncFsmTx.py @@ -22,7 +22,8 @@ import cocotb import pytest -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -52,8 +53,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles=32): """Wait up to timeout_cycles for signal to reach value (1ns settle after each edge).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if signal.value == value: return raise AssertionError( @@ -131,7 +131,7 @@ async def startup_sc0(dut, tb): # Test 1 (SC1): Subclass-1 startup and E3 exit # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 1) async def test_cgs01_subclass1(dut): """Code-group-sync (SC1): IDLE->SYNC->ILA->DATA for Subclass 1 with SYSREF gate. @@ -146,10 +146,6 @@ async def test_cgs01_subclass1(dut): checks use wait_for_signal or are done after bounded settling. """ num_mf = env_int("NUM_ILAS_MF_G", default=4) - subclass = env_int("SUBCLASS", default=1) - - if subclass != 1: - return dut.enable_i.setimmediatevalue(0) dut.nSync_i.setimmediatevalue(0) @@ -203,7 +199,7 @@ async def test_cgs01_subclass1(dut): # Test 2 (SC0): Subclass-0 startup and E3 exit # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_int("SUBCLASS", default=1) != 0) async def test_cgs01_subclass0(dut): """Code-group-sync (SC0): IDLE->SYNC->ILA->DATA for Subclass 0 (no SYSREF). @@ -214,10 +210,6 @@ async def test_cgs01_subclass0(dut): - NUM_ILAS_MF_G-1 pulses leaves FSM in ILA_S (off-by-one guard). """ num_mf = env_int("NUM_ILAS_MF_G", default=4) - subclass = env_int("SUBCLASS", default=1) - - if subclass != 0: - return dut.enable_i.setimmediatevalue(0) dut.nSync_i.setimmediatevalue(0) diff --git a/tests/protocols/jesd204b/test_JesdTxLane.py b/tests/protocols/jesd204b/test_JesdTxLane.py index a7e1321bfe..f26b68889b 100644 --- a/tests/protocols/jesd204b/test_JesdTxLane.py +++ b/tests/protocols/jesd204b/test_JesdTxLane.py @@ -36,7 +36,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_int, @@ -122,8 +123,7 @@ def __init__(self, dut) -> None: async def cycle(self, count: int = 1) -> None: """Advance count clock cycles, settling 1 ns after each rising edge.""" for _ in range(count): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, cycles: int = 4) -> None: """Assert devRst_i for `cycles` clock cycles, then deassert.""" @@ -141,8 +141,7 @@ async def reset(self, cycles: int = 4) -> None: async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 64): """Wait up to timeout_cycles for signal to equal value (1 ns settle).""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(signal.value) == value: return raise AssertionError( @@ -153,8 +152,7 @@ async def wait_for_signal(signal, *, value, clk, timeout_cycles: int = 64): async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 64): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( @@ -273,21 +271,18 @@ async def _capture_ilas_stream( for _mf in range(_NUM_MF_CAPTURE): # Fire LMFC (1-cycle pulse): advances to T_lmfc dut.lmfc_i.value = 1 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) dut.lmfc_i.value = 0 # Advance 1 extra clock to T+1 (lmfcD1 cycle), matching standalone bench. - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Collect one multiframe (k*f/4 GT words) starting from T+1. for _ in range(k * f // 4): data = int(dut.gtTxData_o.value) & _GT_WORD_MASK datak = int(dut.gtTxDataK_o.value) & _K4_MASK raw_words.append((data, datak)) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Fire the 4th LMFC to complete the ILA sequence (FSM exits ILA_S after this). # Collect 2 extra words AFTER firing LMFC4 so that the lmfcD1 cycle of LMFC4 @@ -296,21 +291,18 @@ async def _capture_ilas_stream( # After LMFC4, the FSM transitions to DATA_S; the 2 extra words use the DATA # path (not ILAS), but they are outside the aligned 3k-word window. dut.lmfc_i.value = 1 - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) dut.lmfc_i.value = 0 # Advance 1 extra clock (lmfcD1 cycle of LMFC4 = /A/ of MF2) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Collect 2 extra words (includes lmfcD1 = /A/, and lmfcD2 = /R/ before DATA mux) for _ in range(2): data = int(dut.gtTxData_o.value) & _GT_WORD_MASK datak = int(dut.gtTxDataK_o.value) & _K4_MASK raw_words.append((data, datak)) - await RisingEdge(dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(dut.devClk_i) # Find the first /R/ to align with the golden model start_offset = None @@ -588,7 +580,7 @@ async def _enter_data_phase( # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 0) async def test_char01_nonscrambled(dut): """Char replacement: /F/ and /A/ substitution for non-scrambled links. @@ -607,11 +599,6 @@ async def test_char01_nonscrambled(dut): k = env_int("K_G", default=32) f = env_int("F_G", default=2) subclass = env_int("SUBCLASS", default=1) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # char replacement only meaningful for non-scrambled cases - if scr_enable != 0: - return tb = TxLaneTB(dut) await tb.reset() @@ -661,8 +648,7 @@ async def test_char01_nonscrambled(dut): for cycle_i in range(total_cycles): dut.lmfc_i.value = 0 dut.sampleData_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 - await RisingEdge(tb.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(tb.dut.devClk_i) if cycle_i >= _LATENCY: got_data = int(dut.gtTxData_o.value) & _GT_WORD_MASK @@ -699,7 +685,7 @@ async def test_char01_nonscrambled(dut): # --------------------------------------------------------------------------- -@cocotb.test() +@cocotb.test(skip=env_sl("SCR_ENABLE", default=0) != 1) async def test_char02_scrambled(dut): """Char replacement: /F/ and /A/ substitution for scrambled links. @@ -715,11 +701,6 @@ async def test_char02_scrambled(dut): k = env_int("K_G", default=32) f = env_int("F_G", default=2) subclass = env_int("SUBCLASS", default=1) - scr_enable = env_sl("SCR_ENABLE", default=0) - - # char replacement only meaningful for scrambled cases - if scr_enable != 1: - return tb = TxLaneTB(dut) await tb.reset() @@ -764,8 +745,7 @@ async def test_char02_scrambled(dut): for cycle_i in range(total_cycles): dut.lmfc_i.value = 0 dut.sampleData_i.value = stimulus[cycle_i] if cycle_i < n_words else 0 - await RisingEdge(tb.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(tb.dut.devClk_i) if cycle_i >= _LATENCY: got_data = int(dut.gtTxData_o.value) & _GT_WORD_MASK diff --git a/tests/protocols/jesd204b/test_JesdTxReg.py b/tests/protocols/jesd204b/test_JesdTxReg.py index 3b50dae44f..655f4b2fe9 100644 --- a/tests/protocols/jesd204b/test_JesdTxReg.py +++ b/tests/protocols/jesd204b/test_JesdTxReg.py @@ -28,7 +28,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -113,13 +115,11 @@ def __init__(self, dut) -> None: async def axi_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def dev_cycle(self, n: int = 1) -> None: for _ in range(n): - await RisingEdge(self.dut.devClk_i) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.devClk_i) async def reset(self, axi_cycles: int = 8, dev_cycles: int = 8) -> None: self.dut.S_AXI_ARESETN.value = 0 @@ -156,8 +156,7 @@ async def assert_decerr(axil_master, address: int) -> None: async def wait_for_bit(status_signal, *, bit_mask: int, clk, timeout_cycles: int = 256): """Wait until (status_signal & bit_mask) != 0.""" for _ in range(timeout_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if (int(status_signal.value) & bit_mask) != 0: return raise AssertionError( diff --git a/tests/protocols/line_codes/line_code_test_utils.py b/tests/protocols/line_codes/line_code_test_utils.py index a47b111134..80443bc952 100644 --- a/tests/protocols/line_codes/line_code_test_utils.py +++ b/tests/protocols/line_codes/line_code_test_utils.py @@ -16,6 +16,8 @@ from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.common.regression_utils import ( env_flag, env_sl, @@ -100,7 +102,6 @@ def run_line_code_integration_test( *, test_file: str, toplevel: str, - tb_source: str, parameters: dict[str, object], ) -> None: run_surf_vhdl_test( @@ -108,7 +109,6 @@ def run_line_code_integration_test( toplevel=toplevel, parameters=hdl_parameters_from(parameters), extra_env=parameters, - extra_vhdl_sources={"surf": [tb_source]}, ) @@ -194,8 +194,7 @@ async def drive_integration_symbol(dut, *, data_in: int, data_k_in: int) -> None for _ in range(INTEGRATION_VALID_OUT_TIMEOUT_CYCLES): if int(dut.validOut.value) == 1: return - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) raise AssertionError( f"Timed out waiting for validOut after {INTEGRATION_VALID_OUT_TIMEOUT_CYCLES} cycles" diff --git a/tests/protocols/line_codes/test_LineCode10b12bIntegration.py b/tests/protocols/line_codes/test_LineCode10b12bIntegration.py index c4bcfd8a91..fd4f4f6730 100644 --- a/tests/protocols/line_codes/test_LineCode10b12bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode10b12bIntegration.py @@ -47,6 +47,5 @@ def test_LineCode10b12bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode10b12btb", - tb_source="protocols/line-codes/tb/LineCode10b12bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/line_codes/test_LineCode12b14bIntegration.py b/tests/protocols/line_codes/test_LineCode12b14bIntegration.py index 6ab523f55d..233bd744d1 100644 --- a/tests/protocols/line_codes/test_LineCode12b14bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode12b14bIntegration.py @@ -50,6 +50,5 @@ def test_LineCode12b14bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode12b14btb", - tb_source="protocols/line-codes/tb/LineCode12b14bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/line_codes/test_LineCode8b10bIntegration.py b/tests/protocols/line_codes/test_LineCode8b10bIntegration.py index e10eeb64e0..8bb08447f6 100644 --- a/tests/protocols/line_codes/test_LineCode8b10bIntegration.py +++ b/tests/protocols/line_codes/test_LineCode8b10bIntegration.py @@ -59,6 +59,5 @@ def test_LineCode8b10bIntegration(parameters): run_line_code_integration_test( test_file=__file__, toplevel="surf.linecode8b10btb", - tb_source="protocols/line-codes/tb/LineCode8b10bTb.vhd", parameters=parameters, ) diff --git a/tests/protocols/packetizer/packetizer_test_utils.py b/tests/protocols/packetizer/packetizer_test_utils.py index 53c824125b..811a4709a5 100644 --- a/tests/protocols/packetizer/packetizer_test_utils.py +++ b/tests/protocols/packetizer/packetizer_test_utils.py @@ -15,9 +15,10 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer from tests.axi.utils import wait_sampled_ready +from tests.common.regression_utils import sample_after_tpd PACKETIZER2_VERSION = 0x2 PACKETIZER2_CRC_NONE = 0x0 @@ -99,8 +100,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 128) -> AxisBeat: await Timer(1, unit="ns") if int(self._sig("TVALID").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TVALID").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -108,8 +108,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 128) -> AxisBeat: async def recv(self, *, clk, keep_ready: bool = False) -> AxisBeat: self._sig("TREADY").value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if not keep_ready: self._sig("TREADY").value = 0 return beat @@ -121,8 +120,7 @@ def start_packetizer_clock(dut, *, period_ns: float = 5.0) -> None: async def cycle(clk, count: int = 1) -> None: for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_packetizer_dut(dut, *, cycles: int = 4) -> None: @@ -136,8 +134,7 @@ async def wait_debug_init_done(dut, *, timeout_cycles: int = 64) -> None: for _ in range(timeout_cycles): if int(dut.debugOut.value) & (1 << DEBUG_INIT_DONE): return - await RisingEdge(dut.axisClk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.axisClk) raise AssertionError("Timed out waiting for depacketizer initDone") @@ -356,8 +353,7 @@ async def assert_no_output( if drive_ready: endpoint._sig("TREADY").value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert int(endpoint._sig("TVALID").value) == 0 if drive_ready: endpoint._sig("TREADY").value = 0 @@ -404,16 +400,14 @@ async def send_unpaced_beats(endpoint: FlatAxisEndpoint, beats: list[AxisBeat], # cadence. for beat in beats: endpoint.drive(beat) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint.set_idle() async def recv_valid_pulses(endpoint: FlatAxisEndpoint, count: int, *, clk) -> list[AxisBeat]: beats = [] while len(beats) < count: - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TVALID").value): beats.append(endpoint.snapshot()) return beats @@ -458,12 +452,10 @@ async def recv_beats_with_backpressure( for _ in range(count): beat = await endpoint.wait_valid(clk=clk) for _ in range(hold_cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) assert endpoint.snapshot() == beat endpoint._sig("TREADY").value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) endpoint._sig("TREADY").value = 0 beats.append(beat) return beats diff --git a/tests/protocols/pgp/axil_test_utils.py b/tests/protocols/pgp/axil_test_utils.py index 47bb344060..d31b284932 100644 --- a/tests/protocols/pgp/axil_test_utils.py +++ b/tests/protocols/pgp/axil_test_utils.py @@ -12,10 +12,9 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiLiteBus, AxiLiteMaster -from tests.common.regression_utils import start_lockstep_clocks +from tests.common.regression_utils import sample_after_tpd, start_lockstep_clocks class PgpAxiLiteTb: @@ -76,10 +75,9 @@ async def cycle(self, count: int = 1): """Advance the bench by a whole number of visible wrapper clock edges.""" for _ in range(count): - await RisingEdge(self.cycle_clk) # Most SURF RTL uses the default `TPD_G => 1 ns`, so the tests wait # a small amount after every edge before sampling outputs. - await Timer(1, unit="ns") + await sample_after_tpd(self.cycle_clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 8): """Drive every declared reset signal through its active and idle state.""" diff --git a/tests/protocols/pgp/pgp2_test_utils.py b/tests/protocols/pgp/pgp2_test_utils.py index 3bed6a5a0e..ecb8fe2340 100644 --- a/tests/protocols/pgp/pgp2_test_utils.py +++ b/tests/protocols/pgp/pgp2_test_utils.py @@ -12,7 +12,7 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd from tests.base.crc.crc_test_utils import crc_out_from_remainder, crc_update @@ -51,8 +51,7 @@ async def cycle(self, count: int = 1): # Sample one nanosecond after each edge so registered outputs have # time to reflect the DUT's default `TPD_G`. for _ in range(count): - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 4): # Using an explicit reset coroutine keeps every test's startup sequence diff --git a/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py b/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py index e3b6033737..8b1dc9aadd 100644 --- a/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py +++ b/tests/protocols/pgp/pgp2b/test_Pgp2bCoreWrappers.py @@ -12,7 +12,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -23,14 +24,12 @@ async def pgp2b_core_wrapper_elab_test(dut): dut.rst.setimmediatevalue(1) for _ in range(4): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for _ in range(16): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) PARAMETER_SWEEP = [ diff --git a/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py b/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py index 0820ebd70d..5088fcd07d 100644 --- a/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py +++ b/tests/protocols/pgp/pgp2fc/test_Pgp2fcCoreWrappers.py @@ -12,7 +12,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -23,14 +24,12 @@ async def pgp2fc_core_wrapper_elab_test(dut): dut.rst.setimmediatevalue(1) for _ in range(4): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) dut.rst.value = 0 for _ in range(16): - await RisingEdge(dut.clk) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clk) PARAMETER_SWEEP = [ diff --git a/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py b/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py index 2e1b3239d4..ddebbd47b8 100644 --- a/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py +++ b/tests/protocols/pgp/pgp2fc/test_Pgp2fcTxFixedLatency.py @@ -23,6 +23,7 @@ import cocotb +from tests.axi.utils import wait_sampled_ready from tests.protocols.pgp.pgp2_test_utils import K_FCD, PgpModuleTB, signal_int, wait_for_signal from tests.protocols.pgp.pgp_test_utils import pgp_family_sources, run_pgp_wrapper_test @@ -48,10 +49,7 @@ async def drive_frame_word( tb.dut.vc0FrameEofe.value = eofe tb.dut.vc0FrameValid.value = 1 - while True: - await tb.cycle() - if signal_int(tb.dut, "vc0FrameReady") == 1: - break + await wait_sampled_ready(tb.dut.vc0FrameReady, clk=tb.clk) tb.dut.vc0FrameValid.value = 0 tb.dut.vc0FrameSof.value = 0 diff --git a/tests/protocols/pgp/pgp4/pgp4_test_utils.py b/tests/protocols/pgp/pgp4/pgp4_test_utils.py index 3b4c78c630..d067d00fb1 100644 --- a/tests/protocols/pgp/pgp4/pgp4_test_utils.py +++ b/tests/protocols/pgp/pgp4/pgp4_test_utils.py @@ -12,7 +12,8 @@ import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.axi.utils import wait_sampled_ready @@ -47,8 +48,7 @@ def __init__(self, dut, *, clk_name: str = "clk", rst_name: str = "rst"): async def cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) async def reset(self, *, hold_cycles: int = 4, settle_cycles: int = 4): self.rst.setimmediatevalue(1) diff --git a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py index 611a962aad..f7811a2e47 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4RxEb.py @@ -27,7 +27,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import ( env_flag, @@ -59,20 +61,22 @@ def __init__(self, dut): self.phy_period_ns = env_float("PHY_CLK_PERIOD_NS", default=4.0) self.pgp_period_ns = env_float("PGP_CLK_PERIOD_NS", default=4.125) if env_flag("COMMON_CLK", default=False): - start_lockstep_clocks(dut.phyClk, dut.pgpClk, period_ns=self.phy_period_ns) + self._clock_task = start_lockstep_clocks( + dut.phyClk, + dut.pgpClk, + period_ns=self.phy_period_ns, + ) else: cocotb.start_soon(Clock(dut.phyClk, self.phy_period_ns, unit="ns").start()) cocotb.start_soon(Clock(dut.pgpClk, self.pgp_period_ns, unit="ns").start()) async def cycle_phy(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.phyClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.phyClk) async def cycle_pgp(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.pgpClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.pgpClk) async def sample_pgp_cycle(self): await FallingEdge(self.dut.pgpClk) @@ -95,8 +99,11 @@ def __init__(self, dut, signal_name: str, *, step): self.signal_name = signal_name self.step = step self.seen = False + # Lifetime observer retained by its monitor owner. + self.task = cocotb.start_soon(self.run()) async def run(self): + """Lifetime agent: observe pulses until cocotb ends the test.""" while True: await self.step() if signal_int(self.dut, self.signal_name) == 1: @@ -112,8 +119,11 @@ def __init__(self, dut, *, step, valid_name: str, field_names: tuple[str, ...]): self.valid_name = valid_name self.field_names = field_names self.beats: list[tuple[int, ...]] = [] + # Lifetime observer retained by its collector owner. + self.task = cocotb.start_soon(self.run()) async def run(self): + """Lifetime agent: collect valid beats until cocotb ends the test.""" while True: await self.step() if signal_int(self.dut, self.valid_name) == 1: @@ -204,12 +214,14 @@ async def assert_no_output_words(tb: Pgp4RxEbTB, *, cycles: int): assert signal_int(tb.dut, "pgpRxValid") == 0 -@cocotb.test() +@cocotb.test( + skip=( + env_flag("EXPECT_SKIP_DISABLED", default=False) + or env_flag("EXPECT_OVERFLOW", default=False) + ), +) async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False) or env_flag("EXPECT_OVERFLOW", default=False): - return - initialize_phy_inputs(dut) await tb.reset() @@ -222,7 +234,6 @@ async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): valid_name="pgpRxValid", field_names=("pgpRxHeader", "pgpRxData"), ) - cocotb.start_soon(collector.run()) data_word_a = 0x0123456789ABCDEF idle_word = pgp4_idle_word(rem_link_ready=1, pause_mask=0x1234, overflow_mask=0x00A5) @@ -247,12 +258,9 @@ async def pgp4_rx_eb_filters_skip_and_preserves_stream_order(dut): ] await wait_for_signal_in_domain(tb, "remLinkData", value=skip_data, cycles=64) -@cocotb.test() +@cocotb.test(skip=env_flag("EXPECT_SKIP_DISABLED", default=False)) async def pgp4_rx_eb_reset_flushes_buffered_words(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() @@ -272,23 +280,21 @@ async def pgp4_rx_eb_reset_flushes_buffered_words(dut): assert words == [(PGP4_D_HEADER, marker_word)] -@cocotb.test() +@cocotb.test( + skip=( + env_flag("EXPECT_SKIP_DISABLED", default=False) + or not env_flag("EXPECT_OVERFLOW", default=False) + ), +) async def pgp4_rx_eb_overflow_pulses_when_phy_outpaces_local_clock(dut): tb = Pgp4RxEbTB(dut) - if env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() # Overflow is not expected under the slight-drift case, so keep the normal # regression realistic and only execute the deep fill test when the pytest # parameter sweep requests the explicit overflow-stress clock ratio. - if not env_flag("EXPECT_OVERFLOW", default=False): - return - overflow_monitor = PulseMonitor(dut, "overflow", step=tb.cycle_pgp) - cocotb.start_soon(overflow_monitor.run()) # The DUT uses a 512-entry async FIFO. With the write clock much faster # than the read clock, a sustained burst should eventually overrun that @@ -300,12 +306,9 @@ async def pgp4_rx_eb_overflow_pulses_when_phy_outpaces_local_clock(dut): assert overflow_monitor.seen -@cocotb.test() +@cocotb.test(skip=not env_flag("EXPECT_SKIP_DISABLED", default=False)) async def pgp4_rx_eb_skip_disabled_passes_stream_and_link_error(dut): tb = Pgp4RxEbTB(dut) - if not env_flag("EXPECT_SKIP_DISABLED", default=False): - return - initialize_phy_inputs(dut) await tb.reset() @@ -316,8 +319,6 @@ async def pgp4_rx_eb_skip_disabled_passes_stream_and_link_error(dut): field_names=("pgpRxHeader", "pgpRxData"), ) link_error_monitor = PulseMonitor(dut, "linkError", step=tb.sample_pgp_cycle) - cocotb.start_soon(collector.run()) - cocotb.start_soon(link_error_monitor.run()) data_word = 0x123456789ABCDEF0 await send_phy_word(tb, header=PGP4_D_HEADER, data=data_word, link_error=1) diff --git a/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py b/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py index c0834d3201..97fe8e0cb4 100644 --- a/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py +++ b/tests/protocols/pgp/pgp4/test_Pgp4TxLite.py @@ -50,6 +50,5 @@ def test_Pgp4TxLite(parameters): run_pgp_wrapper_test( test_file=__file__, toplevel="surf.pgp4txlitewrapper", - wrapper_source="protocols/pgp/pgp4/core/rtl/Pgp4TxLiteWrapper.vhd", extra_env=parameters, ) diff --git a/tests/protocols/pgp/pgp_test_utils.py b/tests/protocols/pgp/pgp_test_utils.py index 3e4ee49b86..37bf3f0f38 100644 --- a/tests/protocols/pgp/pgp_test_utils.py +++ b/tests/protocols/pgp/pgp_test_utils.py @@ -111,12 +111,12 @@ def run_pgp_wrapper_test( *, test_file: str, toplevel: str, - wrapper_source: str, + wrapper_source: str | None = None, parameters: dict[str, object] | None = None, extra_env: dict[str, str] | None = None, extra_sources: list[str] | None = None, ) -> None: - surf_sources = [wrapper_source] + surf_sources = [] if wrapper_source is None else [wrapper_source] if extra_sources is not None: surf_sources.extend(extra_sources) @@ -125,7 +125,7 @@ def run_pgp_wrapper_test( toplevel=toplevel, parameters={} if parameters is None else parameters, extra_env=extra_env, - extra_vhdl_sources={"surf": surf_sources}, + extra_vhdl_sources={"surf": surf_sources} if surf_sources else None, ) diff --git a/tests/protocols/pgp/shared/vc_fifo_test_utils.py b/tests/protocols/pgp/shared/vc_fifo_test_utils.py index 37cd844c7a..b4b360e5fa 100644 --- a/tests/protocols/pgp/shared/vc_fifo_test_utils.py +++ b/tests/protocols/pgp/shared/vc_fifo_test_utils.py @@ -15,6 +15,7 @@ from cocotb.triggers import RisingEdge, Timer from cocotbext.axi import AxiStreamBus, AxiStreamSink +from tests.axi.utils import wait_sampled_ready def pack_bytes(data: bytes, width_bytes: int = 8) -> int: """Pack a short little-endian byte string into one AXI Stream beat.""" @@ -140,13 +141,12 @@ async def send_frame( self.dut.S_AXIS_TID.value = tid self.dut.S_AXIS_TUSER.value = tuser_last if index == len(beats) - 1 else 0 - while True: - await RisingEdge(self.source_clk) - await self.settle() - if int(self.dut.S_AXIS_TREADY.value) == 1: - if on_handshake is not None: - await on_handshake(index) - break + await wait_sampled_ready( + self.dut.S_AXIS_TREADY, + clk=self.source_clk, + ) + if on_handshake is not None: + await on_handshake(index) self.drive_source_idle() await self.cycle_source(1) diff --git a/tests/protocols/rssi/README.md b/tests/protocols/rssi/README.md new file mode 100644 index 0000000000..aafda44d22 --- /dev/null +++ b/tests/protocols/rssi/README.md @@ -0,0 +1,79 @@ +# RSSI Regressions + +These tests follow the repository-wide [regression style guide](../../README.md) +and [protocol guidance](../README.md). The implementation and sizing guidance +is documented in [`protocols/rssi/README.md`](../../../protocols/rssi/README.md). + +## Protocol Oracle And Layers + +`rssi_test_utils.py` is the shared oracle for RSSI flags, header encoding, +checksum calculation, frame construction/parsing, SSI transport mechanics, and +common client/server setup. Keep protocol constants and mechanical helpers +there; keep assertions about RSSI policy in the test that names the behavior. + +The suite progresses from leaves to integration: + +- `test_RssiChksum.py` and `test_RssiHeaderReg.py` cover checksum and wire-header + formatting. +- `test_RssiRxFsm.py`, `test_RssiTxFsm.py`, `test_RssiMonitor.py`, and + `test_RssiConnFsm.py` cover receive/transmit legality, ACK/NULL/BUSY timing, + retransmission, connection negotiation, close, and recovery. +- `test_RssiAxiLiteRegItf.py` covers the register map, range clamping, + negotiated/current readback, counters, and visible controls/status. +- `test_RssiCore.py` covers direct client/server negotiation, payload transfer, + backpressure, loss/retransmission, checksums, keepalive, close/reopen, BUSY, + and AXI-Lite-controlled behavior. +- `test_RssiCoreWrapper.py` and `test_RssiCoreWrapperMultiStream.py` cover the + packetizer/chunker boundary, segment/window configurations, routing, + multi-stream loss recovery, and application-side sidebands. + +Default CI runs the currently stable RSSI cases. Focused cases that still expose +unresolved RTL behavior are opt-in behind `RUN_RSSI_KNOWN_ISSUE_TESTS=1`, and a +smaller group of long-running integration cases additionally uses +`RUN_RSSI_EXTENDED_TESTS=1`. `COCOTB_TESTCASE` selects one named scenario, while +`COCOTB_TEST_FILTER` selects an applicable scenario group such as the client or +server connection-FSM cases. The `RUN_*` gates decide whether the corresponding +pytest node is eligible to launch a simulation. Keep these roles separate so an +enabled node cannot silently run unrelated scenarios. + +Keep the skip reason beside each gated pytest entry. A known-issue case must +identify a durable defect reference or documented local issue, state the +expected failure, and say what change allows the gate to be removed. Promote the +case to default coverage in the same change that fixes the blocking RTL. Keep +stable-but-long coverage under the extended gate rather than calling it a known +issue. + +## RSSI-Specific Expectations + +The SURF RSSI profile uses 8-byte non-SYN headers, 24-byte SYN headers, 8-bit +sequence numbers, cumulative ACKs, ordered delivery, retransmission, NULL +keepalives, and BUSY flow control. Current hardware does not implement EACK +out-of-sequence delivery. Tests should use the SURF/Rogue profile as the +concrete contract and consult the RUDP lineage only where the profile leaves a +behavior unspecified. + +Directed negative cases should verify that illegal flag combinations, malformed +headers, bad checksums, and out-of-order frames do not leak application payload. +Recovery cases should then send valid traffic and prove that the endpoint makes +forward progress without duplicate delivery. + +When one methodology block can no longer describe a coherent set of scenarios, +split the integration suite by behavior while continuing to share the RSSI +oracle. Useful boundaries are negotiation and close, data and retransmission, +flow control and keepalive, connection lifecycle, AXI-Lite control, and +multi-stream integration. Preserve the existing pytest case names and gate +semantics during such a split so coverage does not disappear unnoticed. + +Run the default suite with: + +```bash +make MODULES="$PWD" import +./.venv/bin/python -m pytest -n auto --dist=worksteal -q tests/protocols/rssi +``` + +Run known-issue and extended cases explicitly with: + +```bash +RUN_RSSI_KNOWN_ISSUE_TESTS=1 RUN_RSSI_EXTENDED_TESTS=1 \ + ./.venv/bin/python -m pytest -n 0 -q tests/protocols/rssi +``` diff --git a/tests/protocols/rssi/rssi_test_utils.py b/tests/protocols/rssi/rssi_test_utils.py index d69bfd5948..7e183acd23 100644 --- a/tests/protocols/rssi/rssi_test_utils.py +++ b/tests/protocols/rssi/rssi_test_utils.py @@ -26,19 +26,6 @@ RSSI_VERSION = 0x1 -RSSI_CORE_VHDL_SOURCES = [ - "protocols/rssi/v1/rtl/RssiConnFsm.vhd", - "protocols/rssi/v1/rtl/RssiMonitor.vhd", - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/rtl/RssiTxFsm.vhd", - "protocols/rssi/v1/rtl/RssiCore.vhd", -] - -RSSI_CORE_WRAPPER_VHDL_SOURCES = RSSI_CORE_VHDL_SOURCES + [ - "protocols/rssi/v1/rtl/RssiCoreWrapper.vhd", -] - - @dataclass(frozen=True) class RssiParams: # Defaults are ordinary valid negotiation values, not reset values. Tests diff --git a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py index 298f232f57..b1f95a82d7 100644 --- a/tests/protocols/rssi/test_RssiAxiLiteRegItf.py +++ b/tests/protocols/rssi/test_RssiAxiLiteRegItf.py @@ -37,7 +37,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -80,8 +81,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) def _set_defaults(self) -> None: self.dut.negParamVersion_i.value = 1 @@ -333,11 +333,4 @@ def test_RssiAxiLiteRegItf(parameters): test_file=__file__, toplevel="surf.rssiaxiliteregitfwrapper", parameters=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiAxiLiteRegItf.vhd", - "protocols/rssi/v1/wrappers/RssiAxiLiteRegItfWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiChksum.py b/tests/protocols/rssi/test_RssiChksum.py index f6dfe7b107..fc1d9073ba 100644 --- a/tests/protocols/rssi/test_RssiChksum.py +++ b/tests/protocols/rssi/test_RssiChksum.py @@ -20,7 +20,9 @@ # - Stimulus: Feed ACK, DATA, and multi-word SYN headers with the checksum field # cleared for generation mode, then feed complete headers with the checksum # included for validation mode. Additional cases cover enable gaps and reset -# interruptions so the accumulator restart behavior is explicit. +# interruptions so the accumulator restart behavior is explicit. A raw, +# hand-worked ACK header anchors both the Python model and DUT independently +# of the header-builder path. # - Checks: Generated checksums must match the Python one's-complement oracle. # Complete valid headers must assert `check_o`; headers with one altered byte # must leave `check_o` deasserted. Dropping enable or asserting reset must @@ -34,9 +36,10 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import with_timeout from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -58,10 +61,9 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.clk_i) # Most RSSI RTL uses `after TPD_G`; wait past the default 1 ns # transport delay before sampling outputs. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk_i, propagation_time=2) async def reset(self) -> None: # Hold all data/control inputs in a benign state during reset so the @@ -109,6 +111,20 @@ async def known_header_vectors_test(dut): tb = TB(dut) await tb.reset() + # 0x4008 + 0x1234 + 0x0000 = 0x523c; one's complement is 0xadc3. + # Keep the input bytes and result literal so a shared defect in the header + # builders and checksum helper cannot make the model and DUT agree. + raw_ack_without_checksum = bytes.fromhex("4008123400000000") + hand_worked_checksum = 0xADC3 + assert ones_complement_checksum(raw_ack_without_checksum) == hand_worked_checksum, ( + "RSSI checksum oracle disagrees with the hand-worked ACK vector" + ) + observed, check_ok = await tb.run_words([0x4008_1234_0000_0000]) + assert observed == hand_worked_checksum, ( + f"raw ACK checksum: expected {hand_worked_checksum:#06x}, got {observed:#06x}" + ) + assert check_ok == 0, f"generation mode unexpectedly asserted check_o={check_ok}" + vectors = [ build_ack_header(sequence=0x12, acknowledge=0x34), build_data_header(sequence=0x56, acknowledge=0x78, ack=True, busy=True), @@ -133,13 +149,18 @@ async def known_header_vectors_test(dut): ), ] - for header in vectors: + for vector_index, header in enumerate(vectors): # Generation mode feeds the header with the checksum bytes cleared and # expects `chksum_o` to produce the value that belongs in those bytes. expected = ones_complement_checksum(header_without_checksum(header)) observed, check_ok = await tb.run_words(header_words(header_without_checksum(header))) - assert observed == expected - assert check_ok == 0 + assert observed == expected, ( + f"header vector {vector_index}: expected checksum {expected:#06x}, " + f"got {observed:#06x}" + ) + assert check_ok == 0, ( + f"header vector {vector_index}: generation mode asserted check_o={check_ok}" + ) @cocotb.test() @@ -153,16 +174,16 @@ async def validation_mode_accepts_good_and_rejects_bad_headers_test(dut): # Validation mode feeds the complete header, including checksum. A correct # header should reduce to zero after the RTL's final one's complement. observed, check_ok = await tb.run_words(header_words(good_header)) - assert observed == 0 - assert check_ok == 1 + assert observed == 0, f"valid header reduced to nonzero checksum {observed:#06x}" + assert check_ok == 1, f"valid header produced check_o={check_ok}" bad_header = bytearray(good_header) # Flip a negotiated field without updating the checksum; the same length and # structure should now fail only the checksum validation. bad_header[5] ^= 0x20 observed, check_ok = await tb.run_words(header_words(bytes(bad_header))) - assert observed != 0 - assert check_ok == 0 + assert observed != 0, "corrupted header unexpectedly reduced to a zero checksum" + assert check_ok == 0, f"corrupted header produced check_o={check_ok}" @cocotb.test() diff --git a/tests/protocols/rssi/test_RssiConnFsm.py b/tests/protocols/rssi/test_RssiConnFsm.py index 41834586a9..a27ffc42d5 100644 --- a/tests/protocols/rssi/test_RssiConnFsm.py +++ b/tests/protocols/rssi/test_RssiConnFsm.py @@ -29,15 +29,15 @@ # by close rather than counter overflow. # - Timing: Status checks wait past the default `TPD_G` output delay after each # clock edge. Timeout generics are kept small so retries and peer-timeout -# closure remain deterministic within a directed cocotb test. - -import os +# closure remain deterministic within a directed cocotb test. The pytest +# wrapper selects only server-named scenarios for `SERVER_G=true` and only +# client-named scenarios for `SERVER_G=false`. import cocotb import pytest from cocotb.triggers import Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import cocotb_filtered_env, run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import RssiParams from tests.protocols.ssi.ssi_test_utils import ( cycle as ssi_cycle, @@ -163,15 +163,8 @@ async def wait_state(self, expected: int, *, cycles: int = 16) -> None: raise AssertionError(f"Timed out waiting for connState_o={expected:#x}") -def server_mode() -> bool: - return os.environ.get("SERVER_G", "true").lower() == "true" - - @cocotb.test() async def server_accepts_syn_ack_and_opens_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.cycle() @@ -195,9 +188,6 @@ async def server_accepts_syn_ack_and_opens_test(dut): @cocotb.test() async def server_proposes_local_required_parameters_on_mismatch_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams( version=1, @@ -233,9 +223,6 @@ async def server_proposes_local_required_parameters_on_mismatch_test(dut): @cocotb.test() async def server_rejects_out_of_range_syn_parameters_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams( version=1, @@ -278,9 +265,6 @@ async def server_rejects_out_of_range_syn_parameters_test(dut): @cocotb.test() async def server_retries_syn_ack_then_times_out_waiting_for_ack_test(dut): - if not server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.cycle() @@ -306,9 +290,6 @@ async def server_retries_syn_ack_then_times_out_waiting_for_ack_test(dut): @cocotb.test() async def client_accepts_syn_ack_clamps_and_opens_test(dut): - if server_mode(): - return - tb = await TB.create(dut) app_params = RssiParams(max_outs_seg=4, max_seg_size=64) peer_params = RssiParams(max_outs_seg=8, max_seg_size=128) @@ -336,9 +317,6 @@ async def client_accepts_syn_ack_clamps_and_opens_test(dut): @cocotb.test() async def client_rejects_mismatched_syn_ack_with_rst_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.set_app_params(RssiParams(version=1, chksum_en=1, timeout_unit=1)) tb.set_rx_params(RssiParams(version=2, chksum_en=1, timeout_unit=1)) @@ -360,9 +338,6 @@ async def client_rejects_mismatched_syn_ack_with_rst_test(dut): @cocotb.test() async def client_rejects_out_of_range_syn_ack_with_rst_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.set_app_params(RssiParams(version=1, chksum_en=1, timeout_unit=1)) tb.set_rx_params( @@ -394,9 +369,6 @@ async def client_rejects_out_of_range_syn_ack_with_rst_test(dut): @cocotb.test() async def client_retries_syn_then_times_out_waiting_for_syn_ack_test(dut): - if server_mode(): - return - tb = await TB.create(dut) tb.dut.connRq_i.value = 1 await tb.wait_high("sndSyn_o") @@ -422,22 +394,13 @@ async def client_retries_syn_then_times_out_waiting_for_syn_ack_test(dut): pytest.param({"SERVER_G": False}, id="client"), ] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiConnFsm(parameters): + role = "server" if parameters["SERVER_G"] else "client" run_surf_vhdl_test( test_file=__file__, toplevel="surf.rssiconnfsmwrapper", parameters=parameters, - extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiConnFsm.vhd", - "protocols/rssi/v1/wrappers/RssiConnFsmWrapper.vhd", - ], - }, - force_compile=True, + extra_env=cocotb_filtered_env(parameters, rf"{role}_.*_test$"), ) diff --git a/tests/protocols/rssi/test_RssiCore.py b/tests/protocols/rssi/test_RssiCore.py index 16348826ba..46b80cb767 100644 --- a/tests/protocols/rssi/test_RssiCore.py +++ b/tests/protocols/rssi/test_RssiCore.py @@ -39,25 +39,27 @@ # generics so the full integration batch remains bounded. Separate pytest # entries enable narrower cocotb tests for sequence wrap, bidirectional DATA # loss in one connection, and out-of-order recovery with longer retransmit -# spacing. Environment flags keep those specialized cocotb tests inert -# during unrelated parameter runs. +# spacing. The default pytest entry filters out those specialized cocotb +# scenarios before simulation; each focused entry selects its named scenario. # - Timing and scoreboarding: Transport monitors sample accepted RSSI frames at # the source side before optional loopback drops. Application scoreboards # assert both absence of premature output and exact recovered frames. Quiet # output drains account for reset-release FIFO behavior so payload assertions # are about RSSI DATA delivery rather than wrapper initialization. -import os - import cocotb import pytest from cocotb.triggers import FallingEdge, RisingEdge, Timer from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.rssi.rssi_test_utils import ( - RSSI_CORE_VHDL_SOURCES, RSSI_FLAG_ACK, RSSI_FLAG_NULL, RSSI_FLAG_RST, @@ -87,13 +89,6 @@ REG_RESEND_CNT = 0x4C -def _run_extended_case(case_name: str) -> bool: - return ( - env_flag("RUN_RSSI_EXTENDED_TESTS", default=False) - or os.environ.get("COCOTB_TESTCASE") == case_name - ) - - class TB: def __init__(self, dut): self.dut = dut @@ -258,6 +253,9 @@ async def send_app_frame(self, endpoint: FlatSsiEndpoint, beats: list[SsiBeat]) endpoint.set_idle() def start_transport_loopbacks(self) -> None: + # These retained coroutines are lifetime agents for one cocotb + # entrypoint. They own no external resources and cocotb cancels them + # when that entrypoint finishes. self.loopback_tasks = [ cocotb.start_soon( self.loopback_transport( @@ -323,6 +321,7 @@ async def loopback_transport( *, side: str, ) -> None: + """Lifetime agent: relay RSSI traffic until cocotb ends the test.""" dropping = False destination.set_idle() source_ready.value = 0 @@ -610,9 +609,6 @@ async def dropped_client_data_retransmits_and_recovers_payload_test(dut): @cocotb.test() async def bidirectional_data_losses_recover_without_duplicate_delivery_test(dut): - if not env_flag("RSSI_REPEATED_LOSS_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -676,9 +672,6 @@ async def bidirectional_data_losses_recover_without_duplicate_delivery_test(dut) @cocotb.test() async def lost_first_data_drops_later_data_until_retransmit_test(dut): - if not env_flag("RSSI_OUT_OF_ORDER_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -833,9 +826,6 @@ async def server_backpressure_advertises_busy_to_client_test(dut): @cocotb.test() async def server_backpressure_recovers_without_lost_or_duplicate_frames_test(dut): - if not _run_extended_case("server_backpressure_recovers_without_lost_or_duplicate_frames_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1039,9 +1029,6 @@ async def close_then_reopen_clears_state_and_delivers_new_payload_test(dut): @cocotb.test() async def client_axil_control_path_opens_injects_reads_and_closes_test(dut): - if not env_flag("RSSI_AXIL_CONTROL_CASE", default=False): - return - tb = await TB.create(dut, direct_client_open=0) await tb.axil_write(REG_CONTROL, 0x0C) @@ -1099,9 +1086,6 @@ async def client_axil_control_path_opens_injects_reads_and_closes_test(dut): @cocotb.test() async def checksum_disabled_connection_and_payload_test(dut): - if not env_flag("RSSI_CHECKSUM_DISABLED_CORE_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1222,9 +1206,6 @@ async def bidirectional_multi_frame_stress_test(dut): @cocotb.test() async def client_sequence_wraparound_delivers_frame_test(dut): - if not env_flag("RSSI_SEQUENCE_WRAP_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -1270,6 +1251,24 @@ async def client_sequence_wraparound_delivers_frame_test(dut): ) ] +SPECIALIZED_TESTS = ( + "bidirectional_data_losses_recover_without_duplicate_delivery_test", + "checksum_disabled_connection_and_payload_test", + "client_axil_control_path_opens_injects_reads_and_closes_test", + "client_sequence_wraparound_delivers_frame_test", + "lost_first_data_drops_later_data_until_retransmit_test", +) + + +def _default_extra_env(parameters: dict[str, object]) -> dict[str, object]: + excluded = list(SPECIALIZED_TESTS) + if not env_flag("RUN_RSSI_EXTENDED_TESTS", default=False): + excluded.append("server_backpressure_recovers_without_lost_or_duplicate_frames_test") + return cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding(*excluded), + ) + KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" @@ -1280,14 +1279,7 @@ def test_RssiCore(parameters): test_file=__file__, toplevel="surf.rssicoreintegrationwrapper", parameters=parameters, - extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, + extra_env=_default_extra_env(parameters), ) @@ -1314,13 +1306,6 @@ def test_RssiCore_sequence_wraparound(): "COCOTB_TESTCASE": "client_sequence_wraparound_delivers_frame_test", "RSSI_SEQUENCE_WRAP_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1346,13 +1331,6 @@ def test_RssiCore_repeated_data_loss(): "COCOTB_TESTCASE": "bidirectional_data_losses_recover_without_duplicate_delivery_test", "RSSI_REPEATED_LOSS_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1378,13 +1356,6 @@ def test_RssiCore_out_of_order_recovery(): "COCOTB_TESTCASE": "lost_first_data_drops_later_data_until_retransmit_test", "RSSI_OUT_OF_ORDER_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1410,13 +1381,6 @@ def test_RssiCore_axil_control_path(): "COCOTB_TESTCASE": "client_axil_control_path_opens_injects_reads_and_closes_test", "RSSI_AXIL_CONTROL_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -1443,11 +1407,4 @@ def test_RssiCore_checksum_disabled(): "COCOTB_TESTCASE": "checksum_disabled_connection_and_payload_test", "RSSI_CHECKSUM_DISABLED_CORE_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiCoreWrapper.py b/tests/protocols/rssi/test_RssiCoreWrapper.py index 176c151c13..9bda302c2a 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapper.py +++ b/tests/protocols/rssi/test_RssiCoreWrapper.py @@ -32,7 +32,9 @@ # - Parameter strategy: Sweep bypass-chunker and packetizer modes across # multiple `WINDOW_ADDR_SIZE_G` and `MAX_SEG_SIZE_G` values. This catches # wrapper elaboration and derived RSSI FIFO/pause-threshold issues without -# requiring an exhaustive Cartesian product. +# requiring an exhaustive Cartesian product. The default sweep filters out +# the focused BUSY/backpressure scenario; its dedicated pytest node selects +# it explicitly. # - Timing: Small timeout generics keep the wrapper checks bounded. If a # protocol-level failure appears here, reproduce it in `test_RssiCore.py` # unless the failure is clearly caused by wrapper-only packetizer, chunker, or @@ -42,8 +44,12 @@ import pytest from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test -from tests.protocols.rssi.rssi_test_utils import RSSI_CORE_WRAPPER_VHDL_SOURCES +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, SsiBeat, @@ -208,9 +214,6 @@ async def wrapper_partial_keep_and_eofe_payload_test(dut): @cocotb.test() async def wrapper_server_backpressure_advertises_busy_test(dut): - if not env_flag("RSSI_WRAPPER_BACKPRESSURE_CASE", default=False): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -286,14 +289,12 @@ def test_RssiCoreWrapper(parameters): test_file=__file__, toplevel="surf.rssicorewrapperintegrationwrapper", parameters=parameters, - extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperIntegrationWrapper.vhd", - ], - }, - force_compile=True, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding( + "wrapper_server_backpressure_advertises_busy_test" + ), + ), ) @@ -314,11 +315,4 @@ def test_RssiCoreWrapper_backpressure(): "COCOTB_TESTCASE": "wrapper_server_backpressure_advertises_busy_test", "RSSI_WRAPPER_BACKPRESSURE_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py index 056b993052..cee37cbb02 100644 --- a/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py +++ b/tests/protocols/rssi/test_RssiCoreWrapperMultiStream.py @@ -42,15 +42,12 @@ # frames after a test arms the hook, leaving ACK/NULL control traffic free to # maintain the connection. -import os - import cocotb import pytest from cocotb.triggers import FallingEdge, RisingEdge, Timer from tests.common.regression_utils import env_flag, run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import ( - RSSI_CORE_WRAPPER_VHDL_SOURCES, format_transport_frame, parse_header, protocol_bytes_from_stream_word, @@ -73,13 +70,6 @@ DEPACKETIZER2_INIT_WAIT_CYCLES = 1024 -def _run_extended_case(case_name: str) -> bool: - return ( - env_flag("RUN_RSSI_EXTENDED_TESTS", default=False) - or os.environ.get("COCOTB_TESTCASE") == case_name - ) - - def _default_extra_env(parameters: dict[str, object]) -> dict[str, object]: if env_flag("RUN_RSSI_EXTENDED_TESTS", default=False): return parameters @@ -94,14 +84,28 @@ def _explicit_pytest_selection(request, test_name: str) -> bool: def assert_frame_preserves_valid_bytes(actual: list[SsiBeat], expected: list[SsiBeat]) -> None: - assert len(actual) == len(expected) - for actual_beat, expected_beat in zip(actual, expected): + assert len(actual) == len(expected), ( + f"frame beat count: expected {len(expected)}, got {len(actual)}" + ) + for beat_index, (actual_beat, expected_beat) in enumerate(zip(actual, expected)): mask = data_mask_from_keep(expected_beat.keep) - assert actual_beat.data & mask == expected_beat.data & mask - assert actual_beat.keep == expected_beat.keep - assert actual_beat.last == expected_beat.last - assert actual_beat.sof == expected_beat.sof - assert actual_beat.eofe == expected_beat.eofe + assert actual_beat.data & mask == expected_beat.data & mask, ( + f"beat {beat_index} payload: expected " + f"{expected_beat.data & mask:#x}, got {actual_beat.data & mask:#x}" + ) + assert actual_beat.keep == expected_beat.keep, ( + f"beat {beat_index} TKEEP: expected {expected_beat.keep:#x}, " + f"got {actual_beat.keep:#x}" + ) + assert actual_beat.last == expected_beat.last, ( + f"beat {beat_index} TLAST: expected {expected_beat.last}, got {actual_beat.last}" + ) + assert actual_beat.sof == expected_beat.sof, ( + f"beat {beat_index} SOF: expected {expected_beat.sof}, got {actual_beat.sof}" + ) + assert actual_beat.eofe == expected_beat.eofe, ( + f"beat {beat_index} EOFE: expected {expected_beat.eofe}, got {actual_beat.eofe}" + ) class TB: @@ -200,6 +204,9 @@ async def send_app_frame(self, endpoint: FlatSsiEndpoint, beats: list[SsiBeat]) endpoint.set_idle() def start_transport_loopbacks(self) -> None: + # These retained coroutines are lifetime agents for one cocotb + # entrypoint. They own no external resources and cocotb cancels them + # when that entrypoint finishes. self.loopback_tasks = [ cocotb.start_soon( self.loopback_transport( @@ -227,6 +234,7 @@ async def loopback_transport( *, drop_attr: str, ) -> None: + """Lifetime agent: relay RSSI traffic until cocotb ends the test.""" dropping = False destination.set_idle() source_ready.value = 0 @@ -334,9 +342,6 @@ async def multi_stream_client_to_server_payload_routes_test(dut): @cocotb.test() async def multi_stream_bidirectional_payload_routes_test(dut): - if not _run_extended_case("multi_stream_bidirectional_payload_routes_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -413,9 +418,6 @@ async def multi_stream_bidirectional_payload_routes_test(dut): @cocotb.test() async def multi_stream_partial_keep_and_eofe_routes_test(dut): - if not _run_extended_case("multi_stream_partial_keep_and_eofe_routes_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -441,9 +443,6 @@ async def multi_stream_partial_keep_and_eofe_routes_test(dut): @cocotb.test() async def multi_stream_dropped_client_data_retransmits_to_route_test(dut): - if not _run_extended_case("multi_stream_dropped_client_data_retransmits_to_route_test"): - return - tb = await TB.create(dut) await tb.wait_connected() @@ -532,13 +531,6 @@ def test_RssiCoreWrapperMultiStream(parameters): toplevel="surf.rssicorewrappermultistreamintegrationwrapper", parameters=parameters, extra_env=_default_extra_env(parameters), - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -557,13 +549,6 @@ def test_RssiCoreWrapperMultiStream_extended(parameters): **parameters, "RUN_RSSI_EXTENDED_TESTS": 1, }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) @@ -600,11 +585,4 @@ def test_RssiCoreWrapperMultiStream_bidirectional_packetizer2(request): **parameters, "COCOTB_TESTCASE": "multi_stream_bidirectional_payload_routes_test", }, - extra_vhdl_sources={ - "surf": [ - *RSSI_CORE_WRAPPER_VHDL_SOURCES, - "protocols/rssi/v1/wrappers/RssiCoreWrapperMultiStreamIntegrationWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiHeaderReg.py b/tests/protocols/rssi/test_RssiHeaderReg.py index dec46284b8..816e707f0f 100644 --- a/tests/protocols/rssi/test_RssiHeaderReg.py +++ b/tests/protocols/rssi/test_RssiHeaderReg.py @@ -33,9 +33,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -58,10 +58,9 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.clk_i) # Wait past the default `TPD_G` so registered outputs are settled # before Python reads them. - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.clk_i, propagation_time=2) async def reset(self) -> None: # Reset all header request strobes and data fields before deasserting @@ -267,5 +266,4 @@ def test_RssiHeaderReg(parameters): toplevel="surf.rssiheaderregwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={"surf": ["protocols/rssi/v1/wrappers/RssiHeaderRegWrapper.vhd"]}, ) diff --git a/tests/protocols/rssi/test_RssiMonitor.py b/tests/protocols/rssi/test_RssiMonitor.py index 8103ff2e61..663cab970b 100644 --- a/tests/protocols/rssi/test_RssiMonitor.py +++ b/tests/protocols/rssi/test_RssiMonitor.py @@ -32,9 +32,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd class TB: @@ -44,8 +44,7 @@ def __init__(self, dut): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axisClk) - await Timer(2, unit="ns") + await sample_after_tpd(self.dut.axisClk, propagation_time=2) def _set_flag_defaults(self) -> None: self.dut.rxFlagsSyn_i.value = 0 @@ -222,10 +221,7 @@ async def local_busy_generates_periodic_ack_after_cumulative_timeout_test(dut): PARAMETER_SWEEP = [pytest.param({}, id="server_monitor")] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiMonitor(parameters): run_surf_vhdl_test( @@ -233,11 +229,4 @@ def test_RssiMonitor(parameters): toplevel="surf.rssimonitorwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiMonitor.vhd", - "protocols/rssi/v1/wrappers/RssiMonitorWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiRxFsm.py b/tests/protocols/rssi/test_RssiRxFsm.py index 0c24f635ef..f16309b0a0 100644 --- a/tests/protocols/rssi/test_RssiRxFsm.py +++ b/tests/protocols/rssi/test_RssiRxFsm.py @@ -32,13 +32,19 @@ # - Timing: Transport input waits for sampled ready before changing beats. # Status checks wait past the default `TPD_G` output delay, and app-output # checks account for the registered segment RAM read latency used by the real -# `RssiCore` path. +# `RssiCore` path. The checksum-enabled sweep filters out the checksum- +# disabled scenario; its dedicated pytest node selects that case explicitly. import cocotb import pytest from cocotb.triggers import Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import ( + cocotb_filtered_env, + cocotb_test_filter_excluding, + env_flag, + run_surf_vhdl_test, +) from tests.protocols.rssi.rssi_test_utils import ( RssiParams, RSSI_FLAG_BUSY, @@ -326,9 +332,6 @@ async def checksum_failed_data_payload_is_flushed_before_retransmit_test(dut): @cocotb.test() async def checksum_disabled_accepts_data_when_checksum_status_is_bad_test(dut): - if not env_flag("RSSI_CHECKSUM_DISABLED_CASE", default=False): - return - tb = await TB.create(dut) payload = 0xCAFE_0000_0000_BEEF @@ -579,14 +582,12 @@ def test_RssiRxFsm(parameters): test_file=__file__, toplevel="surf.rssirxfsmwrapper", parameters=parameters, - extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiRxFsmWrapper.vhd", - ], - }, - force_compile=True, + extra_env=cocotb_filtered_env( + parameters, + cocotb_test_filter_excluding( + "checksum_disabled_accepts_data_when_checksum_status_is_bad_test" + ), + ), ) @@ -602,11 +603,4 @@ def test_RssiRxFsm_checksum_disabled(): "COCOTB_TESTCASE": "checksum_disabled_accepts_data_when_checksum_status_is_bad_test", "RSSI_CHECKSUM_DISABLED_CASE": 1, }, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiRxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiRxFsmWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/rssi/test_RssiTxFsm.py b/tests/protocols/rssi/test_RssiTxFsm.py index 60517d60dc..673ea98fb3 100644 --- a/tests/protocols/rssi/test_RssiTxFsm.py +++ b/tests/protocols/rssi/test_RssiTxFsm.py @@ -37,9 +37,11 @@ import cocotb import pytest -from cocotb.triggers import FallingEdge, RisingEdge, Timer +from cocotb.triggers import FallingEdge, Timer -from tests.common.regression_utils import env_flag, run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd + +from tests.common.regression_utils import run_surf_vhdl_test from tests.protocols.rssi.rssi_test_utils import ( RssiParams, build_ack_header, @@ -81,8 +83,7 @@ async def _send_contiguous_frame_after_tpd(endpoint, beats: list[SsiBeat], *, cl for beat in beats: endpoint.drive(beat) for _ in range(1024): - await RisingEdge(clk) - await Timer(2, unit="ns") + await sample_after_tpd(clk, propagation_time=2) if int(endpoint._sig("TReady").value) == 1: break else: @@ -174,13 +175,11 @@ async def recv_frame_selected_fields( for field in fields: beat[field] = int(getattr(self.dut, field).value) beats.append(beat) - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) if beat.get("mAxisTLast", 0) == 1: return beats else: - await RisingEdge(self.clk) - await Timer(1, unit="ns") + await sample_after_tpd(self.clk) finally: self.dut.mAxisTReady.value = 0 raise AssertionError("Timed out waiting for selected mAxis frame fields") @@ -815,10 +814,7 @@ async def rst_segment_emits_header_and_consumes_sequence_without_buffering_test( PARAMETER_SWEEP = [pytest.param({}, id="small_window")] -KNOWN_ISSUE_REASON = "set RUN_RSSI_KNOWN_ISSUE_TESTS=1 to run RSSI cases that require follow-up RTL fixes" - -@pytest.mark.skipif(not env_flag("RUN_RSSI_KNOWN_ISSUE_TESTS", default=False), reason=KNOWN_ISSUE_REASON) @pytest.mark.parametrize("parameters", PARAMETER_SWEEP) def test_RssiTxFsm(parameters): run_surf_vhdl_test( @@ -826,11 +822,4 @@ def test_RssiTxFsm(parameters): toplevel="surf.rssitxfsmwrapper", parameters=parameters, extra_env=parameters, - extra_vhdl_sources={ - "surf": [ - "protocols/rssi/v1/rtl/RssiTxFsm.vhd", - "protocols/rssi/v1/wrappers/RssiTxFsmWrapper.vhd", - ], - }, - force_compile=True, ) diff --git a/tests/protocols/srp/srp_test_utils.py b/tests/protocols/srp/srp_test_utils.py index 5fd3384054..4e784fd876 100644 --- a/tests/protocols/srp/srp_test_utils.py +++ b/tests/protocols/srp/srp_test_utils.py @@ -146,7 +146,7 @@ async def send_packed_words(self, words: list[int], *, tdest: int = 0, prefix: s self._sig(prefix, "TLAST").value = 0 self._sig(prefix, "TUSER").value = 0 - async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: + async def _recv_response_until_last(self, *, prefix: str) -> AxisResponse: prefix = self.sink_prefix if prefix is None else prefix self._sig(prefix, "TREADY").value = 1 words = [] @@ -154,7 +154,8 @@ async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: tuser = [] tkeep = [] - while True: + complete = False + while not complete: await RisingEdge(self.clk) if int(self._sig(prefix, "TVALID").value) != 1: continue @@ -174,12 +175,14 @@ async def _recv_response_unbounded(self, *, prefix: str) -> AxisResponse: if hasattr(self.dut, f"{prefix}_TUSER"): tuser.append(int(self._sig(prefix, "TUSER").value)) if int(self._sig(prefix, "TLAST").value) == 1: - return AxisResponse(words=words, tdest=tdest, tuser=tuser, tkeep=tkeep) + complete = True + + return AxisResponse(words=words, tdest=tdest, tuser=tuser, tkeep=tkeep) async def recv_response(self, *, prefix: str | None = None, timeout_time: int = 20) -> AxisResponse: prefix = self.sink_prefix if prefix is None else prefix return await with_timeout( - self._recv_response_unbounded(prefix=prefix), + self._recv_response_until_last(prefix=prefix), timeout_time, "us", ) diff --git a/tests/protocols/srp/test_SrpV0AxiLite.py b/tests/protocols/srp/test_SrpV0AxiLite.py index 7bfab9b071..503de73f68 100644 --- a/tests/protocols/srp/test_SrpV0AxiLite.py +++ b/tests/protocols/srp/test_SrpV0AxiLite.py @@ -87,22 +87,28 @@ async def accept_one_write(self, *, resp: AxiResp = AxiResp.OKAY) -> dict[str, i self.dut.M_AXIL_AWREADY.value = 1 self.dut.M_AXIL_WREADY.value = 1 - while "address" not in record or "data" not in record: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_AWVALID.value) and int(self.dut.M_AXIL_AWREADY.value): record["address"] = int(self.dut.M_AXIL_AWADDR.value) if int(self.dut.M_AXIL_WVALID.value) and int(self.dut.M_AXIL_WREADY.value): record["data"] = int(self.dut.M_AXIL_WDATA.value) record["strobe"] = int(self.dut.M_AXIL_WSTRB.value) + if "address" in record and "data" in record: + break + else: + raise AssertionError(f"Timed out waiting for AXI-Lite write request: {record}") self.dut.M_AXIL_AWREADY.value = 0 self.dut.M_AXIL_WREADY.value = 0 self.dut.M_AXIL_BRESP.value = int(resp) self.dut.M_AXIL_BVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_BREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite write response acceptance") self.dut.M_AXIL_BVALID.value = 0 self.dut.M_AXIL_BRESP.value = 0 return record @@ -110,19 +116,24 @@ async def accept_one_write(self, *, resp: AxiResp = AxiResp.OKAY) -> dict[str, i async def accept_one_read(self, *, data: int, resp: AxiResp = AxiResp.OKAY) -> dict[str, int]: record = {} self.dut.M_AXIL_ARREADY.value = 1 - while "address" not in record: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_ARVALID.value) and int(self.dut.M_AXIL_ARREADY.value): record["address"] = int(self.dut.M_AXIL_ARADDR.value) + break + else: + raise AssertionError("Timed out waiting for AXI-Lite read request") self.dut.M_AXIL_ARREADY.value = 0 self.dut.M_AXIL_RDATA.value = data self.dut.M_AXIL_RRESP.value = int(resp) self.dut.M_AXIL_RVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_RREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read response acceptance") self.dut.M_AXIL_RVALID.value = 0 self.dut.M_AXIL_RRESP.value = 0 return record @@ -240,11 +251,8 @@ async def srpv0_axilite_downstream_error_status_test(dut): assert await read_task == {"address": read_address} -@cocotb.test() +@cocotb.test(skip=os.environ.get("EN_32BIT_ADDR_G", "false").lower() != "true") async def srpv0_axilite_32bit_address_decode_test(dut): - if os.environ.get("EN_32BIT_ADDR_G", "false").lower() != "true": - return - tb = TB(dut, use_ram=False) await tb.reset() diff --git a/tests/protocols/srp/test_SrpV0Loopback.py b/tests/protocols/srp/test_SrpV0Loopback.py index 93a2592ed5..fd25896b69 100644 --- a/tests/protocols/srp/test_SrpV0Loopback.py +++ b/tests/protocols/srp/test_SrpV0Loopback.py @@ -22,7 +22,8 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -38,8 +39,7 @@ def __init__(self, dut): async def cycle(self, count=1): for _ in range(count): - await RisingEdge(self.dut.S_AXI_ACLK) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.S_AXI_ACLK) async def reset(self): # The wrapper uses the standard active-low AXI-Lite reset exposed by diff --git a/tests/protocols/srp/test_SrpV3AxiLite.py b/tests/protocols/srp/test_SrpV3AxiLite.py index b939c4d1bd..d65ddae6ed 100644 --- a/tests/protocols/srp/test_SrpV3AxiLite.py +++ b/tests/protocols/srp/test_SrpV3AxiLite.py @@ -85,18 +85,22 @@ async def reset(self): async def respond_one_read(self, *, data: int, resp: AxiResp = AxiResp.OKAY): self.dut.M_AXIL_ARREADY.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_ARVALID.value) and int(self.dut.M_AXIL_ARREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read request") self.dut.M_AXIL_ARREADY.value = 0 self.dut.M_AXIL_RDATA.value = data self.dut.M_AXIL_RRESP.value = int(resp) self.dut.M_AXIL_RVALID.value = 1 - while True: + for _ in range(1024): await RisingEdge(self.dut.AXIS_ACLK) if int(self.dut.M_AXIL_RREADY.value): break + else: + raise AssertionError("Timed out waiting for AXI-Lite read response acceptance") self.dut.M_AXIL_RVALID.value = 0 self.dut.M_AXIL_RRESP.value = 0 diff --git a/tests/protocols/ssi/ssi_test_utils.py b/tests/protocols/ssi/ssi_test_utils.py index a848b11c31..19c13f7539 100644 --- a/tests/protocols/ssi/ssi_test_utils.py +++ b/tests/protocols/ssi/ssi_test_utils.py @@ -17,6 +17,8 @@ from cocotb.clock import Clock from cocotb.triggers import FallingEdge, RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.axi.utils import wait_sampled_ready @@ -119,8 +121,7 @@ async def wait_valid(self, *, clk, timeout_cycles: int = 64) -> SsiBeat: await Timer(1, unit="ns") if int(self._sig("TValid").value) == 1: return self.snapshot() - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(self._sig("TValid").value) == 1: return self.snapshot() raise AssertionError(f"Timed out waiting for {self.prefix} valid") @@ -131,8 +132,7 @@ async def recv(self, *, clk, ready_signal=None, keep_ready: bool = False) -> Ssi if ready_signal is not None: ready_signal.value = 1 beat = await self.wait_valid(clk=clk) - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if ready_signal is not None and not keep_ready: ready_signal.value = 0 return beat @@ -169,8 +169,7 @@ async def cycle(clk, count: int = 1) -> None: # Most SSI benches sample a little after each edge so registered outputs # have time to settle before Python reads them. for _ in range(count): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) async def reset_dut(dut, *, clk_name: str = "axisClk", rst_name: str = "axisRst") -> None: @@ -264,8 +263,7 @@ async def wait_output_clear( ) -> None: ready_signal.value = 1 for _ in range(cycles): - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) if int(endpoint._sig("TValid").value) == 0: ready_signal.value = 0 return @@ -356,8 +354,7 @@ async def recv_visible_beat( ready_signal.value = 0 beat = await endpoint.wait_valid(clk=clk, timeout_cycles=timeout_cycles) ready_signal.value = 1 - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) ready_signal.value = 0 return beat @@ -499,8 +496,7 @@ async def wait_signal_level(signal, *, clk, expected: int, cycles: int = 32) -> for _ in range(cycles): if int(signal.value) == expected: return - await RisingEdge(clk) - await Timer(1, unit="ns") + await sample_after_tpd(clk) raise AssertionError(f"Timed out waiting for {signal}={expected}") diff --git a/tests/protocols/ssi/test_SsiAxiLiteMaster.py b/tests/protocols/ssi/test_SsiAxiLiteMaster.py index 5b661d707e..6e2a4d98b0 100644 --- a/tests/protocols/ssi/test_SsiAxiLiteMaster.py +++ b/tests/protocols/ssi/test_SsiAxiLiteMaster.py @@ -27,7 +27,11 @@ import pytest from cocotbext.axi import AxiResp -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import ( + cancel_and_join_tasks, + run_surf_vhdl_test, + sample_after_tpd, +) from tests.protocols.ssi.ssi_test_utils import ( FlatSsiEndpoint, recv_frame_and_check, @@ -62,8 +66,13 @@ def __init__(self, dut): # Run independent write and read responders so the DUT sees a realistic # AXI-Lite target rather than zero-delay combinational acks. - cocotb.start_soon(self._run_write()) - cocotb.start_soon(self._run_read()) + self._responder_tasks = ( + cocotb.start_soon(self._run_write()), + cocotb.start_soon(self._run_read()), + ) + + async def close(self) -> None: + await cancel_and_join_tasks(self._responder_tasks) def in_reset(self) -> bool: try: @@ -73,8 +82,7 @@ def in_reset(self) -> bool: async def cycle(self, count: int = 1): for _ in range(count): - await cocotb.triggers.RisingEdge(self.dut.axisClk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(self.dut.axisClk) async def _wait_while_reset(self): # While reset is active, keep every ready/valid output deasserted. @@ -87,6 +95,7 @@ async def _wait_while_reset(self): await self.cycle() async def _run_write(self): + """Lifetime agent: respond to AXI-Lite writes until the test ends.""" while True: await self._wait_while_reset() @@ -143,6 +152,7 @@ async def _run_write(self): self.dut.M_AXIL_BVALID.value = 0 async def _run_read(self): + """Lifetime agent: respond to AXI-Lite reads until the test ends.""" while True: await self._wait_while_reset() @@ -188,6 +198,9 @@ def __init__(self, dut): self.source.set_idle() dut.mAxisTReady.setimmediatevalue(1) + async def close(self) -> None: + await self.axil.close() + async def reset(self): await reset_dut(self.dut) @@ -236,9 +249,7 @@ async def send_read_request(tb: TB, *, echo: int, address: int, count: int): ) -@cocotb.test() -async def ssi_axi_lite_master_test(dut): - tb = TB(dut) +async def _exercise_ssi_axi_lite_master(tb: TB) -> None: await tb.reset() # First prove a single-word write round-trip, including the echoed request @@ -362,6 +373,15 @@ async def ssi_axi_lite_master_test(dut): await recv_task +@cocotb.test() +async def ssi_axi_lite_master_test(dut): + tb = TB(dut) + try: + await _exercise_ssi_axi_lite_master(tb) + finally: + await tb.close() + + @pytest.mark.parametrize("parameters", [pytest.param({}, id="same_clk_error_and_multiword")]) def test_SsiAxiLiteMaster(parameters): run_surf_vhdl_test( diff --git a/tests/protocols/ssi/test_SsiFifo.py b/tests/protocols/ssi/test_SsiFifo.py index 14de5b2164..2e68576a6a 100644 --- a/tests/protocols/ssi/test_SsiFifo.py +++ b/tests/protocols/ssi/test_SsiFifo.py @@ -32,6 +32,7 @@ import pytest from tests.common.regression_utils import env_flag, parameter_case, run_surf_vhdl_test +from tests.common.regression_utils import sample_after_tpd from tests.protocols.ssi.ssi_test_utils import ( assert_beat_list, assert_beat_views, @@ -59,8 +60,7 @@ async def drive_ready_pattern(ready_signal, *, clk, pattern: list[int], cycles: # coroutine keeps watching what traffic was actually accepted. for index in range(cycles): ready_signal.value = pattern[index % len(pattern)] - await cocotb.triggers.RisingEdge(clk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(clk) ready_signal.value = 0 diff --git a/tests/protocols/ssi/test_SsiIncrementingTx.py b/tests/protocols/ssi/test_SsiIncrementingTx.py index c90fdf0623..b44c5bdb06 100644 --- a/tests/protocols/ssi/test_SsiIncrementingTx.py +++ b/tests/protocols/ssi/test_SsiIncrementingTx.py @@ -23,7 +23,7 @@ import cocotb import pytest -from tests.common.regression_utils import run_surf_vhdl_test +from tests.common.regression_utils import run_surf_vhdl_test, sample_after_tpd from tests.protocols.ssi.ssi_test_utils import ( capture_accepted_beats, expect_no_output, @@ -39,8 +39,7 @@ async def pulse_trigger(dut): # Pulse the packet trigger for one cycle, matching how software would kick # the generator in hardware. dut.trig.value = 1 - await cocotb.triggers.RisingEdge(dut.axisClk) - await cocotb.triggers.Timer(1, unit="ns") + await sample_after_tpd(dut.axisClk) dut.trig.value = 0 diff --git a/tests/protocols/ssi/test_SsiPrbs.py b/tests/protocols/ssi/test_SsiPrbs.py index bd6d49eb99..da4ddd11f1 100644 --- a/tests/protocols/ssi/test_SsiPrbs.py +++ b/tests/protocols/ssi/test_SsiPrbs.py @@ -26,7 +26,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer, with_timeout +from cocotb.triggers import RisingEdge, with_timeout + +from tests.common.regression_utils import sample_after_tpd from tests.common.regression_utils import run_surf_vhdl_test @@ -41,13 +43,11 @@ def __init__(self, dut): async def fast_cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.fastClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.fastClk) async def slow_cycle(self, count: int = 1): for _ in range(count): - await RisingEdge(self.dut.slowClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.slowClk) async def reset(self): # Hold both domains in reset long enough for the internal loopback path diff --git a/tests/protocols/ssi/test_SsiResizeFifoEofe.py b/tests/protocols/ssi/test_SsiResizeFifoEofe.py index b9d395bf65..1c2c854ee2 100644 --- a/tests/protocols/ssi/test_SsiResizeFifoEofe.py +++ b/tests/protocols/ssi/test_SsiResizeFifoEofe.py @@ -124,10 +124,11 @@ async def ssi_resize_fifo_eofe_test(dut): await tb.send_eofe_frame() async def wait_for_output_terminal_beat(): - while True: + terminal_seen = False + while not terminal_seen: await RisingEdge(dut.AXIS_ACLK) if int(dut.M_AXIS_TVALID.value) == 1 and int(dut.M_AXIS_TLAST.value) == 1: - return + terminal_seen = True # Wait until the wrapper presents the outgoing terminal beat. Apply the # timeout to the full condition wait so the test cannot hang indefinitely diff --git a/tests/simlink/common/simlink_multi_instance_cocotb.py b/tests/simlink/common/simlink_multi_instance_cocotb.py index f8f7e968ec..2d9eb8248c 100644 --- a/tests/simlink/common/simlink_multi_instance_cocotb.py +++ b/tests/simlink/common/simlink_multi_instance_cocotb.py @@ -30,6 +30,8 @@ from cocotb.clock import Clock from cocotb.triggers import ReadOnly, RisingEdge, Timer +from tests.common.regression_utils import sample_after_tpd + from tests.simlink.common.peer_orchestration import ( spawn_peer_group, terminate_peers, @@ -79,6 +81,7 @@ def _pack(values, width): async def _memory_slaves(dut): + """Lifetime agent: serve memory requests until the owner cancels it.""" stores = [{}, {}] read_countdown = [0, 0] read_data = [0, 0] @@ -127,6 +130,7 @@ async def _memory_slaves(dut): async def _receive_monitor(dut, stream_received, sideband_opcodes, sideband_remdata): + """Lifetime agent: collect transport outputs until the owner cancels it.""" while True: await RisingEdge(dut.clock) await ReadOnly() @@ -223,8 +227,7 @@ async def rogue_simlink_multi_instance_traffic_test(dut): dut.sideBandTxData.value = _pack((vectors[2] for vectors in sideband_vectors), 8) dut.sideBandTxEn.value = 0x3 - await RisingEdge(dut.clock) - await Timer(1, unit="ns") + await sample_after_tpd(dut.clock) dut.streamIbValid.value = 0 dut.streamIbLast.value = 0 dut.sideBandTxEn.value = 0 @@ -256,7 +259,9 @@ def pending_inbound(): return missing deadline = time.monotonic() + MAX_TRAFFIC_SECONDS - while True: + running = list(peers) + inbound = pending_inbound() + while time.monotonic() < deadline: await RisingEdge(dut.clock) await ReadOnly() @@ -264,13 +269,13 @@ def pending_inbound(): inbound = pending_inbound() if not running and not inbound: break - if time.monotonic() >= deadline: - raise TimeoutError( - f"multi-instance traffic did not finish within " - f"{MAX_TRAFFIC_SECONDS}s: peers still running " - f"{[(peer.mode, peer.tag, peer.port) for peer in running]}, " - f"DUT still missing {inbound}" - ) + else: + raise TimeoutError( + f"multi-instance traffic did not finish within " + f"{MAX_TRAFFIC_SECONDS}s: peers still running " + f"{[(peer.mode, peer.tag, peer.port) for peer in running]}, " + f"DUT still missing {inbound}" + ) for peer in peers: assert peer.returncode == 0, f"peer exited with code {peer.returncode}" diff --git a/tests/simlink/ghdl/test_RogueTcpStreamWrap.py b/tests/simlink/ghdl/test_RogueTcpStreamWrap.py index e64e878220..f36476549a 100644 --- a/tests/simlink/ghdl/test_RogueTcpStreamWrap.py +++ b/tests/simlink/ghdl/test_RogueTcpStreamWrap.py @@ -256,11 +256,11 @@ async def stream_wrapper_test(dut): tb = TB(dut) if CASE.get("chan_count", 1) != 1: await _run_elaboration_only(tb) - return - await _run_round_trip(tb) - await _run_sparse_tkeep(tb) - if CASE["paced"]: - await _run_pacing(tb) + else: + await _run_round_trip(tb) + await _run_sparse_tkeep(tb) + if CASE["paced"]: + await _run_pacing(tb) @pytest.mark.parametrize("case_name", CASES) diff --git a/tests/simlink/native/simlink_stream_overload_probe.py b/tests/simlink/native/simlink_stream_overload_probe.py index 4ce14beb54..4dbbcade22 100644 --- a/tests/simlink/native/simlink_stream_overload_probe.py +++ b/tests/simlink/native/simlink_stream_overload_probe.py @@ -30,6 +30,7 @@ NULL_FD = os.open(os.devnull, os.O_WRONLY) os.dup2(NULL_FD, sys.stdout.fileno()) os.close(NULL_FD) +RECEIVE_TIMEOUT_SECONDS = 4.0 def _emit(event, operation, completed_messages, **fields): @@ -121,8 +122,9 @@ def _receive_probe(lib, port, cycle_sleep): context = _create(lib, port) _emit("ready", "socket_bind", 0) cycles = 0 + deadline = time.monotonic() + RECEIVE_TIMEOUT_SECONDS try: - while True: + while time.monotonic() < deadline: result, valid, _ = stream_cycle(lib, context, port, ob_ready=1) if result != 1: raise RuntimeError("Stream update failed") @@ -131,6 +133,11 @@ def _receive_probe(lib, port, cycle_sleep): _emit("received", "nonblocking_receive", 1, cycles=cycles) break time.sleep(cycle_sleep) + else: + raise TimeoutError( + f"Stream receive did not complete within {RECEIVE_TIMEOUT_SECONDS}s " + f"after {cycles} cycles" + ) finally: lib.rogueTcpStreamDestroy(context) _emit("destroyed", "socket_cleanup", 1, cycles=cycles) diff --git a/tests/simlink/rogue/test_RogueSideBandRogue.py b/tests/simlink/rogue/test_RogueSideBandRogue.py index b78ab4ed9e..25fc578efd 100644 --- a/tests/simlink/rogue/test_RogueSideBandRogue.py +++ b/tests/simlink/rogue/test_RogueSideBandRogue.py @@ -8,6 +8,16 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## +# Test methodology: +# - Sweep: Exchange one opcode and one remData value in each direction across a +# real PyRogue SideBandSim process and the GHDL SimLink model. +# - Stimulus: Start the external client first, then drive the HDL-to-client +# opcode before remData so the transport is warm before the reply. +# - Checks: Require the client's JSON result and the DUT's received opcode and +# remData to match the independent constants in both directions. +# - Timing: Bound interpreter discovery, client readiness, result creation, and +# reply observation; always terminate the child process in cleanup. +# # Real-Rogue SideBand contract: a production pyrogue.interfaces.simulation.SideBandSim # (separate process) exchanges one opcode and one remData each direction with # RogueSideBandFlatHarness under GHDL. cocotb is the firmware-side sideband diff --git a/tests/simlink/rogue/test_RogueStreamRogue.py b/tests/simlink/rogue/test_RogueStreamRogue.py index 9396240ebf..aa75cb8277 100644 --- a/tests/simlink/rogue/test_RogueStreamRogue.py +++ b/tests/simlink/rogue/test_RogueStreamRogue.py @@ -8,6 +8,16 @@ ## the terms contained in the LICENSE.txt file. ############################################################################## +# Test methodology: +# - Sweep: Exchange one frame in each direction across a real Rogue TcpClient +# process and the GHDL RogueTcpStream wrapper. +# - Stimulus: Drive the HDL-to-client frame first to establish the ZeroMQ path, +# then receive the independently generated client-to-HDL frame. +# - Checks: Compare both payloads byte-for-byte and require the client JSON +# result to report successful receipt of the HDL frame. +# - Timing: Bound interpreter discovery, client readiness, result creation, and +# the finite cocotb receive task; always terminate the child in cleanup. +# # Real-Rogue Stream contract: a production rogue.interfaces.stream.TcpClient # (separate process) exchanges one frame each direction with RogueTcpStreamWrap # under GHDL. cocotb is the firmware-side AXI-Stream endpoint and drives the diff --git a/tests/xilinx/general/gt_rx_align_check_test_utils.py b/tests/xilinx/general/gt_rx_align_check_test_utils.py index 7cba9472f6..60102e6819 100644 --- a/tests/xilinx/general/gt_rx_align_check_test_utils.py +++ b/tests/xilinx/general/gt_rx_align_check_test_utils.py @@ -23,7 +23,9 @@ from __future__ import annotations import cocotb -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import RisingEdge + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiResp @@ -104,7 +106,8 @@ def __init__(self, dut, *, phases): dut.M_AXI_RRESP.setimmediatevalue(0) dut.M_AXI_RDATA.setimmediatevalue(0) - cocotb.start_soon(self._run_read()) + # Lifetime DRP responder retained by its bus-model owner. + self._responder_task = cocotb.start_soon(self._run_read()) def set_phases(self, phases) -> None: """Replace the scripted phase sequence and restart from its head.""" @@ -129,8 +132,7 @@ def in_reset(self) -> bool: async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def _wait_while_reset(self) -> None: while self.in_reset(): @@ -139,6 +141,7 @@ async def _wait_while_reset(self) -> None: await self.cycle(1) async def _run_read(self) -> None: + """Lifetime agent: serve DRP reads until cocotb ends the test.""" while True: await self._wait_while_reset() @@ -214,7 +217,8 @@ def __init__( dut.resetDone.setimmediatevalue(0) dut.resetErr.setimmediatevalue(0) - cocotb.start_soon(self._run()) + # Lifetime GT peer retained by its model owner. + self._model_task = cocotb.start_soon(self._run()) def arm_error(self, *, lead_cycles: int = 0) -> None: """Make the next alignment attempt report an error.""" @@ -251,6 +255,7 @@ def _reset_requested(self) -> bool: return False async def _run(self) -> None: + """Lifetime agent: model GT buffer bypass until cocotb ends the test.""" while True: # Track the checker's reset request. resetOut is registered in the # axilClk domain, so sample it from the RX clock like real hardware. @@ -303,7 +308,8 @@ def __init__(self, dut): self.pulses = 0 self.max_width = 0 self._width = 0 - cocotb.start_soon(self._run()) + # Lifetime observer retained by its monitor owner. + self._monitor_task = cocotb.start_soon(self._run()) def reset_counts(self) -> None: self.pulses = 0 @@ -311,10 +317,10 @@ def reset_counts(self) -> None: self._width = 0 async def _run(self) -> None: + """Lifetime agent: monitor reset pulses until cocotb ends the test.""" previous = 0 while True: - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) try: current = int(self.dut.resetOut.value) except ValueError: diff --git a/tests/xilinx/general/test_GtRxAlignCheck.py b/tests/xilinx/general/test_GtRxAlignCheck.py index 7f20010b8c..efa49e8dab 100644 --- a/tests/xilinx/general/test_GtRxAlignCheck.py +++ b/tests/xilinx/general/test_GtRxAlignCheck.py @@ -45,7 +45,9 @@ import cocotb import pytest from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer +from cocotb.triggers import Timer + +from tests.common.regression_utils import sample_after_tpd from cocotbext.axi import AxiLiteBus, AxiLiteMaster, AxiResp from tests.axi.utils import axil_read_u32, axil_write_u32 @@ -54,6 +56,7 @@ env_int, parameter_case, run_surf_vhdl_test, + wait_after_edge_offset, ) from tests.xilinx.general.gt_rx_align_check_test_utils import ( CONFIG_ADDR, @@ -128,8 +131,7 @@ def __init__(self, dut, *, phases=(MATCHING_PHASE,)): async def cycle(self, count: int = 1) -> None: for _ in range(count): - await RisingEdge(self.dut.axilClk) - await Timer(1, unit="ns") + await sample_after_tpd(self.dut.axilClk) async def reset(self) -> None: # Hold axilRst so the checker restarts from REG_INIT_C, then let the GT @@ -198,8 +200,10 @@ async def write_config(self, *, target: int, mask: int, rst_len: int) -> None: async def pulse_reset_in(self, *, hold_cycles: int = 4) -> None: # resetIn is documented ASYNC to axilClk, so move it mid-period rather # than on an edge to exercise the synchronizer instead of a clean setup. - await RisingEdge(self.dut.axilClk) - await Timer(RESET_IN_SKEW_NS, unit="ns") + await wait_after_edge_offset( + self.dut.axilClk, + offset_time=RESET_IN_SKEW_NS, + ) self.dut.resetIn.value = 1 await self.cycle(hold_cycles) await Timer(RESET_IN_SKEW_NS, unit="ns") 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" }