mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Ali Rouhi <arouhi@sitime.com>
To: "jiri@resnulli.us" <jiri@resnulli.us>
Cc: "vadim.fedorenko@linux.dev" <vadim.fedorenko@linux.dev>,
	"arkadiusz.kubalewski@intel.com" <arkadiusz.kubalewski@intel.com>,
	"ivecera@redhat.com" <ivecera@redhat.com>,
	"kuba@kernel.org" <kuba@kernel.org>,
	"pabeni@redhat.com" <pabeni@redhat.com>,
	"robh@kernel.org" <robh@kernel.org>,
	"krzk+dt@kernel.org" <krzk+dt@kernel.org>,
	"conor+dt@kernel.org" <conor+dt@kernel.org>,
	"cjubran@nvidia.com" <cjubran@nvidia.com>,
	"Oleg.Zadorozhnyi@devoxsoftware.com"
	<Oleg.Zadorozhnyi@devoxsoftware.com>,
	"prabhakar.mahadev-lad.rj@bp.renesas.com"
	<prabhakar.mahadev-lad.rj@bp.renesas.com>,
	"devicetree@vger.kernel.org" <devicetree@vger.kernel.org>,
	"netdev@vger.kernel.org" <netdev@vger.kernel.org>,
	"linux-kernel@vger.kernel.org" <linux-kernel@vger.kernel.org>
Subject: [PATCH net-next v15 07/13] dpll: sit9531x: implement input pin state on a DPLL
Date: Tue, 10 Nov 2026 20:26:13 +0000	[thread overview]
Message-ID: <20261110202607.55744-8-arouhi@sitime.com> (raw)
In-Reply-To: <20261110202607.55744-1-arouhi@sitime.com>

From: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>

Report and set whether an input takes part in a PLL's reference
selection. The PLL picks its reference from a priority table: an input
in the table is SELECTABLE, one absent is DISCONNECTED, and setting the
state adds or removes it. CONNECTED is refused, as the driver does not
program manual input select.

A table write uses the documented procedure: force holdover, write
the slots, latch with a small update, release. The pin reported active
is the one the part feeds the PLL while the PLL tracks it and the lane
has signal.

A PLL in holdover after losing its reference is re-selected when a
listed source returns.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 1122 ++++++++++++++++++++++++++++++++--
 drivers/dpll/sit9531x/core.h |   65 +-
 drivers/dpll/sit9531x/dpll.c |  257 +++++++-
 drivers/dpll/sit9531x/regs.h |   38 ++
 4 files changed, 1432 insertions(+), 50 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 209671e7917e..af6d7918958d 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -61,10 +61,21 @@ static const struct sit9531x_chip_info sit9531x_chip_ids[] = {
  * One 256-register page is visible at a time, selected by offset 0xFF on every
  * page. The map covers that window, uncached; the accessors select the page.
  */
+/*
+ * Each read of the debug status data register advances the status bus latch;
+ * marking it precious stops a debugfs dump from reading it.
+ */
+static bool sit9531x_precious_reg(struct device *dev __maybe_unused,
+				  unsigned int reg)
+{
+	return reg == SIT9531X_PLL_REG_DBG_STATUS_DATA;
+}
+
 static const struct regmap_config sit9531x_regmap_config = {
 	.reg_bits	= 8,
 	.val_bits	= 8,
 	.max_register	= SIT9531X_PAGE_SEL,
+	.precious_reg	= sit9531x_precious_reg,
 	.cache_type	= REGCACHE_NONE,
 };
 
@@ -206,6 +217,34 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return rc;
 }
 
+#define SIT9531X_CLOSE_TRIES		3
+
+/* Write a register, retrying on failure */
+static int sit9531x_write_close(struct sit9531x_dev *sitdev, unsigned int reg,
+				u8 val)
+{
+	u8 attempt;
+	int rc;
+
+	for (attempt = 0; attempt < SIT9531X_CLOSE_TRIES; attempt++) {
+		if (attempt)
+			usleep_range(1000, 2000);
+		rc = sit9531x_write_u8(sitdev, reg, val);
+		if (!rc)
+			break;
+	}
+
+	return rc;
+}
+
+static int sit9531x_write_pll_close(struct sit9531x_dev *sitdev, u8 pll_idx,
+				    u8 offset, u8 val)
+{
+	return sit9531x_write_close(sitdev,
+				    SIT9531X_REG(sit9531x_pll_page(pll_idx),
+						 offset), val);
+}
+
 static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
 				    u8 index,
 				    unsigned int *force_reg,
@@ -223,6 +262,72 @@ static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
 	}
 }
 
+/* Caller must hold sitdev->multiop_lock. */
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int force_reg, state_reg;
+	struct sit9531x_ref *ref;
+	u8 pair, val;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (!rc)
+		rc = sit9531x_write_u8(sitdev, force_reg, val | BIT(pair));
+
+	ret = sit9531x_read_u8(sitdev, state_reg, &val);
+	if (!ret)
+		ret = sit9531x_write_u8(sitdev, state_reg, val & ~BIT(pair));
+	if (ret && !rc)
+		rc = ret;
+
+	ref->enabled = false;
+	ref->off_pending = rc != 0;
+
+	return rc;
+}
+
+/*
+ * Release the force so the lane uses the loaded configuration. Caller must hold
+ * sitdev->multiop_lock.
+ */
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int force_reg, state_reg;
+	struct sit9531x_ref *ref;
+	u8 pair, val;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
+
+	rc = sit9531x_read_u8(sitdev, force_reg, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, force_reg, val & ~BIT(pair));
+	if (rc)
+		return rc;
+
+	ref->enabled = true;
+	ref->off_pending = false;
+
+	return 0;
+}
+
 /*
  * Output Hi-Z control: each slot has three force/state pairs on Page 0x03,
  * for the differential path and the CMOS OutP and OutN pads. A mute writes
@@ -359,7 +464,7 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
  *   1. Force PLL into holdover (PLL page reg 0x6F bit 4)
  *   2. Write priority slots on Page 1
  *   3. Small change update (Page 0 reg 0x0F bit 1)
- *   4. Release holdover
+ *   4. Release holdover, unless something other than this driver forced it
  *
  * Caller must hold sitdev->multiop_lock.
  */
@@ -381,20 +486,460 @@ static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
 	return val >> SIT9531X_PRIO_HI_SHIFT;
 }
 
+static u8 sit9531x_prio_slot_set(u8 val, u8 slot, u8 src)
+{
+	if (slot & 1)
+		return (val & (SIT9531X_PRIO_NIBBLE_MASK <<
+			       SIT9531X_PRIO_HI_SHIFT)) |
+		       (src & SIT9531X_PRIO_NIBBLE_MASK);
+
+	return (val & SIT9531X_PRIO_NIBBLE_MASK) |
+	       ((src & SIT9531X_PRIO_NIBBLE_MASK) <<
+		SIT9531X_PRIO_HI_SHIFT);
+}
+
+/*
+ * Latch a priority table with a small change update, as in the documented
+ * input_priority_sel() procedure.
+ */
+static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_close(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+				  SIT9531X_SMALL_UPDATE_CMD);
+	if (rc)
+		return rc;
+
+	usleep_range(1000, 2000);
+
+	return 0;
+}
+
+/*
+ * Four registers carry a nibble per lane: loss of signal and the fine and
+ * coarse frequency-drift flags. Caller must hold sitdev->multiop_lock, or run
+ * before registration.
+ */
+static int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev)
+{
+	static const unsigned int regs[] = {
+		SIT9531X_CLKMON_P_STATUS_01, SIT9531X_CLKMON_P_STATUS_23,
+		SIT9531X_CLKMON_N_STATUS_01, SIT9531X_CLKMON_N_STATUS_23,
+	};
+	u8 val[ARRAY_SIZE(regs)], i, pair, nib;
+	int rc;
+
+	for (i = 0; i < ARRAY_SIZE(regs); i++) {
+		rc = sit9531x_read_u8(sitdev, regs[i], &val[i]);
+		if (rc)
+			return rc;
+	}
+
+	for (i = 0; i < sitdev->info->num_inputs && i < SIT9531X_MAX_INPUTS;
+	     i++) {
+		struct sit9531x_ref *ref = &sitdev->ref[i];
+
+		/* Lane 2k is INkP, 2k + 1 INkN; a register holds two pairs. */
+		pair = sit9531x_input_pair(i);
+		nib = val[(sit9531x_input_is_n(i) ? 2 : 0) + pair / 2];
+		nib = (pair & 1) ? nib >> 4 : nib & 0x0F;
+
+		ref->los = !!(nib & SIT9531X_CLKMON_LOSS);
+		ref->qual_fail = !ref->los &&
+				 !!(nib & (SIT9531X_CLKMON_FINE_DRIFT |
+					   SIT9531X_CLKMON_COARSE_DRIFT));
+	}
+
+	return 0;
+}
+
+/*
+ * Check if the device can lock to @src now; a differential pair is watched
+ * through its P lane.
+ */
+static bool sit9531x_prio_src_live(const struct sit9531x_dev *sitdev, u8 src)
+{
+	u8 index = sit9531x_hw_src_input(src);
+
+	if (index >= sitdev->info->num_inputs)
+		return true;
+
+	return !sitdev->ref[index].los;
+}
+
+/*
+ * Map an N-lane source of a differential pair to its P lane, the lane its DPLL
+ * pin represents.
+ */
+static u8 sit9531x_prio_src_canon(const struct sit9531x_dev *sitdev, u8 src)
+{
+	u8 index = sit9531x_hw_src_input(src);
+
+	if (index >= sitdev->info->num_inputs)
+		return src;
+
+	if (sit9531x_input_is_n(index) &&
+	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+		return sit9531x_input_hw_src(index - 1);
+
+	return src;
+}
+
+/*
+ * @input_idx is in hardware encoding (see sit9531x_input_hw_src()). Caller must
+ * hold sitdev->multiop_lock.
+ */
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
+				 u8 input_idx)
+{
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return false;
+
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return false;
+
+	return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
+}
+
+/*
+ * Take the configured priorities from the hardware table: a listed source gets
+ * the first slot it occupies.
+ */
+static void sit9531x_prio_cfg_seed(struct sit9531x_dev *sitdev, u8 pll_idx,
+				   const u8 *srcs)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u16 seeded = 0;
+	u8 slot, src;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		if (!sit9531x_prio_src_usable(src))
+			continue;
+		src = sit9531x_prio_src_canon(sitdev, src);
+		if (seeded & BIT(src))
+			continue;
+		seeded |= BIT(src);
+		chan->cfg_prio[src] = slot;
+		chan->cfg_known |= BIT(src);
+	}
+
+	for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++) {
+		if (chan->cfg_known & BIT(src))
+			continue;
+		chan->cfg_prio[src] = SIT9531X_PRIO_MAX_SLOTS - 1;
+		chan->cfg_known |= BIT(src);
+	}
+
+	memcpy(chan->seen_srcs, srcs, sizeof(chan->seen_srcs));
+	chan->seen_valid = true;
+}
+
+/* Order the members by configured priority */
+static void sit9531x_prio_table_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+				      u16 members, u8 *srcs)
+{
+	const struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 order[SIT9531X_PRIO_NUM_SRC], n = 0, i, j, src;
+
+	for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++)
+		if (members & BIT(src))
+			order[n++] = src;
+
+	for (i = 1; i < n; i++) {
+		u8 cur = order[i];
+
+		for (j = i; j > 0; j--) {
+			u8 prev = order[j - 1];
+			u16 kc, kp;
+
+			kc = (chan->cfg_known & BIT(cur)) ?
+			     chan->cfg_prio[cur] : U8_MAX;
+			kp = (chan->cfg_known & BIT(prev)) ?
+			     chan->cfg_prio[prev] : U8_MAX;
+			if (kc == kp) {
+				/* Keep the hardware order among equals. */
+				kc = chan->prio_last[cur] ?: U8_MAX;
+				kp = chan->prio_last[prev] ?: U8_MAX;
+			}
+			if (kp <= kc)
+				break;
+			order[j] = prev;
+		}
+		order[j] = cur;
+	}
+
+	for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+		srcs[i] = i < n ? order[i] : SIT9531X_PRIO_SRC_NONE;
+}
+
+/*
+ * Refresh the cached membership mask, per-slot copy and first-slot array of a
+ * PLL's priority table.
+ */
 static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
 				     const u8 *srcs)
 {
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 first[SIT9531X_PRIO_NUM_SRC] = { 0 };
+	u8 slot, src, src_canon;
 	u16 mask = 0;
-	u8 slot;
 
 	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
-		u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+		src = srcs[slot];
+		src &= SIT9531X_PRIO_NIBBLE_MASK;
+		chan->prio_srcs[slot] = src;
+		src_canon = sit9531x_prio_src_canon(sitdev, src);
+		if (!sit9531x_prio_src_usable(src))
+			continue;
+
+		mask |= BIT(src_canon);
+		if (!first[src_canon])
+			first[src_canon] = slot + 1;
+	}
+
+	for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++)
+		chan->prio_last[src] = first[src];
+
+	chan->prio_mask = mask;
+}
+
+#define SIT9531X_HO_CLEAR_TRIES		3
+
+static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+				    u8 *srcs);
+static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx);
+
+static u8 sit9531x_prio_top_live(const struct sit9531x_dev *sitdev,
+				 const u8 *srcs)
+{
+	u8 i, src;
+
+	for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++) {
+		src = srcs[i] & SIT9531X_PRIO_NIBBLE_MASK;
+		src = sit9531x_prio_src_canon(sitdev, src);
+		if (sit9531x_prio_src_usable(src) &&
+		    sit9531x_prio_src_live(sitdev, src))
+			return src;
+	}
+
+	return SIT9531X_PRIO_SRC_NONE;
+}
+
+/*
+ * Pick the active selection to latch; after a latch the PLL uses that source
+ * until it loses signal.
+ */
+static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
+				       const u8 *old, const u8 *srcs, u8 cur)
+{
+	u8 top, i;
+
+	top = sit9531x_prio_top_live(sitdev, srcs);
+	if (top != SIT9531X_PRIO_SRC_NONE &&
+	    top != sit9531x_prio_top_live(sitdev, old))
+		return top;
+
+	cur = sit9531x_prio_src_canon(sitdev, cur & SIT9531X_PRIO_NIBBLE_MASK);
+
+	if (sit9531x_prio_src_usable(cur) &&
+	    sit9531x_prio_src_live(sitdev, cur))
+		for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+			if (srcs[i] == cur)
+				return cur;
+
+	if (top != SIT9531X_PRIO_SRC_NONE)
+		return top;
+
+	for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+		if (sit9531x_prio_src_usable(srcs[i]))
+			return srcs[i];
+
+	return srcs[0];
+}
+
+static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
+				      const u8 *srcs)
+{
+	u8 val, last_old, slot, attempt, written = 0, restored = 0;
+	u8 now[SIT9531X_PRIO_MAX_SLOTS], back[SIT9531X_PRIO_MAX_SLOTS];
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	int rc = 0, prg_rc, ho_rc = 0;
+	bool empty, ho_foreign, pending = false;
+	bool pending_before = chan->table_pending;
+	u16 reg;
+
+	empty = true;
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+		if (sit9531x_prio_src_usable(srcs[slot]))
+			empty = false;
+
+	memcpy(back, chan->table_pending ? chan->table_back : chan->prio_srcs,
+	       sizeof(back));
+
+	/*
+	 * Treat a forced holdover as set by this driver only when the table is
+	 * empty or a release is pending; leave any other in place.
+	 */
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+				  &val);
+	if (rc)
+		return rc;
 
-		if (sit9531x_prio_src_usable(src))
-			mask |= BIT(src);
+	ho_foreign = (val & BIT(SIT9531X_PLL_HO_FORCE_BIT)) &&
+		     chan->prio_mask && !chan->ho_release_pending;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+				   val | BIT(SIT9531X_PLL_HO_FORCE_BIT));
+	if (rc)
+		goto release;
+
+	usleep_range(10000, 12000);
+
+	for (slot = 0; slot + 1 < SIT9531X_PRIO_MAX_SLOTS; slot += 2) {
+		reg = sit9531x_prio_reg(pll_idx, slot);
+
+		val = sit9531x_prio_slot_set(0, slot, srcs[slot]);
+		val = sit9531x_prio_slot_set(val, slot + 1, srcs[slot + 1]);
+
+		written = slot + 2;
+		rc = sit9531x_write_u8(sitdev, reg, val);
+		if (rc)
+			goto rollback;
+	}
+
+	/*
+	 * The last register holds slot 10 in [7:4] and the device's active
+	 * selection in [3:0], addressed as the slot past the last.
+	 */
+	reg = sit9531x_prio_reg(pll_idx, slot);
+
+	rc = sit9531x_read_u8(sitdev, reg, &last_old);
+	if (rc)
+		goto rollback;
+
+	val = sit9531x_prio_slot_set(last_old, slot, srcs[slot]);
+
+	if (!empty) {
+		u8 sel = sit9531x_prio_slot_get(val, slot + 1);
+
+		rc = sit9531x_input_mon_fetch(sitdev);
+		if (rc)
+			goto rollback;
+		sel = sit9531x_prio_activesel_pick(sitdev, chan->prio_srcs,
+						   srcs, sel);
+		val = sit9531x_prio_slot_set(val, slot + 1, sel);
+	}
+
+	written = SIT9531X_PRIO_MAX_SLOTS;
+	rc = sit9531x_write_u8(sitdev, reg, val);
+	if (rc)
+		goto rollback;
+
+rollback:
+	if (rc && written) {
+		for (slot = 0; slot < written; slot += 2) {
+			const u8 *prev = &chan->prio_srcs[slot];
+			u8 old;
+
+			if (slot + 1 < SIT9531X_PRIO_MAX_SLOTS) {
+				old = sit9531x_prio_slot_set(0, slot, prev[0]);
+				old = sit9531x_prio_slot_set(old, slot + 1,
+							     prev[1]);
+			} else {
+				old = last_old;
+			}
+			if (sit9531x_write_u8(sitdev,
+					      sit9531x_prio_reg(pll_idx, slot),
+					      old))
+				break;
+
+			restored = slot + 2;
+		}
+		pending = restored < written;
+		written = restored;
+	}
+
+	/*
+	 * Latch unconditionally: the PLL uses the register contents only after
+	 * a latch.
+	 */
+	prg_rc = sit9531x_prio_prg_commit(sitdev);
+	if (prg_rc && !rc) {
+		pending = true;
+		rc = prg_rc;
+	}
+
+	if (!rc) {
+		sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+		memcpy(chan->seen_srcs, srcs, sizeof(chan->seen_srcs));
+		chan->seen_valid = true;
+		chan->table_pending = false;
+	} else if (pending || pending_before) {
+		sit9531x_prio_mask_build(sitdev, pll_idx, back);
+		memcpy(chan->table_back, back, sizeof(chan->table_back));
+		chan->table_pending = true;
+	} else if (!sit9531x_prio_table_read(sitdev, pll_idx, now)) {
+		sit9531x_prio_mask_build(sitdev, pll_idx, now);
+		memcpy(chan->seen_srcs, now, sizeof(chan->seen_srcs));
+		chan->seen_valid = true;
 	}
 
-	sitdev->chan[pll_idx].prio_mask = mask;
+release:
+	/*
+	 * An empty table keeps the forced holdover; the PLL uses the source in
+	 * the selection nibble, which still names the old one.
+	 */
+	if (!chan->prio_mask) {
+		dev_dbg(sitdev->dev,
+			"PLL%c: no source listed, holdover kept\n",
+			'A' + pll_idx);
+		chan->ho_release_pending = false;
+		goto out;
+	}
+
+	if (ho_foreign) {
+		dev_dbg(sitdev->dev,
+			"PLL%c: holdover was forced before the write, left in place\n",
+			'A' + pll_idx);
+		goto out;
+	}
+
+	if (chan->table_pending) {
+		dev_dbg(sitdev->dev,
+			"PLL%c: priority table restore pending, holdover kept\n",
+			'A' + pll_idx);
+		chan->ho_release_pending = true;
+		goto out;
+	}
+
+	for (attempt = 0; attempt < SIT9531X_HO_CLEAR_TRIES; attempt++) {
+		ho_rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+					       SIT9531X_PLL_REG_HO_CTRL,
+					       BIT(SIT9531X_PLL_HO_FORCE_BIT),
+					       0);
+		if (!ho_rc)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (ho_rc) {
+		dev_err(sitdev->dev, "PLL%c left in forced holdover: %d\n",
+			'A' + pll_idx, ho_rc);
+		if (!rc)
+			rc = ho_rc;
+	}
+	chan->ho_release_pending = !!ho_rc;
+
+out:
+	/* The core reads the pin getters as soon as the setter returns. */
+	if (sit9531x_chan_state_fetch(sitdev, pll_idx))
+		chan->stale = true;
+
+	return rc;
 }
 
 /* Caller must hold sitdev->multiop_lock. */
@@ -416,6 +961,128 @@ static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return 0;
 }
 
+/*
+ * Rewrite a PLL's table for a new member set; skip an unchanged table, since
+ * each write forces holdover.
+ */
+static int sit9531x_prio_table_apply(struct sit9531x_dev *sitdev, u8 pll_idx,
+				     u16 members)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+
+	if (hweight16(members) > SIT9531X_PRIO_MAX_SLOTS)
+		return -ENOSPC;
+
+	sit9531x_prio_table_build(sitdev, pll_idx, members, srcs);
+	if (!memcmp(srcs, sitdev->chan[pll_idx].prio_srcs, sizeof(srcs)))
+		return 0;
+
+	return sit9531x_prio_table_commit(sitdev, pll_idx, srcs);
+}
+
+/*
+ * Check if the device holds the table the configured priorities give for
+ * @members.
+ */
+static bool sit9531x_prio_table_held(struct sit9531x_dev *sitdev, u8 pll_idx,
+				     u16 members)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+
+	sit9531x_prio_table_build(sitdev, pll_idx, members, srcs);
+
+	return !memcmp(srcs, sitdev->chan[pll_idx].prio_srcs, sizeof(srcs));
+}
+
+/*
+ * @input_idx is in hardware encoding (0-11, see sit9531x_input_hw_src()). A
+ * source not in the table keeps the priority for when it is connected. Caller
+ * must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 prio)
+{
+	struct sit9531x_chan *chan;
+	u16 old_known;
+	u8 old_prio;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+
+	chan = &sitdev->chan[pll_idx];
+	old_prio = chan->cfg_prio[input_idx];
+	old_known = chan->cfg_known;
+	chan->cfg_prio[input_idx] = prio;
+	chan->cfg_known |= BIT(input_idx);
+
+	if (!(chan->prio_mask & BIT(input_idx)))
+		return 0;
+
+	rc = sit9531x_prio_table_apply(sitdev, pll_idx, chan->prio_mask);
+	if (rc && !sit9531x_prio_table_held(sitdev, pll_idx, chan->prio_mask)) {
+		chan->cfg_prio[input_idx] = old_prio;
+		chan->cfg_known = old_known;
+	}
+
+	return rc;
+}
+
+/*
+ * Removing the last source leaves an empty table, which keeps the PLL in
+ * holdover. Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       u8 input_idx)
+{
+	struct sit9531x_chan *chan;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+
+	chan = &sitdev->chan[pll_idx];
+	if (!(chan->prio_mask & BIT(input_idx)))
+		return 0;
+
+	return sit9531x_prio_table_apply(sitdev, pll_idx,
+					 chan->prio_mask & ~BIT(input_idx));
+}
+
+/*
+ * A source the PLL never listed gets the priority of the last slot. Caller must
+ * hold sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx)
+{
+	struct sit9531x_chan *chan;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	input_idx = sit9531x_prio_src_canon(sitdev, input_idx);
+	if (input_idx >= SIT9531X_PRIO_NUM_SRC)
+		return -EINVAL;
+
+	chan = &sitdev->chan[pll_idx];
+	if (chan->prio_mask & BIT(input_idx))
+		return 0;
+
+	return sit9531x_prio_table_apply(sitdev, pll_idx,
+					 chan->prio_mask | BIT(input_idx));
+}
+
 /* XO doubler register */
 #define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
 #define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
@@ -577,6 +1244,7 @@ static int sit9531x_chan_select_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	if (rc)
 		return rc;
 	chan->manual_input = false;
+	chan->manual_ref = SIT9531X_REF_INVALID;
 	if (!(cfg & SIT9531X_PLL_CONFIG1F_MANUAL_SEL))
 		return 0;
 
@@ -597,6 +1265,9 @@ static int sit9531x_chan_select_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	chan->manual_input = true;
 	if (sel & SIT9531X_MAN_IN_SEL_FROM_REG) {
 		sel = FIELD_GET(SIT9531X_MAN_IN_SEL_MASK, sel);
+		chan->manual_ref =
+			sit9531x_hw_src_input(sit9531x_prio_src_canon(sitdev,
+								      sel));
 		dev_info(sitdev->dev,
 			 "PLL%c: the profile selects its reference from the register, input code %u; reported as manual\n",
 			 'A' + pll_idx, sel);
@@ -609,17 +1280,65 @@ static int sit9531x_chan_select_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	return 0;
 }
 
+/*
+ * Read the reference the device feeds a PLL from the debug status bus; the
+ * active selection does not change on a fallback. The caller rules out free-run
+ * first and must hold sitdev->multiop_lock.
+ */
+static int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev,
+					 u8 pll_idx, u8 *ref)
+{
+	int rc, lock_rc;
+	u8 v, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto relock;
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_STATUS_ADDR,
+				   SIT9531X_DBG_STATUS_ROUTED_SRC);
+	if (rc)
+		goto relock;
+
+	/* The first reads return earlier latches; the last is current. */
+	for (i = 0; i < SIT9531X_DBG_STATUS_READS; i++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DBG_STATUS_DATA,
+					  &v);
+		if (rc)
+			goto relock;
+	}
+
+	/*
+	 * Canonicalize like the table entries: a differential pair fed through
+	 * its N-lane code is the P-lane pin.
+	 */
+	v = sit9531x_prio_src_canon(sitdev, v & SIT9531X_DBG_ROUTED_SRC_MASK);
+	*ref = sit9531x_hw_src_input(v);
+
+relock:
+	lock_rc = sit9531x_write_pll_close(sitdev, pll_idx,
+					   SIT9531X_PLL_REG_DEBUG,
+					   SIT9531X_PLL_DEBUG_LOCK);
+
+	return rc ? rc : lock_rc;
+}
+
 /*
  * Read the PLL's run, lock and holdover state, mode, selection and priority
  * table.
  */
 static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 {
-	u8 status, outer_lol, input_sel, inner_lol, ho_freeze, activesel_reg;
+	u8 status, outer_lol, inner_lol, ho_freeze, pll_status_1;
 	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
 	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
-	u8 pll_status_1;
-	bool active;
+	u8 routed = SIT9531X_REF_INVALID;
+	bool active, tracking;
 	int rc;
 
 	/*
@@ -640,21 +1359,6 @@ static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	if (rc)
 		return rc;
 
-	/*
-	 * The active selection is CLK_ACTIVESEL_PLL, the low nibble of the last
-	 * register of the PLL's Page 1 priority block. PLL-page 0x29 is a
-	 * config register.
-	 */
-	activesel_reg = SIT9531X_PRIO_BASE_REG +
-			SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
-			SIT9531X_PRIO_ACTIVESEL_OFF;
-	rc = sit9531x_read_u8(sitdev,
-			      SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
-					   activesel_reg),
-			      &input_sel);
-	if (rc)
-		return rc;
-
 	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_STATUS,
 			      &inner_lol);
 	if (rc)
@@ -665,23 +1369,48 @@ static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	if (rc)
 		return rc;
 
+	/*
+	 * Read the routed reference only while the PLL tracks one; in free-run
+	 * it has none.
+	 */
+	tracking = active && !(outer_lol & BIT(pll_idx)) &&
+		   !(status & SIT9531X_PLL_STATUS_OUTER_DIS) &&
+		   !(ho_freeze & BIT(pll_idx));
+	if (tracking) {
+		rc = sit9531x_chan_routed_ref_read(sitdev, pll_idx, &routed);
+		if (rc)
+			return rc;
+	}
+
 	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_STATUS_1,
 				  &pll_status_1);
 	if (rc)
 		return rc;
 
-	rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
-	if (rc)
-		return rc;
+	/* Keep the cached table while its write-back is pending */
+	if (!chan->table_pending) {
+		rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+		if (rc)
+			return rc;
 
-	sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+		sit9531x_prio_mask_build(sitdev, pll_idx, srcs);
+
+		/*
+		 * A table that differs from the last one written was changed
+		 * outside the driver; take it as the configuration.
+		 */
+		if (!chan->seen_valid ||
+		    memcmp(srcs, chan->seen_srcs, sizeof(chan->seen_srcs)))
+			sit9531x_prio_cfg_seed(sitdev, pll_idx, srcs);
+	}
 
 	/* STATUS_1_GENERIC reports loss of lock, so invert it. */
 	chan->active = active;
 	chan->locked = active && !(outer_lol & BIT(pll_idx));
 	chan->mode = !!(status & SIT9531X_PLL_STATUS_OUTER_DIS);
-	chan->selected_ref =
-		sit9531x_hw_src_input(input_sel & SIT9531X_PRIO_NIBBLE_MASK);
+	chan->routed_ref = routed;
+	if (chan->manual_input && routed != SIT9531X_REF_INVALID)
+		chan->manual_ref = routed;
 	chan->inner_lol = !!(inner_lol & BIT(pll_idx));
 	chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
 	chan->ho_valid = !!(pll_status_1 & SIT9531X_PLL_STATUS_1_HO_VALID);
@@ -792,11 +1521,39 @@ static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/*
+ * Mark the input each manual-select PLL uses; its table need not list it.
+ * Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_manual_claims_add(struct sit9531x_dev *sitdev)
+{
+	const struct sit9531x_chan *chan;
+	u8 pll_idx;
+
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		chan = &sitdev->chan[pll_idx];
+		if (chan->manual_input &&
+		    chan->manual_ref < sitdev->info->num_inputs)
+			sitdev->ref[chan->manual_ref].pll_mask |= BIT(pll_idx);
+	}
+}
+
+/* Arm the one-shot re-select of every PLL; see sit9531x_reselect(). */
+static void sit9531x_reselect_arm(struct sit9531x_dev *sitdev)
+{
+	u8 i;
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++)
+		sitdev->chan[i].reselect_armed = true;
+}
+
 static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 {
 	int rc;
 	u8 i;
 
+	sit9531x_reselect_arm(sitdev);
+
 	/* Detect SE/DE configuration before any per-lane access */
 	for (i = 0; i < sitdev->info->num_inputs / 2; i++) {
 		rc = sit9531x_input_mode_fetch(sitdev, i);
@@ -816,6 +1573,13 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 		}
 	}
 
+	rc = sit9531x_input_mon_fetch(sitdev);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to read the input clock monitors: %d\n", rc);
+		return rc;
+	}
+
 	mutex_lock(&sitdev->multiop_lock);
 	rc = sit9531x_ref_pll_mask_fetch(sitdev);
 	mutex_unlock(&sitdev->multiop_lock);
@@ -851,34 +1615,211 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 		return rc;
 	}
 
+	/* Needs the select registers and samples read above */
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_manual_claims_add(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+
 	return 0;
 }
 
