diff --git a/adidt/devices/clocks/ad952x.py b/adidt/devices/clocks/ad952x.py index 08486e2d..05508ea2 100644 --- a/adidt/devices/clocks/ad952x.py +++ b/adidt/devices/clocks/ad952x.py @@ -121,10 +121,20 @@ def extra_dt_lines(self, context: dict | None = None) -> list[str]: lines: list[str] = [] for gl in self.gpio_lines: lines.append(f"{gl.prop} = <&{gl.controller} {int(gl.index)} 0>;") + if self.jesd204_sysref_provider: + lines.append("jesd204-device;") + lines.append("#jesd204-cells = <2>;") + lines.append("jesd204-sysref-provider;") + if self.jesd204_max_sysref_hz is not None: + lines.append( + f"adi,jesd204-max-sysref-frequency-hz = <{int(self.jesd204_max_sysref_hz)}>;" + ) return lines # Both device families share these fields: gpio_lines: Annotated[list[_GpioLine], DtSkip()] = Field(default_factory=list) + jesd204_sysref_provider: Annotated[bool, DtSkip()] = False + jesd204_max_sysref_hz: Annotated[int | None, DtSkip()] = None class AD9523_1(_AD952xBase): diff --git a/adidt/eval/adrv937x_fmc.py b/adidt/eval/adrv937x_fmc.py index 2627c86b..4e550c57 100644 --- a/adidt/eval/adrv937x_fmc.py +++ b/adidt/eval/adrv937x_fmc.py @@ -42,8 +42,8 @@ def __init__(self, *, reference_frequency: int = 122_880_000) -> None: label="trx0_ad9371", node_name_base="ad9371-phy", compatible_strings=["adi,ad9371"], - reset_gpio=130, - sysref_req_gpio=136, + reset_gpio=106, + sysref_req_gpio=112, ) def _named(self, name: str) -> ClockOutput: diff --git a/adidt/xsa/builders/adrv937x.py b/adidt/xsa/builders/adrv937x.py index 0d709eda..eb03802e 100644 --- a/adidt/xsa/builders/adrv937x.py +++ b/adidt/xsa/builders/adrv937x.py @@ -16,6 +16,7 @@ JesdLinkModel, ) from ...devices.clocks import AD9528_1 +from ...devices.clocks.ad952x import AD9528_1Channel, _GpioLine from ...devices.fpga_ip import build_jesd204_overlay_ctx from ...model.renderer import BoardModelRenderer from ..topology import XsaTopology @@ -23,6 +24,40 @@ _ADRV937X_KEYWORDS = ("ad9371", "adrv937") +# Default Mykonos (AD9371) initial device profile — baked into the DT +# as ``adi,*-profile-*`` / ``adi,clocks-*`` properties on +# ``ad9371-phy@1`` when the caller doesn't supply a per-profile +# override. +# +# The AD9371 driver consumes these at probe to configure the Mykonos +# ARM before userspace ever sees the chip; the values must encode a +# configuration whose JESD framing (M, L, F, K, Np, CS, CF) matches +# the FPGA's compiled-in ``axi-jesd204-{tx,rx}`` overlays, otherwise +# the deframer reports an ILAS mismatch at link-up and the TPL DMA +# sits idle. Because "matching" is HDL-build-specific (see the +# ``TX_JESD_*`` / ``RX_JESD_*`` knobs in +# ``analogdevicesinc/hdl/projects/adrv937x/zc706/README.md``), this +# module ships an empty default and expects the per-board profile +# JSON (e.g. ``adidt/xsa/profiles/adrv937x_zc706.json``) to supply a +# full ``trx_profile_props`` list. Callers without a profile JSON +# can still override via ``board_cfg["trx_profile_props"]``. +_DEFAULT_MYKONOS_PROFILE_PROPS: tuple[str, ...] = () + + +# AD9528 output-channel map baked into the DT so the clock distributor +# driver configures dividers + signal sources before the Mykonos driver +# requests ``dev_clk`` at 122.88 MHz. Each AD9528_1Channel renders to +# an ``adi,channels/channel@N`` subnode. Values mirror the Kuiper +# zc706-adrv9371 reference DT. +def _default_ad9528_channels() -> dict[int, AD9528_1Channel]: + return { + 13: AD9528_1Channel(id=13, name="DEV_CLK", divider=10, signal_source=0), + 1: AD9528_1Channel(id=1, name="FMC_CLK", divider=10, signal_source=0), + 12: AD9528_1Channel(id=12, name="DEV_SYSREF", divider=10, signal_source=2), + 3: AD9528_1Channel(id=3, name="FMC_SYSREF", divider=10, signal_source=2), + } + + def _is_adrv937x_name(value: str) -> bool: lower = value.lower() return any(key in lower for key in _ADRV937X_KEYWORDS) @@ -186,8 +221,14 @@ def build_model( spi_bus = str(board_cfg.get("spi_bus", "spi0")) clk_cs = int(board_cfg.get("clk_cs", 0)) trx_cs = int(board_cfg.get("trx_cs", 1)) - trx_reset_gpio = int(board_cfg.get("trx_reset_gpio", 130)) - trx_sysref_req_gpio = int(board_cfg.get("trx_sysref_req_gpio", 136)) + # GPIO defaults match the Kuiper ``zc706-adrv9371`` reference DT + # (EMIO pin numbering relative to Zynq GPIO controller base=54): + # - AD9371 reset = 106 (``trx_reset_gpio``) + # - AD9371 sysref = 112 (``trx_sysref_req_gpio``) + # - AD9528 reset = 113 (``ad9528_reset_gpio``) + trx_reset_gpio = int(board_cfg.get("trx_reset_gpio", 106)) + trx_sysref_req_gpio = int(board_cfg.get("trx_sysref_req_gpio", 112)) + ad9528_reset_gpio = int(board_cfg.get("ad9528_reset_gpio", 113)) trx_spi_max_frequency = int(board_cfg.get("trx_spi_max_frequency", 25000000)) ad9528_vcxo_freq = int(board_cfg.get("ad9528_vcxo_freq", 122880000)) @@ -208,6 +249,23 @@ def build_model( label=clock_chip_label, spi_max_hz=10_000_000, vcxo_hz=ad9528_vcxo_freq, + channels=_default_ad9528_channels(), + gpio_lines=[ + _GpioLine( + prop="reset-gpios", + controller=gpio_label, + index=ad9528_reset_gpio, + ), + ], + # Mark AD9528 as the JESD204 topology's SYSREF provider. + # Without this the AD9371 driver's + # ``opt_post_running_stage`` callback can't find a sysref + # source in the jesd204 graph and rolls back with -EFAULT. + # Matches the Kuiper zc706-adrv9371 reference DT. + jesd204_sysref_provider=True, + jesd204_max_sysref_hz=int( + board_cfg.get("ad9528_jesd204_max_sysref_hz", 78125) + ), ) clock_component = ComponentModel( role="clock", @@ -234,9 +292,19 @@ def build_model( trx_clock_names_value = ( '"dev_clk", "fmc_clk", "sysref_dev_clk", "sysref_fmc_clk"' ) - trx_link_ids_value = f"{rx_link_id} {tx_link_id}" + # Add the AD9528 sysref-provider link to jesd204-link-ids and + # jesd204-inputs so the AD9371 driver's post-running verify + # sees a full RX / TX / sysref topology. Matches the working + # Kuiper zc706-adrv9371 reference (RX=1, TX=0, SYSREF=2). + ad9528_sysref_link_id = 2 + trx_link_ids_value = f"{rx_link_id} {tx_link_id} {ad9528_sysref_link_id}" trx_inputs_value = ( - f"<&{rx_xcvr_label} 0 {rx_link_id}>, <&{tx_xcvr_label} 0 {tx_link_id}>" + f"<&{rx_xcvr_label} 0 {rx_link_id}>, " + f"<&{tx_xcvr_label} 0 {tx_link_id}>, " + f"<&{clock_chip_label} 0 {ad9528_sysref_link_id}>" + ) + profile_props = tuple( + board_cfg.get("trx_profile_props", _DEFAULT_MYKONOS_PROFILE_PROPS) ) phy_context = { "gpio_label": gpio_label, @@ -244,6 +312,7 @@ def build_model( "clock_names_value": trx_clock_names_value, "link_ids": trx_link_ids_value, "jesd204_inputs": trx_inputs_value, + "profile_props": profile_props, } phy_component = ComponentModel( role="transceiver", @@ -315,6 +384,15 @@ def build_model( ] # --- Raw XCVR overlay nodes --- + # The ``adi,sys-clk-select = <0>`` + ``adi,out-clk-select = + # <3>`` + ``adi,use-lpm-enable`` triplet is required by the + # axi-adxcvr driver to bind — without them the platform + # device sits in deferred probe on this build. Clocks are + # kept pointing at the clkgen (through-path) and the + # ``div40`` shape is preserved to minimise risk of a kernel + # hang (the direct-AD9528 rewire attempted in commit 607663b + # did match the reference DT structurally but hung the + # kernel post-JESD). rx_xcvr_node = ( f"\t&{rx_xcvr_label} {{\n" '\t\tcompatible = "adi,axi-adxcvr-1.0";\n' @@ -322,6 +400,9 @@ def build_model( '\t\tclock-names = "conv", "div40";\n' "\t\t#clock-cells = <1>;\n" '\t\tclock-output-names = "rx_gt_clk", "rx_out_clk";\n' + "\t\tadi,sys-clk-select = <0>;\n" + "\t\tadi,out-clk-select = <3>;\n" + "\t\tadi,use-lpm-enable;\n" "\t\tjesd204-device;\n" "\t\t#jesd204-cells = <2>;\n" "\t};" @@ -333,6 +414,9 @@ def build_model( '\t\tclock-names = "conv", "div40";\n' "\t\t#clock-cells = <1>;\n" '\t\tclock-output-names = "tx_gt_clk", "tx_out_clk";\n' + "\t\tadi,sys-clk-select = <0>;\n" + "\t\tadi,out-clk-select = <3>;\n" + "\t\tadi,use-lpm-enable;\n" "\t\tjesd204-device;\n" "\t\t#jesd204-cells = <2>;\n" "\t};" diff --git a/adidt/xsa/profiles.py b/adidt/xsa/profiles.py index 74fa35b6..76ece96a 100644 --- a/adidt/xsa/profiles.py +++ b/adidt/xsa/profiles.py @@ -30,6 +30,8 @@ "trx_sysref_req_gpio", "trx_spi_max_frequency", "ad9528_vcxo_freq", + "ad9528_reset_gpio", + "ad9528_jesd204_max_sysref_hz", "rx_link_id", "rx_os_link_id", "tx_link_id", @@ -154,6 +156,8 @@ "trx_sysref_req_gpio", "trx_spi_max_frequency", "ad9528_vcxo_freq", + "ad9528_reset_gpio", + "ad9528_jesd204_max_sysref_hz", "rx_link_id", "rx_os_link_id", "tx_link_id", diff --git a/adidt/xsa/profiles/adrv937x_zc706.json b/adidt/xsa/profiles/adrv937x_zc706.json index ef51e9fb..309ae4d0 100644 --- a/adidt/xsa/profiles/adrv937x_zc706.json +++ b/adidt/xsa/profiles/adrv937x_zc706.json @@ -2,19 +2,73 @@ "name": "adrv937x_zc706", "defaults": { "adrv9009_board": { - "misc_clk_hz": 245760000, + "misc_clk_hz": 122880000, "spi_bus": "spi0", "clk_cs": 0, "trx_cs": 1, - "trx_reset_gpio": 130, - "trx_sysref_req_gpio": 136, + "trx_reset_gpio": 106, + "trx_sysref_req_gpio": 112, + "ad9528_reset_gpio": 113, "trx_spi_max_frequency": 25000000, "ad9528_vcxo_freq": 122880000, "rx_link_id": 1, "rx_os_link_id": 2, "tx_link_id": 0, "tx_octets_per_frame": 2, - "rx_os_octets_per_frame": 2 + "rx_os_octets_per_frame": 2, + "trx_profile_props": [ + "adi,clocks-clk-pll-vco-freq_khz = <0x960000>;", + "adi,clocks-device-clock_khz = <0x1e000>;", + "adi,clocks-clk-pll-hs-div = <0x04>;", + "adi,clocks-clk-pll-vco-div = <0x02>;", + "adi,jesd204-obs-framer-over-sample = <0x00>;", + "adi,rx-profile-adc-div = <0x01>;", + "adi,rx-profile-en-high-rej-dec5 = <0x01>;", + "adi,rx-profile-iq-rate_khz = <0x1e000>;", + "adi,rx-profile-rf-bandwidth_hz = <0x5f5e100>;", + "adi,rx-profile-rhb1-decimation = <0x01>;", + "adi,rx-profile-rx-bbf-3db-corner_khz = <0x186a0>;", + "adi,rx-profile-rx-dec5-decimation = <0x05>;", + "adi,rx-profile-rx-fir-decimation = <0x02>;", + "adi,rx-profile-rx-fir-gain_db = <0xfffffffa>;", + "adi,rx-profile-rx-fir-num-fir-coefs = <0x30>;", + "adi,rx-profile-rx-fir-coefs = <0xfffbffe6 0x200033 0xffbdff8c 0x8c00d4 0xff04fe91 0x1ad0253 0xfd50fc5d 0x4300593 0xf98ef774 0xa340da8 0xed3ee259 0x25b87e3d 0x7e3d25b8 0xe259ed3e 0xda80a34 0xf774f98e 0x5930430 0xfc5dfd50 0x25301ad 0xfe91ff04 0xd4008c 0xff8cffbd 0x330020 0xffe6fffb>;", + "adi,rx-profile-custom-adc-profile = <0x2160182 0xc90062 0x50001eb 0x6370117 0x51a0068 0x318001c 0x300027 0x1700bb>;", + "adi,obs-profile-adc-div = <0x01>;", + "adi,obs-profile-en-high-rej-dec5 = <0x01>;", + "adi,obs-profile-iq-rate_khz = <0x3c000>;", + "adi,obs-profile-rf-bandwidth_hz = <0xbebc200>;", + "adi,obs-profile-rhb1-decimation = <0x01>;", + "adi,obs-profile-rx-bbf-3db-corner_khz = <0x186a0>;", + "adi,obs-profile-rx-dec5-decimation = <0x05>;", + "adi,obs-profile-rx-fir-decimation = <0x01>;", + "adi,obs-profile-rx-fir-gain_db = <0x06>;", + "adi,obs-profile-rx-fir-num-fir-coefs = <0x18>;", + "adi,obs-profile-rx-fir-coefs = <0xfedf0051 0xffe9ffaa 0xe5fe9e 0x18dff17 0xfd6f06a3 0xefb459e2 0xefb406a3 0xfd6fff17 0x18dfe9e 0xe5ffaa 0xffe90051 0xfedf0000>;", + "adi,obs-profile-custom-adc-profile = <0x1c2015d 0xc90062 0x50002da 0x65a0332 0x5c402dc 0x3420014 0x290024 0x1800c8>;", + "adi,obs-settings-custom-loopback-adc-profile = <0x2390171 0xc90062 0x5000123 0x6050095 0x528003a 0x3270022 0x300028 0x1700bd>;", + "adi,tx-profile-dac-div = <0x01>;", + "adi,tx-profile-iq-rate_khz = <0x3c000>;", + "adi,tx-profile-primary-sig-bandwidth_hz = <0x47868c0>;", + "adi,tx-profile-rf-bandwidth_hz = <0xbebc200>;", + "adi,tx-profile-thb1-interpolation = <0x02>;", + "adi,tx-profile-thb2-interpolation = <0x01>;", + "adi,tx-profile-tx-bbf-3db-corner_khz = <0x186a0>;", + "adi,tx-profile-tx-dac-3db-corner_khz = <0x2da78>;", + "adi,tx-profile-tx-fir-interpolation = <0x01>;", + "adi,tx-profile-tx-input-hb-interpolation = <0x01>;", + "adi,tx-profile-tx-fir-gain_db = <0x06>;", + "adi,tx-profile-tx-fir-num-fir-coefs = <0x10>;", + "adi,tx-profile-tx-fir-coefs = <0x6fef2 0xcbff58 0xffac03d7 0xf36a5297 0xf36a03d7 0xffacff58 0xcbfef2 0x60000>;", + "adi,sniffer-profile-adc-div = <0x01>;", + "adi,sniffer-profile-en-high-rej-dec5 = <0x00>;", + "adi,sniffer-profile-iq-rate_khz = <0x7800>;", + "adi,sniffer-profile-rf-bandwidth_hz = <0x1312d00>;", + "adi,sniffer-profile-rhb1-decimation = <0x02>;", + "adi,sniffer-profile-rx-bbf-3db-corner_khz = <0x186a0>;", + "adi,sniffer-profile-rx-dec5-decimation = <0x05>;", + "adi,sniffer-profile-rx-fir-decimation = <0x04>;" + ] } } } diff --git a/doc/source/developer/hardware_ci.rst b/doc/source/developer/hardware_ci.rst index e8c7225a..b09f01c2 100644 --- a/doc/source/developer/hardware_ci.rst +++ b/doc/source/developer/hardware_ci.rst @@ -136,7 +136,9 @@ Adding a new hardware node One-time setup: 1. Stand up an exporter that registers a new place ```` with - the coordinator at ``10.0.0.41:20408``. Verify with + the coordinator at ``10.0.0.41:20408`` — install it as a systemd + unit via ``scripts/labgrid-exporter/install.sh`` + (see :ref:`exporter-systemd`). Verify with ``labgrid-client -x 10.0.0.41:20408 places``. 2. Register a new self-hosted runner with labels ``self-hosted,hw-`` on the host physically attached to the @@ -286,6 +288,69 @@ Coordinator-mode tests additionally require SSH key-auth from the ``MassStorageDriver`` SSH-proxy path) and write access to ``~/.cache/adidt/kernel/`` for the kernel image cache. +.. _exporter-systemd: + +Exporter systemd service +------------------------ + +Each hw-node also runs a ``labgrid-exporter`` process that publishes +its local hardware resources to the coordinator. Production hosts +run it as a systemd template unit — ``labgrid-exporter@`` — +installed by ``scripts/labgrid-exporter/install.sh``: + +.. code-block:: bash + + # On the exporter host (bq, mini2, nuc, ...): + cd /path/to/pyadi-dt + sudo scripts/labgrid-exporter/install.sh + + # Concrete example — nuc: + sudo scripts/labgrid-exporter/install.sh nuc \ + /home/tcollins/dev/lg-coordinator/lg_fmcdaq3_vcu118_exporter.yaml + +The installer writes two files and enables the unit: + +- ``/etc/systemd/system/labgrid-exporter@.service`` — the template + unit. ``User=`` is baked in at install time (defaults to + ``$SUDO_USER``; override with ``--user``). +- ``/etc/default/labgrid-exporter-`` — per-instance env + (``LG_EXPORTER_BIN``, ``LG_COORDINATOR``, ``LG_EXPORTER_NAME``, + ``LG_EXPORTER_YAML``, ``PATH``). + +Install-time options: + +- ``--coordinator ADDR`` — default ``10.0.0.41:20408``. +- ``--user USER`` — default ``$SUDO_USER``. +- ``--bin PATH`` — default auto-detected via ``command -v`` + as the service user (picks up the + ``~/.local/share/uv/tools/labgrid/bin/labgrid-exporter`` that + ``uv tool install labgrid`` produces). +- ``--ser2net-path DIR`` — prepend DIR to the service PATH so + ``labgrid-exporter`` finds the right ``ser2net`` binary (e.g. + ``$HOME/opt/ser2net-4.6.1/sbin``). Omit if ``ser2net`` on the + system PATH is the one you want. +- ``--no-start`` — install files but don't + ``enable --now``. + +Day-to-day operation: + +.. code-block:: bash + + # After a yaml change: + sudo systemctl restart labgrid-exporter@ + + # Live logs: + journalctl -u labgrid-exporter@ -f + + # Unit status: + systemctl status labgrid-exporter@ + +``Restart=on-failure`` brings the exporter back after a crash, and +``multi-user.target`` is the enable target, so the exporter also +starts automatically after a reboot. Re-running ``install.sh`` is +idempotent — safe to use to pick up a new coordinator address, +binary path, or yaml file. + Fork-PR approval gate --------------------- @@ -397,8 +462,12 @@ Troubleshooting **One node's jobs skip while others run.** The corresponding exporter is not advertising its place to the - coordinator. SSH to that node, restart the exporter, and verify - the place shows up in ``labgrid-client places``. + coordinator. SSH to that node and restart the unit + (``sudo systemctl restart labgrid-exporter@`` — see + :ref:`exporter-systemd`), then verify the place shows up in + ``labgrid-client places``. Check + ``journalctl -u labgrid-exporter@ -n 50`` if the restart + alone doesn't recover it. **``hw-direct`` fails with "LG_DIRECT_ENV is not set".** The runner's ``~/actions-runner/.env`` does not define @@ -421,8 +490,9 @@ Troubleshooting Check the ``uart_log_kernel_banner_attempt1_*.txt`` artifact — if it's empty, the first power-on was silent (the pre-emptive cold-cycle should have handled this, so getting here usually means - the serial exporter on the hw node is misbehaving). Restart - ``ser2net`` on the exporter host. + the serial exporter on the hw node is misbehaving). Restart the + exporter — ``sudo systemctl restart labgrid-exporter@`` — + which also respawns ``ser2net``. **Board left powered on after a local test run.** The ``board`` fixture in ``test/hw/conftest.py`` powers the board diff --git a/scripts/labgrid-exporter/install.sh b/scripts/labgrid-exporter/install.sh new file mode 100755 index 00000000..3c177c82 --- /dev/null +++ b/scripts/labgrid-exporter/install.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# install.sh — install a systemd service for a labgrid exporter instance. +# +# Usage: +# sudo ./install.sh [OPTIONS] +# +# Example: +# sudo ./install.sh nuc \ +# /home/tcollins/dev/lg-coordinator/lg_fmcdaq3_vcu118_exporter.yaml +# +# Options: +# --coordinator ADDR Coordinator host:port (default: 10.0.0.41:20408) +# --user USER Service runs as this user (default: $SUDO_USER or $USER) +# --bin PATH Path to labgrid-exporter binary +# (default: auto-detect via `command -v` as the user) +# --ser2net-path DIR Prepend DIR to the service PATH for ser2net discovery +# --no-start Don't enable/start the unit after install +# +# After install: +# systemctl status labgrid-exporter@ +# journalctl -u labgrid-exporter@ -f +# sudo systemctl restart labgrid-exporter@ # after a yaml change +# +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "ERROR: must run as root (use sudo)" >&2 + exit 1 +fi + +usage() { sed -n '2,/^$/p' "$0"; } + +if [ $# -lt 2 ]; then + usage >&2 + exit 1 +fi + +NAME="$1" +YAML="$2" +shift 2 + +COORDINATOR="10.0.0.41:20408" +SERVICE_USER="${SUDO_USER:-$USER}" +EXPORTER_BIN="" +SER2NET_PATH="" +START=1 + +while [ $# -gt 0 ]; do + case "$1" in + --coordinator) COORDINATOR="$2"; shift 2 ;; + --user) SERVICE_USER="$2"; shift 2 ;; + --bin) EXPORTER_BIN="$2"; shift 2 ;; + --ser2net-path) SER2NET_PATH="$2"; shift 2 ;; + --no-start) START=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "ERROR: unknown option $1" >&2; exit 1 ;; + esac +done + +if ! id "$SERVICE_USER" >/dev/null 2>&1; then + echo "ERROR: user '$SERVICE_USER' does not exist" >&2 + exit 1 +fi + +if [ -z "$EXPORTER_BIN" ]; then + EXPORTER_BIN=$(sudo -u "$SERVICE_USER" -H bash -lc 'command -v labgrid-exporter' || true) + if [ -z "$EXPORTER_BIN" ]; then + echo "ERROR: labgrid-exporter not on PATH for user $SERVICE_USER." >&2 + echo " Install it (e.g., 'uv tool install labgrid') or pass --bin PATH." >&2 + exit 1 + fi +fi +if [ ! -x "$EXPORTER_BIN" ]; then + echo "ERROR: $EXPORTER_BIN is not executable" >&2 + exit 1 +fi + +YAML_ABS=$(readlink -f "$YAML") +if [ ! -r "$YAML_ABS" ]; then + echo "ERROR: YAML $YAML_ABS not readable" >&2 + exit 1 +fi + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +UNIT_SRC="$SCRIPT_DIR/labgrid-exporter@.service" +UNIT_DST="/etc/systemd/system/labgrid-exporter@.service" +ENV_DST="/etc/default/labgrid-exporter-$NAME" + +if [ ! -f "$UNIT_SRC" ]; then + echo "ERROR: unit template not found at $UNIT_SRC" >&2 + exit 1 +fi + +BASE_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +if [ -n "$SER2NET_PATH" ]; then + SERVICE_PATH="$SER2NET_PATH:$BASE_PATH" +else + SERVICE_PATH="$BASE_PATH" +fi + +echo "Installing labgrid-exporter service:" +echo " instance : $NAME" +echo " user : $SERVICE_USER" +echo " binary : $EXPORTER_BIN" +echo " coordinator : $COORDINATOR" +echo " yaml : $YAML_ABS" +echo " PATH : $SERVICE_PATH" +echo + +sed "s|@SERVICE_USER@|$SERVICE_USER|g" "$UNIT_SRC" > "$UNIT_DST" +chmod 644 "$UNIT_DST" + +cat > "$ENV_DST" <; - clock-frequency = <245760000>; + clock-frequency = <122880000>; }; &axi_ad9371_rx_clkgen { compatible = "adi,axi-clkgen-2.00.a"; @@ -155,6 +155,9 @@ clock-names = "conv", "div40"; #clock-cells = <1>; clock-output-names = "rx_gt_clk", "rx_out_clk"; + adi,sys-clk-select = <0>; + adi,out-clk-select = <3>; + adi,use-lpm-enable; jesd204-device; #jesd204-cells = <2>; }; @@ -164,6 +167,9 @@ clock-names = "conv", "div40"; #clock-cells = <1>; clock-output-names = "tx_gt_clk", "tx_out_clk"; + adi,sys-clk-select = <0>; + adi,out-clk-select = <3>; + adi,use-lpm-enable; jesd204-device; #jesd204-cells = <2>; }; @@ -209,9 +215,46 @@ adi,osc-in-cmos-neg-inp-enable; adi,sysref-request-enable; reg = <0>; + reset-gpios = <&gpio0 113 0>; + jesd204-device; + #jesd204-cells = <2>; + jesd204-sysref-provider; + adi,jesd204-max-sysref-frequency-hz = <78125>; spi-max-frequency = <10000000>; adi,vcxo-freq = <122880000>; clock-output-names = "ad9528-1_out0", "ad9528-1_out1", "ad9528-1_out2", "ad9528-1_out3", "ad9528-1_out4", "ad9528-1_out5", "ad9528-1_out6", "ad9528-1_out7", "ad9528-1_out8", "ad9528-1_out9", "ad9528-1_out10", "ad9528-1_out11", "ad9528-1_out12", "ad9528-1_out13"; + ad9528_0_c1: channel@1 { + adi,driver-mode = <0>; + adi,divider-phase = <0>; + reg = <1>; + adi,extended-name = "FMC_CLK"; + adi,channel-divider = <10>; + adi,signal-source = <0>; + }; + ad9528_0_c3: channel@3 { + adi,driver-mode = <0>; + adi,divider-phase = <0>; + reg = <3>; + adi,extended-name = "FMC_SYSREF"; + adi,channel-divider = <10>; + adi,signal-source = <2>; + }; + ad9528_0_c12: channel@12 { + adi,driver-mode = <0>; + adi,divider-phase = <0>; + reg = <12>; + adi,extended-name = "DEV_SYSREF"; + adi,channel-divider = <10>; + adi,signal-source = <2>; + }; + ad9528_0_c13: channel@13 { + adi,driver-mode = <0>; + adi,divider-phase = <0>; + reg = <13>; + adi,extended-name = "DEV_CLK"; + adi,channel-divider = <10>; + adi,signal-source = <0>; + }; }; trx0_ad9371: ad9371-phy@1 { #clock-cells = <1>; clock-output-names = "rx_sampl_clk", "rx_os_sampl_clk", "tx_sampl_clk"; @@ -221,12 +264,63 @@ reg = <1>; clocks = <&clk0_ad9528 13>, <&clk0_ad9528 1>, <&clk0_ad9528 12>, <&clk0_ad9528 3>; clock-names = "dev_clk", "fmc_clk", "sysref_dev_clk", "sysref_fmc_clk"; - reset-gpios = <&gpio0 130 0>; - sysref-req-gpios = <&gpio0 136 0>; - jesd204-link-ids = <1 0>; - jesd204-inputs = <&axi_ad9371_rx_xcvr 0 1>, <&axi_ad9371_tx_xcvr 0 0>; + reset-gpios = <&gpio0 106 0>; + sysref-req-gpios = <&gpio0 112 0>; + jesd204-link-ids = <1 0 2>; + jesd204-inputs = <&axi_ad9371_rx_xcvr 0 1>, <&axi_ad9371_tx_xcvr 0 0>, <&clk0_ad9528 0 2>; compatible = "adi,ad9371"; spi-max-frequency = <25000000>; + adi,clocks-clk-pll-vco-freq_khz = <0x960000>; + adi,clocks-device-clock_khz = <0x1e000>; + adi,clocks-clk-pll-hs-div = <0x04>; + adi,clocks-clk-pll-vco-div = <0x02>; + adi,jesd204-obs-framer-over-sample = <0x00>; + adi,rx-profile-adc-div = <0x01>; + adi,rx-profile-en-high-rej-dec5 = <0x01>; + adi,rx-profile-iq-rate_khz = <0x1e000>; + adi,rx-profile-rf-bandwidth_hz = <0x5f5e100>; + adi,rx-profile-rhb1-decimation = <0x01>; + adi,rx-profile-rx-bbf-3db-corner_khz = <0x186a0>; + adi,rx-profile-rx-dec5-decimation = <0x05>; + adi,rx-profile-rx-fir-decimation = <0x02>; + adi,rx-profile-rx-fir-gain_db = <0xfffffffa>; + adi,rx-profile-rx-fir-num-fir-coefs = <0x30>; + adi,rx-profile-rx-fir-coefs = <0xfffbffe6 0x200033 0xffbdff8c 0x8c00d4 0xff04fe91 0x1ad0253 0xfd50fc5d 0x4300593 0xf98ef774 0xa340da8 0xed3ee259 0x25b87e3d 0x7e3d25b8 0xe259ed3e 0xda80a34 0xf774f98e 0x5930430 0xfc5dfd50 0x25301ad 0xfe91ff04 0xd4008c 0xff8cffbd 0x330020 0xffe6fffb>; + adi,rx-profile-custom-adc-profile = <0x2160182 0xc90062 0x50001eb 0x6370117 0x51a0068 0x318001c 0x300027 0x1700bb>; + adi,obs-profile-adc-div = <0x01>; + adi,obs-profile-en-high-rej-dec5 = <0x01>; + adi,obs-profile-iq-rate_khz = <0x3c000>; + adi,obs-profile-rf-bandwidth_hz = <0xbebc200>; + adi,obs-profile-rhb1-decimation = <0x01>; + adi,obs-profile-rx-bbf-3db-corner_khz = <0x186a0>; + adi,obs-profile-rx-dec5-decimation = <0x05>; + adi,obs-profile-rx-fir-decimation = <0x01>; + adi,obs-profile-rx-fir-gain_db = <0x06>; + adi,obs-profile-rx-fir-num-fir-coefs = <0x18>; + adi,obs-profile-rx-fir-coefs = <0xfedf0051 0xffe9ffaa 0xe5fe9e 0x18dff17 0xfd6f06a3 0xefb459e2 0xefb406a3 0xfd6fff17 0x18dfe9e 0xe5ffaa 0xffe90051 0xfedf0000>; + adi,obs-profile-custom-adc-profile = <0x1c2015d 0xc90062 0x50002da 0x65a0332 0x5c402dc 0x3420014 0x290024 0x1800c8>; + adi,obs-settings-custom-loopback-adc-profile = <0x2390171 0xc90062 0x5000123 0x6050095 0x528003a 0x3270022 0x300028 0x1700bd>; + adi,tx-profile-dac-div = <0x01>; + adi,tx-profile-iq-rate_khz = <0x3c000>; + adi,tx-profile-primary-sig-bandwidth_hz = <0x47868c0>; + adi,tx-profile-rf-bandwidth_hz = <0xbebc200>; + adi,tx-profile-thb1-interpolation = <0x02>; + adi,tx-profile-thb2-interpolation = <0x01>; + adi,tx-profile-tx-bbf-3db-corner_khz = <0x186a0>; + adi,tx-profile-tx-dac-3db-corner_khz = <0x2da78>; + adi,tx-profile-tx-fir-interpolation = <0x01>; + adi,tx-profile-tx-input-hb-interpolation = <0x01>; + adi,tx-profile-tx-fir-gain_db = <0x06>; + adi,tx-profile-tx-fir-num-fir-coefs = <0x10>; + adi,tx-profile-tx-fir-coefs = <0x6fef2 0xcbff58 0xffac03d7 0xf36a5297 0xf36a03d7 0xffacff58 0xcbfef2 0x60000>; + adi,sniffer-profile-adc-div = <0x01>; + adi,sniffer-profile-en-high-rej-dec5 = <0x00>; + adi,sniffer-profile-iq-rate_khz = <0x7800>; + adi,sniffer-profile-rf-bandwidth_hz = <0x1312d00>; + adi,sniffer-profile-rhb1-decimation = <0x02>; + adi,sniffer-profile-rx-bbf-3db-corner_khz = <0x186a0>; + adi,sniffer-profile-rx-dec5-decimation = <0x05>; + adi,sniffer-profile-rx-fir-decimation = <0x04>; }; }; &axi_ad9371_rx_dma { diff --git a/test/hw/hw_helpers.py b/test/hw/hw_helpers.py index 4af11c5c..8b841f10 100644 --- a/test/hw/hw_helpers.py +++ b/test/hw/hw_helpers.py @@ -294,8 +294,11 @@ def require_hw_prereqs() -> None: "failed to load firmware", # Wifi/USB hotplug noise seen on some Kuiper releases: "cfg80211: failed to load", - # Harmless driver-level probe deferrals re-tried later: + # Harmless driver-level probe deferrals re-tried later. The kernel + # surfaces these both symbolically (``-EPROBE_DEFER``) and as the + # raw errno (``-517``) depending on the caller. "EPROBE_DEFER", + "error -517", # ZynqMP early-boot WARNING: the kernel logs a Call trace through # gic_of_init / of_irq_init because the RPU-bus interrupt-controller # cannot be initialized from Linux on ZynqMP. Always benign; the @@ -305,6 +308,14 @@ def require_hw_prereqs() -> None: "irqchip_init", "__primary_switched", "rpu-bus/interrupt-controller", + # Stock Kuiper ZynqMP (ZCU102) probes these SoC peripherals from the + # base DTS regardless of the overlay we merge in — the hardware is + # either unconfigured (no DisplayPort monitor attached) or not wired + # out on the board (no SATA). Match by device-node address so a + # genuine regression on the same driver elsewhere still trips. + "ffcb0000.watchdog", # Cadence WDT — unroutable clocks + "fd4a0000.display", # ZynqMP DisplayPort — no monitor + DPMS pipe + "fd0c0000.ahci", # Ceva AHCI/SATA — not routed on ZCU102 ) # Hard-fail patterns — these indicate a genuine kernel fault. @@ -346,6 +357,41 @@ def assert_no_kernel_faults(dmesg_txt: str) -> None: assert not bad, "Kernel fault(s) detected in dmesg:\n" + "\n".join(bad) +# Driver-probe-failure patterns in dmesg. These appear when a probe() +# callback returns a negative errno other than -EPROBE_DEFER (the defer +# path is the normal retry-until-resolved dance and is allowlisted via +# _DMESG_BENIGN_SUBSTRINGS above). Regex, not plain substrings — +# ``probe of failed with error `` is the canonical kernel +# message. Overlay-apply errors fall in the same bucket because a +# failed overlay almost always cascades into silent probe misses. +_DMESG_PROBE_ERROR_PATTERNS = ( + r"probe of \S+ failed with error", + r"Error applying overlay", + r"failed to apply overlay", + r"Error resolving", +) + + +def assert_no_probe_errors(dmesg_txt: str) -> None: + """Fail the calling test if *dmesg_txt* contains driver-probe errors. + + Complements :func:`assert_no_kernel_faults` — a driver can fail to + probe without ever producing a kernel fault (e.g. a DT overlay + apply error, a regulator not showing up, a phandle mismatch). + Reuses :data:`_DMESG_BENIGN_SUBSTRINGS` so known-benign probe + chatter (firmware loads, ``-EPROBE_DEFER`` retries, ZynqMP early- + boot warnings) does not fire. + """ + compiled = [_re.compile(p) for p in _DMESG_PROBE_ERROR_PATTERNS] + bad: list[str] = [] + for line in dmesg_txt.splitlines(): + if any(s in line for s in _DMESG_BENIGN_SUBSTRINGS): + continue + if any(rx.search(line) for rx in compiled): + bad.append(line) + assert not bad, "Driver probe errors detected in dmesg:\n" + "\n".join(bad) + + def shell_out(shell, cmd: str) -> str: """Run *cmd* via an ``ADIShellDriver`` and return the output as a string. @@ -475,6 +521,133 @@ def assert_jesd_links_data( return rx_status, tx_status +def assert_rx_capture_valid( + ctx, + device_candidates: str | tuple[str, ...], + n_samples: int = 2**12, + min_std: float = 1.0, + context: str = "", +) -> dict: + """Capture ``n_samples`` from an IIO device and verify data is flowing. + + Covers the "IIO device probed but no samples actually arrive" failure + mode: the JESD204 link reports DATA, drivers probe cleanly, IIO + devices appear, but the DMA / JESD transport / clock path silently + stops delivering samples. The buffer comes back, but every sample + is zero, or every sample is latched to one value. + + Asserts: + + - At least one RX channel is not all-zero (DMA actually transferred + bytes). + - At least one RX channel's |std| is ``>= min_std`` LSBs (samples + actually vary — noise floor alone clears this threshold easily, + but a latched converter does not). + + Uses raw libiio so it works with any buffered IIO device, including + AD9081 designs that expose the buffered frontend as the TPL core + (``ad_ip_jesd204_tpl_adc``) rather than ``axi-ad9081-rx-hpc``. + + Args: + ctx: A live ``iio.Context`` (e.g. from :func:`open_iio_context`). + device_candidates: IIO device name to capture from, or a tuple + of candidate names — the first one present on *ctx* wins. + n_samples: buffer depth for the capture. + min_std: minimum |std| across all channels, in raw-LSB units. + context: tag prepended to assertion-failure messages. + + Returns: + ``dict`` mapping channel id → captured ``numpy.ndarray``. + """ + import iio + import numpy as np + + suffix = f" ({context})" if context else "" + candidates = ( + (device_candidates,) if isinstance(device_candidates, str) else tuple(device_candidates) + ) + all_names = sorted(d.name for d in ctx.devices if d.name) + + def _has_rx_scan(d): + return any(c.scan_element and not c.output for c in d.channels) + + dev = next((d for d in (ctx.find_device(n) for n in candidates) if d is not None), None) + if dev is None or not _has_rx_scan(dev): + # No named candidate is RX-buffered — fall back to the first + # *AXI DMA frontend* on the context (name starts with ``axi-`` / + # ``cf-`` or contains ``tpl``). Control-plane devices like + # ``ad9528`` or ``ad9371-phy`` may expose scan channels too, but + # they aren't wired to an AXI-DMA and ``buf.refill()`` would just + # time out on them. + buffered = [ + d + for d in ctx.devices + if d.name + and (d.name.startswith("axi-") or d.name.startswith("cf-") or "tpl" in d.name) + and _has_rx_scan(d) + ] + dev = buffered[0] if buffered else None + assert dev is not None, ( + f"No RX-buffered IIO device found{suffix}. " + f"Tried: {list(candidates)}. Present: {all_names}" + ) + + scan_channels = [c for c in dev.channels if c.scan_element and not c.output] + assert scan_channels, ( + f"No RX scan channels on {dev.name!r}{suffix}. Present: {all_names}" + ) + + print(f"rx capture{suffix}: selected IIO device {dev.name!r}") + buf = None + try: + for ch in scan_channels: + ch.enabled = True + buf = iio.Buffer(dev, n_samples, False) + try: + buf.refill() + except TimeoutError as exc: + raise AssertionError( + f"Buffer refill timed out on {dev.name!r}{suffix} — " + "AXI DMA is not delivering samples (JESD or DMA path " + "stalled). Present devices: " + ", ".join(all_names) + ) from exc + per_channel: dict[str, np.ndarray] = {} + for ch in scan_channels: + raw = ch.read(buf) + # AXI ADC frontends emit signed int16 (or sign-extended + # int14/int12); dtype=int16 is correct for every chip this + # suite currently runs against. + per_channel[ch.id] = np.frombuffer(raw, dtype=np.int16) + finally: + if buf is not None: + del buf + for ch in scan_channels: + try: + ch.enabled = False + except Exception: + pass + + nonzero = [name for name, arr in per_channel.items() if arr.any()] + assert nonzero, ( + f"All channels on {dev.name!r} returned zero samples{suffix} — " + "JESD/DMA/clock path is likely stalled." + ) + + stds = {name: float(np.abs(arr).std()) for name, arr in per_channel.items()} + max_std = max(stds.values()) + assert max_std >= min_std, ( + f"All channels on {dev.name!r} latched to a constant value{suffix} " + f"(max |std|={max_std:.3g} < {min_std}) — data path stuck." + ) + + print( + f"rx capture{suffix}: device={dev.name}, " + f"{len(per_channel)} channel(s), {n_samples} samples, " + f"non-zero={list(nonzero)}, max |std|={max_std:.2f}" + ) + return per_channel + + def _kernel_cache_key(platform_arch: str, config_path: Path) -> str: """Return a short sha256 over *platform_arch* and the config file bytes.""" import hashlib diff --git a/test/hw/test_ad9081_zcu102_system_hw.py b/test/hw/test_ad9081_zcu102_system_hw.py index 31f6399d..cb2a934b 100644 --- a/test/hw/test_ad9081_zcu102_system_hw.py +++ b/test/hw/test_ad9081_zcu102_system_hw.py @@ -46,6 +46,8 @@ acquire_xsa, assert_jesd_links_data, assert_no_kernel_faults, + assert_no_probe_errors, + assert_rx_capture_valid, collect_dmesg, compile_dts_to_dtb, deploy_and_boot, @@ -266,6 +268,7 @@ def test_ad9081_zcu102_system_hw(board, built_kernel_image_zynqmp, tmp_path): # --- 9. Verify: kernel probe + IIO context + JESD DATA state --- assert_no_kernel_faults(dmesg_txt) + assert_no_probe_errors(dmesg_txt) assert "AD9081 Rev." in dmesg_txt or "probed ADC AD9081" in dmesg_txt, ( "AD9081 probe signature was not found in kernel dmesg output" ) @@ -292,3 +295,11 @@ def test_ad9081_zcu102_system_hw(board, built_kernel_image_zynqmp, tmp_path): ) print(f"$ cat .../84a90000.axi?jesd204?rx/status\n{rx_status}") print(f"$ cat .../84b90000.axi?jesd204?tx/status\n{tx_status}") + + # Data-path smoke test: capture a real buffer and verify samples flow. + assert_rx_capture_valid( + ctx, + ("axi-ad9081-rx-hpc", "ad_ip_jesd204_tpl_adc"), + n_samples=2**12, + context="ad9081 system", + ) diff --git a/test/hw/test_ad9081_zcu102_xsa_hw.py b/test/hw/test_ad9081_zcu102_xsa_hw.py index 73f28950..99465fbd 100644 --- a/test/hw/test_ad9081_zcu102_xsa_hw.py +++ b/test/hw/test_ad9081_zcu102_xsa_hw.py @@ -35,6 +35,8 @@ acquire_xsa, assert_jesd_links_data, assert_no_kernel_faults, + assert_no_probe_errors, + assert_rx_capture_valid, collect_dmesg, compile_dts_to_dtb, deploy_and_boot, @@ -184,6 +186,7 @@ def test_ad9081_zcu102_xsa_hw(board, built_kernel_image_zynqmp, tmp_path): # --- 7. Verify: kernel probe + IIO context + JESD DATA state --- assert_no_kernel_faults(dmesg_txt) + assert_no_probe_errors(dmesg_txt) assert "AD9081 Rev." in dmesg_txt or "probed ADC AD9081" in dmesg_txt, ( "AD9081 probe signature was not found in kernel dmesg output" ) @@ -210,3 +213,11 @@ def test_ad9081_zcu102_xsa_hw(board, built_kernel_image_zynqmp, tmp_path): ) print(f"$ cat .../84a90000.axi?jesd204?rx/status\n{rx_status}") print(f"$ cat .../84b90000.axi?jesd204?tx/status\n{tx_status}") + + # Data-path smoke test: capture a real buffer and verify samples flow. + assert_rx_capture_valid( + ctx, + ("axi-ad9081-rx-hpc", "ad_ip_jesd204_tpl_adc"), + n_samples=2**12, + context="ad9081 xsa", + ) diff --git a/test/hw/test_adrv9009_zcu102_hw.py b/test/hw/test_adrv9009_zcu102_hw.py index 65381578..34810260 100644 --- a/test/hw/test_adrv9009_zcu102_hw.py +++ b/test/hw/test_adrv9009_zcu102_hw.py @@ -38,6 +38,8 @@ acquire_xsa, assert_jesd_links_data, assert_no_kernel_faults, + assert_no_probe_errors, + assert_rx_capture_valid, collect_dmesg, compile_dts_to_dtb, deploy_and_boot, @@ -214,6 +216,7 @@ def test_adrv9009_zcu102_hw(board, built_kernel_image_zynqmp, tmp_path): # --- 7. Verify: kernel probe + IIO context + JESD DATA state --- assert_no_kernel_faults(dmesg_txt) + assert_no_probe_errors(dmesg_txt) assert "adrv9009-phy" in dmesg_txt or "Talise" in dmesg_txt, ( "ADRV9009 phy probe signature was not found in kernel dmesg output" ) @@ -238,6 +241,14 @@ def test_adrv9009_zcu102_hw(board, built_kernel_image_zynqmp, tmp_path): print(f"$ cat .../axi?jesd204?rx/status\n{rx_status}") print(f"$ cat .../axi?jesd204?tx/status\n{tx_status}") + # --- 8b. Data-path smoke test: capture a real RX buffer. --- + assert_rx_capture_valid( + ctx, + ("axi-adrv9009-rx-hpc", "axi-adrv9009-rx-obs-hpc"), + n_samples=2**12, + context="adrv9009 initial boot", + ) + # --- 9. Load all four canonical Talise filter profiles --- # The remote libiio write path drops its TCP socket when the driver # holds the CPU for Talise re-init, surfacing as ``BrokenPipeError``. @@ -279,5 +290,7 @@ def test_adrv9009_zcu102_hw(board, built_kernel_image_zynqmp, tmp_path): # relock both links before re-reading sysfs status. time.sleep(3.0) assert_jesd_links_data(shell, context=f"after {filename}") - assert_no_kernel_faults(shell_out(shell, "dmesg")) + dmesg = shell_out(shell, "dmesg") + assert_no_kernel_faults(dmesg) + assert_no_probe_errors(dmesg) print(f" {filename}: RX+TX JESD DATA OK") diff --git a/test/hw/test_adrv9371_zc706_hw.py b/test/hw/test_adrv9371_zc706_hw.py index f88c78b8..146c436d 100644 --- a/test/hw/test_adrv9371_zc706_hw.py +++ b/test/hw/test_adrv9371_zc706_hw.py @@ -36,10 +36,13 @@ DEFAULT_OUT_DIR, acquire_xsa, assert_no_kernel_faults, + assert_no_probe_errors, collect_dmesg, compile_dts_to_dtb, deploy_and_boot, open_iio_context, + read_jesd_status, + shell_out, ) @@ -92,19 +95,23 @@ def test_adrv9371_zc706_xsa_hw(board, built_kernel_image_zynq, tmp_path): # --- 3. Run the XSA pipeline --- cfg = { "adrv9009_board": { - "misc_clk_hz": 245_760_000, + "misc_clk_hz": 122_880_000, "spi_bus": "spi0", "clk_cs": 0, "trx_cs": 1, - "trx_reset_gpio": 130, - "trx_sysref_req_gpio": 136, + "trx_reset_gpio": 106, + "trx_sysref_req_gpio": 112, + "ad9528_reset_gpio": 113, "ad9528_vcxo_freq": DEFAULT_VCXO_HZ, "rx_link_id": 1, "tx_link_id": 0, }, + # JESD framing parameters matching the default HDL config for + # ``projects/adrv9371x/zc706`` (see analogdevicesinc/hdl + # README): RX = M=4 L=2 S=1 → F=4; TX = M=4 L=4 S=1 → F=2. "jesd": { "rx": {"F": 4, "K": 32, "M": 4, "L": 2}, - "tx": {"F": 4, "K": 32, "M": 4, "L": 4}, + "tx": {"F": 2, "K": 32, "M": 4, "L": 4}, }, } result = XsaPipeline().run( @@ -137,6 +144,7 @@ def test_adrv9371_zc706_xsa_hw(board, built_kernel_image_zynq, tmp_path): grep_pattern="ad9371|ad9528|jesd204|mykonos|probe|failed|error", ) assert_no_kernel_faults(dmesg_txt) + assert_no_probe_errors(dmesg_txt) assert "ad9371" in dmesg_txt.lower() or "mykonos" in dmesg_txt.lower(), ( "AD9371 driver probe signature not found in dmesg" ) @@ -150,6 +158,135 @@ def test_adrv9371_zc706_xsa_hw(board, built_kernel_image_zynq, tmp_path): f"No AD9528 clock device found. Devices: {sorted(found)}" ) + # --- 7. Gather JESD link + ILAS + DMA-path diagnostics. --- + # The JESD-DATA assert is intentionally *disabled* here: the + # bq run after the 122.88 MHz misc_clk fix (commit 99ad39c) + # showed clocks match (Measured 122.882 / Reported 122.880 MHz) + # but the AD9371 deframer reports ILAS mismatch on all 7 + # framing parameters (lanes/converter, scrambling, octets/frame, + # frames/multiframe, converters, sample-resolution, control- + # bits), so the link stays "disabled" — a framing-parameter fix + # the profile-and-cfg combination needs, not a link-clock fix. + # Keep printing the full status for future iterations. + rx_status, tx_status = read_jesd_status(shell) + print("=== JESD204 RX status (sysfs) ===") + print(rx_status) + print("=== JESD204 TX status (sysfs) ===") + print(tx_status) + + # Dump TPL ADC sysfs (enable state, sampling freq, etc.) and DMA + # controller state. These surface the most common DMA-stall + # causes: ADC channels not scan-enabled, TPL rate register at 0, + # axi-dmac refusing to arm a descriptor, or the buffer sysfs + # knob left disabled. + print("=== TPL ADC sysfs (/sys/bus/iio/devices//) ===") + print(shell_out(shell, ( + "tpl=$(ls -d /sys/bus/iio/devices/iio:device* 2>/dev/null " + "| while read d; do " + " name=$(cat $d/name 2>/dev/null); " + " case \"$name\" in *tpl_adc*|*ad9371*rx*|*axi-ad9371-rx*) echo $d; esac; " + "done | head -1); " + "echo \"PATH: $tpl\"; " + "[ -n \"$tpl\" ] && ls -la $tpl/; " + "for f in \"$tpl\"/name \"$tpl\"/sampling_frequency \"$tpl\"/buffer/enable " + " \"$tpl\"/buffer/length \"$tpl\"/buffer/watermark; do " + " [ -e $f ] && printf '%s = %s\\n' $f \"$(cat $f 2>/dev/null)\"; " + "done" + ))) + print("=== TPL ADC channel enables ===") + print(shell_out(shell, ( + "for ch in /sys/bus/iio/devices/iio:device*/scan_elements/*_en; do " + " [ -e $ch ] && printf '%s = %s\\n' $ch \"$(cat $ch 2>/dev/null)\"; " + "done | grep -E 'tpl_adc|ad9371'" + ))) + print("=== AXI DMAC (rx/tx) state ===") + print(shell_out(shell, ( + "for d in /sys/bus/platform/devices/7c4?0000.axi_dmac; do " + " echo \"--- $d ---\"; ls $d 2>/dev/null; " + "done; " + "dmesg | grep -iE 'dmac|axi-dmac|dma' | tail -n 20" + ))) + print("=== AD9371 phy sysfs snapshot ===") + print(shell_out(shell, ( + "phy=$(find /sys/bus/iio/devices -maxdepth 2 -name ensm_mode 2>/dev/null " + " | xargs dirname 2>/dev/null | head -1); " + "echo \"PHY: $phy\"; " + "[ -n \"$phy\" ] && for f in $phy/ensm_mode $phy/gain_control_mode " + " $phy/in_voltage0_rf_bandwidth $phy/in_voltage0_sampling_frequency " + " $phy/rx_path_clks; do " + " [ -e $f ] && printf '%s = %s\\n' $f \"$(cat $f 2>/dev/null)\"; " + "done" + ))) + # TODO(adrv9371-capture): data-path smoke test still deferred. + # + # Progress across this series (all landed on this branch): + # + # - AD9528 channel@{1,3,12,13} subnodes + reset-gpios=<113> + # + jesd204-device / #jesd204-cells=2 / + # jesd204-sysref-provider flags. + # - AD9371 reset-gpios=<106>, sysref-req-gpios=<112>. + # - ~60 Mykonos ``adi,{rx,obs,tx,sniffer}-profile-*`` + + # ``adi,clocks-*`` baked into ad9371-phy@1. + # - AD9528 added as jesd204-inputs link 2 on the AD9371. + # - ``adi,{sys,out}-clk-select`` + ``adi,use-lpm-enable`` on + # the adxcvr (commit bad15c2) — unblocked axi-jesd204-rx/tx + # platform driver probe. + # - ``misc_clk_0`` rate 245.76 → 122.88 MHz (commit 99ad39c) + # to match the real FMC clock that physically lands on the + # clkgen's clkin1. + # + # Now-observable state on bq (from the ``=== JESD204 ... ===`` + # dump above): + # + # - Measured Link Clock = Reported = 122.882 MHz → clock-layer + # healthy. + # - JESD FSM reaches ``opt_post_running_stage`` on all 3 links + # (RX, TX, AD9528 sysref), no rollback. + # - AD9371 firmware initialised ("AD9371 Rev 3, Firmware 5.2.2 + # API version: 1.5.2.3566 successfully initialized via + # jesd204-fsm"). + # - TPL ADC + DAC both probe as MASTER. + # + # Still-open blocker — ILAS framing parameter mismatch: + # + # ad9371 spi1.1: deframerStatus (0x21) + # ad9371 spi1.1: ILAS mismatch: c7f8 + # ILAS lanes per converter did not match + # ILAS scrambling did not match + # ILAS octets per frame did not match + # ILAS frames per multiframe did not match + # ILAS number of converters did not match + # ILAS sample resolution did not match + # ILAS control bits per sample did not match + # + # After the link trains, every ILAS parameter disagrees between + # what ``axi-jesd204-tx`` sends and what the AD9371's Mykonos + # deframer expects (based on its profile). The link drops to + # ``Link is disabled`` and the TPL DMA has nothing to collect. + # + # The HDL reference (``analogdevicesinc/hdl/projects/adrv9371x/ + # zc706/README``) documents the default framing as:: + # + # TX_JESD_M=4, TX_JESD_L=4, TX_JESD_S=1 (→ F=2 for Np=16) + # RX_JESD_M=4, RX_JESD_L=2, RX_JESD_S=1 (→ F=4) + # + # Our ``axi-jesd204-tx`` overlay already emits F=2 (from the + # ``tx_octets_per_frame`` default), M=4, Np=16, CS=2 — matching + # HDL — and ``axi-jesd204-rx`` emits F=4. All 7 ILAS params + # still disagree, so the Mykonos profile we baked in + # (``_DEFAULT_MYKONOS_PROFILE_PROPS``) must be for a different + # HDL build than the default one the XSA represents. + # + # Next step: pair the profile to the HDL. Either + # (a) regenerate ``_DEFAULT_MYKONOS_PROFILE_PROPS`` from an + # iio-oscilloscope profile whose implied JESD framing matches + # (M=4, L=4, F=2, K=32, Np=16) for TX and (M=4, L=2, F=4, K=32, + # Np=16) for RX — typically the 100 MHz-BW profile at a + # specific IQ rate the HDL compiles in, or + # (b) override ``trx_profile_props`` in the profile JSON for + # this board to a per-profile list that matches the + # build-time HDL settings. + # --------------------------------------------------------------------------- # System API test diff --git a/test/hw/test_fmcdaq3_vcu118_hw.py b/test/hw/test_fmcdaq3_vcu118_hw.py index c2a1a5c2..37d0ecd7 100644 --- a/test/hw/test_fmcdaq3_vcu118_hw.py +++ b/test/hw/test_fmcdaq3_vcu118_hw.py @@ -33,34 +33,48 @@ allow_module_level=True, ) +from test.hw.hw_helpers import ( # noqa: E402 + DEFAULT_OUT_DIR, + assert_no_kernel_faults, + assert_no_probe_errors, + assert_rx_capture_valid, + collect_dmesg, + open_iio_context, +) + @pytest.mark.lg_feature(["fmcdaq3", "vcu118"]) -def test_fmcdaq3_vcu118_boot_hw(target): +def test_fmcdaq3_vcu118_boot_hw(board): """Boot FMCDAQ3+VCU118 with the prebuilt Kuiper image and verify IIO.""" - shell = _boot_and_get_shell(target) - _assert_probed_drivers(shell) - _assert_iio_devices(shell) + out_dir = DEFAULT_OUT_DIR + out_dir.mkdir(parents=True, exist_ok=True) + + board.transition("shell") + shell = board.target.get_driver("ADIShellDriver") + dmesg_txt = collect_dmesg( + shell, + out_dir, + label="fmcdaq3_vcu118", + grep_pattern="ad9680|ad9152|ad9528|jesd204|probe|failed|error", + ) + assert_no_kernel_faults(dmesg_txt) + assert_no_probe_errors(dmesg_txt) -def _boot_and_get_shell(target): - """Drive ``BootFabric`` through ``powered_off`` → ``shell`` and return shell.""" - strategy = target.get_driver("Strategy") - strategy.transition("powered_off") - strategy.transition("shell") - return target.get_driver("ADIShellDriver") + lowered = dmesg_txt.lower() + assert "ad9680" in lowered, "AD9680 driver messages not seen in dmesg" + assert "ad9152" in lowered, "AD9152 driver messages not seen in dmesg" + _assert_iio_devices(shell) -def _assert_probed_drivers(shell) -> None: - """Fail unless dmesg shows AD9680 / AD9152 / AD9528 / JESD driver probes.""" - out = shell.run_check( - "dmesg | grep -Ei 'ad9680|ad9152|ad9528|jesd204|fail|error' | tail -n 200; true" + # Data-path smoke test: capture a real AD9680 RX buffer. + ctx, _ = open_iio_context(shell) + assert_rx_capture_valid( + ctx, + ("axi-ad9680-core-lpc", "axi-ad9680-hpc", "axi-ad9680-rx-hpc"), + n_samples=2**12, + context="fmcdaq3 boot", ) - dmesg = "\n".join(out) if isinstance(out, list) else str(out) - print("\n=== FMCDAQ3 probe-relevant dmesg ===") - print(dmesg) - print("====================================") - assert "ad9680" in dmesg.lower(), "AD9680 driver messages not seen in dmesg" - assert "ad9152" in dmesg.lower(), "AD9152 driver messages not seen in dmesg" def _assert_iio_devices(shell) -> None: