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 09/13] dpll: sit9531x: add support to get and set frequency on pins
Date: Tue, 10 Nov 2026 20:26:14 +0000	[thread overview]
Message-ID: <20261110202607.55744-10-arouhi@sitime.com> (raw)
In-Reply-To: <20261110202607.55744-1-arouhi@sitime.com>

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

Report an input's frequency from the firmware description and an
output's as the VCO rate over its output divider; set an output's
frequency by writing a new divider. The VCO rate is Fref * DIVN, and a
rate is accepted only when the VCO rate is a whole multiple of it.

A divider write takes effect only inside the programming state, and the
documented settling time after the loop-lock command, about 100 ms, is
waited under the device lock. The phase flush after it is enabled
per PLL, so it realigns every output the PLL drives.

Output pins are registered by the next patch, which adds the state
callback the core requires.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 614 +++++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h |  18 +-
 drivers/dpll/sit9531x/dpll.c | 120 ++++++-
 drivers/dpll/sit9531x/prop.c |  28 +-
 drivers/dpll/sit9531x/regs.h |  21 ++
 5 files changed, 793 insertions(+), 8 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index b4e44f0d9820..c7ab33993c28 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -459,6 +459,101 @@ static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
 	return 0;
 }
 
+#define SIT9531X_LOOP_LOCK_TRIES	3
+
+/* Close the loops and lock the debug block without committing */
+static void sit9531x_prg_abort(struct sit9531x_dev *sitdev)
+{
+	int rc = -EIO;
+	u8 attempt;
+
+	for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+		rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+				       SIT9531X_LOOP_LOCK);
+		if (!rc)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (rc)
+		dev_err(sitdev->dev,
+			"output loops left unlocked after a failed entry: %d\n",
+			rc);
+
+	sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			  SIT9531X_DEBUG_LOCK_VAL);
+}
+
+/*
+ * Unlock the Page 3 debug registers and enter PRG_CMD; output-system writes
+ * take effect only in this state.
+ */
+static int sit9531x_prg_enter(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+			       SIT9531X_DEBUG_UNLOCK_VAL);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_PRG_CMD_STATE);
+	if (rc) {
+		sit9531x_prg_abort(sitdev);
+		return rc;
+	}
+
+	return 0;
+}
+
+/*
+ * Update the NVM shadow and re-lock the loops after sit9531x_prg_enter(). The
+ * sleep is the settling time after loop lock.
+ */
+static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
+{
+	int rc, rc2 = 0, rc3;
+	u8 attempt;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_UPDATE_NVM);
+
+	for (attempt = 0; attempt < SIT9531X_LOOP_LOCK_TRIES; attempt++) {
+		rc2 = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+					SIT9531X_LOOP_LOCK);
+		if (!rc2)
+			break;
+		usleep_range(1000, 2000);
+	}
+	if (rc2)
+		dev_err(sitdev->dev,
+			"output loops left unlocked after programming: %d\n",
+			rc2);
+
+	sitdev->loops_open = !!rc2;
+
+	msleep(100);
+
+	/*
+	 * The debug key register unlocks every debug register while it holds
+	 * the unlock value.
+	 */
+	rc3 = sit9531x_write_close(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+				   SIT9531X_DEBUG_LOCK_VAL);
+
+	if (rc)
+		return rc;
+	if (rc2)
+		return rc2;
+
+	if (rc3)
+		dev_warn(sitdev->dev,
+			 "output debug block left unlocked after programming: %d\n",
+			 rc3);
+
+	return 0;
+}
+
 /*
  * Procedure:
  *   1. Force PLL into holdover (PLL page reg 0x6F bit 4)
@@ -1123,6 +1218,11 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
 					 chan->prio_mask | BIT(input_idx));
 }
 
+/* Per-slot DIVO base register offsets (6 slots per page) */
+static const u8 clkout_odr_divn_base[] = {
+	0x14, 0x24, 0x34, 0x44, 0x54, 0x64
+};
+
 /* XO doubler register */
 #define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
 #define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
@@ -1135,6 +1235,507 @@ int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
 
 #define SIT9531X_DIVO_MAX			GENMASK_ULL(33, 0)
 
+/* Register 0x2D bit 7 is active-low: 0 = doubler enabled. */
+static int sit9531x_is_xo_doubler_enabled(struct sit9531x_dev *sitdev)
+{
+	u8 val;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_XO2_GENERIC, &val);
+	if (rc)
+		return rc;
+
+	return (~val >> SIT9531X_XO_DOUBLER_ENB_BIT) & 1u;
+}
+
+/*
+ * DIVN in fixed point: int_part + fracn/fracd, SIT9531X_DIVN_SCALE steps per
+ * unit.
+ */
+static s64 sit9531x_divn_fixed(u32 int_part, s64 fracn, u64 fracd)
+{
+	s64 whole = (s64)int_part * SIT9531X_DIVN_SCALE;
+	u64 frac;
+
+	if (!fracd)
+		return whole;
+
+	frac = mul_u64_u64_div_u64(abs(fracn), SIT9531X_DIVN_SCALE, fracd);
+
+	return fracn < 0 ? whole - (s64)frac : whole + (s64)frac;
+}
+
+/*
+ * Read a PLL's configured DIVN, fixed point as per sit9531x_divn_fixed().
+ * Fractions are stored LSB first; the numerator is two's complement 32-bit and
+ * the denominator register holds the divisor minus one.
+ */
+static int sit9531x_divn_static(struct sit9531x_dev *sitdev, u8 pll_idx,
+				s64 *divn)
+{
+	u32 int_part, fracn_raw = 0, fracd_raw = 0;
+	u64 fracd;
+	s64 fracn;
+	int rc, i;
+	u8 v;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DIVN_INT, &v);
+	if (rc)
+		return rc;
+	int_part = v;
+
+	for (i = 3; i >= 0; i--) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DIVN_NUM + i, &v);
+		if (rc)
+			return rc;
+		fracn_raw = (fracn_raw << 8) | v;
+	}
+
+	for (i = 3; i >= 0; i--) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DIVN_DEN + i, &v);
+		if (rc)
+			return rc;
+		fracd_raw = (fracd_raw << 8) | v;
+	}
+
+	fracn = (s32)fracn_raw;
+	fracd = (u64)fracd_raw + 1;
+	if ((u64)abs(fracn) >= fracd)
+		return -ENODATA;
+
+	*divn = sit9531x_divn_fixed(int_part, fracn, fracd);
+
+	return 0;
+}
+
+/* PLLB and PLLD run their VCO in the high band, PLLA and PLLC in the low. */
+static bool sit9531x_fvco_in_band(u8 pll_idx, u64 fvco)
+{
+	if (pll_idx == 1 || pll_idx == 3)
+		return fvco >= SIT9531X_FVCO_HIGHBAND_MIN &&
+		       fvco <= SIT9531X_FVCO_HIGHBAND_MAX;
+	return fvco >= SIT9531X_FVCO_LOWBAND_MIN &&
+	       fvco <= SIT9531X_FVCO_LOWBAND_MAX;
+}
+
+/*
+ * Fvco = Fref * DIVN with Fref = xtal_freq << doubler; DIVN is the profile's
+ * steady-state value in free-run and sync modes alike.
+ */
+static int sit9531x_get_fvco(struct sit9531x_dev *sitdev, u8 pll_idx,
+			     u64 *fvco)
+{
+	int doubler, rc;
+	u64 fref;
+	s64 divn;
+
+	rc = sit9531x_divn_static(sitdev, pll_idx, &divn);
+	if (rc)
+		return rc;
+	if (divn <= 0)
+		return -ENODATA;
+
+	doubler = sit9531x_is_xo_doubler_enabled(sitdev);
+	if (doubler < 0)
+		return doubler;
+
+	fref = (u64)sitdev->xtal_freq << doubler;
+
+	/*
+	 * Round to the nearest hertz; the fixed-point DIVN is already floored
+	 * once.
+	 */
+	*fvco = mul_u64_u64_div_u64(2 * fref, (u64)divn, SIT9531X_DIVN_SCALE);
+	*fvco = (*fvco + 1) / 2;
+
+	/* Below the low VCO band the divider is not programmed */
+	if (*fvco < SIT9531X_FVCO_LOWBAND_MIN)
+		return -ENODATA;
+
+	/*
+	 * Outside the PLL's VCO band, Fref * DIVN is usually not the rate the
+	 * PLL runs at, as for a PLL fed from another PLL.
+	 */
+	if (!sit9531x_fvco_in_band(pll_idx, *fvco) &&
+	    !(sitdev->fvco_band_warned & BIT(pll_idx))) {
+		sitdev->fvco_band_warned |= BIT(pll_idx);
+		dev_warn(sitdev->dev,
+			 "PLL%c: Fref * DIVN = %llu Hz is outside its VCO band\n",
+			 'A' + pll_idx, *fvco);
+	}
+
+	return 0;
+}
+
+static int sit9531x_pll_small_update(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	return sit9531x_write_pll_u8(sitdev, pll_idx,
+				     SIT9531X_PLL_REG_SMALL_UPDATE,
+				     SIT9531X_SMALL_UPDATE_CMD);
+}
+
+/*
+ * Realign the PLL's output dividers with the on-demand phase flush (PHFL), or
+ * restart the PLL when its profile has PHFL off (PLL page 0x47 bit 7 clear), as
+ * Dely_program_output.py does. Without either, a rewritten DIVO keeps a fixed
+ * edge offset. Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	u8 ctrl, orig, phfl, armed_mask = 0, parked = 0, i;
+	int rc, ret;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_CONFIG47,
+				  &phfl);
+	if (rc)
+		return rc;
+	if (!(phfl & SIT9531X_PLL_CONFIG47_PHFL_EN))
+		return sit9531x_write_pll_u8(sitdev, pll_idx,
+					     SIT9531X_PLL_REG_DIRECTIVES,
+					     SIT9531X_PLL_DIRECTIVE_RESTART);
+
+	/*
+	 * The phase trigger is chip-global and fires every PLL with on-demand
+	 * flush enabled; record which ones the profile arms.
+	 */
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_read_pll_u8(sitdev, i, SIT9531X_PLL_REG_PHFL_CTRL,
+					  &phfl);
+		if (rc)
+			return rc;
+		if (phfl & SIT9531X_PLL_PHFL_ON_DEMAND_EN)
+			armed_mask |= BIT(i);
+	}
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (i == pll_idx || !(armed_mask & BIT(i)))
+			continue;
+		parked |= BIT(i);
+		rc = sit9531x_update_pll_u8(sitdev, i,
+					    SIT9531X_PLL_REG_PHFL_CTRL,
+					    SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+		if (rc)
+			goto unpark;
+		rc = sit9531x_pll_small_update(sitdev, i);
+		if (rc)
+			goto unpark;
+	}
+
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_PHFL_CTRL,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+				    SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+	if (rc)
+		goto disarm;
+
+	/*
+	 * Write the update register whole; a read-modify-write skips the write
+	 * while the bit still reads set from the previous command.
+	 */
+	rc = sit9531x_pll_small_update(sitdev, pll_idx);
+	if (rc)
+		goto disarm;
+
+	/*
+	 * Select the in-register phase trigger; the register also holds the OEb
+	 * bits.
+	 */
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
+	if (rc)
+		goto disarm;
+
+	orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
+	ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
+	if (rc)
+		goto restore;
+
+	/*
+	 * No delay between set and clear; one I2C transfer is longer than the
+	 * minimum pulse width.
+	 */
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+			       ctrl | SIT9531X_DIVO_PHASE_TRIG);
+
+restore:
+	ret = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, orig);
+	if (ret && !rc)
+		rc = ret;
+
+disarm:
+	/*
+	 * Restore the profile's flush enable; an armed flush re-flushes every
+	 * output divider of this PLL on the next trigger.
+	 */
+	if (!(armed_mask & BIT(pll_idx))) {
+		ret = sit9531x_update_pll_u8(sitdev, pll_idx,
+					     SIT9531X_PLL_REG_PHFL_CTRL,
+					     SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
+		if (!ret)
+			ret = sit9531x_pll_small_update(sitdev, pll_idx);
+		if (ret && !rc)
+			rc = ret;
+	}
+
+unpark:
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (!(parked & BIT(i)))
+			continue;
+		ret = sit9531x_update_pll_u8(sitdev, i,
+					     SIT9531X_PLL_REG_PHFL_CTRL,
+					     SIT9531X_PLL_PHFL_ON_DEMAND_EN,
+					     SIT9531X_PLL_PHFL_ON_DEMAND_EN);
+		if (!ret)
+			ret = sit9531x_pll_small_update(sitdev, i);
+		if (ret && !rc)
+			rc = ret;
+	}
+
+	return rc;
+}
+
+/*
+ * Compute an output's divider and VCO rate; call before entering the
+ * programming state.
+ */
+static int sit9531x_output_divo_calc(struct sit9531x_dev *sitdev, u8 out_idx,
+				     u8 pll_idx, u64 frequency, u64 *fvco_out,
+				     u64 *divo_out)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u64 fvco, divo, nearest, rem;
+	int rc;
+
+	if (out_idx >= info->num_outputs || pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+
+	/* An unrouted output has a placeholder in pll_idx. */
+	if (!sitdev->out[out_idx].routed)
+		return -ENODEV;
+
+	if (!frequency)
+		return -EINVAL;
+
+	if (frequency > U32_MAX)
+		return -EINVAL;
+
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+
+	if (!sit9531x_fvco_in_band(pll_idx, fvco)) {
+		dev_dbg(sitdev->dev,
+			"out%u: PLL%c Fvco %llu Hz is outside its VCO band, rate not settable\n",
+			out_idx, 'A' + pll_idx, fvco);
+		return -EINVAL;
+	}
+
+	divo = div64_u64(fvco + frequency / 2, frequency);
+	if (!divo)
+		return -EINVAL;
+
+	if (divo > SIT9531X_DIVO_MAX)
+		return -EINVAL;
+
+	/*
+	 * Refuse a rate that is not exactly Fvco/N; the quotient truncates to
+	 * the request whenever the remainder is below N.
+	 */
+	nearest = div64_u64_rem(fvco, divo, &rem);
+	if (nearest != frequency || rem) {
+		dev_dbg(sitdev->dev,
+			"out%u: %llu Hz is not Fvco/N (Fvco=%llu, nearest %llu Hz)\n",
+			out_idx, frequency, fvco, nearest);
+		return -EINVAL;
+	}
+
+	dev_dbg(sitdev->dev, "out%u: Fvco=%llu freq=%llu DIVO=%llu\n",
+		out_idx, fvco, frequency, divo);
+
+	*fvco_out = fvco;
+	*divo_out = divo;
+
+	return 0;
+}
+
+/*
+ * Caller must be in the programming state; @back reports whether the divider
+ * holds its old value.
+ */
+static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
+				      u64 divo, bool *back)
+{
+	u8 slot, page, base_reg, divo_bytes[5], old_bytes[5], msb_old;
+	const struct sit9531x_chip_info *info = sitdev->info;
+	int rc, j, rb_rc;
+	u8 written = 0;
+
+	*back = false;
+
+	slot = info->clkout_map[out_idx];
+
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	divo_bytes[0] = (divo >>  0) & 0xFF;
+	divo_bytes[1] = (divo >>  8) & 0xFF;
+	divo_bytes[2] = (divo >> 16) & 0xFF;
+	divo_bytes[3] = (divo >> 24) & 0xFF;
+	divo_bytes[4] = (divo >> 32) & 0x03;  /* only bits [1:0] */
+
+	for (j = 0; j < 5; j++) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(page, base_reg - j),
+				      &old_bytes[j]);
+		if (rc)
+			return rc;
+	}
+
+	msb_old = old_bytes[4];
+	divo_bytes[4] |= msb_old & 0xFC;
+
+	for (j = 0; j < 5; j++) {
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(page, base_reg - j),
+				       divo_bytes[j]);
+		if (rc)
+			goto rollback;
+		written++;
+	}
+
+	return 0;
+
+rollback:
+	/* The byte whose write failed may have reached the part */
+	*back = true;
+	for (j = 0; j <= written && j < 5; j++) {
+		rb_rc = sit9531x_write_u8(sitdev,
+					  SIT9531X_REG(page, base_reg - j),
+					  old_bytes[j]);
+		if (rb_rc) {
+			*back = false;
+			dev_err(sitdev->dev,
+				"out%u: DIVO rollback failed (%d), the divider is part old and part new\n",
+				out_idx, rb_rc);
+			if (!rc)
+				rc = rb_rc;
+		}
+	}
+
+	return rc;
+}
+
+static int sit9531x_output_divo_read(struct sit9531x_dev *sitdev, u8 out_idx,
+				     u64 *divo)
+{
+	u8 slot, page, base_reg, v;
+	int rc, j;
+
+	slot = sitdev->info->clkout_map[out_idx];
+	if (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX)
+		page = SIT9531X_PAGE_OUTSYS1;
+	else
+		page = SIT9531X_PAGE_OUTSYS0;
+	base_reg = clkout_odr_divn_base[slot % 6];
+
+	*divo = 0;
+	for (j = 4; j >= 0; j--) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(page, base_reg - j), &v);
+		if (rc)
+			return rc;
+		if (j == 4)
+			v &= 0x03;
+		*divo = (*divo << 8) | v;
+	}
+
+	return *divo ? 0 : -ENODATA;
+}
+
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency)
+{
+	u64 fvco, divo;
+	int rc, ret;
+	bool back;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_output_divo_calc(sitdev, out_idx, pll_idx, frequency,
+				       &fvco, &divo);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_divo_write(sitdev, out_idx, divo, &back);
+	/* NVM update and loop lock */
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+	if (rc)
+		return rc;
+
+	/* Flush the output phase so the new DIVO starts aligned */
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (rc) {
+		dev_warn(sitdev->dev,
+			 "out%u: rate changed but the divider phase was not realigned (%d)\n",
+			 out_idx, rc);
+		rc = 0;
+	}
+
+	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
+
+	return 0;
+}
+
+/*
+ * Read the output rate back as Fvco / DIVO and refresh the cached rate. Caller
+ * must hold sitdev->multiop_lock.
+ */
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u64 *frequency)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u64 fvco, divo;
+	u8 pll_idx;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	pll_idx = sitdev->out[out_idx].pll_idx;
+	if (pll_idx >= SIT9531X_NUM_PLLS || !sitdev->out[out_idx].routed) {
+		sitdev->out[out_idx].freq = 0;
+		return -ENODATA;
+	}
+
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (!rc)
+		rc = sit9531x_output_divo_read(sitdev, out_idx, &divo);
+	if (rc) {
+		if (rc == -ENODATA)
+			sitdev->out[out_idx].freq = 0;
+		return rc;
+	}
+
+	*frequency = div64_u64(fvco, divo);
+	sitdev->out[out_idx].freq = *frequency;
+
+	return 0;
+}
+
 /*
  * Phase adjust: per-output 34-bit coarse delay in VCO periods plus a 3-bit
  * fine delay in 30 ps steps, in PROG6..PROG2:
@@ -1841,6 +2442,10 @@ sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
 	bool answered = false;
 	int i, rc;
 
+	/* Retry pending writes first so the reads below see their result */
+	if (sitdev->loops_open)
+		sit9531x_prg_commit(sitdev);
+
 	sit9531x_input_off_retry(sitdev);
 	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
 		sit9531x_table_back_retry(sitdev, i);
@@ -2163,6 +2768,15 @@ static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
 				 "PLL%c left in forced holdover\n", 'A' + i);
 	}
 	mutex_unlock(&sitdev->multiop_lock);
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->loops_open) {
+		sit9531x_prg_commit(sitdev);
+		if (sitdev->loops_open)
+			dev_warn(sitdev->dev,
+				 "output loops left unlocked at removal\n");
+	}
+	mutex_unlock(&sitdev->multiop_lock);
 }
 
 static struct sit9531x_dpll_pin *
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index fa779acf7de5..6b50b5b64b75 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -212,20 +212,25 @@ 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
+ * @fvco_band_warned: bit per PLL warned for Fref * DIVN outside its VCO
+ *   band; written under @multiop_lock
  * @loops_open: output loops not yet re-locked
+ * @flush_ctrl_pending: @flush_ctrl still to be written back
+ * @flush_ctrl: trigger register value to restore
+ * @flush_arm_pending: bit per PLL whose flush enable is still to be restored
+ * @flush_armed: of those, the PLLs the profile arms
  */
 struct sit9531x_dev {
 	struct device			*dev;
 	struct i2c_client		*client;
 	struct regmap			*regmap;
 	const struct sit9531x_chip_info	*info;
-	/* Serializes multi-step register sequences */
 	struct mutex			multiop_lock;
-	/* Serializes a page select and the transfer it is for */
 	struct mutex			page_lock;
 	int				page;
 
 	u8			irq_ack_fails;
+	u8			fvco_band_warned;
 	struct sit9531x_ref	ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
 	struct sit9531x_out	out[SIT9531X_MAX_OUTPUTS];
 	struct sit9531x_chan	chan[SIT9531X_NUM_PLLS];
@@ -245,6 +250,10 @@ struct sit9531x_dev {
 	s8			intsync_src;
 
 	bool			loops_open;
+	bool			flush_ctrl_pending;
+	u8			flush_ctrl;
+	u8			flush_arm_pending;
+	u8			flush_armed;
 };
 
 int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
@@ -272,6 +281,11 @@ int sit9531x_input_prio_remove(struct sit9531x_dev *sitdev, u8 pll_idx,
 int sit9531x_input_prio_add(struct sit9531x_dev *sitdev, u8 pll_idx,
 			    u8 input_idx);
 
+int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u8 pll_idx, u64 frequency);
+int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
+			     u64 *frequency);
+
 static inline u8 sit9531x_pll_page(u8 pll_idx)
 {
 	return SIT9531X_PAGE_PLLA + pll_idx;
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index f03145f4d1ee..a450a81d02f4 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -302,6 +302,24 @@ sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
 	return 0;
 }
 
+/* Report the first supported-frequencies-hz entry */
+static int
+sit9531x_dpll_input_pin_frequency_get(const struct dpll_pin *pin,
+				      void *pin_priv,
+				      const struct dpll_device *dpll,
+				      void *dpll_priv, u64 *frequency,
+				      struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	const struct sit9531x_ref *ref;
+
+	ref = sit9531x_ref_state_get(sitdpll->dev, dpin->id);
+	*frequency = ref->freq;
+
+	return 0;
+}
+
 static int
 sit9531x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *pin,
 					  void *pin_priv,
@@ -470,6 +488,20 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 }
 
 static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_input_pin_frequency_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,
+	.prio_get		= sit9531x_dpll_input_pin_prio_get,
+	.prio_set		= sit9531x_dpll_input_pin_prio_set,
+};
+
+/*
+ * For an input with no supported-frequencies-hz: the core fails a whole pin
+ * dump if any callback returns an error.
+ */
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_norate_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,
@@ -483,6 +515,13 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
  * 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,
+	.frequency_get		= sit9531x_dpll_input_pin_frequency_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,
+};
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_norate_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,
@@ -518,6 +557,7 @@ sit9531x_dpll_xo_pin_state_on_dpll_get(const struct dpll_pin *pin,
 
 static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
 	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_input_pin_frequency_get,
 	.state_on_dpll_get	= sit9531x_dpll_xo_pin_state_on_dpll_get,
 };
 
@@ -533,23 +573,97 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
 	return 0;
 }
 
+/*
+ * Report Fvco / DIVO, or the cached rate when the divider chain does not
+ * resolve.
+ */
+static int
+sit9531x_dpll_output_pin_frequency_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 *frequency,
+				       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;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_output_freq_get(sitdev, dpin->id, frequency);
+	if (rc == -ENODATA)
+		*frequency = sit9531x_out_state_get(sitdev, dpin->id)->freq;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return rc == -ENODATA ? 0 : rc;
+}
+
+static int
+sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv, u64 frequency,
+				       struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	bool moved = false;
+	u64 was, hz;
+	u8 actual_pll;
+	int rc;
+
+	actual_pll = sitdev->out[dpin->id].pll_idx;
+
+	mutex_lock(&sitdev->multiop_lock);
+	was = sitdev->out[dpin->id].freq;
+	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
+				      frequency);
+	if (rc) {
+		if (!sit9531x_output_freq_get(sitdev, dpin->id, &hz))
+			moved |= hz != was;
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		struct dpll_pin *dpll_pin;
+
+		NL_SET_ERR_MSG(extack, "Output frequency set failed");
+		/* The core notifies only a request that succeeded */
+		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_output_pin_ops = {
 	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+	.frequency_get		= sit9531x_dpll_output_pin_frequency_get,
+	.frequency_set		= sit9531x_dpll_output_pin_frequency_set,
 };
 
 const struct dpll_pin_ops *
 sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
 {
 	const struct sit9531x_dev *sitdev = pin->dpll->dev;
+	bool manual;
 
 	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;
+	manual = sit9531x_chan_state_get(sitdev, pin->dpll->id)->manual_input;
+	/*
+	 * Set before any pin of the input is published and not written again.
+	 */
+	if (!sit9531x_ref_state_get(sitdev, pin->id)->freq)
+		return manual ? &sit9531x_dpll_input_pin_norate_manual_ops :
+				&sit9531x_dpll_input_pin_norate_ops;
+	return manual ? &sit9531x_dpll_input_pin_manual_ops :
+			&sit9531x_dpll_input_pin_ops;
 }
 
 /* Caller must hold sitdev->multiop_lock. */
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index 69877fc94028..f726fbe037f3 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -233,7 +233,10 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 		goto err_alloc_ranges;
 	}
 
-	/* The first listed rate seeds the input's rate */
+	/*
+	 * The driver does not read an input's rate; the first listed rate seeds
+	 * it.
+	 */
 	if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT && !xo)
 		curr_freq = freqs[0];
 
@@ -241,6 +244,21 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 	if (sync_in || sync_out)
 		return props;
 
+	/*
+	 * Read the divider into the cached output rate used by phase adjust;
+	 * -ENODATA is an unprogrammed divider or unknown VCO rate.
+	 */
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    index < sitdev->info->num_outputs) {
+		u64 hw_freq;
+
+		mutex_lock(&sitdev->multiop_lock);
+		rc = sit9531x_output_freq_get(sitdev, index, &hw_freq);
+		mutex_unlock(&sitdev->multiop_lock);
+		if (rc && rc != -ENODATA)
+			goto err_alloc_ranges;
+	}
+
 	/*
 	 * An output with no listed rates gets one wide range, as the setter
 	 * checks Fvco/N; the XO gets its one rate.
@@ -251,8 +269,12 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 		goto err_alloc_ranges;
 	}
 
-	/* Set after the last allocation that can fail */
-	if (curr_freq && dir == DPLL_PIN_DIRECTION_INPUT)
+	/*
+	 * Set after the last allocation that can fail, and only once, as pins
+	 * of other DPLLs on this input may already be published.
+	 */
+	if (curr_freq && dir == DPLL_PIN_DIRECTION_INPUT &&
+	    !sitdev->ref[index].freq)
 		sitdev->ref[index].freq = curr_freq;
 
 	j = 0;
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 39d836f6b563..3f8ceb7e9798 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -156,6 +156,7 @@
 /* Debug register (same offset, per-page) */
 #define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
 #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
+#define SIT9531X_DEBUG_LOCK_VAL			0x00
 
 /*
  * Register-triggered phase flush: DIVO_PHASE_SEL_REG selects the register
@@ -169,6 +170,18 @@
 /* PLL page register offsets, pages 0x0A-0x0D */
 #define SIT9531X_PLL_REG_SMALL_UPDATE	0x0F
 
+/* On-demand phase-flush enable (PLL page reg 0x3D bit 7) */
+#define SIT9531X_PLL_REG_PHFL_CTRL	0x3D
+#define SIT9531X_PLL_PHFL_ON_DEMAND_EN	BIT(7)
+
+/* Whether the PLL has the phase-flush feature (PLL page reg 0x47 bit 7) */
+#define SIT9531X_PLL_REG_CONFIG47	0x47
+#define SIT9531X_PLL_CONFIG47_PHFL_EN	BIT(7)
+
+/* Directives_GENERIC_PLL: the restart bit restarts the PLL, self-clearing */
+#define SIT9531X_PLL_REG_DIRECTIVES	0x05
+#define SIT9531X_PLL_DIRECTIVE_RESTART	BIT(0)
+
 /*
  * PLL page regs 0x10-0x15 hold the loop-filter coefficients (3 normal, 3
  * fast-lock); they are GUI generated and must not be changed at runtime.
@@ -182,6 +195,14 @@
 #define SIT9531X_PLL_REG_OUT_MAP_LO		0x28
 #define SIT9531X_PLL_REG_STATUS		0x31
 
+/* DIVN registers (free-run divider readback) */
+#define SIT9531X_PLL_REG_DIVN_INT		0x30
+#define SIT9531X_PLL_REG_DIVN_NUM		0x32  /* 4 bytes (0x32-0x35) */
+#define SIT9531X_PLL_REG_DIVN_DEN		0x38  /* 4 bytes (0x38-0x3B) */
+
+/* DIVN is carried as fixed point, in steps of 1e-12 of a whole divider */
+#define SIT9531X_DIVN_SCALE		1000000000000ULL
+
 #define SIT9531X_PLL_REG_ACTIVE		0x02
 #define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
 
-- 
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 ` [PATCH net-next v15 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
2026-11-10 20:26 ` Ali Rouhi [this message]
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-10-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®