-static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+/*
+ * Read each receiver's enable and every lane's clock monitor; return true if
+ * any read succeeds.
+ */
+static bool
+sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
 {
+	bool answered = false;
 	int i, rc;
 
 	for (i = 0; i < sitdev->info->num_inputs; i++) {
 		rc = sit9531x_ref_state_fetch(sitdev, i);
 		if (rc)
-			dev_warn(sitdev->dev,
-				 "Failed to get REF%u status: %d\n", i, rc);
+			dev_warn_ratelimited(sitdev->dev,
+					     "Failed to get REF%u status: %d\n",
+					     i, rc);
+		else
+			answered = true;
 	}
+
+	rc = sit9531x_input_mon_fetch(sitdev);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "Failed to read the input clock monitors: %d\n",
+				     rc);
+	else
+		answered = true;
+
+	return answered;
 }
 
-static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+/*
+ * Retry turning off a receiver unless a PLL has claimed the input since. Caller
+ * must hold sitdev->multiop_lock.
+ */
+static void sit9531x_input_off_retry(struct sit9531x_dev *sitdev)
 {
+	u8 i;
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		struct sit9531x_ref *ref = &sitdev->ref[i];
+
+		if (!ref->off_pending)
+			continue;
+		if (ref->pll_mask)
+			ref->off_pending = false;
+		else
+			sit9531x_input_disable(sitdev, i);
+	}
+}
+
+/*
+ * Rebuild pll_mask from the cached priority tables and the manual-select
+ * inputs. Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_ref_pll_mask_rebuild(struct sit9531x_dev *sitdev)
+{
+	u8 pll_idx, src, index;
+
+	for (index = 0; index < sitdev->info->num_inputs; index++)
+		sitdev->ref[index].pll_mask = 0;
+
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		u16 mask = sitdev->chan[pll_idx].prio_mask;
+
+		for (src = 0; src < SIT9531X_PRIO_NUM_SRC; src++) {
+			if (!(mask & BIT(src)))
+				continue;
+
+			index = sit9531x_hw_src_input(src);
+			if (index < sitdev->info->num_inputs)
+				sitdev->ref[index].pll_mask |= BIT(pll_idx);
+		}
+	}
+
+	sit9531x_manual_claims_add(sitdev);
+}
+
+/*
+ * Retry a pending forced-holdover release; a pending table write-back releases
+ * it instead. Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_ho_release_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+
+	if (!chan->ho_release_pending || chan->table_pending)
+		return;
+
+	if (sit9531x_update_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_HO_CTRL,
+				   BIT(SIT9531X_PLL_HO_FORCE_BIT), 0))
+		return;
+
+	chan->ho_release_pending = false;
+	dev_info(sitdev->dev, "PLL%c: forced holdover released\n",
+		 'A' + pll_idx);
+}
+
+/*
+ * Write back the priority table after a failed write. Caller must hold
+ * sitdev->multiop_lock.
+ */
+static void sit9531x_table_back_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 back[SIT9531X_PRIO_MAX_SLOTS];
+
+	if (!sitdev->chan[pll_idx].table_pending)
+		return;
+
+	memcpy(back, sitdev->chan[pll_idx].table_back, sizeof(back));
+	if (sit9531x_prio_table_commit(sitdev, pll_idx, back))
+		return;
+
+	dev_info(sitdev->dev, "PLL%c: priority table written back\n",
+		 'A' + pll_idx);
+}
+
+/*
+ * Rewrite the priority table when a listed source returns to a PLL without a
+ * reference; the latch restarts acquisition. Caller must hold
+ * sitdev->multiop_lock.
+ */
+static void sit9531x_reselect(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 top;
+	int rc;
+
+	/* A PLL on manual input select uses no priority table */
+	if (chan->manual_input || !chan->prio_mask || chan->mode ||
+	    chan->ho_release_pending || chan->table_pending)
+		return;
+
+	top = sit9531x_prio_top_live(sitdev, chan->prio_srcs);
+	if (chan->reselect_armed) {
+		if (top == SIT9531X_PRIO_SRC_NONE || !chan->ho_freeze) {
+			chan->reselect_armed = false;
+			chan->live_top = top;
+			return;
+		}
+	} else {
+		if (top == chan->live_top)
+			return;
+		/*
+		 * Tracking is locked and not frozen; the two bits are read
+		 * independently.
+		 */
+		if (top == SIT9531X_PRIO_SRC_NONE ||
+		    (chan->locked && !chan->ho_freeze)) {
+			chan->live_top = top;
+			return;
+		}
+	}
+
+	rc = sit9531x_prio_table_commit(sitdev, pll_idx, chan->prio_srcs);
+	if (rc) {
+		dev_warn_ratelimited(sitdev->dev,
+				     "PLL%c: failed to re-select a returned source: %d\n",
+				     'A' + pll_idx, rc);
+		return;
+	}
+
+	chan->reselect_armed = false;
+	chan->live_top = top;
+	dev_info(sitdev->dev, "PLL%c: re-selecting, a source is back\n",
+		 'A' + pll_idx);
+}
+
+/*
+ * Retry pending writes, then read each PLL's state; return true if any state
+ * read succeeds.
+ */
+static bool
+sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+	bool answered = false;
 	int i, rc;
 
+	sit9531x_input_off_retry(sitdev);
 	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sit9531x_table_back_retry(sitdev, i);
+		sit9531x_ho_release_retry(sitdev, i);
 		rc = sit9531x_chan_state_fetch(sitdev, i);
 		if (rc) {
 			sitdev->chan[i].stale = true;
-			dev_warn(sitdev->dev,
-				 "Failed to get PLL%c state: %d\n",
-				 'A' + i, rc);
+			dev_warn_ratelimited(sitdev->dev,
+					     "Failed to get PLL%c state: %d\n",
+					     'A' + i, rc);
+			continue;
 		}
+		answered = true;
+		sit9531x_reselect(sitdev, i);
 	}
+
+	sit9531x_ref_pll_mask_rebuild(sitdev);
+
+	return answered;
 }
 
 static void sit9531x_dev_periodic_work(struct kthread_work *work)
@@ -886,6 +1827,7 @@ static void sit9531x_dev_periodic_work(struct kthread_work *work)
 	struct sit9531x_dev *sitdev = container_of(work, struct sit9531x_dev,
 						   work.work);
 	struct sit9531x_dpll *sitdpll;
+	bool answered;
 	int rc;
 
 	/*
@@ -893,8 +1835,8 @@ static void sit9531x_dev_periodic_work(struct kthread_work *work)
 	 * take DPLL-subsystem locks, which nest outside it.
 	 */
 	mutex_lock(&sitdev->multiop_lock);
-	sit9531x_dev_ref_states_update(sitdev);
-	sit9531x_dev_chan_states_update(sitdev);
+	answered = sit9531x_dev_ref_states_update(sitdev);
+	answered |= sit9531x_dev_chan_states_update(sitdev);
 	mutex_unlock(&sitdev->multiop_lock);
 
 	list_for_each_entry(sitdpll, &sitdev->dplls, list)
@@ -912,8 +1854,14 @@ static void sit9531x_dev_periodic_work(struct kthread_work *work)
 				     "Failed to clear notifications: %d\n",
 				     rc);
 
+	if (answered || !rc)
+		sitdev->poll_fails = 0;
+	else if (sitdev->poll_fails < SIT9531X_POLL_STRETCH_MAX)
+		sitdev->poll_fails++;
+
 	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
-				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS <<
+						    sitdev->poll_fails));
 }
 
 /* Queue an immediate poll tick; polling continues as a fallback. */
@@ -1046,6 +1994,63 @@ static void sit9531x_pll_states_report(struct sit9531x_dev *sitdev)
 	}
 }
 
