Devicetree
 help / color / mirror / Atom feed
From: Ali Rouhi <arouhi@sitime.com>
To: Jiri Pirko <jiri@resnulli.us>
Cc: Vadim Fedorenko <vadim.fedorenko@linux.dev>,
	Arkadiusz Kubalewski <arkadiusz.kubalewski@intel.com>,
	Ivan Vecera <ivecera@redhat.com>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>,
	Carolina Jubran <cjubran@nvidia.com>,
	Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.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 v12 12/12] dpll: sit9531x: model the inter-PLL sync net as a pair of pins
Date: Fri, 9 Oct 2026 18:32:00 +0000	[thread overview]
Message-ID: <20261009183151.78497-13-arouhi@sitime.com> (raw)
In-Reply-To: <20261009183151.78497-1-arouhi@sitime.com>

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

The device has an internal net by which one PLL can drive the others: the
source PLL puts its output on it, and any other PLL can select it as a
reference instead of an external input.  The two ends are nothing alike --
one is driven, the other is selected -- so they are two pins rather than
one: an output pin on the source and an input pin on each destination.

That keeps each pin honest about what its state means.  The source pin
reports whether this PLL is the one driving the net, and setting it takes
the net over or gives it up; a destination pin reports whether its PLL has
selected the net, and behaves like any other selectable input.  A single
pin would have had to answer both questions at once and could only have
been right about one of them.

A destination pin whose net no PLL drives reports no signal, as an
external input that lost its clock does.  The driver takes the net as
driven when it enabled the source itself or found that configuration
already in place.  The net has no signal monitor, and a loaded profile
can drive it with a configuration of its own, so a PLL the device feeds
from the net and holds locked also counts as the net carrying a signal.
A source that failed to take the net over, or whose configuration the
scan found only partly in place, is recorded as a partial owner, so a
retry re-runs the enable instead of being refused as busy; giving the
net up restores the global enable only while the net is still
configured, and the restore is latched as every other write to that
register is.  A failed request that moved the owner is announced.
Ownership and the output mutes are read back from the part on resume.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Assisted-by: LLM
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 414 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |   7 +
 drivers/dpll/sit9531x/dpll.c | 409 +++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/regs.h |   3 +
 4 files changed, 823 insertions(+), 10 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index fbac70787ada..c2d6fbd0363f 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -3027,6 +3027,381 @@ int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/*
+ * Close the debug window on a PLL's EXT page.  The key register opens
+ * every debug register on that page while it holds the unlock value.
+ */
+static int sit9531x_intsync_debug_lock(struct sit9531x_dev *sitdev, u8 ext_page)
+{
+	return sit9531x_write_u8(sitdev,
+				 SIT9531X_REG(ext_page, SIT9531X_PLL_REG_DEBUG),
+				 SIT9531X_PLL_DEBUG_LOCK);
+}
+
+/*
+ * INTSYNC configuration register values.
+ * These are written to the source PLL's EXT page to enable/disable
+ * inter-PLL synchronization (lock frequency PLL to phase PLL).
+ */
+struct sit9531x_intsync_reg {
+	u8 offset;
+	u8 en_val;
+	u8 dis_val;
+};
+
+static const struct sit9531x_intsync_reg intsync_config[] = {
+	{ 0x2D, 0x02, 0x00 },
+	{ 0x50, 0x08, 0x00 },
+	{ 0x51, 0x04, 0x00 },
+	{ 0x54, 0x02, 0x00 },
+	{ 0x55, 0x28, 0x20 },
+	{ 0x5C, 0x0F, 0x00 },
+	{ 0x5D, 0xFF, 0x00 },
+	{ 0x6C, 0xDD, 0x00 },
+};
+
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
+{
+	s8 src = -1, partial = -1;
+	u8 global;
+	u8 pll, ext_page;
+	int rc, ret, n_en, n_dis;
+	unsigned int i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
+	if (rc)
+		return rc;
+
+	if (!(global & BIT(SIT9531X_INTSYNC_EN_BIT))) {
+		sitdev->intsync_src = -1;
+		sitdev->intsync_partial = false;
+		return 0;
+	}
+
+	for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+		ext_page = SIT9531X_PLL_EXT_PAGE(pll);
+
+		/*
+		 * These are debug registers, which the enable and disable
+		 * sequences only touch with the EXT page's debug window
+		 * open; read them the same way.
+		 */
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    SIT9531X_PLL_REG_DEBUG),
+				       SIT9531X_PLL_DEBUG_UNLOCK);
+		if (rc)
+			return rc;
+
+		n_en = 0;
+		n_dis = 0;
+		for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+			u16 reg;
+			u8 val;
+
+			reg = SIT9531X_REG(ext_page, intsync_config[i].offset);
+
+			rc = sit9531x_read_u8(sitdev, reg, &val);
+			if (rc)
+				break;
+			if (val == intsync_config[i].en_val)
+				n_en++;
+			else if (val == intsync_config[i].dis_val)
+				n_dis++;
+		}
+
+		ret = sit9531x_intsync_debug_lock(sitdev, ext_page);
+		if (!rc)
+			rc = ret;
+		if (rc)
+			return rc;
+
+		if (n_en == ARRAY_SIZE(intsync_config)) {
+			/*
+			 * Only one PLL can drive the net.  If a second
+			 * one matches, the registers are not describing
+			 * a state this driver put the device in, so say
+			 * so rather than pick silently.
+			 */
+			if (src < 0)
+				src = pll;
+			else
+				dev_warn(sitdev->dev,
+					 "PLL%c also matches the INTSYNC source pattern; keeping PLL%c\n",
+					 'A' + pll, 'A' + src);
+		} else if (n_en && n_dis &&
+			   n_en + n_dis == ARRAY_SIZE(intsync_config) &&
+			   partial < 0) {
+			/*
+			 * Every register holds one of the two patterns and both
+			 * occur: the residue of an enable or a disable that
+			 * stopped part way, and nothing a profile writes.
+			 */
+			partial = pll;
+		}
+	}
+
+	/*
+	 * An enable or a disable that stopped part way leaves a PLL holding
+	 * a mix of the two patterns.  Treat it as the owner, so that another
+	 * PLL cannot be enabled on top of the leftover configuration, but
+	 * record that it is partial: a repeated connect on that PLL must run
+	 * the enable again rather than take the PLL for configured, and a
+	 * repeated disconnect runs the disable again.
+	 */
+	sitdev->intsync_partial = src < 0 && partial >= 0;
+	if (sitdev->intsync_partial) {
+		dev_warn(sitdev->dev,
+			 "PLL%c holds a partial INTSYNC source configuration\n",
+			 'A' + partial);
+		src = partial;
+	}
+
+	sitdev->intsync_src = src;
+
+	return 0;
+}
+
+static int __sit9531x_intsync_disable(struct sit9531x_dev *sitdev,
+				      u8 src_pll_idx, bool restore);
+
+/*
+ * sit9531x_intsync_enable - enable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Enables INTSYNC global bit, unlocks the source PLL's EXT page
+ * debug registers, writes configuration, and triggers a small
+ * update on the source PLL.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+	u8 ext_page, val;
+	int rc, lock_rc;
+	unsigned int i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (src_pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val | BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		return rc;
+
+	/* Small update on Page 0 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	usleep_range(1000, 2000);
+	if (rc)
+		goto relock_err;
+
+	/* Unlock debug on EXT page */
+	rc = sit9531x_write_u8(sitdev,
+			       SIT9531X_REG(ext_page,
+					    SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto relock_err;
+
+	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    intsync_config[i].offset),
+				       intsync_config[i].en_val);
+		if (rc)
+			goto relock_err;
+	}
+
+	/* Small update on source PLL */
+	rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+	if (rc)
+		goto relock_err;
+
+	rc = 0;
+	goto relock;
+
+relock_err:
+	sit9531x_intsync_debug_lock(sitdev, ext_page);
+	goto err_disable;
+
+relock:
+	/*
+	 * Close the EXT page debug window the sequence opened.  Nothing
+	 * else writes the key back, so leaving it open would keep the block
+	 * unlocked for as long as the device runs.  The net is driven by
+	 * now, though, so a failure here must not read as a failed enable:
+	 * the caller would then not record the owner of a net that is
+	 * driven.
+	 */
+	lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+	if (lock_rc)
+		dev_warn(sitdev->dev,
+			 "PLL%c: INTSYNC enabled but the debug window was left open: %d\n",
+			 'A' + src_pll_idx, lock_rc);
+
+	return rc;
+
+err_disable:
+	/*
+	 * The global enable is already set at this point.  The caller only
+	 * records the source PLL when this function succeeds, so nothing
+	 * else will ever clear the bit: undo it here rather than leave the
+	 * net asserted with a half-written EXT page.  The disable must not
+	 * put the bit back when it fails part way, as it does for a
+	 * disconnect: here the bit is what the rollback set out to clear,
+	 * and restoring it would leave the net asserted with no PLL
+	 * matching the source pattern, which no owner on record would ever
+	 * clear.
+	 */
+	{
+		int rollback_rc;
+
+		rollback_rc = __sit9531x_intsync_disable(sitdev, src_pll_idx,
+							 false);
+		if (rollback_rc)
+			dev_warn(sitdev->dev,
+				 "INTSYNC rollback failed after enable error: %d (original %d)\n",
+				 rollback_rc, rc);
+	}
+
+	return rc;
+}
+
+/*
+ * The disable sequence.  @restore says whether a failure before the EXT
+ * page holds the disable pattern puts the global enable back, which a
+ * disconnect wants (see restore_global below) and the rollback of a
+ * failed enable does not.
+ */
+static int __sit9531x_intsync_disable(struct sit9531x_dev *sitdev,
+				      u8 src_pll_idx, bool restore)
+{
+	u8 ext_page, val;
+	int rc, lock_rc;
+	unsigned int i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (src_pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	ext_page = SIT9531X_PLL_EXT_PAGE(src_pll_idx);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		goto restore_global;
+
+	/* Small update on Page 0 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	usleep_range(1000, 2000);
+	if (rc)
+		goto restore_global;
+
+	/* Unlock debug on EXT page */
+	rc = sit9531x_write_u8(sitdev,
+			       SIT9531X_REG(ext_page,
+					    SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto restore_global;
+
+	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    intsync_config[i].offset),
+				       intsync_config[i].dis_val);
+		if (rc)
+			goto restore_global;
+	}
+
+	/*
+	 * Small update on source PLL.  The global enable is latched off and
+	 * every register holds the disable value by now, so the net is not
+	 * driven whether or not this latch went through.  Putting the bit
+	 * back for a failure here would describe the net as driven with no
+	 * PLL matching the source pattern: the detector would record no
+	 * owner, and nothing would ever clear the bit.
+	 */
+	rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+
+	goto relock;
+
+restore_global:
+	/*
+	 * The global enable was cleared first, so a failure here leaves the
+	 * EXT page still holding the enable pattern with nothing pointing
+	 * at it: the source detector keys on the global bit, would report
+	 * the net as unowned, and a retry of the disable would then
+	 * short-circuit.  Put the bit back so the state stays one the
+	 * driver can describe and the request can be repeated.
+	 */
+	if (restore &&
+	    !sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &val) &&
+	    !sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val | BIT(SIT9531X_INTSYNC_EN_BIT))) {
+		/*
+		 * Every write of the global enable, here and in SiTime's
+		 * sequences, is followed by the page-0 small update that
+		 * applies it.  Without it the bit would sit pending until
+		 * an unrelated commit through the same update register
+		 * applied it, re-asserting the net over a half-written
+		 * EXT page with nobody having asked.
+		 */
+		sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+				  SIT9531X_SMALL_UPDATE_CMD);
+		usleep_range(1000, 2000);
+	}
+
+relock:
+	/*
+	 * Close the EXT page debug window the sequence opened.  After a
+	 * disable that took effect, a failure here is a warning, for the
+	 * same reason as in sit9531x_intsync_enable().
+	 */
+	lock_rc = sit9531x_intsync_debug_lock(sitdev, ext_page);
+	if (lock_rc) {
+		if (rc)
+			dev_warn(sitdev->dev,
+				 "PLL%c: debug window left open: %d\n",
+				 'A' + src_pll_idx, lock_rc);
+		else
+			dev_warn(sitdev->dev,
+				 "PLL%c: INTSYNC disabled but the debug window was left open: %d\n",
+				 'A' + src_pll_idx, lock_rc);
+	}
+
+	return rc;
+}
+
+/*
+ * sit9531x_intsync_disable - disable inter-PLL synchronization
+ * @src_pll_idx: source (frequency) PLL index (0-3)
+ *
+ * Clears INTSYNC global bit, writes disable values to the source
+ * PLL's EXT page, and triggers a small update.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx)
+{
+	return __sit9531x_intsync_disable(sitdev, src_pll_idx, true);
+}
+
 /**
  * sit9531x_phase_offset_read - read phase difference via TDC
  * @sitdev:	device pointer
@@ -3678,6 +4053,15 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 		return rc;
 	}
 
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_intsync_src_detect(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to detect INTSYNC source: %d\n", rc);
+		return rc;
+	}
+
 	for (i = 0; i < sitdev->info->num_outputs; i++) {
 		bool clamped;
 		s32 phase_ps;
@@ -4418,13 +4802,13 @@ static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
 		if (index == SIT9531X_MAX_INPUTS)
 			return true;
 		if (index == SIT9531X_INTSYNC_PIN_ID)
-			return false;
+			return true;
 
 		return sit9531x_input_pin_is_registrable(sitdev, index);
 	}
 
 	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
-		return false;
+		return true;
 
 	if (index >= sitdev->info->num_outputs)
 		return false;
@@ -4860,8 +5244,34 @@ static int sit9531x_suspend(struct device *dev)
 static int sit9531x_resume(struct device *dev)
 {
 	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+	int rc;
+	u8 i;
 
 	sit9531x_page_cache_drop(sitdev);
+
+	/*
+	 * A part that lost power across suspend comes back with its
+	 * profile's INTSYNC routing and output mutes, not with what the
+	 * caches say, and the poll watches neither.  Read them back here;
+	 * a cache that cannot be read is left for the getter's read-through.
+	 * Nothing races this: the poll is parked and the IRQ disabled.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_intsync_src_detect(sitdev);
+	if (rc)
+		dev_warn(sitdev->dev,
+			 "INTSYNC source not re-detected on resume: %d\n", rc);
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		rc = sit9531x_output_state_refresh(sitdev, i);
+		if (rc) {
+			dev_warn(sitdev->dev,
+				 "Output %u mute state not read on resume: %d\n",
+				 i, rc);
+			sitdev->out[i].state_stale = true;
+		}
+	}
+	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 1ed20354f94d..a5b343033a41 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -211,6 +211,9 @@ struct sit9531x_chan {
  *			I2C client), 0 if no IRQ is wired
  * @intsync_src:	PLL index currently sourcing inter-PLL
  *			synchronization (INTSYNC), or -1 when disabled
+ * @intsync_partial:	@intsync_src holds only part of the source
+ *			configuration, left by an enable or a disable
+ *			that stopped part way
  * @irq_ack_fails:	consecutive failures to acknowledge the
  *			notification latches from the interrupt handler
  * @fvco_band_warned:	bit per PLL whose Fref * DIVN has been reported as
@@ -248,6 +251,7 @@ struct sit9531x_dev {
 
 	/* Inter-PLL synchronization state */
 	s8			intsync_src;
+	bool			intsync_partial;
 
 };
 
@@ -308,6 +312,9 @@ int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
 int sit9531x_clear_notifications(struct sit9531x_dev *sitdev);
 
 /* ---- INTSYNC (inter-PLL synchronization) ---- */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx);
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev);
 
 /* ---- Phase offset (TDC readback) ---- */
 int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index aab5a4f0f946..4c86ee9e7980 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -27,6 +27,20 @@ static bool sit9531x_dpll_is_input_pin(const struct sit9531x_dpll_pin *pin)
 	return pin->dir == DPLL_PIN_DIRECTION_INPUT;
 }
 
+static bool
+sit9531x_dpll_is_intsync_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_PIN_ID;
+}
+
+static bool
+sit9531x_dpll_is_intsync_src_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return !sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_INTSYNC_OUT_PIN_ID;
+}
+
 static bool
 sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
 {
@@ -311,6 +325,59 @@ sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
 		*state = DPLL_PIN_STATE_DISCONNECTED;
 }
 
+/*
+ * Does the device feed a locked PLL from the INTSYNC net?
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static bool sit9531x_dpll_intsync_tracked(struct sit9531x_dev *sitdev)
+{
+	const struct sit9531x_chan *chan;
+	u8 pll;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+		chan = sit9531x_chan_state_get(sitdev, pll);
+		if (chan->routed_ref == SIT9531X_INTSYNC_PIN_ID &&
+		    chan->locked && !chan->ho_freeze)
+			return true;
+	}
+
+	return false;
+}
+
+/*
+ * Does a selection-role pin carry a signal?  A physical input has the
+ * device's LOS monitor; the INTSYNC net has no monitor, and what stands in
+ * for one is the driver's record of a PLL driving it.  A partial source
+ * configuration does not count: the source pin reports it as no
+ * connection, and the net is not confirmed driven.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static bool
+sit9531x_dpll_selection_no_signal(struct sit9531x_dev *sitdev, u8 pin_id)
+{
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pin_id == SIT9531X_INTSYNC_PIN_ID) {
+		/*
+		 * The driver knows the net is driven when it enabled the
+		 * source itself or found the configuration it would have
+		 * written.  A profile can drive the net with a configuration
+		 * of its own; then a PLL the device feeds from the net and
+		 * holds locked is the evidence that the net carries a signal.
+		 */
+		if (sitdev->intsync_src >= 0 && !sitdev->intsync_partial)
+			return false;
+		return !sit9531x_dpll_intsync_tracked(sitdev);
+	}
+
+	return pin_id < sitdev->info->num_inputs &&
+	       sit9531x_ref_state_get(sitdev, pin_id)->los;
+}
+
 /*
  * Is this the reference the PLL is tracking now?  See the S && L && !N
  * predicate in the pin-state contract.  This is also what gates the
@@ -334,11 +401,10 @@ sit9531x_dpll_selection_active(struct sit9531x_dev *sitdev,
 
 	/*
 	 * Fed to the PLL is not locked to: the device names the source it
-	 * routes, and a lane that lost its signal is one the PLL is about
+	 * routes, and a source that lost its signal is one the PLL is about
 	 * to be moved off, whatever the lock bit still says.
 	 */
-	if (pin_id < sitdev->info->num_inputs &&
-	    sit9531x_ref_state_get(sitdev, pin_id)->los)
+	if (sit9531x_dpll_selection_no_signal(sitdev, pin_id))
 		return false;
 
 	return true;
@@ -364,12 +430,17 @@ sit9531x_dpll_selection_operstate_get(struct sit9531x_dev *sitdev,
 		return;
 	}
 
+	/*
+	 * Standby is a qualified source the PLL is not using.  A net nobody
+	 * drives is no more that than a lane in LOS is.
+	 */
+	if (sit9531x_dpll_selection_no_signal(sitdev, pin_id)) {
+		*operstate = DPLL_PIN_OPERSTATE_NO_SIGNAL;
+		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;
@@ -869,8 +940,325 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
 				       enum dpll_pin_direction *direction,
 				       struct netlink_ext_ack *extack);
 
+/*
+ * Does this PLL drive the INTSYNC net with a complete configuration?
+ * What the source pin reports as CONNECTED.
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+static bool
+sit9531x_dpll_intsync_src_connected(const struct sit9531x_dev *sitdev,
+				    const struct sit9531x_dpll *sitdpll)
+{
+	return sitdev->intsync_src == sitdpll->id && !sitdev->intsync_partial;
+}
+
+static int
+sit9531x_dpll_intsync_src_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 *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sit9531x_dpll_intsync_src_connected(sitdev, sitdpll))
+		*state = DPLL_PIN_STATE_CONNECTED;
+	else
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_src_state_on_dpll_set - drive INTSYNC from a PLL
+ *
+ *   CONNECTED    -> this PLL drives the INTSYNC net
+ *   DISCONNECTED -> stop driving INTSYNC if this PLL drives it
+ *
+ * SELECTABLE is rejected: driving the net is an explicit output routing,
+ * not an automatic-selection candidate, matching the regular output pin.
+ */
+static int
+sit9531x_dpll_intsync_src_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;
+	int rc = 0, detect_rc = 0;
+	bool changed = false;
+	bool was_connected, moved;
+	u8 hw_src;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	was_connected = sit9531x_dpll_intsync_src_connected(sitdev, sitdpll);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		/*
+		 * A partial configuration on this PLL is not a connection:
+		 * run the enable again so a retry after a failed one repairs
+		 * the device instead of reporting success over it.
+		 */
+		if (was_connected)
+			break;
+		if (sitdev->intsync_src >= 0 &&
+		    sitdev->intsync_src != sitdpll->id) {
+			NL_SET_ERR_MSG(extack,
+				       "INTSYNC is already sourced by another PLL");
+			rc = -EBUSY;
+			break;
+		}
+		/*
+		 * A PLL that already lists INTSYNC among its references must
+		 * not also drive it: the destination side refuses the mirror
+		 * of this, and without the check here the net could be routed
+		 * back into the PLL feeding it.
+		 */
+		hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+		if (sit9531x_input_prio_present(sitdev, sitdpll->id, hw_src)) {
+			NL_SET_ERR_MSG(extack,
+				       "PLL selects INTSYNC as a reference; it cannot drive it");
+			rc = -EBUSY;
+			break;
+		}
+		rc = sit9531x_intsync_enable(sitdev, sitdpll->id);
+		changed = true;
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (sitdev->intsync_src != sitdpll->id)
+			break;
+		rc = sit9531x_intsync_disable(sitdev, sitdpll->id);
+		changed = true;
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	/*
+	 * Record what was done before confirming it, and only when this PLL
+	 * actually changed: the core forwards a request for the state the
+	 * pin is already in, and a DISCONNECTED on a PLL that never drove the
+	 * net must not erase the owner.  The refresh below leaves the cache
+	 * untouched when a read fails, and a cache that wrongly says nobody
+	 * drives the net would let a second PLL be configured to drive it.
+	 */
+	if (changed && !rc) {
+		sitdev->intsync_src = state == DPLL_PIN_STATE_CONNECTED ?
+				      sitdpll->id : -1;
+		sitdev->intsync_partial = false;
+	}
+
+	/*
+	 * Re-scan hardware after a transition so the cache follows a
+	 * partially failed enable or disable as closely as possible.
+	 */
+	if (changed)
+		detect_rc = sit9531x_intsync_src_detect(sitdev);
+	/*
+	 * A failed enable that the scan still finds on this PLL is partial
+	 * even when every register reads the enable pattern: the small
+	 * update that commits it may be the step that failed.  Mark it, so
+	 * a retried connect runs the enable again.
+	 *
+	 * When the scan itself failed, nothing says how far the enable and
+	 * its rollback got, and the net may be driven.  Record this PLL as
+	 * the partial owner rather than leave the cache saying nobody drives
+	 * the net, which would let a second PLL be configured on top of it.
+	 * A disconnect on this PLL runs the disable and a scan that settle
+	 * the record.
+	 */
+	if (changed && rc && state == DPLL_PIN_STATE_CONNECTED &&
+	    (detect_rc || sitdev->intsync_src == sitdpll->id)) {
+		sitdev->intsync_src = sitdpll->id;
+		sitdev->intsync_partial = true;
+	}
+	/*
+	 * The refresh only re-reads what the device now shows.  Failing
+	 * the request because that read hit a bus error would tell
+	 * userspace the enable did not happen when it did.
+	 */
+	if (detect_rc)
+		dev_warn(sitdev->dev,
+			 "INTSYNC source cache not refreshed: %d\n",
+			 detect_rc);
+
+	/*
+	 * What the pin reports may have moved although the request failed:
+	 * a disable that stopped at its last step leaves nothing driving
+	 * the net, a failed enable leaves a partial owner.  The core
+	 * notifies only a request that succeeded and the poll does not
+	 * watch this pin, so announce it here, as the output setter does.
+	 */
+	moved = sit9531x_dpll_intsync_src_connected(sitdev, sitdpll) !=
+		was_connected;
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		struct dpll_pin *dpll_pin;
+
+		if (rc != -EBUSY && rc != -EINVAL && rc != -EOPNOTSUPP)
+			NL_SET_ERR_MSG(extack,
+				       "Failed to set INTSYNC source state");
+		/*
+		 * The core's lock is held here, as the helper requires.
+		 * Teardown clears the handle before it unregisters the pin;
+		 * see the output state setter.
+		 */
+		dpll_pin = READ_ONCE(dpin->dpll_pin);
+		if (moved && dpll_pin)
+			__dpll_pin_change_ntf(dpll_pin);
+	}
+
+	return rc;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_src_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_src_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_intsync_src_state_on_dpll_set,
+};
+
 /* ---- INTSYNC destination (input) pin ---- */
 
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_get - INTSYNC reference state
+ *
+ * Selection role, so the contract above decides this exactly as it does
+ * for a physical input: the priority table is the eligibility record, and
+ * whether a source PLL happens to be driving the net right now is no more
+ * a state than a momentary LOS is on an external reference.  The one
+ * addition is that the PLL driving INTSYNC is never its own destination.
+ */
+static int
+sit9531x_dpll_intsync_dst_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 *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->intsync_src == sitdpll->id)
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	else
+		sit9531x_dpll_selection_state_get(sitdev, sitdpll,
+						  SIT9531X_INTSYNC_PIN_ID,
+						  state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * sit9531x_dpll_intsync_dst_state_on_dpll_set - lock a PLL to INTSYNC
+ *
+ * Selection role, so this accepts and refuses what a physical input does,
+ * CONNECTED included: the device pins no reference on request whichever
+ * source is asked for.  INTSYNC is an internal net with no physical
+ * receiver, so only the per-PLL priority table is touched; the source pin
+ * controls generation.
+ */
+static int
+sit9531x_dpll_intsync_dst_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 *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	switch (state) {
+	case DPLL_PIN_STATE_DISCONNECTED:
+		rc = sit9531x_input_prio_remove(sitdev, sitdpll->id, hw_src);
+		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:
+		if (sitdev->intsync_src == sitdpll->id) {
+			NL_SET_ERR_MSG(extack,
+				       "PLL cannot lock to the INTSYNC it drives");
+			rc = -EINVAL;
+			break;
+		}
+		rc = sit9531x_input_prio_add(sitdev, sitdpll->id, hw_src);
+		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 != -EINVAL && rc != -EOPNOTSUPP)
+		NL_SET_ERR_MSG(extack, "Failed to set INTSYNC input state");
+
+	return rc;
+}
+
+static int
+sit9531x_dpll_intsync_dst_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 *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_dpll_selection_operstate_get(sitdev, sitdpll,
+					      SIT9531X_INTSYNC_PIN_ID,
+					      state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * Do not add .frequency_get / the generic input state getter here: the
+ * destination pin id is SIT9531X_INTSYNC_PIN_ID, one past the end of the
+ * ref[] array (INTSYNC is an internal net with no ref[] entry).  The ops
+ * below only ever key on chan[] and the priority table, never ref[id].
+ */
+static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_dst_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_intsync_dst_state_on_dpll_set,
+	.operstate_on_dpll_get	=
+		sit9531x_dpll_intsync_dst_operstate_on_dpll_get,
+	.prio_get		= sit9531x_dpll_input_pin_prio_get,
+	.prio_set		= sit9531x_dpll_input_pin_prio_set,
+};
+
 /*
  * XO (crystal oscillator) pin ops
  *
@@ -1197,8 +1585,13 @@ 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)
 {
-	if (!sit9531x_dpll_is_input_pin(pin))
+	if (!sit9531x_dpll_is_input_pin(pin)) {
+		if (sit9531x_dpll_is_intsync_src_pin(pin))
+			return &sit9531x_dpll_intsync_src_pin_ops;
 		return &sit9531x_dpll_output_pin_ops;
+	}
+	if (sit9531x_dpll_is_intsync_pin(pin))
+		return &sit9531x_dpll_intsync_dst_pin_ops;
 	if (sit9531x_dpll_is_xo_pin(pin))
 		return &sit9531x_dpll_xo_pin_ops;
 	/*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 1891679698e2..601dafbb2179 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -312,6 +312,9 @@
 #define SIT9531X_PLL_REG_ACTIVE		0x02
 #define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
 
+/* PLL EXT page INTSYNC configuration registers */
+#define SIT9531X_PLL_EXT_PAGE(_idx)	(SIT9531X_PAGE_PLLA_EXT + (_idx))
+
 #define SIT9531X_PLL_STATUS_OUTER_DIS	BIT(5)
 
 /*
-- 
2.39.2 (Apple Git-143)


      parent reply	other threads:[~2026-10-09 18:32 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-09 18:31 [PATCH net-next v12 00/12] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 01/12] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 02/12] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 03/12] dpll: add basic SiTime SiT9531x support Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 05/12] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 04/12] dpll: sit9531x: read DPLL types and pin properties from system firmware Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 06/12] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 08/12] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 07/12] dpll: sit9531x: add support to get and set priority on input pins Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 09/12] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 10/12] dpll: sit9531x: add support to adjust output phase Ali Rouhi
2026-10-09 18:31 ` [PATCH net-next v12 11/12] dpll: sit9531x: add support to get phase offset on the connected input pin Ali Rouhi
2026-10-09 18:32 ` Ali Rouhi [this message]

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=20261009183151.78497-13-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=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