mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver
@ 2026-11-10 20:26 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
                   ` (12 more replies)
  0 siblings, 13 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

This series adds a DPLL subsystem driver for the SiTime SiT95316 and
SiT95317 I2C clock generators. Each device integrates four PLLs with
automatic reference selection and on-chip TDC phase-offset measurement,
and is used for synchronization in telecom, networking, and data-center
timing.

Patch 1 changes the DPLL core: phase_offset_get and phase_adjust_get
may return -ENODATA, and the core then leaves the attribute out of the
pin message instead of failing the dump. Patches 11 and 12 use it. The
rest is the device-tree binding, the driver under
drivers/dpll/sit9531x/, and the MAINTAINERS entry.

v1: https://lore.kernel.org/netdev/20260511211143.19792-1-arouhi@sitime.com/
v2: https://lore.kernel.org/netdev/20260520191943.73938-1-arouhi@sitime.com/
v3: https://lore.kernel.org/netdev/20260731180951.65725-1-arouhi@sitime.com/
v4: https://lore.kernel.org/netdev/20260806232439.27551-1-arouhi@sitime.com/
v5: https://lore.kernel.org/netdev/20260810230439.22866-1-arouhi@sitime.com/
v6: https://lore.kernel.org/netdev/20260812175337.18155-1-arouhi@sitime.com/
v7: https://lore.kernel.org/netdev/20260815221919.64226-1-arouhi@sitime.com/
v8: https://lore.kernel.org/netdev/20260902214030.20955-1-arouhi@sitime.com/
v9: https://lore.kernel.org/netdev/20260915000015.80480-1-arouhi@sitime.com/
v10: https://lore.kernel.org/netdev/20260921201108.42676-1-arouhi@sitime.com/
v11: https://lore.kernel.org/netdev/20260930233714.87679-1-arouhi@sitime.com/
v12: https://lore.kernel.org/netdev/20261009183151.78497-1-arouhi@sitime.com/
v13: https://lore.kernel.org/netdev/20261013224131.27149-1-arouhi@sitime.com/
v14: https://lore.kernel.org/netdev/20261016005823.13690-1-arouhi@sitime.com/

Most of the changes in v15 answer the netdev AI reviewer's findings on
v14 and further runs of the same reviewer on drafts of this revision.
Each fix is folded into the patch that introduces the code. The design
section below states once the decisions those findings kept returning
to. One v14 finding doesn't hold: the DIVN fraction read is not
byte-reversed, because the part stores the fraction least significant
byte first, and the kernel-doc of sit9531x_divn_static() now says so.

Rob Herring's Reviewed-by on the binding is carried on patch 3. One
sentence of its description changed since v14: it now lists the die
outputs the SiT95317 bonds out and the names its datasheet gives them.
Ivan Vecera's Reviewed-by on v14 patches 4, 6, 7, 9 and 11 is not
carried, since each of those patches changed.

Oleg and I used LLM coding assistants to write parts of the driver code
and of the commit messages, and reviewed and revised what they
produced. v14 marked each such patch with Assisted-by: LLM; v15 drops
the tag following commit 1e035297ffda ("docs: remove Assisted-by
references").

  1     dpll core: omit phase attributes a driver has no value for
  2     bindings: vendor prefix
  3     bindings: the device schema
  4     basic support: paged regmap, variant detection, probe
  5     DPLL types and pin properties from system firmware
  6     register the DPLL devices and pins, and keep their state
  7     input pin state and operational state on a DPLL
  8     input pin priority
  9     pin frequency, both directions
  10    output pin state (mute)
  11    output phase adjust
  12    phase offset through the TDC
  13    the inter-PLL sync line as a pair of pins

The two bindings patches come before the driver, so the driver never
matches on a compatible string before the schema that describes it is
in the tree.

Every patch builds with only the earlier patches applied: no patch
calls something a later patch introduces. This was re-checked for this
posting patch by patch with W=1 and with sparse, and kernel-doc -Wall
is clean on every driver patch. checkpatch --strict at 80 columns is
clean except for the "does MAINTAINERS need updating?" hint on patches
5 and 6, which add files under drivers/dpll/sit9531x/; patch 4 already
covers that directory with an F: entry.

Design decisions and hardware assumptions
=========================================

Configuration ownership. The part runs a configuration it loaded before
the driver binds, from efuse or from a blob the board applies. The
driver reads that configuration and never resets the part, rewrites
loop-filter coefficients or touches a register the configurator sets.
What it changes at runtime is what the DPLL API asks it to change:
input selection and priorities, output enables, rates and delays, the
inter-PLL sync source. Where the loaded configuration puts a PLL
outside what the API can express, the driver reports what the part
does and withholds the control rather than reconfiguring the part:
a PLL in manual input select is reported in MANUAL mode with no
selection or priority setters; a half-armed fast lock registers without
the attribute. The driver does not write the part at probe or resume,
with one exception below.

Automatic selection. A PLL whose profile arbitrates by priority is
reported AUTOMATIC: the part picks among the listed inputs by the
priorities it is given, and the driver keeps the table and the active
selection register in step with the priorities it was asked for. When
a PLL that lost every input sits in holdover and an input returns, the
driver doesn't wait for the part: it re-selects the PLL when an input
edge is sampled, and once on the first sample after probe or resume,
where no edge could have been seen. Whether a PLL returns to a
higher-priority input that recovers is the profile's REVERT_EN; the
DPLL API has no attribute for revertive selection and no driver
exposes its setting.

Qualification. The driver selects on loss of signal and reports ACTIVE
from it. The frequency-drift monitors are reported as a qualification
failure on an input the PLL is not on; an input the part keeps tracking
while its drift flag is set is reported as the part reports it.

Register access. The part shows one 256-byte page at a time, selected
through the register at offset 0xFF of every page and latched on the
STOP of that write. The regmap is that flat window; the driver selects
the page itself under its own lock, remembers the page it selected,
forgets it on any failed transfer and on resume, and re-selects on the
next access. No other regmap user, debugfs included, can move the
page. The I2C_SLAVE_FORCE path of i2c-dev is the
documented debugging override and is not defended against.

Clocks and supplies. The XO is taken through
devm_clk_get_optional_enabled() for its rate and to hold a reference,
so clk_disable_unused() cannot gate an XO the loaded configuration runs
from; the enable is not what starts the XO, and the reference goes back
to the provider on unbind or a failed probe. A clock or a rail the part
must keep without a driver bound is the provider's property
(CLK_IS_CRITICAL, regulator-always-on), as for any part that is
configured before Linux runs.

VCO rate. The VCO rate is derived as Fref * DIVN, the XO rate (with the
doubler) times the PLL page's static divider, which is how the register
map defines the inner loop; the binding describes the XO and no other
reference. The estimate is bounded: a product outside the PLL's VCO
band is reported once and refuses every frequency request, and inside
the band a rate is programmed only when the estimated Fvco is a whole
multiple of it.

Output pads. The driver reads the pad-enable bits only to tell a CMOS
output from a differential one, and mutes a CMOS output by forcing both
pads off. It reports an output muted when both pads are forced off, the
only state its own mute produces; a profile that forces one pad is
reported as driven. The debug key gates the Force registers only; the
directive registers that commit a programming sequence are written
without it, as the configurator's scripts do.

Phase offset. The measurement compares the selected input against a
physical output the PLL drives, as the documented procedure does, so a
PLL with no routed, enabled output reports no phase offset (the
attribute is omitted). A sample taken while the input disappears is
rejected when the monitor reports the loss afterwards.

Notifications. The driver sends again a change notification it could
not build for a change it made itself (a failed request that moved
hardware, a read-through that moved a value, the sync-line source). It
does not retry the notifications the core sends after a successful
request: dpll_pin_prio_set() and dpll_pin_state_set() discard that
result for every driver, and a retry in the driver would notify the
change twice.

Recovery after a failed write. A sequence that fails part-way leaves
the part in a state the driver records as pending: a priority table
not latched, a forced holdover to release, a delay to re-time, an
output realignment, a debug window to close, a sync-line page half
written. The poll retries each until the write succeeds; unbind retries
once more and warns about what is left; resume drops the pending delay
and realignment, since the registers it re-reads describe the part
after a suspend that may have reloaded it, and keeps the sync-line
ownership records until a retry resolves them.

Changes in v15:

  - Rebased onto net-next after the v7.4 merge window.

  - New patch 1 in the DPLL core, described above.

  - Patch 3: the description names the die outputs the SiT95317 bonds
    out.

  - Patch 6: a PLL in manual input select is reported MANUAL. A PLL
    whose status read failed is marked stale and reports UNLOCKED
    until a read succeeds.

  - Patch 7: the poll re-selects a PLL that fell into holdover by
    itself when an input returns, and once after probe or resume.

  - Patches 7 to 13: a sequence that fails part-way is recorded as
    pending and retried, as described under Recovery after a failed
    write.

  - Patch 11: phase adjust on an output no PLL drives fails with
    -ENODEV; it was computed against PLL A's VCO rate. A profile delay
    the s32 attribute cannot carry leaves the attribute out.

  - Patch 12: a pin with no measurement returns -ENODATA instead of
    zero, and a sample taken while the lane lost its signal is
    rejected.

  - Patch 13: resume re-reads the owner of the sync line, the output
    mutes and the output delays from the part.

  - Decisions that held across several patches moved out of the commit
    messages into the design section above, and each message now says
    only what its patch adds.

  - Comments rewritten in the style of the zl3073x driver: a one-line
    label, a register fact, a datasheet or procedure citation, a
    locking rule, or the reason an order of operations matters.

Ali Rouhi (2):
  dt-bindings: vendor-prefixes: add SiTime Corporation
  dt-bindings: dpll: add SiTime SiT95316 clock generator

Oleg Zadorozhnyi (11):
  dpll: omit pin phase attributes the driver has no value for
  dpll: add basic SiTime SiT9531x support
  dpll: sit9531x: read DPLL types and pin properties from system
    firmware
  dpll: sit9531x: register DPLL devices and pins
  dpll: sit9531x: implement input pin state on a DPLL
  dpll: sit9531x: add support to get and set priority on input pins
  dpll: sit9531x: add support to get and set frequency on pins
  dpll: sit9531x: implement output pin state on a DPLL
  dpll: sit9531x: add support to adjust output phase
  dpll: sit9531x: add support to get phase offset on the connected input
    pin
  dpll: sit9531x: model the inter-PLL sync line as a pair of pins

 .../bindings/dpll/sitime,sit95316.yaml        |  214 +
 .../devicetree/bindings/vendor-prefixes.yaml  |    2 +
 MAINTAINERS                                   |    7 +
 drivers/dpll/Kconfig                          |    2 +
 drivers/dpll/Makefile                         |    1 +
 drivers/dpll/dpll_netlink.c                   |   14 +-
 drivers/dpll/sit9531x/Kconfig                 |   17 +
 drivers/dpll/sit9531x/Makefile                |    4 +
 drivers/dpll/sit9531x/core.c                  | 5148 +++++++++++++++++
 drivers/dpll/sit9531x/core.h                  |  433 ++
 drivers/dpll/sit9531x/dpll.c                  | 1496 +++++
 drivers/dpll/sit9531x/dpll.h                  |   63 +
 drivers/dpll/sit9531x/prop.c                  |  365 ++
 drivers/dpll/sit9531x/prop.h                  |   37 +
 drivers/dpll/sit9531x/regs.h                  |  351 ++
 15 files changed, 8150 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
 create mode 100644 drivers/dpll/sit9531x/Kconfig
 create mode 100644 drivers/dpll/sit9531x/Makefile
 create mode 100644 drivers/dpll/sit9531x/core.c
 create mode 100644 drivers/dpll/sit9531x/core.h
 create mode 100644 drivers/dpll/sit9531x/dpll.c
 create mode 100644 drivers/dpll/sit9531x/dpll.h
 create mode 100644 drivers/dpll/sit9531x/prop.c
 create mode 100644 drivers/dpll/sit9531x/prop.h
 create mode 100644 drivers/dpll/sit9531x/regs.h


base-commit: fa5196392691051963726787944db872420dfa75
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 01/13] dpll: omit pin phase attributes the driver has no value for
  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 ` 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
                   ` (11 subsequent siblings)
  12 siblings, 1 reply; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Let phase_offset_get and phase_adjust_get return -ENODATA for a pin the
driver has no value for, and leave DPLL_A_PIN_PHASE_OFFSET and
DPLL_A_PIN_PHASE_ADJUST out of the message in that case, as
dpll_msg_add_ffo() already does for the fractional frequency offset.
Today any error from either callback fails the whole pin dump.

A driver that measures the phase offset only on the input its PLL
tracks, or whose part holds a delay larger than the s32 the adjust
attribute carries, has no value to report for the pin, and a zero
cannot be told from a measurement.  A phase adjust request on such a
pin is passed to the driver without the comparison against the old
value, so the delay can still be set or cleared.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/dpll_netlink.c | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
index e5380d95f238..acf71a93f230 100644
--- a/drivers/dpll/dpll_netlink.c
+++ b/drivers/dpll/dpll_netlink.c
@@ -388,8 +388,11 @@ dpll_msg_add_pin_phase_adjust(struct sk_buff *msg, struct dpll_pin *pin,
 	ret = ops->phase_adjust_get(pin, dpll_pin_on_dpll_priv(dpll, pin),
 				    dpll, dpll_priv(dpll),
 				    &phase_adjust, extack);
-	if (ret)
+	if (ret) {
+		if (ret == -ENODATA)
+			return 0;
 		return ret;
+	}
 	if (nla_put_s32(msg, DPLL_A_PIN_PHASE_ADJUST, phase_adjust))
 		return -EMSGSIZE;
 
@@ -411,8 +414,11 @@ dpll_msg_add_phase_offset(struct sk_buff *msg, struct dpll_pin *pin,
 	ret = ops->phase_offset_get(pin, dpll_pin_on_dpll_priv(dpll, pin),
 				    dpll, dpll_priv(dpll), &phase_offset,
 				    extack);
-	if (ret)
+	if (ret) {
+		if (ret == -ENODATA)
+			return 0;
 		return ret;
+	}
 	if (nla_put_64bit(msg, DPLL_A_PIN_PHASE_OFFSET, sizeof(phase_offset),
 			  &phase_offset, DPLL_A_PIN_PAD))
 		return -EMSGSIZE;
@@ -1452,11 +1458,11 @@ dpll_pin_phase_adj_set(struct dpll_pin *pin, struct nlattr *phase_adj_attr,
 	ret = ops->phase_adjust_get(pin, dpll_pin_on_dpll_priv(dpll, pin),
 				    dpll, dpll_priv(dpll), &old_phase_adj,
 				    extack);
-	if (ret) {
+	if (ret && ret != -ENODATA) {
 		NL_SET_ERR_MSG(extack, "unable to get old phase adjust value");
 		return ret;
 	}
-	if (phase_adj == old_phase_adj)
+	if (!ret && phase_adj == old_phase_adj)
 		return 0;
 
 	ret = ops->phase_adjust_set(pin, dpll_pin_on_dpll_priv(dpll, pin),
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation
  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-10 20:26 ` Ali Rouhi
  2026-11-10 20:26 ` [PATCH net-next v15 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

Add vendor prefix for SiTime Corporation, manufacturer of
programmable clock generators and MEMS oscillators.

Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Acked-by: Conor Dooley <conor.dooley@microchip.com>
---
 Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 5471a06f93f1..b0f26ab800be 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1581,6 +1581,8 @@ patternProperties:
     description: SiRF Technology, Inc.
   "^sis,.*":
     description: Silicon Integrated Systems Corp.
+  "^sitime,.*":
+    description: SiTime Corporation
   "^sitronix,.*":
     description: Sitronix Technology Corporation
   "^skov,.*":
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (2 preceding siblings ...)
  2026-11-10 20:26 ` [PATCH net-next v15 04/13] dpll: add basic SiTime SiT9531x support Ali Rouhi
@ 2026-11-10 20:26 ` Ali Rouhi
  2026-11-10 20:26 ` [PATCH net-next v15 06/13] dpll: sit9531x: register DPLL devices and pins Ali Rouhi
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

Add a binding for the SiTime SiT9531x family of clock generators: an
I2C-controlled device with four independent PLLs, up to eight input
clocks and up to twelve outputs, described as a DPLL provider.

The XO is given either as a clock or, for firmware that describes the
oscillator as a property rather than as a clock provider, such as ACPI,
as clock-frequency.

The supplies are described by their pin names: the core and input rails,
the GPIO rail the SiT95317 has, and one per output-driver pin: VDDO0 to
VDDO7 on the SiT95317, VDDO0_1, VDDO2 to VDDO9 and VDDO10_11 on the
SiT95316.  The driver does not enable them, since the boards so far
power the part permanently; describing them gives a board with
switchable rails a way to say so.

Signed-off-by: Ali Rouhi <arouhi@sitime.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
---
 .../bindings/dpll/sitime,sit95316.yaml        | 214 ++++++++++++++++++
 MAINTAINERS                                   |   6 +
 2 files changed, 220 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml

diff --git a/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
new file mode 100644
index 000000000000..5356a73a8062
--- /dev/null
+++ b/Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
@@ -0,0 +1,214 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/dpll/sitime,sit95316.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SiTime SiT95316/SiT95317 DPLL Clock Generator
+
+maintainers:
+  - Ali Rouhi <arouhi@sitime.com>
+
+description: |
+  SiTime SiT95316 and SiT95317 are I2C-controlled programmable clock
+  generators with integrated DPLL for synchronization applications.  Both
+  variants contain four PLLs with automatic/manual reference selection,
+  DCO frequency adjustment, and phase offset measurement via an on-chip
+  TDC (Time-to-Digital Converter).
+
+  Both parts have 4 differential input pairs whose lanes can also be
+  driven independently as single-ended references, so 8 inputs are
+  individually selectable.  SiT95317 drives 8 outputs, SiT95316
+  drives 12.
+
+  Pin numbering.  An input pin's reg is its lane: 2k is INkP and 2k + 1
+  is INkN, 0 to 7.  A pair the loaded configuration runs differential is
+  one input, described at 2k.  An output pin's reg is its physical output
+  OUTn, 0 to 11; the SiT95317 bonds out the die's outputs 0, 3, 4, 5, 7, 8,
+  9 and 11, which its datasheet names OUT0 to OUT7 in that order.
+
+properties:
+  compatible:
+    enum:
+      - sitime,sit95316
+      - sitime,sit95317
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    maxItems: 1
+
+  clock-names:
+    items:
+      - const: xtal
+
+  clock-frequency:
+    description:
+      XO rate in Hz feeding XIN/XO_CLK, for firmware that describes the
+      oscillator as a property rather than as a clock provider, such as
+      ACPI.  Use either this or "clocks", not both.
+
+  reset-gpios:
+    maxItems: 1
+    description:
+      GPIO connected to the chip's active-low reset pin (RESETB).
+
+  interrupts:
+    maxItems: 1
+    description:
+      Interrupt from the chip's active-low INTRB output.  Asserted when
+      the device detects a status change such as lock acquisition or loss.
+
+  dpll-types:
+    maxItems: 4
+
+  vdd-supply:
+    description: Core supply of the PLLs, 1.8 V (the VDD pins).
+
+  vddin-supply:
+    description:
+      Supply of the input receivers and dividers, 3.3 V (VDDIN).
+
+  vdds-supply:
+    description:
+      Supply of the two GPIO pins that have their own rail (VDDS).  The
+      SiT95316 has no such pin.
+
+  input-pins:
+    type: object
+    patternProperties:
+      "^pin@[0-9a-f]+$":
+        type: object
+        properties:
+          reg:
+            maximum: 7
+
+          supported-frequencies-hz:
+            maxItems: 1
+            description:
+              The rate wired to the input, in Hz, as a single entry.  An
+              input runs at the rate the board feeds it and has no
+              frequency to select, so that entry is what the pin reports
+              as its frequency.
+
+patternProperties:
+  "^vddo([0-9]|1[01]|0_1|10_11)-supply$":
+    description: |
+      Supply of an output driver, named after its VDDO pin.  The SiT95317
+      has one per output, VDDO0 to VDDO7, numbered as that part's
+      datasheet numbers its outputs; in the pin numbering of this binding
+      those are outputs 0, 3, 4, 5, 7, 8, 9 and 11, in that order.  The
+      SiT95316 has VDDO0_1, VDDO2 to VDDO9 and VDDO10_11, the two shared
+      pins each powering the pair of outputs they are named after.
+
+required:
+  - compatible
+  - reg
+
+dependencies:
+  clocks: [ clock-names ]
+
+oneOf:
+  - required:
+      - clocks
+  - required:
+      - clock-frequency
+
+allOf:
+  - $ref: /schemas/dpll/dpll-device.yaml#
+  - if:
+      properties:
+        compatible:
+          const: sitime,sit95317
+    then:
+      properties:
+        output-pins:
+          patternProperties:
+            "^pin@[0-9a-f]+$":
+              properties:
+                reg:
+                  enum: [0, 3, 4, 5, 7, 8, 9, 11]
+      patternProperties:
+        "^vddo(8|9|1[01]|0_1|10_11)-supply$": false
+    else:
+      properties:
+        vdds-supply: false
+        output-pins:
+          patternProperties:
+            "^pin@[0-9a-f]+$":
+              properties:
+                reg:
+                  maximum: 11
+      patternProperties:
+        "^vddo(1[01]|[01])-supply$": false
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        dpll@68 {
+            compatible = "sitime,sit95316";
+            reg = <0x68>;
+            clocks = <&xo2>;
+            clock-names = "xtal";
+            reset-gpios = <&gpio 78 GPIO_ACTIVE_LOW>;
+            interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
+            vdd-supply = <&reg_1v8>;
+            vddin-supply = <&reg_3v3>;
+            vddo0_1-supply = <&reg_3v3>;
+            vddo10_11-supply = <&reg_1v8>;
+            dpll-types = "eec", "eec", "eec", "eec";
+
+            input-pins {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                /* IN0P and IN0N: pair 0 runs single-ended */
+                pin@0 {
+                    reg = <0>;
+                    label = "clkin0";
+                    connection-type = "ext";
+                    supported-frequencies-hz = <10000000>;
+                };
+
+                pin@1 {
+                    reg = <1>;
+                    label = "clkin1";
+                    connection-type = "synce";
+                    supported-frequencies-hz = <156250000>;
+                };
+            };
+
+            output-pins {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pin@0 {
+                    reg = <0>;
+                    label = "clkout0";
+                    supported-frequencies-hz = <156250000>;
+                };
+
+                pin@1 {
+                    reg = <1>;
+                    label = "clkout1";
+                    supported-frequencies-hz = <25000000>;
+                };
+
+                pin@a {
+                    reg = <10>;
+                    label = "clkout10";
+                    supported-frequencies-hz = <10000000>;
+                };
+            };
+        };
+    };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index fe23913b1e43..6c831bf9e812 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25870,6 +25870,12 @@ S:	Maintained
 W:	http://www.winischhofer.at/linuxsisusbvga.shtml
 F:	drivers/usb/misc/sisusbvga/
 
+SITIME SIT9531X DPLL DRIVER
+M:	Ali Rouhi <arouhi@sitime.com>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+
 SL28 CPLD MFD DRIVER
 M:	Michael Walle <mwalle@kernel.org>
 S:	Maintained
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 04/13] dpll: add basic SiTime SiT9531x support
  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-10 20:26 ` [PATCH net-next v15 02/13] dt-bindings: vendor-prefixes: add SiTime Corporation Ali Rouhi
@ 2026-11-10 20:26 ` Ali Rouhi
  2026-11-10 20:26 ` [PATCH net-next v15 03/13] dt-bindings: dpll: add SiTime SiT95316 clock generator Ali Rouhi
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Add the I2C probe for the SiTime SiT9531x family: four PLLs, up to eight
inputs and up to twelve outputs, configured from efuse or from a blob
applied before the driver binds. Later patches register the PLLs with
the DPLL subsystem.

The register space is 32 pages of 256 registers, selected through offset
0xFF of any page. The regmap is the flat 256-byte window, and the
accessors select the page under a lock.

Probe takes the optional reset line and the XO rate, waits the boot time
with reset deasserted so the configuration the part holds is kept, and
identifies the variant from the ID register.

Suggested-by: Ivan Vecera <ivecera@redhat.com>
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 MAINTAINERS                    |   1 +
 drivers/dpll/Kconfig           |   2 +
 drivers/dpll/Makefile          |   1 +
 drivers/dpll/sit9531x/Kconfig  |  13 ++
 drivers/dpll/sit9531x/Makefile |   4 +
 drivers/dpll/sit9531x/core.c   | 308 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/core.h   |  88 ++++++++++
 drivers/dpll/sit9531x/regs.h   |  36 ++++
 8 files changed, 453 insertions(+)
 create mode 100644 drivers/dpll/sit9531x/Kconfig
 create mode 100644 drivers/dpll/sit9531x/Makefile
 create mode 100644 drivers/dpll/sit9531x/core.c
 create mode 100644 drivers/dpll/sit9531x/core.h
 create mode 100644 drivers/dpll/sit9531x/regs.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 6c831bf9e812..936996cef03a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25875,6 +25875,7 @@ M:	Ali Rouhi <arouhi@sitime.com>
 L:	netdev@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/dpll/sitime,sit95316.yaml
+F:	drivers/dpll/sit9531x/
 
 SL28 CPLD MFD DRIVER
 M:	Michael Walle <mwalle@kernel.org>
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040a..f8f7ca121b93 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,8 @@ config DPLL_REFCNT_TRACKER
 
 	  If unsure, say N.
 
+source "drivers/dpll/sit9531x/Kconfig"
+
 source "drivers/dpll/zl3073x/Kconfig"
 
 endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e..4adc50d748d4 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -8,4 +8,5 @@ dpll-y                  += dpll_core.o
 dpll-y                  += dpll_netlink.o
 dpll-y                  += dpll_nl.o
 
+obj-$(CONFIG_SIT9531X_DPLL)	+= sit9531x/
 obj-$(CONFIG_ZL3073X)	+= zl3073x/
diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
new file mode 100644
index 000000000000..ac5b7f31a034
--- /dev/null
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config SIT9531X_DPLL
+	tristate "SiTime SiT9531x DPLL driver"
+	depends on I2C && NET
+	select DPLL
+	select REGMAP_I2C
+	help
+	  Driver for SiTime SiT9531x family clock generators
+	  (SiT95317, SiT95316).
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
new file mode 100644
index 000000000000..a221fe55386a
--- /dev/null
+++ b/drivers/dpll/sit9531x/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
+sit9531x-y := core.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
new file mode 100644
index 000000000000..3ec1ab5aa56c
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.c
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * I2C probe, paged regmap configuration and register access helpers.
+ */
+
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include "core.h"
+#include "regs.h"
+
+#define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
+	{ .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
+	  .name = (_name), .clkout_map = (_map) }
+
+/* Per-variant output index -> physical slot mapping */
+static const u8 clkout_map_95317[] = {0, 3, 4, 5, 7, 8, 9, 11};
+static const u8 clkout_map_95316[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+static const struct sit9531x_chip_info sit9531x_chip_ids[] = {
+	SIT9531X_CHIP(SIT9531X_VARIANT_ID_95317, 8, 8, "SiT95317",
+		      clkout_map_95317),
+	SIT9531X_CHIP(SIT9531X_VARIANT_ID_95316, 8, 12, "SiT95316",
+		      clkout_map_95316),
+};
+
+/*
+ * 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.
+ */
+static const struct regmap_config sit9531x_regmap_config = {
+	.reg_bits	= 8,
+	.val_bits	= 8,
+	.max_register	= SIT9531X_PAGE_SEL,
+	.cache_type	= REGCACHE_NONE,
+};
+
+/* Caller holds page_lock. */
+static int sit9531x_page_select(struct sit9531x_dev *sitdev, int page)
+{
+	int rc;
+
+	if (sitdev->page == page)
+		return 0;
+
+	sitdev->page = -1;
+	rc = regmap_write(sitdev->regmap, SIT9531X_PAGE_SEL, page);
+	if (rc)
+		return rc;
+	sitdev->page = page;
+
+	return 0;
+}
+
+/* Caller holds page_lock. */
+static int sit9531x_read_locked(struct sit9531x_dev *sitdev, unsigned int reg,
+				unsigned int *val)
+{
+	int rc;
+
+	rc = sit9531x_page_select(sitdev, SIT9531X_REG_PAGE(reg));
+	if (rc)
+		return rc;
+	rc = regmap_read(sitdev->regmap, SIT9531X_REG_OFFSET(reg), val);
+	if (rc)
+		sitdev->page = -1;
+
+	return rc;
+}
+
+/* Caller holds page_lock. */
+static int sit9531x_write_locked(struct sit9531x_dev *sitdev,
+				 unsigned int reg, u8 val)
+{
+	int rc;
+
+	rc = sit9531x_page_select(sitdev, SIT9531X_REG_PAGE(reg));
+	if (rc)
+		return rc;
+	rc = regmap_write(sitdev->regmap, SIT9531X_REG_OFFSET(reg), val);
+	if (rc)
+		sitdev->page = -1;
+	else if (SIT9531X_REG_OFFSET(reg) == SIT9531X_PAGE_SEL)
+		sitdev->page = val;
+
+	return rc;
+}
+
+/* @reg is a SIT9531X_REG(page, offset) value. */
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		     u8 *val)
+{
+	unsigned int tmp;
+	int rc;
+
+	mutex_lock(&sitdev->page_lock);
+	rc = sit9531x_read_locked(sitdev, reg, &tmp);
+	mutex_unlock(&sitdev->page_lock);
+	if (rc) {
+		dev_err_ratelimited(sitdev->dev,
+				    "Failed to read page 0x%02x reg 0x%02x: %d\n",
+				    SIT9531X_REG_PAGE(reg),
+				    SIT9531X_REG_OFFSET(reg), rc);
+		return rc;
+	}
+	*val = (u8)tmp;
+
+	return 0;
+}
+
+/* @reg is a SIT9531X_REG(page, offset) value. */
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		      u8 val)
+{
+	int rc;
+
+	mutex_lock(&sitdev->page_lock);
+	rc = sit9531x_write_locked(sitdev, reg, val);
+	mutex_unlock(&sitdev->page_lock);
+	if (rc)
+		dev_err_ratelimited(sitdev->dev,
+				    "Failed to write page 0x%02x reg 0x%02x: %d\n",
+				    SIT9531X_REG_PAGE(reg),
+				    SIT9531X_REG_OFFSET(reg), rc);
+
+	return rc;
+}
+
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			 u8 offset, u8 *val)
+{
+	u8 page = sit9531x_pll_page(pll_idx);
+
+	return sit9531x_read_u8(sitdev, SIT9531X_REG(page, offset), val);
+}
+
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			  u8 offset, u8 val)
+{
+	u8 page = sit9531x_pll_page(pll_idx);
+
+	return sit9531x_write_u8(sitdev, SIT9531X_REG(page, offset), val);
+}
+
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			   u8 offset, u8 mask, u8 val)
+{
+	u8 page = sit9531x_pll_page(pll_idx);
+	unsigned int reg = SIT9531X_REG(page, offset);
+	unsigned int orig, tmp;
+	int rc;
+
+	mutex_lock(&sitdev->page_lock);
+	rc = sit9531x_read_locked(sitdev, reg, &orig);
+	if (!rc) {
+		tmp = (orig & ~mask) | (val & mask);
+		if (tmp != orig)
+			rc = sit9531x_write_locked(sitdev, reg, tmp);
+	}
+	mutex_unlock(&sitdev->page_lock);
+	if (rc)
+		dev_err_ratelimited(sitdev->dev,
+				    "Failed to update page 0x%02x reg 0x%02x: %d\n",
+				    page, offset, rc);
+
+	return rc;
+}
+
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+		if (sit9531x_chip_ids[i].id == id)
+			return &sit9531x_chip_ids[i];
+	}
+
+	return NULL;
+}
+
+static int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+	struct clk *xtal_clk;
+	u8 variant_id;
+	int rc;
+
+	/*
+	 * Fvco = Fref * (DIVN + frac/2^32); Fref is the rate of the XO feeding
+	 * XIN/XO_CLK.
+	 */
+	xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+	if (IS_ERR(xtal_clk))
+		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+				     "Failed to get xtal clock\n");
+	sitdev->xtal_freq = clk_get_rate(xtal_clk);
+	if (!sitdev->xtal_freq) {
+		u32 freq;
+
+		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+					      &freq))
+			sitdev->xtal_freq = freq;
+	}
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+	/*
+	 * Keep reset deasserted, never pulse it: a reset discards the
+	 * configuration from efuse or a pre-probe NVM blob. Release it before
+	 * the first I2C access.
+	 */
+	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+						     GPIOD_OUT_LOW);
+	if (IS_ERR(sitdev->reset_gpio))
+		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+				     "Failed to request reset gpio\n");
+	if (sitdev->reset_gpio)
+		fsleep(10000);	/* internal boot after release */
+
+	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	if (rc)
+		return rc;
+
+	sitdev->info = sit9531x_match_variant(variant_id);
+	if (!sitdev->info)
+		return dev_err_probe(sitdev->dev, -ENODEV,
+				     "Unknown variant ID: 0x%02x\n",
+				     variant_id);
+
+	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	if (rc)
+		return dev_err_probe(sitdev->dev, rc,
+				     "Failed to initialize mutex\n");
+
+	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+		 sitdev->info->name, sitdev->info->num_inputs,
+		 sitdev->info->num_outputs);
+
+	return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+	struct sit9531x_dev *sitdev;
+	struct regmap *regmap;
+	int rc;
+
+	regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+	if (IS_ERR(regmap))
+		return dev_err_probe(&client->dev, PTR_ERR(regmap),
+				     "Failed to initialize regmap\n");
+
+	sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+	if (!sitdev)
+		return -ENOMEM;
+
+	sitdev->dev = &client->dev;
+	sitdev->client = client;
+	sitdev->regmap = regmap;
+	sitdev->page = -1;
+	rc = devm_mutex_init(&client->dev, &sitdev->page_lock);
+	if (rc)
+		return rc;
+	i2c_set_clientdata(client, sitdev);
+
+	return sit9531x_dev_probe(sitdev);
+}
+
+static const struct of_device_id sit9531x_of_match[] = {
+	{ .compatible = "sitime,sit95316" },
+	{ .compatible = "sitime,sit95317" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+static struct i2c_driver sit9531x_i2c_driver = {
+	.driver			= {
+		.name		= "sit9531x",
+		.of_match_table	= sit9531x_of_match,
+	},
+	.probe		= sit9531x_i2c_probe,
+};
+module_i2c_driver(sit9531x_i2c_driver);
+
+MODULE_AUTHOR("Ali Rouhi <arouhi@sitime.com>");
+MODULE_AUTHOR("Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>");
+MODULE_DESCRIPTION("SiTime SiT9531x DPLL subsystem driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
new file mode 100644
index 000000000000..0f826313eff8
--- /dev/null
+++ b/drivers/dpll/sit9531x/core.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL core driver
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Device structure, register access helpers, and core function
+ * declarations.
+ */
+
+#ifndef _SIT9531X_CORE_H
+#define _SIT9531X_CORE_H
+
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+
+#include "regs.h"
+
+#define SIT9531X_NUM_PLLS		4
+#define SIT9531X_MAX_INPUTS		8
+#define SIT9531X_MAX_OUTPUTS		12
+
+/*
+ * struct sit9531x_chip_info - chip variant identification
+ * @id: variant ID byte read from the device
+ * @num_inputs: number of input clock pins
+ * @num_outputs: number of output clock pins
+ * @name: variant name
+ * @clkout_map: physical slot of each output index
+ */
+struct sit9531x_chip_info {
+	u8		id;
+	u8		num_inputs;
+	u8		num_outputs;
+	const char	*name;
+	const u8	*clkout_map;
+};
+
+/*
+ * struct sit9531x_dev - SiT9531x device instance
+ * @dev: parent device
+ * @client: I2C client backing @regmap
+ * @regmap: the 256-byte register window
+ * @info: chip variant info
+ * @multiop_lock: serializes multi-register operations
+ * @page_lock: held across a page select and its transfer; nests inside
+ *   @multiop_lock
+ * @page: current page, -1 when unknown
+ * @xtal_freq: crystal frequency in Hz
+ * @reset_gpio: optional reset line, NULL if absent
+ */
+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;
+
+	u32			xtal_freq;
+
+	struct gpio_desc	*reset_gpio;
+};
+
+/* Register page of PLL @pll_idx (0 = PLLA, 3 = PLLD). */
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+	return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+			 u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+			  u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
+			   u8 mask, u8 val);
+
+#endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
new file mode 100644
index 000000000000..3a6151f44d61
--- /dev/null
+++ b/drivers/dpll/sit9531x/regs.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x register definitions
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_REGS_H
+#define _SIT9531X_REGS_H
+
+/*
+ * The page select is at offset 0xFF in every page; some pages are paired, e.g.
+ * 0x0A/0x1A for PLLA.
+ */
+#define SIT9531X_PAGE_SEL		0xFF
+#define SIT9531X_PAGE_SIZE		0x100
+#define SIT9531X_NUM_PAGES		32
+
+#define SIT9531X_REG(_page, _offset)		(((_page) << 8) | (_offset))
+#define SIT9531X_REG_PAGE(_reg)		((_reg) >> 8)
+#define SIT9531X_REG_OFFSET(_reg)		((_reg) & 0xFF)
+
+#define SIT9531X_PAGE_OUTSYS0		0x03
+#define SIT9531X_PAGE_OUTSYS1		0x04
+#define SIT9531X_PAGE_PLLA			0x0A
+#define SIT9531X_PAGE_PLLA_EXT		0x1A
+
+/* Reg 0x03 holds an unrelated revision byte, not part of the variant ID */
+#define SIT9531X_REG_VARIANT_ID		SIT9531X_REG(0x00, 0x02)
+
+#define SIT9531X_VARIANT_ID_95317	0x17
+#define SIT9531X_VARIANT_ID_95316	0x31
+
+#endif /* _SIT9531X_REGS_H */
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 05/13] dpll: sit9531x: read DPLL types and pin properties from system firmware
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (4 preceding siblings ...)
  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
  2026-11-10 20:26 ` [PATCH net-next v15 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Read the DPLL type and, for each pin, its label, direction, capabilities
and supported frequencies from the firmware node, with defaults for a
node that gives none.

Inputs are the P and N lanes of four pairs: a pair run single-ended
gives two inputs, a differential pair one. Outputs are labeled OUTn by
the driver's index, while reg stays the die slot the binding defines, so
one binding serves both variants.

An output with no supported-frequencies-hz is advertised as a continuous
range, since the core refuses every frequency request on a pin with
none.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/Makefile |   2 +-
 drivers/dpll/sit9531x/core.h   |  56 ++++++
 drivers/dpll/sit9531x/prop.c   | 327 +++++++++++++++++++++++++++++++++
 drivers/dpll/sit9531x/prop.h   |  37 ++++
 4 files changed, 421 insertions(+), 1 deletion(-)
 create mode 100644 drivers/dpll/sit9531x/prop.c
 create mode 100644 drivers/dpll/sit9531x/prop.h

diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index a221fe55386a..819af61123f5 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o
+sit9531x-y := core.o prop.o
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 0f826313eff8..7a81c9b14b7f 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -23,7 +23,15 @@
 
 #define SIT9531X_NUM_PLLS		4
 #define SIT9531X_MAX_INPUTS		8
+#define SIT9531X_NUM_INPUT_PAIRS	(SIT9531X_MAX_INPUTS / 2)
 #define SIT9531X_MAX_OUTPUTS		12
+/*
+ * INTSYNC, the inter-PLL sync line, is two pins: an input pin on the PLL that
+ * locks to it, numbered after the inputs and the xtal, and an output pin on the
+ * PLL that drives it, numbered after the outputs.
+ */
+#define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
+#define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
 
 /*
  * struct sit9531x_chip_info - chip variant identification
@@ -41,6 +49,35 @@ struct sit9531x_chip_info {
 	const u8	*clkout_map;
 };
 
+/*
+ * enum sit9531x_signal_mode - input signal electrical mode
+ * @SIT9531X_MODE_SE: single-ended
+ * @SIT9531X_MODE_DE: differential
+ */
+enum sit9531x_signal_mode {
+	SIT9531X_MODE_SE = 0,
+	SIT9531X_MODE_DE,
+};
+
+/*
+ * struct sit9531x_ref - input reference state
+ * @freq: configured frequency in Hz
+ * @sig_mode: signal mode of the input pair, read from CLKINx_INPUT_MODE at
+ *   probe
+ */
+struct sit9531x_ref {
+	u64		freq;
+	enum sit9531x_signal_mode	sig_mode;
+};
+
+/*
+ * struct sit9531x_out - output state
+ * @freq: output frequency in Hz
+ */
+struct sit9531x_out {
+	u64		freq;
+};
+
 /*
  * struct sit9531x_dev - SiT9531x device instance
  * @dev: parent device
@@ -51,6 +88,8 @@ struct sit9531x_chip_info {
  * @page_lock: held across a page select and its transfer; nests inside
  *   @multiop_lock
  * @page: current page, -1 when unknown
+ * @ref: input reference states
+ * @out: output states
  * @xtal_freq: crystal frequency in Hz
  * @reset_gpio: optional reset line, NULL if absent
  */
@@ -65,11 +104,28 @@ struct sit9531x_dev {
 	struct mutex			page_lock;
 	int				page;
 
+	struct sit9531x_ref	ref[SIT9531X_MAX_INPUTS + 1]; /* +1 for xtal */
+	struct sit9531x_out	out[SIT9531X_MAX_OUTPUTS];
 	u32			xtal_freq;
 
 	struct gpio_desc	*reset_gpio;
 };
 
+/*
+ * Input index 2n is the P lane and 2n + 1 the N lane of pair n; index
+ * SIT9531X_MAX_INPUTS is the XO.
+ */
+
+static inline u8 sit9531x_input_pair(u8 index)
+{
+	return index >> 1;
+}
+
+static inline bool sit9531x_input_is_n(u8 index)
+{
+	return index & 1;
+}
+
 /* Register page of PLL @pll_idx (0 = PLLA, 3 = PLLD). */
 static inline u8 sit9531x_pll_page(u8 pll_idx)
 {
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
new file mode 100644
index 000000000000..b9405eba49a3
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * Retrieves pin properties from Device Tree firmware nodes (or
+ * applies defaults when no firmware node exists).
+ */
+
+#include <linux/dev_printk.h>
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/fwnode.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "core.h"
+#include "prop.h"
+
+static void sit9531x_input_pin_label(struct sit9531x_dev *sitdev,
+				     struct sit9531x_pin_props *props, u8 id)
+{
+	u8 pair = sit9531x_input_pair(id);
+
+	if (sitdev->ref[id].sig_mode == SIT9531X_MODE_DE)
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u", pair);
+	else
+		snprintf(props->package_label,
+			 sizeof(props->package_label), "IN%u%c", pair,
+			 sit9531x_input_is_n(id) ? 'N' : 'P');
+}
+
+/* A differential input pair is labeled IN<n>; its N lane is not registered. */
+static void
+sit9531x_prop_pin_package_label_set(struct sit9531x_dev *sitdev,
+				    struct sit9531x_pin_props *props,
+				    enum dpll_pin_direction dir, u8 id)
+{
+	if (dir == DPLL_PIN_DIRECTION_INPUT &&
+	    id == SIT9531X_INTSYNC_PIN_ID) {
+		strscpy(props->package_label, "INTSYNC",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT && id == SIT9531X_MAX_INPUTS) {
+		strscpy(props->package_label, "XO",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_OUTPUT &&
+	    id == SIT9531X_INTSYNC_OUT_PIN_ID) {
+		strscpy(props->package_label, "SYNCOUT",
+			sizeof(props->package_label));
+		props->dpll_props.package_label = props->package_label;
+		return;
+	}
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT)
+		sit9531x_input_pin_label(sitdev, props, id);
+	else
+		/*
+		 * Package numbering; on the SiT95317 it differs from the chip
+		 * slot that the registers and the binding's reg use.
+		 */
+		snprintf(props->package_label, sizeof(props->package_label),
+			 "OUT%u", id);
+
+	props->dpll_props.package_label = props->package_label;
+}
+
+/* An output's reg is the chip slot it drives, not the driver's index. */
+static int
+sit9531x_prop_pin_fwnode_get(struct sit9531x_dev *sitdev,
+			     struct sit9531x_pin_props *props,
+			     enum dpll_pin_direction dir, u8 id)
+{
+	struct fwnode_handle *pins_node, *pin_node;
+	const char *node_name;
+
+	if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		node_name = "input-pins";
+	} else {
+		node_name = "output-pins";
+		if (id < sitdev->info->num_outputs)
+			id = sitdev->info->clkout_map[id];
+	}
+
+	pins_node = device_get_named_child_node(sitdev->dev, node_name);
+	if (!pins_node) {
+		dev_dbg(sitdev->dev, "'%s' sub-node is missing\n", node_name);
+		return -ENOENT;
+	}
+
+	fwnode_for_each_child_node(pins_node, pin_node) {
+		u32 reg;
+
+		if (fwnode_property_read_u32(pin_node, "reg", &reg))
+			continue;
+
+		if (id == reg)
+			break;
+	}
+
+	fwnode_handle_put(pins_node);
+
+	props->fwnode = pin_node;
+
+	dev_dbg(sitdev->dev, "Firmware node for %s %sfound\n",
+		props->package_label, pin_node ? "" : "NOT ");
+
+	return pin_node ? 0 : -ENOENT;
+}
+
+/* Free the result with sit9531x_pin_props_put(). */
+struct sit9531x_pin_props *
+sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+		       enum dpll_pin_direction dir, u8 index)
+{
+	bool sync_in = dir == DPLL_PIN_DIRECTION_INPUT &&
+		       index == SIT9531X_INTSYNC_PIN_ID;
+	bool sync_out = dir == DPLL_PIN_DIRECTION_OUTPUT &&
+			index == SIT9531X_INTSYNC_OUT_PIN_ID;
+	bool xo = dir == DPLL_PIN_DIRECTION_INPUT &&
+		  index == SIT9531X_MAX_INPUTS;
+	struct dpll_pin_frequency *ranges;
+	struct sit9531x_pin_props *props;
+	int i, j, num_freqs = 0, rc;
+	u32 *freqs = NULL;
+	const char *type;
+	u64 curr_freq;
+
+	props = kzalloc_obj(*props, GFP_KERNEL);
+	if (!props)
+		return ERR_PTR(-ENOMEM);
+
+	if (sync_in) {
+		/* INTSYNC destination: a reference the PLL can lock to. */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (sync_out) {
+		/* INTSYNC source: driving it is not a reference selection */
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	} else if (xo) {
+		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		props->dpll_props.capabilities = 0;
+		curr_freq = sitdev->xtal_freq;
+	} else if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = sitdev->ref[index].freq;
+	} else {
+		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+		curr_freq = 0;
+	}
+
+	sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
+
+	/* Internal pins take no properties from the firmware node. */
+	if (sync_in || sync_out || xo)
+		goto skip_fwnode_props;
+
+	rc = sit9531x_prop_pin_fwnode_get(sitdev, props, dir, index);
+	if (rc)
+		goto skip_fwnode_props;
+
+	fwnode_property_read_string(props->fwnode, "label",
+				    &props->dpll_props.board_label);
+
+	if (!fwnode_property_read_string(props->fwnode, "connection-type",
+					 &type)) {
+		if (!strcmp(type, "ext"))
+			props->dpll_props.type = DPLL_PIN_TYPE_EXT;
+		else if (!strcmp(type, "gnss"))
+			props->dpll_props.type = DPLL_PIN_TYPE_GNSS;
+		else if (!strcmp(type, "int"))
+			props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
+		else if (!strcmp(type, "synce"))
+			props->dpll_props.type = DPLL_PIN_TYPE_SYNCE_ETH_PORT;
+		else if (!strcmp(type, "mux"))
+			props->dpll_props.type = DPLL_PIN_TYPE_MUX;
+		else
+			dev_warn(sitdev->dev,
+				 "Unknown pin type '%s'\n", type);
+	}
+
+	if (!fwnode_property_present(props->fwnode,
+				     "supported-frequencies-hz"))
+		goto skip_fwnode_props;
+
+	/* A -hz property is 32-bit; no rate of this part needs more. */
+	num_freqs = fwnode_property_count_u32(props->fwnode,
+					      "supported-frequencies-hz");
+	if (num_freqs <= 0) {
+		rc = num_freqs ? num_freqs : -EINVAL;
+		dev_err(sitdev->dev,
+			"malformed supported-frequencies-hz for %s: %d\n",
+			props->package_label, rc);
+		goto err_alloc_freqs;
+	}
+
+	freqs = kcalloc(num_freqs, sizeof(*freqs), GFP_KERNEL);
+	if (!freqs) {
+		rc = -ENOMEM;
+		goto err_alloc_freqs;
+	}
+
+	rc = fwnode_property_read_u32_array(props->fwnode,
+					    "supported-frequencies-hz",
+					    freqs, num_freqs);
+	if (rc) {
+		dev_warn(sitdev->dev,
+			 "failed to parse supported-frequencies-hz for %s: %d\n",
+			 props->package_label, rc);
+		goto err_alloc_ranges;
+	}
+
+	/* The first listed rate seeds the input's rate */
+	if (num_freqs > 0 && dir == DPLL_PIN_DIRECTION_INPUT && !xo)
+		curr_freq = freqs[0];
+
+skip_fwnode_props:
+	if (sync_in || sync_out)
+		return props;
+
+	/*
+	 * An output with no listed rates gets one wide range, as the setter
+	 * checks Fvco/N; the XO gets its one rate.
+	 */
+	ranges = kcalloc(num_freqs + 1, sizeof(*ranges), GFP_KERNEL);
+	if (!ranges) {
+		rc = -ENOMEM;
+		goto err_alloc_ranges;
+	}
+
+	/* Set after the last allocation that can fail */
+	if (curr_freq && dir == DPLL_PIN_DIRECTION_INPUT)
+		sitdev->ref[index].freq = curr_freq;
+
+	j = 0;
+
+	for (i = 0; i < num_freqs; i++) {
+		struct dpll_pin_frequency freq = DPLL_PIN_FREQUENCY(freqs[i]);
+
+		ranges[j++] = freq;
+	}
+
+	if (!num_freqs && dir == DPLL_PIN_DIRECTION_OUTPUT) {
+		ranges[j].min = 1;
+		ranges[j].max = 1000000000ULL; /* 1 GHz */
+		j++;
+	} else if (!num_freqs && curr_freq) {
+		struct dpll_pin_frequency f = DPLL_PIN_FREQUENCY(curr_freq);
+
+		ranges[j++] = f;
+	}
+
+	if (j > 0) {
+		props->dpll_props.freq_supported = ranges;
+		props->dpll_props.freq_supported_num = j;
+	} else {
+		kfree(ranges);
+		props->dpll_props.freq_supported = NULL;
+		props->dpll_props.freq_supported_num = 0;
+	}
+
+	kfree(freqs);
+
+	return props;
+
+err_alloc_ranges:
+	kfree(freqs);
+err_alloc_freqs:
+	fwnode_handle_put(props->fwnode);
+	kfree(props);
+
+	return ERR_PTR(rc);
+}
+
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props)
+{
+	kfree(props->dpll_props.freq_supported);
+
+	if (props->fwnode)
+		fwnode_handle_put(props->fwnode);
+
+	kfree(props);
+}
+
+enum dpll_type
+sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev, u8 index)
+{
+	const char *types[SIT9531X_NUM_PLLS];
+	int count;
+
+	count = device_property_read_string_array(sitdev->dev, "dpll-types",
+						  types, ARRAY_SIZE(types));
+
+	if (index >= count || !types[index])
+		return DPLL_TYPE_PPS;
+
+	if (!strcmp(types[index], "pps"))
+		return DPLL_TYPE_PPS;
+	else if (!strcmp(types[index], "eec"))
+		return DPLL_TYPE_EEC;
+
+	dev_warn(sitdev->dev, "Unknown DPLL type '%s', using default\n",
+		 types[index]);
+
+	return DPLL_TYPE_PPS;
+}
diff --git a/drivers/dpll/sit9531x/prop.h b/drivers/dpll/sit9531x/prop.h
new file mode 100644
index 000000000000..b16db0bc51c6
--- /dev/null
+++ b/drivers/dpll/sit9531x/prop.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x firmware node property parsing
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ */
+
+#ifndef _SIT9531X_PROP_H
+#define _SIT9531X_PROP_H
+
+#include <linux/dpll.h>
+#include <linux/fwnode.h>
+
+struct sit9531x_dev;
+
+/*
+ * struct sit9531x_pin_props - pin properties from firmware
+ * @fwnode: firmware node handle, NULL if no DT node
+ * @dpll_props: DPLL core pin properties
+ * @package_label: pin package label (e.g. "IN0", "OUT3")
+ */
+struct sit9531x_pin_props {
+	struct fwnode_handle		*fwnode;
+	struct dpll_pin_properties	dpll_props;
+	char				package_label[8];
+};
+
+enum dpll_type sit9531x_prop_dpll_type_get(struct sit9531x_dev *sitdev,
+					   u8 index);
+struct sit9531x_pin_props *sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
+						  enum dpll_pin_direction dir,
+						  u8 index);
+void sit9531x_pin_props_put(struct sit9531x_pin_props *props);
+
+#endif /* _SIT9531X_PROP_H */
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 06/13] dpll: sit9531x: register DPLL devices and pins
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (3 preceding siblings ...)
  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 ` 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
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Register each PLL as a DPLL device with its lock status and mode
callbacks, and the XO as a pin on each. Inputs and outputs are
registered by the patches that add their state callbacks.

A kthread worker polls the status registers twice a second into a cache
the callbacks answer from, so a netlink call does not become a burst of
I2C transfers; INTRB, where wired, schedules a tick early. Each tick
notifies the core of what changed.

A PLL that arbitrates by priority is reported AUTOMATIC, one on manual
input select MANUAL. The clock_id is built from the SiTime OUI, the I2C
bus number and the address.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/Kconfig  |    4 +
 drivers/dpll/sit9531x/Makefile |    2 +-
 drivers/dpll/sit9531x/core.c   | 1466 ++++++++++++++++++++++++++++++--
 drivers/dpll/sit9531x/core.h   |  170 +++-
 drivers/dpll/sit9531x/dpll.c   |  295 +++++++
 drivers/dpll/sit9531x/dpll.h   |   61 ++
 drivers/dpll/sit9531x/regs.h   |  205 +++++
 7 files changed, 2116 insertions(+), 87 deletions(-)
 create mode 100644 drivers/dpll/sit9531x/dpll.c
 create mode 100644 drivers/dpll/sit9531x/dpll.h

diff --git a/drivers/dpll/sit9531x/Kconfig b/drivers/dpll/sit9531x/Kconfig
index ac5b7f31a034..47aea8674327 100644
--- a/drivers/dpll/sit9531x/Kconfig
+++ b/drivers/dpll/sit9531x/Kconfig
@@ -9,5 +9,9 @@ config SIT9531X_DPLL
 	  Driver for SiTime SiT9531x family clock generators
 	  (SiT95317, SiT95316).
 
+	  This driver registers each on-chip PLL as a DPLL device
+	  and exposes input/output clocks as DPLL pins, providing
+	  runtime configuration via Generic Netlink.
+
 	  To compile this driver as a module, choose M here: the
 	  module will be called sit9531x.
diff --git a/drivers/dpll/sit9531x/Makefile b/drivers/dpll/sit9531x/Makefile
index 819af61123f5..b97d2656a460 100644
--- a/drivers/dpll/sit9531x/Makefile
+++ b/drivers/dpll/sit9531x/Makefile
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 
 obj-$(CONFIG_SIT9531X_DPLL) += sit9531x.o
-sit9531x-y := core.o prop.o
+sit9531x-y := core.o dpll.o prop.o
diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 3ec1ab5aa56c..209671e7917e 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -6,9 +6,11 @@
  * Author: Ali Rouhi <arouhi@sitime.com>
  * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
  *
- * I2C probe, paged regmap configuration and register access helpers.
+ * Core I2C probe, regmap configuration, hardware state management,
+ * and periodic work thread.
  */
 
+#include <linux/bitfield.h>
 #include <linux/bits.h>
 #include <linux/clk.h>
 #include <linux/delay.h>
@@ -16,14 +18,30 @@
 #include <linux/device.h>
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
+#include <linux/interrupt.h>
 #include <linux/kernel.h>
+#include <linux/kthread.h>
 #include <linux/module.h>
+#include <linux/pm.h>
 #include <linux/property.h>
 #include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/string.h>
 
 #include "core.h"
+#include "dpll.h"
+#include "prop.h"
 #include "regs.h"
 
+/*
+ * Extra pin positions: the crystal and the INTSYNC destination as inputs, the
+ * INTSYNC source as an output.
+ */
+#define SIT9531X_NUM_INPUT_PINS		(SIT9531X_MAX_INPUTS + 2)
+#define SIT9531X_NUM_OUTPUT_PINS	(SIT9531X_MAX_OUTPUTS + 1)
+#define SIT9531X_NUM_PINS_TOTAL		(SIT9531X_NUM_INPUT_PINS + \
+					 SIT9531X_NUM_OUTPUT_PINS)
+
 #define SIT9531X_CHIP(_id, _nin, _nout, _name, _map) \
 	{ .id = (_id), .num_inputs = (_nin), .num_outputs = (_nout), \
 	  .name = (_name), .clkout_map = (_map) }
@@ -67,6 +85,13 @@ static int sit9531x_page_select(struct sit9531x_dev *sitdev, int page)
 	return 0;
 }
 
+static void sit9531x_page_cache_drop(struct sit9531x_dev *sitdev)
+{
+	mutex_lock(&sitdev->page_lock);
+	sitdev->page = -1;
+	mutex_unlock(&sitdev->page_lock);
+}
+
 /* Caller holds page_lock. */
 static int sit9531x_read_locked(struct sit9531x_dev *sitdev, unsigned int reg,
 				unsigned int *val)
@@ -181,122 +206,1415 @@ int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return rc;
 }
 
-static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
+				    u8 index,
+				    unsigned int *force_reg,
+				    unsigned int *state_reg)
 {
-	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+	if (sitdev->ref[index].sig_mode == SIT9531X_MODE_DE) {
+		*force_reg = SIT9531X_REG_IN_DE_FORCE;
+		*state_reg = SIT9531X_REG_IN_DE_STATE;
+	} else if (sit9531x_input_is_n(index)) {
+		*force_reg = SIT9531X_REG_IN_SEN_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEN_STATE;
+	} else {
+		*force_reg = SIT9531X_REG_IN_SEP_FORCE;
+		*state_reg = SIT9531X_REG_IN_SEP_STATE;
+	}
 }
 
-static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+/*
+ * 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
+ * all three, as in Output_Disable_Control_HiZ.py. ODRn_MISC0 bits 3:2
+ * distinguish a CMOS output from a differential one.
+ *
+ *   slot 0-7 :
+ *     DIFF 0xF2/0xF3   CMOS OutP 0xF8/0xF9   CMOS OutN 0xF6/0xF7
+ *   slot 8-11:
+ *     DIFF 0xF4/0xF5 bits 3:0   CMOS OutP 0xFA/0xFB
+ *     CMOS OutN 0xF4/0xF5 bits 7:4
+ *
+ * MASK = 1 takes control of the Hi-Z state; STATE = 0 forces Hi-Z, 1 drives.
+ */
+
+enum {
+	SIT9531X_HIZ_DIFF,
+	SIT9531X_HIZ_CMOS_P,
+	SIT9531X_HIZ_CMOS_N,
+	SIT9531X_HIZ_PAIRS,
+};
+
+struct sit9531x_hiz_pair {
+	unsigned int mask;
+	unsigned int state;
+	u8 bit;
+};
+
+static void sit9531x_output_get_hiz_regs(u8 slot,
+					 struct sit9531x_hiz_pair *p)
 {
-	unsigned int i;
+	struct sit9531x_hiz_pair *cmos_p = &p[SIT9531X_HIZ_CMOS_P];
+	struct sit9531x_hiz_pair *cmos_n = &p[SIT9531X_HIZ_CMOS_N];
+	struct sit9531x_hiz_pair *diff = &p[SIT9531X_HIZ_DIFF];
 
-	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
-		if (sit9531x_chip_ids[i].id == id)
-			return &sit9531x_chip_ids[i];
+	if (slot <= 7) {
+		diff->mask = SIT9531X_REG_HIZ_DIFF_07_MASK;
+		diff->state = SIT9531X_REG_HIZ_DIFF_07_STATE;
+		diff->bit = slot;
+		cmos_p->mask = SIT9531X_REG_HIZ_SE_07_MASK;
+		cmos_p->state = SIT9531X_REG_HIZ_SE_07_STATE;
+		cmos_p->bit = slot;
+		cmos_n->mask = SIT9531X_REG_HIZ_SEN_07_MASK;
+		cmos_n->state = SIT9531X_REG_HIZ_SEN_07_STATE;
+		cmos_n->bit = slot;
+	} else {
+		diff->mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+		diff->state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+		diff->bit = slot - 8;
+		cmos_p->mask = SIT9531X_REG_HIZ_SE_811_MASK;
+		cmos_p->state = SIT9531X_REG_HIZ_SE_811_STATE;
+		cmos_p->bit = slot - 8;
+		cmos_n->mask = SIT9531X_REG_HIZ_DIFF_811_MASK;
+		cmos_n->state = SIT9531X_REG_HIZ_DIFF_811_STATE;
+		cmos_n->bit = slot - 8 + SIT9531X_HIZ_SEN_811_SHIFT;
 	}
+}
 
-	return NULL;
+/*
+ * Read whether an output is wired CMOS. Only the Hi-Z pairs for its wiring
+ * control the pad; the other pairs hold what the profile left. Caller must hold
+ * sitdev->multiop_lock.
+ */
+static int sit9531x_output_mode_fetch(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 slot, page, reg, val;
+	int rc;
+
+	slot = info->clkout_map[out_idx];
+	page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+		SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	reg = SIT9531X_OUT_MISC0_BASE +
+	      SIT9531X_OUT_MISC0_STRIDE * (slot % 6);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG(page, reg), &val);
+	if (rc)
+		return rc;
+
+	sitdev->out[out_idx].cmos = !!(val & (SIT9531X_OUT_CMOS_ENP |
+					      SIT9531X_OUT_CMOS_ENN));
+
+	return 0;
 }
 
-static int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+static int sit9531x_hiz_pair_forced(struct sit9531x_dev *sitdev,
+				    const struct sit9531x_hiz_pair *p,
+				    bool *forced)
 {
-	struct clk *xtal_clk;
-	u8 variant_id;
+	u8 mask, state;
 	int rc;
 
-	/*
-	 * Fvco = Fref * (DIVN + frac/2^32); Fref is the rate of the XO feeding
-	 * XIN/XO_CLK.
-	 */
-	xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
-	if (IS_ERR(xtal_clk))
-		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
-				     "Failed to get xtal clock\n");
-	sitdev->xtal_freq = clk_get_rate(xtal_clk);
-	if (!sitdev->xtal_freq) {
-		u32 freq;
+	rc = sit9531x_read_u8(sitdev, p->mask, &mask);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, p->state, &state);
+	if (rc)
+		return rc;
 
-		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
-					      &freq))
-			sitdev->xtal_freq = freq;
+	*forced = (mask & BIT(p->bit)) && !(state & BIT(p->bit));
+
+	return 0;
+}
+
+static int sit9531x_output_forced_hiz(struct sit9531x_dev *sitdev,
+				      u8 out_idx, bool *muted, bool *part)
+{
+	struct sit9531x_hiz_pair p[SIT9531X_HIZ_PAIRS];
+	bool pad_p, pad_n;
+	int rc;
+
+	sit9531x_output_get_hiz_regs(sitdev->info->clkout_map[out_idx], p);
+
+	*part = false;
+	if (!sitdev->out[out_idx].cmos)
+		return sit9531x_hiz_pair_forced(sitdev,
+						&p[SIT9531X_HIZ_DIFF], muted);
+
+	rc = sit9531x_hiz_pair_forced(sitdev, &p[SIT9531X_HIZ_CMOS_P], &pad_p);
+	if (!rc)
+		rc = sit9531x_hiz_pair_forced(sitdev, &p[SIT9531X_HIZ_CMOS_N],
+					      &pad_n);
+	if (rc)
+		return rc;
+
+	*muted = pad_p && pad_n;
+	*part = pad_p != pad_n;
+
+	return 0;
+}
+
+/*
+ * Procedure:
+ *   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
+ *
+ * Caller must hold sitdev->multiop_lock.
+ */
+
+static u16 sit9531x_prio_reg(u8 pll_idx, u8 slot)
+{
+	return SIT9531X_REG(SIT9531X_PAGE_PRIOSYS,
+			    SIT9531X_PRIO_BASE_REG +
+			    SIT9531X_PRIO_REGS_PER_PLL * pll_idx +
+			    slot / SIT9531X_PRIO_SLOTS_PER_REG);
+}
+
+/* Slot 2n is the high nibble of its register, 2n + 1 the low one. */
+static u8 sit9531x_prio_slot_get(u8 val, u8 slot)
+{
+	if (slot & 1)
+		return val & SIT9531X_PRIO_NIBBLE_MASK;
+
+	return val >> SIT9531X_PRIO_HI_SHIFT;
+}
+
+static void sit9531x_prio_mask_build(struct sit9531x_dev *sitdev, u8 pll_idx,
+				     const u8 *srcs)
+{
+	u16 mask = 0;
+	u8 slot;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		u8 src = srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK;
+
+		if (sit9531x_prio_src_usable(src))
+			mask |= BIT(src);
 	}
-	if (!sitdev->xtal_freq)
-		return dev_err_probe(sitdev->dev, -EINVAL,
-				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+	sitdev->chan[pll_idx].prio_mask = mask;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+static int sit9531x_prio_table_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+				    u8 *srcs)
+{
+	u8 val, slot;
+	int rc;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+		rc = sit9531x_read_u8(sitdev,
+				      sit9531x_prio_reg(pll_idx, slot), &val);
+		if (rc)
+			return rc;
+
+		srcs[slot] = sit9531x_prio_slot_get(val, slot);
+	}
+
+	return 0;
+}
+
+/* XO doubler register */
+#define SIT9531X_REG_XO2_GENERIC		SIT9531X_REG(0x00, 0x2D)
+#define SIT9531X_XO_DOUBLER_ENB_BIT		7   /* inverted: 0 = enabled */
+
+/* VCO frequency bands (Hz) */
+#define SIT9531X_FVCO_LOWBAND_MIN		4915200000ULL
+#define SIT9531X_FVCO_LOWBAND_MAX		5898240000ULL
+#define SIT9531X_FVCO_HIGHBAND_MIN		6875000000ULL
+#define SIT9531X_FVCO_HIGHBAND_MAX		7812500000ULL
+
+#define SIT9531X_DIVO_MAX			GENMASK_ULL(33, 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:
+ *   base + 0  PROG6  [7:5] OPSTG_VCASC_BUMP (preserved)
+ *                    [4:2] PRG_RST_FINE_DELAY
+ *                    [1:0] PRG_RST_DELAY[33:32]
+ *   base + 1  PROG5  PRG_RST_DELAY[31:24]
+ *   base + 2  PROG4  PRG_RST_DELAY[23:16]
+ *   base + 3  PROG3  PRG_RST_DELAY[15:8]
+ *   base + 4  PROG2  PRG_RST_DELAY[7:0]
+ * Outputs 0-5 are on Page 3, 6-11 on Page 4; base = 0x15 + 16 * (out_idx % 6).
+ * PRG_RST_DELAY is unsigned; a negative adjustment is wrapped to
+ * (T_out - |phase|) modulo one output period.
+ */
+
+/*
+ * Clear the write-1-to-clear notification registers on page 0 and the clock
+ * monitor ones on page 6. Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
+{
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTER_LOL_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_HO_FREEZE_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PLL_INNER_LOL_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_CMON_NOTIF, 0xFF);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_01, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_P_NOTIF_23, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_01, 0xFF);
+	if (rc)
+		return rc;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_CLKMON_N_NOTIF_23, 0xFF);
+	if (rc)
+		return rc;
+
+	dev_dbg(sitdev->dev, "All notification registers cleared\n");
+	return 0;
+}
+
+/* Read the lane's receiver state from the Page 0x02 force and state bits */
+static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+	unsigned int force_reg, state_reg;
+	struct sit9531x_ref *ref;
+	u8 pair, force, state;
+	int rc;
+
+	/* The XO (index SIT9531X_MAX_INPUTS) has no receiver to read. */
+	if (index >= SIT9531X_MAX_INPUTS)
+		return -EINVAL;
+
+	ref = &sitdev->ref[index];
+	pair = sit9531x_input_pair(index);
 
 	/*
-	 * Keep reset deasserted, never pulse it: a reset discards the
-	 * configuration from efuse or a pre-probe NVM blob. Release it before
-	 * the first I2C access.
+	 * A lane is disabled only while its force bit overrides it off; with
+	 * the force bit clear the loaded configuration sets it.
 	 */
-	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
-						     GPIOD_OUT_LOW);
-	if (IS_ERR(sitdev->reset_gpio))
-		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
-				     "Failed to request reset gpio\n");
-	if (sitdev->reset_gpio)
-		fsleep(10000);	/* internal boot after release */
+	sit9531x_input_get_regs(sitdev, index, &force_reg, &state_reg);
 
-	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	rc = sit9531x_read_u8(sitdev, force_reg, &force);
+	if (rc)
+		return rc;
+	rc = sit9531x_read_u8(sitdev, state_reg, &state);
 	if (rc)
 		return rc;
 
-	sitdev->info = sit9531x_match_variant(variant_id);
-	if (!sitdev->info)
-		return dev_err_probe(sitdev->dev, -ENODEV,
-				     "Unknown variant ID: 0x%02x\n",
-				     variant_id);
+	ref->enabled = !((force & BIT(pair)) && !(state & BIT(pair)));
 
-	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	return 0;
+}
+
+/*
+ * Read CLKINx_INPUT_MODE into both lanes of the pair; a pair with neither SE
+ * lane enabled is differential.
+ */
+static int sit9531x_input_mode_fetch(struct sit9531x_dev *sitdev, u8 pair)
+{
+	enum sit9531x_signal_mode sig_mode;
+	u8 mode;
+	int rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_IN_MODE(pair), &mode);
 	if (rc)
-		return dev_err_probe(sitdev->dev, rc,
-				     "Failed to initialize mutex\n");
+		return rc;
 
-	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
-		 sitdev->info->name, sitdev->info->num_inputs,
-		 sitdev->info->num_outputs);
+	if (mode & (SIT9531X_IN_MODE_SE_P_EN | SIT9531X_IN_MODE_SE_N_EN))
+		sig_mode = SIT9531X_MODE_SE;
+	else
+		sig_mode = SIT9531X_MODE_DE;
+
+	sitdev->ref[pair * 2].sig_mode = sig_mode;
+	sitdev->ref[pair * 2 + 1].sig_mode = sig_mode;
+
+	dev_dbg(sitdev->dev, "CLKIN%u mode reg 0x%02x -> %s\n", pair, mode,
+		sig_mode == SIT9531X_MODE_DE ? "differential" : "single-ended");
 
 	return 0;
 }
 
-static int sit9531x_i2c_probe(struct i2c_client *client)
+/* Read the PLL active-state bit (PLL page reg 0x02 bit 0). */
+static int sit9531x_pll_is_active(struct sit9531x_dev *sitdev, u8 pll_idx,
+				  bool *active)
 {
-	struct sit9531x_dev *sitdev;
-	struct regmap *regmap;
 	int rc;
+	u8 v;
 
-	regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
-	if (IS_ERR(regmap))
-		return dev_err_probe(&client->dev, PTR_ERR(regmap),
-				     "Failed to initialize regmap\n");
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_ACTIVE, &v);
+	if (rc)
+		return rc;
 
-	sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
-	if (!sitdev)
-		return -ENOMEM;
+	*active = !!(v & SIT9531X_PLL_ACTIVE_BIT);
 
-	sitdev->dev = &client->dev;
-	sitdev->client = client;
-	sitdev->regmap = regmap;
-	sitdev->page = -1;
-	rc = devm_mutex_init(&client->dev, &sitdev->page_lock);
+	return 0;
+}
+
+/*
+ * Manual active select (PLL_CONFIG1F_PLL bit 6) uses the table writes and is
+ * reported AUTOMATIC. Manual input select (MISCINNER_PLL bit 5) uses the
+ * input-select pins, or GPIO_INPUT_FUNC_CTRL5..8 when bit 4 is set, and is
+ * reported MANUAL. Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_chan_select_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 cfg, misc, sel;
+	int rc;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_CONFIG1F,
+				  &cfg);
 	if (rc)
 		return rc;
-	i2c_set_clientdata(client, sitdev);
+	chan->manual_input = false;
+	if (!(cfg & SIT9531X_PLL_CONFIG1F_MANUAL_SEL))
+		return 0;
 
-	return sit9531x_dev_probe(sitdev);
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_MISCINNER,
+				  &misc);
+	if (rc)
+		return rc;
+	if (!(misc & SIT9531X_PLL_MISCINNER_MAN_IN_SEL)) {
+		dev_info(sitdev->dev,
+			 "PLL%c: manual active select, the selection uses the priorities\n",
+			 'A' + pll_idx);
+		return 0;
+	}
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_MAN_IN_SEL(pll_idx), &sel);
+	if (rc)
+		return rc;
+	chan->manual_input = true;
+	if (sel & SIT9531X_MAN_IN_SEL_FROM_REG) {
+		sel = FIELD_GET(SIT9531X_MAN_IN_SEL_MASK, 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);
+	} else {
+		dev_info(sitdev->dev,
+			 "PLL%c: the profile selects its reference from the input-select pins; reported as manual\n",
+			 'A' + pll_idx);
+	}
+
+	return 0;
 }
 
-static const struct of_device_id sit9531x_of_match[] = {
-	{ .compatible = "sitime,sit95316" },
-	{ .compatible = "sitime,sit95317" },
-	{ }
-};
-MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+/*
+ * 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;
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 pll_status_1;
+	bool active;
+	int rc;
+
+	/*
+	 * On an inactive PLL the loss-of-lock bit stays clear and reads as
+	 * locked.
+	 */
+	rc = sit9531x_pll_is_active(sitdev, pll_idx, &active);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_STATUS, &status);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_OUTER_LOL_STATUS,
+			      &outer_lol);
+	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)
+		return rc;
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_HO_FREEZE_STATUS,
+			      &ho_freeze);
+	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;
+
+	sit9531x_prio_mask_build(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->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);
+	chan->stale = false;
+
+	return 0;
+}
+
+/*
+ * Read routing from the PLL output map registers and drive state from the Hi-Z
+ * force bits.
+ */
+static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
+{
+	struct sit9531x_out *out = &sitdev->out[index];
+	u8 map_lo, map_hi, slot, bit, claimed;
+	int pll_idx;
+	bool muted, part;
+	u16 mask;
+	int rc;
+
+	slot = sitdev->info->clkout_map[index];
+
+	rc = sit9531x_output_mode_fetch(sitdev, index);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_forced_hiz(sitdev, index, &muted, &part);
+	if (rc)
+		return rc;
+
+	sitdev->out[index].part_muted = part;
+
+	/*
+	 * Each PLL page holds a 12-bit output-enable mask: OUT_MAP_LO bits 0-7
+	 * and OUT_MAP_HI[3:0] bits 8-11. PLLA and PLLB map slot n to bit n,
+	 * PLLC and PLLD to bit 11 - n, as in the documented procedures.
+	 */
+	claimed = 0;
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_LO, &map_lo);
+		if (rc)
+			return rc;
+
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_OUT_MAP_HI, &map_hi);
+		if (rc)
+			return rc;
+
+		mask = map_lo | (map_hi & GENMASK(3, 0)) << 8;
+		bit = pll_idx < 2 ? slot : SIT9531X_MAX_OUTPUTS - 1 - slot;
+		if (mask & BIT(bit))
+			claimed |= BIT(pll_idx);
+	}
+
+	if (!claimed) {
+		out->pll_idx = 0;
+		out->routed = false;
+		out->enabled = false;
+		return 0;
+	}
+
+	out->pll_idx = __ffs(claimed);
+	out->routed = true;
+	out->enabled = !muted;
+
+	if (claimed & (claimed - 1))
+		dev_warn(sitdev->dev,
+			 "OUT%u is in the output-enable mask of more than one PLL (0x%x); taking PLL%c\n",
+			 slot, claimed, 'A' + out->pll_idx);
+
+	return 0;
+}
+
+/*
+ * Build ref->pll_mask from the priority tables; it decides when a receiver may
+ * be powered down. Caller must hold sitdev->multiop_lock.
+ */
+static int sit9531x_ref_pll_mask_fetch(struct sit9531x_dev *sitdev)
+{
+	u8 srcs[SIT9531X_PRIO_MAX_SLOTS];
+	u8 pll_idx, slot, index;
+	int rc;
+
+	for (pll_idx = 0; pll_idx < SIT9531X_NUM_PLLS; pll_idx++) {
+		rc = sit9531x_prio_table_read(sitdev, pll_idx, srcs);
+		if (rc)
+			return rc;
+
+		for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++) {
+			index = sit9531x_hw_src_input(srcs[slot]);
+			if (index >= sitdev->info->num_inputs)
+				continue;
+
+			/*
+			 * A differential pair exposes only its P lane; count an
+			 * N-lane entry as the P lane.
+			 */
+			if (sit9531x_input_is_n(index) &&
+			    sitdev->ref[index].sig_mode == SIT9531X_MODE_DE)
+				index--;
+
+			sitdev->ref[index].pll_mask |= BIT(pll_idx);
+		}
+	}
+
+	return 0;
+}
+
+static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
+{
+	int rc;
+	u8 i;
+
+	/* 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);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch CLKIN%u mode: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	for (i = 0; i < sitdev->info->num_inputs; i++) {
+		rc = sit9531x_ref_state_fetch(sitdev, i);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch input %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_ref_pll_mask_fetch(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev,
+			"Failed to fetch input priority tables: %d\n", rc);
+		return rc;
+	}
+
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		mutex_lock(&sitdev->multiop_lock);
+		rc = sit9531x_out_state_fetch(sitdev, i);
+		mutex_unlock(&sitdev->multiop_lock);
+		if (rc) {
+			dev_err(sitdev->dev,
+				"Failed to fetch output %u state: %d\n", i, rc);
+			return rc;
+		}
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		rc = sit9531x_chan_select_fetch(sitdev, i);
+		if (!rc)
+			rc = sit9531x_chan_state_fetch(sitdev, i);
+		if (rc)
+			break;
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_err(sitdev->dev, "Failed to fetch PLL%c state: %d\n",
+			'A' + i, rc);
+		return rc;
+	}
+
+	return 0;
+}
+
+static void sit9531x_dev_ref_states_update(struct sit9531x_dev *sitdev)
+{
+	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);
+	}
+}
+
+static void sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
+{
+	int i, rc;
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; 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);
+		}
+	}
+}
+
+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;
+	int rc;
+
+	/*
+	 * Release multiop_lock before the change checks: their notifications
+	 * 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);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_changes_check(sitdpll);
+
+	/*
+	 * Clear the W1C notification latches on pages 0 and 6 after acting on
+	 * them; while set they keep INTRB asserted.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		dev_warn_ratelimited(sitdev->dev,
+				     "Failed to clear notifications: %d\n",
+				     rc);
+
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work,
+				   msecs_to_jiffies(SIT9531X_STATUS_POLL_MS));
+}
+
+/* Queue an immediate poll tick; polling continues as a fallback. */
+static irqreturn_t sit9531x_irq_thread_fn(int irq, void *data)
+{
+	struct sit9531x_dev *sitdev = data;
+	int rc;
+
+	/*
+	 * Acknowledge here: IRQF_ONESHOT unmasks the line on return, and a
+	 * still-asserted INTRB would fire again at once.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_clear_notifications(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc) {
+		dev_warn_ratelimited(sitdev->dev,
+				     "IRQ: failed to clear notifications: %d\n",
+				     rc);
+		if (++sitdev->irq_ack_fails < SIT9531X_IRQ_ACK_TRIES)
+			return IRQ_NONE;
+
+		dev_err(sitdev->dev,
+			"IRQ %d disabled: notifications cannot be cleared, polling only\n",
+			irq);
+		disable_irq_nosync(irq);
+		return IRQ_NONE;
+	}
+
+	sitdev->irq_ack_fails = 0;
+
+	kthread_mod_delayed_work(sitdev->kworker, &sitdev->work, 0);
+	return IRQ_HANDLED;
+}
+
+/*
+ * Report the loaded EEPROM profile and warn on a bad load. Caller must hold
+ * sitdev->multiop_lock.
+ */
+static void sit9531x_eeprom_state_report(struct sit9531x_dev *sitdev)
+{
+	u32 rec_crc = 0, cal_crc = 0, prof_id = 0;
+	u8 notif, v;
+	int rc, i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	/* Profile id: three bytes, least significant first. */
+	for (i = 2; i >= 0; i--) {
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_PROFILE_ID + i, &v);
+		if (rc)
+			return;
+		prof_id = prof_id << 8 | v;
+	}
+
+	dev_info(sitdev->dev, "profile id %u\n", prof_id);
+
+	/* Both CRCs: four bytes, most significant first. */
+	for (i = 0; i < 4; i++) {
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_REC_CRC + i, &v);
+		if (rc)
+			return;
+		rec_crc = rec_crc << 8 | v;
+
+		rc = sit9531x_read_u8(sitdev, SIT9531X_REG_CAL_CRC + i, &v);
+		if (rc)
+			return;
+		cal_crc = cal_crc << 8 | v;
+	}
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_EEPROM_NOTIF, &notif);
+	if (rc)
+		return;
+
+	/*
+	 * A clean load sets only the read-done bit. Both CRCs zero means no
+	 * EEPROM read happened: the configuration was pushed over I2C.
+	 */
+	if (!rec_crc && !cal_crc)
+		dev_dbg(sitdev->dev,
+			"no EEPROM profile (configuration pushed over I2C)\n");
+	else if (rec_crc != cal_crc)
+		dev_warn(sitdev->dev,
+			 "EEPROM CRC mismatch: stored %08x, computed %08x\n",
+			 rec_crc, cal_crc);
+	else if (notif != SIT9531X_EEPROM_READ_DONE)
+		dev_warn(sitdev->dev,
+			 "EEPROM read reported defects (notify %02x)\n",
+			 notif);
+	else
+		dev_dbg(sitdev->dev, "EEPROM profile loaded, CRC %08x\n",
+			cal_crc);
+}
+
+/*
+ * Report which PLLs are active; an unused PLL never goes active. Caller must
+ * hold sitdev->multiop_lock.
+ */
+static void sit9531x_pll_states_report(struct sit9531x_dev *sitdev)
+{
+	unsigned int idx, i;
+	bool routed;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (sitdev->chan[i].active) {
+			dev_dbg(sitdev->dev, "PLL%c active\n", 'A' + i);
+			continue;
+		}
+
+		routed = false;
+		for (idx = 0; idx < sitdev->info->num_outputs; idx++) {
+			const struct sit9531x_out *out;
+
+			out = sit9531x_out_state_get(sitdev, idx);
+			if (out->routed && out->pll_idx == i) {
+				routed = true;
+				break;
+			}
+		}
+
+		if (routed)
+			dev_warn(sitdev->dev,
+				 "PLL%c drives outputs but is not in its active state\n",
+				 'A' + i);
+		else
+			dev_dbg(sitdev->dev, "PLL%c unused by the loaded configuration\n",
+				'A' + i);
+	}
+}
+
+static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+	int rc;
+
+	rc = sit9531x_dev_state_fetch(sitdev);
+	if (rc)
+		return rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_eeprom_state_report(sitdev);
+	sit9531x_pll_states_report(sitdev);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		rc = sit9531x_dpll_register(sitdpll);
+		if (rc) {
+			dev_err_probe(sitdev->dev, rc,
+				      "Failed to register DPLL%u\n",
+				      sitdpll->id);
+			goto err_unregister;
+		}
+	}
+
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+	return 0;
+
+err_unregister:
+	list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_unregister(sitdpll);
+
+	return rc;
+}
+
+static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	kthread_cancel_delayed_work_sync(&sitdev->work);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		if (sitdpll->dpll_dev)
+			sit9531x_dpll_unregister(sitdpll);
+	}
+}
+
+static struct sit9531x_dpll_pin *
+sit9531x_dpll_pin_alloc(struct sit9531x_dpll *sitdpll,
+			enum dpll_pin_direction dir, u8 id)
+{
+	struct sit9531x_dpll_pin *pin;
+
+	pin = kzalloc_obj(*pin, GFP_KERNEL);
+	if (!pin)
+		return ERR_PTR(-ENOMEM);
+
+	pin->dpll = sitdpll;
+	pin->dir = dir;
+	pin->id = id;
+
+	return pin;
+}
+
+/* @index is the pin's absolute index in the clock_id namespace. */
+static int sit9531x_dpll_pin_register(struct sit9531x_dpll_pin *pin,
+				      u32 index)
+{
+	struct sit9531x_dpll *sitdpll = pin->dpll;
+	struct sit9531x_pin_props *props;
+	const struct dpll_pin_ops *ops;
+	int rc;
+
+	props = sit9531x_pin_props_get(sitdpll->dev, pin->dir, pin->id);
+	if (IS_ERR(props))
+		return PTR_ERR(props);
+
+	strscpy(pin->label, props->package_label, sizeof(pin->label));
+	pin->fwnode = fwnode_handle_get(props->fwnode);
+
+	pin->dpll_pin = dpll_pin_get(sitdpll->dev->clock_id, index,
+				     THIS_MODULE, &props->dpll_props,
+				     &pin->tracker);
+	if (IS_ERR(pin->dpll_pin)) {
+		rc = PTR_ERR(pin->dpll_pin);
+		goto err_pin_get;
+	}
+	dpll_pin_fwnode_set(pin->dpll_pin, props->fwnode);
+
+	ops = sit9531x_dpll_pin_ops_get(pin);
+
+	rc = dpll_pin_register(sitdpll->dpll_dev, pin->dpll_pin, ops, pin);
+	if (rc)
+		goto err_register;
+
+	sit9531x_pin_props_put(props);
+
+	return 0;
+
+err_register:
+	dpll_pin_put(pin->dpll_pin, &pin->tracker);
+err_pin_get:
+	pin->dpll_pin = NULL;
+	fwnode_handle_put(pin->fwnode);
+	pin->fwnode = NULL;
+	sit9531x_pin_props_put(props);
+
+	return rc;
+}
+
+static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
+{
+	struct sit9531x_dpll *sitdpll = pin->dpll;
+	struct dpll_pin *dpll_pin = pin->dpll_pin;
+	const struct dpll_pin_ops *ops;
+
+	ops = sit9531x_dpll_pin_ops_get(pin);
+
+	/*
+	 * Clear before unregistering: callbacks test it under the DPLL device
+	 * lock, which dpll_pin_unregister() also takes.
+	 */
+	pin->dpll_pin = NULL;
+
+	dpll_pin_unregister(sitdpll->dpll_dev, dpll_pin, ops, pin);
+	dpll_pin_put(dpll_pin, &pin->tracker);
+
+	fwnode_handle_put(pin->fwnode);
+	pin->fwnode = NULL;
+}
+
+static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	struct sit9531x_dpll_pin *pin, *next;
+	LIST_HEAD(dead);
+
+	/*
+	 * Not under multiop_lock: the unregister takes the DPLL device lock,
+	 * which nests outside it.
+	 */
+	list_for_each_entry(pin, &sitdpll->pins, list)
+		sit9531x_dpll_pin_unregister(pin);
+
+	/*
+	 * Detached under multiop_lock, which callbacks walking the list hold;
+	 * the poll is already canceled or not yet queued.
+	 */
+	mutex_lock(&sitdev->multiop_lock);
+	list_splice_init(&sitdpll->pins, &dead);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry_safe(pin, next, &dead, list) {
+		list_del(&pin->list);
+		kfree(pin);
+	}
+}
+
+/* Check if the pin class has a state callback, which the DPLL core requires */
+static bool sit9531x_dpll_pin_is_registrable(struct sit9531x_dpll *sitdpll,
+					     enum dpll_pin_direction dir,
+					     u8 index)
+{
+	if (dir != DPLL_PIN_DIRECTION_INPUT)
+		return false;
+
+	return index == SIT9531X_MAX_INPUTS;
+}
+
+static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dpll_pin *pin;
+	enum dpll_pin_direction dir;
+	u8 id, index;
+	int rc;
+
+	for (index = 0; index < SIT9531X_NUM_PINS_TOTAL; index++) {
+		if (index < SIT9531X_NUM_INPUT_PINS) {
+			id = index;
+			dir = DPLL_PIN_DIRECTION_INPUT;
+		} else {
+			id = index - SIT9531X_NUM_INPUT_PINS;
+			dir = DPLL_PIN_DIRECTION_OUTPUT;
+		}
+
+		if (!sit9531x_dpll_pin_is_registrable(sitdpll, dir, id))
+			continue;
+
+		pin = sit9531x_dpll_pin_alloc(sitdpll, dir, id);
+		if (IS_ERR(pin)) {
+			rc = PTR_ERR(pin);
+			goto error;
+		}
+
+		rc = sit9531x_dpll_pin_register(pin, index);
+		if (rc) {
+			kfree(pin);
+			goto error;
+		}
+
+		/*
+		 * Earlier pins are already reachable from netlink, and the
+		 * walks over this list run under multiop_lock.
+		 */
+		mutex_lock(&sitdpll->dev->multiop_lock);
+		list_add(&pin->list, &sitdpll->pins);
+		mutex_unlock(&sitdpll->dev->multiop_lock);
+	}
+
+	return 0;
+
+error:
+	sit9531x_dpll_pins_unregister(sitdpll);
+	return rc;
+}
+
+static int sit9531x_dpll_device_register(struct sit9531x_dpll *sitdpll)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	sitdpll->ops = sit9531x_dpll_device_ops;
+
+	sitdpll->dpll_dev = dpll_device_get(sitdev->clock_id, sitdpll->id,
+					    THIS_MODULE, &sitdpll->tracker);
+	if (IS_ERR(sitdpll->dpll_dev)) {
+		rc = PTR_ERR(sitdpll->dpll_dev);
+		sitdpll->dpll_dev = NULL;
+		return rc;
+	}
+
+	rc = dpll_device_register(sitdpll->dpll_dev,
+				  sit9531x_prop_dpll_type_get(sitdev,
+							      sitdpll->id),
+				  &sitdpll->ops, sitdpll);
+	if (rc) {
+		dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+		sitdpll->dpll_dev = NULL;
+	}
+
+	return rc;
+}
+
+static void sit9531x_dpll_device_unregister(struct sit9531x_dpll *sitdpll)
+{
+	dpll_device_unregister(sitdpll->dpll_dev, &sitdpll->ops, sitdpll);
+	dpll_device_put(sitdpll->dpll_dev, &sitdpll->tracker);
+	sitdpll->dpll_dev = NULL;
+}
+
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	sitdpll = kzalloc_obj(*sitdpll, GFP_KERNEL);
+	if (!sitdpll)
+		return ERR_PTR(-ENOMEM);
+
+	sitdpll->dev = sitdev;
+	sitdpll->id = ch;
+	sitdpll->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+	sitdpll->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+	INIT_LIST_HEAD(&sitdpll->pins);
+
+	return sitdpll;
+}
+
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll)
+{
+	kfree(sitdpll);
+}
+
+int sit9531x_dpll_register(struct sit9531x_dpll *sitdpll)
+{
+	int rc;
+
+	rc = sit9531x_dpll_device_register(sitdpll);
+	if (rc)
+		return rc;
+
+	sit9531x_dpll_lock_status_baseline(sitdpll);
+
+	rc = sit9531x_dpll_pins_register(sitdpll);
+	if (rc) {
+		sit9531x_dpll_device_unregister(sitdpll);
+		return rc;
+	}
+
+	return 0;
+}
+
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll)
+{
+	sit9531x_dpll_pins_unregister(sitdpll);
+	sit9531x_dpll_device_unregister(sitdpll);
+}
+
+static void sit9531x_dpll_list_free(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll, *next;
+
+	list_for_each_entry_safe(sitdpll, next, &sitdev->dplls, list) {
+		list_del(&sitdpll->list);
+		sit9531x_dpll_free(sitdpll);
+	}
+}
+
+/* Runs only once the device is fully started, see the caller. */
+static void sit9531x_dev_dpll_fini(void *ptr)
+{
+	struct sit9531x_dev *sitdev = ptr;
+
+	sit9531x_dev_stop(sitdev);
+	kthread_destroy_worker(sitdev->kworker);
+	sit9531x_dpll_list_free(sitdev);
+}
+
+static int sit9531x_devm_dpll_init(struct sit9531x_dev *sitdev)
+{
+	struct kthread_worker *kworker;
+	struct sit9531x_dpll *sitdpll;
+	unsigned int i;
+	int rc;
+
+	INIT_LIST_HEAD(&sitdev->dplls);
+	kthread_init_delayed_work(&sitdev->work, sit9531x_dev_periodic_work);
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sitdpll = sit9531x_dpll_alloc(sitdev, i);
+		if (IS_ERR(sitdpll)) {
+			rc = dev_err_probe(sitdev->dev, PTR_ERR(sitdpll),
+					   "Failed to alloc DPLL%u\n", i);
+			goto err_free_dplls;
+		}
+
+		list_add_tail(&sitdpll->list, &sitdev->dplls);
+	}
+
+	kworker = kthread_run_worker(0, "sit9531x-%s", dev_name(sitdev->dev));
+	if (IS_ERR(kworker)) {
+		rc = PTR_ERR(kworker);
+		goto err_free_dplls;
+	}
+	sitdev->kworker = kworker;
+
+	rc = sit9531x_dev_start(sitdev);
+	if (rc) {
+		rc = dev_err_probe(sitdev->dev, rc, "Failed to start device\n");
+		goto err_destroy_worker;
+	}
+
+	/* Added last: only now is every field the cleanup touches valid. */
+	return devm_add_action_or_reset(sitdev->dev, sit9531x_dev_dpll_fini,
+					sitdev);
+
+err_destroy_worker:
+	kthread_destroy_worker(sitdev->kworker);
+err_free_dplls:
+	sit9531x_dpll_list_free(sitdev);
+
+	return rc;
+}
+
+/* See SIT9531X_REG_VARIANT_ID: one byte, reg 0x03 is not part of it. */
+static int sit9531x_read_variant_id(struct sit9531x_dev *sitdev, u8 *id)
+{
+	return sit9531x_read_u8(sitdev, SIT9531X_REG_VARIANT_ID, id);
+}
+
+static const struct sit9531x_chip_info *sit9531x_match_variant(u8 id)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(sit9531x_chip_ids); i++) {
+		if (sit9531x_chip_ids[i].id == id)
+			return &sit9531x_chip_ids[i];
+	}
+
+	return NULL;
+}
+
+/*
+ * Build the clock id from the SiTime OUI, the I2C bus number and the I2C
+ * address.
+ */
+static int sit9531x_derive_clock_id(struct sit9531x_dev *sitdev)
+{
+	int adap = i2c_adapter_id(sitdev->client->adapter);
+
+	/*
+	 * The OUI with its EUI-64 filler takes the top 40 bits; the bus gets 16
+	 * bits.
+	 */
+	if (adap > U16_MAX)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "I2C adapter number %d does not fit the clock id\n",
+				     adap);
+
+	sitdev->clock_id = SIT9531X_OUI << 24 | (u64)adap << 8 |
+			   sitdev->client->addr;
+
+	return 0;
+}
+
+static int sit9531x_dev_probe(struct sit9531x_dev *sitdev)
+{
+	struct clk *xtal_clk;
+	u8 variant_id;
+	int rc;
+
+	/*
+	 * Request before enabling the XO: a probe deferral would gate the XO
+	 * and remove the reference of a running part.
+	 */
+	sitdev->reset_gpio = devm_gpiod_get_optional(sitdev->dev, "reset",
+						     GPIOD_ASIS);
+	if (IS_ERR(sitdev->reset_gpio))
+		return dev_err_probe(sitdev->dev, PTR_ERR(sitdev->reset_gpio),
+				     "Failed to request reset gpio\n");
+
+	/* Every rate the driver derives starts from the XO feeding XIN. */
+	xtal_clk = devm_clk_get_optional_enabled(sitdev->dev, "xtal");
+	if (IS_ERR(xtal_clk))
+		return dev_err_probe(sitdev->dev, PTR_ERR(xtal_clk),
+				     "Failed to get xtal clock\n");
+	sitdev->xtal_freq = clk_get_rate(xtal_clk);
+	if (!sitdev->xtal_freq) {
+		u32 freq;
+
+		if (!device_property_read_u32(sitdev->dev, "clock-frequency",
+					      &freq))
+			sitdev->xtal_freq = freq;
+	}
+	if (!sitdev->xtal_freq)
+		return dev_err_probe(sitdev->dev, -EINVAL,
+				     "no xtal rate: provide clocks=<&xo> + clock-names=\"xtal\", or a clock-frequency property\n");
+
+	/*
+	 * Keep reset deasserted, never pulse it: a reset discards the
+	 * configuration from efuse or a pre-probe NVM blob. Release it before
+	 * the first I2C access.
+	 */
+	if (sitdev->reset_gpio) {
+		rc = gpiod_direction_output(sitdev->reset_gpio, 0);
+		if (rc)
+			return dev_err_probe(sitdev->dev, rc,
+					     "Failed to release the reset line\n");
+		fsleep(10000);	/* internal boot after release */
+	}
+
+	rc = sit9531x_read_variant_id(sitdev, &variant_id);
+	if (rc)
+		return rc;
+
+	sitdev->info = sit9531x_match_variant(variant_id);
+	if (!sitdev->info)
+		return dev_err_probe(sitdev->dev, -ENODEV,
+				     "Unknown variant ID: 0x%02x\n",
+				     variant_id);
+
+	rc = sit9531x_derive_clock_id(sitdev);
+	if (rc)
+		return rc;
+
+	sitdev->intsync_src = -1;
+
+	rc = devm_mutex_init(sitdev->dev, &sitdev->multiop_lock);
+	if (rc)
+		return dev_err_probe(sitdev->dev, rc,
+				     "Failed to initialize mutex\n");
+
+	dev_info(sitdev->dev, "%s detected, %u inputs, %u outputs\n",
+		 sitdev->info->name, sitdev->info->num_inputs,
+		 sitdev->info->num_outputs);
+
+	/* Before the IRQ: the handler queues work on sitdev->kworker. */
+	rc = sit9531x_devm_dpll_init(sitdev);
+	if (rc)
+		return rc;
+
+	/*
+	 * Without an "interrupts" property client->irq is 0 and only the poll
+	 * runs.
+	 */
+	sitdev->irq = sitdev->client->irq;
+	if (sitdev->irq > 0) {
+		rc = devm_request_threaded_irq(sitdev->dev, sitdev->irq,
+					       NULL, sit9531x_irq_thread_fn,
+					       IRQF_ONESHOT,
+					       dev_name(sitdev->dev), sitdev);
+		if (rc)
+			return dev_err_probe(sitdev->dev, rc,
+					     "Failed to request IRQ %d\n",
+					     sitdev->irq);
+	}
+
+	return 0;
+}
+
+static int sit9531x_i2c_probe(struct i2c_client *client)
+{
+	struct sit9531x_dev *sitdev;
+	struct regmap *regmap;
+	int rc;
+
+	regmap = devm_regmap_init_i2c(client, &sit9531x_regmap_config);
+	if (IS_ERR(regmap))
+		return dev_err_probe(&client->dev, PTR_ERR(regmap),
+				     "Failed to initialize regmap\n");
+
+	sitdev = devm_kzalloc(&client->dev, sizeof(*sitdev), GFP_KERNEL);
+	if (!sitdev)
+		return -ENOMEM;
+
+	sitdev->dev = &client->dev;
+	sitdev->client = client;
+	sitdev->regmap = regmap;
+	sitdev->page = -1;
+	rc = devm_mutex_init(&client->dev, &sitdev->page_lock);
+	if (rc)
+		return rc;
+	i2c_set_clientdata(client, sitdev);
+
+	return sit9531x_dev_probe(sitdev);
+}
+
+static const struct of_device_id sit9531x_of_match[] = {
+	{ .compatible = "sitime,sit95316" },
+	{ .compatible = "sitime,sit95317" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, sit9531x_of_match);
+
+/*
+ * Stop the poll and the IRQ across suspend so no paged I2C sequence reaches a
+ * suspended adapter; the poll worker is not freezable.
+ */
+static int sit9531x_suspend(struct device *dev)
+{
+	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+	if (sitdev->irq > 0)
+		disable_irq(sitdev->irq);
+	kthread_cancel_delayed_work_sync(&sitdev->work);
+
+	return 0;
+}
+
+static int sit9531x_resume(struct device *dev)
+{
+	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+
+	/* After power loss the part is on page 0, not the cached page */
+	sit9531x_page_cache_drop(sitdev);
+	if (sitdev->irq > 0)
+		enable_irq(sitdev->irq);
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(sit9531x_pm_ops,
+				sit9531x_suspend, sit9531x_resume);
 
 static struct i2c_driver sit9531x_i2c_driver = {
 	.driver			= {
 		.name		= "sit9531x",
 		.of_match_table	= sit9531x_of_match,
+		.pm		= pm_sleep_ptr(&sit9531x_pm_ops),
 	},
 	.probe		= sit9531x_i2c_probe,
 };
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 7a81c9b14b7f..9e873432df3d 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -15,6 +15,8 @@
 
 #include <linux/gpio/consumer.h>
 #include <linux/i2c.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
 #include <linux/mutex.h>
 #include <linux/regmap.h>
 #include <linux/types.h>
@@ -32,6 +34,20 @@
  */
 #define SIT9531X_INTSYNC_PIN_ID		(SIT9531X_MAX_INPUTS + 1)
 #define SIT9531X_INTSYNC_OUT_PIN_ID	SIT9531X_MAX_OUTPUTS
+#define SIT9531X_STATUS_POLL_MS		500
+/*
+ * Consecutive polls with no answer after which the poll period stops doubling:
+ * eight periods, 4 s.
+ */
+#define SIT9531X_POLL_STRETCH_MAX	3
+
+/* selected_ref value when the active source is not a registered input */
+#define SIT9531X_REF_INVALID		0xFF
+
+/* SiTime IEEE OUI for EUI-64 generation */
+#define SIT9531X_OUI			0x0090C2FFFEULL
+
+struct sit9531x_dpll;
 
 /*
  * struct sit9531x_chip_info - chip variant identification
@@ -62,36 +78,104 @@ enum sit9531x_signal_mode {
 /*
  * struct sit9531x_ref - input reference state
  * @freq: configured frequency in Hz
+ * @enabled: input receiver is on
+ * @pll_mask: PLLs this input feeds (bit 0 = PLLA)
  * @sig_mode: signal mode of the input pair, read from CLKINx_INPUT_MODE at
  *   probe
  */
 struct sit9531x_ref {
 	u64		freq;
+	bool		enabled;
+	u8		pll_mask;
 	enum sit9531x_signal_mode	sig_mode;
 };
 
 /*
  * struct sit9531x_out - output state
  * @freq: output frequency in Hz
+ * @enabled: output is driving, not forced into Hi-Z
+ * @cmos: single-ended; the CMOS pad Hi-Z pairs apply, not the differential one
+ * @part_muted: single-ended with one pad forced and the other not; reported
+ *   as driven. The driver's mute forces both pads.
+ * @routed: mapped to @pll_idx by the initial configuration; an unrouted
+ *   output has no pin
+ * @pll_idx: PLL driving this output (0-3)
  */
 struct sit9531x_out {
 	u64		freq;
+	bool		enabled;
+	bool		cmos;
+	bool		part_muted;
+	bool		routed;
+	u8		pll_idx;
+};
+
+/*
+ * struct sit9531x_chan - per-PLL channel state
+ * @active: PLL reached its active state; an unused PLL never does and its
+ *   loss-of-lock bit stays clear
+ * @locked: PLL is active and its outer loop reports lock
+ * @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
+ * @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
+ * @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;
+ *   the table is built from it
+ * @cfg_known: bit per canonical source that has a @cfg_prio
+ * @seen_srcs: table last written or read into @cfg_prio; a read-back that
+ *   differs reloads @cfg_prio from hardware
+ * @seen_valid: @seen_srcs holds a table
+ */
+struct sit9531x_chan {
+	bool		active;
+	bool		locked;
+	bool		stale;
+	u8		mode;
+	u8		selected_ref;
+	bool		inner_lol;
+	bool		ho_freeze;
+	bool		ho_valid;
+	bool		manual_input;
+	u16		prio_mask;
+	u8		cfg_prio[SIT9531X_PRIO_NUM_SRC];
+	u16		cfg_known;
+	u8		seen_srcs[SIT9531X_PRIO_MAX_SLOTS];
+	bool		seen_valid;
 };
 
 /*
  * struct sit9531x_dev - SiT9531x device instance
+ * @info: chip variant info
  * @dev: parent device
  * @client: I2C client backing @regmap
  * @regmap: the 256-byte register window
- * @info: chip variant info
+ * @dplls: registered DPLL devices
  * @multiop_lock: serializes multi-register operations
  * @page_lock: held across a page select and its transfer; nests inside
  *   @multiop_lock
  * @page: current page, -1 when unknown
  * @ref: input reference states
  * @out: output states
+ * @chan: per-PLL channel states
  * @xtal_freq: crystal frequency in Hz
+ * @kworker: poll worker
+ * @work: poll work
+ * @poll_fails: consecutive failed polls, up to SIT9531X_POLL_STRETCH_MAX;
+ *   each doubles the period
+ * @clock_id: EUI-64 clock identifier
  * @reset_gpio: optional reset line, NULL if absent
+ * @irq: optional INTRB IRQ, 0 if not wired
+ * @intsync_src: PLL sourcing INTSYNC, or -1
+ * @irq_ack_fails: consecutive failed INTRB acknowledgements
  */
 struct sit9531x_dev {
 	struct device			*dev;
@@ -104,13 +188,42 @@ struct sit9531x_dev {
 	struct mutex			page_lock;
 	int				page;
 
+	u8			irq_ack_fails;
 	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];
 	u32			xtal_freq;
 
+	struct list_head	dplls;
+
+	struct kthread_worker		*kworker;
+	struct kthread_delayed_work	work;
+	u8				poll_fails;
+
+	u64			clock_id;
+
 	struct gpio_desc	*reset_gpio;
+	int			irq;
+
+	s8			intsync_src;
 };
 
+int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		     u8 *val);
+int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg,
+		      u8 val);
+int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			 u8 offset, u8 *val);
+int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			  u8 offset, u8 val);
+int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx,
+			   u8 offset, u8 mask, u8 val);
+
+static inline u8 sit9531x_pll_page(u8 pll_idx)
+{
+	return SIT9531X_PAGE_PLLA + pll_idx;
+}
+
 /*
  * Input index 2n is the P lane and 2n + 1 the N lane of pair n; index
  * SIT9531X_MAX_INPUTS is the XO.
@@ -126,19 +239,52 @@ static inline bool sit9531x_input_is_n(u8 index)
 	return index & 1;
 }
 
-/* Register page of PLL @pll_idx (0 = PLLA, 3 = PLLD). */
-static inline u8 sit9531x_pll_page(u8 pll_idx)
+static inline u8 sit9531x_input_hw_src(u8 index)
 {
-	return SIT9531X_PAGE_PLLA + pll_idx;
+	if (index == SIT9531X_MAX_INPUTS)
+		return SIT9531X_PRIO_SRC_OCXO;
+	if (index == SIT9531X_INTSYNC_PIN_ID)
+		return SIT9531X_PRIO_SRC_INTSYNC;
+	if (sit9531x_input_is_n(index))
+		return SIT9531X_PRIO_SRC_N_BASE + sit9531x_input_pair(index);
+	return sit9531x_input_pair(index);
+}
+
+static inline u8 sit9531x_hw_src_input(u8 src)
+{
+	if (src < SIT9531X_NUM_INPUT_PAIRS)
+		return src * 2;
+	if (src == SIT9531X_PRIO_SRC_OCXO)
+		return SIT9531X_MAX_INPUTS;
+	if (src == SIT9531X_PRIO_SRC_INTSYNC)
+		return SIT9531X_INTSYNC_PIN_ID;
+	if (src >= SIT9531X_PRIO_SRC_N_BASE &&
+	    src < SIT9531X_PRIO_SRC_N_BASE + SIT9531X_NUM_INPUT_PAIRS)
+		return (src - SIT9531X_PRIO_SRC_N_BASE) * 2 + 1;
+	return SIT9531X_REF_INVALID;
+}
+
+static inline bool sit9531x_prio_src_usable(u8 src)
+{
+	return sit9531x_hw_src_input(src) != SIT9531X_REF_INVALID;
 }
 
-int sit9531x_read_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 *val);
-int sit9531x_write_u8(struct sit9531x_dev *sitdev, unsigned int reg, u8 val);
-int sit9531x_read_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
-			 u8 *val);
-int sit9531x_write_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
-			  u8 val);
-int sit9531x_update_pll_u8(struct sit9531x_dev *sitdev, u8 pll_idx, u8 offset,
-			   u8 mask, u8 val);
+static inline const struct sit9531x_ref *
+sit9531x_ref_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+	return &sitdev->ref[index];
+}
+
+static inline const struct sit9531x_out *
+sit9531x_out_state_get(const struct sit9531x_dev *sitdev, u8 index)
+{
+	return &sitdev->out[index];
+}
+
+static inline const struct sit9531x_chan *
+sit9531x_chan_state_get(const struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	return &sitdev->chan[pll_idx];
+}
 
 #endif /* _SIT9531X_CORE_H */
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
new file mode 100644
index 000000000000..6d676fabf13e
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * SiTime SiT9531x DPLL subsystem callbacks and registration
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device ops, pin ops (separate input/output), pin registration,
+ * and periodic change detection.
+ */
+
+#include <linux/dpll.h>
+#include <linux/err.h>
+#include <linux/kthread.h>
+#include <linux/list.h>
+#include <linux/netlink.h>
+#include <linux/slab.h>
+
+#include "core.h"
+#include "dpll.h"
+#include "prop.h"
+#include "regs.h"
+
+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_xo_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       pin->id == SIT9531X_MAX_INPUTS;
+}
+
+/*
+ * 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).
+ */
+static int
+sit9531x_dpll_lock_status_get(const struct dpll_device *dpll, void *dpll_priv,
+			      enum dpll_lock_status *status,
+			      enum dpll_lock_status_error *status_error,
+			      struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_chan *chan;
+
+	if (status_error)
+		*status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+
+	chan = sit9531x_chan_state_get(sitdev, sitdpll->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	if (chan->stale) {
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+		if (status_error)
+			*status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+	} else if (!chan->active) {
+		/*
+		 * An unused PLL never goes active and its loss-of-lock bit is
+		 * not driven.
+		 */
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	} else if (chan->inner_lol) {
+		/*
+		 * The core reports the error detail only with an unlocked or
+		 * holdover status.
+		 */
+		*status = chan->ho_freeze ? DPLL_LOCK_STATUS_HOLDOVER :
+					    DPLL_LOCK_STATUS_UNLOCKED;
+	} else if (chan->mode) {
+		/*
+		 * In free-run the outer loop is disabled and its loss-of-lock
+		 * bit is meaningless.
+		 */
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	} else if (chan->ho_freeze) {
+		/*
+		 * 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) {
+		if (chan->ho_valid)
+			*status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ;
+		else
+			*status = DPLL_LOCK_STATUS_LOCKED;
+	} else {
+		*status = DPLL_LOCK_STATUS_UNLOCKED;
+	}
+
+	if (status_error && chan->inner_lol)
+		*status_error = DPLL_LOCK_STATUS_ERROR_UNDEFINED;
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * Record the lock status the create notification carried as the poll baseline.
+ * Call once the device is registered.
+ */
+void sit9531x_dpll_lock_status_baseline(struct sit9531x_dpll *sitdpll)
+{
+	sit9531x_dpll_lock_status_get(sitdpll->dpll_dev, sitdpll,
+				      &sitdpll->lock_status,
+				      &sitdpll->lock_status_error, NULL);
+}
+
+/*
+ * Report AUTOMATIC for a PLL on its priority table, also on manual active
+ * select, which uses the same nibble, and MANUAL on manual input select
+ * (MISCINNER_PLL bit 5), where the select pins or GPIO_INPUT_FUNC_CTRL5..8
+ * choose the input. Free-run (PLL page reg 0x31[5]) shows in the lock status.
+ */
+static int
+sit9531x_dpll_mode_get(const struct dpll_device *dpll, void *dpll_priv,
+		       enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+
+	/* Read once at probe, before the device is registered. */
+	if (sit9531x_chan_state_get(sitdpll->dev, sitdpll->id)->manual_input)
+		*mode = DPLL_MODE_MANUAL;
+	else
+		*mode = DPLL_MODE_AUTOMATIC;
+
+	return 0;
+}
+
+static int
+sit9531x_dpll_supported_modes_get(const struct dpll_device *dpll,
+				  void *dpll_priv, unsigned long *modes,
+				  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+
+	if (sit9531x_chan_state_get(sitdpll->dev, sitdpll->id)->manual_input)
+		__set_bit(DPLL_MODE_MANUAL, modes);
+	else
+		__set_bit(DPLL_MODE_AUTOMATIC, modes);
+
+	return 0;
+}
+
+const struct dpll_device_ops sit9531x_dpll_device_ops = {
+	.lock_status_get	= sit9531x_dpll_lock_status_get,
+	.mode_get		= sit9531x_dpll_mode_get,
+	.supported_modes_get	= sit9531x_dpll_supported_modes_get,
+};
+
+static int
+sit9531x_dpll_input_pin_direction_get(const struct dpll_pin *pin,
+				      void *pin_priv,
+				      const struct dpll_device *dpll,
+				      void *dpll_priv,
+				      enum dpll_pin_direction *direction,
+				      struct netlink_ext_ack *extack)
+{
+	*direction = DPLL_PIN_DIRECTION_INPUT;
+	return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+};
+
+/*
+ * INTSYNC is the inter-PLL sync line: one PLL drives it and other PLLs may lock
+ * to it. Each DPLL registers a source pin to drive the line and a destination
+ * pin to lock to it.
+ */
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv,
+				       enum dpll_pin_direction *direction,
+				       struct netlink_ext_ack *extack);
+
+/* The XO feeds every PLL and is always connected. */
+
+static int
+sit9531x_dpll_xo_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)
+{
+	*state = DPLL_PIN_STATE_CONNECTED;
+	return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_xo_pin_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_xo_pin_state_on_dpll_get,
+};
+
+static int
+sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
+				       void *pin_priv,
+				       const struct dpll_device *dpll,
+				       void *dpll_priv,
+				       enum dpll_pin_direction *direction,
+				       struct netlink_ext_ack *extack)
+{
+	*direction = DPLL_PIN_DIRECTION_OUTPUT;
+	return 0;
+}
+
+static const struct dpll_pin_ops sit9531x_dpll_output_pin_ops = {
+	.direction_get		= sit9531x_dpll_output_pin_direction_get,
+};
+
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
+{
+	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;
+	return &sit9531x_dpll_input_pin_ops;
+}
+
+/*
+ * Send notifications for changed lock status and pin state; called from
+ * sit9531x_dev_periodic_work().
+ */
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
+{
+	enum dpll_lock_status_error status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	enum dpll_lock_status lock_status;
+	struct sit9531x_dpll_pin *pin;
+	int rc;
+
+	rc = sit9531x_dpll_lock_status_get(sitdpll->dpll_dev, sitdpll,
+					   &lock_status, &status_error, NULL);
+	if (rc) {
+		dev_err(sitdev->dev, "Failed to get DPLL%u lock status: %d\n",
+			sitdpll->id, rc);
+		return;
+	}
+
+	/*
+	 * The core publishes the error detail with the status, so a change in
+	 * either is notified.
+	 */
+	if (sitdpll->lock_status != lock_status ||
+	    sitdpll->lock_status_error != status_error) {
+		sitdpll->lock_status = lock_status;
+		sitdpll->lock_status_error = status_error;
+		sitdpll->ntf_pending = true;
+	}
+
+	if (sitdpll->ntf_pending && !dpll_device_change_ntf(sitdpll->dpll_dev))
+		sitdpll->ntf_pending = false;
+
+	list_for_each_entry(pin, &sitdpll->pins, list) {
+		const struct dpll_pin_ops *ops;
+		enum dpll_pin_state state;
+		bool changed;
+
+		/*
+		 * Only references and the INTSYNC destination change state
+		 * without a request.
+		 */
+		if (!sit9531x_dpll_is_input_pin(pin) ||
+		    sit9531x_dpll_is_xo_pin(pin))
+			continue;
+
+		ops = sit9531x_dpll_pin_ops_get(pin);
+		rc = ops->state_on_dpll_get(pin->dpll_pin, pin,
+					    sitdpll->dpll_dev, sitdpll,
+					    &state, NULL);
+		if (rc)
+			continue;
+
+		changed = pin->seen && state != pin->pin_state;
+		pin->pin_state = state;
+		pin->seen = true;
+		if (changed) {
+			dev_dbg(sitdev->dev, "%s state changed\n", pin->label);
+			dpll_pin_change_ntf(pin->dpll_pin);
+		}
+	}
+}
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
new file mode 100644
index 000000000000..6c02aa131328
--- /dev/null
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * SiTime SiT9531x DPLL subsystem interface
+ *
+ * Copyright (C) 2026 SiTime Corp.
+ * Author: Ali Rouhi <arouhi@sitime.com>
+ * Author: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
+ *
+ * DPLL device and pin structures, and function declarations for
+ * the DPLL registration and callback layer.
+ */
+
+#ifndef _SIT9531X_DPLL_H
+#define _SIT9531X_DPLL_H
+
+#include <linux/dpll.h>
+#include <linux/list.h>
+#include <linux/types.h>
+
+struct sit9531x_dev;
+
+struct sit9531x_dpll_pin {
+	struct list_head		list;
+	struct sit9531x_dpll		*dpll;
+	struct dpll_pin			*dpll_pin;
+	dpll_tracker			tracker;
+	struct fwnode_handle		*fwnode;
+	char				label[8];	/* "IN0", "OUT3" */
+	enum dpll_pin_direction		dir;
+	u8				id;		/* hardware index */
+	u8				prio;
+	enum dpll_pin_state		pin_state;
+	bool				seen;	/* baseline taken by the poll */
+};
+
+struct sit9531x_dpll {
+	struct list_head		list;
+	struct sit9531x_dev		*dev;
+	struct dpll_device		*dpll_dev;
+	dpll_tracker			tracker;
+	struct dpll_device_ops		ops;	/* per-instance copy */
+	struct list_head		pins;
+	u8				id;	/* 0 = PLLA .. 3 = PLLD */
+	enum dpll_lock_status		lock_status;
+	enum dpll_lock_status_error	lock_status_error;
+	bool				ntf_pending;
+};
+
+extern const struct dpll_device_ops sit9531x_dpll_device_ops;
+const struct dpll_pin_ops *
+sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin);
+
+struct sit9531x_dpll *sit9531x_dpll_alloc(struct sit9531x_dev *sitdev, u8 ch);
+void sit9531x_dpll_free(struct sit9531x_dpll *sitdpll);
+int  sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
+
+void sit9531x_dpll_lock_status_baseline(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
+
+#endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 3a6151f44d61..832d0360a398 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -30,6 +30,211 @@
 /* Reg 0x03 holds an unrelated revision byte, not part of the variant ID */
 #define SIT9531X_REG_VARIANT_ID		SIT9531X_REG(0x00, 0x02)
 
+/* PLL inner loop loss-of-lock */
+#define SIT9531X_REG_PLL_INNER_LOL_STATUS	SIT9531X_REG(0x00, 0x92)
+#define SIT9531X_REG_PLL_INNER_LOL_NOTIF	SIT9531X_REG(0x00, 0x93)
+
+/* Clock monitor PLL / XO status */
+#define SIT9531X_REG_CMON_NOTIF		SIT9531X_REG(0x00, 0x9E)
+
+/* PLL outer-loop loss-of-lock */
+#define SIT9531X_REG_OUTER_LOL_STATUS	SIT9531X_REG(0x00, 0x06)
+#define SIT9531X_REG_OUTER_LOL_NOTIF		SIT9531X_REG(0x00, 0x07)
+
+/* PLL holdover freeze status */
+#define SIT9531X_REG_HO_FREEZE_STATUS	SIT9531X_REG(0x00, 0x0A)
+#define SIT9531X_REG_HO_FREEZE_NOTIF	SIT9531X_REG(0x00, 0x0B)
+
+/* INTSYNC (inter-PLL synchronization) global enable */
+#define SIT9531X_REG_INTSYNC_GLOBAL		SIT9531X_REG(0x00, 0x40)
+#define SIT9531X_INTSYNC_EN_BIT		6
+
+/*
+ * Priority table: six registers per PLL from 0x16 + 6 * PLL, holding slots
+ * 0-10 and the active-reference nibble; slot 2n in [7:4], 2n+1 in [3:0].
+ *
+ * Source encoding (4-bit):
+ *   0-3 = IN0P-IN3P, 5 = OCXO, 6 = INTSYNC, 7-10 = IN0N-IN3N
+ *
+ * Codes 4 and 11 take the place of a fifth input pair; they read back as no
+ * valid reference.
+ */
+#define SIT9531X_PAGE_PRIOSYS		0x01
+#define SIT9531X_PRIO_BASE_REG		0x16
+#define SIT9531X_PRIO_REGS_PER_PLL		6
+#define SIT9531X_PRIO_SLOTS_PER_REG		2
+/*
+ * CLK_SPARE0SEL_PLL..CLK_SPARE10SEL_PLL; the twelfth nibble is
+ * CLK_ACTIVESEL_PLL.
+ */
+#define SIT9531X_PRIO_MAX_SLOTS		11
+/* Number of source encodings (0-11), unrelated to the slot count */
+#define SIT9531X_PRIO_NUM_SRC		12
+#define SIT9531X_PRIO_NIBBLE_MASK		0x0F
+#define SIT9531X_PRIO_HI_SHIFT		4
+/* Codes 4 and 11 both mean no source; the driver writes 4 to empty a slot. */
+#define SIT9531X_PRIO_SRC_NONE			4
+#define SIT9531X_PRIO_SRC_OCXO		5
+#define SIT9531X_PRIO_SRC_INTSYNC		6
+#define SIT9531X_PRIO_SRC_N_BASE		7
+/*
+ * Low nibble of the last priority register: CLK_ACTIVESEL_PLL, the PLL's active
+ * reference, in the same encoding.
+ */
+#define SIT9531X_PRIO_ACTIVESEL_OFF		5
+
+/*
+ * PRG_Directives_GENERIC_0; every page has a copy at offset 0x0F:
+ *   bit 6  proceed from the PRG_CMD state to loop lock / active state
+ *   bit 4  update the NVM bank from the efuse
+ *   bit 3  read the efuse into the volatile registers
+ *   bit 2  program the efuse (the driver never issues it)
+ *   bit 1  small change update
+ *   bit 0  escape to the PRG_CMD state
+ * PRG_CMD sequences close with bit 4, as the documented sequences do.
+ */
+#define SIT9531X_REG_GLOBAL_UPDATE		SIT9531X_REG(0x00, 0x0F)
+#define SIT9531X_SMALL_UPDATE_CMD		0x02
+
+/* 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)
+#define SIT9531X_REG_IN_SEP_FORCE		SIT9531X_REG(0x02, 0xEA)
+#define SIT9531X_REG_IN_SEP_STATE		SIT9531X_REG(0x02, 0xEB)
+#define SIT9531X_REG_IN_SEN_FORCE		SIT9531X_REG(0x02, 0xF2)
+#define SIT9531X_REG_IN_SEN_STATE		SIT9531X_REG(0x02, 0xF3)
+
+/*
+ * SE_P_EN/SE_N_EN set: that lane is single-ended; both clear: the pair is
+ * differential.
+ */
+#define SIT9531X_REG_IN_MODE(_pair)		\
+	SIT9531X_REG(0x02, 0x1B + 0x10 * (_pair))
+#define SIT9531X_IN_MODE_SE_P_EN		BIT(0)
+#define SIT9531X_IN_MODE_SE_N_EN		BIT(1)
+
+#define SIT9531X_REG_HIZ_DIFF_07_MASK	SIT9531X_REG(0x03, 0xF2)
+#define SIT9531X_REG_HIZ_DIFF_07_STATE	SIT9531X_REG(0x03, 0xF3)
+#define SIT9531X_REG_HIZ_DIFF_811_MASK	SIT9531X_REG(0x03, 0xF4)
+#define SIT9531X_REG_HIZ_DIFF_811_STATE	SIT9531X_REG(0x03, 0xF5)
+#define SIT9531X_REG_HIZ_SE_07_MASK		SIT9531X_REG(0x03, 0xF8)
+#define SIT9531X_REG_HIZ_SE_07_STATE		SIT9531X_REG(0x03, 0xF9)
+#define SIT9531X_REG_HIZ_SE_811_MASK		SIT9531X_REG(0x03, 0xFA)
+#define SIT9531X_REG_HIZ_SE_811_STATE	SIT9531X_REG(0x03, 0xFB)
+/*
+ * The SE pairs force the OutP pad of a CMOS output; the OutN pad uses 0xF6/0xF7
+ * for slots 0-7 and 0xF4/0xF5 bits 7:4 for slots 8-11.
+ */
+#define SIT9531X_REG_HIZ_SEN_07_MASK	SIT9531X_REG(0x03, 0xF6)
+#define SIT9531X_REG_HIZ_SEN_07_STATE	SIT9531X_REG(0x03, 0xF7)
+#define SIT9531X_HIZ_SEN_811_SHIFT		4
+
+/*
+ * Either CMOS enable set: single-ended (one lane, or both as CMOS); neither:
+ * differential.
+ */
+#define SIT9531X_OUT_MISC0_BASE		0x1E
+#define SIT9531X_OUT_MISC0_STRIDE		0x10
+#define SIT9531X_OUT_CMOS_ENP		BIT(3)
+#define SIT9531X_OUT_CMOS_ENN		BIT(2)
+
+/*
+ * Output divider: 34 bits in 5 bytes, LSB at the slot base, MSB in base-4
+ * [1:0]. Slots 0-5 on page 3, 6-11 on page 4; clkout_map[] gives the slot.
+ */
+#define SIT9531X_PAGE_OUTSYS0_SLOT_MAX	5   /* slots 0-5 on Page 0x03 */
+
+#define SIT9531X_REG_PRG_DIR_GEN		SIT9531X_REG(0x03, 0x0F)
+#define SIT9531X_PRG_CMD_STATE		0x01
+#define SIT9531X_UPDATE_NVM			0x10
+#define SIT9531X_LOOP_LOCK			0x40
+
+/* Debug register (same offset, per-page) */
+#define SIT9531X_REG_OUTSYS_DEBUG		SIT9531X_REG(0x03, 0xBD)
+#define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
+
+/*
+ * Register-triggered phase flush: DIVO_PHASE_SEL_REG selects the register
+ * trigger; pulsing DIVO_PHASE_TRIG high then low flushes. Preserve the OEb
+ * trigger bits [7:6].
+ */
+#define SIT9531X_REG_GPIO_FUNC_CTRL1	SIT9531X_REG(0x00, 0x65)
+#define SIT9531X_DIVO_PHASE_SEL_REG	BIT(5)
+#define SIT9531X_DIVO_PHASE_TRIG	BIT(4)
+
+/* PLL page register offsets, pages 0x0A-0x0D */
+#define SIT9531X_PLL_REG_SMALL_UPDATE	0x0F
+
+/*
+ * 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.
+ */
+
+/*
+ * Output-enable mask, twelve bits: bits 0-7 in LO, bits 8-11 in HI[3:0].
+ * OUTn is bit n on PLLA and PLLB and bit 11 - n on PLLC and PLLD.
+ */
+#define SIT9531X_PLL_REG_OUT_MAP_HI		0x27
+#define SIT9531X_PLL_REG_OUT_MAP_LO		0x28
+#define SIT9531X_PLL_REG_STATUS		0x31
+
+#define SIT9531X_PLL_REG_ACTIVE		0x02
+#define SIT9531X_PLL_ACTIVE_BIT		BIT(0)  /* PLL reached active state */
+
+#define SIT9531X_PLL_STATUS_OUTER_DIS	BIT(5)
+
+/*
+ * PLL_CONFIG1F_PLL bit 6: manual active select, no priority arbitration. The
+ * register is GUI generated; the driver only reads it.
+ */
+#define SIT9531X_PLL_REG_CONFIG1F		0x1F
+#define SIT9531X_PLL_CONFIG1F_MANUAL_SEL	BIT(6)
+
+/*
+ * MISCINNER_PLL bit 5: in manual active select, use the manual input select.
+ * The register is GUI generated; the driver only reads it.
+ */
+#define SIT9531X_PLL_REG_MISCINNER		0x18
+#define SIT9531X_PLL_MISCINNER_MAN_IN_SEL	BIT(5)
+
+/*
+ * GPIO_INPUT_FUNC_CTRL5..8, one per PLL: with bit 4 set the manual input is the
+ * low nibble, otherwise the input-select pins. Bit 6 enables the indirect
+ * choice and bit 5 picks active or spare; the driver does not decode them.
+ */
+#define SIT9531X_REG_MAN_IN_SEL(_pll)	SIT9531X_REG(0x00, 0xE8 + (_pll))
+#define SIT9531X_MAN_IN_SEL_FROM_REG		BIT(4)
+#define SIT9531X_MAN_IN_SEL_MASK		GENMASK(3, 0)
+
+/*
+ * HO_VALID: holdover memory has been acquired. HO_FREEZE (page 0, reg 0x0A)
+ * means the PLL is in holdover.
+ */
+#define SIT9531X_PLL_REG_STATUS_1		0x06
+#define SIT9531X_PLL_STATUS_1_HO_VALID	BIT(2)
+
+#define SIT9531X_CLKMON_P_NOTIF_01		SIT9531X_REG(0x06, 0x03)
+#define SIT9531X_CLKMON_P_NOTIF_23		SIT9531X_REG(0x06, 0x07)
+
+#define SIT9531X_CLKMON_N_NOTIF_01		SIT9531X_REG(0x06, 0x93)
+#define SIT9531X_CLKMON_N_NOTIF_23		SIT9531X_REG(0x06, 0x97)
+
+/*
+ * The device compares the CRC stored in the EEPROM with the one it computes.
+ * NOTIFY_4 bits are sticky; a good load leaves only the read-done bit set.
+ */
+/* 4 bytes each, MSB first */
+#define SIT9531X_REG_REC_CRC		SIT9531X_REG(0x00, 0x8A)
+#define SIT9531X_REG_CAL_CRC		SIT9531X_REG(0x00, 0x8E)
+#define SIT9531X_REG_EEPROM_NOTIF		SIT9531X_REG(0x00, 0x97)
+#define SIT9531X_EEPROM_READ_DONE		BIT(0)
+
+/* Profile identifier of the loaded configuration, page 1: 3 bytes, LSB first */
+#define SIT9531X_REG_PROFILE_ID		SIT9531X_REG(0x01, 0x44)
+
+/* Consecutive failed INTRB acknowledgements before the IRQ is disabled */
+#define SIT9531X_IRQ_ACK_TRIES			8
+
 #define SIT9531X_VARIANT_ID_95317	0x17
 #define SIT9531X_VARIANT_ID_95316	0x31
 
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 07/13] dpll: sit9531x: implement input pin state on a DPLL
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (5 preceding siblings ...)
  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 ` Ali Rouhi
  2026-11-10 20:26 ` [PATCH net-next v15 09/13] dpll: sit9531x: add support to get and set frequency on pins Ali Rouhi
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 09/13] dpll: sit9531x: add support to get and set frequency on pins
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (6 preceding siblings ...)
  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
  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
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 08/13] dpll: sit9531x: add support to get and set priority on input pins
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (7 preceding siblings ...)
  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 ` Ali Rouhi
  2026-11-10 20:26 ` [PATCH net-next v15 10/13] dpll: sit9531x: implement output pin state on a DPLL Ali Rouhi
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Keep a priority per input and PLL, seeded from the table the part
loaded, and rebuild the table from it on a set. Priority and state are
separate: a priority set on a disconnected input applies when it is
connected. The range is 0 to 255 over eleven slots; inputs of equal
priority keep their table order.

A table rewritten outside the driver, by a profile reload or an I2C
tool, becomes the new configuration, and the poll notifies the pins
whose priority moved.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c |  52 ++++++++++-
 drivers/dpll/sit9531x/core.h |   2 +
 drivers/dpll/sit9531x/dpll.c | 163 +++++++++++++++++++++++++++++++----
 drivers/dpll/sit9531x/dpll.h |   4 +-
 drivers/dpll/sit9531x/prop.c |   2 +
 5 files changed, 201 insertions(+), 22 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index af6d7918958d..b4e44f0d9820 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -605,6 +605,33 @@ bool sit9531x_input_prio_present(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return !!(sitdev->chan[pll_idx].prio_mask & BIT(input_idx));
 }
 
+/*
+ * @input_idx is in hardware encoding (see sit9531x_input_hw_src()). A source
+ * the PLL has never listed reports the last slot index, 10. Caller must hold
+ * sitdev->multiop_lock.
+ */
+int sit9531x_input_prio_get(struct sit9531x_dev *sitdev, u8 pll_idx,
+			    u8 input_idx, u8 *prio)
+{
+	const 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->cfg_known & BIT(input_idx))
+		*prio = chan->cfg_prio[input_idx];
+	else
+		*prio = SIT9531X_PRIO_MAX_SLOTS - 1;
+
+	return 0;
+}
+
 /*
  * Take the configured priorities from the hardware table: a listed source gets
  * the first slot it occupies.
@@ -735,7 +762,8 @@ static u8 sit9531x_prio_top_live(const struct sit9531x_dev *sitdev,
  * until it loses signal.
  */
 static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
-				       const u8 *old, const u8 *srcs, u8 cur)
+				       const u8 *old, const u8 *srcs, u8 cur,
+				       u8 routed)
 {
 	u8 top, i;
 
@@ -744,6 +772,12 @@ static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
 	    top != sit9531x_prio_top_live(sitdev, old))
 		return top;
 
+	if (sit9531x_prio_src_usable(routed) &&
+	    sit9531x_prio_src_live(sitdev, routed))
+		for (i = 0; i < SIT9531X_PRIO_MAX_SLOTS; i++)
+			if (srcs[i] == routed)
+				return routed;
+
 	cur = sit9531x_prio_src_canon(sitdev, cur & SIT9531X_PRIO_NIBBLE_MASK);
 
 	if (sit9531x_prio_src_usable(cur) &&
@@ -826,12 +860,18 @@ static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
 
 	if (!empty) {
 		u8 sel = sit9531x_prio_slot_get(val, slot + 1);
+		u8 routed = SIT9531X_PRIO_SRC_NONE;
 
+		/*
+		 * The pick needs the signal state now, not as of the last poll.
+		 */
 		rc = sit9531x_input_mon_fetch(sitdev);
 		if (rc)
 			goto rollback;
+		if (!chan->stale && chan->routed_ref != SIT9531X_REF_INVALID)
+			routed = sit9531x_input_hw_src(chan->routed_ref);
 		sel = sit9531x_prio_activesel_pick(sitdev, chan->prio_srcs,
-						   srcs, sel);
+						   srcs, sel, routed);
 		val = sit9531x_prio_slot_set(val, slot + 1, sel);
 	}
 
@@ -2277,6 +2317,14 @@ static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
 			goto error;
 		}
 
+		/*
+		 * Taken before the pin is published; see
+		 * sit9531x_dpll_pin_baseline().
+		 */
+		mutex_lock(&sitdpll->dev->multiop_lock);
+		sit9531x_dpll_pin_baseline(pin);
+		mutex_unlock(&sitdpll->dev->multiop_lock);
+
 		rc = sit9531x_dpll_pin_register(pin, index);
 		if (rc) {
 			kfree(pin);
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index c2fa6a17be43..fa779acf7de5 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -265,6 +265,8 @@ 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_get(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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index 56bdec4bcf67..f03145f4d1ee 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -34,6 +34,14 @@ sit9531x_dpll_is_xo_pin(const struct sit9531x_dpll_pin *pin)
 	       pin->id == SIT9531X_MAX_INPUTS;
 }
 
+/* Check if the pin's state and priority can change without a request */
+static bool
+sit9531x_dpll_is_selection_pin(const struct sit9531x_dpll_pin *pin)
+{
+	return sit9531x_dpll_is_input_pin(pin) &&
+	       !sit9531x_dpll_is_xo_pin(pin);
+}
+
 /*
  * A frozen PLL with an empty table is in forced holdover; report holdover only
  * while its holdover memory is valid.
@@ -396,11 +404,78 @@ sit9531x_dpll_input_pin_state_on_dpll_set(const struct dpll_pin *pin,
 	return rc;
 }
 
+/* Report the configured priority of the source on this PLL, listed or not */
+static int
+sit9531x_dpll_input_pin_prio_get(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll,
+				 void *dpll_priv, u32 *prio,
+				 struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 slot;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_get(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id), &slot);
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		return rc;
+
+	*prio = slot;
+	return 0;
+}
+
+/*
+ * Rewrite the Page 1 priority table for a listed pin; an unlisted pin keeps the
+ * priority until it is connected.
+ */
+static int
+sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+				 const struct dpll_device *dpll,
+				 void *dpll_priv, u32 prio,
+				 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;
+
+	if (dpin->dir != DPLL_PIN_DIRECTION_INPUT) {
+		NL_SET_ERR_MSG(extack, "Priority applies only to input pins");
+		return -EINVAL;
+	}
+
+	if (prio > U8_MAX) {
+		NL_SET_ERR_MSG(extack, "Priority out of range (0-255)");
+		return -EINVAL;
+	}
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_input_prio_set(sitdev, sitdpll->id,
+				     sit9531x_input_hw_src(dpin->id),
+				     (u8)prio);
+	if (!rc)
+		dpin->prio = prio;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "Failed to set input priority");
+		return rc;
+	}
+
+	return 0;
+}
+
 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,
+	.prio_get		= sit9531x_dpll_input_pin_prio_get,
+	.prio_set		= sit9531x_dpll_input_pin_prio_set,
 };
 
 /*
@@ -477,6 +552,43 @@ sit9531x_dpll_pin_ops_get(const struct sit9531x_dpll_pin *pin)
 	return &sit9531x_dpll_input_pin_ops;
 }
 
+/* Caller must hold sitdev->multiop_lock. */
+static void sit9531x_dpll_pin_snapshot(struct sit9531x_dpll *sitdpll,
+				       const struct sit9531x_dpll_pin *pin,
+				       enum dpll_pin_state *state,
+				       enum dpll_pin_operstate *operstate,
+				       u8 *prio)
+{
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	u8 id = pin->id;
+
+	if (id == SIT9531X_INTSYNC_PIN_ID && sitdev->intsync_src == sitdpll->id)
+		*state = DPLL_PIN_STATE_DISCONNECTED;
+	else
+		sit9531x_dpll_selection_state_get(sitdev, sitdpll, id, state);
+	sit9531x_dpll_selection_operstate_get(sitdev, sitdpll, id, operstate);
+	if (sit9531x_input_prio_get(sitdev, sitdpll->id,
+				    sit9531x_input_hw_src(id), prio))
+		*prio = pin->prio;
+}
+
+/*
+ * Take the baseline before the pin is registered, so the first poll notifies a
+ * change a request made. Caller must hold sitdev->multiop_lock.
+ */
+void sit9531x_dpll_pin_baseline(struct sit9531x_dpll_pin *pin)
+{
+	struct sit9531x_dpll *sitdpll = pin->dpll;
+
+	lockdep_assert_held(&sitdpll->dev->multiop_lock);
+
+	if (!sit9531x_dpll_is_selection_pin(pin))
+		return;
+
+	sit9531x_dpll_pin_snapshot(sitdpll, pin, &pin->pin_state,
+				   &pin->operstate, &pin->prio);
+}
+
 /*
  * Send notifications for changed lock status and pin state; called from
  * sit9531x_dev_periodic_work().
@@ -511,32 +623,45 @@ void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
 	if (sitdpll->ntf_pending && !dpll_device_change_ntf(sitdpll->dpll_dev))
 		sitdpll->ntf_pending = false;
 
+	mutex_lock(&sitdev->multiop_lock);
 	list_for_each_entry(pin, &sitdpll->pins, list) {
-		const struct dpll_pin_ops *ops;
+		enum dpll_pin_operstate operstate;
 		enum dpll_pin_state state;
 		bool changed;
+		u8 prio;
 
-		/*
-		 * Only references and the INTSYNC destination change state
-		 * without a request.
-		 */
-		if (!sit9531x_dpll_is_input_pin(pin) ||
-		    sit9531x_dpll_is_xo_pin(pin))
+		if (!sit9531x_dpll_is_selection_pin(pin))
 			continue;
 
-		ops = sit9531x_dpll_pin_ops_get(pin);
-		rc = ops->state_on_dpll_get(pin->dpll_pin, pin,
-					    sitdpll->dpll_dev, sitdpll,
-					    &state, NULL);
-		if (rc)
-			continue;
+		sit9531x_dpll_pin_snapshot(sitdpll, pin, &state, &operstate,
+					   &prio);
+
+		changed = state != pin->pin_state ||
+			  operstate != pin->operstate || prio != pin->prio;
+		if (changed)
+			dev_dbg(sitdev->dev,
+				"%s: state %u->%u operstate %u->%u prio %u->%u\n",
+				pin->label, pin->pin_state, state,
+				pin->operstate, operstate, pin->prio, prio);
 
-		changed = pin->seen && state != pin->pin_state;
 		pin->pin_state = state;
-		pin->seen = true;
-		if (changed) {
-			dev_dbg(sitdev->dev, "%s state changed\n", pin->label);
-			dpll_pin_change_ntf(pin->dpll_pin);
-		}
+		pin->operstate = operstate;
+		pin->prio = prio;
+
+		/*
+		 * The notification takes DPLL-subsystem locks the callbacks run
+		 * under; send it after dropping multiop_lock.
+		 */
+		if (changed)
+			pin->ntf_pending = true;
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+
+	list_for_each_entry(pin, &sitdpll->pins, list) {
+		if (!pin->ntf_pending)
+			continue;
+		if (dpll_pin_change_ntf(pin->dpll_pin))
+			continue;
+		pin->ntf_pending = false;
 	}
 }
diff --git a/drivers/dpll/sit9531x/dpll.h b/drivers/dpll/sit9531x/dpll.h
index 6c02aa131328..3ded177b871b 100644
--- a/drivers/dpll/sit9531x/dpll.h
+++ b/drivers/dpll/sit9531x/dpll.h
@@ -30,7 +30,8 @@ struct sit9531x_dpll_pin {
 	u8				id;		/* hardware index */
 	u8				prio;
 	enum dpll_pin_state		pin_state;
-	bool				seen;	/* baseline taken by the poll */
+	enum dpll_pin_operstate		operstate;
+	bool				ntf_pending;
 };
 
 struct sit9531x_dpll {
@@ -56,6 +57,7 @@ int  sit9531x_dpll_register(struct sit9531x_dpll *sitdpll);
 void sit9531x_dpll_unregister(struct sit9531x_dpll *sitdpll);
 
 void sit9531x_dpll_lock_status_baseline(struct sit9531x_dpll *sitdpll);
+void sit9531x_dpll_pin_baseline(struct sit9531x_dpll_pin *pin);
 void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll);
 
 #endif /* _SIT9531X_DPLL_H */
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index b9405eba49a3..69877fc94028 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -146,6 +146,7 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 		/* INTSYNC destination: a reference the PLL can lock to. */
 		props->dpll_props.type = DPLL_PIN_TYPE_INT_OSCILLATOR;
 		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
 			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
 		curr_freq = 0;
 	} else if (sync_out) {
@@ -161,6 +162,7 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 	} else if (dir == DPLL_PIN_DIRECTION_INPUT) {
 		props->dpll_props.type = DPLL_PIN_TYPE_EXT;
 		props->dpll_props.capabilities =
+			DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
 			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
 		curr_freq = sitdev->ref[index].freq;
 	} else {
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 10/13] dpll: sit9531x: implement output pin state on a DPLL
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (8 preceding siblings ...)
  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 ` 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
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

An output's state says whether it carries a signal: CONNECTED while it
drives, DISCONNECTED while muted. Setting it forces the output's pads to
Hi-Z, which leaves the divider alone. Output pins are registered from
this patch on.

A mute forces the slot's three Hi-Z pairs, differential, CMOS OutP and
CMOS OutN, inside the programming state; an un-mute releases the force
and the pads return to the loaded configuration. An output is reported
muted only when both pads are forced off.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 247 +++++++++++++++++++++++++++++------
 drivers/dpll/sit9531x/core.h |  10 ++
 drivers/dpll/sit9531x/dpll.c | 139 +++++++++++++++++++-
 3 files changed, 355 insertions(+), 41 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index c7ab33993c28..458de502bc8f 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -459,53 +459,42 @@ 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)
+/* Re-read an output's mute state. Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev, u8 out_idx)
 {
-	int rc = -EIO;
-	u8 attempt;
+	bool muted, part;
+	int rc;
 
-	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);
-	}
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_output_forced_hiz(sitdev, out_idx, &muted, &part);
 	if (rc)
-		dev_err(sitdev->dev,
-			"output loops left unlocked after a failed entry: %d\n",
-			rc);
+		return rc;
+
+	sitdev->out[out_idx].enabled = !muted;
+	sitdev->out[out_idx].part_muted = part;
+	sitdev->out[out_idx].state_stale = false;
 
-	sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
-			  SIT9531X_DEBUG_LOCK_VAL);
+	return 0;
 }
 
-/*
- * 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)
+static int sit9531x_hiz_set_bit(struct sit9531x_dev *sitdev,
+				unsigned int reg, u8 bit, bool set)
 {
+	u8 cur, new_val;
 	int rc;
 
-	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
-			       SIT9531X_DEBUG_UNLOCK_VAL);
+	rc = sit9531x_read_u8(sitdev, reg, &cur);
 	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;
-	}
+	new_val = set ? (cur | BIT(bit)) : (cur & ~BIT(bit));
 
-	return 0;
+	return sit9531x_write_u8(sitdev, reg, new_val);
 }
 
+#define SIT9531X_LOOP_LOCK_TRIES	3
+
 /*
  * Update the NVM shadow and re-lock the loops after sit9531x_prg_enter(). The
  * sleep is the settling time after loop lock.
@@ -554,6 +543,174 @@ static int sit9531x_prg_commit(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/*
+ * 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) {
+		sit9531x_write_close(sitdev, SIT9531X_REG_OUTSYS_DEBUG,
+				     SIT9531X_DEBUG_LOCK_VAL);
+		return rc;
+	}
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_PRG_DIR_GEN,
+			       SIT9531X_PRG_CMD_STATE);
+	if (rc) {
+		sit9531x_prg_commit(sitdev);
+		return rc;
+	}
+
+	return 0;
+}
+
+/*
+ * Mute or unmute every Hi-Z pair of a slot. Caller must already be in the
+ * programming state.
+ */
+static int sit9531x_output_hiz_write(struct sit9531x_dev *sitdev, u8 slot,
+				     bool mute)
+{
+	u8 mask[SIT9531X_HIZ_PAIRS], state[SIT9531X_HIZ_PAIRS];
+	struct sit9531x_hiz_pair p[SIT9531X_HIZ_PAIRS];
+	int i, rc, undo_rc;
+
+	sit9531x_output_get_hiz_regs(slot, p);
+
+	if (!mute) {
+		for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+			rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+						  false);
+			if (rc)
+				return rc;
+		}
+		return 0;
+	}
+
+	for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+		rc = sit9531x_read_u8(sitdev, p[i].state, &state[i]);
+		if (rc)
+			return rc;
+		rc = sit9531x_read_u8(sitdev, p[i].mask, &mask[i]);
+		if (rc)
+			return rc;
+	}
+
+	/*
+	 * STATE before MASK: enabling the override first can drive the pad for
+	 * one I2C transfer.
+	 */
+	for (i = 0; i < SIT9531X_HIZ_PAIRS; i++) {
+		rc = sit9531x_hiz_set_bit(sitdev, p[i].state, p[i].bit, false);
+		if (!rc)
+			rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+						  true);
+		if (rc)
+			break;
+	}
+	if (!rc)
+		return 0;
+
+	for (; i >= 0; i--) {
+		u8 bit = BIT(p[i].bit);
+
+		undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].mask, p[i].bit,
+					       mask[i] & bit);
+		if (!undo_rc)
+			undo_rc = sit9531x_hiz_set_bit(sitdev, p[i].state,
+						       p[i].bit,
+						       state[i] & bit);
+		if (undo_rc)
+			dev_err(sitdev->dev,
+				"slot%u: Hi-Z override left part applied (%d)\n",
+				slot, undo_rc);
+	}
+
+	return rc;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_disable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	int rc, ret, state_rc;
+	bool muted, part;
+	u8 slot;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_hiz_write(sitdev, slot, true);
+
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+
+	/* Read back the Hi-Z state */
+	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted, &part);
+	if (!state_rc) {
+		sitdev->out[index].enabled = !muted;
+		sitdev->out[index].part_muted = part;
+		sitdev->out[index].state_stale = false;
+	} else {
+		sitdev->out[index].state_stale = true;
+		if (!rc)
+			rc = state_rc;
+	}
+
+	return rc;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	int rc, ret, state_rc;
+	bool muted, part;
+	u8 slot;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (index >= info->num_outputs)
+		return -EINVAL;
+
+	slot = info->clkout_map[index];
+	rc = sit9531x_prg_enter(sitdev);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_hiz_write(sitdev, slot, false);
+
+	ret = sit9531x_prg_commit(sitdev);
+	if (ret && !rc)
+		rc = ret;
+
+	state_rc = sit9531x_output_forced_hiz(sitdev, index, &muted, &part);
+	if (!state_rc) {
+		sitdev->out[index].enabled = !muted;
+		sitdev->out[index].part_muted = part;
+		sitdev->out[index].state_stale = false;
+	} else {
+		sitdev->out[index].state_stale = true;
+		if (!rc)
+			rc = state_rc;
+	}
+
+	return rc;
+}
+
 /*
  * Procedure:
  *   1. Force PLL into holdover (PLL page reg 0x6F bit 4)
@@ -2084,6 +2241,7 @@ static int sit9531x_out_state_fetch(struct sit9531x_dev *sitdev, u8 index)
 		return rc;
 
 	sitdev->out[index].part_muted = part;
+	sitdev->out[index].state_stale = false;
 
 	/*
 	 * Each PLL page holds a 12-bit output-enable mask: OUT_MAP_LO bits 0-7
@@ -2854,7 +3012,7 @@ static void sit9531x_dpll_pin_unregister(struct sit9531x_dpll_pin *pin)
 	 * Clear before unregistering: callbacks test it under the DPLL device
 	 * lock, which dpll_pin_unregister() also takes.
 	 */
-	pin->dpll_pin = NULL;
+	WRITE_ONCE(pin->dpll_pin, NULL);
 
 	dpll_pin_unregister(sitdpll->dpll_dev, dpll_pin, ops, pin);
 	dpll_pin_put(dpll_pin, &pin->tracker);
@@ -2890,20 +3048,33 @@ static void sit9531x_dpll_pins_unregister(struct sit9531x_dpll *sitdpll)
 	}
 }
 
-/* Check if the pin class has a state callback, which the DPLL core requires */
+/*
+ * An output is registered when routed to this DPLL, whether or not it is
+ * driving.
+ */
 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)
+	if (dir == DPLL_PIN_DIRECTION_INPUT) {
+		if (index == SIT9531X_MAX_INPUTS)
+			return true;
+		if (index == SIT9531X_INTSYNC_PIN_ID)
+			return false;
+
+		return sit9531x_input_pin_is_registrable(sitdev, index);
+	}
+
+	if (index == SIT9531X_INTSYNC_OUT_PIN_ID)
 		return false;
 
-	if (index == SIT9531X_MAX_INPUTS)
-		return true;
+	if (index >= sitdev->info->num_outputs)
+		return false;
 
-	return sit9531x_input_pin_is_registrable(sitdev, index);
+	return sitdev->out[index].pll_idx == sitdpll->id &&
+	       sitdev->out[index].routed;
 }
 
 static int sit9531x_dpll_pins_register(struct sit9531x_dpll *sitdpll)
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 6b50b5b64b75..4f3f74ac8d7c 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -101,6 +101,9 @@ struct sit9531x_ref {
  * @freq: output frequency in Hz
  * @enabled: output is driving, not forced into Hi-Z
  * @cmos: single-ended; the CMOS pad Hi-Z pairs apply, not the differential one
+ * @state_stale: cached mute state must be read back before it is reported
+ * @ntf_pending: pin change not yet notified; the poll notifies once the state
+ *   is read
  * @part_muted: single-ended with one pad forced and the other not; reported
  *   as driven. The driver's mute forces both pads.
  * @routed: mapped to @pll_idx by the initial configuration; an unrouted
@@ -113,6 +116,8 @@ struct sit9531x_out {
 	u64		freq;
 	bool		enabled;
 	bool		cmos;
+	bool		state_stale;
+	bool		ntf_pending;
 	bool		part_muted;
 	bool		routed;
 	u8		pll_idx;
@@ -281,6 +286,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_disable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_output_enable(struct sit9531x_dev *sitdev, u8 index);
+int sit9531x_output_state_refresh(struct sit9531x_dev *sitdev,
+				  u8 out_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,
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index a450a81d02f4..a32d37a396b1 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -184,8 +184,9 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
  * 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.
+ * one. On manual input select, the routed input is CONNECTED. An output is
+ * CONNECTED while driven and DISCONNECTED while muted; SELECTABLE is refused.
+ * The XO pin is always CONNECTED.
  */
 
 /* Caller must hold sitdev->multiop_lock. */
@@ -638,10 +639,114 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
 	return rc;
 }
 
+/*
+ * DISCONNECTED while the output is forced to Hi-Z, by a mute request or by the
+ * loaded profile.
+ */
+static int
+sit9531x_dpll_output_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;
+	const struct sit9531x_out *out;
+	int rc = 0;
+
+	mutex_lock(&sitdev->multiop_lock);
+	out = sit9531x_out_state_get(sitdev, dpin->id);
+	if (out->state_stale)
+		rc = sit9531x_output_state_refresh(sitdev, dpin->id);
+	if (!rc)
+		*state = out->enabled ? DPLL_PIN_STATE_CONNECTED
+				      : DPLL_PIN_STATE_DISCONNECTED;
+	mutex_unlock(&sitdev->multiop_lock);
+	if (rc)
+		NL_SET_ERR_MSG(extack,
+			       "Output mute state could not be read back");
+
+	return rc;
+}
+
+/*
+ * Force Hi-Z through the slot's Hi-Z force/state register pairs: differential,
+ * CMOS OutP and CMOS OutN.
+ */
+static int
+sit9531x_dpll_output_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)
+{
+	bool enable = state == DPLL_PIN_STATE_CONNECTED;
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	bool was_enabled, changed;
+	int rc;
+
+	/* An output is routed explicitly, not a selection candidate */
+	if (!enable && state != DPLL_PIN_STATE_DISCONNECTED)
+		return -EINVAL;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * Skip a request for the current state: the programming sequence
+	 * unlocks the output loops for its 100 ms commit. One CMOS pad forced
+	 * and the other not is neither state.
+	 */
+	if (!sit9531x_output_state_refresh(sitdev, dpin->id) &&
+	    sitdev->out[dpin->id].enabled == enable &&
+	    !sitdev->out[dpin->id].part_muted) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return 0;
+	}
+
+	was_enabled = sitdev->out[dpin->id].enabled;
+
+	if (enable)
+		rc = sit9531x_output_enable(sitdev, dpin->id);
+	else
+		rc = sit9531x_output_disable(sitdev, dpin->id);
+
+	changed = sitdev->out[dpin->id].enabled != was_enabled ||
+		  sitdev->out[dpin->id].state_stale;
+
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		struct dpll_pin *dpll_pin;
+
+		NL_SET_ERR_MSG(extack, "Failed to set output pin state");
+		/*
+		 * The core notifies only a request that succeeded. Teardown
+		 * clears dpll_pin without the core's lock, which is held here;
+		 * read it once, as a set handle stays valid until this returns.
+		 */
+		dpll_pin = READ_ONCE(dpin->dpll_pin);
+		if (changed && dpll_pin && __dpll_pin_change_ntf(dpll_pin)) {
+			mutex_lock(&sitdev->multiop_lock);
+			sitdev->out[dpin->id].ntf_pending = true;
+			mutex_unlock(&sitdev->multiop_lock);
+		}
+	}
+
+	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,
+	.state_on_dpll_get	= sit9531x_dpll_output_pin_state_on_dpll_get,
+	.state_on_dpll_set	= sit9531x_dpll_output_pin_state_on_dpll_set,
 };
 
 const struct dpll_pin_ops *
@@ -703,6 +808,31 @@ void sit9531x_dpll_pin_baseline(struct sit9531x_dpll_pin *pin)
 				   &pin->operstate, &pin->prio);
 }
 
+/*
+ * Re-read an output with a pending change notification; return true once the
+ * read succeeds. Caller must hold sitdev->multiop_lock.
+ */
+static bool
+sit9531x_dpll_output_ntf_pending(struct sit9531x_dev *sitdev,
+				 const struct sit9531x_dpll_pin *pin)
+{
+	struct sit9531x_out *out;
+
+	if (sit9531x_dpll_is_input_pin(pin) ||
+	    pin->id >= sitdev->info->num_outputs)
+		return false;
+
+	out = &sitdev->out[pin->id];
+	if (!out->ntf_pending)
+		return false;
+	if (out->state_stale && sit9531x_output_state_refresh(sitdev, pin->id))
+		return false;
+
+	out->ntf_pending = false;
+
+	return true;
+}
+
 /*
  * Send notifications for changed lock status and pin state; called from
  * sit9531x_dev_periodic_work().
@@ -744,8 +874,11 @@ void sit9531x_dpll_changes_check(struct sit9531x_dpll *sitdpll)
 		bool changed;
 		u8 prio;
 
-		if (!sit9531x_dpll_is_selection_pin(pin))
+		if (!sit9531x_dpll_is_selection_pin(pin)) {
+			pin->ntf_pending |=
+				sit9531x_dpll_output_ntf_pending(sitdev, pin);
 			continue;
+		}
 
 		sit9531x_dpll_pin_snapshot(sitdpll, pin, &state, &operstate,
 					   &prio);
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 12/13] dpll: sit9531x: add support to get phase offset on the connected input pin
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (9 preceding siblings ...)
  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 ` 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 13/13] dpll: sit9531x: model the inter-PLL sync line as a pair of pins Ali Rouhi
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Report the phase difference between a PLL's reference and the PLL, read
from the on-chip time-to-digital converter through the debug window as
the documented procedure does. A read latches a sample and returns the
previous one, so each sample is read three times.

Only the input the part feeds the PLL has an offset. Any other pin, a PLL
without a reference, or a PLL that drives no enabled output returns
-ENODATA and the core leaves the attribute out.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 357 +++++++++++++++++++++++++++++------
 drivers/dpll/sit9531x/core.h |  13 ++
 drivers/dpll/sit9531x/dpll.c | 120 ++++++++++++
 drivers/dpll/sit9531x/regs.h |  19 ++
 4 files changed, 456 insertions(+), 53 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 9896d5aa40f5..8f305cc27737 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -262,6 +262,28 @@ static int sit9531x_update_pll_close(struct sit9531x_dev *sitdev, u8 pll_idx,
 	return rc;
 }
 
+/*
+ * Restore the debug taps a phase-offset read changed. The PLL page's debug
+ * window must already be open.
+ */
+static int sit9531x_tap_restore(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	int rc;
+
+	rc = sit9531x_write_pll_close(sitdev, pll_idx,
+				      SIT9531X_PLL_REG_DBG_READ_CODE,
+				      chan->tap_read_code);
+	if (!rc)
+		rc = sit9531x_write_pll_close(sitdev, pll_idx,
+					      SIT9531X_PLL_REG_DBG_WRITE_CODE,
+					      chan->tap_write_code);
+	if (!rc)
+		chan->tap_pending = false;
+
+	return rc;
+}
+
 static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
 				    u8 index,
 				    unsigned int *force_reg,
@@ -785,12 +807,16 @@ static int sit9531x_prio_prg_commit(struct sit9531x_dev *sitdev)
 	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.
+/**
+ * sit9531x_input_mon_fetch - read the clock monitor status of every lane
+ * @sitdev: device pointer
+ *
+ * One nibble per lane: loss of signal, fine and coarse frequency drift.
+ * Caller must hold sitdev->multiop_lock, or run before registration.
+ *
+ * Return: 0 on success, <0 on error
  */
-static int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev)
+int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev)
 {
 	static const unsigned int regs[] = {
 		SIT9531X_CLKMON_P_STATUS_01, SIT9531X_CLKMON_P_STATUS_23,
@@ -2559,6 +2585,171 @@ static int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/**
+ * sit9531x_phase_offset_read - read phase difference via TDC
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @phase_offset: phase difference in 1/DPLL_PHASE_OFFSET_DIVIDER ps
+ *
+ * The TDC code is a 35-bit magnitude with a separate sign bit;
+ * phase_diff = tdc_code / fvco. Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA if Fvco is unknown, <0 on other errors
+ */
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       s64 *phase_offset)
+{
+	u8 v, old_write_code, old_read_code;
+	struct sit9531x_chan *chan;
+	bool have_old = false;
+	int rc, ret, i;
+	s64 tdc_signed;
+	u64 fvco, mag;
+	u64 tdc_raw;
+	bool sign;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	chan = &sitdev->chan[pll_idx];
+
+	/*
+	 * Read Fvco first; without it there is no reason to open the debug
+	 * window.
+	 */
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc) {
+		if (rc == -ENODATA)
+			dev_dbg(sitdev->dev,
+				"PLL%c: Fvco unknown, skip TDC\n",
+				'A' + pll_idx);
+		return rc;
+	}
+
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto relock;
+
+	/* Restore pending taps before saving the selection */
+	if (chan->tap_pending) {
+		rc = sit9531x_tap_restore(sitdev, pll_idx);
+		if (rc)
+			goto relock;
+	}
+
+	/* Save the tap selection; the debug key does not cover the mux. */
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_WRITE_CODE,
+				  &old_write_code);
+	if (!rc)
+		rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+					  SIT9531X_PLL_REG_DBG_READ_CODE,
+					  &old_read_code);
+	if (rc)
+		goto relock;
+
+	have_old = true;
+
+	/*
+	 * Enable the debug clock for taps below 200 kHz, then select the TDC
+	 * tap. The modifier register also holds fields of other taps.
+	 */
+	rc = sit9531x_update_pll_u8(sitdev, pll_idx,
+				    SIT9531X_PLL_REG_DBG_WRITE_CODE,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT,
+				    SIT9531X_DBG_LOW_FREQ_CLK_BIT);
+	if (rc)
+		goto relock;
+	rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+				   SIT9531X_PLL_REG_DBG_READ_CODE,
+				   SIT9531X_DBG_READ_CODE_TDC);
+	if (rc)
+		goto relock;
+
+	/*
+	 * A status data read latches a sample and returns the previous one;
+	 * read three times as the documented procedure does.
+	 */
+	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;
+	}
+
+	tdc_raw = 0;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_4, &v);
+	if (rc)
+		goto relock;
+	sign = !!(v & BIT(SIT9531X_TDC_SIGN_BIT));
+	tdc_raw = (u64)(v & SIT9531X_TDC_MAG_HI_MASK) << 32;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_3, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 24;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_2, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 16;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_1, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= (u64)v << 8;
+
+	rc = sit9531x_read_pll_u8(sitdev, pll_idx,
+				  SIT9531X_PLL_REG_DBG_DATA_0, &v);
+	if (rc)
+		goto relock;
+	tdc_raw |= v;
+
+	/* Per the register map the sign bit set means a positive offset. */
+	tdc_signed = sign ? (s64)tdc_raw : -(s64)tdc_raw;
+
+	/*
+	 * phase_diff (ps) = tdc_code * 1e12 / fvco. Scale by the subsystem's
+	 * divider before dividing to keep the fractional digits; one code is
+	 * 203.45 ps at 4.9152 GHz.
+	 */
+	mag = mul_u64_u64_div_u64(tdc_signed < 0 ? -tdc_signed : tdc_signed,
+				  1000000000000ULL * DPLL_PHASE_OFFSET_DIVIDER,
+				  fvco);
+	*phase_offset = tdc_signed < 0 ? -(s64)mag : (s64)mag;
+
+	rc = 0;
+
+relock:
+	if (have_old) {
+		chan->tap_read_code = old_read_code;
+		chan->tap_write_code = old_write_code;
+		ret = sit9531x_tap_restore(sitdev, pll_idx);
+		if (ret)
+			chan->tap_pending = true;
+		if (ret && !rc)
+			rc = ret;
+	}
+
+	ret = sit9531x_write_pll_close(sitdev, pll_idx,
+				       SIT9531X_PLL_REG_DEBUG,
+				       SIT9531X_PLL_DEBUG_LOCK);
+	chan->key_pending = !!ret;
+	if (ret && !rc)
+		rc = ret;
+
+	return rc;
+}
+
 /* Read the lane's receiver state from the Page 0x02 force and state bits */
 static int sit9531x_ref_state_fetch(struct sit9531x_dev *sitdev, u8 index)
 {
@@ -2689,13 +2880,20 @@ 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.
+/**
+ * sit9531x_chan_routed_ref_read - read the reference the device feeds a PLL
+ * @sitdev: device pointer
+ * @pll_idx: PLL index (0-3)
+ * @ref: logical input index, SIT9531X_INTSYNC_PIN_ID, or SIT9531X_REF_INVALID
+ *
+ * The device falls back between listed sources without changing the active
+ * selection, so read the source from the debug status bus. The caller rules
+ * out free-run first and must hold multiop_lock.
+ *
+ * Return: 0 on success, <0 on error
  */
-static int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev,
-					 u8 pll_idx, u8 *ref)
+int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+				  u8 *ref)
 {
 	int rc, lock_rc;
 	u8 v, i;
@@ -3189,10 +3387,32 @@ static void sit9531x_table_back_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
 		 'A' + pll_idx);
 }
 
+/*
+ * Restore the debug taps and close the debug window after a failed phase-offset
+ * read. Caller must hold sitdev->multiop_lock.
+ */
+static void sit9531x_tap_back_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+
+	if (!chan->tap_pending && !chan->key_pending)
+		return;
+
+	if (chan->tap_pending &&
+	    !sit9531x_write_pll_u8(sitdev, pll_idx, SIT9531X_PLL_REG_DEBUG,
+				   SIT9531X_PLL_DEBUG_UNLOCK))
+		sit9531x_tap_restore(sitdev, pll_idx);
+
+	chan->key_pending = !!sit9531x_write_pll_close(sitdev, pll_idx,
+						     SIT9531X_PLL_REG_DEBUG,
+						     SIT9531X_PLL_DEBUG_LOCK);
+}
+
 /*
  * 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.
+ * reference; the write latches the active selection again. Each latch restarts
+ * acquisition, so the table is written once per change of the best live source.
+ * Caller must hold sitdev->multiop_lock.
  */
 static void sit9531x_reselect(struct sit9531x_dev *sitdev, u8 pll_idx)
 {
@@ -3273,6 +3493,7 @@ sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
 	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
 		sit9531x_table_back_retry(sitdev, i);
 		sit9531x_ho_release_retry(sitdev, i);
+		sit9531x_tap_back_retry(sitdev, i);
 		/*
 		 * A flush pulse sent into open loops may be lost when they
 		 * re-lock.
@@ -3525,55 +3746,30 @@ static void sit9531x_pin_nodes_check(struct sit9531x_dev *sitdev)
 	fwnode_handle_put(pins);
 }
 
-static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+/* Retry the pending debug tap restore and debug window close */
+static void sit9531x_tap_pending_retry(struct sit9531x_dev *sitdev)
 {
-	struct sit9531x_dpll *sitdpll;
-	int rc;
-
-	rc = sit9531x_dev_state_fetch(sitdev);
-	if (rc)
-		return rc;
+	u8 i;
 
 	mutex_lock(&sitdev->multiop_lock);
-	sit9531x_eeprom_state_report(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) {
-			dev_err_probe(sitdev->dev, rc,
-				      "Failed to register DPLL%u\n",
-				      sitdpll->id);
-			goto err_unregister;
-		}
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		sit9531x_tap_back_retry(sitdev, i);
+		if (sitdev->chan[i].tap_pending || sitdev->chan[i].key_pending)
+			dev_warn(sitdev->dev,
+				 "PLL%c left with its debug taps moved or window open\n",
+				 'A' + i);
 	}
-
-	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
-
-	return 0;
-
-err_unregister:
-	list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
-		sit9531x_dpll_unregister(sitdpll);
-
-	return rc;
+	mutex_unlock(&sitdev->multiop_lock);
 }
 
-static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+/*
+ * Retry every pending write once more and report what is still pending. Call
+ * with the DPLLs unregistered.
+ */
+static void sit9531x_dev_pending_retry(struct sit9531x_dev *sitdev)
 {
-	struct sit9531x_dpll *sitdpll;
 	u8 i;
 
-	kthread_cancel_delayed_work_sync(&sitdev->work);
-
-	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
-		if (sitdpll->dpll_dev)
-			sit9531x_dpll_unregister(sitdpll);
-	}
-
 	/*
 	 * Write back tables first: forced holdover stays set while a table
 	 * write-back is pending.
@@ -3637,6 +3833,61 @@ static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
 				 'A' + i);
 	}
 	mutex_unlock(&sitdev->multiop_lock);
+
+	sit9531x_tap_pending_retry(sitdev);
+}
+
+static int sit9531x_dev_start(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+	int rc;
+
+	rc = sit9531x_dev_state_fetch(sitdev);
+	if (rc)
+		return rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	sit9531x_eeprom_state_report(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) {
+			dev_err_probe(sitdev->dev, rc,
+				      "Failed to register DPLL%u\n",
+				      sitdpll->id);
+			goto err_unregister;
+		}
+	}
+
+	kthread_queue_delayed_work(sitdev->kworker, &sitdev->work, 0);
+
+	return 0;
+
+err_unregister:
+	list_for_each_entry_continue_reverse(sitdpll, &sitdev->dplls, list)
+		sit9531x_dpll_unregister(sitdpll);
+
+	sit9531x_dev_pending_retry(sitdev);
+
+	return rc;
+}
+
+static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
+{
+	struct sit9531x_dpll *sitdpll;
+
+	kthread_cancel_delayed_work_sync(&sitdev->work);
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		if (sitdpll->dpll_dev)
+			sit9531x_dpll_unregister(sitdpll);
+	}
+
+	sit9531x_dev_pending_retry(sitdev);
 }
 
 static struct sit9531x_dpll_pin *
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index bbf5026f945d..8718c463c560 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -165,6 +165,10 @@ struct sit9531x_out {
  *   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
+ * @tap_write_code: tap selection to restore while @tap_pending
+ * @tap_read_code: readback selection to restore while @tap_pending
+ * @key_pending: debug key not locked again after a phase-offset read or tap
+ *   retry
  * @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
@@ -194,6 +198,9 @@ struct sit9531x_chan {
 	bool		table_pending;
 	u8		table_back[SIT9531X_PRIO_MAX_SLOTS];
 	bool		tap_pending;
+	u8		tap_write_code;
+	u8		tap_read_code;
+	bool		key_pending;
 	u8		prio_srcs[SIT9531X_PRIO_MAX_SLOTS];
 	u8		prio_last[SIT9531X_PRIO_NUM_SRC];
 	u16		prio_mask;
@@ -311,6 +318,12 @@ int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
 int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
 				     u8 out_idx, s32 phase_ps);
 
+int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+				  u8 *ref);
+int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev);
+int sit9531x_phase_offset_read(struct sit9531x_dev *sitdev, u8 pll_idx,
+			       s64 *phase_offset);
+
 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 e6271ebd47ac..ba4ac5e7d365 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -488,6 +488,122 @@ sit9531x_dpll_input_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
 	return 0;
 }
 
+/*
+ * Report the loop's residual phase error, sampled with the loop closed. The TDC
+ * compares against a signal the PLL drives, so a PLL driving no output returns
+ * -ENODATA.
+ */
+static int
+sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
+					 void *pin_priv,
+					 const struct dpll_device *dpll,
+					 void *dpll_priv, s64 *phase_offset,
+					 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 drives = false;
+	u8 routed, i;
+	s64 offset;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	/*
+	 * Check the cache first: the core calls this for every pin of every
+	 * DPLL in a dump.
+	 */
+	if (!sit9531x_dpll_selection_active(sitdev, sitdpll, dpin->id)) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return -ENODATA;
+	}
+
+	/*
+	 * The device can change the routed reference, so read it before and
+	 * after the sample. A PLL with no routed, enabled output has no
+	 * measurement.
+	 */
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		if (!sitdev->out[i].routed ||
+		    sitdev->out[i].pll_idx != sitdpll->id)
+			continue;
+		if (sitdev->out[i].state_stale) {
+			rc = sit9531x_output_state_refresh(sitdev, i);
+			if (rc) {
+				mutex_unlock(&sitdev->multiop_lock);
+				NL_SET_ERR_MSG(extack,
+					       "Output mute state could not be read back");
+				return rc;
+			}
+		}
+		if (sitdev->out[i].enabled)
+			drives = true;
+	}
+
+	if (!drives) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return -ENODATA;
+	}
+
+	/*
+	 * Read the monitors live: on loss of signal the device falls back
+	 * without a request.
+	 */
+	rc = sit9531x_input_mon_fetch(sitdev);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		NL_SET_ERR_MSG(extack,
+			       "Failed to read the input clock monitors");
+		return rc;
+	}
+
+	if (!sit9531x_dpll_selection_active(sitdev, sitdpll, dpin->id)) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return -ENODATA;
+	}
+
+	rc = sit9531x_chan_routed_ref_read(sitdev, sitdpll->id, &routed);
+	if (!rc && routed == dpin->id) {
+		rc = sit9531x_phase_offset_read(sitdev, sitdpll->id, &offset);
+		if (!rc)
+			rc = sit9531x_chan_routed_ref_read(sitdev, sitdpll->id,
+							   &routed);
+		if (!rc && routed != dpin->id)
+			rc = -ENODATA;
+	} else if (!rc) {
+		rc = -ENODATA;
+	}
+
+	/*
+	 * A reference lost during the sample can leave the routed reference
+	 * unchanged while the PLL is in holdover; read the monitors again.
+	 */
+	if (!rc) {
+		rc = sit9531x_input_mon_fetch(sitdev);
+		if (rc) {
+			mutex_unlock(&sitdev->multiop_lock);
+			NL_SET_ERR_MSG(extack,
+				       "Failed to read the input clock monitors");
+			return rc;
+		}
+		if (dpin->id < sitdev->info->num_inputs &&
+		    sit9531x_ref_state_get(sitdev, dpin->id)->los)
+			rc = -ENODATA;
+	}
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc == -ENODATA)
+		return rc;
+	if (rc) {
+		NL_SET_ERR_MSG(extack, "TDC phase readback failed");
+		return rc;
+	}
+
+	*phase_offset = offset;
+	return 0;
+}
+
 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,
@@ -496,6 +612,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_ops = {
 	.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,
+	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
 };
 
 /*
@@ -509,6 +626,7 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_norate_ops = {
 	.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,
+	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
 };
 
 /*
@@ -520,12 +638,14 @@ static const struct dpll_pin_ops sit9531x_dpll_input_pin_manual_ops = {
 	.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,
+	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_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,
+	.phase_offset_get	= sit9531x_dpll_input_pin_phase_offset_get,
 };
 
 /*
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 8c76609b99c5..d6c6b4a6a6bb 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -229,6 +229,25 @@
 #define SIT9531X_PLL_REG_DIVN_NUM		0x32  /* 4 bytes (0x32-0x35) */
 #define SIT9531X_PLL_REG_DIVN_DEN		0x38  /* 4 bytes (0x38-0x3B) */
 
+/*
+ * Signal pathway debug readback: Dig_Sys_ReadCode selects the tap,
+ * Dig_Sys_WriteCode its modifiers; a read of the debug status data register
+ * latches a sample into Dig_Sys_read7..read0.
+ */
+#define SIT9531X_PLL_REG_DBG_READ_CODE	0xB3
+#define SIT9531X_PLL_REG_DBG_WRITE_CODE	0xB4
+#define SIT9531X_DBG_LOW_FREQ_CLK_BIT	BIT(7)
+#define SIT9531X_PLL_REG_DBG_DATA_0		0xB5  /* [7:0] */
+#define SIT9531X_PLL_REG_DBG_DATA_1		0xB6  /* [15:8] */
+#define SIT9531X_PLL_REG_DBG_DATA_2		0xB7  /* [23:16] */
+#define SIT9531X_PLL_REG_DBG_DATA_3		0xB8  /* [31:24] */
+#define SIT9531X_PLL_REG_DBG_DATA_4		0xB9  /* [34:32] + sign */
+
+/* Read code of the TDC phase tap, and the sign bit of its sample */
+#define SIT9531X_DBG_READ_CODE_TDC		69
+#define SIT9531X_TDC_SIGN_BIT		3
+#define SIT9531X_TDC_MAG_HI_MASK	GENMASK(2, 0)
+
 /* DIVN is carried as fixed point, in steps of 1e-12 of a whole divider */
 #define SIT9531X_DIVN_SCALE		1000000000000ULL
 
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 11/13] dpll: sit9531x: add support to adjust output phase
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (10 preceding siblings ...)
  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 ` 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
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

Shift an output in time against the other outputs of its PLL. A delay is
a coarse count of VCO cycles plus a 3-bit fine field in 30 ps steps; a
request is split between them and the getter reports what the registers
hold.

The range advertised to the core is +/-1 ms at 1 ps granularity. The
driver writes only positive delays, so an advance is held as the
complementary delay within the output period. The delay is applied by
the PLL's phase flush, which restarts the divider phase of every output
the PLL drives, and a rate change re-times a programmed delay in the
same programming window.

Suggested-by: Ivan Vecera <ivecera@redhat.com>
Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 848 ++++++++++++++++++++++++++++++++---
 drivers/dpll/sit9531x/core.h |  15 +
 drivers/dpll/sit9531x/dpll.c | 116 ++++-
 drivers/dpll/sit9531x/prop.c |  14 +
 drivers/dpll/sit9531x/regs.h |  29 ++
 5 files changed, 946 insertions(+), 76 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 458de502bc8f..9896d5aa40f5 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -245,6 +245,23 @@ static int sit9531x_write_pll_close(struct sit9531x_dev *sitdev, u8 pll_idx,
 						 offset), val);
 }
 
+static int sit9531x_update_pll_close(struct sit9531x_dev *sitdev, u8 pll_idx,
+				     u8 offset, u8 mask, u8 val)
+{
+	u8 attempt;
+	int rc;
+
+	for (attempt = 0; attempt < SIT9531X_CLOSE_TRIES; attempt++) {
+		if (attempt)
+			usleep_range(1000, 2000);
+		rc = sit9531x_update_pll_u8(sitdev, pll_idx, offset, mask, val);
+		if (!rc)
+			break;
+	}
+
+	return rc;
+}
+
 static void sit9531x_input_get_regs(const struct sit9531x_dev *sitdev,
 				    u8 index,
 				    unsigned int *force_reg,
@@ -1534,6 +1551,65 @@ static int sit9531x_pll_small_update(struct sit9531x_dev *sitdev, u8 pll_idx)
 				     SIT9531X_SMALL_UPDATE_CMD);
 }
 
+static int sit9531x_pll_small_update_close(struct sit9531x_dev *sitdev,
+					   u8 pll_idx)
+{
+	return sit9531x_write_pll_close(sitdev, pll_idx,
+					SIT9531X_PLL_REG_SMALL_UPDATE,
+					SIT9531X_SMALL_UPDATE_CMD);
+}
+
+/*
+ * Undo a phase flush: restore the trigger select and the flush enable of @plls
+ * from @armed.
+ */
+static int sit9531x_phase_flush_undo(struct sit9531x_dev *sitdev,
+				     bool ctrl_back, u8 ctrl, u8 plls, u8 armed)
+{
+	const u8 en = SIT9531X_PLL_PHFL_ON_DEMAND_EN;
+	int rc = 0, ret;
+	u8 i;
+
+	if (ctrl_back) {
+		rc = sit9531x_write_close(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1,
+					  ctrl);
+		sitdev->flush_ctrl_pending = !!rc;
+		sitdev->flush_ctrl = ctrl;
+	}
+
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (!(plls & BIT(i)))
+			continue;
+		ret = sit9531x_update_pll_close(sitdev, i,
+						SIT9531X_PLL_REG_PHFL_CTRL, en,
+						(armed & BIT(i)) ? en : 0);
+		if (!ret)
+			ret = sit9531x_pll_small_update_close(sitdev, i);
+		if (ret) {
+			sitdev->flush_arm_pending |= BIT(i);
+			sitdev->flush_armed &= ~BIT(i);
+			sitdev->flush_armed |= armed & BIT(i);
+			if (!rc)
+				rc = ret;
+		} else {
+			sitdev->flush_arm_pending &= ~BIT(i);
+		}
+	}
+
+	return rc;
+}
+
+static int sit9531x_phase_flush_undo_retry(struct sit9531x_dev *sitdev)
+{
+	if (!sitdev->flush_ctrl_pending && !sitdev->flush_arm_pending)
+		return 0;
+
+	return sit9531x_phase_flush_undo(sitdev, sitdev->flush_ctrl_pending,
+					 sitdev->flush_ctrl,
+					 sitdev->flush_arm_pending,
+					 sitdev->flush_armed);
+}
+
 /*
  * 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
@@ -1542,27 +1618,39 @@ static int sit9531x_pll_small_update(struct sit9531x_dev *sitdev, u8 pll_idx)
  */
 static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
 {
-	u8 ctrl, orig, phfl, armed_mask = 0, parked = 0, i;
+	u8 ctrl, orig = 0, phfl, armed_mask = 0, parked = 0, touched, i;
+	bool ctrl_back = false, armed_here = false, fired = false;
 	int rc, ret;
 
+	/*
+	 * Retry a pending undo first so the reads below see the profile's
+	 * settings.
+	 */
+	rc = sit9531x_phase_flush_undo_retry(sitdev);
+	if (rc)
+		goto out;
+
 	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);
+		goto out;
+	if (!(phfl & SIT9531X_PLL_CONFIG47_PHFL_EN)) {
+		rc = sit9531x_write_pll_u8(sitdev, pll_idx,
+					   SIT9531X_PLL_REG_DIRECTIVES,
+					   SIT9531X_PLL_DIRECTIVE_RESTART);
+		fired = !rc;
+		goto out;
+	}
 
 	/*
 	 * The phase trigger is chip-global and fires every PLL with on-demand
-	 * flush enabled; record which ones the profile arms.
+	 * flush enabled; disable it on the other PLLs for the pulse.
 	 */
 	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;
+			goto out;
 		if (phfl & SIT9531X_PLL_PHFL_ON_DEMAND_EN)
 			armed_mask |= BIT(i);
 	}
@@ -1575,18 +1663,19 @@ static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
 					    SIT9531X_PLL_REG_PHFL_CTRL,
 					    SIT9531X_PLL_PHFL_ON_DEMAND_EN, 0);
 		if (rc)
-			goto unpark;
+			goto undo;
 		rc = sit9531x_pll_small_update(sitdev, i);
 		if (rc)
-			goto unpark;
+			goto undo;
 	}
 
+	armed_here = true;
 	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;
+		goto undo;
 
 	/*
 	 * Write the update register whole; a read-modify-write skips the write
@@ -1594,21 +1683,19 @@ static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
 	 */
 	rc = sit9531x_pll_small_update(sitdev, pll_idx);
 	if (rc)
-		goto disarm;
+		goto undo;
 
-	/*
-	 * Select the in-register phase trigger; the register also holds the OEb
-	 * bits.
-	 */
+	/* Select the in-register phase trigger, preserving the OEb bits. */
 	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, &ctrl);
 	if (rc)
-		goto disarm;
+		goto undo;
 
 	orig = ctrl & ~SIT9531X_DIVO_PHASE_TRIG;
 	ctrl = orig | SIT9531X_DIVO_PHASE_SEL_REG;
+	ctrl_back = true;
 	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GPIO_FUNC_CTRL1, ctrl);
 	if (rc)
-		goto restore;
+		goto undo;
 
 	/*
 	 * No delay between set and clear; one I2C transfer is longer than the
@@ -1616,40 +1703,19 @@ static int sit9531x_output_phase_flush(struct sit9531x_dev *sitdev, u8 pll_idx)
 	 */
 	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);
+	fired = !rc;
+
+undo:
+	touched = parked;
+	if (armed_here && !(armed_mask & BIT(pll_idx)))
+		touched |= BIT(pll_idx);
+	ret = sit9531x_phase_flush_undo(sitdev, ctrl_back, orig, touched,
+					armed_mask);
 	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;
-	}
+out:
+	sitdev->chan[pll_idx].flush_pending = !fired;
 
 	return rc;
 }
@@ -1730,7 +1796,7 @@ static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
 	int rc, j, rb_rc;
 	u8 written = 0;
 
-	*back = false;
+	*back = true;
 
 	slot = info->clkout_map[out_idx];
 
@@ -1757,6 +1823,7 @@ static int sit9531x_output_divo_write(struct sit9531x_dev *sitdev, u8 out_idx,
 	msb_old = old_bytes[4];
 	divo_bytes[4] |= msb_old & 0xFC;
 
+	*back = false;
 	for (j = 0; j < 5; j++) {
 		rc = sit9531x_write_u8(sitdev,
 				       SIT9531X_REG(page, base_reg - j),
@@ -1815,12 +1882,357 @@ static int sit9531x_output_divo_read(struct sit9531x_dev *sitdev, u8 out_idx,
 	return *divo ? 0 : -ENODATA;
 }
 
+/*
+ * Phase adjust is a 34-bit coarse delay in VCO cycles plus a 3-bit fine delay
+ * in 30 ps steps, in bytes PROG6..PROG2. PRG_RST_DELAY is unsigned, so an
+ * advance is written as T_out - |phase|.
+ */
+
+/* Register of byte @i (PROG6 first) of an output's PRG_RST_DELAY block. */
+static unsigned int sit9531x_output_prg_reg(struct sit9531x_dev *sitdev,
+					    u8 out_idx, u8 i)
+{
+	u8 slot = sitdev->info->clkout_map[out_idx];
+	u8 page = (slot > SIT9531X_PAGE_OUTSYS0_SLOT_MAX) ?
+		  SIT9531X_PAGE_OUTSYS1 : SIT9531X_PAGE_OUTSYS0;
+	u8 base = SIT9531X_OUT_PRG_DELAY_BASE +
+		  SIT9531X_OUT_PRG_SLOT_STRIDE * (slot % 6);
+
+	return SIT9531X_REG(page, base + i);
+}
+
+static int sit9531x_output_phase_bytes_read(struct sit9531x_dev *sitdev,
+					    u8 out_idx, u8 *bytes)
+{
+	unsigned int reg;
+	int rc;
+	u8 i;
+
+	for (i = 0; i < SIT9531X_OUT_PRG_BYTES; i++) {
+		reg = sit9531x_output_prg_reg(sitdev, out_idx, i);
+		rc = sit9531x_read_u8(sitdev, reg, &bytes[i]);
+		if (rc)
+			return rc;
+	}
+
+	return 0;
+}
+
+/*
+ * The register value includes the divider's settling cycles; a value below them
+ * means no delay.
+ */
+static void sit9531x_output_phase_bytes_decode(const u8 *bytes, u64 *coarse,
+					       u8 *fine)
+{
+	u64 prg_coarse;
+
+	*fine = (bytes[0] & SIT9531X_OUT_PRG_FINE_MASK) >>
+		SIT9531X_OUT_PRG_FINE_SHIFT;
+	prg_coarse = (u64)(bytes[0] & SIT9531X_OUT_PRG_COARSE_HI_MASK) << 32;
+	prg_coarse |= (u64)bytes[1] << 24;
+	prg_coarse |= (u64)bytes[2] << 16;
+	prg_coarse |= (u64)bytes[3] << 8;
+	prg_coarse |= bytes[4];
+	*coarse = (prg_coarse > SIT9531X_OUT_PRG_DIVO_CYCLES) ?
+		  prg_coarse - SIT9531X_OUT_PRG_DIVO_CYCLES : 0;
+}
+
+/*
+ * @coarse excludes the divider's two settling cycles; the register value
+ * includes them.
+ */
+static void sit9531x_output_phase_bytes_build(const u8 *old_bytes, u64 coarse,
+					      u8 fine, u8 *new_bytes)
+{
+	u64 prg_coarse = coarse + SIT9531X_OUT_PRG_DIVO_CYCLES;
+
+	/* PROG6 RMW: preserve OPSTG_VCASC_BUMP in [7:5] */
+	new_bytes[0] = old_bytes[0] & SIT9531X_OUT_PRG_OPSTG_MASK;
+	new_bytes[0] |= (fine << SIT9531X_OUT_PRG_FINE_SHIFT) &
+			SIT9531X_OUT_PRG_FINE_MASK;
+	new_bytes[0] |= (u8)((prg_coarse >> 32) &
+			     SIT9531X_OUT_PRG_COARSE_HI_MASK);
+	new_bytes[1] = (u8)((prg_coarse >> 24) & 0xFF);
+	new_bytes[2] = (u8)((prg_coarse >> 16) & 0xFF);
+	new_bytes[3] = (u8)((prg_coarse >> 8) & 0xFF);
+	new_bytes[4] = (u8)(prg_coarse & 0xFF);
+}
+
+/*
+ * Caller must be in the programming state; @back reports whether every byte
+ * holds its old value.
+ */
+static int sit9531x_output_phase_bytes_write(struct sit9531x_dev *sitdev,
+					     u8 out_idx, const u8 *new_bytes,
+					     const u8 *old_bytes, bool *back)
+{
+	int rc, ret, rb_rc = 0;
+	unsigned int reg;
+	u8 i;
+
+	*back = false;
+
+	for (i = 0; i < SIT9531X_OUT_PRG_BYTES; i++) {
+		reg = sit9531x_output_prg_reg(sitdev, out_idx, i);
+		rc = sit9531x_write_u8(sitdev, reg, new_bytes[i]);
+		if (rc)
+			goto rollback;
+	}
+
+	return 0;
+
+rollback:
+	for (i = 0; i < SIT9531X_OUT_PRG_BYTES; i++) {
+		reg = sit9531x_output_prg_reg(sitdev, out_idx, i);
+		ret = sit9531x_write_u8(sitdev, reg, old_bytes[i]);
+		if (ret && !rb_rc)
+			rb_rc = ret;
+	}
+	*back = !rb_rc;
+	if (rb_rc) {
+		dev_err(sitdev->dev,
+			"out%u: phase-adjust rollback failed (%d), the delay registers are part old and part new\n",
+			out_idx, rb_rc);
+		if (!rc)
+			rc = rb_rc;
+	}
+
+	return rc;
+}
+
+/*
+ * Return the error of @cycles VCO cycles plus the nearest fine steps (in @fine)
+ * against @abs_ps.
+ */
+static u64 sit9531x_output_phase_quant_err(u64 abs_ps, u64 fvco, u64 cycles,
+					   u8 *fine)
+{
+	u64 cycles_ps, steps = 0;
+
+	cycles_ps = mul_u64_u64_div_u64(cycles, 1000000000000ULL, fvco);
+	if (abs_ps > cycles_ps)
+		steps = div64_u64(abs_ps - cycles_ps +
+				  SIT9531X_OUT_PRG_FINE_STEP_PS / 2,
+				  SIT9531X_OUT_PRG_FINE_STEP_PS);
+	*fine = min_t(u64, steps, SIT9531X_OUT_PRG_FINE_MAX);
+
+	return abs_diff(abs_ps, cycles_ps +
+			(u64)*fine * SIT9531X_OUT_PRG_FINE_STEP_PS);
+}
+
+/*
+ * Fold @phase_ps into one output period and split it into VCO cycles and fine
+ * steps; @phase_adj gets the realized delay in the request's sign. Pure
+ * arithmetic, so call it before entering the programming state.
+ */
+static int sit9531x_output_phase_encode(u64 fvco, u64 divo, s32 phase_ps,
+					u64 *coarse_out, u8 *fine_out,
+					s32 *phase_adj)
+{
+	u64 abs_ps, cycles, frac_ps, coarse = 0, coarse_ps, t_out_ps;
+	s64 phase_norm_ps = 0;
+	u8 fine = 0;
+
+	t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL, fvco);
+	if (!t_out_ps)
+		return -EINVAL;
+
+	if (!phase_ps) {
+		*coarse_out = 0;
+		*fine_out = 0;
+		*phase_adj = 0;
+		return 0;
+	}
+
+	/*
+	 * The period is DIVO VCO cycles but often fractional in ps (7812.5 ps
+	 * at 128 MHz from 5.12 GHz), so fold whole cycles modulo DIVO and carry
+	 * the sub-cycle part. The DPLL core limits the request to +/-1 ms.
+	 */
+	abs_ps = abs(phase_ps);
+	cycles = mul_u64_u64_div_u64(abs_ps, fvco, 1000000000000ULL);
+	frac_ps = abs_ps - mul_u64_u64_div_u64(cycles, 1000000000000ULL, fvco);
+	div64_u64_rem(cycles, divo, &cycles);
+	abs_ps = mul_u64_u64_div_u64(cycles, 1000000000000ULL, fvco) + frac_ps;
+	/*
+	 * Truncation can leave a request within 1 ps of a whole period, which
+	 * is the same edge as no delay.
+	 */
+	if (abs_ps >= t_out_ps)
+		abs_ps = 0;
+	phase_norm_ps = phase_ps < 0 ? -(s64)abs_ps : (s64)abs_ps;
+	abs_ps = (phase_ps < 0 && abs_ps) ? t_out_ps - abs_ps : abs_ps;
+
+	if (abs_ps) {
+		u64 floor_cycles, err, alt_err, realized;
+		u8 alt_fine;
+
+		/* mul_u64_u64_div_u64(): abs_ps * fvco would overflow. */
+		floor_cycles = mul_u64_u64_div_u64(abs_ps, fvco,
+						   1000000000000ULL);
+
+		/*
+		 * The fine field spans 210 ps, more than one VCO cycle in every
+		 * band, so take the nearest of floor, floor + 1, and floor - 1
+		 * with a larger fine code; the floor on a tie.
+		 */
+		coarse = floor_cycles;
+		err = sit9531x_output_phase_quant_err(abs_ps, fvco, coarse,
+						      &fine);
+		alt_err = sit9531x_output_phase_quant_err(abs_ps, fvco,
+							  floor_cycles + 1,
+							  &alt_fine);
+		if (alt_err < err) {
+			coarse = floor_cycles + 1;
+			fine = alt_fine;
+			err = alt_err;
+		}
+		if (floor_cycles) {
+			u64 fewer = floor_cycles - 1;
+
+			alt_err = sit9531x_output_phase_quant_err(abs_ps, fvco,
+								  fewer,
+								  &alt_fine);
+			if (alt_err < err) {
+				coarse = fewer;
+				fine = alt_fine;
+			}
+		}
+
+		/*
+		 * Program a delay that quantizes to a whole period or more as
+		 * none, and round one past the advertised range toward the
+		 * request's side.
+		 */
+		coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+		realized = coarse_ps +
+			   (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+		if (realized >= t_out_ps) {
+			coarse = 0;
+			fine = 0;
+		} else if (phase_norm_ps < 0 ?
+			   t_out_ps - realized > SIT9531X_OUT_PHASE_ADJ_MAX_PS :
+			   realized > SIT9531X_OUT_PHASE_ADJ_MAX_PS) {
+			u64 rem;
+
+			coarse = floor_cycles;
+			coarse_ps = mul_u64_u64_div_u64(coarse,
+							1000000000000ULL, fvco);
+			rem = abs_ps - coarse_ps;
+			if (phase_norm_ps < 0)
+				rem += SIT9531X_OUT_PRG_FINE_STEP_PS - 1;
+			fine = div_u64(rem, SIT9531X_OUT_PRG_FINE_STEP_PS);
+		}
+
+		if (coarse + SIT9531X_OUT_PRG_DIVO_CYCLES >=
+		    (1ULL << SIT9531X_OUT_PRG_COARSE_BITS))
+			return -ERANGE;
+	}
+
+	coarse_ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+	abs_ps = coarse_ps + (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+	/* The result is within the range, so it fits the s32 the ABI carries */
+	if (phase_norm_ps < 0)
+		*phase_adj = abs_ps ? -(s32)(t_out_ps - abs_ps) : 0;
+	else
+		*phase_adj = (s32)abs_ps;
+
+	*coarse_out = coarse;
+	*fine_out = fine;
+
+	return 0;
+}
+
+/**
+ * sit9531x_output_phase_read - read an output's programmed delay back
+ * @sitdev: device pointer
+ * @out_idx: logical output index
+ * @phase_ps: result in picoseconds
+ * @beyond: set when @phase_ps is outside the advertised range
+ *
+ * An advance is stored as the period minus the advance and reads back as one
+ * only when it fits the range. Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, -ENODATA for an output no PLL drives or whose Fvco
+ * is unknown, <0 on other errors
+ */
+int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
+			       s64 *phase_ps, bool *beyond)
+{
+	const struct sit9531x_chip_info *info = sitdev->info;
+	u8 bytes[SIT9531X_OUT_PRG_BYTES], fine;
+	u64 coarse = 0, fvco, ps, divo;
+	int rc;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	*beyond = false;
+
+	if (!sitdev->out[out_idx].routed)
+		return -ENODATA;
+
+	rc = sit9531x_get_fvco(sitdev, sitdev->out[out_idx].pll_idx, &fvco);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_phase_bytes_read(sitdev, out_idx, bytes);
+	if (rc)
+		return rc;
+	sit9531x_output_phase_bytes_decode(bytes, &coarse, &fine);
+
+	/*
+	 * Take the period from the divider; the cached rate is unset at probe
+	 * and whole hertz at best.
+	 */
+	rc = sit9531x_output_divo_read(sitdev, out_idx, &divo);
+	if (rc && rc != -ENODATA)
+		return rc;
+
+	/*
+	 * Fold the cycle count, not the picoseconds; the fine steps can carry
+	 * the sum a fraction of a cycle past the period.
+	 */
+	if (!rc)
+		div64_u64_rem(coarse, divo, &coarse);
+
+	ps = mul_u64_u64_div_u64(coarse, 1000000000000ULL, fvco);
+	ps += (u64)fine * SIT9531X_OUT_PRG_FINE_STEP_PS;
+
+	if (!rc) {
+		u64 t_out_ps = mul_u64_u64_div_u64(divo, 1000000000000ULL,
+						   fvco);
+
+		if (ps >= t_out_ps)
+			ps -= t_out_ps;
+		if (ps > SIT9531X_OUT_PHASE_ADJ_MAX_PS &&
+		    t_out_ps - ps <= SIT9531X_OUT_PHASE_ADJ_MAX_PS) {
+			*phase_ps = -(s64)(t_out_ps - ps);
+			return 0;
+		}
+	}
+	*beyond = ps > SIT9531X_OUT_PHASE_ADJ_MAX_PS;
+	*phase_ps = ps;
+
+	return 0;
+}
+
 int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
 			     u8 pll_idx, u64 frequency)
 {
-	u64 fvco, divo;
+	u8 old_bytes[SIT9531X_OUT_PRG_BYTES], new_bytes[SIT9531X_OUT_PRG_BYTES];
+	u64 fvco, divo, coarse, old_divo = 0;
+	bool retime = false, bytes_in = false, bytes_back = false;
+	bool divo_back = false, divo_in, back, landed, encoded = false;
+	bool beyond;
+	struct sit9531x_out *out = &sitdev->out[out_idx];
+	struct sit9531x_chan *chan = &sitdev->chan[pll_idx];
+	s32 phase_adj = 0, held = 0;
 	int rc, ret;
-	bool back;
+	u8 fine;
 
 	lockdep_assert_held(&sitdev->multiop_lock);
 
@@ -1829,28 +2241,117 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
 	if (rc)
 		return rc;
 
+	if (out->phase_stale && !out->retime_pending) {
+		s64 phase_ps;
+
+		rc = sit9531x_output_phase_read(sitdev, out_idx, &phase_ps,
+						&beyond);
+		if (rc)
+			return rc;
+		out->phase_adj = phase_ps;
+		out->phase_armed = !beyond && phase_ps;
+		out->phase_stale = false;
+	}
+
+	/*
+	 * The reset delay counts VCO cycles; an advance is stored as T_out -
+	 * |advance| and must be re-encoded for the new period. Write it in the
+	 * divider's programming window; a second flush would restart the PLL's
+	 * dividers.
+	 */
+	rc = sit9531x_output_phase_bytes_read(sitdev, out_idx, old_bytes);
+	if (rc)
+		return rc;
+	sit9531x_output_phase_bytes_decode(old_bytes, &coarse, &fine);
+	beyond = !out->phase_armed && out->phase_adj;
+	if (out->retime_pending || out->phase_armed ||
+	    ((coarse || fine) && !beyond)) {
+		held = out->retime_pending ? out->retime_ps : out->phase_adj;
+
+		rc = sit9531x_output_phase_encode(fvco, divo, held, &coarse,
+						  &fine, &phase_adj);
+		if (rc)
+			return rc;
+		encoded = true;
+		sit9531x_output_phase_bytes_build(old_bytes, coarse, fine,
+						  new_bytes);
+		retime = memcmp(old_bytes, new_bytes, sizeof(new_bytes)) != 0;
+	}
+
+	if (retime) {
+		rc = sit9531x_output_divo_read(sitdev, out_idx, &old_divo);
+		if (rc && rc != -ENODATA)
+			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 */
+	landed = !back;
+	divo_in = !rc;
+	if (!rc && retime) {
+		rc = sit9531x_output_phase_bytes_write(sitdev, out_idx,
+						       new_bytes, old_bytes,
+						       &bytes_back);
+		if (rc) {
+			divo_back = bytes_back &&
+				!sit9531x_output_divo_write(sitdev, out_idx,
+							    old_divo, &back);
+			landed = !divo_back;
+		} else {
+			bytes_in = true;
+		}
+	}
+	/* Always leave PRG_CMD so the loops re-lock */
 	ret = sit9531x_prg_commit(sitdev);
 	if (ret && !rc)
 		rc = ret;
-	if (rc)
+	if (rc) {
+		if (bytes_in || (encoded && !retime && divo_in)) {
+			out->phase_adj = phase_adj;
+			out->phase_armed = phase_adj != 0;
+			out->phase_stale = false;
+			out->retime_pending = false;
+		} else if (retime && landed) {
+			out->retime_ps = held;
+			out->retime_pending = true;
+			out->phase_stale = true;
+		} else if (encoded && landed) {
+			out->phase_stale = true;
+		} else if (beyond) {
+			out->phase_stale = true;
+		}
+		if (landed)
+			chan->flush_pending = true;
 		return rc;
+	}
 
-	/* Flush the output phase so the new DIVO starts aligned */
 	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
-	if (rc) {
+	if (chan->flush_pending)
 		dev_warn(sitdev->dev,
-			 "out%u: rate changed but the divider phase was not realigned (%d)\n",
+			 "out%u: rate changed but the divider phase was not realigned (%d), the poll retries it\n",
 			 out_idx, rc);
-		rc = 0;
+	else if (rc)
+		dev_warn(sitdev->dev,
+			 "out%u: rate changed, the flush left something to put back (%d)\n",
+			 out_idx, rc);
+	if (encoded) {
+		out->phase_adj = phase_adj;
+		out->phase_armed = phase_adj != 0;
+		out->phase_stale = false;
 	}
 
-	sitdev->out[out_idx].freq = div64_u64(fvco, divo);
+	/*
+	 * A delay beyond the range was not re-timed and now decodes against the
+	 * new divider.
+	 */
+	if (beyond)
+		out->phase_stale = true;
+
+	out->freq = div64_u64(fvco, divo);
+	out->retime_pending = false;
 
 	return 0;
 }
@@ -1893,20 +2394,130 @@ int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
 	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:
- *   base + 0  PROG6  [7:5] OPSTG_VCASC_BUMP (preserved)
- *                    [4:2] PRG_RST_FINE_DELAY
- *                    [1:0] PRG_RST_DELAY[33:32]
- *   base + 1  PROG5  PRG_RST_DELAY[31:24]
- *   base + 2  PROG4  PRG_RST_DELAY[23:16]
- *   base + 3  PROG3  PRG_RST_DELAY[15:8]
- *   base + 4  PROG2  PRG_RST_DELAY[7:0]
- * Outputs 0-5 are on Page 3, 6-11 on Page 4; base = 0x15 + 16 * (out_idx % 6).
- * PRG_RST_DELAY is unsigned; a negative adjustment is wrapped to
- * (T_out - |phase|) modulo one output period.
- */
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+				     u8 out_idx, s32 phase_ps)
+{
+	u8 old_bytes[SIT9531X_OUT_PRG_BYTES], new_bytes[SIT9531X_OUT_PRG_BYTES];
+	const struct sit9531x_chip_info *info = sitdev->info;
+	bool bytes_back, bytes_in, same;
+	struct sit9531x_chan *chan;
+	struct sit9531x_out *out;
+	u64 fvco, divo, coarse;
+	u8 pll_idx, fine;
+	s32 phase_adj;
+	int rc, ret;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (out_idx >= info->num_outputs)
+		return -EINVAL;
+
+	/* An unrouted output has only a placeholder in pll_idx. */
+	if (!sitdev->out[out_idx].routed)
+		return -ENODEV;
+
+	out = &sitdev->out[out_idx];
+	pll_idx = out->pll_idx;
+	if (pll_idx >= SIT9531X_NUM_PLLS)
+		return -EINVAL;
+	chan = &sitdev->chan[pll_idx];
+
+	rc = sit9531x_get_fvco(sitdev, pll_idx, &fvco);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+
+	/* Fref * DIVN outside the VCO band is not the rate the PLL runs at. */
+	if (!sit9531x_fvco_in_band(pll_idx, fvco)) {
+		dev_dbg(sitdev->dev,
+			"out%u: PLL%c Fvco %llu Hz is outside its VCO band, delay not settable\n",
+			out_idx, 'A' + pll_idx, fvco);
+		return -EINVAL;
+	}
+
+	/* Take the period from the divider; the cached rate is whole hertz. */
+	rc = sit9531x_output_divo_read(sitdev, out_idx, &divo);
+	if (rc)
+		return rc == -ENODATA ? -ENODEV : rc;
+
+	rc = sit9531x_output_phase_encode(fvco, divo, phase_ps, &coarse, &fine,
+					  &phase_adj);
+	if (rc)
+		return rc;
+
+	rc = sit9531x_output_phase_bytes_read(sitdev, out_idx, old_bytes);
+	if (rc)
+		return rc;
+	sit9531x_output_phase_bytes_build(old_bytes, coarse, fine, new_bytes);
+
+	/*
+	 * Skip the write if the quantized bytes are unchanged; a rewrite
+	 * restarts the divider phase of every output on the PLL.
+	 */
+	same = !memcmp(old_bytes, new_bytes, sizeof(new_bytes)) &&
+	       !out->phase_stale;
+	if (same && !chan->flush_pending) {
+		rc = 0;
+		goto cache;
+	}
+
+	if (!same) {
+		/*
+		 * Output-system registers take effect only inside PRG_CMD,
+		 * committed to the NVM shadow.
+		 */
+		rc = sit9531x_prg_enter(sitdev);
+		if (rc)
+			return rc;
+
+		rc = sit9531x_output_phase_bytes_write(sitdev, out_idx,
+						       new_bytes, old_bytes,
+						       &bytes_back);
+		bytes_in = !rc;
+
+		ret = sit9531x_prg_commit(sitdev);
+		if (ret && !rc)
+			rc = ret;
+		if (rc) {
+			if (bytes_in) {
+				out->phase_adj = phase_adj;
+				out->phase_armed = phase_adj != 0;
+				out->phase_stale = false;
+				out->retime_pending = false;
+				chan->flush_pending = true;
+			} else if (!bytes_back) {
+				out->phase_stale = true;
+				chan->flush_pending = true;
+			}
+			return rc;
+		}
+	}
+
+	/*
+	 * Restart the divider phase so the delay applies against a known edge.
+	 */
+	rc = sit9531x_output_phase_flush(sitdev, pll_idx);
+
+cache:
+	/* Cache what the registers hold */
+	out->phase_adj = phase_adj;
+	out->phase_stale = false;
+	out->retime_pending = false;
+
+	/*
+	 * A delay that quantized to zero needs no re-time: the quantum is the
+	 * VCO cycle, not the output period.
+	 */
+	out->phase_armed = phase_adj != 0;
+
+	if (chan->flush_pending)
+		return rc;
+	if (rc)
+		dev_warn(sitdev->dev,
+			 "out%u: delay applied, the flush left something to put back (%d)\n",
+			 out_idx, rc);
+
+	return 0;
+}
 
 /*
  * Clear the write-1-to-clear notification registers on page 0 and the clock
@@ -2389,6 +3000,9 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 	}
 
 	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		bool beyond;
+		s64 phase_ps;
+
 		mutex_lock(&sitdev->multiop_lock);
 		rc = sit9531x_out_state_fetch(sitdev, i);
 		mutex_unlock(&sitdev->multiop_lock);
@@ -2397,6 +3011,23 @@ static int sit9531x_dev_state_fetch(struct sit9531x_dev *sitdev)
 				"Failed to fetch output %u state: %d\n", i, rc);
 			return rc;
 		}
+
+		/*
+		 * The profile can set a delay; read it so a request of 0 ps can
+		 * clear it.
+		 */
+		mutex_lock(&sitdev->multiop_lock);
+		rc = sit9531x_output_phase_read(sitdev, i, &phase_ps, &beyond);
+		mutex_unlock(&sitdev->multiop_lock);
+		if (!rc) {
+			sitdev->out[i].phase_adj = phase_ps;
+			sitdev->out[i].phase_armed = !beyond && phase_ps;
+		} else if (rc != -ENODATA) {
+			dev_err(sitdev->dev,
+				"Failed to read output %u delay: %d\n",
+				i, rc);
+			return rc;
+		}
 	}
 
 	mutex_lock(&sitdev->multiop_lock);
@@ -2520,6 +3151,25 @@ static void sit9531x_ho_release_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
 		 'A' + pll_idx);
 }
 
+/*
+ * Re-time the output delay after a failed rate change. Caller must hold
+ * sitdev->multiop_lock.
+ */
+static void sit9531x_retime_retry(struct sit9531x_dev *sitdev, u8 out_idx)
+{
+	struct sit9531x_out *out = &sitdev->out[out_idx];
+	s64 was;
+
+	if (!out->retime_pending)
+		return;
+
+	was = out->phase_adj;
+	if (!sit9531x_output_phase_adjust_set(sitdev, out_idx, out->retime_ps))
+		dev_info(sitdev->dev, "out%u: delay re-timed\n", out_idx);
+	if (out->phase_adj != was || out->phase_stale)
+		out->ntf_pending = true;
+}
+
 /*
  * Write back the priority table after a failed write. Caller must hold
  * sitdev->multiop_lock.
@@ -2590,6 +3240,18 @@ static void sit9531x_reselect(struct sit9531x_dev *sitdev, u8 pll_idx)
 		 'A' + pll_idx);
 }
 
+/* Run a pending output-divider flush. Caller must hold sitdev->multiop_lock. */
+static void sit9531x_flush_retry(struct sit9531x_dev *sitdev, u8 pll_idx)
+{
+	if (!sitdev->chan[pll_idx].flush_pending)
+		return;
+
+	sit9531x_output_phase_flush(sitdev, pll_idx);
+	if (!sitdev->chan[pll_idx].flush_pending)
+		dev_info(sitdev->dev, "PLL%c: output dividers realigned\n",
+			 'A' + pll_idx);
+}
+
 /*
  * Retry pending writes, then read each PLL's state; return true if any state
  * read succeeds.
@@ -2603,11 +3265,20 @@ sit9531x_dev_chan_states_update(struct sit9531x_dev *sitdev)
 	/* Retry pending writes first so the reads below see their result */
 	if (sitdev->loops_open)
 		sit9531x_prg_commit(sitdev);
+	sit9531x_phase_flush_undo_retry(sitdev);
+	for (i = 0; i < sitdev->info->num_outputs; i++)
+		sit9531x_retime_retry(sitdev, i);
 
 	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);
+		/*
+		 * A flush pulse sent into open loops may be lost when they
+		 * re-lock.
+		 */
+		if (!sitdev->loops_open)
+			sit9531x_flush_retry(sitdev, i);
 		rc = sit9531x_chan_state_fetch(sitdev, i);
 		if (rc) {
 			sitdev->chan[i].stale = true;
@@ -2925,15 +3596,46 @@ static void sit9531x_dev_stop(struct sit9531x_dev *sitdev)
 			dev_warn(sitdev->dev,
 				 "PLL%c left in forced holdover\n", 'A' + i);
 	}
+
+	sit9531x_phase_flush_undo_retry(sitdev);
+	if (sitdev->flush_ctrl_pending)
+		dev_warn(sitdev->dev,
+			 "phase flush trigger source select not put back\n");
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (!(sitdev->flush_arm_pending & BIT(i)))
+			continue;
+		dev_warn(sitdev->dev, "PLL%c: on-demand phase flush left %s\n",
+			 'A' + i,
+			 (sitdev->flush_armed & BIT(i)) ? "parked" : "armed");
+	}
 	mutex_unlock(&sitdev->multiop_lock);
 
+	/*
+	 * Re-time first, since it re-locks the loops; the flush runs only into
+	 * locked loops.
+	 */
 	mutex_lock(&sitdev->multiop_lock);
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		sit9531x_retime_retry(sitdev, i);
+		if (sitdev->out[i].retime_pending)
+			dev_warn(sitdev->dev,
+				 "out%u: delay left not re-timed to its rate\n",
+				 i);
+	}
 	if (sitdev->loops_open) {
 		sit9531x_prg_commit(sitdev);
 		if (sitdev->loops_open)
 			dev_warn(sitdev->dev,
 				 "output loops left unlocked at removal\n");
 	}
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		if (!sitdev->loops_open)
+			sit9531x_flush_retry(sitdev, i);
+		if (sitdev->chan[i].flush_pending)
+			dev_warn(sitdev->dev,
+				 "PLL%c: output dividers left unaligned\n",
+				 'A' + i);
+	}
 	mutex_unlock(&sitdev->multiop_lock);
 }
 
diff --git a/drivers/dpll/sit9531x/core.h b/drivers/dpll/sit9531x/core.h
index 4f3f74ac8d7c..bbf5026f945d 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -27,6 +27,8 @@
 #define SIT9531X_MAX_INPUTS		8
 #define SIT9531X_NUM_INPUT_PAIRS	(SIT9531X_MAX_INPUTS / 2)
 #define SIT9531X_MAX_OUTPUTS		12
+/* Output phase-adjust range advertised to the core, +/-1 ms in ps */
+#define SIT9531X_OUT_PHASE_ADJ_MAX_PS	1000000000
 /*
  * INTSYNC, the inter-PLL sync line, is two pins: an input pin on the PLL that
  * locks to it, numbered after the inputs and the xtal, and an output pin on the
@@ -109,6 +111,11 @@ 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)
+ * @phase_stale: programmed delay may differ from @phase_adj
+ * @phase_armed: non-zero in-range delay programmed; a rate change re-times it
+ * @phase_adj: realized delay in whole VCO cycles plus 30 ps steps, with the
+ *   requested sign; an out-of-range profile delay is held as the positive
+ *   decode
  * @retime_pending: re-timed delay not yet applied; the poll applies @retime_ps
  * @retime_ps: delay to apply while @retime_pending
  */
@@ -121,6 +128,9 @@ struct sit9531x_out {
 	bool		part_muted;
 	bool		routed;
 	u8		pll_idx;
+	s64		phase_adj;
+	bool		phase_armed;
+	bool		phase_stale;
 	bool		retime_pending;
 	s32		retime_ps;
 };
@@ -296,6 +306,11 @@ int sit9531x_output_freq_set(struct sit9531x_dev *sitdev, u8 out_idx,
 int sit9531x_output_freq_get(struct sit9531x_dev *sitdev, u8 out_idx,
 			     u64 *frequency);
 
+int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
+			       s64 *phase_ps, bool *beyond);
+int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
+				     u8 out_idx, s32 phase_ps);
+
 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 a32d37a396b1..e6271ebd47ac 100644
--- a/drivers/dpll/sit9531x/dpll.c
+++ b/drivers/dpll/sit9531x/dpll.c
@@ -611,6 +611,7 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
 	struct sit9531x_dev *sitdev = sitdpll->dev;
 	bool moved = false;
 	u64 was, hz;
+	s64 phase_was;
 	u8 actual_pll;
 	int rc;
 
@@ -618,10 +619,15 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *pin,
 
 	mutex_lock(&sitdev->multiop_lock);
 	was = sitdev->out[dpin->id].freq;
+	phase_was = sitdev->out[dpin->id].phase_adj;
 	rc = sit9531x_output_freq_set(sitdev, dpin->id, actual_pll,
 				      frequency);
 	if (rc) {
-		if (!sit9531x_output_freq_get(sitdev, dpin->id, &hz))
+		moved = sitdev->out[dpin->id].phase_stale ||
+			sitdev->out[dpin->id].phase_adj != phase_was;
+		if (sit9531x_output_freq_get(sitdev, dpin->id, &hz))
+			moved = true;
+		else
 			moved |= hz != was;
 	}
 	mutex_unlock(&sitdev->multiop_lock);
@@ -632,8 +638,11 @@ sit9531x_dpll_output_pin_frequency_set(const struct dpll_pin *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);
+		if (moved && dpll_pin && __dpll_pin_change_ntf(dpll_pin)) {
+			mutex_lock(&sitdev->multiop_lock);
+			sitdev->out[dpin->id].ntf_pending = true;
+			mutex_unlock(&sitdev->multiop_lock);
+		}
 	}
 
 	return rc;
@@ -741,12 +750,102 @@ sit9531x_dpll_output_pin_state_on_dpll_set(const struct dpll_pin *pin,
 	return rc;
 }
 
+/*
+ * Report the delay as programmed after quantization, including one whose flush
+ * is pending.
+ */
+static int
+sit9531x_dpll_output_pin_phase_adjust_get(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 *phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	s64 phase_adj;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	if (sitdev->out[dpin->id].phase_stale) {
+		bool beyond;
+		s64 phase_ps;
+
+		rc = sit9531x_output_phase_read(sitdev, dpin->id, &phase_ps,
+						&beyond);
+		if (rc) {
+			mutex_unlock(&sitdev->multiop_lock);
+			NL_SET_ERR_MSG(extack,
+				       "Output delay could not be read back");
+			return rc;
+		}
+		sitdev->out[dpin->id].phase_adj = phase_ps;
+		sitdev->out[dpin->id].phase_armed = !beyond && phase_ps;
+		sitdev->out[dpin->id].phase_stale = false;
+	}
+	phase_adj = sit9531x_out_state_get(sitdev, dpin->id)->phase_adj;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	/*
+	 * A profile delay on a slow output can exceed the s32 attribute; leave
+	 * it out.
+	 */
+	if (phase_adj > S32_MAX)
+		return -ENODATA;
+	*phase_adjust = phase_adj;
+
+	return 0;
+}
+
+/* Program the per-output PRG_RST_DELAY registers */
+static int
+sit9531x_dpll_output_pin_phase_adjust_set(const struct dpll_pin *pin,
+					  void *pin_priv,
+					  const struct dpll_device *dpll,
+					  void *dpll_priv, s32 phase_adjust,
+					  struct netlink_ext_ack *extack)
+{
+	struct sit9531x_dpll_pin *dpin = pin_priv;
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	const struct sit9531x_out *out;
+	bool moved;
+	s64 was;
+	int rc;
+
+	out = sit9531x_out_state_get(sitdev, dpin->id);
+
+	mutex_lock(&sitdev->multiop_lock);
+	was = out->phase_adj;
+	rc = sit9531x_output_phase_adjust_set(sitdev, dpin->id, phase_adjust);
+	moved = out->phase_adj != was || out->phase_stale;
+	mutex_unlock(&sitdev->multiop_lock);
+
+	if (rc) {
+		struct dpll_pin *dpll_pin;
+
+		NL_SET_ERR_MSG(extack, "Phase adjust 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)) {
+			mutex_lock(&sitdev->multiop_lock);
+			sitdev->out[dpin->id].ntf_pending = true;
+			mutex_unlock(&sitdev->multiop_lock);
+		}
+	}
+
+	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,
 	.state_on_dpll_get	= sit9531x_dpll_output_pin_state_on_dpll_get,
 	.state_on_dpll_set	= sit9531x_dpll_output_pin_state_on_dpll_set,
+	.phase_adjust_get	= sit9531x_dpll_output_pin_phase_adjust_get,
+	.phase_adjust_set	= sit9531x_dpll_output_pin_phase_adjust_set,
 };
 
 const struct dpll_pin_ops *
@@ -827,6 +926,17 @@ sit9531x_dpll_output_ntf_pending(struct sit9531x_dev *sitdev,
 		return false;
 	if (out->state_stale && sit9531x_output_state_refresh(sitdev, pin->id))
 		return false;
+	if (out->phase_stale) {
+		bool beyond;
+		s64 phase_ps;
+
+		if (sit9531x_output_phase_read(sitdev, pin->id, &phase_ps,
+					       &beyond))
+			return false;
+		out->phase_adj = phase_ps;
+		out->phase_armed = !beyond && phase_ps;
+		out->phase_stale = false;
+	}
 
 	out->ntf_pending = false;
 
diff --git a/drivers/dpll/sit9531x/prop.c b/drivers/dpll/sit9531x/prop.c
index f726fbe037f3..6cf5fc3459dd 100644
--- a/drivers/dpll/sit9531x/prop.c
+++ b/drivers/dpll/sit9531x/prop.c
@@ -170,6 +170,20 @@ sit9531x_pin_props_get(struct sit9531x_dev *sitdev,
 		props->dpll_props.capabilities =
 			DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
 		curr_freq = 0;
+
+		/*
+		 * The part takes a delay anywhere within the output period;
+		 * +/-1 ms is a round figure within the s32 ps attribute.
+		 */
+		props->dpll_props.phase_range.min =
+			-SIT9531X_OUT_PHASE_ADJ_MAX_PS;
+		props->dpll_props.phase_range.max =
+			SIT9531X_OUT_PHASE_ADJ_MAX_PS;
+		/*
+		 * The 30 ps fine step is not advertised; requests are rounded
+		 * to the nearest achievable delay.
+		 */
+		props->dpll_props.phase_gran = 1;
 	}
 
 	sit9531x_prop_pin_package_label_set(sitdev, props, dir, index);
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index 3f8ceb7e9798..8c76609b99c5 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -158,6 +158,35 @@
 #define SIT9531X_DEBUG_UNLOCK_VAL		0xC3
 #define SIT9531X_DEBUG_LOCK_VAL			0x00
 
+/*
+ * Per-output phase delay: 34-bit coarse in VCO cycles plus a 3-bit fine
+ * field in 30 ps steps, in a five-byte block PROG6..PROG2:
+ *
+ *   base + 0     PROG6  [7:5] OPSTG_VCASC_BUMP (preserve)
+ *                       [4:2] PRG_RST_FINE_DELAY[2:0]
+ *                       [1:0] PRG_RST_DELAY[33:32]
+ *   base + 1..4  PROG5..PROG2  PRG_RST_DELAY[31:0], MSB first
+ *
+ * Block base is 0x15 + 16 * (slot % 6), slot from clkout_map[]; slots 0-5
+ * on page 3, 6-11 on page 4.
+ */
+#define SIT9531X_OUT_PRG_DELAY_BASE		0x15
+#define SIT9531X_OUT_PRG_SLOT_STRIDE		0x10
+#define SIT9531X_OUT_PRG_BYTES			5
+#define SIT9531X_OUT_PRG_OPSTG_MASK		0xE0
+#define SIT9531X_OUT_PRG_FINE_SHIFT		2
+#define SIT9531X_OUT_PRG_FINE_MASK		0x1C	/* bits [4:2] */
+#define SIT9531X_OUT_PRG_COARSE_HI_MASK		0x03	/* bits [1:0] */
+/*
+ * The divider takes two VCO cycles to act on a programmed delay; the encoded
+ * value includes them, as in the reference flow.
+ */
+#define SIT9531X_OUT_PRG_DIVO_CYCLES	2
+
+#define SIT9531X_OUT_PRG_FINE_STEP_PS		30
+#define SIT9531X_OUT_PRG_FINE_MAX		7	/* 3-bit field */
+#define SIT9531X_OUT_PRG_COARSE_BITS		34
+
 /*
  * Register-triggered phase flush: DIVO_PHASE_SEL_REG selects the register
  * trigger; pulsing DIVO_PHASE_TRIG high then low flushes. Preserve the OEb
-- 
2.39.2 (Apple Git-143)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH net-next v15 13/13] dpll: sit9531x: model the inter-PLL sync line as a pair of pins
  2026-11-10 20:26 [PATCH net-next v15 00/13] dpll: add SiTime SiT9531x DPLL clock driver Ali Rouhi
                   ` (11 preceding siblings ...)
  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 ` Ali Rouhi
  12 siblings, 0 replies; 15+ messages in thread
From: Ali Rouhi @ 2026-11-10 20:26 UTC (permalink / raw)
  To: jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, ivecera, kuba, pabeni,
	robh, krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

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

The part has an internal line by which one PLL can drive the others as a
reference. Model it as two pins: an output pin on the source PLL, whose
state says whether it drives the line, and an input pin on each
destination, selectable like any input and reporting no signal while
no PLL drives the line.

Taking the line writes a pattern to the source's EXT page and sets a
global enable; giving it up writes the disable pattern. The owner is
read back from the pages, and a line enabled from a page the driver does
not recognize, or from two pages, refuses every connect.

Resume re-reads the line owner, the output mutes and the output delays,
which a profile reload may have changed.

Signed-off-by: Oleg Zadorozhnyi <Oleg.Zadorozhnyi@devoxsoftware.com>
Signed-off-by: Ali Rouhi <arouhi@sitime.com>
---
 drivers/dpll/sit9531x/core.c | 708 ++++++++++++++++++++++++++++++++++-
 drivers/dpll/sit9531x/core.h |  32 ++
 drivers/dpll/sit9531x/dpll.c | 398 ++++++++++++++++++--
 drivers/dpll/sit9531x/regs.h |   3 +
 4 files changed, 1112 insertions(+), 29 deletions(-)

diff --git a/drivers/dpll/sit9531x/core.c b/drivers/dpll/sit9531x/core.c
index 8f305cc27737..e3ce7b6cad6b 100644
--- a/drivers/dpll/sit9531x/core.c
+++ b/drivers/dpll/sit9531x/core.c
@@ -857,6 +857,8 @@ static bool sit9531x_prio_src_live(const struct sit9531x_dev *sitdev, u8 src)
 {
 	u8 index = sit9531x_hw_src_input(src);
 
+	if (index == SIT9531X_INTSYNC_PIN_ID)
+		return sit9531x_intsync_driven(sitdev);
 	if (index >= sitdev->info->num_inputs)
 		return true;
 
@@ -1091,6 +1093,18 @@ static u8 sit9531x_prio_activesel_pick(struct sit9531x_dev *sitdev,
 	return srcs[0];
 }
 
+static bool sit9531x_prio_table_lists_intsync(const u8 *srcs)
+{
+	u8 slot;
+
+	for (slot = 0; slot < SIT9531X_PRIO_MAX_SLOTS; slot++)
+		if ((srcs[slot] & SIT9531X_PRIO_NIBBLE_MASK) ==
+		    SIT9531X_PRIO_SRC_INTSYNC)
+			return true;
+
+	return false;
+}
+
 static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
 				      const u8 *srcs)
 {
@@ -1107,6 +1121,11 @@ static int sit9531x_prio_table_commit(struct sit9531x_dev *sitdev, u8 pll_idx,
 		if (sit9531x_prio_src_usable(srcs[slot]))
 			empty = false;
 
+	if (sitdev->intsync_stale &&
+	    (sit9531x_prio_table_lists_intsync(srcs) ||
+	     sit9531x_prio_table_lists_intsync(chan->prio_srcs)))
+		sit9531x_intsync_refresh(sitdev);
+
 	memcpy(back, chan->table_pending ? chan->table_back : chan->prio_srcs,
 	       sizeof(back));
 
@@ -2585,6 +2604,556 @@ static int sit9531x_clear_notifications(struct sit9531x_dev *sitdev)
 	return 0;
 }
 
+/*
+ * Close the debug window on a PLL's EXT page; the key opens every debug
+ * register on the page while it holds the unlock value.
+ */
+static int sit9531x_intsync_debug_lock(struct sit9531x_dev *sitdev, u8 ext_page)
+{
+	return sit9531x_write_close(sitdev,
+				    SIT9531X_REG(ext_page,
+						 SIT9531X_PLL_REG_DEBUG),
+				    SIT9531X_PLL_DEBUG_LOCK);
+}
+
+/*
+ * Values written to the source PLL's EXT page to enable or disable INTSYNC,
+ * which locks the frequency PLL to the 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 },
+};
+
+/*
+ * Find the PLL whose EXT page holds the INTSYNC enable pattern, whole or in
+ * part. Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev)
+{
+	s8 src = -1, partial = -1;
+	int rc, ret, n_en, n_dis;
+	u8 pll, ext_page;
+	u8 n_pages = 0;
+	unsigned int i;
+	bool enabled;
+	u8 global;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	rc = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL, &global);
+	if (rc)
+		return rc;
+	enabled = global & BIT(SIT9531X_INTSYNC_EN_BIT);
+
+	/*
+	 * Read the EXT pages even with the global enable clear; a page can hold
+	 * the enable pattern under a clear bit.
+	 */
+	for (pll = 0; pll < SIT9531X_NUM_PLLS; pll++) {
+		ext_page = SIT9531X_PLL_EXT_PAGE(pll);
+
+		/*
+		 * Open the EXT page's debug window to read the INTSYNC
+		 * registers.
+		 */
+		rc = sit9531x_write_u8(sitdev,
+				       SIT9531X_REG(ext_page,
+						    SIT9531X_PLL_REG_DEBUG),
+				       SIT9531X_PLL_DEBUG_UNLOCK);
+		if (rc) {
+			sit9531x_intsync_debug_lock(sitdev, ext_page);
+			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;
+
+		/*
+		 * A page with the disable pattern or with other values is not a
+		 * source.
+		 */
+		if (!n_en || n_en + n_dis != ARRAY_SIZE(intsync_config))
+			continue;
+		n_pages++;
+
+		if (n_dis || !enabled) {
+			/*
+			 * A page holding both patterns, or the whole enable
+			 * pattern under a clear global enable, does not drive
+			 * the line.
+			 */
+			if (partial < 0)
+				partial = pll;
+			continue;
+		}
+
+		if (src < 0)
+			src = pll;
+	}
+
+	/*
+	 * Record a PLL with a partial pattern, though it does not drive the
+	 * line, so that no other PLL is enabled alongside it.
+	 */
+	sitdev->intsync_partial = src < 0 && partial >= 0;
+	if (sitdev->intsync_partial) {
+		dev_warn(sitdev->dev,
+			 "PLL%c holds an INTSYNC source configuration that does not drive the line\n",
+			 'A' + partial);
+		src = partial;
+	}
+
+	/*
+	 * With more than one page holding the pattern, refuse every connect; an
+	 * enable on one would also set the global enable over the other.
+	 */
+	sitdev->intsync_conflict = n_pages > 1;
+	if (sitdev->intsync_conflict)
+		dev_warn(sitdev->dev,
+			 "%u PLLs hold the INTSYNC source pattern; recording PLL%c\n",
+			 n_pages, 'A' + src);
+
+	sitdev->intsync_src = src;
+	sitdev->intsync_foreign = enabled && src < 0;
+
+	return 0;
+}
+
+/**
+ * sit9531x_intsync_readback - re-read the sync source record from the part
+ * @sitdev: device pointer
+ *
+ * A page reads the same latched or not, so a recorded partial source is kept
+ * unless the scan resolves it. Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 on success, <0 on a read error
+ */
+int sit9531x_intsync_readback(struct sit9531x_dev *sitdev)
+{
+	bool partial = sitdev->intsync_partial;
+	s8 src = sitdev->intsync_src;
+	int rc;
+
+	rc = sit9531x_intsync_src_detect(sitdev);
+	if (rc)
+		return rc;
+
+	if (partial && (sitdev->intsync_src < 0 ||
+			(sitdev->intsync_src == src &&
+			 !sitdev->intsync_partial))) {
+		sitdev->intsync_src = src;
+		sitdev->intsync_partial = true;
+		sitdev->intsync_foreign = false;
+	}
+
+	return 0;
+}
+
+/**
+ * sit9531x_intsync_refresh - re-read a stale sync source record
+ * @sitdev: device pointer
+ *
+ * Caller must hold sitdev->multiop_lock.
+ *
+ * Return: 0 when the record is current, <0 when the scan fails
+ */
+int sit9531x_intsync_refresh(struct sit9531x_dev *sitdev)
+{
+	s8 owner;
+	int rc;
+
+	if (!sitdev->intsync_stale)
+		return 0;
+
+	owner = sit9531x_intsync_owner(sitdev);
+	rc = sit9531x_intsync_readback(sitdev);
+	if (rc)
+		return rc;
+	sitdev->intsync_stale = false;
+	if (owner != sit9531x_intsync_owner(sitdev))
+		sitdev->intsync_ntf_pending = true;
+
+	return 0;
+}
+
+/*
+ * Check if the inter-PLL sync line carries a signal, from the recorded source
+ * or a PLL locked to the line. Caller must hold sitdev->multiop_lock.
+ */
+bool sit9531x_intsync_driven(const struct sit9531x_dev *sitdev)
+{
+	const struct sit9531x_chan *chan;
+	u8 pll;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (sitdev->intsync_src >= 0 && !sitdev->intsync_partial &&
+	    !sitdev->intsync_stale)
+		return true;
+
+	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 &&
+		    !chan->stale && !chan->intsync_dropped)
+			return true;
+	}
+
+	return false;
+}
+
+static int __sit9531x_intsync_disable(struct sit9531x_dev *sitdev,
+				      u8 src_pll_idx, bool restore,
+				      bool *owner_kept, bool *touched);
+
+/*
+ * Set the global enable and latch the INTSYNC source pattern into the PLL's EXT
+ * page. Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx,
+			    bool *rollback_failed, bool *touched)
+{
+	u8 ext_page, val;
+	int rc, lock_rc;
+	unsigned int i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (rollback_failed)
+		*rollback_failed = false;
+	if (touched)
+		*touched = false;
+	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;
+	if (touched)
+		*touched = true;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val | BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		goto err_disable;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	usleep_range(1000, 2000);
+	if (rc)
+		goto relock_err;
+
+	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;
+	}
+
+	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:
+	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:
+	{
+		int rollback_rc;
+
+		rollback_rc = __sit9531x_intsync_disable(sitdev, src_pll_idx,
+							 false, NULL, NULL);
+		if (rollback_rc) {
+			dev_warn(sitdev->dev,
+				 "INTSYNC rollback failed after enable error: %d (original %d)\n",
+				 rollback_rc, rc);
+			if (rollback_failed)
+				*rollback_failed = true;
+		}
+	}
+
+	return rc;
+}
+
+/*
+ * Check if every INTSYNC register of an EXT page holds its disable value. The
+ * page's debug window must be open.
+ */
+static int sit9531x_intsync_page_off(struct sit9531x_dev *sitdev,
+				     u8 ext_page, bool *off)
+{
+	unsigned int i;
+	u8 val;
+	int rc;
+
+	*off = false;
+	for (i = 0; i < ARRAY_SIZE(intsync_config); i++) {
+		rc = sit9531x_read_u8(sitdev,
+				      SIT9531X_REG(ext_page,
+						   intsync_config[i].offset),
+				      &val);
+		if (rc)
+			return rc;
+		if (val != intsync_config[i].dis_val)
+			return 0;
+	}
+	*off = true;
+
+	return 0;
+}
+
+static int __sit9531x_intsync_disable(struct sit9531x_dev *sitdev,
+				      u8 src_pll_idx, bool restore,
+				      bool *owner_kept, bool *touched)
+{
+	bool in_window = false, was_enabled;
+	u8 ext_page, val;
+	int rc, lock_rc;
+	unsigned int i;
+
+	lockdep_assert_held(&sitdev->multiop_lock);
+
+	if (owner_kept)
+		*owner_kept = false;
+	if (touched)
+		*touched = false;
+	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;
+	was_enabled = val & BIT(SIT9531X_INTSYNC_EN_BIT);
+	if (touched)
+		*touched = true;
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+			       val & ~BIT(SIT9531X_INTSYNC_EN_BIT));
+	if (rc)
+		goto restore_global;
+	/* A sample taken before this write can still show a lock. */
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++)
+		if (sitdev->chan[i].routed_ref == SIT9531X_INTSYNC_PIN_ID)
+			sitdev->chan[i].intsync_dropped = true;
+
+	rc = sit9531x_write_u8(sitdev, SIT9531X_REG_GLOBAL_UPDATE,
+			       SIT9531X_SMALL_UPDATE_CMD);
+	usleep_range(1000, 2000);
+	if (rc)
+		goto restore_global;
+
+	rc = sit9531x_write_u8(sitdev,
+			       SIT9531X_REG(ext_page,
+					    SIT9531X_PLL_REG_DEBUG),
+			       SIT9531X_PLL_DEBUG_UNLOCK);
+	if (rc)
+		goto restore_global;
+	in_window = true;
+
+	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;
+	}
+
+	rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+	if (rc && owner_kept)
+		*owner_kept = true;
+
+	goto relock;
+
+restore_global:
+	/*
+	 * Restore the global enable, or finish the disable if the page holds
+	 * the disable pattern.
+	 */
+	if (restore && in_window) {
+		bool off;
+
+		if (sit9531x_intsync_page_off(sitdev, ext_page, &off)) {
+			if (owner_kept)
+				*owner_kept = true;
+		} else if (off) {
+			rc = sit9531x_pll_small_update(sitdev, src_pll_idx);
+			if (rc && owner_kept)
+				*owner_kept = true;
+			goto relock;
+		}
+	}
+
+	if (restore && was_enabled) {
+		int ret;
+
+		ret = sit9531x_read_u8(sitdev, SIT9531X_REG_INTSYNC_GLOBAL,
+				       &val);
+		if (!ret)
+			ret = sit9531x_write_u8(sitdev,
+						SIT9531X_REG_INTSYNC_GLOBAL,
+						val |
+						BIT(SIT9531X_INTSYNC_EN_BIT));
+		if (!ret) {
+			/* The page-0 small update applies the global enable */
+			ret = sit9531x_write_close(sitdev,
+						   SIT9531X_REG_GLOBAL_UPDATE,
+						   SIT9531X_SMALL_UPDATE_CMD);
+			usleep_range(1000, 2000);
+		}
+		if (ret) {
+			dev_warn(sitdev->dev,
+				 "PLL%c: INTSYNC enable not put back after a failed disable: %d\n",
+				 'A' + src_pll_idx, ret);
+			if (owner_kept)
+				*owner_kept = true;
+		}
+	}
+
+relock:
+	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;
+}
+
+/*
+ * Clear the global enable and latch the INTSYNC disable pattern into the PLL's
+ * EXT page. Caller must hold sitdev->multiop_lock.
+ */
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx,
+			     bool *owner_kept, bool *touched)
+{
+	return __sit9531x_intsync_disable(sitdev, src_pll_idx, true,
+					  owner_kept, touched);
+}
+
+/*
+ * Notify the output pins in @outs and, when @src, the INTSYNC source pin. Call
+ * without multiop_lock: the notification takes the subsystem locks, which are
+ * taken before multiop_lock.
+ */
+static void sit9531x_drive_pins_ntf(struct sit9531x_dev *sitdev, u16 outs,
+				    bool src)
+{
+	struct sit9531x_dpll *sitdpll;
+	struct sit9531x_dpll_pin *pin;
+	struct dpll_pin *dpll_pin;
+	int rc;
+
+	if (!outs && !src)
+		return;
+
+	list_for_each_entry(sitdpll, &sitdev->dplls, list) {
+		list_for_each_entry(pin, &sitdpll->pins, list) {
+			if (pin->dir != DPLL_PIN_DIRECTION_OUTPUT)
+				continue;
+			if (pin->id == SIT9531X_INTSYNC_OUT_PIN_ID ?
+			    !src : !(outs & BIT(pin->id)))
+				continue;
+			dpll_pin = READ_ONCE(pin->dpll_pin);
+			if (!dpll_pin)
+				continue;
+			rc = dpll_pin_change_ntf(dpll_pin);
+			/*
+			 * The source pin is one pin registered on every DPLL.
+			 */
+			if (pin->id == SIT9531X_INTSYNC_OUT_PIN_ID)
+				src = false;
+			if (!rc)
+				continue;
+			mutex_lock(&sitdev->multiop_lock);
+			if (pin->id == SIT9531X_INTSYNC_OUT_PIN_ID)
+				sitdev->intsync_ntf_pending = true;
+			else
+				sitdev->out[pin->id].ntf_pending = true;
+			mutex_unlock(&sitdev->multiop_lock);
+		}
+	}
+}
+
+/*
+ * Re-read a stale sync source record; return true when a source pin
+ * notification is pending. Caller must hold sitdev->multiop_lock.
+ */
+static bool sit9531x_intsync_stale_retry(struct sit9531x_dev *sitdev)
+{
+	if (sit9531x_intsync_refresh(sitdev))
+		return false;
+
+	if (!sitdev->intsync_ntf_pending)
+		return false;
+	sitdev->intsync_ntf_pending = false;
+
+	return true;
+}
+
 /**
  * sit9531x_phase_offset_read - read phase difference via TDC
  * @sitdev: device pointer
@@ -3022,6 +3591,7 @@ static int sit9531x_chan_state_fetch(struct sit9531x_dev *sitdev, u8 pll_idx)
 	chan->ho_freeze = !!(ho_freeze & BIT(pll_idx));
 	chan->ho_valid = !!(pll_status_1 & SIT9531X_PLL_STATUS_1_HO_VALID);
 	chan->stale = false;
+	chan->intsync_dropped = false;
 
 	return 0;
 }
@@ -3197,6 +3767,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 beyond;
 		s64 phase_ps;
@@ -3522,6 +4101,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 src_moved;
 	bool answered;
 	int rc;
 
@@ -3531,16 +4111,16 @@ static void sit9531x_dev_periodic_work(struct kthread_work *work)
 	 */
 	mutex_lock(&sitdev->multiop_lock);
 	answered = sit9531x_dev_ref_states_update(sitdev);
+	src_moved = sit9531x_intsync_stale_retry(sitdev);
 	answered |= sit9531x_dev_chan_states_update(sitdev);
 	mutex_unlock(&sitdev->multiop_lock);
 
 	list_for_each_entry(sitdpll, &sitdev->dplls, list)
 		sit9531x_dpll_changes_check(sitdpll);
+	/* The source pin is not a selection pin; the walk above skips it. */
+	sit9531x_drive_pins_ntf(sitdev, 0, src_moved);
 
-	/*
-	 * Clear the W1C notification latches on pages 0 and 6 after acting on
-	 * them; while set they keep INTRB asserted.
-	 */
+	/* The latches are W1C; left set, they keep INTRB asserted. */
 	mutex_lock(&sitdev->multiop_lock);
 	rc = sit9531x_clear_notifications(sitdev);
 	mutex_unlock(&sitdev->multiop_lock);
@@ -4015,13 +4595,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;
@@ -4420,12 +5000,128 @@ static int sit9531x_suspend(struct device *dev)
 static int sit9531x_resume(struct device *dev)
 {
 	struct sit9531x_dev *sitdev = dev_get_drvdata(dev);
+	u16 outs_was = 0, outs_now = 0, outs_unread = 0, outs_moved = 0;
+	bool src_moved;
+	s8 owner;
+	int rc;
+	u8 i;
 
 	/* After power loss the part is on page 0, not the cached page */
 	sit9531x_page_cache_drop(sitdev);
+
+	/*
+	 * A part that lost power returns with its profile's INTSYNC routing and
+	 * output mutes, which the poll does not read; read them back. Arm one
+	 * re-select for a source lost and restored between samples.
+	 */
 	mutex_lock(&sitdev->multiop_lock);
 	sit9531x_reselect_arm(sitdev);
+	/*
+	 * Drop and report writes pending from before suspend: a reloaded
+	 * profile may have replaced their registers.
+	 */
+	for (i = 0; i < SIT9531X_NUM_PLLS; i++) {
+		struct sit9531x_chan *chan = &sitdev->chan[i];
+		bool retime = false;
+		u8 j, ho;
+
+		/*
+		 * A table write-back is pending only under forced holdover;
+		 * once that is cleared, the next poll reads the table from the
+		 * registers.
+		 */
+		if ((chan->table_pending || chan->ho_release_pending) &&
+		    !sit9531x_read_pll_u8(sitdev, i, SIT9531X_PLL_REG_HO_CTRL,
+					  &ho) &&
+		    !(ho & BIT(SIT9531X_PLL_HO_FORCE_BIT))) {
+			dev_warn(sitdev->dev,
+				 "PLL%c: forced holdover cleared over suspend, priority table restore dropped\n",
+				 'A' + i);
+			chan->table_pending = false;
+			chan->ho_release_pending = false;
+		}
+
+		for (j = 0; j < sitdev->info->num_outputs; j++) {
+			if (!sitdev->out[j].retime_pending ||
+			    sitdev->out[j].pll_idx != i)
+				continue;
+			sitdev->out[j].retime_pending = false;
+			retime = true;
+		}
+		if (!retime && !chan->flush_pending &&
+		    !(sitdev->flush_arm_pending & BIT(i)))
+			continue;
+		dev_warn(sitdev->dev,
+			 "PLL%c: pending before suspend, dropped:%s%s%s\n",
+			 'A' + i, retime ? " delay re-time" : "",
+			 chan->flush_pending ? " output realignment" : "",
+			 (sitdev->flush_arm_pending & BIT(i)) ?
+			 " on-demand flush enable" : "");
+		chan->flush_pending = false;
+		sitdev->flush_arm_pending &= ~BIT(i);
+	}
+	if (sitdev->flush_ctrl_pending) {
+		dev_warn(sitdev->dev,
+			 "flush trigger source select pending before suspend, dropped\n");
+		sitdev->flush_ctrl_pending = false;
+	}
+	owner = sit9531x_intsync_owner(sitdev);
+	/*
+	 * The delay registers are reloaded with the profile too; read them back
+	 * as probe does.
+	 */
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		struct sit9531x_out *out = &sitdev->out[i];
+		bool beyond;
+		s64 phase_ps;
+
+		if (!out->routed)
+			continue;
+		rc = sit9531x_output_phase_read(sitdev, i, &phase_ps,
+						&beyond);
+		if (rc) {
+			dev_warn(sitdev->dev,
+				 "Output %u delay not read on resume: %d\n",
+				 i, rc);
+			out->phase_stale = true;
+			outs_unread |= BIT(i);
+			continue;
+		}
+		if (out->phase_adj != phase_ps || out->phase_stale)
+			outs_moved |= BIT(i);
+		out->phase_adj = phase_ps;
+		out->phase_armed = !beyond && phase_ps;
+		out->phase_stale = false;
+	}
+	rc = sit9531x_intsync_readback(sitdev);
+	if (rc)
+		dev_warn(sitdev->dev,
+			 "INTSYNC source not re-detected on resume: %d\n", rc);
+	sitdev->intsync_stale = rc != 0;
+	src_moved = rc || owner != sit9531x_intsync_owner(sitdev);
+	for (i = 0; i < sitdev->info->num_outputs; i++) {
+		if (sitdev->out[i].enabled)
+			outs_was |= BIT(i);
+		/* The mute state of a request left unconfirmed is unknown */
+		if (sitdev->out[i].state_stale)
+			outs_unread |= BIT(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;
+			outs_unread |= BIT(i);
+		}
+		if (sitdev->out[i].enabled)
+			outs_now |= BIT(i);
+	}
 	mutex_unlock(&sitdev->multiop_lock);
+
+	sit9531x_drive_pins_ntf(sitdev,
+				(outs_was ^ outs_now) | outs_unread |
+				outs_moved, src_moved);
+
 	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 8718c463c560..7dc78ddb18e5 100644
--- a/drivers/dpll/sit9531x/core.h
+++ b/drivers/dpll/sit9531x/core.h
@@ -142,6 +142,8 @@ struct sit9531x_out {
  * @locked: PLL is active and its outer loop reports lock
  * @stale: last state read failed; the fields hold the previous sample and
  *   are not reported
+ * @intsync_dropped: driver turned INTSYNC off after this sample; a lock to
+ *   INTSYNC in it is not trusted
  * @mode: 0 = sync (outer loop enabled), 1 = free-run
  * @routed_ref: input the device routes to the PLL, as a logical index
  *   (INTSYNC: SIT9531X_INTSYNC_PIN_ID); SIT9531X_REF_INVALID if none
@@ -184,6 +186,7 @@ struct sit9531x_chan {
 	bool		active;
 	bool		locked;
 	bool		stale;
+	bool		intsync_dropped;
 	u8		mode;
 	u8		routed_ref;
 	bool		inner_lol;
@@ -233,6 +236,12 @@ struct sit9531x_chan {
  * @reset_gpio: optional reset line, NULL if absent
  * @irq: optional INTRB IRQ, 0 if not wired
  * @intsync_src: PLL sourcing INTSYNC, or -1
+ * @intsync_partial: @intsync_src has a partial source configuration or the
+ *   global enable clear; not driving
+ * @intsync_conflict: more than one PLL page holds the source pattern
+ * @intsync_foreign: global enable set with no page holding the pattern
+ * @intsync_stale: @intsync_src must be read back before use
+ * @intsync_ntf_pending: the poll must send a source pin notification
  * @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
@@ -270,6 +279,11 @@ struct sit9531x_dev {
 	int			irq;
 
 	s8			intsync_src;
+	bool			intsync_partial;
+	bool			intsync_conflict;
+	bool			intsync_foreign;
+	bool			intsync_stale;
+	bool			intsync_ntf_pending;
 
 	bool			loops_open;
 	bool			flush_ctrl_pending;
@@ -318,6 +332,24 @@ int sit9531x_output_phase_read(struct sit9531x_dev *sitdev, u8 out_idx,
 int sit9531x_output_phase_adjust_set(struct sit9531x_dev *sitdev,
 				     u8 out_idx, s32 phase_ps);
 
+/*
+ * Return the PLL driving INTSYNC with a complete source configuration, or -1.
+ * Caller must hold sitdev->multiop_lock.
+ */
+static inline s8 sit9531x_intsync_owner(const struct sit9531x_dev *sitdev)
+{
+	return sitdev->intsync_partial ? -1 : sitdev->intsync_src;
+}
+
+int sit9531x_intsync_enable(struct sit9531x_dev *sitdev, u8 src_pll_idx,
+			    bool *rollback_failed, bool *touched);
+int sit9531x_intsync_disable(struct sit9531x_dev *sitdev, u8 src_pll_idx,
+			     bool *owner_kept, bool *touched);
+int sit9531x_intsync_src_detect(struct sit9531x_dev *sitdev);
+int sit9531x_intsync_readback(struct sit9531x_dev *sitdev);
+int sit9531x_intsync_refresh(struct sit9531x_dev *sitdev);
+bool sit9531x_intsync_driven(const struct sit9531x_dev *sitdev);
+
 int sit9531x_chan_routed_ref_read(struct sit9531x_dev *sitdev, u8 pll_idx,
 				  u8 *ref);
 int sit9531x_input_mon_fetch(struct sit9531x_dev *sitdev);
diff --git a/drivers/dpll/sit9531x/dpll.c b/drivers/dpll/sit9531x/dpll.c
index ba4ac5e7d365..1bd2b962cdab 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)
 {
@@ -177,16 +191,18 @@ const struct dpll_device_ops sit9531x_dpll_device_ops = {
 };
 
 /*
- * 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. An output is
- * CONNECTED while driven and DISCONNECTED while muted; SELECTABLE is refused.
- * The XO pin is always CONNECTED.
+ * Pin-state contract. An input or the INTSYNC destination 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 source (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 INTSYNC destination has signal while the driver's record shows
+ * a PLL driving the line. An output or the INTSYNC source is CONNECTED while
+ * driven and DISCONNECTED otherwise; SELECTABLE is refused. The XO pin is
+ * always CONNECTED.
  */
 
 /* Caller must hold sitdev->multiop_lock. */
@@ -215,6 +231,22 @@ sit9531x_dpll_selection_state_get(struct sit9531x_dev *sitdev,
 		*state = DPLL_PIN_STATE_DISCONNECTED;
 }
 
+/*
+ * For INTSYNC, use the driver's record of a PLL driving or locked to the line
+ * in place of the LOS monitor. 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)
+		return !sit9531x_intsync_driven(sitdev);
+
+	return pin_id < sitdev->info->num_inputs &&
+	       sit9531x_ref_state_get(sitdev, pin_id)->los;
+}
+
 /* Caller must hold sitdev->multiop_lock. */
 static bool
 sit9531x_dpll_selection_active(struct sit9531x_dev *sitdev,
@@ -229,12 +261,7 @@ sit9531x_dpll_selection_active(struct sit9531x_dev *sitdev,
 	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)
+	if (sit9531x_dpll_selection_no_signal(sitdev, pin_id))
 		return false;
 
 	return true;
@@ -256,12 +283,13 @@ sit9531x_dpll_selection_operstate_get(struct sit9531x_dev *sitdev,
 		return;
 	}
 
+	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;
@@ -587,8 +615,7 @@ sit9531x_dpll_input_pin_phase_offset_get(const struct dpll_pin *pin,
 				       "Failed to read the input clock monitors");
 			return rc;
 		}
-		if (dpin->id < sitdev->info->num_inputs &&
-		    sit9531x_ref_state_get(sitdev, dpin->id)->los)
+		if (sit9531x_dpll_selection_no_signal(sitdev, dpin->id))
 			rc = -ENODATA;
 	}
 	mutex_unlock(&sitdev->multiop_lock);
@@ -662,6 +689,325 @@ sit9531x_dpll_output_pin_direction_get(const struct dpll_pin *pin,
 				       enum dpll_pin_direction *direction,
 				       struct netlink_ext_ack *extack);
 
+/* 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;
+}
+
+/* Caller must hold sitdev->multiop_lock. */
+static int sit9531x_dpll_intsync_refresh(struct sit9531x_dev *sitdev,
+					 struct netlink_ext_ack *extack)
+{
+	int rc;
+
+	rc = sit9531x_intsync_refresh(sitdev);
+	if (rc)
+		NL_SET_ERR_MSG(extack, "INTSYNC source could not be read back");
+
+	return rc;
+}
+
+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;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_dpll_intsync_refresh(sitdev, extack);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+	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;
+}
+
+/*
+ * Driving the line is an explicit routing, not a selection candidate, so
+ * SELECTABLE is refused.
+ */
+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;
+	bool was_connected, moved, owner_kept = false;
+	int rc = 0, detect_rc = 0;
+	bool changed = false;
+	u8 hw_src;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	rc = sit9531x_dpll_intsync_refresh(sitdev, extack);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+	was_connected = sit9531x_dpll_intsync_src_connected(sitdev, sitdpll);
+
+	switch (state) {
+	case DPLL_PIN_STATE_CONNECTED:
+		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;
+		}
+		/*
+		 * The enable drives the line from every page holding the source
+		 * pattern.
+		 */
+		if (sitdev->intsync_conflict) {
+			NL_SET_ERR_MSG(extack,
+				       "More than one PLL holds the INTSYNC source pattern");
+			rc = -EBUSY;
+			break;
+		}
+		/*
+		 * The global enable covers every page, so this PLL would drive
+		 * the line alongside the configuration that set it.
+		 */
+		if (sitdev->intsync_foreign) {
+			NL_SET_ERR_MSG(extack,
+				       "The sync line is enabled by a configuration the driver does not recognise");
+			rc = -EBUSY;
+			break;
+		}
+		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, &owner_kept,
+					     &changed);
+		break;
+	case DPLL_PIN_STATE_DISCONNECTED:
+		if (sitdev->intsync_src != sitdpll->id)
+			break;
+		rc = sit9531x_intsync_disable(sitdev, sitdpll->id,
+					      &owner_kept, &changed);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+
+	if (changed && !rc) {
+		sitdev->intsync_src = state == DPLL_PIN_STATE_CONNECTED ?
+				      sitdpll->id : -1;
+		sitdev->intsync_partial = false;
+	}
+
+	if (changed)
+		detect_rc = sit9531x_intsync_src_detect(sitdev);
+	if (changed && rc) {
+		if (owner_kept ||
+		    (state == DPLL_PIN_STATE_CONNECTED &&
+		     (detect_rc || sitdev->intsync_src == sitdpll->id))) {
+			sitdev->intsync_src = sitdpll->id;
+			sitdev->intsync_partial = true;
+			sitdev->intsync_foreign = false;
+		} else if (detect_rc) {
+			sitdev->intsync_stale = true;
+		}
+	} else if (changed && detect_rc && sitdev->intsync_conflict) {
+		sitdev->intsync_stale = true;
+	}
+	if (detect_rc)
+		dev_warn(sitdev->dev,
+			 "INTSYNC source cache not refreshed: %d\n",
+			 detect_rc);
+
+	/* The core notifies only a request that succeeded */
+	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");
+		/*
+		 * Runs under the core's lock, as __dpll_pin_change_ntf()
+		 * requires.
+		 */
+		dpll_pin = READ_ONCE(dpin->dpll_pin);
+		if (moved && dpll_pin && __dpll_pin_change_ntf(dpll_pin)) {
+			mutex_lock(&sitdev->multiop_lock);
+			sitdev->intsync_ntf_pending = true;
+			mutex_unlock(&sitdev->multiop_lock);
+		}
+	}
+
+	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,
+};
+
+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;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_dpll_intsync_refresh(sitdev, extack);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+	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;
+}
+
+/* INTSYNC is an internal line; only the per-PLL priority table is touched. */
+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)
+{
+	u8 hw_src = sit9531x_input_hw_src(SIT9531X_INTSYNC_PIN_ID);
+	struct sit9531x_dpll *sitdpll = dpll_priv;
+	struct sit9531x_dev *sitdev = sitdpll->dev;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+
+	rc = sit9531x_dpll_intsync_refresh(sitdev, extack);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+	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;
+	int rc;
+
+	mutex_lock(&sitdev->multiop_lock);
+	rc = sit9531x_dpll_intsync_refresh(sitdev, extack);
+	if (rc) {
+		mutex_unlock(&sitdev->multiop_lock);
+		return rc;
+	}
+	sit9531x_dpll_selection_operstate_get(sitdev, sitdpll,
+					      SIT9531X_INTSYNC_PIN_ID,
+					      state);
+	mutex_unlock(&sitdev->multiop_lock);
+
+	return 0;
+}
+
+/*
+ * The destination pin id SIT9531X_INTSYNC_PIN_ID is one past the end of ref[];
+ * do not add ops that index 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,
+};
+
+static const struct dpll_pin_ops sit9531x_dpll_intsync_dst_pin_manual_ops = {
+	.direction_get		= sit9531x_dpll_input_pin_direction_get,
+	.state_on_dpll_get	= sit9531x_dpll_intsync_dst_state_on_dpll_get,
+	.operstate_on_dpll_get	=
+		sit9531x_dpll_intsync_dst_operstate_on_dpll_get,
+};
+
 /* The XO feeds every PLL and is always connected. */
 
 static int
@@ -974,12 +1320,18 @@ 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))
+	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_xo_pin(pin))
 		return &sit9531x_dpll_xo_pin_ops;
 	/* Read once at probe, before any pin is registered. */
 	manual = sit9531x_chan_state_get(sitdev, pin->dpll->id)->manual_input;
+	if (sit9531x_dpll_is_intsync_pin(pin))
+		return manual ? &sit9531x_dpll_intsync_dst_pin_manual_ops :
+				&sit9531x_dpll_intsync_dst_pin_ops;
 	/*
 	 * Set before any pin of the input is published and not written again.
 	 */
diff --git a/drivers/dpll/sit9531x/regs.h b/drivers/dpll/sit9531x/regs.h
index d6c6b4a6a6bb..6913f9f7f0ad 100644
--- a/drivers/dpll/sit9531x/regs.h
+++ b/drivers/dpll/sit9531x/regs.h
@@ -254,6 +254,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)


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH net-next v15 01/13] dpll: omit pin phase attributes the driver has no value for
  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
  0 siblings, 0 replies; 15+ messages in thread
From: Ivan Vecera @ 2026-11-11 15:35 UTC (permalink / raw)
  To: Ali Rouhi, jiri
  Cc: vadim.fedorenko, arkadiusz.kubalewski, kuba, pabeni, robh,
	krzk+dt, conor+dt, cjubran, Oleg.Zadorozhnyi,
	prabhakar.mahadev-lad.rj, devicetree, netdev, linux-kernel

On 11/10/26 21:26, Ali Rouhi wrote:
> From: Oleg Zadorozhnyi<Oleg.Zadorozhnyi@devoxsoftware.com>
> 
> Let phase_offset_get and phase_adjust_get return -ENODATA for a pin the
> driver has no value for, and leave DPLL_A_PIN_PHASE_OFFSET and
> DPLL_A_PIN_PHASE_ADJUST out of the message in that case, as
> dpll_msg_add_ffo() already does for the fractional frequency offset.
> Today any error from either callback fails the whole pin dump.
> 
> A driver that measures the phase offset only on the input its PLL
> tracks, or whose part holds a delay larger than the s32 the adjust
> attribute carries, has no value to report for the pin, and a zero
> cannot be told from a measurement.  A phase adjust request on such a
> pin is passed to the driver without the comparison against the old
> value, so the delay can still be set or cleared.
> 
> Signed-off-by: Oleg Zadorozhnyi<Oleg.Zadorozhnyi@devoxsoftware.com>
> Signed-off-by: Ali Rouhi<arouhi@sitime.com>
> ---
>   drivers/dpll/dpll_netlink.c | 14 ++++++++++----
>   1 file changed, 10 insertions(+), 4 deletions(-)

Reviewed-by: Ivan Vecera <ivecera@redhat.com>


^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-11-11 15:35 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 06/13] dpll: sit9531x: register DPLL devices and pins 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 07/13] dpll: sit9531x: implement input pin state on a DPLL Ali Rouhi
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 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 11/13] dpll: sit9531x: add support to adjust output phase 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

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®