+static bool sit9531x_input_pin_is_registrable(struct sit9531x_dev *sitdev,
+					      u8 index)
+{
+	if (index >= sitdev->info->num_inputs)
+		return false;
+
+	/* The N lane of a differential pair is not a standalone input. */
+	if (sit9531x_input_is_n(index) &&
+	    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+		return false;
+
+	return true;
+}
+
+/* Warn about a pin node whose reg names no registrable pin */
+static void sit9531x_pin_nodes_check(struct sit9531x_dev *sitdev)
+{
+	struct fwnode_handle *pins, *node;
+	bool found;
+	u32 reg;
+	u8 i;
+
+	pins = device_get_named_child_node(sitdev->dev, "input-pins");
+	fwnode_for_each_child_node(pins, node) {
+		if (fwnode_property_read_u32(node, "reg", &reg))
+			continue;
+		if (reg < sitdev->info->num_inputs &&
+		    sit9531x_input_pin_is_registrable(sitdev, reg))
+			continue;
+		dev_warn(sitdev->dev,
+			 "input-pins/%pfwP: reg %u is %s, node ignored\n",
+			 node, reg,
+			 reg < sitdev->info->num_inputs ?
+			 "the N lane of a differential pair" :
+			 "not an input lane");
+	}
+	fwnode_handle_put(pins);
+
+	pins = device_get_named_child_node(sitdev->dev, "output-pins");
+	fwnode_for_each_child_node(pins, node) {
+		if (fwnode_property_read_u32(node, "reg", &reg))
+			continue;
+		found = false;
+		for (i = 0; i < sitdev->info->num_outputs; i++) {
+			if (sitdev->info->clkout_map[i] == reg) {
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			dev_warn(sitdev->dev,
+				 "output-pins/%pfwP: reg %u is not an output of %s, node ignored\n",
+				 node, reg, sitdev->info->name);
+	}
+	fwnode_handle_put(pins);
+}
+
 static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
 {
 	struct sit9531x_dpll *sitdpll;
@@ -1060,6 +2065,8 @@ static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
 	sit9531x_pll_states_report(sitdev);
 	mutex_unlock(&sitdev->multiop_lock);
 
+	sit9531x_pin_nodes_check(sitdev);
+
 	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
 		rc = sit9531x_dpll_register(sitdpll);
 		if (rc) {
@@ -1084,6 +2091,7 @@ static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
 static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
 {
 	struct sit9531x_dpll *sitdpll;
+	u8 i;
 
 	kthread_cancel_delayed_work_sync(&sitdev->work);
 
@@ -1091,6 +2099,30 @@ static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
 		if (sitdpll->dpll_dev)
 			sit9531x_dpll_unregister(sitdpll);
 	}
+
+	/*
+	 * Write back tables first: forced holdover stays set while a table
+	 * write-back is pending.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sit9531x_table_back_retry(sitdev, i);
+		if (sitdev->chan[i].table_pending)
+			dev_warn(sitdev->dev,
+				 "PLL%c priority table left as written, holdover kept\n",
+				 'A' + i);
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+
+	mutex_lock(&sitdev->multiop_lock);
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sit9531x_ho_release_retry(sitdev, i);
+		if (sitdev->chan[i].ho_release_pending &&
+		    !sitdev->chan[i].table_pending)
+			dev_warn(sitdev->dev,
+				 "PLL%c left in forced holdover\n", 'A' + i);
+	}
+	mutex_unlock(&sitdev->multiop_lock);
 }
 
 static struct sit9531x_dpll_pin *
@@ -1209,10 +2241,15 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
 					     enum dpll_pin_direction dir,
 					     u8 index)
 {
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
 	if (dir != DPLL_PIN_DIRECTION_INPUT)
 		return false;
 
-	return index == SIT9531X_MAX_INPUTS;
+	if (index == SIT9531X_MAX_INPUTS)
+		return true;
+
+	return sit9531x_input_pin_is_registrable(sitdev, index);
 }
 
 static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
@@ -1600,6 +2637,9 @@ static int sit9531x_resume(struct device *dev)
 
 	/* After power loss the part is on page 0, not the cached page */
 	sit9531x_page_cache_drop(sitdev);
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_reselect_arm(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
 	if (sitdev->irq > 0)
 		enable_irq(sitdev->irq);
 	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 9e873432df3d..c2fa6a17be43 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -41,7 +41,7 @@
  */
 #define SIT9531X_POLL_STRETCH_MAX	3
 
-/* selected_ref value when the active source is not a registered input */
+/* routed_ref value when the PLL is fed no registered input */
 #define SIT9531X_REF_INVALID		0xFF
 
 /* SiTime IEEE OUI for EUI-64 generation */
@@ -79,14 +79,20 @@ enum sit9531x_signal_mode {
  * struct sit9531x_ref - input reference state
  * @freq: configured frequency in Hz
  * @enabled: input receiver is on
+ * @off_pending: receiver gate-off to retry while no PLL claims the input
  * @pll_mask: PLLs this input feeds (bit 0 = PLLA)
+ * @los: clock monitor reports loss of signal
+ * @qual_fail: clock monitor reports frequency drift while the input has signal
  * @sig_mode: signal mode of the input pair, read from CLKINx_INPUT_MODE at
  *   probe
  */
 struct sit9531x_ref {
 	u64		freq;
 	bool		enabled;
+	bool		off_pending;
 	u8		pll_mask;
+	bool		los;
+	bool		qual_fail;
 	enum sit9531x_signal_mode	sig_mode;
 };
 
@@ -100,6 +106,8 @@ struct sit9531x_ref {
  * @routed: mapped to @pll_idx by the initial configuration; an unrouted
  *   output has no pin
  * @pll_idx: PLL driving this output (0-3)
+ * @retime_pending: re-timed delay not yet applied; the poll applies @retime_ps
+ * @retime_ps: delay to apply while @retime_pending
  */
 struct sit9531x_out {
 	u64		freq;
@@ -108,6 +116,8 @@ struct sit9531x_out {
 	bool		part_muted;
 	bool		routed;
 	u8		pll_idx;
+	bool		retime_pending;
+	s32		retime_ps;
 };
 
 /*
@@ -118,14 +128,30 @@ struct sit9531x_out {
  * @stale: last state read failed; the fields hold the previous sample and
  *   are not reported
  * @mode: 0 = sync (outer loop enabled), 1 = free-run
- * @selected_ref: logical index of the selected reference (INTSYNC:
- *   SIT9531X_INTSYNC_PIN_ID), or SIT9531X_REF_INVALID for a reserved code
+ * @routed_ref: input the device routes to the PLL, as a logical index
+ *   (INTSYNC: SIT9531X_INTSYNC_PIN_ID); SIT9531X_REF_INVALID if none
  * @inner_lol: inner loop loss of lock
  * @ho_freeze: holdover freeze active
  * @ho_valid: holdover memory holds a valid estimate
  * @manual_input: PLL uses its manual input select (pins or
  *   GPIO_INPUT_FUNC_CTRL5..8), which table writes do not change; reported
  *   as MANUAL
+ * @manual_ref: logical input of a manual PLL, SIT9531X_REF_INVALID if
+ *   unknown; its receiver stays claimed for the PLL
+ * @ho_release_pending: forced holdover not yet released
+ * @live_top: highest-priority listed source with signal at the last poll,
+ *   or SIT9531X_PRIO_SRC_NONE; a change while the PLL tracks no source
+ *   rewrites the table
+ * @reselect_armed: no poll since probe or resume; the first poll rewrites
+ *   the table of a PLL in holdover while a listed source has signal
+ * @flush_pending: output divider realignment flush not yet triggered after
+ *   a rate or delay commit
+ * @table_pending: priority table partly written or not latched; the PLL
+ *   stays in forced holdover
+ * @table_back: table reported and to be rewritten while @table_pending
+ * @tap_pending: debug taps not restored after a phase-offset read
+ * @prio_srcs: cached priority table, a source code per slot
+ * @prio_last: first slot of each source plus one, 0 if absent
  * @prio_mask: bit per canonical source in the priority table read back
  *   from hardware
  * @cfg_prio: configured priority of each canonical source, listed or not;
@@ -140,11 +166,21 @@ struct sit9531x_chan {
 	bool		locked;
 	bool		stale;
 	u8		mode;
-	u8		selected_ref;
+	u8		routed_ref;
 	bool		inner_lol;
 	bool		ho_freeze;
 	bool		ho_valid;
 	bool		manual_input;
+	u8		manual_ref;
+	bool		ho_release_pending;
+	u8		live_top;
+	bool		reselect_armed;
+	bool		flush_pending;
+	bool		table_pending;
+	u8		table_back[SIT9531X_PRIO_MAX_SLOTS];
+	bool		tap_pending;
+	u8		prio_srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8		prio_last[SIT9531X_PRIO_NUM_SRC];
 	u16		prio_mask;
 	u8		cfg_prio[SIT9531X_PRIO_NUM_SRC];
 	u16		cfg_known;
@@ -176,6 +212,7 @@ struct sit9531x_chan {
  * @irq: optional INTRB IRQ, 0 if not wired
  * @intsync_src: PLL sourcing INTSYNC, or -1
  * @irq_ack_fails: consecutive failed INTRB acknowledgements
+ * @loops_open: output loops not yet re-locked
  */
 struct sit9531x_dev {
 	struct device			*dev;
@@ -206,6 +243,8 @@ struct sit9531x_dev {
 	int			irq;
 
 	s8			intsync_src;
+
+	bool			loops_open;
 };
 
 int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
@@ -219,6 +258,18 @@ int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
 int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
 			   u8 offset, u8 mask, u8 val);
 
+int sit9531x_input_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_input_enable(struct sit9531x_dev *sitdev, u8 index);
+
+bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev,
+				 u8 pll_idx, u8 input_idx);
+int sit9531x_input_prio_set(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 prio);
+int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       u8 input_idx);
+int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx);
+
 static inline u8 sit9531x_pll_page(u8 pll_idx)
 {
 	return SIT9531X_PAGE_PLLA + pll_idx;
@@ -287,4 +338,10 @@ sit9531x_chan_state_get(const struct sit9531x_dev *sitdev, u8 pll_idx)
 	return &sitdev->chan[pll_idx];
 }
 
+static inline bool sit9531x_chan_tracking(const struct sit9531x_chan *chan)
+{
+	return chan->locked && !chan->inner_lol && !chan->mode &&
+	       !chan->ho_freeze && !chan->stale;
+}
+
 #endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 6d676fabf13e..56bdec4bcf67 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -34,10 +34,23 @@ sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
 	       pin->id == SIT9531X_MAX_INPUTS;
 }
 
+/*
+ * A frozen PLL with an empty table is in forced holdover; report holdover only
+ * while its holdover memory is valid.
+ */
+static enum dpll_lock_status
+sit9531x_dpll_frozen_status(const struct sit9531x_chan *chan)
+{
+	if (!chan->prio_mask && !chan->ho_valid)
+		return DPLL_LOCK_STATUS_UNLOCKED;
+
+	return DPLL_LOCK_STATUS_HOLDOVER;
+}
+
 /*
  * Report the lock status from the last poll: outer LOL (page 0 reg 0x06), PLL
- * mode (PLL page reg 0x31), inner LOL (reg 0x92), holdover freeze (reg 0x0A)
- * and holdover-valid (PLL page reg 0x06).
+ * mode (PLL page reg 0x31), inner LOL (reg 0x92), holdover freeze (reg 0x0A),
+ * holdover-valid (PLL page reg 0x06) and priority table.
  */
 static int
 sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
@@ -71,7 +84,7 @@ sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
 		 * The core reports the error detail only with an unlocked or
 		 * holdover status.
 		 */
-		*status = chan->ho_freeze ? DPLL_LOCK_STATUS_HOLDOVER :
+		*status = chan->ho_freeze ? sit9531x_dpll_frozen_status(chan) :
 					    DPLL_LOCK_STATUS_UNLOCKED;
 	} else if (chan->mode) {
 		/*
@@ -84,8 +97,8 @@ sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
 		 * Test freeze before lock: a frozen PLL tracks no reference
 		 * whatever its outer loss-of-lock bit reads.
 		 */
-		*status = DPLL_LOCK_STATUS_HOLDOVER;
-	} else if (chan->locked) {
+		*status = sit9531x_dpll_frozen_status(chan);
+	} else if (sit9531x_chan_tracking(chan)) {
 		if (chan->ho_valid)
 			*status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
 		else
@@ -155,6 +168,120 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
 	.supported_modes_get	= sit9531x_dpll_supported_modes_get,
 };
 
+/*
+ * Pin-state contract. An input is SELECTABLE while listed in this PLL's
+ * priority table and DISCONNECTED otherwise; CONNECTED is refused because the
+ * device selects by priority. Operstate is ACTIVE when the device feeds the PLL
+ * this input (chan->routed_ref), the PLL is tracking and the lane has signal;
+ * NO_SIGNAL on loss of signal; QUAL_FAILED on frequency drift; STANDBY
+ * otherwise. The routed reference is used because on loss of signal the device
+ * falls back to another listed source and leaves the selection naming the lost
+ * one. On manual input select, the routed input is CONNECTED. The XO pin is
+ * always CONNECTED.
+ */
+
+/* Caller must hold sitdev->multiop_lock. */
+static void
+sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
+				  const struct sit9531x_dpll *sitdpll,
+				  u8 pin_id, enum dpll_pin_state *state)
+{
+	const struct sit9531x_chan *chan;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	if (chan->manual_input) {
+		if (chan->routed_ref == pin_id && !chan->stale)
+			*state = DPLL_PIN_STATE_CONNECTED;
+		else
+			*state = DPLL_PIN_STATE_DISCONNECTED;
+		return;
+	}
+
+	if (chan->prio_mask & BIT(sit9531x_input_hw_src(pin_id)))
+		*state = DPLL_PIN_STATE_SELECTABLE;
+	else
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+static bool
+sit9531x_dpll_selection_active(struct sit9531x_dev *sitdev,
+			       const struct sit9531x_dpll *sitdpll, u8 pin_id)
+{
+	const struct sit9531x_chan *chan;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	if (chan->routed_ref != pin_id || !sit9531x_chan_tracking(chan))
+		return false;
+
+	/*
+	 * Do not report a lane without signal as locked, whatever the lock bit
+	 * says; the PLL is about to switch away from it.
+	 */
+	if (pin_id < sitdev->info->num_inputs &&
+	    sit9531x_ref_state_get(sitdev, pin_id)->los)
+		return false;
+
+	return true;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+static void
+sit9531x_dpll_selection_operstate_get(struct sit9531x_dev *sitdev,
+				      const struct sit9531x_dpll *sitdpll,
+				      u8 pin_id,
+				      enum dpll_pin_operstate *operstate)
+{
+	const struct sit9531x_ref *ref;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (sit9531x_dpll_selection_active(sitdev, sitdpll, pin_id)) {
+		*operstate = DPLL_PIN_OPERSTATE_ACTIVE;
+		return;
+	}
+
+	if (pin_id < sitdev->info->num_inputs) {
+		ref = sit9531x_ref_state_get(sitdev, pin_id);
+		if (ref->los) {
+			*operstate = DPLL_PIN_OPERSTATE_NO_SIGNAL;
+			return;
+		}
+		if (ref->qual_fail) {
+			*operstate = DPLL_PIN_OPERSTATE_QUAL_FAILED;
+			return;
+		}
+	}
+
+	*operstate = DPLL_PIN_OPERSTATE_STANDBY;
+}
+
+static int
+sit9531x_dpll_input_pin_operstate_on_dpll_get(const struct dpll_pin *pin,
+					      void *pin_priv,
+					      const struct dpll_device *dpll,
+					      void *dpll_priv,
+					      enum dpll_pin_operstate *state,
+					      struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dpll_selection_operstate_get(sitdev, sitdpll, dpin->id,
+					      state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
 static int
 sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 				      void *pin_priv,
@@ -167,8 +294,123 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 	return 0;
 }
 
+static int
+sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv,
+					  enum dpll_pin_state *state,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dpll_selection_state_get(sitdev, sitdpll, dpin->id, state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * The input receiver is shared by all DPLLs and is switched off when
+ * ref->pll_mask becomes empty.
+ */
+static int
+sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv,
+					  enum dpll_pin_state state,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	struct sit9531x_ref *ref = &sitdev->ref[dpin->id];
+	u8 hw_src = sit9531x_input_hw_src(dpin->id);
+	u8 pll_bit = BIT(sitdpll->id);
+	bool enabled_here = false;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (!sit9531x_input_prio_present(sitdev, sitdpll->id, hw_src) &&
+		    !ref->off_pending) {
+			rc = 0;
+			break;
+		}
+		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+		if (rc && sit9531x_input_prio_present(sitdev, sitdpll->id,
+						      hw_src))
+			break;
+		ref->pll_mask &= ~pll_bit;
+		if (!ref->pll_mask) {
+			int off_rc = sit9531x_input_disable(sitdev, dpin->id);
+
+			if (off_rc && !rc)
+				rc = off_rc;
+		}
+		break;
+	case DPLL_PIN_STATE_CONNECTED:
+		NL_SET_ERR_MSG(extack,
+			       "Device selects its reference by priority; use selectable");
+		rc = -EOPNOTSUPP;
+		break;
+	case DPLL_PIN_STATE_SELECTABLE:
+		/*
+		 * The receiver can be off even when this PLL already lists the
+		 * input.
+		 */
+		if (!ref->enabled) {
+			rc = sit9531x_input_enable(sitdev, dpin->id);
+			if (rc)
+				break;
+			enabled_here = true;
+		}
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		if (rc && !sit9531x_input_prio_present(sitdev, sitdpll->id,
+						       hw_src)) {
+			if (enabled_here && !ref->pll_mask)
+				sit9531x_input_disable(sitdev, dpin->id);
+			break;
+		}
+		ref->pll_mask |= pll_bit;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc == -ENOSPC)
+		NL_SET_ERR_MSG(extack,
+			       "Priority table is full of unique sources on this PLL");
+	else if (rc && rc != -EOPNOTSUPP && rc != -EINVAL)
+		NL_SET_ERR_MSG(extack, "Failed to set input pin state");
+
+	return rc;
+}
+
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_input_pin_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_input_pin_state_on_dpll_set,
+	.operstate_on_dpll_get	= sit9531x_dpll_input_pin_operstate_on_dpll_get,
+};
+
+/*
+ * For a PLL on manual input select: the select pins or register choose the
+ * reference, so the pin has no state setter and no priority.
+ */
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_manual_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_input_pin_state_on_dpll_get,
+	.operstate_on_dpll_get	= sit9531x_dpll_input_pin_operstate_on_dpll_get,
 };
 
 /*
@@ -223,10 +465,15 @@ static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
 const struct dpll_pin_ops *
 sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
 {
+	const struct sit9531x_dev *sitdev = pin->dpll->dev;
+
 	if (!sit9531x_dpll_is_input_pin(pin))
 		return &sit9531x_dpll_output_pin_ops;
 	if (sit9531x_dpll_is_xo_pin(pin))
 		return &sit9531x_dpll_xo_pin_ops;
+	/* Read once at probe, before any pin is registered. */
+	if (sit9531x_chan_state_get(sitdev, pin->dpll->id)->manual_input)
+		return &sit9531x_dpll_input_pin_manual_ops;
 	return &sit9531x_dpll_input_pin_ops;
 }
 
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 832d0360a398..39d836f6b563 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -96,6 +96,10 @@
 #define SIT9531X_REG_GLOBAL_UPDATE		SIT9531X_REG(0x00, 0x0F)
 #define SIT9531X_SMALL_UPDATE_CMD		0x02
 
+/* PLL holdover control (PLL page offset) */
+#define SIT9531X_PLL_REG_HO_CTRL		0x6F
+#define SIT9531X_PLL_HO_FORCE_BIT		4
+
 /* One bit per input PAIR (bit 0 = CLKIN0, ..., bit 3 = CLKIN3) */
 #define SIT9531X_REG_IN_DE_FORCE		SIT9531X_REG(0x02, 0xE8)
 #define SIT9531X_REG_IN_DE_STATE		SIT9531X_REG(0x02, 0xE9)
@@ -183,6 +187,29 @@
 
 #define SIT9531X_PLL_STATUS_OUTER_DIS	BIT(5)
 
+/*
+ * Writing the key unlocks the page's debug registers; writing zero locks them.
+ */
+#define SIT9531X_PLL_REG_DEBUG		0xBD
+#define SIT9531X_PLL_DEBUG_UNLOCK		0xC3
+#define SIT9531X_PLL_DEBUG_LOCK			0x00
+
+/*
+ * The address register selects the block the data register reports. A data read
+ * returns what the previous read latched; the documented procedures read it
+ * three times.
+ */
+#define SIT9531X_PLL_REG_DBG_STATUS_ADDR	0xD1
+#define SIT9531X_PLL_REG_DBG_STATUS_DATA	0xD0
+#define SIT9531X_DBG_STATUS_READS		3
+
+/*
+ * Low nibble: the source the input subsystem routes to the PLL, in the
+ * priority-table encoding.
+ */
+#define SIT9531X_DBG_STATUS_ROUTED_SRC	0xB0
+#define SIT9531X_DBG_ROUTED_SRC_MASK		GENMASK(3, 0)
+
 /*
  * PLL_CONFIG1F_PLL bit 6: manual active select, no priority arbitration. The
  * register is GUI generated; the driver only reads it.
@@ -213,6 +240,17 @@
 #define SIT9531X_PLL_REG_STATUS_1		0x06
 #define SIT9531X_PLL_STATUS_1_HO_VALID	BIT(2)
 
+/*
+ * Clock monitor status, one nibble per input lane; the lower-numbered input of
+ * each register is in the low nibble.
+ */
+#define SIT9531X_CLKMON_P_STATUS_01		SIT9531X_REG(0x06, 0x02)
+#define SIT9531X_CLKMON_P_STATUS_23		SIT9531X_REG(0x06, 0x06)
+#define SIT9531X_CLKMON_N_STATUS_01		SIT9531X_REG(0x06, 0x92)
+#define SIT9531X_CLKMON_N_STATUS_23		SIT9531X_REG(0x06, 0x96)
+#define SIT9531X_CLKMON_FINE_DRIFT		BIT(0)
+#define SIT9531X_CLKMON_COARSE_DRIFT		BIT(1)
+#define SIT9531X_CLKMON_LOSS			BIT(2)
 #define SIT9531X_CLKMON_P_NOTIF_01		SIT9531X_REG(0x06, 0x03)
 #define SIT9531X_CLKMON_P_NOTIF_23		SIT9531X_REG(0x06, 0x07)
 
-- 
2.39.2 (Apple Git-143)


  parent reply	other threads:[~2026-11-10 20:26 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 01/13] dpll: omit pin phase attributes the driver has no value for Ali Rouhi
2026-11-11 15:35   ` Ivan Vecera
2026-11-10 20:26 ` [PATCH net-next v15 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 06/13] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
2026-11-10 20:26 ` Ali Rouhi [this message]
2026-11-10 20:26 ` [PATCH net-next v15 09/13] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 08/13] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 10/13] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 11/13] dpll: sit9531x: add support to adjust output phase Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
2026-11-10 20:26 ` [PATCH net-next v15 13/13] dpll: sit9531x: model the inter-PLL sync line as a pair of pins Ali Rouhi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261110202607.55744-8-arouhi@sitime.com \
    --to=arouhi@sitime.com \
    --cc=Oleg.Zadorozhnyi@devoxsoftware.com \
    --cc=arkadiusz.kubalewski@intel.com \
    --cc=cjubran@nvidia.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=ivecera@redhat.com \
    --cc=jiri@resnulli.us \
    --cc=krzk+dt@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=prabhakar.mahadev-lad.rj@bp.renesas.com \
    --cc=robh@kernel.org \
    --cc=vadim.fedorenko@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®